mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 14:25:37 +00:00
Implement posthog product analytics
This commit is contained in:
parent
80760f85d5
commit
b743afd8d4
16 changed files with 159 additions and 6 deletions
backend
requirements
src/baserow
tests/baserow
changelog/entries/unreleased/feature
docker-compose.local-build.ymldocker-compose.no-caddy.ymldocker-compose.ymldocs/installation
web-frontend/modules/database/components/airtable
|
@ -61,4 +61,5 @@ Brotli==1.0.9
|
|||
loguru==0.6.0
|
||||
django-cachalot==2.5.3
|
||||
celery-singleton==0.3.1
|
||||
posthog==3.0.1
|
||||
https://github.com/fellowapp/prosemirror-py/archive/refs/tags/v0.3.5.zip
|
||||
|
|
|
@ -37,7 +37,9 @@ azure-core==1.26.4
|
|||
azure-storage-blob==12.16.0
|
||||
# via django-storages
|
||||
backoff==2.2.1
|
||||
# via opentelemetry-exporter-otlp-proto-http
|
||||
# via
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
# posthog
|
||||
billiard==3.6.4.0
|
||||
# via celery
|
||||
boto3==1.26.103
|
||||
|
@ -211,6 +213,8 @@ kombu==5.2.4
|
|||
# via celery
|
||||
loguru==0.6.0
|
||||
# via -r base.in
|
||||
monotonic==1.6
|
||||
# via posthog
|
||||
msgpack==1.0.4
|
||||
# via channels-redis
|
||||
ndg-httpsclient==0.5.1
|
||||
|
@ -315,6 +319,8 @@ opentelemetry-util-http==0.38b0
|
|||
# opentelemetry-instrumentation-wsgi
|
||||
pillow==9.4.0
|
||||
# via -r base.in
|
||||
posthog==3.0.1
|
||||
# via -r base.in
|
||||
prompt-toolkit==3.0.31
|
||||
# via click-repl
|
||||
prosemirror @ https://github.com/fellowapp/prosemirror-py/archive/refs/tags/v0.3.5.zip
|
||||
|
@ -360,6 +366,7 @@ python-dateutil==2.8.2
|
|||
# botocore
|
||||
# celery-redbeat
|
||||
# faker
|
||||
# posthog
|
||||
# pysaml2
|
||||
# python-crontab
|
||||
python-dotenv==0.21.0
|
||||
|
@ -393,6 +400,7 @@ requests==2.31.0
|
|||
# google-api-core
|
||||
# google-cloud-storage
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
# posthog
|
||||
# pysaml2
|
||||
# requests-oauthlib
|
||||
requests-oauthlib==1.3.1
|
||||
|
@ -413,6 +421,7 @@ six==1.16.0
|
|||
# click-repl
|
||||
# google-auth
|
||||
# isodate
|
||||
# posthog
|
||||
# python-dateutil
|
||||
# service-identity
|
||||
sniffio==1.3.0
|
||||
|
|
|
@ -23,7 +23,7 @@ pytest-html==3.2.0
|
|||
coverage==7.2.2
|
||||
pytest-split==0.8.0
|
||||
bandit==1.7.5
|
||||
pip-tools==6.12.3
|
||||
pip-tools==6.13.0
|
||||
autopep8==2.0.2
|
||||
pytest-unordered==0.5.2
|
||||
debugpy==1.6.6
|
||||
|
|
|
@ -183,7 +183,7 @@ pexpect==4.8.0
|
|||
# via ipython
|
||||
pickleshare==0.7.5
|
||||
# via ipython
|
||||
pip-tools==6.12.3
|
||||
pip-tools==6.13.0
|
||||
# via -r dev.in
|
||||
platformdirs==2.5.2
|
||||
# via black
|
||||
|
|
|
@ -12,6 +12,7 @@ from urllib.parse import urljoin, urlparse
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
import dj_database_url
|
||||
import posthog
|
||||
from corsheaders.defaults import default_headers
|
||||
|
||||
from baserow.cachalot_patch import patch_cachalot_for_baserow
|
||||
|
@ -1108,6 +1109,15 @@ USE_PG_FULLTEXT_SEARCH = str_to_bool(
|
|||
PG_SEARCH_CONFIG = os.getenv("BASEROW_PG_SEARCH_CONFIG", "simple")
|
||||
AUTO_VACUUM_AFTER_SEARCH_UPDATE = str_to_bool(os.getenv("BASEROW_AUTO_VACUUM", "true"))
|
||||
|
||||
POSTHOG_PROJECT_API_KEY = os.getenv("POSTHOG_PROJECT_API_KEY", "")
|
||||
POSTHOG_HOST = os.getenv("POSTHOG_HOST", "")
|
||||
POSTHOG_ENABLED = POSTHOG_PROJECT_API_KEY and POSTHOG_HOST
|
||||
if POSTHOG_ENABLED:
|
||||
posthog.project_api_key = POSTHOG_PROJECT_API_KEY
|
||||
posthog.host = POSTHOG_HOST
|
||||
else:
|
||||
posthog.disabled = True
|
||||
|
||||
# Indicates whether we are running the tests or not. Set to True in the test.py settings
|
||||
# file used by pytest.ini
|
||||
TESTS = False
|
||||
|
|
|
@ -11,7 +11,7 @@ def extract_share_id_from_url(public_base_url: str) -> str:
|
|||
:return: The extracted share id.
|
||||
"""
|
||||
|
||||
result = re.search(r"https:\/\/airtable.com\/shr(.*)$", public_base_url)
|
||||
result = re.search(r"https:\/\/airtable.com\/(shr|app)(.*)$", public_base_url)
|
||||
|
||||
if not result:
|
||||
raise ValueError(
|
||||
|
@ -19,4 +19,4 @@ def extract_share_id_from_url(public_base_url: str) -> str:
|
|||
f"https://airtable.com/shrxxxxxxxxxxxxxx)"
|
||||
)
|
||||
|
||||
return f"shr{result.group(1)}"
|
||||
return f"{result.group(1)}{result.group(2)}"
|
||||
|
|
|
@ -286,6 +286,9 @@ class CoreConfig(AppConfig):
|
|||
WorkspaceInvitationRejectedNotificationType()
|
||||
)
|
||||
|
||||
# Must import the Posthog signal, otherwise it won't work.
|
||||
import baserow.core.posthog # noqa: F403, F401
|
||||
|
||||
self._setup_health_checks()
|
||||
|
||||
# Clear the key after migration so we will trigger a new template sync.
|
||||
|
|
71
backend/src/baserow/core/posthog.py
Normal file
71
backend/src/baserow/core/posthog.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from copy import deepcopy
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.dispatch import receiver
|
||||
|
||||
import posthog
|
||||
|
||||
from baserow.core.action.signals import ActionCommandType, action_done
|
||||
from baserow.core.models import Workspace
|
||||
|
||||
|
||||
def capture_event(
|
||||
user: AbstractUser,
|
||||
event: str,
|
||||
properties: dict,
|
||||
session: Optional[str] = None,
|
||||
workspace: Optional[Workspace] = None,
|
||||
):
|
||||
"""
|
||||
Captures a Posthog event in a consistent property format.
|
||||
|
||||
:param user: The user that performed the event.
|
||||
:param event: Unique name identifying the event.
|
||||
:param properties: A dictionary containing the properties that must be added.
|
||||
Note that the `user_email`, `user_session`, `workspace_id`,
|
||||
and `workspace_name` will be overwritten.
|
||||
:param session: A unique session id that identifies the user throughout their
|
||||
session.
|
||||
:param workspace: Optionally the workspace related to the event.
|
||||
"""
|
||||
|
||||
if not settings.POSTHOG_ENABLED:
|
||||
return
|
||||
|
||||
properties = deepcopy(properties)
|
||||
properties["user_email"] = user.email
|
||||
|
||||
if session is not None:
|
||||
properties["user_session"] = session
|
||||
|
||||
if workspace is not None:
|
||||
properties["workspace_id"] = workspace.id
|
||||
properties["workspace_name"] = workspace.name
|
||||
|
||||
posthog.capture(
|
||||
distinct_id=user.id,
|
||||
event=event,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
|
||||
@receiver(action_done)
|
||||
def capture_event_action_done(
|
||||
sender,
|
||||
user,
|
||||
action_type,
|
||||
action_params,
|
||||
action_timestamp,
|
||||
action_command_type,
|
||||
workspace,
|
||||
session,
|
||||
**kwargs,
|
||||
):
|
||||
# Only capture do commands for now because the undo might make it more difficult
|
||||
# to do analytics on the data.
|
||||
if action_command_type == ActionCommandType.DO:
|
||||
capture_event(
|
||||
user, action_type.type, action_params, workspace=workspace, session=session
|
||||
)
|
|
@ -11,6 +11,10 @@ def test_extract_share_id_from_url():
|
|||
extract_share_id_from_url("https://airtable.com/shrxxxxxxxxxxxxxx")
|
||||
== "shrxxxxxxxxxxxxxx"
|
||||
)
|
||||
assert (
|
||||
extract_share_id_from_url("https://airtable.com/appxxxxxxxxxxxxxx")
|
||||
== "appxxxxxxxxxxxxxx"
|
||||
)
|
||||
assert (
|
||||
extract_share_id_from_url("https://airtable.com/shrXxmp0WmqsTkFWTzv")
|
||||
== "shrXxmp0WmqsTkFWTzv"
|
||||
|
|
35
backend/tests/baserow/core/test_posthog.py
Normal file
35
backend/tests/baserow/core/test_posthog.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow.core.posthog import capture_event
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(POSTHOG_ENABLED=False)
|
||||
@patch("baserow.core.posthog.posthog")
|
||||
def test_not_capture_event_if_not_enabled(mock_posthog, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
capture_event(user, "test", {})
|
||||
mock_posthog.capture.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(POSTHOG_ENABLED=True)
|
||||
@patch("baserow.core.posthog.posthog")
|
||||
def test_capture_event_if_enabled(mock_posthog, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
workspace = data_fixture.create_workspace()
|
||||
capture_event(user, "test", {}, session="session", workspace=workspace)
|
||||
mock_posthog.capture.assert_called_once_with(
|
||||
distinct_id=user.id,
|
||||
event="test",
|
||||
properties={
|
||||
"user_email": user.email,
|
||||
"user_session": "session",
|
||||
"workspace_id": workspace.id,
|
||||
"workspace_name": workspace.name,
|
||||
},
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Implemented optional Posthog product analytics.",
|
||||
"issue_number": null,
|
||||
"bullet_points": [],
|
||||
"created_at": "2023-07-21"
|
||||
}
|
|
@ -78,6 +78,8 @@ x-backend-variables: &backend-variables
|
|||
BASEROW_DEPLOYMENT_ENV:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||
OTEL_RESOURCE_ATTRIBUTES:
|
||||
POSTHOG_PROJECT_API_KEY:
|
||||
POSTHOG_HOST:
|
||||
|
||||
PRIVATE_BACKEND_URL: http://backend:8000
|
||||
PUBLIC_BACKEND_URL:
|
||||
|
|
|
@ -98,6 +98,8 @@ x-backend-variables: &backend-variables
|
|||
BASEROW_DEPLOYMENT_ENV:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||
OTEL_RESOURCE_ATTRIBUTES:
|
||||
POSTHOG_PROJECT_API_KEY:
|
||||
POSTHOG_HOST:
|
||||
|
||||
PRIVATE_BACKEND_URL: http://backend:8000
|
||||
BASEROW_PUBLIC_URL:
|
||||
|
|
|
@ -94,6 +94,8 @@ x-backend-variables: &backend-variables
|
|||
BASEROW_DEPLOYMENT_ENV:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||
OTEL_RESOURCE_ATTRIBUTES:
|
||||
POSTHOG_PROJECT_API_KEY:
|
||||
POSTHOG_HOST:
|
||||
|
||||
PRIVATE_BACKEND_URL: http://backend:8000
|
||||
PUBLIC_BACKEND_URL:
|
||||
|
|
|
@ -262,3 +262,10 @@ domain than your Baserow, you need to make sure CORS is configured correctly.
|
|||
| BASEROW\_PLUGIN\_URLS | A comma separated list of plugin urls to install on startup. | |
|
||||
| BASEROW\_DISABLE\_PLUGIN\_INSTALL\_ON\_STARTUP | When set to any non-empty values no automatic startup check and/or install of plugins will be run. Disables the above two env variables. |
|
||||
| BASEROW\_PLUGIN\_DIR | **INTERNAL** Sets the folder where the Baserow plugin scripts look for plugins. | In the all-in-one image `/baserow/data/plugins`, otherwise `/baserow/plugins` |
|
||||
|
||||
### Posthog configuration
|
||||
|
||||
| Name | Description | Defaults |
|
||||
|----------------------------|-----------------------------------------------------------------|-------------------------------------------------------------------------------|
|
||||
| POSTHOG\_PROJECT\_API\_KEY | Set this to your Posthog project API key for product analytics. | |
|
||||
| POSTHOG\_HOST | Set this to your Posthog host for product analytics. | |
|
||||
|
|
|
@ -149,7 +149,7 @@ export default {
|
|||
validations: {
|
||||
airtableUrl: {
|
||||
valid(value) {
|
||||
const regex = /https:\/\/airtable.com\/shr(.*)$/g
|
||||
const regex = /https:\/\/airtable.com\/[shr|app](.*)$/g
|
||||
return !!value.match(regex)
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue