Skip to content

feat(observability): name AI traces in PostHog with a $ai_trace envelope - #10223

Merged
JSONbored merged 1 commit into
mainfrom
feat/ai-trace-envelope
Jul 31, 2026
Merged

feat(observability): name AI traces in PostHog with a $ai_trace envelope#10223
JSONbored merged 1 commit into
mainfrom
feat/ai-trace-envelope

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

The Trace name column is empty for every row in PostHog's Traces view, because this project emits no $ai_trace event at all.

PostHog derives the trace row itself from the generations underneath — which is why latency and cost populate — but the name comes from a trace-level event. Confirmed upstream in PostHog/posthog#33179: an $ai_span_name set on a generation does not populate that column. So every trace read as an anonymous id, with no way to tell a gate review from an embedding batch from a summary at a glance.

The envelope is emitted from withReviewPipelineSpan, which already wraps a whole review and carries the repo, the PR number, and the ambient OTel trace id that #10187 made the generations group by. It runs inside the OTel span, because that trace id only exists once the span is open — outside it there is nothing yet to name.

The two guards, both load-bearing

  • Outermost only. withReviewPipelineSpan is called from several sites in src/queue/processors.ts and they can nest, while PostHog expects one $ai_trace per trace id. A depth counter means inner calls contribute nothing, and the name that survives is the whole operation rather than whichever leaf happened to finish last.
  • Only when generations exist. Not every pipeline span wraps an AI call — the gate does not. Emitting unconditionally would manufacture trace rows with zero generations under them, which reads worse than an unnamed trace. capturePostHogAiGeneration marks its trace as having real work; the envelope is skipped otherwise.

It is a transparent pass-through when PostHog is off or there is no ambient trace, so it never changes what the wrapped work does or what it throws — the callback's return value and exception propagate untouched.

Closes #10221

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (e.g. Closes #123) — a linked open issue is required for every contributor PR.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Also run and green, because this change touches their inputs:

  • npm run dead-exports:check — green. POSTHOG_AI_TRACE_EVENT is referenced from the tests and the docs drift guard, following POSTHOG_MONITOR_HEARTBEAT_EVENT's existing pattern.
  • prettier --check on the changed MDX — clean.

If any required check was skipped, explain why:

  • Coverage measured scoped to the two changed source files across selfhost-posthog + selfhost-otel (the suite that already drives withReviewPipelineSpan): 100% of the changed lines AND branches, verified line-by-line against lcov.
  • The unchecked commands cover untouched surfaces and are left to CI.

Tests added

Nine cases, covering every branch rather than the happy path: pass-through with PostHog off; pass-through with no ambient trace; the envelope claiming the same trace id its generations carry; a pipeline span with no AI calls getting no envelope; nested spans emitting exactly one, named by the outermost; a throwing callback marked errored and rethrown; a non-Error thrown value and a bounded 500-char message; the repo group omitted when there is no repo; and a second span reusing the same trace id starting from a clean slate rather than inheriting the first's generation count.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

The envelope carries only the span name, timing, an error flag and the already-processed operational context — the same metadata-only posture as $ai_generation. It reuses operationalProperties and repoGroup, so under the shared central key the repo is HMAC-anonymized and fail-closed exactly as every other event on this path. No prompt, completion or trace state is attached.

UI Evidence

Not applicable — no visible UI or frontend change. The docs change is a FeatureRow entry on the existing self-hosting operations page, pinned by a drift test.

Notes

The bookkeeping map is keyed by trace id and deleted when depth returns to zero, in a finally, so a throwing span cannot leak an entry. A test asserts a later span over the same trace id starts clean rather than inheriting a stale generation count.

captureAiTraceEnvelope takes the client that was active when the span opened rather than re-reading the module binding in the finally — which also means it needs no second, unreachable off-switch check of its own.

@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 5785282 Commit Preview URL

Branch Preview URL
Jul 31 2026, 01:49 PM

@JSONbored JSONbored self-assigned this Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 277 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.96MB 277 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-CIieCRuH.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-Di1bIY6z.js (New) 957.42kB 957.42kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-Cn9a1RNl.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-OEPCe3DH.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BjSS6pG3.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-DzITNHam.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/self-hosting-operations-IyOi5YDh.js (New) 141.49kB 141.49kB 100.0% 🚀
assets/maintainer-panel-OLPRR35i.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/routes-DM6pbVK1.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/owner-panel-BH1LFTdl.js (New) 28.42kB 28.42kB 100.0% 🚀
assets/app-BhyyU3aU.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-BTiI-Jtq.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-CzgZ7MMj.js (New) 23.44kB 23.44kB 100.0% 🚀
assets/miner-panel-e6ucNE-L.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-D52fBw-K.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-BLbYK4HQ.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-DGaXb_0x.js (New) 15.68kB 15.68kB 100.0% 🚀
assets/playground-panel-8MSKF8ZR.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-D_d-5mMG.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-BHrpBZgO.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-DLwy9Q98.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-DMVEbp5L.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DEFFNO_8.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/proof._owner._repo-B0o_OVeC.js (New) 7.11kB 7.11kB 100.0% 🚀
assets/commands-panel-Bfa3RN13.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-BtiOHxSe.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/docs-nav-BNhJ7owL.js (New) 6.19kB 6.19kB 100.0% 🚀
assets/digest-panel-B8P7s3Rf.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-Ljr9htK6.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-CZULnuJd.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-BEKXdGS6.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-D4ofCz1d.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-D55VSI2U.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-hJIQVVrn.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-DS7n1wNy.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-BnB70zAA.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-C3raqldu.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-DN9E8m5G.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-CURKVKRl.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-3Gnq_Zg2.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-oB3KAzDI.js (New) 554 bytes 554 bytes 100.0% 🚀
assets/app.maintainer-B0mIoG_5.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-DrN1rwZc.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BsAVXVs9.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BEuGrqot.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-fXuk1iye.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-CsyngSJ_.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DZhJW9ij.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-BK2SsPul.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CAPNfqKF.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-BuqUDrrn.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-AjSfVy-E.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-Ck2StF9O.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-ADOqzVzM.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-D2v_naKW.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-D31ZcHLu.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-aCUTkoOE.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-C38XYJxv.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-CzSi3mzM.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-BTNdIh_c.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-CkArEOKE.js (Deleted) -957.42kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CiqZA8uo.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-C1RRbk6J.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-CrOdAMWi.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-DgoRxMxI.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/self-hosting-operations-CM6zd6Y6.js (Deleted) -141.21kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-VwW-F5CH.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/routes-BB4gyhGK.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-Cb-Udab-.js (Deleted) -28.42kB 0 bytes -100.0% 🗑️
assets/app-CjyyzEtW.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CAIToxac.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-B3vbPlea.js (Deleted) -23.44kB 0 bytes -100.0% 🗑️
assets/miner-panel-xVjV2D7u.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-DXgkVS9g.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-9466eZM6.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-C0DEiHDY.js (Deleted) -15.68kB 0 bytes -100.0% 🗑️
assets/playground-panel-B5fvNCOY.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-DnA-kPFa.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-cYbfL9rM.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DE2x9eiM.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-iD_B-Wxd.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-CntuqvQo.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/proof._owner._repo-C65_Owga.js (Deleted) -7.11kB 0 bytes -100.0% 🗑️
assets/commands-panel-BoL5jBW9.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BxCYRv6K.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/docs-nav-BbJbXU4y.js (Deleted) -6.19kB 0 bytes -100.0% 🗑️
assets/digest-panel-BxAczt-w.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BsGe3hsE.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-IXjcqBuX.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-P-MpviGH.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BzR74NG-.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-CRdVgqe0.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-DX7hHlE7.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-4DnpPBAj.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-DpFsqvhF.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-Dvjp0Ae4.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-BLpsRgY3.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-GlCuw4LA.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-Da-yvBv0.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-D_X4npt1.js (Deleted) -554 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-R1UaFyKT.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-B7SohvOF.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-Bc68EkU5.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-cQURDgd0.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-CVzGBsNs.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-BMKv6F2U.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-BJsAoNl5.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-C8pjAnU7.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-CCvbTjOt.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CCv6Wa3H.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-QN2bdZaB.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-yjYlzo6x.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-klUc1vef.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-C9srKolI.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-B5ncYirm.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-o8VCtRni.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-BKSkUbUO.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-AcQZHHv5.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.38%. Comparing base (ff28627) to head (5785282).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10223      +/-   ##
==========================================
- Coverage   92.25%   91.38%   -0.87%     
==========================================
  Files         938      938              
  Lines      114659   114688      +29     
  Branches    27680    27689       +9     
==========================================
- Hits       105774   104809     -965     
- Misses       7580     8769    +1189     
+ Partials     1305     1110     -195     
Flag Coverage Δ
backend 94.13% <100.00%> (-1.55%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/selfhost/posthog.ts 99.57% <100.00%> (+0.05%) ⬆️
src/selfhost/review-tracing.ts 100.00% <100.00%> (ø)

... and 3 files with indirect coverage changes

PostHog's Traces view takes a trace's NAME from a trace-level $ai_trace event,
not from any property on the generations underneath it -- confirmed upstream in
PostHog/posthog#33179, where an $ai_span_name set on a generation does not
populate that column. This project emitted only generations, so every trace read
as an anonymous id with no way to tell a gate review from an embedding batch.

Emit the envelope from withReviewPipelineSpan, which already wraps a whole review
and carries the repo, PR and the ambient OTel trace id the generations group by.
It runs INSIDE the OTel span, because that id only exists once the span is open.

Two guards, both load-bearing:

- Outermost only. withReviewPipelineSpan is called from several sites that can
  nest, and PostHog expects one $ai_trace per trace. A depth counter means inner
  calls contribute nothing and the surviving name is the whole operation rather
  than whichever leaf finished last.
- Only when generations exist. Not every pipeline span wraps an AI call -- the
  gate does not. Emitting unconditionally would manufacture trace rows with
  nothing under them, which reads worse than an unnamed trace.

A pass-through when PostHog is off or no ambient trace exists, so it never
changes what the wrapped work does or what it throws.

Closes #10221
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored
JSONbored merged commit f759c0e into main Jul 31, 2026
6 of 7 checks passed
@JSONbored
JSONbored deleted the feat/ai-trace-envelope branch July 31, 2026 13:48
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.

ai(observability): the Trace name column is empty — no $ai_trace event is ever emitted

1 participant