feat: opt-in OTel trace/span IDs on regular capture() via capture_trace_context - #894
Conversation
…ce_context Attach the active OpenTelemetry span's trace_id/span_id as `$trace_id`/`$span_id` properties on events captured with `capture()`/`capture_ai()`, behind an opt-in `capture_trace_context` client option. Reuses the existing `_get_current_otel_span_properties()` helper already used by `capture_exception()`. Explicit `$trace_id`/`$span_id` in properties win; default is off, so no behavior change for existing clients. Generated-By: PostHog Desktop Task-Id: 44c7be3e-4938-4f9a-bb80-89fd9d74db6d
posthog-python Compliance ReportDate: 2026-08-25 14:17:02 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
The initial snapshot was generated under Python 3.12, but CI runs the check under 3.11.11 and griffe's AST unparsing differs across versions (self._ph_client -> (self._ph_client), Callable[..., Any] -> (Callable[..., Any])). Regenerated in a pinned 3.11.11 uv env matching CI (uv sync --extra dev), so `make public_api_check` now passes there. No code changes. Generated-By: PostHog Desktop Task-Id: 44c7be3e-4938-4f9a-bb80-89fd9d74db6d
Prompt To Fix All With AI### Issue 1
posthog/client.py:692
**Constructor arguments shift positionally**
If an existing external caller passes `code_variables_mask_patterns` or any later `Client`/`Posthog` option positionally, inserting `capture_trace_context` here shifts every subsequent value to the wrong setting. This can unintentionally enable trace-property capture and misconfigure masking, exception capture, capture mode, compression, or initialization behavior.
### Issue 2
posthog/test/test_client.py:649-682
**Trace tests omit capture_ai**
The feature explicitly covers both `capture()` and `capture_ai()`, but these tests exercise only `capture()`. Parameterizing the entrypoint would ensure a regression in the dedicated AI capture path cannot pass while breaking the newly documented trace-correlation behavior.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "chore: regenerate public API snapshot un..." | Re-trigger Greptile |
| enable_local_evaluation=True, | ||
| flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None, | ||
| capture_exception_code_variables=False, | ||
| capture_trace_context=False, |
There was a problem hiding this comment.
Constructor arguments shift positionally
If an existing external caller passes code_variables_mask_patterns or any later Client/Posthog option positionally, inserting capture_trace_context here shifts every subsequent value to the wrong setting. This can unintentionally enable trace-property capture and misconfigure masking, exception capture, capture mode, compression, or initialization behavior.
Knowledge Base Used: Client lifecycle and configuration
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/client.py
Line: 692
Comment:
**Constructor arguments shift positionally**
If an existing external caller passes `code_variables_mask_patterns` or any later `Client`/`Posthog` option positionally, inserting `capture_trace_context` here shifts every subsequent value to the wrong setting. This can unintentionally enable trace-property capture and misconfigure masking, exception capture, capture mode, compression, or initialization behavior.
**Knowledge Base Used:** [Client lifecycle and configuration](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-python/-/docs/client-lifecycle-and-configuration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| def test_capture_uses_current_otel_span_context_when_enabled( | ||
| self, | ||
| _, | ||
| context_trace_id, | ||
| context_span_id, | ||
| properties, | ||
| expected_trace_id, | ||
| expected_span_id, | ||
| ): | ||
| span_context = SpanContext( | ||
| trace_id=context_trace_id, | ||
| span_id=context_span_id, | ||
| is_remote=False, | ||
| trace_flags=TraceFlags.SAMPLED, | ||
| ) | ||
|
|
||
| with ( | ||
| mock.patch("posthog.client.batch_post") as mock_post, | ||
| use_span(NonRecordingSpan(span_context)), | ||
| ): | ||
| client = Client( | ||
| FAKE_TEST_API_KEY, sync_mode=True, capture_trace_context=True | ||
| ) | ||
| client.capture( | ||
| "test_event", distinct_id="distinct_id", properties=properties | ||
| ) | ||
|
|
||
| event = mock_post.call_args.kwargs["batch"][0] | ||
| if expected_trace_id is None: | ||
| self.assertNotIn("$trace_id", event["properties"]) | ||
| self.assertNotIn("$span_id", event["properties"]) | ||
| else: | ||
| self.assertEqual(event["properties"]["$trace_id"], expected_trace_id) | ||
| self.assertEqual(event["properties"]["$span_id"], expected_span_id) |
There was a problem hiding this comment.
The feature explicitly covers both capture() and capture_ai(), but these tests exercise only capture(). Parameterizing the entrypoint would ensure a regression in the dedicated AI capture path cannot pass while breaking the newly documented trace-correlation behavior.
Knowledge Base Used: Event capture and delivery
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/test/test_client.py
Line: 649-682
Comment:
**Trace tests omit capture_ai**
The feature explicitly covers both `capture()` and `capture_ai()`, but these tests exercise only `capture()`. Parameterizing the entrypoint would ensure a regression in the dedicated AI capture path cannot pass while breaking the newly documented trace-correlation behavior.
**Knowledge Base Used:** [Event capture and delivery](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-python/-/docs/event-capture-and-delivery.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
marandaneto
left a comment
There was a problem hiding this comment.
Automated advisory code review.
| enable_local_evaluation=True, | ||
| flag_definition_cache_provider: Optional[FlagDefinitionCacheProvider] = None, | ||
| capture_exception_code_variables=False, | ||
| capture_trace_context=False, |
There was a problem hiding this comment.
blocking: Preserve existing positional constructor arguments — Inserting capture_trace_context in the middle of the public, non-keyword-only Client signature shifts every later positional argument. Existing callers passing code_variables_mask_patterns or subsequent settings positionally will silently assign those values to the wrong options, potentially enabling trace capture and misconfiguring code-variable redaction despite the advertised opt-in default. Append the new argument instead so existing calls retain their meaning.
| properties = {**(properties or {}), **system_context()} | ||
|
|
||
| if self.capture_trace_context: | ||
| properties = {**_get_current_otel_span_properties(), **properties} |
There was a problem hiding this comment.
blocking: Super properties override explicit trace IDs — The documented “explicit properties win” guarantee does not hold when super_properties contains $trace_id or $span_id: _enqueue() merges super properties afterward with higher precedence, replacing both the current span and values passed directly to capture(). A reproduced event passed event-trace/event-span but sent super-trace/super-span, causing incorrect trace correlation.
There was a problem hiding this comment.
if thats intentional, thats ok
…eir slots Inserting the option next to capture_exception_code_variables shifted every later Client/Posthog parameter by one position, so a caller passing code_variables_mask_patterns (or anything after it) positionally would silently land on the wrong setting. Move it to the end of the public parameters, matching how secret_key and metrics were appended rather than grouped with their related options. Snapshot regenerated under Python 3.11.11 to match CI. Generated-By: PostHog Desktop Task-Id: 44c7be3e-4938-4f9a-bb80-89fd9d74db6d
The option is documented as applying to both capture() and capture_ai(), but the tests only exercised capture(). Parameterize the entrypoint so a future divergence of capture_ai from the shared _capture path breaks the documented trace-correlation behavior instead of passing silently. Verified by mutation: gating the injection on the analytics lane fails capture_ai_active_context. Generated-By: PostHog Desktop Task-Id: 44c7be3e-4938-4f9a-bb80-89fd9d74db6d
|
Both comments were correct, pushed fixes.
|
PR #894 merged without a changeset, so Sampo has nothing pending and the option won't be published. Adds the missing minor changeset for the already-merged capture_trace_context option. Generated-By: PostHog Desktop Task-Id: 44c7be3e-4938-4f9a-bb80-89fd9d74db6d
💡 Motivation and Context
Today
posthog-pythononly attaches OpenTelemetrytrace_id/span_idto$exceptionevents (capture_exception). Regularcapture()events carry no OTel trace context, so they can't be correlated with backend traces. This adds an opt-incapture_trace_contextclient option that generalizes the existing behavior tocapture()/capture_ai(), reusing the same_get_current_otel_span_properties()helper.$trace_id/$span_idpassed inpropertieswin over the OTel span.💚 How did you test it?
TDD — failing tests first, then implementation:
test_capture_uses_current_otel_span_context_when_enabled(active span, explicit-properties-win, invalid span)test_capture_does_not_attach_otel_span_context_by_defaultEnd-to-end: real
Client.capture()POST against a local mock ingestion endpoint, inside an active OTel span, dumping the server-side payload before vs after:📝 Checklist
If releasing new changes
sampo add— N/A, no release requested; changeset left to maintainer release flow🤖 Agent context
Autonomy: Human-driven (agent-assisted)
_get_current_otel_span_properties()rather than duplicating OTel logic. Explicit-properties-win precedence mirrorscapture_exception().Created with PostHog Desktop