Repository navigation
Conversation
…1528) Agent-driven search has no web-style click signal, so ranking quality is unmeasurable. This adds the stateless funnel: every search JSON response carries a client-minted search_id ULID, compact hits carry a ready-made "explain" command (with --repo for cross-repo checkpoint hits), and two flat telemetry events — cli_search_performed (search_id + hashed query + counts) and cli_checkpoint_explained (opaque id + resolution path) — let PostHog assemble search→explain conversion, reformulation, and which results agents actually fetch. Privacy: raw query text is never sent (truncated SHA-256 only), no impression tuples, no session ids, no local correlation state. Both events are double-gated on settings.Telemetry and ENTIRE_TELEMETRY_OPTOUT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZSB7CJP1E44YDD43VD1EABT
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6393e08. Configure here.
| } | ||
| switch len(matches) { | ||
| case 1: | ||
| emitCheckpointExplained(ctx, string(matches[0]), telemetry.ExplainSourceExport) |
There was a problem hiding this comment.
False explain telemetry from tokens
Medium Severity
emitCheckpointExplained is hooked inside resolveExplainCheckpointID, which entire checkpoint tokens also calls. A successful tokens lookup therefore emits cli_checkpoint_explained with source export, inflating the search→explain funnel with non-explain traffic (and twice when --compare resolves two IDs).
Reviewed by Cursor Bugbot for commit 6393e08. Configure here.
| } | ||
| switch len(matches) { | ||
| case 1: | ||
| emitCheckpointExplained(ctx, string(matches[0]), telemetry.ExplainSourceExport) |
There was a problem hiding this comment.
Export commit path skips telemetry
Medium Severity
In the export resolver, emitCheckpointExplained runs only on checkpoint-prefix matches. Successful --commit resolution and the positional commit-ref fallback return without emitting, so JSON/transcript/raw-transcript explains resolved via commit never produce cli_checkpoint_explained.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 6393e08. Configure here.
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end correlation for the agent-driven search → explain funnel by minting a client-side search_id, enriching compact search hits with copy/pasteable explain hints (including cross-repo --repo when needed), and emitting two privacy-preserving telemetry events to measure result relevance without storing any local correlation state.
Changes:
- Add
search_idto search JSON envelopes (--jsonand--compact) and return served-count from JSON writers for telemetry. - Add per-hit
explainhints to--compactresults (including cross-repo--repo owner/namefor checkpoint hits). - Add new telemetry events
cli_search_performedandcli_checkpoint_explained, with corresponding tests and documentation updates.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/security-and-privacy.md | Documents the two new relevance telemetry events and their privacy constraints. |
| cmd/entire/cli/telemetry/search.go | Implements payload building + detached tracking for cli_search_performed and cli_checkpoint_explained. |
| cmd/entire/cli/telemetry/search_test.go | Adds payload-shape/privacy tests for the new telemetry events. |
| cmd/entire/cli/search_telemetry.go | Adds search_id minting, query hashing, and CLI-side gated emit helpers. |
| cmd/entire/cli/search_telemetry_test.go | Tests query hash normalization/truncation and ULID-shaped search_id. |
| cmd/entire/cli/search_cmd.go | Threads search_id into JSON envelopes, adds compact per-hit explain hints, and emits search telemetry. |
| cmd/entire/cli/search_cmd_test.go | Expands JSON writer tests for search_id, served-count, and explain hints (same-repo/cross-repo/no-hint types). |
| cmd/entire/cli/explain.go | Emits cli_checkpoint_explained on successful prose-mode checkpoint ID resolution. |
| cmd/entire/cli/explain_repo.go | Emits cli_checkpoint_explained on successful cross-repo explain resolution. |
| cmd/entire/cli/explain_export.go | Emits cli_checkpoint_explained on successful export-mode checkpoint ID resolution. |
Suppressed comments (1)
cmd/entire/cli/search_cmd.go:1136
- Same issue as the full JSON envelope:
search_idis tagged withomitempty, so the key can disappear if the ID can't be minted. If agents depend on the field existing, prefer always emitting it and letting the value be empty only in the extreme failure case.
out := struct {
Results []compactSearchHit `json:"results"`
Total int `json:"total"`
Page int `json:"page"`
TotalPages int `json:"total_pages"`
Limit int `json:"limit"`
SearchID string `json:"search_id,omitempty"`
Counts *search.TypeCounts `json:"counts,omitempty"`
}{
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @@ -981,21 +996,23 @@ func writeSearchJSON(w io.Writer, resp *search.Response, limit, page int) error | |||
| Page int `json:"page"` | |||
| TotalPages int `json:"total_pages"` | |||
| Limit int `json:"limit"` | |||
| SearchID string `json:"search_id,omitempty"` | |||
| Counts *search.TypeCounts `json:"counts,omitempty"` | |||
| }{ | |||
resolveExplainCheckpointID is shared with `entire checkpoint tokens` (via loadCheckpointTokensReport), so emitting cli_checkpoint_explained inside it counted token lookups — twice under --compare — as search funnel clicks. Move the emit into the two explain-only callers (runExplainCheckpointJSON, runExplainStreamTranscript), which also covers the export path's commit-SHA fallback the old placement missed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZSBWVH8ERZRADEXKMB30FGY
Time-proximity correlation can't prove an explained checkpoint came from a search. Since the CLI authors the click command, embed the link in it: compact hints now end with --search-id <search_id>:<rank>, and explain gains that hidden flag, threading the validated token through the context (same pattern as withCrossRepoRead) into cli_checkpoint_explained as search_id + rank. The PostHog funnel joins exactly on search_id and gets CTR-by-rank client-side; token-less explains still fall back to time-window correlation, and their share measures hint compliance. The flag value is agent-controlled text, so it is strictly validated (26-char ULID, positive rank) and silently dropped when malformed — attribution metadata must never fail an explain or leak junk into telemetry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KZT1WV38X3FAW2B0FX8H3NSQ
…T-1528) A checkpoint id resolves to repo/session/author via the API, so sending it raw in cli_checkpoint_explained violated the "no content identifiers in analytics" guarantee (trail 1019 high finding). Replace it with doc_ref, a truncated SHA-256 that is deterministic but content-opaque. Also drop query_hash from cli_search_performed: it overclaimed non-reversibility for what a later task will rework anyway, so stop emitting it now rather than carry a property nothing consumes.
…xplained tests TestBuildCheckpointExplainedPayload only checked for absence of checkpoint_id; the brief mandates the same forbidden set used for cli_search_performed (query, query_hash, impressions, results, session_id, checkpoint_id) on this event too. Apply it to the base test and its two search-attribution variants for consistent coverage.
…rch_performed (ENT-1528) Adds Outcome (ok/error_request), QueryLength, honest server Total (resp.Total, never served/len(results)), ZeroResults, AllRepos, and per-filter booleans to the cli_search_performed event. Writers (writeSearchJSON/writeSearchCompactJSON) now return the paginateSearchResults-normalized limit/page alongside served count, so --limit 0 reports the real page size (10) instead of 0. RunE emits on request failure too (JSON-mode only, zero counts) via the shared buildSearchPerformedEvent helper, so a failed search shows up in the funnel instead of vanishing silently.
agent-help omits hidden flags, so agents that validate their commands against agent-help output strip --search-id before running them, silently zeroing hint compliance. Keep the flag registered and described, just drop the hide.


Summary
Agent-driven search (~80% of expected search traffic) has no web-style click signal, so we can't tell whether the results we return are relevant. This closes the loop statelessly — no local correlation files (ENT-1528, full plan + updates in the ticket comments).
search_idin every search JSON envelope (--jsonand--compact): a client-minted ULID — the server response carries no request id and the CLI merges per-cell responses, so it can't come from the server. Also the future seam for server-side impression logging in query-serve.explainhint in--compactoutput:"explain": "entire checkpoint explain <id> [--repo <org>/<name>] --search-id <search_id>:<rank>". The--repoform covers cross-repo checkpoint hits (rides feat(explain): cross-repo checkpoint explain via the entire-api cell (ENT-1523) #1942); repo/pr/session rows get no hint since explain can't resolve them. The trailing token is the deterministic link: agents copy the hint verbatim, like a web SERP link carrying its tracking params.cli_search_performedevent:search_id,query_hash(truncated SHA-256 of the normalized query — raw text never sent), mode, result_count, total, page, limit.cli_checkpoint_explainedevent: opaque checkpoint id + which resolution path fired (prose/export/cross_repo), hooked once at each of explain's three id-resolution success points. When the invocation carried--search-id(hidden flag, threaded via context — same pattern aswithCrossRepoRead), the event also carriessearch_id+rank.Analysis unlocked in PostHog: exact search→explain funnel joined on
search_id, CTR-by-rank, reformulation/abandonment chains, and hint compliance (share of explain events carryingsearch_id; token-less explains fall back to time-proximity correlation).Privacy
No query text, prompts, file paths, repo names, or session ids in any event; nothing written locally.
search_id/rankare opaque CLI-minted values, and the--search-idflag value is strictly validated (26-char ULID, positive rank) with malformed tokens silently dropped — attribution can never fail an explain or leak free-form flag text into telemetry. Both events follow the existingBuild*Payload/Track*pattern through the detached__send_analyticsspawn and are double-gated:settings.Telemetry == trueat the call site +ENTIRE_TELEMETRY_OPTOUTinsideTrack*. Documented indocs/security-and-privacy.md.Review feedback addressed
resolveExplainCheckpointID(shared withentire checkpoint tokens) into the explain-only callers, so token lookups don't pollute the funnel.Test plan
mise run fmt && mise run lint— cleanmise run test:ci(unit + integration + Vogon/roger-roger E2E canary) — all passed, rerun after every changesearch_id/rankpresent only with attribution), query-hash normalization, ULID search_id,search_idin both envelopes, same-repo vs cross-repo hints with rank tokens, absolute rank across pages, no hint on repo/pr rows,--search-idtoken validation (junk/short/bad-rank cases), context round-trip🤖 Generated with Claude Code
Design update (2026-08-16) — v2 amendments incoming
Following the trail 1019 review findings and an adversarial pass on the wider feedback-loop design (see ENT-1528 comments), this PR will be amended. The mechanism (client-minted
search_id, stateless hint tokens, search→explain funnel) is unchanged; the following correct privacy and soundness defects:Privacy (trail high finding):
cli_checkpoint_explainedwill carrydoc_ref— a truncated SHA-256 of the checkpoint id — instead of the rawcheckpoint_id. A checkpoint id resolves via our own API to repo/session/author/prompt, contradicting the security doc's "no repository identifiers" guarantee. The hash is deterministic so server-side analysis can still join it against candidate ids fromsearch_eventrecords (the search service is gaining that record in a sibling PR); nothing in PostHog alone can resolve it to content.query_hashis removed entirely (not HMAC'd): a truncated unsalted SHA-256 of low-entropy natural language is dictionary-confirmable, and no v1 analysis consumes it — reformulation chains group by machine + time window; query-content analysis lives server-side. The event keepsquery_lengthonly.Soundness (trail medium/low findings):
search_idis minted before the request and sent asX-Entire-Search-Id(plusX-Entire-Client: <surface>/<version>), making it the real server-side seam — each cell will log it in itssearch_eventrecord.--search-idis un-hidden soagent-helpadvertises it (hidden flags are stripped by agents that validate commands against agent-help, which would silently zero hint compliance).cli_checkpoint_explainedfires after a successful render, not at id resolution — failures no longer count as clicks.page/limit(matching the envelope and hint ranks) and bothfetched_countand the server's truetotal.cli_search_performedfires on the error path too (outcome: error_*) and gainszero_results,reranked,total_ms,all_repos, and filter-presence booleans — all already on the wire, previously discarded.--jsonoutput as well (a large share of agent traffic doesn't use--compact; those explains could otherwise only ever join by time proximity).Tests (trail finding): a hint→explain round-trip contract test (generated hint parsed by the real command's flag set) and settings-gate tests for both emit helpers, including the nil-settings case.
Analysis-layer rules that go with this (documented in the design): funnel joins enforce a ≤1h validity window on the ULID timestamp (stale hints replayed from transcripts must not count), and server-minted-id records are excluded from funnel denominators.