Skip to content

feat(observability): opt-in capture of AI prompts and completions - #10220

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
feat/ai-content-capture
Jul 31, 2026
Merged

feat(observability): opt-in capture of AI prompts and completions#10220
loopover-orb[bot] merged 1 commit into
mainfrom
feat/ai-content-capture

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

PostHog's LLM Analytics Traces view shows a dash for Input message, Output message and Sentiment on every row in this project, because $ai_input and $ai_output_choices were deliberately never populated. That default was defensible, but it also made the entire content-dependent half of the product unreachable: the conversation view, sentiment, evaluations, LLM-as-judge and online evals all read the captured prompt and completion.

This populates them behind LOOPOVER_POSTHOG_AI_CONTENT, default OFF. An operator who has not opted in gets byte-identical behaviour to before — upgrading an ORB must never start shipping private PR diffs and model completions to a vendor as a side effect of a version bump.

Design decisions worth reviewing

  • The gate lives in the capture function, not at the call sites. ai.ts always passes the content and posthog.ts alone decides whether to emit it. A future call site therefore cannot forget the check — the failure mode of a per-call-site gate is exactly the silent leak this feature must not have.
  • Content reuses the existing redaction, not a parallel path. It rides in the normal properties bag, so before_sendscrubPostHogEventscrubRecord applies the same credential/JWT/query-string scrubbing it applies to every other field. A test asserts a leaked PAT in a prompt comes out [redacted].
  • Bounded, and marked when bounded. Each message is capped (default 10,000 chars) and suffixed …[truncated], so a clipped prompt is never mistaken for the whole one. A review prompt carries up to 120k chars of diff plus a 240k-char context budget — uncapped, these events would exceed PostHog's payload limits and be dropped wholesale, which is a worse outcome than truncation. A non-numeric, zero, negative or fractional override falls back to the default rather than disabling the cap.
  • Images are dropped, not encoded. Reuses contentText, the same projection the subscription CLIs already apply when building their stdin prompt — so what is captured is what the provider was actually given, and no base64 blob lands in a telemetry event.
  • Embeddings and chat share one property shape. An embedding has no messages, so its raw texts are reported under a synthetic user role; it has no completion, so $ai_output_choices is omitted rather than emitted empty.
  • The failure path captures the prompt and no completion. A claude_stalled_no_output or a context-length rejection is only diagnosable against the input that produced it.

Closes #10218

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 specifically:

  • npm run selfhost:env-reference — regenerated and committed; two new env.* reads under src/selfhost/** land in apps/loopover-ui/src/lib/selfhost-env-reference.ts. selfhost:env-reference:check passes.
  • npm run dead-exports:check — green. The two resolver helpers are module-local on purpose: exporting them with no consumer outside the file is precisely what put dead-exports:check red on a clean main in fix(ci): unbreak main — drop the unused MCP_SERVER_NAME export #10195, and they are covered through initPostHog instead.
  • prettier --check on the changed MDX and the generated reference — clean.

If any required check was skipped, explain why:

  • Coverage measured scoped to the two changed source files: 100% of the changed lines AND branches, verified line-by-line against lcov rather than read off a summary percentage.
  • The unchecked commands cover untouched surfaces (no workflow, binding, OpenAPI schema or MCP manifest change here) and are left to CI.

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.

This is the change on this page that most deserves scrutiny, so it is stated plainly rather than buried: with the flag on, prompts contain the PR diff and the assembled repo context, and on a private repository that is private source code leaving the operator's infrastructure. The redaction pass removes credential shapes, not source code — the docs say so in those words rather than implying the scrub makes it safe. The default is off, the flag is a deliberate operator action, and nothing about the emitted event changes until it is set.

UI Evidence

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

Notes

Tests cover both states throughout rather than only the new one: content present and absent, each field omitted independently, at-cap and over-cap, the flag off with content supplied, images dropped, embeddings, and the failure path. The existing "never carries prompt/response content" assertion was flipped to cover both states rather than deleted — it now passes content in and asserts it does not appear, which is a strictly stronger check than the original.

Token usage is a separate root cause and is not addressed here — see #10211.

PostHog's LLM Analytics conversation view, sentiment, evaluations and LLM-judge
surfaces all read $ai_input/$ai_output_choices, which were deliberately never
populated -- so every trace in the project showed a dash for input, output and
sentiment, and the whole content-dependent half of the product was unavailable.

Populate them behind LOOPOVER_POSTHOG_AI_CONTENT, default OFF. Upgrading an ORB
must never start shipping private PR diffs and model completions to a vendor as
a side effect of a version bump, so an operator who has not opted in is
byte-identical to before.

The gate lives in the capture function rather than at each call site: ai.ts
always passes the content and posthog.ts alone decides, so a future call site
cannot forget it. Captured content passes through the same before_send scrub as
every other field, not a second weaker path. Each message is capped at 10,000
chars (LOOPOVER_POSTHOG_AI_CONTENT_MAX_CHARS) and marked when truncated -- a
review prompt can exceed 300k chars and would otherwise blow past PostHog's
payload limits and be dropped wholesale; a non-numeric, zero or negative
override falls back to the default rather than disabling the cap.

An image block is dropped rather than base64-encoded into the event, reusing the
same projection the subscription CLIs already apply when building their stdin
prompt. Embedding requests report their raw texts under a synthetic user role so
one property shape covers both request kinds. The failure path captures the
prompt and no completion -- a claude_stalled_no_output or a context-length
rejection is only diagnosable against the input that produced it.

The self-hosting doc promised metadata-only unconditionally and would have become
false; it now documents the opt-in, the truncation, and what enabling it actually
sends. The absence-assertion test now covers BOTH states rather than being
deleted.

Closes #10218
@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.

@JSONbored JSONbored self-assigned this Jul 31, 2026
@cloudflare-workers-and-pages

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 0eb9721 Commit Preview URL

Branch Preview URL
Jul 31 2026, 12:54 PM

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 5.27kB (0.07%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.96MB 5.27kB (0.07%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-C2-Aszpp.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-CyjDT9hL.js (New) 956.91kB 956.91kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-DZQ00HcP.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-DjEsHWMX.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BQe05-zU.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-BUL8e-m9.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/self-hosting-operations-CM6zd6Y6.js (New) 141.21kB 141.21kB 100.0% 🚀
assets/self-hosting-configuration-DsacuqE_.js (New) 118.83kB 118.83kB 100.0% 🚀
assets/maintainer-panel-D7MHKqyB.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/routes-Bg_0HEXp.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/owner-panel-BZ5J1AP1.js (New) 28.42kB 28.42kB 100.0% 🚀
assets/app-BSGpucOH.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-Cjc1GJpM.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-DggtGiPX.js (New) 23.44kB 23.44kB 100.0% 🚀
assets/miner-panel-KSbQEHD4.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-BuZZiXxq.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-CUdFR-VK.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-Cra0WzS-.js (New) 15.68kB 15.68kB 100.0% 🚀
assets/playground-panel-MsnmfVlx.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-D16WWZL0.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-DrOrRwl3.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-3s_vo92s.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-D0SROI7V.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-CyGggeJ4.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/proof._owner._repo-_sD9Aka-.js (New) 7.11kB 7.11kB 100.0% 🚀
assets/commands-panel-DvcdJzOb.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-Crd-cQlJ.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/docs-nav-syXhWfUf.js (New) 6.19kB 6.19kB 100.0% 🚀
assets/digest-panel-uSlKSN8V.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-CVeaqAE7.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-CdyE3YZ2.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-BXYNO5Ma.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-CToklyX0.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-CUb3kry6.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-Cz_dBlGD.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-Cw14UVrs.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-CgTooCA_.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-BoD5QTJW.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-rw1cszYA.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BN4mdrCZ.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-C-uqeKsk.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-CHSFsQQ8.js (New) 554 bytes 554 bytes 100.0% 🚀
assets/app.maintainer-BO8gjz25.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-CHtrk4tU.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-Cf1D82xy.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-C4rgYkym.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-ky8V7krG.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-C9chV8l4.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-Clk4qFs4.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-CNDCxxiI.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-BCazP5T3.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-DuGYGrkc.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-sl6M51hn.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-G0tf_T2g.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-mhmca2Y2.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-DSeFd94G.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-Dp9xdMVj.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-flQ8jGsi.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-CJspkXsM.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-CW_VRq59.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-EYkH8spU.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-CL-34Gak.js (Deleted) -956.91kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-BBMZyuv5.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-B0m7TGP_.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-CjRvc1Wl.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-gHM-vD8t.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/self-hosting-operations-DHg80WDa.js (Deleted) -136.08kB 0 bytes -100.0% 🗑️
assets/self-hosting-configuration-dEuu3-gg.js (Deleted) -118.7kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-BdZaSggc.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/routes-SFRHtKe1.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-DoPR5tqm.js (Deleted) -28.42kB 0 bytes -100.0% 🗑️
assets/app-BK9IH_Yt.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-Bnof2WdV.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-Ccw3ll6X.js (Deleted) -23.44kB 0 bytes -100.0% 🗑️
assets/miner-panel-Bn9kPN0Z.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-D4AOiwMo.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-lH3aAQNn.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-KQwdercn.js (Deleted) -15.68kB 0 bytes -100.0% 🗑️
assets/playground-panel-Czp1aVIb.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-Dar9KtCs.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-CRiOwcs-.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-CJbCkq7_.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-DLUoxWxu.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-DEU0pPAK.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/proof._owner._repo-CKFjpkKE.js (Deleted) -7.11kB 0 bytes -100.0% 🗑️
assets/commands-panel-Cuuczjgo.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-wXDz7Qpg.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/docs-nav-C49mNMEX.js (Deleted) -6.19kB 0 bytes -100.0% 🗑️
assets/digest-panel-CZyU55T0.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-DMgyVXvI.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-BK4Muhcc.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-D-Aryqo7.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-dMdcPZ2o.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-lOqDXh7w.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-CgpmFjX7.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-Rl_mg1ze.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-BnZUDdjD.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-DanE0kri.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-D3RgYqO4.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-DwspPsn5.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-C1Mb4xbl.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-CKwE6t66.js (Deleted) -554 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-BwLkkDGm.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-BtpI79cM.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-uIKueTqS.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-DnQDbUaw.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-BDK3-v9-.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-6i1Y4yhG.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-kHlEDhgn.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-Cje5J1fG.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-CVuvmfaq.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CAd4FTYF.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-VoTdj4Ki.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-DLfyJV8R.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-lJWtW84f.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-DxWnLVoA.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-CjtdNyLg.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-CaHoF2NS.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-sm-2Wzh7.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-COpZTflA.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.35%. Comparing base (0c2c845) to head (0eb9721).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10220      +/-   ##
==========================================
- Coverage   92.22%   91.35%   -0.87%     
==========================================
  Files         937      937              
  Lines      114334   114355      +21     
  Branches    27645    27651       +6     
==========================================
- Hits       105440   104467     -973     
- Misses       7588     8777    +1189     
+ Partials     1306     1111     -195     
Flag Coverage Δ
backend 94.12% <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/ai.ts 98.45% <100.00%> (+<0.01%) ⬆️
src/selfhost/posthog.ts 99.50% <100.00%> (+0.05%) ⬆️

... and 3 files with indirect coverage changes

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

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 07a43da into main Jul 31, 2026
12 checks passed
@loopover-orb
loopover-orb Bot deleted the feat/ai-content-capture branch July 31, 2026 13:17
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): PostHog's conversation, sentiment and evaluation views are empty — $ai_input/$ai_output_choices are never populated

1 participant