diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..20f839ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## 1.3.1 - 2021-05-07 + +- Add $set and $set_once support (#23) +- Add distinct id to $create_alias event (#27) +- Add UUID to ID_TYPES (#26) + +## 1.2.1 + +Initial release logged in changelog. \ No newline at end of file diff --git a/Makefile b/Makefile index c02f3f2f..960fba91 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,8 @@ release_analytics: e2e_test: .buildscripts/e2e.sh +django_example: + python -m pip install -e ".[sentry]" + cd sentry_django_example && python manage.py runserver 8080 + .PHONY: test lint release e2e_test diff --git a/README.md b/README.md index deee1ef1..a4030e8e 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,22 @@ Specifically, the [Python integration](https://posthog.com/docs/integrations/pyt ## Running Locally -Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action. \ No newline at end of file +Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action. + +## Running the Django Sentry Integration Locally + +There's a sample Django project included, called `sentry_django_example`, which explains how to use PostHog with Sentry. + +There's 2 places of importance (Changes required are all marked with TODO in the sample project directory) + +1. Settings.py + 1. Input your Sentry DSN + 2. Input your Sentry Org and ProjectID details into `PosthogIntegration()` + 3. Add `POSTHOG_DJANGO` to settings.py. This allows the `PosthogDistinctIdMiddleware` to get the distinct_ids + +2. urls.py + 1. This includes the `sentry-debug/` endpoint, which generates an exception + +To run things: `make django_example`. This installs the posthog-python library with the sentry-sdk add-on, and then runs the django app. +Also start the PostHog app locally. +Then navigate to `http://127.0.0.1:8080/sentry-debug/` and you should get an event in both Sentry and PostHog, with links to each other. diff --git a/posthog/sentry/__init__.py b/posthog/sentry/__init__.py new file mode 100644 index 00000000..05b2a55b --- /dev/null +++ b/posthog/sentry/__init__.py @@ -0,0 +1 @@ +POSTHOG_ID_TAG = "posthog_distinct_id" diff --git a/posthog/sentry/django.py b/posthog/sentry/django.py new file mode 100644 index 00000000..48bee70d --- /dev/null +++ b/posthog/sentry/django.py @@ -0,0 +1,28 @@ +from django.conf import settings +from sentry_sdk import configure_scope + +from posthog.sentry import POSTHOG_ID_TAG + +GET_DISTINCT_ID = getattr(settings, "POSTHOG_DJANGO", {}).get("distinct_id") + + +def get_distinct_id(request): + if not GET_DISTINCT_ID: + return None + try: + return GET_DISTINCT_ID(request) + except: + return None + + +class PosthogDistinctIdMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + with configure_scope() as scope: + distinct_id = get_distinct_id(request) + if distinct_id: + scope.set_tag(POSTHOG_ID_TAG, distinct_id) + response = self.get_response(request) + return response diff --git a/posthog/sentry/posthog_integration.py b/posthog/sentry/posthog_integration.py new file mode 100644 index 00000000..2ebbc938 --- /dev/null +++ b/posthog/sentry/posthog_integration.py @@ -0,0 +1,47 @@ +from sentry_sdk._types import MYPY +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.scope import add_global_event_processor + +import posthog +from posthog.request import DEFAULT_HOST +from posthog.sentry import POSTHOG_ID_TAG + +if MYPY: + from typing import Any, Dict, Optional + + from sentry_sdk._types import Event, Hint + + +class PostHogIntegration(Integration): + identifier = "posthog-python" + organization = None # The Sentry organization, used to send a direct link from PostHog to Sentry + project_id = None # The Sentry project id, used to send a direct link from PostHog to Sentry + prefix = "https://sentry.io/organizations/" # Url of a self-hosted sentry instance (default: https://sentry.io/organizations/) + + @staticmethod + def setup_once(): + @add_global_event_processor + def processor(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + if Hub.current.get_integration(PostHogIntegration) is not None: + if event.get("level") != "error": + return event + + if event.get("tags", {}).get(POSTHOG_ID_TAG): + posthog_distinct_id = event["tags"][POSTHOG_ID_TAG] + event["tags"]["PostHog URL"] = f"{posthog.host or DEFAULT_HOST}/person/{posthog_distinct_id}" + + properties = { + "$sentry_event_id": event["event_id"], + "$sentry_exception": event["exception"], + } + + if PostHogIntegration.organization and PostHogIntegration.project_id: + properties[ + "$sentry_url" + ] = f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={PostHogIntegration.project_id}&query={event['event_id']}" + + posthog.capture(posthog_distinct_id, "$exception", properties) + + return event diff --git a/sentry_django_example/db.sqlite3 b/sentry_django_example/db.sqlite3 new file mode 100644 index 00000000..e69de29b diff --git a/sentry_django_example/manage.py b/sentry_django_example/manage.py new file mode 100755 index 00000000..a49cf6a8 --- /dev/null +++ b/sentry_django_example/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/sentry_django_example/sentry_django_example/__init__.py b/sentry_django_example/sentry_django_example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sentry_django_example/sentry_django_example/asgi.py b/sentry_django_example/sentry_django_example/asgi.py new file mode 100644 index 00000000..2aea0866 --- /dev/null +++ b/sentry_django_example/sentry_django_example/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for sentry_django_example project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings") + +application = get_asgi_application() diff --git a/sentry_django_example/sentry_django_example/settings.py b/sentry_django_example/sentry_django_example/settings.py new file mode 100644 index 00000000..a9ddcc4e --- /dev/null +++ b/sentry_django_example/sentry_django_example/settings.py @@ -0,0 +1,168 @@ +""" +Django settings for sentry_django_example project. + +Generated by 'django-admin startproject' using Django 3.2.2. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path +from uuid import uuid4 + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-4kzfiq7vb(t0+jbl#vq)u=%06ouf)n*=l%730c8=tk(wkm9i9o" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# PostHog Setup (can be a separate app) +import posthog + +# You can find this key on the /setup page in PostHog +posthog.api_key = "LXP6nQXvo-2TCqGVrWvPah8uJIyVykoMmhnEkEBi5PA" # TODO: replace with your api key + +posthog.personal_api_key = "" + +# Where you host PostHog, with no trailing /. +# You can remove this line if you're using posthog.com +posthog.host = "http://127.0.0.1:8000" + +from posthog.sentry.posthog_integration import PostHogIntegration + +PostHogIntegration.organization = "posthog" # TODO: your sentry organization +PostHogIntegration.project_id = "5624115" # TODO: your sentry projectID +# PostHogIntegration.prefix = # TODO: your self hosted Sentry url. (default: https://sentry.io/organizations/) + +# Since Sentry doesn't allow Integrations configuration (see https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/__init__.py#L171-L183) +# we work around this by setting static class variables beforehand + +# Sentry Setup +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +sentry_sdk.init( + dsn="https://27ac54f7f4cf484abf1335436b0c52e5@o344752.ingest.sentry.io/5624115", # TODO: your Sentry DSN here + integrations=[DjangoIntegration(), PostHogIntegration()], + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0, + # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=True, +) + +POSTHOG_DJANGO = { + "distinct_id": lambda request: str(uuid4()) # TODO: your logic for generating unique ID, given the request object +} + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "posthog.sentry.django.PosthogDistinctIdMiddleware", +] + +ROOT_URLCONF = "sentry_django_example.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "sentry_django_example.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/sentry_django_example/sentry_django_example/urls.py b/sentry_django_example/sentry_django_example/urls.py new file mode 100644 index 00000000..5156f4cb --- /dev/null +++ b/sentry_django_example/sentry_django_example/urls.py @@ -0,0 +1,27 @@ +"""sentry_django_example URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + + +def trigger_error(request): + division_by_zero = 1 / 0 + + +urlpatterns = [ + path("admin/", admin.site.urls), + path("sentry-debug/", trigger_error), +] diff --git a/sentry_django_example/sentry_django_example/wsgi.py b/sentry_django_example/sentry_django_example/wsgi.py new file mode 100644 index 00000000..8626f315 --- /dev/null +++ b/sentry_django_example/sentry_django_example/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sentry_django_example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings") + +application = get_wsgi_application() diff --git a/setup.py b/setup.py index a8aa8d11..4a3b961c 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ "pre-commit", ], "test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage"], + "sentry": ["sentry-sdk", "django"], } setup( diff --git a/setup_analytics.py b/setup_analytics.py index fb744b77..a5edd074 100644 --- a/setup_analytics.py +++ b/setup_analytics.py @@ -31,6 +31,9 @@ license="MIT License", install_requires=install_requires, tests_require=tests_require, + extras_require={ + "sentry": ["sentry-sdk", "django"], + }, description="Integrate PostHog into any python application.", long_description=long_description, classifiers=[