mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +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
|
loguru==0.6.0
|
||||||
django-cachalot==2.5.3
|
django-cachalot==2.5.3
|
||||||
celery-singleton==0.3.1
|
celery-singleton==0.3.1
|
||||||
|
posthog==3.0.1
|
||||||
https://github.com/fellowapp/prosemirror-py/archive/refs/tags/v0.3.5.zip
|
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
|
azure-storage-blob==12.16.0
|
||||||
# via django-storages
|
# via django-storages
|
||||||
backoff==2.2.1
|
backoff==2.2.1
|
||||||
# via opentelemetry-exporter-otlp-proto-http
|
# via
|
||||||
|
# opentelemetry-exporter-otlp-proto-http
|
||||||
|
# posthog
|
||||||
billiard==3.6.4.0
|
billiard==3.6.4.0
|
||||||
# via celery
|
# via celery
|
||||||
boto3==1.26.103
|
boto3==1.26.103
|
||||||
|
@ -211,6 +213,8 @@ kombu==5.2.4
|
||||||
# via celery
|
# via celery
|
||||||
loguru==0.6.0
|
loguru==0.6.0
|
||||||
# via -r base.in
|
# via -r base.in
|
||||||
|
monotonic==1.6
|
||||||
|
# via posthog
|
||||||
msgpack==1.0.4
|
msgpack==1.0.4
|
||||||
# via channels-redis
|
# via channels-redis
|
||||||
ndg-httpsclient==0.5.1
|
ndg-httpsclient==0.5.1
|
||||||
|
@ -315,6 +319,8 @@ opentelemetry-util-http==0.38b0
|
||||||
# opentelemetry-instrumentation-wsgi
|
# opentelemetry-instrumentation-wsgi
|
||||||
pillow==9.4.0
|
pillow==9.4.0
|
||||||
# via -r base.in
|
# via -r base.in
|
||||||
|
posthog==3.0.1
|
||||||
|
# via -r base.in
|
||||||
prompt-toolkit==3.0.31
|
prompt-toolkit==3.0.31
|
||||||
# via click-repl
|
# via click-repl
|
||||||
prosemirror @ https://github.com/fellowapp/prosemirror-py/archive/refs/tags/v0.3.5.zip
|
prosemirror @ https://github.com/fellowapp/prosemirror-py/archive/refs/tags/v0.3.5.zip
|
||||||
|
@ -360,6 +366,7 @@ python-dateutil==2.8.2
|
||||||
# botocore
|
# botocore
|
||||||
# celery-redbeat
|
# celery-redbeat
|
||||||
# faker
|
# faker
|
||||||
|
# posthog
|
||||||
# pysaml2
|
# pysaml2
|
||||||
# python-crontab
|
# python-crontab
|
||||||
python-dotenv==0.21.0
|
python-dotenv==0.21.0
|
||||||
|
@ -393,6 +400,7 @@ requests==2.31.0
|
||||||
# google-api-core
|
# google-api-core
|
||||||
# google-cloud-storage
|
# google-cloud-storage
|
||||||
# opentelemetry-exporter-otlp-proto-http
|
# opentelemetry-exporter-otlp-proto-http
|
||||||
|
# posthog
|
||||||
# pysaml2
|
# pysaml2
|
||||||
# requests-oauthlib
|
# requests-oauthlib
|
||||||
requests-oauthlib==1.3.1
|
requests-oauthlib==1.3.1
|
||||||
|
@ -413,6 +421,7 @@ six==1.16.0
|
||||||
# click-repl
|
# click-repl
|
||||||
# google-auth
|
# google-auth
|
||||||
# isodate
|
# isodate
|
||||||
|
# posthog
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# service-identity
|
# service-identity
|
||||||
sniffio==1.3.0
|
sniffio==1.3.0
|
||||||
|
|
|
@ -23,7 +23,7 @@ pytest-html==3.2.0
|
||||||
coverage==7.2.2
|
coverage==7.2.2
|
||||||
pytest-split==0.8.0
|
pytest-split==0.8.0
|
||||||
bandit==1.7.5
|
bandit==1.7.5
|
||||||
pip-tools==6.12.3
|
pip-tools==6.13.0
|
||||||
autopep8==2.0.2
|
autopep8==2.0.2
|
||||||
pytest-unordered==0.5.2
|
pytest-unordered==0.5.2
|
||||||
debugpy==1.6.6
|
debugpy==1.6.6
|
||||||
|
|
|
@ -183,7 +183,7 @@ pexpect==4.8.0
|
||||||
# via ipython
|
# via ipython
|
||||||
pickleshare==0.7.5
|
pickleshare==0.7.5
|
||||||
# via ipython
|
# via ipython
|
||||||
pip-tools==6.12.3
|
pip-tools==6.13.0
|
||||||
# via -r dev.in
|
# via -r dev.in
|
||||||
platformdirs==2.5.2
|
platformdirs==2.5.2
|
||||||
# via black
|
# via black
|
||||||
|
|
|
@ -12,6 +12,7 @@ from urllib.parse import urljoin, urlparse
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
|
import posthog
|
||||||
from corsheaders.defaults import default_headers
|
from corsheaders.defaults import default_headers
|
||||||
|
|
||||||
from baserow.cachalot_patch import patch_cachalot_for_baserow
|
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")
|
PG_SEARCH_CONFIG = os.getenv("BASEROW_PG_SEARCH_CONFIG", "simple")
|
||||||
AUTO_VACUUM_AFTER_SEARCH_UPDATE = str_to_bool(os.getenv("BASEROW_AUTO_VACUUM", "true"))
|
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
|
# Indicates whether we are running the tests or not. Set to True in the test.py settings
|
||||||
# file used by pytest.ini
|
# file used by pytest.ini
|
||||||
TESTS = False
|
TESTS = False
|
||||||
|
|
|
@ -11,7 +11,7 @@ def extract_share_id_from_url(public_base_url: str) -> str:
|
||||||
:return: The extracted share id.
|
: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:
|
if not result:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -19,4 +19,4 @@ def extract_share_id_from_url(public_base_url: str) -> str:
|
||||||
f"https://airtable.com/shrxxxxxxxxxxxxxx)"
|
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()
|
WorkspaceInvitationRejectedNotificationType()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Must import the Posthog signal, otherwise it won't work.
|
||||||
|
import baserow.core.posthog # noqa: F403, F401
|
||||||
|
|
||||||
self._setup_health_checks()
|
self._setup_health_checks()
|
||||||
|
|
||||||
# Clear the key after migration so we will trigger a new template sync.
|
# 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")
|
extract_share_id_from_url("https://airtable.com/shrxxxxxxxxxxxxxx")
|
||||||
== "shrxxxxxxxxxxxxxx"
|
== "shrxxxxxxxxxxxxxx"
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
extract_share_id_from_url("https://airtable.com/appxxxxxxxxxxxxxx")
|
||||||
|
== "appxxxxxxxxxxxxxx"
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
extract_share_id_from_url("https://airtable.com/shrXxmp0WmqsTkFWTzv")
|
extract_share_id_from_url("https://airtable.com/shrXxmp0WmqsTkFWTzv")
|
||||||
== "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:
|
BASEROW_DEPLOYMENT_ENV:
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||||
OTEL_RESOURCE_ATTRIBUTES:
|
OTEL_RESOURCE_ATTRIBUTES:
|
||||||
|
POSTHOG_PROJECT_API_KEY:
|
||||||
|
POSTHOG_HOST:
|
||||||
|
|
||||||
PRIVATE_BACKEND_URL: http://backend:8000
|
PRIVATE_BACKEND_URL: http://backend:8000
|
||||||
PUBLIC_BACKEND_URL:
|
PUBLIC_BACKEND_URL:
|
||||||
|
|
|
@ -98,6 +98,8 @@ x-backend-variables: &backend-variables
|
||||||
BASEROW_DEPLOYMENT_ENV:
|
BASEROW_DEPLOYMENT_ENV:
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||||
OTEL_RESOURCE_ATTRIBUTES:
|
OTEL_RESOURCE_ATTRIBUTES:
|
||||||
|
POSTHOG_PROJECT_API_KEY:
|
||||||
|
POSTHOG_HOST:
|
||||||
|
|
||||||
PRIVATE_BACKEND_URL: http://backend:8000
|
PRIVATE_BACKEND_URL: http://backend:8000
|
||||||
BASEROW_PUBLIC_URL:
|
BASEROW_PUBLIC_URL:
|
||||||
|
|
|
@ -94,6 +94,8 @@ x-backend-variables: &backend-variables
|
||||||
BASEROW_DEPLOYMENT_ENV:
|
BASEROW_DEPLOYMENT_ENV:
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||||
OTEL_RESOURCE_ATTRIBUTES:
|
OTEL_RESOURCE_ATTRIBUTES:
|
||||||
|
POSTHOG_PROJECT_API_KEY:
|
||||||
|
POSTHOG_HOST:
|
||||||
|
|
||||||
PRIVATE_BACKEND_URL: http://backend:8000
|
PRIVATE_BACKEND_URL: http://backend:8000
|
||||||
PUBLIC_BACKEND_URL:
|
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\_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\_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` |
|
| 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: {
|
validations: {
|
||||||
airtableUrl: {
|
airtableUrl: {
|
||||||
valid(value) {
|
valid(value) {
|
||||||
const regex = /https:\/\/airtable.com\/shr(.*)$/g
|
const regex = /https:\/\/airtable.com\/[shr|app](.*)$/g
|
||||||
return !!value.match(regex)
|
return !!value.match(regex)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue