Repository navigation
feat(cli): Build command hints with a sentry… tag and lint them - #2071
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
BYK
left a comment
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 5 total unresolved issues (including 2 from previous reviews).
❌ 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.
BYK
left a comment
There was a problem hiding this comment.
Maybe asInvocationCommand can become a template tag which may help with interpolation too as we can make it recursive?
|
Agreed. In 96ebd2a the rule covers all of Module-level usage lines ( |
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>
… tag and lint them
Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
|
Went with the template tag, reworked in place in df1b5a8:
|
…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 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 sentryorsentry-mcp. Thesentry-cli/no-hardcoded-sentry-commandoxlint rule flags hints that skip the tag.check:doc-exampleschecks every tag's command and flags against the real route tree.What changed
sentryinsrc/lib/app-identity.tstakes the command without the program name, for examplesentry`issue list ${org}/`. It addsgetInvocationCommand()when the hint is built. It's recursive: an interpolated value that's itself asentry …command gets the same prefix. A value the tag already built keeps its prefix, so nothing is named twice. No code insrcusedsentryas an identifier, so the name doesn't clash. The Sentry SDK is imported asSentry.asInvocationCommand("sentry …")andasRunningAppCommand("sentry …")calls on literals now use the tag. I converted them with a TypeScript-AST codemod, so the template text is unchanged.asInvocationCommand(command)remains for 51 commands that live in runtime data:USAGE_HINT/usageHintpassed into errors,org-list'scommandPrefix, suggestion lists, and theContextError/ResolutionErrorusage lines. A tag can't wrap a value whose text isn't known at the call site.asRunningAppCommandis gone. Help usage lines and--helpexamples now go through the tag orasInvocationCommand, so they also printnpx -y sentry …under npx.sentry <word>is flagged only when<word>is one of the top-level routes insrc/command-manifest.ts(the rule reads the keys ofCOMMAND_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:asInvocationCommand()at module scope, which runs before the entry point sets the app identity;asInvocationCommand()on a literal (use the tag instead);sentry.script/check-doc-examples.tsalready resolves commands and flags against the live route tree. It now also scanssrcforsentry`…`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 throughci:policy. It found two DSN hints that told people to runsentry config set defaults.org/defaults.project, which don't exist. They now saysentry cli defaults org/project.packages/cli/AGENTS.mdnote.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 inenv-registry.ts, andshell.ts's "Added sentry ${label}", where an interpolation followssentry.Testing
node --test scripts/cli-lint-rules.test.mjs: 116/116 pass. New cases cover route-aware prose,sentry --help, a literal passed toasInvocationCommand, a doubledsentry, and a module-level tag.test/lib/app-identity.test.tscovers the tag under the default name, undersentry-mcpand under npx, plus the rewriting of interpolated commands without doubling.isue list) and an unknown flag (--bogus).pnpm run check:doc-examplesreported both, then I removed it. The real run passes 1,551 examples. All ofpnpm run ci:policypasses.pnpm run tscandpnpm run lintpass.pnpm --filter sentry run test: 12,879 pass and 5 fail. These are the same 5 file-permission tests that fail on unchangedmainin my sandbox (completions EACCES/EPERM, the read-only db warning, unreadable files in the DSN scanner,acquireLock).Known gaps
sentryand isn't a command, such as unquoted user text. Search queries are passed throughshellQuotefirst, so they start with a quote and aren't affected.Refs #2054
Refs #2050
via @BYK.
--
View Session [Sentry]