Skip to content

feat(search): emit outcome telemetry for search invocations (ENT-1938) - #2130

Open
evisdren wants to merge 4 commits into
mainfrom
evis/ent-1938-cli-emit-outcome-telemetry-for-entire-search-invocations
Open

feat(search): emit outcome telemetry for search invocations (ENT-1938)#2130
evisdren wants to merge 4 commits into
mainfrom
evis/ent-1938-cli-emit-outcome-telemetry-for-entire-search-invocations

Conversation

@evisdren

@evisdren evisdren commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1146

Why

The tester audit (ENT-1938) found 88.5% of no-value search sessions failed to obtain a usable response at all, dominated by client-side auth/configuration failures that never reach a server. cli_command_executed records that entire search ran but not whether it worked.

What

New cli_search_completed PostHog event, emitted once per executed search request on entire search and entire checkpoint search (same command; both the checkpoint and --code paths):

  • success (bool), duration_ms
  • result_count and coverage_incomplete — success only; zero-result rate never counts failures, and a degraded success (failed regions, skipped repos, truncated index) is distinguishable from a genuine zero-result search
  • error_class — from typed errors only, never message matching: auth (not logged in, 401/403), cell_skip (auth.ErrNoCellForJurisdiction, client-side), region_unavailable (search.ErrCellUnavailable, gateway without query-serve — kept distinct from cell_skip per the ticket), repo_unavailable, network, server (5xx), http_other, other
  • plus command, mode (checkpoint/code), and the standard version/os/arch props

Plumbing needed to classify from types:

  • loginHintErr and the all-cells-skipped path previously replaced typed causes with fresh errors.New — a new hintError wrapper keeps the user-facing message byte-identical while leaving the chain matchable with errors.Is.
  • Semantic-search non-OK responses now return a typed search.HTTPStatusError (message wording unchanged); code search already returned api.HTTPError.

Content-free by construction: booleans, enums, counts, durations only — no query text, snippets, or repo names. Gated on the telemetry opt-in setting at the call site and ENTIRE_TELEMETRY_OPTOUT in the tracker, via the existing detached PostHog child.

Scope choices

  • Instrumented at the searcher seam (instrumentSemanticSearcher, searchAllCells), so every entry point emits by construction: one-shot commands, the TUI initial fetch, interactive re-searches, pagination, and the code tab. Each timer wraps only the request — TUI dwell time is never counted. TUI searches fire per submitted query, so volume stays one event per real search request.
  • Pre-request local failures (flag validation, not a git repo) emit nothing — the event measures search request outcomes; invocation counts stay on cli_command_executed.

Validation

  • New tests: payload shape (success/failure property omission), classifySearchError table over real error-chain shapes, hintError message/chain preservation, typed HTTPStatusError status code.
  • mise run fmt, mise run lint (0 issues), mise run test:ci (unit + integration + canary) all pass.

Closes ENT-1938.

🤖 Generated with Claude Code


Note

Low Risk
Search behavior and user-visible errors are unchanged; new telemetry is opt-in, best-effort, and non-blocking with no query or repo data in payloads.

Overview
Adds an opt-in cli_search_completed PostHog event so product can see whether searches succeed, not just that entire search ran. Each checkpoint (semantic) and --code request emits once right after the network fan-out finishes—before the interactive TUI—so duration_ms covers the API work only.

Payloads stay content-free: success, mode, command path, duration, result_count on success only, and error_class on failure (never both, so failures aren’t counted as zero-result searches). Emission respects telemetry settings and the existing detached analytics path.

Failure error_class values come from typed errors, not message parsing. Supporting changes: search.HTTPStatusError for non-OK semantic responses (same user-facing strings), a hintError wrapper so login/region hints keep errors.Is chains, and classifySemanticCells wrapping the all-skipped-region case with the underlying skip cause for cell_skip vs region_unavailable.

Reviewed by Cursor Bugbot for commit cd05c83. Configure here.

Add a cli_search_completed event carrying success, error_class,
result_count, and duration_ms for entire search and entire checkpoint
search (both the checkpoint and --code paths). Client-side failures —
auth, jurisdiction cell skips, gateway-without-query-serve — never reach
a server, so the CLI is the only place they can be measured.

Error classes come from typed errors, never message matching. To make
the chain classifiable: loginHintErr and the all-cells-skipped path now
preserve their typed cause behind the unchanged user-facing message
(hintError), and semantic-search non-OK responses return a typed
search.HTTPStatusError instead of an untyped fmt.Errorf.

Content-free by construction: booleans, enums, counts, durations only.
Gated on the telemetry opt-in setting and ENTIRE_TELEMETRY_OPTOUT via
the detached PostHog path. Interactive TUI re-searches are not emitted;
the initial fetch is measured before the TUI starts so duration never
includes TUI time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01M0Y4Z4KW8N1GK3TMA3KGV9ZW
@evisdren
evisdren requested a review from a team as a code owner August 26, 2026 04:24
Copilot AI lite review requested due to automatic review settings August 26, 2026 04:24

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, 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 cd05c83. Configure here.

Comment thread cmd/entire/cli/search_v4.go Outdated
// cause in the chain: the two "region" variants (gateway without
// query-serve vs. client-side jurisdiction skip) are different
// failures and telemetry classifies them from the typed cause.
lastErr = &hintError{msg: errNoRegionAvailable.Error(), errs: []error{errNoRegionAvailable, skipErr}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed skip causes misclassified

Medium Severity

When every cell is skipped and the causes mix auth.ErrNoCellForJurisdiction and search.ErrCellUnavailable, only the last cell’s error is kept in the hintError chain. classifySearchError then reports whichever class that last cause maps to, so the same multi-cell failure can emit cell_skip or region_unavailable depending on cell order—undermining the distinction this change is meant to preserve.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cd05c83. Configure here.

Copilot AI 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.

Pull request overview

This PR adds opt-in, content-free PostHog telemetry for search outcomes so the CLI can distinguish “search invoked” from “search succeeded/failed” (ENT-1938), without logging query text or results content. It threads typed errors through existing search paths so failures can be classified via errors.Is/As instead of message matching.

Changes:

  • Introduces a new cli_search_completed event emitted once per executed search request (semantic checkpoint search and --code search) with duration, success, and either result count or a coarse typed error class.
  • Adds search.HTTPStatusError to preserve existing user-facing error wording while exposing HTTP status for typed classification.
  • Adds a hintError wrapper to preserve user-facing hint strings while keeping underlying typed causes matchable for telemetry classification.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cmd/entire/cli/telemetry/search_outcome.go Defines SearchOutcome and builds/sends the new cli_search_completed event via detached telemetry.
cmd/entire/cli/telemetry/search_outcome_test.go Tests event payload shape (success vs failure fields).
cmd/entire/cli/search/search.go Introduces typed HTTPStatusError for non-OK semantic search responses.
cmd/entire/cli/search/search_test.go Verifies semantic search non-OK responses return *HTTPStatusError with status code preserved.
cmd/entire/cli/search_v4.go Adds hintError wrapper and preserves typed causes in the “all semantic cells skipped” path.
cmd/entire/cli/search_telemetry.go Implements classifySearchError and emitSearchOutcome to record the new outcome event when telemetry is enabled.
cmd/entire/cli/search_telemetry_test.go Table-tests error classification and validates hintError preserves message + errors.Is chain.
cmd/entire/cli/search_cmd.go Emits outcome telemetry for both semantic search and code search, timing only the request (excluding TUI time).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/entire/cli/search_telemetry.go Outdated
Comment on lines +67 to +71
func emitSearchOutcome(ctx context.Context, cmd *cobra.Command, mode string, resultCount int, duration time.Duration, err error) {
s, loadErr := LoadEntireSettings(ctx)
if loadErr != nil || !s.IsTelemetryEnabled() {
return
}
Comment on lines 390 to 394
switch {
case errors.Is(r.err, search.ErrCellUnavailable), errors.Is(r.err, auth.ErrNoCellForJurisdiction):
skipped = append(skipped, r.group.label())
skipErr = r.err
case errors.Is(r.err, search.ErrRepoFilterUnmatched):
evisdren and others added 3 commits August 25, 2026 21:48
Review findings applied:

- Classify network failures via the existing isRecapNetworkError instead
  of a raw net.Error check, so a user's Ctrl-C (context.Canceled inside
  url.Error) is "other", never "network" — the raw check inflated the
  exact rate the event exists to measure. Timeouts stay "network".
- Keep every per-cell skip cause in the all-cells-skipped error, not just
  the last one, so a mixed-cause fan-out classifies by the classifier's
  precedence instead of cell iteration order. New end-to-end test pins
  classifySemanticCells → classifySearchError including mixed causes.
- Drop the derivable SearchOutcome.Success field (success == empty
  ErrorClass, enforced by construction instead of convention).
- Add coverage_incomplete to successful payloads so a degraded success
  (failed regions, skipped repos, truncated index) is distinguishable
  from a genuine zero-result search.
- Check ENTIRE_TELEMETRY_OPTOUT before the settings load so opted-out
  users pay nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01M0Y6B8YAGVG9K84WPXNV3CZR
Trail Review finding: parseSearchResponse returned plain fmt.Errorf for
a 200 with undecodable JSON or an application-level error field, so
those service failures classified as "other" while the same bugs
surfaced with a proper status code classified as "server".

Both shapes now return a typed search.MalformedResponseError (message
wording unchanged) and the classifier maps it to the server class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01M0Y7CXG0K9R0EKEVEAYYSG6D
Trail Review finding: emitSearchOutcome only fired for the one-shot
initial request, so TUI re-searches, pagination, the code tab, and bare
`entire search` (straight to TUI) never emitted cli_search_completed —
a large share of real invocations was invisible.

Instrument the seams instead of the call sites: the semanticSearcher is
wrapped once per invocation (instrumentSemanticSearcher), and
searchAllCells emits for itself, so every entry point — one-shot, TUI
initial fetch, re-searches, pagination, code tab — is covered by
construction and new entry points can't silently drop off. Each timer
wraps only the request, so TUI dwell time is still never counted. The
command path travels via the wrapper closure and codeSearchOpts because
the TUI searches long after the command layer returns. TUI searches
fire per submitted query (Enter), not per keystroke, so event volume
stays one event per real search request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 01M0Y8JN191FD8SXA4KK4M6XDT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants