Skip to content

feat: add next.js and nuxt server span context - #5315

Merged
pauldambra merged 10 commits into
posthog/node-framework-span-contextfrom
posthog/nextjs-otel-span-context
Oct 28, 2026
Merged

pauldambra merged 10 commits into
posthog/node-framework-span-contextfrom
posthog/nextjs-otel-span-context

Conversation

@pauldambra

@pauldambra pauldambra commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

[Robot] Prepared with PostHog Desktop on behalf of @pauldambra.

Problem

Next.js and Nuxt developers need to connect backend work to browser sessions. Their existing server clients do not keep request IDs active around application spans.

Changes

  • Add Node-only @posthog/next/otel. createPostHogSpanContext(config) reuses the existing configuration, identity resolver, consent handling, and cached client.
  • Expose createContextSpanProcessor() and withPostHogContext(work) for App Router handlers, actions, and wrapped server component work.
  • Add Nuxt serverConfig.requestContext, default false, and Node-only getPostHogServerClient() from @posthog/nuxt/server.
  • Reuse Nuxt's plugin client for events, errors, and the optional span processor. The accessor returns undefined before startup and after close.
  • Preserve application identity and explicit canonical attributes. Fill missing request and child span IDs through the shared Node processor.
  • End each scope when its callback or handler settles. Later stream work and detached jobs need their own explicit context.
  • Keep OpenTelemetry out of ordinary entry declarations and Nuxt analytics setup. Nuxt calls the dependency-free helper from the main Node entry.
  • Test the packaged Nuxt consumer with the built Node SDK from this stack, including an install without OpenTelemetry.

The processor follows only parent span objects it observed. A sampled-out intermediate hides earlier server ancestors, which remain unchanged.

Next.js support is Node App Router work. Pages Router arguments, browser/Edge tracing, later React child rendering, and work after the wrapped callback are outside it. Nuxt covers Nitro 2's H3 handler and Nitro 3's fetch handler. Handler completion can precede the end of a streamed response body.

Builds on #5314. Tracks sdk-specs#128. Release after the shared Node bridge, with the exact minimum dependency set through the release process.

Validation:

  • All 274 Next.js tests pass. The ordered workspace build passes 18 tasks. Both packages pass type and lint checks.
  • A real Next.js 15.5.22 app passes concurrent routes, an error response, a multipart server action, request roots, child exports, and callback/late-work boundaries.
  • Strict ordinary Next.js declarations and its bundle graph need no OpenTelemetry. The opt-in processor matches the application's processor type.
  • Nuxt tests pass both real H3 adapters. A built Nuxt 4.5.1 app passes SSR, 12 concurrent requests, 36 spans, and two attributed errors.
  • The real packaged Nuxt 5 consumer builds and flushes its captured error without OpenTelemetry installed.
  • Frozen dependencies, API checks, formatting, and normal commit hooks pass. No production ingestion or replay test was run.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Added the equivalent minor changeset directly.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Models: GPT-6 (Codex) implemented the framework integrations. GPT-6 Astra reviewed SDK APIs and implemented the incoming Nuxt span regression fix. Used PostHog Desktop, local Node tools, synthetic framework apps, and GitHub CLI. The final change uses one shared processor and keeps the stack at two layers. Human review is required.


Created with PostHog Desktop

@pauldambra pauldambra self-assigned this Oct 17, 2026

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 7f93d89: 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

Comment thread packages/next/src/otel.ts Resolved
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Coherence

0 spec problems · 0 failing chokepoints · 2 spec gaps · 0 guard failures

44 components, 7 bullets (5 invariants, 2 requirements), 0 problems

Chokepoints: none declared.

Spec gaps: 2 entrances with outside or unknown trust and no traced control, and 10 more held by the adoption baseline

  • packages/web/Web.spec.md entrance public api (trust host-app): packages/web
  • packages/webpack-plugin/Webpack-plugin.spec.md entrance webpack plugin (trust build): packages/webpack-plugin

Close one with guard: <chokepoint>, an invariant that names it on its entrances: line, or control: none — <reason>; scaffold control proposes it.

@pauldambra
pauldambra added this pull request to stack #5316 October 17, 2026 15:03
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Node SDK compliance v2

Commit: 7dc2d42
Workflow run and artifacts

Capture v0 — acceptance

✅ 28 passed / ❌ 3 non-passing / ⚠️ 853 not selected

Drill-down: run artifacts → node-v2-v0 → acceptance/report.json.diagnostics.json (match case_id below).

❌ server_snapshot_payload_false — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_zero — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_null — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:

Harness exit: 1; report validation exit: 1.

Capture v1 — acceptance

✅ 28 passed / ❌ 3 non-passing / ⚠️ 853 not selected

Drill-down: run artifacts → node-v2-v1 → acceptance/report.json.diagnostics.json (match case_id below).

❌ server_snapshot_payload_false — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_zero — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_null — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:

Harness exit: 1; report validation exit: 1.

Capture v0 — migration

✅ 55 passed / ❌ 1 non-passing / ⚠️ 101 not selected

Drill-down: run artifacts → node-v2-v0 → reports/report.json.diagnostics.json (match case_id below).

❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertion

Flags field differs

Code: flag_request_field

Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134

the first flags request field "geoip_disable" should equal JSON false

Operation: /flags/

Field: geoip_disable

Expected:

false

Actual value:

true

Harness exit: 1; report validation exit: 1.

Capture v1 — migration

✅ 120 passed / ❌ 1 non-passing / ⚠️ 36 not selected

Drill-down: run artifacts → node-v2-v1 → reports/report.json.diagnostics.json (match case_id below).

❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertion

Flags field differs

Code: flag_request_field

Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134

the first flags request field "geoip_disable" should equal JSON false

Operation: /flags/

Field: geoip_disable

Expected:

false

Actual value:

true

Harness exit: 1; report validation exit: 1.

@pauldambra pauldambra changed the title feat(next): add optional server span context bridge feat: add next.js and nuxt server span context Oct 17, 2026
@pauldambra
pauldambra force-pushed the posthog/nextjs-otel-span-context branch from 7f93d89 to e63e115 Compare October 17, 2026 22:08
@pauldambra
pauldambra marked this pull request as ready for review October 17, 2026 22:29
@pauldambra
pauldambra requested review from a team as code owners October 17, 2026 22:29

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed e63e115: 3 Should fix.

Comment thread packages/next/src/otel.ts Resolved
@pauldambra

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Verdict: ✅ APPROVE (round 1 @ 032965a)

Restacked on the Node privacy fix in #5314. Nuxt tests now use enabled clients that drop events, so the span checks run against the new privacy gate.

Checks

  • Framework build passes. Next unit tests pass. Five offline Nuxt test files, build, and lint pass.
  • The Nuxt 5 consumer test waits for package versions that are not on npm yet, so it cannot run locally before release. CI covers it.
  • Local runs with enabled clients: Remix, Astro, Nuxt, and Next 15 span checks pass.

Known limit: when a sampler drops an intermediate span, the bridge cannot prove ancestry, so that case stays unenriched.


Automated by QA Swarm. Human review is required. This comment does not approve a merge or release.

@marandaneto

Copy link
Copy Markdown
Member

found same issues as the bot

@pauldambra
pauldambra force-pushed the posthog/nextjs-otel-span-context branch from 032965a to 1da8a47 Compare October 26, 2026 12:14

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 1da8a47: 1 Must fix.

This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Comment thread packages/next/src/otel.ts Resolved
@pauldambra
pauldambra force-pushed the posthog/nextjs-otel-span-context branch from 1da8a47 to 8bb9fe4 Compare October 26, 2026 14:22

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 8bb9fe4: 1 Should fix.

This is review round 4 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bot still has open comments but approving to unblock, my findings were addressed

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Error tracking smoke

Commit: 7dc2d4219e5a4c671eef5a40788bd09df4bdaa06 · CI run and artifacts

7/8 passed (0 skipped, 0 uncovered)

Fixture Result First failure
rollup-4 ✅ Passed —
vite-8-autocapture ✅ Passed —
webpack-5.112 ✅ Passed —
next-14 ✅ Passed —
next-15 ✅ Passed —
next-16-webpack ✅ Passed —
next-16-turbopack ❌ Failed release 1.0.0: maps dangling (none), expected deleted
nuxt-4 ✅ Passed —

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
Generated-By: PostHog Desktop
Task-Id: bfd18848-e498-4352-a58a-b9ba85de4198
withPostHogContext passed a proxy that put the request identity back on
every call, so posthog.withContext inside the callback was ignored. The
callback already runs inside the request scope, so pass the client.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
With requestContext on, the request scope now keeps properties from an
outer application context. The error hook now prefers the active
application identity over the saved header identity.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
Match posthog-node: withPostHogContext and the Nitro request wrapper
no longer end the context when the handler returns or throws. Work the
handler starts keeps the request IDs.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
Next.js 15 bundles instrumentation and App Router handlers in separate
webpack layers, each loading its own copy of clientCache.node.ts. That
gave each layer its own client for the same key, so the OTel span
processor (reading one client's context) and withPostHogContext
(writing another's) could disagree about the current request. Cache
clients on globalThis, keyed by Symbol.for, so every layer shares one.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
@pauldambra
pauldambra force-pushed the posthog/nextjs-otel-span-context branch from 9201663 to 7dc2d42 Compare October 28, 2026 13:27
@pauldambra
pauldambra added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit e4df2d5 Oct 28, 2026
72 of 73 checks passed
@pauldambra
pauldambra deleted the posthog/nextjs-otel-span-context branch October 28, 2026 14:19
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.

2 participants