Repository navigation
feat(django): opt-in posthog-js cookie fallback for request context - #1045
DanielVisca wants to merge 7 commits into
Conversation
…sing The middleware reads the distinct ID and the live session ID from the ph_<token>_posthog cookie as a fallback. Headers still win, and an authenticated user ID still wins over the cookie distinct ID. POSTHOG_MW_READ_POSTHOG_COOKIE = False turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
posthog-python Compliance ReportDate: 2026-10-09T20:34:28.669446+00:00 ✅ All Tests Passed!121/121 tests passed Capture_V1 Tests✅ 95/95 tests passed View Details
Capture_Ai Tests✅ 5/5 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
Feature_Flags_Local_Evaluation Tests✅ 4/4 tests passed View Details
|
|
[Medium impact] The PR appears safe to merge, with a non-blocking consent-scoping issue in the unknown-key fallback. Reviews (2) · Last reviewed commit: "fix(django): only read this project's po..." · Reviewed by Greptile |
…pt-out Read the cookie for the known project key only (trimmed, with the posthog-js character replacement), ignore it when the consent cookie opts out, compare timestamps with an absolute difference, update the public API snapshot, and cover the setting and the 24-hour cap in tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
| consent_values = [ | ||
| value | ||
| for name, value in cookies.items() | ||
| if name.startswith(_POSTHOG_CONSENT_COOKIE_PREFIX) | ||
| ] |
There was a problem hiding this comment.
Unrelated opt-outs drop session links
When no middleware or module key is set, _read_posthog_cookie checks every project's consent cookie. A request with only ph_projectA_posthog and __ph_opt_in_out_projectB=0 therefore loses project A's distinct ID and session ID, even though the visitor opted out only of project B. This can drop browser-session links for apps that capture through an explicit client without setting POSTHOG_MW_CLIENT.
Keep the selected cookie's name and check consent only for that project.
Knowledge Base Used: Framework integrations
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/integrations/django.py
Line: 117-121
Comment:
**Unrelated opt-outs drop session links**
When no middleware or module key is set, `_read_posthog_cookie` checks every project's consent cookie. A request with only `ph_projectA_posthog` and `__ph_opt_in_out_projectB=0` therefore loses project A's distinct ID and session ID, even though the visitor opted out only of project B. This can drop browser-session links for apps that capture through an explicit client without setting `POSTHOG_MW_CLIENT`.
Keep the selected cookie's name and check consent only for that project.
**Knowledge Base Used:** [Framework integrations](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-python/-/docs/framework-integrations.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Not approved — this change needs a human reviewer.
Re-adding the stamphog label gives the same result unless the changed files change.
stamphog can't approve this pull request, because two policy gates refused it. The deny-list gate matched public_api: the change adds a new read_posthog_cookie attribute to PosthogContextMiddleware, which shows up in references/public_api_snapshot.txt. The tier gate classified it as T2-never (260 lines across 4 files, cross-cutting, a feat change), a category that always needs a human reviewer. The size gate passed, so splitting the PR won't clear the refusal. Please ask a human maintainer to review it, ideally someone who owns the Django integration and the public API surface.
- 👍 on the PR from greptile-apps[bot].
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✗ | matches: public_api |
| size | ✓ | 125L, 1F substantive, 260L/4F incl. docs/generated/snapshots — within ceiling |
| tier | ✗ | classified as T2-never: T2-never (260L, 4F, cross-cutting, feat) |
| stamphog 2.4.1 | .stamphog/policy.yml @ unknown · reviewed head 4efbf1a |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
…safe POSTHOG_MW_READ_POSTHOG_COOKIE is now off by default, because the server cannot see an opt-out that posthog-js keeps in localStorage. Read the cookie only when neither tracing header is present, take the distinct ID only for identified users so anonymous visitors stay personless, and accept the older two-item $sesid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
…back Add POSTHOG_MW_COOKIE_SESSION_IDLE_TIMEOUT_SECONDS for projects with a custom posthog-js session_idle_timeout_seconds, and list the unsupported posthog-js configurations in the middleware docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
Clamp POSTHOG_MW_COOKIE_SESSION_IDLE_TIMEOUT_SECONDS to 60 seconds through 10 hours, the bounds posthog-js applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
Add POSTHOG_MW_COOKIE_OPT_OUT_BY_DEFAULT, so a visitor without a consent cookie counts as opted out, like posthog-js with opt_out_capturing_by_default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
POSTHOG_MW_COOKIE_SESSION_IDLE_TIMEOUT_SECONDS = 0 now falls back to 30 minutes, like posthog-js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
💡 Motivation and Context
Backend events only link to a browser session when the frontend sets
tracing_headersfor the backend hostname. Most apps never set it, so their Django events have no$session_idand do not show with session replays.posthog-js already stores the distinct ID and the session ID in its first-party cookie
ph_<project token>_posthog(default persistencelocalStorage+cookie). The browser sends this cookie on every same-site request, so the middleware can link the request with no frontend change.Changes:
POSTHOG_MW_READ_POSTHOG_COOKIE(defaultFalse). When it isTrue,PosthogContextMiddlewarereads the posthog-js cookie, but only when the request has neither tracing header. A request never mixes header and cookie values.$user_stateisidentified. Anonymous visitors stay personless on the backend, as they are with posthog-js's defaultidentified_onlyprofiles. An authenticated user ID still wins over the cookie distinct ID.$sesidformat is accepted.+,/,=replacement that posthog-js uses. When no key is known, the cookie is used only when exactly oneph_*_posthogcookie is present.__ph_opt_in_out_<token>) opts the visitor out.Why opt-in: posthog-js keeps opt-out consent in localStorage by default, and the server cannot read that. A default-on fallback would keep linking backend events to a visitor who rejected tracking. Teams that store consent in a cookie, or that do not use opt-out, can turn it on.
A custom posthog-js
session_idle_timeout_seconds(clamped to 60 seconds through 10 hours, like posthog-js) andopt_out_capturing_by_defaultare supported throughPOSTHOG_MW_COOKIE_SESSION_IDLE_TIMEOUT_SECONDSandPOSTHOG_MW_COOKIE_OPT_OUT_BY_DEFAULT.Known limits: a custom
persistence_name, custom consent cookie names,respect_dnt, and non-cookie posthog-js persistence are not handled. The option docs list these.Spec: the tracing headers spec lists only headers as server-side input. PostHog/sdk-specs#116 adds the optional cookie fallback this PR implements.
Like the headers, the cookie is analytics context only and is never used for authentication.
💚 How did you test it?
I ran these (agent):
pytest posthog/test/integrations/passes. The new parameterized test covers a live cookie session, headers that win over the cookie, an authenticated user that wins over the cookie distinct ID, idle, too-long and future-dated sessions that are dropped, another project's cookie, the opt-out cookie,POSTHOG_MW_READ_POSTHOG_COOKIEoff by default and set toFalse, an anonymous visitor, and the older two-item session.ruffpasses, andmypyshows only errors that already exist in this file onmain. I did not test in a running Django app.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
COOKIE_PERSISTED_PROPERTIES,$sesid = [lastActivity, sessionId, sessionStart]).Created with PostHog from a Slack thread
🤖 Generated with Claude Code