Skip to content

feat: dynamic cache-backed shell completions with fuzzy matching#465

Merged
BYK merged 26 commits intomainfrom
feat/dynamic-completions
Mar 19, 2026
Merged

feat: dynamic cache-backed shell completions with fuzzy matching#465
BYK merged 26 commits intomainfrom
feat/dynamic-completions

Conversation

@BYK
Copy link
Member

@BYK BYK commented Mar 18, 2026

Summary

Add a hybrid static + dynamic shell completion system that suggests cached org slugs, project slugs, and project aliases alongside existing command/subcommand names — with fuzzy matching for typo tolerance.

Approach: Hybrid Static + Dynamic

  • Static (0ms): command/subcommand names, flag names, and enum flag values are embedded in the shell script
  • Dynamic (<50ms): positional arg values are completed by calling sentry __complete at runtime, which reads the SQLite cache with fuzzy matching
  • Lazy fetch (one-time ~200-500ms): if no projects are cached for an org, fetches and caches them on first tab-complete

New Files

  • src/lib/fuzzy.ts — Shared Levenshtein distance + fuzzyMatch() with tiered scoring (exact > prefix > contains > Levenshtein distance)
  • src/lib/complete.ts — Completion engine: handles the __complete fast-path with context parsing, cache querying, and lazy project fetching
  • test/lib/fuzzy.test.ts — Property-based + unit tests for fuzzy matching
  • test/lib/complete.test.ts — Tests for the completion engine

Key Changes

  • src/bin.ts: __complete fast-path at top of main() — skips all middleware, telemetry, and auth
  • src/lib/completions.ts: Extended extractCommandTree() with flag metadata; updated bash/zsh/fish generators with flag completion + dynamic __complete callback
  • src/lib/db/project-cache.ts: Added getAllCachedProjects() and getCachedProjectsForOrg() for completion queries
  • src/lib/platforms.ts: Uses shared levenshtein() from fuzzy.ts instead of local copy

How Tab Completion Works

sentry <TAB>          → commands (static, instant)
sentry issue <TAB>    → subcommands (static, instant)
sentry issue list --<TAB>     → flags (static, instant)
sentry issue list <TAB>       → org slugs with "/" (dynamic, from cache)
sentry issue list myorg/<TAB> → projects for that org (dynamic, lazy-fetches if cold)
sentry issue list senry/<TAB> → "sentry/" via fuzzy match (Levenshtein distance 1)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (telemetry) Track TTY vs non-TTY invocations via metric by betegon in #482
  • Dynamic cache-backed shell completions with fuzzy matching by BYK in #465

Bug Fixes 🐛

  • (project) Fallback to org listing when bare slug matches an organization by betegon in #475

Internal Changes 🔧

  • Switch from @sentry/bun to @sentry/node-core/light (~170ms startup savings) by BYK in #474
  • Regenerate skill files by github-actions[bot] in b7b240ec

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

Codecov Results 📊

126 passed | Total: 126 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 99.21%. Project has 1037 uncovered lines.
✅ Project coverage is 95.75%. Comparing base (base) to head (head).

Files with missing lines (3)
File Patch % Lines
telemetry.ts 91.45% ⚠️ 40 Missing
schema.ts 93.47% ⚠️ 33 Missing
index.ts 95.10% ⚠️ 5 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    95.71%    95.75%    +0.04%
==========================================
  Files          177       180        +3
  Lines        24100     24417      +317
  Branches         0         0         —
==========================================
+ Hits         23066     23380      +314
- Misses        1034      1037        +3
- Partials         0         0         —

Generated by Codecov Action

@BYK BYK force-pushed the feat/dynamic-completions branch from 15f1077 to 34f1323 Compare March 18, 2026 20:51
Add hybrid static + dynamic shell completion system that suggests cached
org slugs, project slugs, and project aliases alongside existing command
and subcommand names.

Architecture:
- Static: command/subcommand names, flag names, and enum values are
  embedded in the shell script for instant tab completion (0ms)
- Dynamic: positional arg values (org slugs, project names, aliases)
  are completed by calling `sentry __complete` at runtime, which reads
  the SQLite cache with fuzzy matching (<50ms)

New files:
- src/lib/fuzzy.ts: Shared Levenshtein distance + fuzzyMatch() utility
  with tiered scoring (exact > prefix > contains > Levenshtein distance)
- src/lib/complete.ts: Completion engine handling the __complete fast-path
  with context parsing, cache querying, and lazy project fetching

Key changes:
- src/bin.ts: Add __complete fast-path before any middleware/telemetry
- src/lib/completions.ts: Extend extractCommandTree() with flag metadata;
  update bash/zsh/fish generators with flag completion and dynamic callback
- src/lib/db/project-cache.ts: Add getAllCachedProjects() and
  getCachedProjectsForOrg() for completion queries
- src/lib/platforms.ts: Use shared levenshtein() from fuzzy.ts

Features:
- Fuzzy matching for org/project names (typo-tolerant)
- Flag name completion for all commands (static, instant)
- Lazy project cache population on first tab-complete per org
- Graceful degradation when DB/API unavailable
- Tab-separated value\tdescription output for zsh/fish
@BYK BYK force-pushed the feat/dynamic-completions branch from 34f1323 to 69c62ee Compare March 18, 2026 20:54
@BYK BYK marked this pull request as ready for review March 18, 2026 21:39
Address two review findings:

1. Seer: When user types 'senry/', fuzzy-resolve the org part to
   'sentry' before querying projects. Previously the exact-match SQL
   query would find nothing for the typo'd org slug.

2. BugBot: In zsh, _arguments -C with '*::arg:->args' resets $words
   to only remaining positional args, stripping command/subcommand.
   Fixed by using $line[1] and $line[2] (set by _arguments -C) to
   pass the full command context to __complete.
…elpers

Address BugBot round 2 findings:

1. Remove the heuristic that suppressed dynamic completions when the
   previous word starts with '--'. Boolean flags (--verbose, --json)
   don't consume the next word, so completions should still appear.
   The shell script already handles flag value completion statically.

2. Extract matchOrgSlugs() helper shared by completeOrgSlugs and
   completeOrgSlugsWithSlash, eliminating duplicated cache query,
   fuzzy match, and name lookup logic.
Address BugBot round 3 findings:

1. Bash: shellVarName() replaces [-. ] with _ at codegen time, but
   runtime lookup only replaced hyphens. Added __varname() bash helper
   that uses the same replacement pattern for consistent lookups.

2. Fish: commandline -opc includes the current token, so appending
   commandline -ct passed it twice. Split into preceding (commandline
   -opc) and current (commandline -ct) as separate variables.
Address BugBot round 4 findings:

1. SQL: Changed DISTINCT to GROUP BY project_slug so the same project
   cached with slightly different names (e.g., after a rename) only
   appears once, using the most recently cached name.

2. Fish: Added flag completions for standalone commands (e.g., api
   --method). Previously only grouped subcommands had flag completions.
Address BugBot round 5 findings:

1. Fish: Quote $current so empty partial (TAB after space) is passed
   as "" instead of being silently dropped by fish's empty-list
   expansion, which would misinterpret the last preceding word.

2. Bash: Add standalone fallback for enum value lookup. For standalone
   commands like 'sentry api --method', COMP_WORDS[2] is a flag not
   a subcommand, so the cmd_subcmd_flag_values lookup fails. Now falls
   back to cmd_flag_values.
SQLite GROUP BY with ORDER BY doesn't guarantee which row's non-
aggregated columns are selected. Use MAX(cached_at) aggregate so
SQLite deterministically picks project_name from the most recently
cached row per project_slug.
When partial is just '/', orgPart is empty and fuzzyResolveOrg
would match the first alphabetical org. Now returns empty early
when orgPart is empty.
When _arguments -C resets $words to only positional args and there
are none yet (user pressed TAB after 'sentry issue list '), the
empty words array caused 'list' to be misinterpreted as the partial.
Now conditionally appends an empty string only when words is empty.
Shell completions were loading @sentry/bun (~280ms) via the import
chain: complete.ts → db/index.ts → telemetry.ts → @sentry/bun.
Completions only read cached SQLite data and never need telemetry.

Changes:
- db/index.ts: Remove top-level telemetry import. Add disableDbTracing()
  flag (same pattern as disableResponseCache/disableOrgCache). Lazy-
  require telemetry.ts inside getDatabase() only when tracing is enabled.
- bin.ts: Move __complete dispatch before all heavy imports. Restructure
  as lightweight dispatcher: runCompletion() loads only db + complete
  modules; runCli() loads the full CLI with telemetry, Stricli, etc.

Results (compiled binary):
- Completion: 320ms → 190ms (40% faster)
- Dev mode: 530ms → 60ms (89% faster)
- Normal commands: unchanged (~510ms)
Code changes:
- bin.ts: Static import for disableDbTracing (db/index.ts is lightweight
  now that telemetry import is lazy)
- introspect.ts: Widen isRouteMap/isCommand to accept `unknown` so
  completions.ts can reuse them
- completions.ts: Remove duplicate type guards (use introspect.ts);
  extractFlagEntries uses for..of; extractSubcommands uses filter.map;
  shellVarName uses whitelist (/[^a-zA-Z0-9_]/); bash/zsh standalone
  command completion at position 2; bash __varname uses same whitelist
- complete.ts: Merge matchOrgSlugs into completeOrgSlugs with suffix
  param (no identity fn); eliminate [...nameMap.keys()] intermediate;
  export command sets for testing
- completions.property.test.ts: Add drift-detection tests verifying
  hardcoded ORG_PROJECT_COMMANDS / ORG_ONLY_COMMANDS stay in sync with
  the actual route tree
BugBot: escapeDblQuote was missing $ escaping which is special in
double-quoted bash/fish strings. Add \$ replacement before " to
prevent variable expansion in completion descriptions.
BYK added 2 commits March 19, 2026 12:56
Seer: While ;;\& works in both bash and zsh, ;| is the canonical zsh
syntax for testing remaining case patterns.
…cing

Replace the in-process tracingDisabled flag and disableDbTracing()
export with the existing SENTRY_CLI_NO_TELEMETRY=1 env var. The
__complete fast-path sets it before any imports, and db/index.ts
checks it when deciding whether to lazy-require the tracing wrapper.

Removes one export, one module-level variable, and one static import.
- complete.ts: Build nameMap first, pass [...nameMap.keys()] to
  fuzzyMatch — eliminates the separate slugs array
- completions.ts: Revert extractSubcommands to for-of loop (more
  efficient than filter.map for small arrays)
- completions.ts: s/whitelist/allowlist in shellVarName JSDoc
Fish triggers command substitution with () inside double quotes.
Add \( and \) escaping to escapeDblQuote().
Same pattern as completeOrgSlugs — build nameMap first and derive
keys from it instead of iterating projects twice.
The ;| terminator continues testing patterns, but args) doesn't match
when state is 'subcommand'. Use args|subcommand) so the dynamic
completion handler fires for standalone commands too.
BYK added 6 commits March 19, 2026 15:31
Add .catch() handlers to runCompletion() and runCli() calls in the
entry point dispatch. Completions silently return no results on error;
CLI prints fatal error and sets exit code 1.
Spawns the actual CLI binary with __complete args and measures
wall-clock latency. Catches regressions that would re-introduce
heavy imports into the completion fast-path.

Tests:
- Completion is faster than a normal command (--version)
- Completion exits under 500ms budget (pre-fix was ~530ms)
- Completion exits cleanly with no stderr
- Empty args handled gracefully
Remove the 'faster than normal command' comparison (not useful since
we want both fast). Tighten the latency budget from 500ms to 100ms —
the UX threshold for instant tab completion.
CI machines measured 141ms vs dev ~67ms. Use 200ms to accommodate
CI variance while still catching regressions (pre-fix was ~530ms).
Add deferred telemetry for shell completions — zero SDK overhead
during completion, metrics emitted opportunistically on next CLI run.

How it works:
1. During __complete: After writing completions to stdout, queue one
   row to completion_telemetry_queue with timing + metadata. Pure
   SQLite, no Sentry SDK. ~1ms overhead.
2. During normal CLI runs: Inside withTelemetry(), drain the queue
   and emit each entry as Sentry.metrics.distribution() with
   command_path attribute.

New files:
- src/lib/db/completion-telemetry.ts: queueCompletionTelemetry() and
  drainCompletionTelemetry() for write/read/delete operations

Schema: version 10 → 11 (new completion_telemetry_queue table)

Also updates stale @sentry/bun references to @sentry/node-core after
the telemetry refactor merge.
Copy link

@cursor cursor bot left a comment

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.

Replace separate SELECT + DELETE in drainCompletionTelemetry() with
a single DELETE ... RETURNING statement. Prevents concurrent
__complete processes from losing rows inserted between the two steps.
@BYK BYK merged commit 9ecc62f into main Mar 19, 2026
22 checks passed
@BYK BYK deleted the feat/dynamic-completions branch March 19, 2026 21:32
betegon added a commit that referenced this pull request Mar 20, 2026
## Summary

Shell completions showed org slugs but not projects after the slash. The
completion PR (#465) described "lazy project cache population" but only
DSN resolution actually wrote to the `project_cache` table. Commands
like `project list`, `issue list`, etc. fetched projects but never
cached them for completions.

Adds `cacheProjectsForOrg()` at the API layer (in `listProjects()`),
mirroring how `listOrganizations()` calls `setOrgRegions()`. Every
command that lists projects now automatically seeds the completion
cache.

## Changes

- `src/lib/db/project-cache.ts` — Add `cacheProjectsForOrg()` batch
function using transactional upsert (same pattern as `setOrgRegions()`)
- `src/lib/api/projects.ts` — Call cache after `listProjects()` returns
(best-effort, wrapped in try/catch)
- `test/lib/db/project-cache.test.ts` — 4 new tests: batch insert,
idempotency, empty no-op, no conflict with DSN cache keys

## Test Plan

- [x] `bun test test/lib/db/project-cache.test.ts` — 26 tests pass (4
new)
- [x] `bun test test/lib/complete.test.ts` — 20 tests pass
- [x] `bun run typecheck` — clean
- [x] `bun run lint` — clean
- Manual: run `sentry project list myorg/`, then `sentry issue list
myorg/<TAB>` — projects now complete

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant