Skip to content

refactor(cli): Build stored command hints lazily and drop asInvocationCommand - #2085

Merged
BYK merged 1 commit into
mainfrom
chore/remove-as-invocation-command
Nov 7, 2026
Merged

BYK merged 1 commit into
mainfrom
chore/remove-as-invocation-command

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2071, as BYK asked: remove asInvocationCommand(command) and the runtime startsWith("sentry ") rewriting of hints.

What changed

  • asInvocationCommand is deleted. All 51 call sites are gone. ContextError, ResolutionError, buildValidationMessage and org-list now print the text they receive, with no rewriting.
  • Stored commands are LazyCommand thunks. Any command kept in a constant or table is now () => sentry`…` and gets built when it is shown. That covers 98 module-level sentry … literals: USAGE_HINTs, commandPrefix, suggestion tables, SLUG_REDIRECT_COMMAND, and the issue, release, replay and dashboard argument builders. Every one of them was static text, so none needed runtime data.
  • Recursion works by type, not by text. The sentry`…` tag inserts interpolated values as they are. A nested tag result already starts with the program name, and plain text that happens to start with "sentry " is no longer rewritten. That closes the gap where user input could get rewritten.
  • Lint rule (sentry-cli/no-hardcoded-sentry-command):
    • The exemption for module-level whole-command literals is removed, so a bare 'sentry issue list …' constant is now flagged.
    • A module-level tag call is still flagged, and the message now points to the thunk form.
    • The asInvocationCommand handling is removed.
  • A real bug is fixed. The issueTarget default sentry`issue` used to be evaluated at module load, before setAppIdentity. The new test/lib/app-identity-import.test.ts imports app.js and mcp-server-app.js and asserts that no hint gets built at import. I checked that it fails when that eager evaluation is put back.
  • The packages/cli AGENTS.md note, the errors.ts JSDoc, the RuleTester cases and the app-identity tests are updated.

Design choice: thunks instead of a lazy object

I first tried having the tag return a lazy { toString() } command value. That produced 174 tsc errors across about 50 files, because every string slot would have to accept the new type. A value like that would also serialize as {} in JSON output. Thunks are type-checked, make the moment of evaluation explicit, and keep the literal tag text visible to check:doc-examples. Its tag scan still covers the text inside the thunks: 1649 examples in 669 files match the CLI.

One remaining prefix swap (open question)

The --help rendering of docs.examples still replaces a leading sentry with the running program, in a private exampleForRunningProgram in lib/command.ts. That data covers about 480 examples, and the generated docs and skill references share it, so they need the literal sentry name. The static alternative is to drop sentry from the docs.examples data and let each renderer add its own program name. That is a schema change across every command, so I left it out of this PR. I can do it as a follow-up if you want.

How it was tested

  • Root pnpm run tsc: all packages pass.
  • Root pnpm run lint: 0 warnings, 0 errors (includes ast-grep and the deps check).
  • node --test scripts/cli-lint-rules.test.mjs: 116 pass.
  • pnpm --filter sentry run check:doc-examples: 1649 examples match.
  • pnpm --filter sentry run test: only the 5 known sandbox permission failures remain (completions EACCES/EPERM, resilient readonly, code-scanner unreadable files, upgrade lock). The 35 test failures from string fixtures were fixed by switching those fixtures to thunks, and those 4 files pass again (108 tests).

Refs #2071
Refs #2050

via @BYK.

--

View Session [Sentry]

…nCommand

Commands kept in constants and tables are now LazyCommand thunks
(`() => sentry\`issue view <id>\``) that build the hint when it is shown,
so nothing rewrites a leading `sentry ` at runtime. ContextError,
ResolutionError, buildValidationMessage and org-list print what they get.

The sentry tag inserts interpolated values as they are, so user text
that starts with "sentry " is no longer rewritten. The lint rule now
flags module-level `sentry …` literals too, and a test checks that
loading the command tree builds no hints.

Refs #2071
Refs #2050

Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sentry-local Ready Ready Preview Nov 7, 2026 10:55am UTC

Request Review


/** Usage hint for trace mode error messages */
const TRACE_USAGE_HINT = "sentry log list [<org>/[<project>/]]<trace-id>";
const traceUsageHint = () => sentry`log list [<org>/[<project>/]]<trace-id>`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

traceUsageHint under npx/mcp double-prefixes ambiguous-project errors

Building this hint with sentry makes it start with npx -y … or sentry-mcp, but commandNameFromUsageHint still strips only /^sentry\s+/, so ambiguous project-search errors rebuild suggestions like npx -y sentry npx -y sentry log list …. Strip via getInvocationCommand() (or pass a bare command name) before re-wrapping with sentry.

Evidence
  • traceUsageHint is now () => sentry\log list …`and is passed intoresolveTraceOrg` (list.ts) as a full invocation string.
  • commandNameFromUsageHint only strips /^sentry\s+/ (trace-target.ts), so under npx/mcp it returns e.g. npx -y sentry log list instead of log list.
  • resolveTraceOrg → project-search → resolveOrgOnlyTarget/resolveProjectBoundTarget re-wraps that value with sentry\${commandName} …`` on ambiguous matches, producing a doubled program prefix.
  • Default identity sentry still works; the failure needs npx (npm_command=exec / _npx) or setAppIdentity({ name: "sentry-mcp", … }) plus an ambiguous bare project + trace id.
Also found at 1 additional location
  • packages/cli/src/commands/span/list.ts:186-186

Identified by Warden · code-review · D89-TDK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in #2086. The trace resolvers now take the bare command name (log list, span list, span view, trace logs, trace view) and no longer derive it from the hint. commandNameFromUsageHint is deleted. A regression test checks that under sentry-mcp the program name appears only once.

@BYK
BYK merged commit 78d9764 into main Nov 7, 2026
36 of 37 checks passed
@BYK
BYK deleted the chore/remove-as-invocation-command branch November 7, 2026 11:19
BYK pushed a commit that referenced this pull request Nov 7, 2026
)

Fixes a regression from #2085 that [Warden
reported](#2085 (comment)).
It also removes the last runtime parsing of a leading `sentry ` on this
path.

## Summary

#2085 made the trace usage hints start with the running program (`npx -y
sentry …`, `sentry-mcp …`). `commandNameFromUsageHint` in
`lib/trace-target.ts` still stripped only a leading `sentry `. Under npx
or `sentry-mcp`, it returned `npx -y sentry log list`, and the `sentry`
tag then added the program name a second time. Ambiguous-project errors
suggested `npx -y sentry npx -y sentry log list org-a/frontend/…`.

## What changed

- `resolveTraceOrg`, `resolveTraceOrgProject` and
`resolveTraceOrgOptionalProject` take a `commandName` argument. Each
command passes its own bare name: `log list`, `span list`, `span view`,
`trace logs` or `trace view`.
- `commandNameFromUsageHint` and its two regexes are deleted. Nothing
derives a command name from hint text anymore.
- The usage hint is still used only as the hint that gets displayed.

## How it was tested

- New regression test in `test/lib/trace-target.test.ts`. Under a
`sentry-mcp` identity, an ambiguous bare project in all three resolvers
suggests `sentry-mcp <command> org-a/frontend` and never `sentry-mcp
sentry-mcp`. I checked that the test fails when the old hint parsing is
put back.
- `tsc --noEmit` (packages/cli): passes.
- Root `pnpm run lint`: 0 warnings, 0 errors.
- `node --test scripts/cli-lint-rules.test.mjs`: 116 pass.
- `pnpm --filter sentry run check:doc-examples`: 1649 examples match.
- `vitest` on `test/lib/trace-target.test.ts` and
`test/commands/{log,span,trace}`: 374 pass, 2 skipped.

Refs #2085
Refs #2050

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ac4d53d732a16cb4ca0834ed4529d411d -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ac4d53d732a16cb4ca0834ed4529d411d)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ac4d53d732a16cb4ca0834ed4529d411d/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 7, 2026
)

`docs.examples` steps no longer repeat `sentry `.
`exampleForRunningProgram` is deleted from `src/lib/command.ts`. It was
the last runtime match on command text: `--help` used to swap a leading
`sentry ` for the running program.

## Schema

```ts
type ExampleStep = string | { prefix?: string; args: string; suffix?: string };
type CommandExample = { description: string; run: ExampleStep | readonly ExampleStep[] };
```

- The field is renamed `command` → `run`, so tsc flags any call site
that was missed. This is a hard cutover with no shim.
- A string step holds only the words after the program name, e.g. `run:
"issue list my-org/frontend"`.
- `exampleSteps(example, program)` renders `prefix + program + " " +
args + suffix`. Each renderer picks the program:
- `--help` (`prepareNativeDocs`) uses `getInvocationCommand()`, so it
shows `sentry-mcp …` or `npx -y @sentry/mcp-server …`.
- `introspect.ts`, which feeds the generated command docs, skill
references and `check:fragments`, uses `"sentry"`. So do
`check:doc-examples` and `generate-docs-sections`.

## Steps that aren't a bare `sentry` command

Of 711 steps in 697 examples, 4 wrap the invocation in other shell text.
These became `{ prefix, args, suffix }`:

| File | Step |
| --- | --- |
| `commands/cli/fix.ts` | `sudo sentry cli fix` → `{ prefix: "sudo ",
args: "cli fix" }` |
| `commands/cli/completion.ts` | `eval "$(sentry cli completion bash)"`
→ `{ prefix: 'eval "$(', args: "cli completion bash", suffix: ')"' }` |
| `commands/wasm-split.ts` | `BUILD_ID=$(sentry wasm-split app.wasm)` →
`{ prefix: "BUILD_ID=$(", args: "wasm-split app.wasm", suffix: ")" }` |
| `commands/monitor/run.ts` | `SENTRY_DSN=… sentry monitor run
nightly-job -- python manage.py cron` → `{ prefix: "SENTRY_DSN=… ",
args: "monitor run …" }` |

No step is a pipe, an `npx …` line, another tool, or a shell line with
no `sentry` invocation in it. A literal `{ shell }` form would have had
no users, so I left it out. Trailing redirections such as `cli
completion zsh > ~/.local/share/zsh/site-functions/_sentry` follow the
command, so they stay in the plain string.

Behavior change: under `sentry-mcp` or npx, `--help` now names the
running program inside these 4 wrapped steps too. The old swap only
matched a leading `sentry `, so it skipped them.

## Conversion and guard

- An AST codemod (TypeScript compiler API) found each `examples: [{
description, command }]` entry. It renamed the key to `run`, stripped
the leading `sentry ` from every string step (keeping the original
quotes and escapes), and converted the 4 wrapped steps above. It found
no non-literal steps and no unmatched ones.
- `sentry-cli/no-hardcoded-sentry-command` now rejects an example step
that starts with `sentry `: a `run` string, an element of a `run` array,
or a step's `args`. RuleTester cases cover each form.
- `check:doc-examples` still checks every rendered step against the
route tree (1649 examples in 669 files). A doubled `sentry sentry …`
would fail there as an unknown command.

## Verification

- **Byte-identical output.** I ran `pnpm run generate:docs` on `main`
and on this branch, then diffed recursively: `apps/cli-docs/src/content`
(command pages, `index.md`, `configuration.md`, docs sections),
`plugins/sentry-cli` (SKILL.md plus 42 references),
`.well-known/skills/index.json`, `src/generated/` (including
`skill-content.ts`) and `sdk.generated.*`. There were no differences.
- New `test/lib/command-examples.test.ts`: `--help` renders `sentry-mcp
…`, `sudo sentry-mcp cli fix` and `npx -y @sentry/mcp-server …` under
those identities, and `exampleSteps(e, "sentry")` gives the docs form.
- `pnpm run tsc` and `pnpm run lint` (root) pass.
- Rule tests: `node --test scripts/cli-lint-rules.test.mjs` passes 118
of 118.
- `check:doc-examples`, `check:fragments` and `check:docs-sections`
pass.
- `pnpm --filter sentry run test`: 12933 passed. 5 failed, all known
sandbox permission failures: completions EACCES/EPERM (2), db/resilient
readonly, the dsn code-scanner unreadable file, and the upgrade lock.

Refs #2085
Refs #2050

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ac4d53d732a16cb4ca0834ed4529d411d -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ac4d53d732a16cb4ca0834ed4529d411d)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ac4d53d732a16cb4ca0834ed4529d411d/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 42bd081d Deployed Nov 7, 2026 by vercel[bot]
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