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
Plugin Attachments #2263
Plugin Attachments #2263
Conversation
I like it! From a user experience point of view, wouldn't it be nicer if we used something like https://github.com/GitSquared/node-geolite2-redist to automatically download the database without the need for the user to go off and download it themselves? This way it could be really turn key. I think the implementation ends up being the same (with attachments), it'll just happen automatically |
IANAL, but I think we'll be in violation of maxmind's terms if we bundle their library in any form. You can't even download the free geolite library anymore without logging in and signing their terms. I would like to include an upgrade service though. You'd enter a few keys on the plugin config page and a scheduled job would refetch the new database once per month. Similar to this cronjob. |
…ch up for visibility
This is now ready for a look! What changed:
After this lands in master, the API for plugins will have changed just a little bit. All old plugins still work after these changes, yet some new features will not work with PostHog To use the posthog-maxmind-plugin, install it with its NPM url: https://www.npmjs.com/package/posthog-maxmind-plugin We can add this URL to the global repository once this PR is merged. Relevant repository PR here: PostHog/plugin-repository#1 |
Fixed mypy, should be good to review now. Tagging @yakkomajuri here for info about the docs and API changes. About how to make plugins, this is the old format. There is a
I already updated the This is the new format and a typescript example: https://github.com/PostHog/posthog-maxmind-plugin The old format still works, the only thing that really changed is the second parameter (
It used to contain the Passing all of this in the function params makes the plugin's code simpler to write and test, as we no longer rely on magic globals. There are still three magic globals that can be used:
There's also a project https://github.com/PostHog/posthog-plugins - which contains all the shared TypeScript types. Regarding the architecture of posthog-plugin-server, what now happens is that each "plugin_config" in the database gets a new and isolated VM. So if you have 1 plugin globally active, it's just 1 VM. If you have 1 plugin globally installed, yet enabled individually under 3 teams, that's 3 VMs. Every plugin installed and enabled under every individual project is also another VM. This might have some memory implications. E.g. the maxmind plugin loads a 60MB database (and stores it somehow, duplicating the data), thus enabling it globally on I hope all of this makes some sense and can be used as input in the docs. I'd also suggest releasing |
To get this to run on Heroku I had to upgrade the review app's
I'd guess the process of converting the file into a While testing I also had to upgrade the Redis addon to the $15/mo plan as this stuff came up:
The limit for the free redis plan is 20 connections. We seem to be going over that, but just slightly. The average never goes over 20. Reducing our reliance on redis for task queuing or the pubsub reloads is a way to bring this connection limit down. Pooling the connection between threads and processes in a worker could be another thing to look at. However when all worked (using a deployment that costs $175/mo), it was great to see real IP information on an event in a Heroku app. All it took was a few clicks in the interface plus one large upload and I had IP information on all events. Regarding performance, with the db uploaded, it takes
|
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.
Took a look from a code-level standpoint.
@@ -76,19 +77,37 @@ export const pluginsLogic = kea< | |||
|
|||
const { __enabled: enabled, ...config } = pluginConfigChanges | |||
|
|||
const configSchema = getConfigSchemaObject(editingPlugin.config_schema) | |||
|
|||
const formData = new FormData() |
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.
Nitpicking:
I usually get anxious when reading code this long and this nested - it is hard to test and it is hard to understand.
Suggestion: extract frontend/src/scenes/plugins/formBuilder.ts
with a buildPluginForm
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.
I'm not really sure what form
you're suggesting I build here nor what should be in that file. Can you elaborate?
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.
https://github.com/PostHog/posthog/pull/2263/files#diff-574d8f890891ccdbbc51c20732343013f38ec392cf33b1fd262bc6b5217231f8R82-R97 Extract method for these lines. (sorry for the late response)
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.
Extracted into getPluginConfigFormData
in scenes/plugins/utils.ts
class Meta: | ||
model = PluginConfig | ||
fields = ["id", "plugin", "enabled", "order", "config", "error"] | ||
read_only_fields = ["id"] | ||
|
||
def get_config(self, plugin_config: PluginConfig): |
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.
No tests?
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 now tested via the API tests, especially test_plugin_config_attachment
. All other API methods are also tested.
plugin_config = super().create(validated_data) | ||
self._update_plugin_attachments(plugin_config) | ||
reload_plugins_on_workers() | ||
return plugin_config | ||
|
||
def update(self, plugin_config: PluginConfig, validated_data: Dict, *args: Any, **kwargs: Any) -> PluginConfig: # type: ignore |
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.
So this has some interesting implicit arguments - it relies on being called to update inside a request. Coupling model with view that way is a bad idea as it's hard to detangle the two later on and it becomes impossible to test anything.
I think this should be simplified. E.g. move the file handling into the view files (or helper functions) and in the model handle byteIO/file objects.
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.
As a django n00b
I'm not sure what to do here.
So this has some interesting implicit arguments - it relies on being called to update inside a request.
I have seen code like request = self.context["request"]
in a lot of serializers, so I'm not sure if this is something that we should untangle... nor how:
Keep in mind, that I need to make sure a lot of different pieces work together here: 1) rc-form
in antd and its rc-upload
file upload component, both for adding new files and for displaying old files (custom metadata format) 2) a unified form for config vars that live in two models (plugin config and plugin attachments), 3) file uploads with FormData while also sending non-file fields, 4) JSON serialization for config
vars, 5) receiving the file in django and storing it in postgresql with custom fields for metadata.
If you have some suggestions on how to clean this up into a more djangoesque style, while retaining all those assumptions, please let me know. I'm just happy I got anything to work :).
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.
Not too sure either how to do this without introducing an extra mapping layer to be honest.
A start would be exposing the request as argument on _fix_formdata_config_json
, _update_plugin_attachments
instead of it being read from context - making the thing a bit more pure and closer to testable. But that does relatively little on it's own.
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 sure what would this solve and why do we need to make these _internal
methods of the serializer pure and testable. Where would I test them?
From what I can reason, the entire point of OOP is that you have access to instance variables when you need them.
I have added a bunch of tests to the plugin API (file uploads still to come) that indirectly test these methods, so... 🤔 🤷
…st fields as read-only
plugin_attachment.save() | ||
else: | ||
plugin_attachment.delete() | ||
except ObjectDoesNotExist: |
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 think we can get rid of this!
plugin_attachment, created = PluginAttachment.objects.get_or_create(team=plugin_config.team, plugin_config=plugin_config, key=key)
From https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create
I believe it will also perform a django upsert under the hood.
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.
The thing is I don't always need to create the object. Sometimes I also want to delete it... and a "create" directly followed by "delete" is IMO worse practice than catching a ObjectDoesNotExist
@macobo did most of the changes, not sure about the OOP refactor bit though, left an inline comment there. |
Changes
Checklist