-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(apps): inject code to posthog-js #12003
Conversation
posthog/api/decide.py
Outdated
@@ -159,5 +159,19 @@ def get_decide(request: HttpRequest): | |||
on_permitted_recording_domain(team, request) or not team.recording_domains | |||
): | |||
response["sessionRecording"] = {"endpoint": "/s/"} | |||
|
|||
source_files = PluginSourceFile.objects.filter( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: decide endpoint is really sensitive to lag - e.g. 200 extra milliseconds to it can mean thousands of events dropped.
I believe we should make sure we have the appropriate indexes and heavily cache this as a result - both in-memory and e.g. via redis? The joins seem scary wrt performance here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to work but it's complex enough to warrant some questions :)
|
||
export function PluginSourceTabs({ logic }: { logic: BuiltLogic<pluginSourceLogicType> }): JSX.Element { | ||
const { setCurrentFile } = useActions(logic) | ||
const { setCurrentFile, addFilePrompt } = useActions(logic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- How can I remove a file once it's added? This bit is confusing.
- The file addition prompt asks for an arbitrary TypeScript file name, but it makes zero sense to allow adding "foobar.tsx" as things stand. The only file that makes sense is
web.ts
… so why not just expose that by default, same asfrontend.tsx
? - Bug here: I can also "add" existing files, which just erases the content of that existing file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I added a "remove" button. It's not the greatest 🍞 in the world, but does what it says.
-
Initially I wanted to just hide
web.ts
if not present, and show the other 2-3 files (plugin.json
,index.ts
, plusfrontend.ts
if the flag is enabled) at all times. I now changed it to only show the files that are actually present. I think this is cleaner, as I had the worry I might accidentally add a blank "index.ts" with something before. As for adding arbitrary files, I think we should support that sooner or later. However ok, limited it to only the files we support for now. -
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of a regression in terms of the experience:
At the very least this should be a dropdown with available files instead of a freeform prompt()
. The idea with arbitrary files is very far out at the moment, so optimizing for it is very premature.
Then also this placement of the "Delete" button doesn't seem super intuitive. The button appearing and disappearing moves the "Add new file" button around a lot, and there's a difference in scope between "Add some new file" and "Delete this open file" that doesn't sit well with me for some reason…
I'd still argue for keeping this simpler and using the same mechanism as with frontend.tsx
– users with the feature flag on will see web.ts
, others won't. This should still scale well here.
code: true, | ||
babelrc: false, | ||
configFile: false, | ||
filename: 'web.ts', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of the web.ts
name… but also I don't have a better idea right now.
Both this and frontend.tsx
technically touch the frontend, though of different things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inject.ts
?
@@ -71,6 +71,35 @@ export async function loadPlugin(hub: Hub, pluginConfig: PluginConfig): Promise< | |||
} | |||
} | |||
|
|||
// transpile "web" app if needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block seems awfully similar to the // transpile "frontend" app if needed'
one. I'm not super strict about DRY but this is so complex that I'd be more at peace if the two blocks were refactored into one function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm.. I made it just a bit more abstract, though I would be vary of prematurely optimising this too much.
indexes = [ | ||
models.Index(fields=["web_token"]), | ||
models.Index(fields=["enabled"]), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we know our queries will use id
, web_token
AND enabled
, Postgres will be able to optimize much better if there is one composite index on the three columns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disclaimer: This is the general theory, the practice needs to be tested with EXPLAIN ANALYZE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Luckily postgres indexes are one of the few things we can add and remove as needed :). I went for simple indices since I didn't know what shape the queries will take, and didn't want to overanalyse before this was a problem. Many simple indices also take up less space than multiple combined ones, so I considered this a fair tradeoff 🤷
Already the queries for /decide and /web_js/id/token/ are quite different in what indices they use. Currently: id
& web_token
& enabled
... vs ... team_id
& enabled
.
@@ -206,6 +213,8 @@ class PluginConfig(models.Model): | |||
# - e.g: "undefined is not a function on index.js line 23" | |||
# - error = { message: "Exception in processEvent()", time: "iso-string", ...meta } | |||
error: models.JSONField = models.JSONField(default=None, null=True) | |||
# Used to access web.ts from a public URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably missed something but I don't really get what's this web token for. Can you expand on this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Web apps will be injected from /web_js/:id/:token/
. I don't want somebody to id++
in a loop and download all possible apps.
Sidenote: this definitely should go behind some CDN/cache, but in v0.2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me the primary distinction between token and ID is:
- a token serves authentication purposes and can change throughout its entity's life
- an ID serves unique identification purposes and cannot change
I don't see how authentication would work for a posthog-js
feature, so currently I'd say the only real job of this code is identifying the app to fetch code for. And if the only reason for not using id
is preventing enumeration, could a plugin UUID (UUIDT) – which would be more general than web_token
– work here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points! Generally agree, though we might want cache invalidation as well. Tokens due to their refreshability serve well for this purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Twixes I addressed most of the feedback. Can you give it another look and mark out what's still blocking, and what can be in v0.0.2?
|
||
export function PluginSourceTabs({ logic }: { logic: BuiltLogic<pluginSourceLogicType> }): JSX.Element { | ||
const { setCurrentFile } = useActions(logic) | ||
const { setCurrentFile, addFilePrompt } = useActions(logic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of a regression in terms of the experience:
At the very least this should be a dropdown with available files instead of a freeform prompt()
. The idea with arbitrary files is very far out at the moment, so optimizing for it is very premature.
Then also this placement of the "Delete" button doesn't seem super intuitive. The button appearing and disappearing moves the "Add new file" button around a lot, and there's a difference in scope between "Add some new file" and "Delete this open file" that doesn't sit well with me for some reason…
I'd still argue for keeping this simpler and using the same mechanism as with frontend.tsx
– users with the feature flag on will see web.ts
, others won't. This should still scale well here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
Problem
We are discussing giving apps the capability to inject code onto websites via posthog-js
Changes
If you add a file called "web.ts" to your plugin, we will transpile it and inject it on your website via the
/decide
endpoint.Here's an example app "Pineapple Mode":
This is a simple proof of concept. Other app ideas this enables are:
Security concerns
Injecting arbitrary code on someone else's website sounds bad. However there are some things that prevent people using this as a blatant security hole:
web.ts
file to inject any random code.On self hosted, where plugins are enabled for everyone, this is more concerning. To get around that:
web.ts
file is not included by default if you make a new plugin, you need to explicitly add it.Additional notes
Still missing or to consider developing:
/decide
, but imported separately)The corresponding PR on posthog-js: PostHog/posthog-js#453
Should it be
web.ts
orinject.ts
or ... ?How did you test this code?
In the browser, see screencast. Tests can and will be added if we take this further.