Skip to content

feat(cli): Build command hints with a sentry… tag and lint them - #2071

Merged
BYK merged 5 commits into
mainfrom
chore/lint-invocation-command
Nov 7, 2026
Merged

BYK merged 5 commits into
mainfrom
chore/lint-invocation-command

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

This follows up on #2054. Command hints in the CLI now use a sentry`…` template tag, so each hint names the program that's actually running: sentry, npx -y sentry or sentry-mcp. The sentry-cli/no-hardcoded-sentry-command oxlint rule flags hints that skip the tag. check:doc-examples checks every tag's command and flags against the real route tree.

What changed

  • The tag. sentry in src/lib/app-identity.ts takes the command without the program name, for example sentry`issue list ${org}/`. It adds getInvocationCommand() when the hint is built. It's recursive: an interpolated value that's itself a sentry … command gets the same prefix. A value the tag already built keeps its prefix, so nothing is named twice. No code in src used sentry as an identifier, so the name doesn't clash. The Sentry SDK is imported as Sentry.
  • Migration. About 390 asInvocationCommand("sentry …") and asRunningAppCommand("sentry …") calls on literals now use the tag. I converted them with a TypeScript-AST codemod, so the template text is unchanged.
  • One function form stays. asInvocationCommand(command) remains for 51 commands that live in runtime data: USAGE_HINT/usageHint passed into errors, org-list's commandPrefix, suggestion lists, and the ContextError/ResolutionError usage lines. A tag can't wrap a value whose text isn't known at the call site. asRunningAppCommand is gone. Help usage lines and --help examples now go through the tag or asInvocationCommand, so they also print npx -y sentry … under npx.
  • A route-aware lint rule. sentry <word> is flagged only when <word> is one of the top-level routes in src/command-manifest.ts (the rule reads the keys of COMMAND_MANIFEST), a flag (sentry --help), a <placeholder>, or an interpolation. Prose such as "sentry binary", "sentry entries in …" or "Installed sentry v…" passes, so I removed the 6 disable comments that existed only for those cases. The rule also flags:
    • a tag or asInvocationCommand() at module scope, which runs before the entry point sets the app identity;
    • asInvocationCommand() on a literal (use the tag instead);
    • a tag whose text repeats sentry.
  • Checking tag text. script/check-doc-examples.ts already resolves commands and flags against the live route tree. It now also scans src for sentry`…` tags, with each interpolation read as a placeholder. I put the check there rather than in the lint rule: oxlint plugins run as plain JS and can't load the TypeScript route tree, while this script already loads it and already runs in CI through ci:policy. It found two DSN hints that told people to run sentry config set defaults.org/defaults.project, which don't exist. They now say sentry cli defaults org/project.
  • Updated the packages/cli/AGENTS.md note.

A few disable comments remain, each with a reason: help/introspect code that matches command paths, the telemetry value app.server.distribution, the env-var reference text in env-registry.ts, and shell.ts's "Added sentry ${label}", where an interpolation follows sentry.

Testing

  • node --test scripts/cli-lint-rules.test.mjs: 116/116 pass. New cases cover route-aware prose, sentry --help, a literal passed to asInvocationCommand, a doubled sentry, and a module-level tag.
  • test/lib/app-identity.test.ts covers the tag under the default name, under sentry-mcp and under npx, plus the rewriting of interpolated commands without doubling.
  • I added a temporary tag with a bad command (isue list) and an unknown flag (--bogus). pnpm run check:doc-examples reported both, then I removed it. The real run passes 1,551 examples. All of pnpm run ci:policy passes.
  • pnpm run tsc and pnpm run lint pass.
  • pnpm --filter sentry run test: 12,879 pass and 5 fail. These are the same 5 file-permission tests that fail on unchanged main in my sandbox (completions EACCES/EPERM, the read-only db warning, unreadable files in the DSN scanner, acquireLock).

Known gaps

  • The lint rule can't follow data flow. A module-level command constant printed without going through one of the wrapping helpers would slip past it.
  • The tag also rewrites an interpolated value that happens to start with sentry and isn't a command, such as unquoted user text. Search queries are passed through shellQuote first, so they start with a quote and aren't affected.

Refs #2054
Refs #2050

via @BYK.

--

View Session [Sentry]

Add the sentry-cli/no-hardcoded-sentry-command oxlint rule. In code that
@sentry/mcp-server can run (all of packages/cli/src except app.ts and the
commands the bundle does not ship), a `sentry <command>` string must go
through asInvocationCommand(), so hints name the running program. Command
help text (docs, fullDescription) is exempt. The rule also rejects a
module-level asInvocationCommand() call, which runs before the entry point
sets the app identity.

Wrap the 127 existing hints, and move the few module-level hint constants
into the functions that build them. Lookup tables that are rendered
through asInvocationCommand(), the env var reference text, and one
telemetry value keep their literals behind disable comments.

Refs #2054
Refs #2050
@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 2:08am UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 7, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should not make this specific to @sentry/mcp-server as one can run sentry as npx sentry or pnpx sentry or even npx @sentry/cli in the near future.

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread packages/cli/src/lib/dsn/errors.ts Resolved
The same code runs as sentry, npx sentry, pnpx sentry and sentry-mcp,
so the rule now covers all of packages/cli/src, not only the code the
mcp-server bundle ships. Hints inside functions wrap their command in
asInvocationCommand(). Module-level usage lines stay plain data, and
ContextError, ResolutionError, buildValidationMessage and org-list wrap
them where they are printed.

Refs #2054
Refs #2050
@github-actions github-actions Bot removed the risk: medium PR risk score: medium label Nov 7, 2026
@sentry-junior sentry-junior Bot changed the title feat(cli): Lint hardcoded sentry commands in mcp-server code feat(cli): Lint hardcoded sentry commands in CLI hints Nov 7, 2026

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 5 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 96ebd2a. Configure here.

Comment thread packages/cli/src/commands/cli/setup.ts Resolved

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe asInvocationCommand can become a template tag which may help with interpolation too as we can make it recursive?

@sentry-junior

sentry-junior Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Agreed. In 96ebd2a the rule covers all of packages/cli/src, not just the code the mcp-server bundle ships, and the message now names npx sentry and sentry-mcp. About 240 more hints in commands/ go through asInvocationCommand(), so npx sentry, pnpx sentry and sentry-mcp each get their own invocation, and npx @sentry/cli will too once app-identity knows that package.

Module-level usage lines (USAGE_HINT, commandPrefix) stay plain data. ContextError, ResolutionError, buildValidationMessage and org-list now wrap them when they print them. Command help (docs, fullDescription, brief, examples) is still exempt. I updated the PR body with the details.

The no-hardcoded-sentry-command rule now treats each template
interpolation as a word and matches a <command> placeholder, so
`sentry ${commandName} …` and `sentry <command> …` hints are caught.
The new hits in resolve-target, target-positional, the DSN errors and
the --help tips now go through asInvocationCommand(). The help listing
names the running program. Command paths and the uninstall labels that
name the binary keep `sentry` behind disable comments.

Refs #2054
Refs #2050
Hints now write their command with the `sentry` template tag from
lib/app-identity.ts, without the program name: sentry`issue list ${org}/`.
The tag names the running program (sentry, npx -y sentry, sentry-mcp), and
an interpolated `sentry …` command gets the same prefix, never twice.
asInvocationCommand() stays only for commands held in variables
(USAGE_HINT, commandPrefix, suggestions); asRunningAppCommand() is gone.

no-hardcoded-sentry-command now flags `sentry <word>` only when the word
is a route from src/command-manifest.ts, a flag, a placeholder or an
interpolation, so prose such as "sentry binary" passes without disable
comments. It also flags asInvocationCommand() on a literal and a tag that
repeats `sentry`. check:doc-examples checks each tag's command and flags
against the route tree, which caught the DSN hints that named a
nonexistent `sentry config set`; they now name `sentry cli defaults`.

Refs #2054
Refs #2050

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@sentry-junior sentry-junior Bot changed the title feat(cli): Lint hardcoded sentry commands in CLI hints feat(cli): Build command hints with a sentry… tag and lint them Nov 7, 2026
Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@sentry-junior

sentry-junior Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Went with the template tag, reworked in place in df1b5a8:

  • sentry`issue list ${org}/` (in app-identity.ts) prepends the running program: sentry, npx -y sentry, or sentry-mcp. It's recursive as you suggested: an interpolated sentry … command gets the same prefix, and a value the tag already built is never doubled.
  • About 390 call sites moved to the tag via an AST codemod. asRunningAppCommand is gone. asInvocationCommand(command) stays only for the 51 commands that are runtime data (USAGE_HINT, commandPrefix, suggestion lists), since a tag can't wrap a variable's text.
  • The lint rule is now route-aware: it flags sentry <word> only when <word> is a real top-level route from command-manifest.ts (or a flag, placeholder, or interpolation), so "sentry binary" and friends pass with no disable comments. It also flags a tag at module scope and asInvocationCommand() on a literal.
  • check:doc-examples now validates every tag's command path and flags against the live route tree. It immediately caught two DSN hints pointing at a nonexistent sentry config set defaults.org; they now say sentry cli defaults org/project.

@BYK
BYK merged commit ad4ae7f into main Nov 7, 2026
40 of 41 checks passed
@BYK
BYK deleted the chore/lint-invocation-command branch November 7, 2026 02:20
BYK added a commit that referenced this pull request Nov 7, 2026
…nCommand (#2085)

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_HINT`s,
`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

<!-- 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: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 0a1b33c3 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