Skip to content

docs(capture-ai): specify how AI instrumentation attributes events to the active context - #125

Merged
turnipdabeets merged 2 commits into
mainfrom
posthog/spec-ai-instrumentation-context
Oct 21, 2026
Merged

turnipdabeets merged 2 commits into
mainfrom
posthog/spec-ai-instrumentation-context

Conversation

@posthog

@posthog posthog Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Why

A server that traces LLM calls for many users through the PostHog OpenTelemetry span processor used to send every AI event under one person, or none — the only way to set the user was a process-wide resource attribute. Three SDKs fixed that the same way, in the same week, with the same semantics. capture-ai is the spec that governs what PostHog's AI instrumentation emits, and it said nothing about attribution, so the contract existed in three implementations and nowhere in writing.

Source changes

All three: snapshot the context in onStart, merge it into a copy of the span in onEnd, and leave the live span alone. Each one's first version wrote onto the live span and was changed after review, because that leaks the person and session to every other processor and exporter on the provider.

What changed in the spec

New capture-ai requirement AI instrumentation attributes events to the active context (proposed, applied and archived on this branch per AGENTS.md), conditioned on the SDK having both an OTel AI bridge and a context primitive:

  • Context distinct id → posthog.distinct_id, context session → $session_id (the same key as capture, so AI events join replays). $ai_session_id explicitly stays free for the instrumentation's conversation id.
  • Each context property is exported under its own key.
  • Precedence: span's own attribute → context property → context distinct id / session id.
  • A span that already names a user (posthog.distinct_id, user.id, or the ai.telemetry/ai.settings distinct-id keys) keeps it.
  • Snapshot at span start, applied at span end, so concurrent requests don't mix.
  • The live span is never mutated; other processors see nothing.
  • Stamping is best effort: a failing context source or an unrepresentable value skips the key, never raises into the instrumented call. Absent, null, empty-string and non-finite values are skipped; the rest of the value conversion stays platform-idiomatic.

One sentence added to Applicability noting this requirement binds any SDK shipping the OTel AI bridge, which now includes posthog-go, whether or not it exposes capture_ai.

openspec validate --specs --strict goes from 155 to 156 warnings: the new requirement trips the same pre-existing "requirement text is very long" advisory as 155 others. No errors introduced.

Uncertain / worth a look

  • Spec home is a judgement call. traces covers PostHog's own OTel-free tracing API and uses the logs wire keys (posthogDistinctId, sessionId); the AI bridge is OTel-native and uses the ingestion keys. capture-ai was the closest fit, but a dedicated capability for the whole AI bridge (span-to-event mapping, redaction, exporter) may be the better long-term home — happy to move it.
  • Value conversion is deliberately left platform-idiomatic. The three SDKs are close but not identical (Go typed slices, Python stringifying out-of-range integers); only the skip rules are pinned. Tighten if you'd rather they converge.
  • Scenarios are prose, matching the neighbouring style, so no new harness steps are needed.

Not included, flagged for human review

  • posthog-js#5271 — react-native stops dropping native crash reports for fatals JS never saw. The author checked and no spec covers JS/native fatal dedup in hybrid SDKs; the marker protocol is one SDK's mechanism, so writing a cross-layer dedup contract needs a deliberate call.
  • posthog-js#5289 — node links exceptions to active OTel spans. Already covered by the open feat(exception-event-metadata): require trace context on exception events #123.
  • posthog-js#5300 — a new click-id parameter. No spec enumerates click ids contractually (only an "e.g." list), so nothing drifted.
  • posthog-ruby#302 — the Rails init block was missing a setter for an already-spec'd flag cache option. A framework-wrapper gap, not a contract change; worth deciding whether "every config option is reachable from every init form" should be a rule.
  • Still parked, unchanged from #119 and #124: posthog-python's unreleased capture-v1 series, and trace exemplars on flushed metric series (no metrics capability on main — still open in Add metrics capability spec #30).

Created with PostHog Desktop

🤖 Generated with Claude Code

posthog-js (@posthog/ai), posthog-python and posthog-go all shipped the same
behavior for the OpenTelemetry AI span processor: the active PostHog context is
stamped onto the AI spans it exports. Record the attribution contract — the wire
keys, the precedence order, the already-names-a-user carve-out, and the rule that
the live span is never mutated.

Proposed, applied and archived on this branch per AGENTS.md.

Generated-By: PostHog Desktop
Task-Id: d8d19349-0ed9-4718-adc6-359209e5c7f6
@turnipdabeets turnipdabeets self-assigned this Oct 20, 2026
…ope posthog-js to the client option

posthog-js reads the context only when PostHogSpanProcessor is given client, and resolves
context properties at span end, so in-place mutation visibility is platform-specific.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets marked this pull request as ready for review October 20, 2026 19:47
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 20, 2026 19:47

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

The code looks good; no qualifying findings. Verdict: correct.

@turnipdabeets
turnipdabeets merged commit b942ce1 into main Oct 21, 2026
7 checks passed
@turnipdabeets
turnipdabeets deleted the posthog/spec-ai-instrumentation-context branch October 21, 2026 14:26
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