Part of epic #8286 (Phase 3 — AI observability).
Problem
The Trace name column is empty for every row in PostHog's Traces view, because the project emits no $ai_trace event at all — only $ai_generation and $ai_embedding.
PostHog derives the trace row itself from the generations (which is why latency and cost populate), but the name comes from a trace-level event. This is confirmed upstream: PostHog/posthog#33179 — $ai_span_name set on a generation does not populate the Traces tab's Trace name column; it needs the trace event.
So every trace reads as an anonymous id, and there is no way to tell a gate review from an embedding batch from a summary at a glance.
What a trace envelope would carry
withReviewPipelineSpan (src/selfhost/review-tracing.ts) already wraps a whole review and is the natural chokepoint — it has the name, repo, PR number and the ambient OTel trace id that #10187 made the generations group by. A $ai_trace emitted as that span completes would carry $ai_trace_id, $ai_span_name, $ai_latency, and $ai_is_error/$ai_error.
The two things that make this non-trivial
- Duplicate emission.
withReviewPipelineSpan is called from ~7 sites in src/queue/processors.ts, and they can nest. PostHog expects one $ai_trace per trace id, so the envelope must be emitted once — by the outermost span only — not once per nested call.
- Empty traces. Not every pipeline span wraps an AI call (the gate, for instance). Emitting unconditionally would create trace rows with zero generations, which is worse than an unnamed trace. The envelope should only be emitted when at least one generation was actually captured under that trace id.
Both point at the same shape: track, per trace id, whether any generation was captured, and emit the envelope on outermost-span exit only if so.
Deliverables
Related
Part of epic #8286 (Phase 3 — AI observability).
Problem
The Trace name column is empty for every row in PostHog's Traces view, because the project emits no
$ai_traceevent at all — only$ai_generationand$ai_embedding.PostHog derives the trace row itself from the generations (which is why latency and cost populate), but the name comes from a trace-level event. This is confirmed upstream: PostHog/posthog#33179 —
$ai_span_nameset on a generation does not populate the Traces tab's Trace name column; it needs the trace event.So every trace reads as an anonymous id, and there is no way to tell a gate review from an embedding batch from a summary at a glance.
What a trace envelope would carry
withReviewPipelineSpan(src/selfhost/review-tracing.ts) already wraps a whole review and is the natural chokepoint — it has the name, repo, PR number and the ambient OTel trace id that #10187 made the generations group by. A$ai_traceemitted as that span completes would carry$ai_trace_id,$ai_span_name,$ai_latency, and$ai_is_error/$ai_error.The two things that make this non-trivial
withReviewPipelineSpanis called from ~7 sites insrc/queue/processors.ts, and they can nest. PostHog expects one$ai_traceper trace id, so the envelope must be emitted once — by the outermost span only — not once per nested call.Both point at the same shape: track, per trace id, whether any generation was captured, and emit the envelope on outermost-span exit only if so.
Deliverables
$ai_traceemitted once per trace, named from the pipeline span.$ai_session_idconsidered while here — it is the documented way to group related traces, and a review with retries is exactly that.Related
$ai_input/$ai_output_choices;$ai_input_state/$ai_output_stateare the trace-level equivalents and should follow the same opt-in gate.--tools "", so they legitimately have none.