Skip to content

feat(tracing-headers): allow an optional posthog-js cookie fallback - #116

Open
DanielVisca wants to merge 2 commits into
mainfrom
posthog/tracing-headers-cookie-fallback
Open

DanielVisca wants to merge 2 commits into
mainfrom
posthog/tracing-headers-cookie-fallback

Conversation

@DanielVisca

Copy link
Copy Markdown

Problem

Server SDKs link backend events to a browser session only through the X-POSTHOG-* tracing headers, and most apps never set tracing_headers. posthog-js already keeps the distinct ID, the session ID and the user state in its first-party cookie, and the browser sends that cookie on every same-site request. posthog-python and posthog-node now add an off-by-default option to read it, but the spec lists only headers as server input.

Changes

This adds three requirements to tracing-headers, with scenarios:

  • Optional posthog-js cookie fallback: the option is off by default, the middleware uses headers only when any header is present, and the cookie identity is never used for authentication.
  • Cookie fallback identity and consent: the middleware reads only the project's cookie (with the posthog-js PL/SL/EQ token replacement), honors the consent cookie and opt-out by default, and uses the distinct ID only for identified visitors.
  • Cookie fallback session liveness: the session ID is used only when it is still live, with absolute ages, a 24-hour cap, and an idle timeout clamped to 60 seconds through 10 hours, like posthog-js.

The change is synced into openspec/specs/tracing-headers/spec.md and archived under openspec/changes/archive/2026-10-09-add-optional-posthog-js-cookie-fallback/.

Implementations:

How did you test this?

openspec validate tracing-headers --type spec --strict passes. openspec validate --specs --strict (CLI 1.14.1) reports the same failures in other specs with and without this change.


Created with PostHog from a Slack thread

🤖 Generated with Claude Code

Server middleware may read the session id, and the distinct id of an identified visitor, from the posthog-js cookie when a request has neither tracing header. The option is off by default, honors the consent cookie, and checks session liveness like posthog-js.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2

@dustinbyrne dustinbyrne left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional cookie fallback and session-liveness checks look good. One point for consideration: retain anonymous cookie identity while avoiding new profile creation from that fallback.

AI-assisted review.

Comment thread openspec/specs/tracing-headers/spec.md Resolved
@dustinbyrne
dustinbyrne requested a review from a team October 10, 2026 02:48
…a new profile

The cookie fallback now uses the cookie distinct id for anonymous visitors too and sets $process_person_profile = false, instead of generating a random id. This keeps browser and backend events on one distinct id, as @posthog/next does, while the fallback still creates no new person profile. An explicit or authenticated distinct id overrides the cookie and is not made personless by the cookie $user_state.

Generated-By: PostHog Desktop
Task-Id: fe92e314-5188-4f31-95f6-3eb2d36dc3e1

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional cookie fallback contract looks good, including anonymous identity retention and explicit-identity precedence. One non-blocking consent clarification is noted inline.

AI-assisted review.


### Requirement: Cookie fallback identity and consent

With the fallback on, the middleware SHALL read only the configured project's cookie, `ph_<token>_posthog` with `+`, `/`, `=` in the token replaced by `PL`, `SL`, `EQ`. It SHALL ignore the cookie when the consent cookie `__ph_opt_in_out_<token>` is no-like, or absent under configured opt-out by default. It SHALL use the cookie `distinct_id` for identified and anonymous visitors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. [P2] Define pending consent under opt-out-by-default

Severity: MINOR
Disposition: OPTIONAL
Relationship: Introduced specification precision gap—not a runtime regression.
Location: openspec/specs/tracing-headers/spec.md:279, RIGHT; mirror in the archived delta at line 27.

Consider covering present-but-pending consent, as well as an absent consent cookie.

With fallback enabled, no tracing headers, a live project cookie, and opt-out-by-default enabled, either of these consent values is present but neither yes-like nor no-like:

__ph_opt_in_out_test-token=
__ph_opt_in_out_test-token=unexpected

The new sentence explicitly requires rejection for no-like values and absence, but leaves these pending values unspecified. The expected result is that neither cookie identity nor cookie session is used.

At pinned revision 69814c77c675918a658e8dad4a02dfdb152398e4:

An implementation translating only the listed rejection conditions could therefore link identity/session without granted consent. The prose does not require that permissive result; it simply leaves the boundary unspecified.

Consider replacing the condition in both canonical and archived text with:

It SHALL ignore the cookie when the consent cookie is no-like, or when opt-out-by-default is configured and consent is not yes-like.

A corresponding scenario would preserve that boundary:

#### Scenario: pending consent is not linked under opt-out-by-default
- **GIVEN** cookie fallback and opt-out-by-default are enabled
- **AND** the request has no tracing headers and a live project cookie
- **AND** the project's consent cookie is present with an empty or unrecognized value
- **WHEN** capture is called without an explicit distinct id
- **THEN** neither the cookie distinct id nor the cookie session id is used

Verification and impact limits: Deterministic source/contract comparison establishes this omission; no failing SDK execution is claimed or applicable. Base had no fallback contract, while head introduces this condition. Browser consent APIs ordinarily write 0 or 1, so the additional trigger requires externally written, legacy, migrated, or malformed state. No production incident or current Node defect was established.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants