From 0d93415d7750368aafd87e0b242c6e89a49edd05 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Thu, 22 Oct 2026 10:54:01 +0200 Subject: [PATCH 1/2] fix(python): use the framework integrations for exception capture Co-Authored-By: Claude Opus 5.5 --- context/commandments.yaml | 17 ++++++---- .../posthog-best-practices/description.md | 33 +++++++++++++------ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/context/commandments.yaml b/context/commandments.yaml index 8c06db3c..a4cc1e42 100644 --- a/context/commandments.yaml +++ b/context/commandments.yaml @@ -73,6 +73,7 @@ commandments: - 'In CLIs and scripts: MUST call posthog.shutdown() before exit or all events are lost' - Always use the Posthog() class constructor (instance-based API) instead of module-level posthog.api_key config - Always include enable_exception_autocapture=True in the Posthog() constructor to automatically track exceptions + - PosthogFlaskIntegration (posthog.integrations.flask), PosthogASGIMiddleware (posthog.integrations.asgi), and the Django REST Framework exception handler (posthog.integrations.drf) need posthog 7.69.0 or later - when you add one, require at least that version - NEVER send PII in capture() event properties — no emails, full names, phone numbers, physical addresses, IP addresses, or user-generated content - PII belongs in identify() person properties, NOT in capture() event properties. Safe event properties are metadata like message_length, form_type, boolean flags. - Register posthog_client.shutdown with atexit.register() to ensure all events are flushed on exit @@ -140,23 +141,25 @@ commandments: - For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups django: - - Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions - - Initialize PostHog in AppConfig.ready() with api_key and host from environment variables + - Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions raised by views + - Create the Posthog() client in settings.py with the token and host from environment variables, and assign it to POSTHOG_MW_CLIENT. The middleware and the DRF exception handler send exceptions to this client; without it they use the unconfigured global client, and the exceptions never reach PostHog. Read the client elsewhere as settings.POSTHOG_MW_CLIENT, and do not assign settings at runtime, such as in AppConfig.ready() + - With Django REST Framework, set REST_FRAMEWORK['EXCEPTION_HANDLER'] to 'posthog.integrations.drf.exception_handler', or wrap an existing handler with posthog.integrations.drf.create_exception_handler - DRF turns API exceptions into responses before the middleware sees them - The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own - The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() - Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these flask: - - Initialize PostHog globally in create_app() using posthog.api_key and posthog.host (NOT per-request) - - Manually capture exceptions with `posthog.capture_exception(e)` for error tracking since Flask has built-in error handlers + - Create one Posthog() client in create_app() (at module level in a single-file app), NOT per-request, and register PosthogFlaskIntegration(app, client=posthog_client). The integration opens a context for each request and captures exceptions that escape your routes + - Do NOT add an @app.errorhandler(Exception) to call capture_exception() - the integration already captures unhandled route exceptions. An error handler the app already has consumes its exceptions before the integration sees them, so call posthog_client.capture_exception(e) inside it when those exceptions should be reported - Blueprint registration happens AFTER PostHog initialization in create_app() fastapi: - - Initialize PostHog in the lifespan context manager on startup using posthog.api_key and posthog.host - - Call posthog.flush() in the lifespan shutdown to ensure all events are sent before the app exits + - Create one Posthog() client where you build the app (at module level or in the app factory) and add app.add_middleware(PosthogASGIMiddleware, client=posthog_client). The middleware opens a context for each request and captures exceptions that escape your routes. Middleware cannot be added once the app starts, so do not create the client in the lifespan + - Call posthog_client.shutdown() in the lifespan shutdown to ensure all events are sent before the app exits + - An @app.exception_handler(Exception) catch-all still lets the middleware capture the exception, so do not call capture_exception() in it. A handler for a specific exception class turns that exception into a response before the middleware sees it - call posthog_client.capture_exception(exc) there when that exception should be reported - Use Pydantic Settings with @lru_cache decorator on get_settings() for caching and easy test overrides - Use FastAPI dependency injection (Depends) for accessing current_user and settings in route handlers - - Use the same context API pattern as Flask/Django (with new_context(), identify_context(user_id), then capture()) + - The middleware already opens the request context - in a route, call identify_context(user_id) and then capture(), without a new_context() of your own php: - Remember that source code is available in the vendor directory after composer install diff --git a/context/skills/posthog-best-practices/description.md b/context/skills/posthog-best-practices/description.md index 2d5e21f6..6340c844 100644 --- a/context/skills/posthog-best-practices/description.md +++ b/context/skills/posthog-best-practices/description.md @@ -160,7 +160,13 @@ If the project uses Django: context identified as whoever they were beforehand, which is nobody on a login. There, open a fresh context and identify it after the auth state changes, or the event is attributed to the wrong person. -- Initialize PostHog in `AppConfig.ready()` from env vars. +- Create the `Posthog()` client in `settings.py` from env vars and assign it to + `POSTHOG_MW_CLIENT`. The middleware and the Django REST Framework exception + handler send exceptions to that client. Without it, they use the unconfigured + global client, and view exceptions never reach PostHog. +- With Django REST Framework, set `REST_FRAMEWORK["EXCEPTION_HANDLER"]` to + `posthog.integrations.drf.exception_handler`. DRF turns API exceptions into + responses before the middleware sees them. - Do not write custom middleware or `distinct_id` helpers first. - For management commands or other short-lived processes, call the underlying Python client's `posthog.shutdown()` before exit. - If this applies, consult [the Django docs](https://posthog.com/docs/libraries/django). @@ -169,11 +175,13 @@ If the project uses Django: If the project uses Flask: -- Initialize PostHog globally in `create_app()` before blueprint registration. -- No middleware ships for Flask, so nothing binds identity for you: open a fresh - context at the top of each route and identify it there, preferring the - authenticated user id and falling back to the incoming `X-POSTHOG-DISTINCT-ID`. -- Call `posthog.capture_exception(e)` from centralized error handlers. +- Create one `Posthog()` client in `create_app()` before blueprint registration, + and register `PosthogFlaskIntegration(app, client=posthog_client)`. It opens a + context for each request and captures exceptions that escape your routes. +- The integration does not identify the user: call `identify_context(...)` in + the route, preferring the authenticated user id. +- An error handler consumes its exceptions before the integration sees them. + Call `capture_exception(e)` only inside error handlers the app already has. - For CLI commands, jobs, or other short-lived processes, call the underlying Python client's `posthog.shutdown()` before exit. - If this applies, consult [the Flask docs](https://posthog.com/docs/libraries/flask). @@ -181,10 +189,15 @@ If the project uses Flask: If the project uses FastAPI: -- Initialize PostHog in lifespan startup and call `posthog.shutdown()` in lifespan shutdown. -- No middleware ships for FastAPI either — bind identity per request yourself, in - a dependency or middleware that opens a context and identifies it, rather than - at each capture. +- Create one `Posthog()` client where you build the app, add + `app.add_middleware(PosthogASGIMiddleware, client=posthog_client)`, and call + `posthog_client.shutdown()` in lifespan shutdown. The middleware opens a + context for each request and captures exceptions that escape your routes. +- The middleware does not identify the user: call `identify_context(...)` in a + dependency or the route, rather than at each capture. +- An `@app.exception_handler(Exception)` catch-all still lets the middleware + capture the exception. A handler for a specific exception class does not: + call `capture_exception(exc)` there. - Prefer Pydantic Settings with `@lru_cache`. - Use `Depends` for current user and settings in handlers. - If this applies, consult [the Python docs](https://posthog.com/docs/libraries/python). From 1f828ffcb76267e83dda96eb6d4ca004b928652d Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Thu, 22 Oct 2026 13:25:03 +0200 Subject: [PATCH 2/2] fix(python): keep framework setup in the docs Co-Authored-By: Claude Opus 5.5 --- context/commandments.yaml | 17 +++++----- .../posthog-best-practices/description.md | 33 ++++++------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/context/commandments.yaml b/context/commandments.yaml index a4cc1e42..6325aea3 100644 --- a/context/commandments.yaml +++ b/context/commandments.yaml @@ -141,25 +141,24 @@ commandments: - For Microsoft.FeatureManagement, call `UseFeatureManagement()` and implement `IPostHogFeatureFlagContextProvider` to provide the current distinct ID, person properties, and groups django: - - Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions raised by views - - Create the Posthog() client in settings.py with the token and host from environment variables, and assign it to POSTHOG_MW_CLIENT. The middleware and the DRF exception handler send exceptions to this client; without it they use the unconfigured global client, and the exceptions never reach PostHog. Read the client elsewhere as settings.POSTHOG_MW_CLIENT, and do not assign settings at runtime, such as in AppConfig.ready() - - With Django REST Framework, set REST_FRAMEWORK['EXCEPTION_HANDLER'] to 'posthog.integrations.drf.exception_handler', or wrap an existing handler with posthog.integrations.drf.create_exception_handler - DRF turns API exceptions into responses before the middleware sees them + - Add 'posthog.integrations.django.PosthogContextMiddleware' to MIDDLEWARE, after AuthenticationMiddleware, it auto-extracts tracing headers and captures exceptions + - Pass the Posthog() client to the middleware with the POSTHOG_MW_CLIENT setting, as the Django docs show - do not assign django.conf.settings at runtime, such as in AppConfig.ready() - The middleware identifies the request context from the X-POSTHOG-DISTINCT-ID header or the authenticated user's pk, so in a view where the user is already logged in a plain capture() is already attributed - do not wrap it in a context of its own - The middleware reads the user once, before the view runs, so a login request's context is identified as whoever the user was beforehand - nobody - and calling login() does not update it. A bare capture there is personless. Hook Django's user_logged_in signal and call identify_context(str(user.pk)) inside it: the signal runs inside the login request, so it fixes the ambient context and every later capture in that request is attributed. Logout views need no special handling, the user is still authenticated when the middleware runs - capture before calling logout() - Do NOT create custom middleware, distinct_id helpers, or conditional checks - the SDK handles these flask: - - Create one Posthog() client in create_app() (at module level in a single-file app), NOT per-request, and register PosthogFlaskIntegration(app, client=posthog_client). The integration opens a context for each request and captures exceptions that escape your routes - - Do NOT add an @app.errorhandler(Exception) to call capture_exception() - the integration already captures unhandled route exceptions. An error handler the app already has consumes its exceptions before the integration sees them, so call posthog_client.capture_exception(e) inside it when those exceptions should be reported + - Register PosthogFlaskIntegration with one Posthog() client, as the Flask docs show (NOT per-request) + - Do NOT add an @app.errorhandler(Exception) to call capture_exception() - the integration already captures unhandled route exceptions - Blueprint registration happens AFTER PostHog initialization in create_app() fastapi: - - Create one Posthog() client where you build the app (at module level or in the app factory) and add app.add_middleware(PosthogASGIMiddleware, client=posthog_client). The middleware opens a context for each request and captures exceptions that escape your routes. Middleware cannot be added once the app starts, so do not create the client in the lifespan - - Call posthog_client.shutdown() in the lifespan shutdown to ensure all events are sent before the app exits - - An @app.exception_handler(Exception) catch-all still lets the middleware capture the exception, so do not call capture_exception() in it. A handler for a specific exception class turns that exception into a response before the middleware sees it - call posthog_client.capture_exception(exc) there when that exception should be reported + - Add PosthogASGIMiddleware, as the ASGI section of the Python docs shows. Create the client where you build the app, not in the lifespan - middleware cannot be added once the app starts + - Call posthog.flush() in the lifespan shutdown to ensure all events are sent before the app exits + - Do NOT call capture_exception() in an @app.exception_handler(Exception) catch-all - the middleware already captures those exceptions - Use Pydantic Settings with @lru_cache decorator on get_settings() for caching and easy test overrides - Use FastAPI dependency injection (Depends) for accessing current_user and settings in route handlers - - The middleware already opens the request context - in a route, call identify_context(user_id) and then capture(), without a new_context() of your own + - Use the same context API pattern as Flask/Django (with new_context(), identify_context(user_id), then capture()) php: - Remember that source code is available in the vendor directory after composer install diff --git a/context/skills/posthog-best-practices/description.md b/context/skills/posthog-best-practices/description.md index 6340c844..5c729fa6 100644 --- a/context/skills/posthog-best-practices/description.md +++ b/context/skills/posthog-best-practices/description.md @@ -160,13 +160,8 @@ If the project uses Django: context identified as whoever they were beforehand, which is nobody on a login. There, open a fresh context and identify it after the auth state changes, or the event is attributed to the wrong person. -- Create the `Posthog()` client in `settings.py` from env vars and assign it to - `POSTHOG_MW_CLIENT`. The middleware and the Django REST Framework exception - handler send exceptions to that client. Without it, they use the unconfigured - global client, and view exceptions never reach PostHog. -- With Django REST Framework, set `REST_FRAMEWORK["EXCEPTION_HANDLER"]` to - `posthog.integrations.drf.exception_handler`. DRF turns API exceptions into - responses before the middleware sees them. +- Pass the `Posthog()` client to the middleware with `POSTHOG_MW_CLIENT`, as the + Django docs show. - Do not write custom middleware or `distinct_id` helpers first. - For management commands or other short-lived processes, call the underlying Python client's `posthog.shutdown()` before exit. - If this applies, consult [the Django docs](https://posthog.com/docs/libraries/django). @@ -175,13 +170,10 @@ If the project uses Django: If the project uses Flask: -- Create one `Posthog()` client in `create_app()` before blueprint registration, - and register `PosthogFlaskIntegration(app, client=posthog_client)`. It opens a - context for each request and captures exceptions that escape your routes. -- The integration does not identify the user: call `identify_context(...)` in - the route, preferring the authenticated user id. -- An error handler consumes its exceptions before the integration sees them. - Call `capture_exception(e)` only inside error handlers the app already has. +- Initialize PostHog in `create_app()` before blueprint registration, and + register `PosthogFlaskIntegration`, as the Flask docs show. It opens a context + for each request and captures route exceptions. Identify the user with + `identify_context(...)` in the route. - For CLI commands, jobs, or other short-lived processes, call the underlying Python client's `posthog.shutdown()` before exit. - If this applies, consult [the Flask docs](https://posthog.com/docs/libraries/flask). @@ -189,15 +181,10 @@ If the project uses Flask: If the project uses FastAPI: -- Create one `Posthog()` client where you build the app, add - `app.add_middleware(PosthogASGIMiddleware, client=posthog_client)`, and call - `posthog_client.shutdown()` in lifespan shutdown. The middleware opens a - context for each request and captures exceptions that escape your routes. -- The middleware does not identify the user: call `identify_context(...)` in a - dependency or the route, rather than at each capture. -- An `@app.exception_handler(Exception)` catch-all still lets the middleware - capture the exception. A handler for a specific exception class does not: - call `capture_exception(exc)` there. +- Add `PosthogASGIMiddleware`, as the Python docs show, and call + `posthog.shutdown()` in lifespan shutdown. The middleware opens a context for + each request and captures route exceptions. Identify the user in a dependency + rather than at each capture. - Prefer Pydantic Settings with `@lru_cache`. - Use `Depends` for current user and settings in handlers. - If this applies, consult [the Python docs](https://posthog.com/docs/libraries/python).