track_event() mechanism for analytics and plugins

* Closes #2240
* Documentation for event plugin hooks, refs #2240
* Include example track_event plugin in docs, refs #2240
* Tests for track_event() and register_events() hooks, refs #2240
* Initial documentation for core events, refs #2240
* Internals documentation for datasette.track_event()
This commit is contained in:
Simon Willison 2024-01-31 15:21:40 -08:00 committed by GitHub
commit bcc4f6bf1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 614 additions and 10 deletions

View file

@ -40,6 +40,8 @@ extensions = [
if not os.environ.get("DISABLE_SPHINX_INLINE_TABS"):
extensions += ["sphinx_inline_tabs"]
autodoc_member_order = "bysource"
extlinks = {
"issue": ("https://github.com/simonw/datasette/issues/%s", "#%s"),
}

14
docs/events.rst Normal file
View file

@ -0,0 +1,14 @@
.. _events:
Events
======
Datasette includes a mechanism for tracking events that occur while the software is running. This is primarily intended to be used by plugins, which can both trigger events and listen for events.
The core Datasette application triggers events when certain things happen. This page describes those events.
Plugins can listen for events using the :ref:`plugin_hook_track_event` plugin hook, which will be called with instances of the following classes (or additional classes registered by other plugins):
.. automodule:: datasette.events
:members:
:exclude-members: Event

View file

@ -63,5 +63,6 @@ Contents
plugin_hooks
testing_plugins
internals
events
contributing
changelog

View file

@ -593,6 +593,26 @@ Using either of these pattern will result in the in-memory database being served
This removes a database that has been previously added. ``name=`` is the unique name of that database.
.. _datasette_track_event:
await .track_event(event)
-------------------------
``event`` - ``Event``
An instance of a subclass of ``datasette.events.Event``.
Plugins can call this to track events, using classes they have previously registered. See :ref:`plugin_event_tracking` for details.
The event will then be passed to all plugins that have registered to receive events using the :ref:`plugin_hook_track_event` hook.
Example usage, assuming the plugin has previously registered the ``BanUserEvent`` class:
.. code-block:: python
await datasette.track_event(
BanUserEvent(user={"id": 1, "username": "cleverbot"})
)
.. _datasette_sign:
.sign(value, namespace="default")

View file

@ -1759,3 +1759,103 @@ top_canned_query(datasette, request, database, query_name)
The name of the canned query.
Returns HTML to be displayed at the top of the canned query page.
.. _plugin_event_tracking:
Event tracking
--------------
Datasette includes an internal mechanism for tracking analytical events. This can be used for analytics, but can also be used by plugins that want to listen out for when key events occur (such as a table being created) and take action in response.
Plugins can register to receive events using the ``track_event`` plugin hook.
They can also define their own events for other plugins to receive using the ``register_events`` plugin hook, combined with calls to the ``datasette.track_event(...)`` internal method.
.. _plugin_hook_track_event:
track_event(datasette, event)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.
``event`` - ``Event``
Information about the event, represented as an instance of a subclass of the ``Event`` base class.
This hook will be called any time an event is tracked by code that calls the :ref:`datasette.track_event(...) <datasette_track_event>` internal method.
The ``event`` object will always have the following properties:
- ``name``: a string representing the name of the event, for example ``logout`` or ``create-table``.
- ``actor``: a dictionary representing the actor that triggered the event, or ``None`` if the event was not triggered by an actor.
- ``created``: a ``datatime.datetime`` object in the ``timezone.utc`` timezone representing the time the event object was created.
Other properties on the event will be available depending on the type of event. You can also access those as a dictionary using ``event.properties()``.
The events fired by Datasette core are :ref:`documented here <events>`.
This example plugin logs details of all events to standard error:
.. code-block:: python
from datasette import hookimpl
import json
import sys
@hookimpl
def track_event(event):
name = event.name
actor = event.actor
properties = event.properties()
msg = json.dumps(
{
"name": name,
"actor": actor,
"properties": properties,
}
)
print(msg, file=sys.stderr, flush=True)
.. _plugin_hook_register_events:
register_events(datasette)
~~~~~~~~~~~~~~~~~~~~~~~~~~
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.
This hook should return a list of ``Event`` subclasses that represent custom events that the plugin might send to the ``datasette.track_event()`` method.
This example registers event subclasses for ``ban-user`` and ``unban-user`` events:
.. code-block:: python
from dataclasses import dataclass
from datasette import hookimpl, Event
@dataclass
class BanUserEvent(Event):
name = "ban-user"
user: dict
@dataclass
class UnbanUserEvent(Event):
name = "unban-user"
user: dict
@hookimpl
def register_events():
return [BanUserEvent, UnbanUserEvent]
The plugin can then call ``datasette.track_event(...)`` to send a ``ban-user`` event:
.. code-block:: python
await datasette.track_event(
BanUserEvent(user={"id": 1, "username": "cleverbot"})
)

View file

@ -228,6 +228,15 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
"skip_csrf"
]
},
{
"name": "datasette.events",
"static": false,
"templates": false,
"version": null,
"hooks": [
"register_events"
]
},
{
"name": "datasette.facets",
"static": false,