diff --git a/apps/cli-docs/src/content/docs/contributing.md b/apps/cli-docs/src/content/docs/contributing.md index 4eff832a0..7b0ee68d5 100644 --- a/apps/cli-docs/src/content/docs/contributing.md +++ b/apps/cli-docs/src/content/docs/contributing.md @@ -88,7 +88,7 @@ cli/ │ │ ├── init.ts # Initialize Sentry in your project (experimental) │ │ └── schema.ts # Browse the Sentry API schema │ ├── lib/ # Shared utilities -│ └── types/ # TypeScript types and Zod schemas +│ └── types/ # TypeScript types and Valibot schemas ├── test/ # Test files (mirrors src/ structure) ├── script/ # Build and utility scripts ├── plugins/ # Agent skill files diff --git a/apps/cli-docs/src/content/docs/features.md b/apps/cli-docs/src/content/docs/features.md index b8f203386..1e9b0f8aa 100644 --- a/apps/cli-docs/src/content/docs/features.md +++ b/apps/cli-docs/src/content/docs/features.md @@ -25,16 +25,22 @@ For monorepos or when DSN detection picks up the wrong project, use a [`.sentryc ### Supported Languages -The CLI can detect DSNs from source code in these languages: - -| Language | File Extensions | Detection Pattern | -|----------|-----------------|-------------------| -| JavaScript/TypeScript | `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.cjs` | `Sentry.init({ dsn: "..." })` | -| Python | `.py` | `sentry_sdk.init(dsn="...")` | -| Go | `.go` | `sentry.Init(sentry.ClientOptions{Dsn: "..."})` | -| Java | `.java` | `Sentry.init(options -> options.setDsn("..."))` | -| Ruby | `.rb` | `Sentry.init { |config| config.dsn = "..." }` | -| PHP | `.php` | `\Sentry\init(['dsn' => '...'])` | +The CLI scans source files for DSN URLs (the `https://…@….ingest.sentry.io/…` pattern) using a universal regex — no language-specific parsing is needed. Any text file with a recognized extension is scanned, including: + +| Language Family | File Extensions | +|----------------|-----------------| +| JavaScript/TypeScript | `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.cjs`, `.astro`, `.vue`, `.svelte` | +| Python | `.py` | +| Go | `.go` | +| JVM (Java, Kotlin, Scala, Groovy) | `.java`, `.kt`, `.kts`, `.scala`, `.groovy` | +| .NET (C#, F#, VB) | `.cs`, `.fs`, `.vb` | +| Ruby | `.rb`, `.erb` | +| PHP | `.php` | +| Swift/Objective-C | `.swift`, `.m`, `.mm` | +| Rust | `.rs` | +| Dart/Flutter | `.dart` | +| Elixir/Erlang | `.ex`, `.exs`, `.erl` | +| Config files | `.json`, `.yaml`, `.yml`, `.toml`, `.xml`, `.properties` | ### Caching diff --git a/apps/cli-docs/src/fragments/commands/cli.md b/apps/cli-docs/src/fragments/commands/cli.md index 4f3e6f79c..beaad4f60 100644 --- a/apps/cli-docs/src/fragments/commands/cli.md +++ b/apps/cli-docs/src/fragments/commands/cli.md @@ -131,6 +131,18 @@ Feedback is sent via Sentry's telemetry system. If telemetry is disabled (`SENTR sentry cli fix ``` +### Print shell completions + +```bash +# Print completions for your current shell (auto-detected from $SHELL) +sentry cli completion + +# Generate for a specific shell +sentry cli completion zsh > ~/.local/share/zsh/site-functions/_sentry +eval "$(sentry cli completion bash)" +sentry cli completion fish > ~/.config/fish/completions/sentry.fish +``` + ### Configure shell integration ```bash diff --git a/apps/cli-docs/src/fragments/commands/dashboard.md b/apps/cli-docs/src/fragments/commands/dashboard.md index 6977cc957..2e896102f 100644 --- a/apps/cli-docs/src/fragments/commands/dashboard.md +++ b/apps/cli-docs/src/fragments/commands/dashboard.md @@ -34,6 +34,9 @@ sentry dashboard view 12345 # Auto-refresh every 30 seconds sentry dashboard view "Backend Performance" --refresh 30 +# Render as a sixel image (for terminals that support sixel graphics) +sentry dashboard view "Backend Performance" --sixel + # Open in browser sentry dashboard view 12345 -w ``` diff --git a/packages/cli/package.json b/packages/cli/package.json index 594694d62..962e8c16f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -81,6 +81,7 @@ "check:errors": "pnpm tsx script/check-error-patterns.ts", "check:patches": "pnpm tsx script/check-patches.ts", "check:docs-sections": "pnpm tsx script/generate-docs-sections.ts --check", + "check:env-coverage": "pnpm tsx script/check-env-coverage.ts", "check:stale-refs": "pnpm tsx script/check-stale-references.ts" }, "devDependencies": { diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md index 20f714426..2343f5dd5 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md @@ -15,6 +15,18 @@ CLI-related commands Print the shell completion script +**Examples:** + +```bash +# Print completions for your current shell (auto-detected from $SHELL) +sentry cli completion + +# Generate for a specific shell +sentry cli completion zsh > ~/.local/share/zsh/site-functions/_sentry +eval "$(sentry cli completion bash)" +sentry cli completion fish > ~/.config/fish/completions/sentry.fish +``` + ### `sentry cli defaults ` View and manage default settings diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md index d9f39ff09..0667023f8 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md @@ -57,6 +57,9 @@ sentry dashboard view 12345 # Auto-refresh every 30 seconds sentry dashboard view "Backend Performance" --refresh 30 +# Render as a sixel image (for terminals that support sixel graphics) +sentry dashboard view "Backend Performance" --sixel + # Open in browser sentry dashboard view 12345 -w ``` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md index 8c9800e99..7e5d2611e 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -22,7 +22,7 @@ Query aggregate event data (Explore) - `-d, --dataset - Dataset to query (errors, spans, metrics, logs, replays) - (default: "errors")` - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort field (prefix with - for desc, e.g., "-count()")` -- `-e, --environment ... - Replay environment filter for --dataset replays (repeatable, comma-separated)` +- `-e, --environment ... - Environment filter (repeatable, comma-separated)` - `-n, --limit - Number of rows (1-1000) - (default: "25")` - `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "24h")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` diff --git a/packages/cli/script/check-env-coverage.ts b/packages/cli/script/check-env-coverage.ts new file mode 100644 index 000000000..b7ff76f4f --- /dev/null +++ b/packages/cli/script/check-env-coverage.ts @@ -0,0 +1,192 @@ +#!/usr/bin/env tsx +/** + * Environment variable documentation coverage. + * + * Scans the source tree for environment variables the CLI reads at runtime and + * fails if any user-facing variable is missing from `ENV_VAR_REGISTRY` + * (src/lib/env-registry.ts), which is the single source of truth for the + * generated `configuration.md` docs. + * + * This closes the class of "code reads an env var but the docs never mention it" + * bugs: a new `getEnv().SENTRY_FOO` read forces a deliberate choice — either + * document it in the registry, or mark it internal in INTERNAL_ENV_VARS below. + * + * A variable counts as user-facing when its name starts with `SENTRY_` or is one + * of the well-known cross-tool vars in PUBLIC_ENV_VARS. Internal plumbing, + * test-only hooks, and vars inherited from the SDK are listed (with a reason) in + * INTERNAL_ENV_VARS so their exclusion is explicit and reviewable. + * + * Usage: + * tsx script/check-env-coverage.ts + * + * Exit codes: + * 0 - Every referenced user-facing env var is documented or explicitly internal + * 1 - Coverage gap, redundant allowlist entry, or stale allowlist entry + */ + +import { readFile } from "node:fs/promises"; +import { glob } from "tinyglobby"; +import { ENV_VAR_REGISTRY } from "../src/lib/env-registry.js"; + +/** + * Well-known env vars that don't start with `SENTRY_` but are still part of the + * CLI's documented, user-facing surface. Anything read here must be in the + * registry too. + */ +const PUBLIC_ENV_VARS = new Set([ + "DO_NOT_TRACK", + "NO_COLOR", + "FORCE_COLOR", + "NODE_EXTRA_CA_CERTS", +]); + +/** + * Env vars the CLI reads but intentionally keeps out of the user-facing docs. + * Each needs a one-line reason so the exclusion is a deliberate, reviewable + * decision rather than an oversight. Keep this list minimal. + */ +const INTERNAL_ENV_VARS = new Map([ + [ + "SENTRY_ENVIRONMENT", + "bash-hook traceback template only; inherited SDK convention", + ], + [ + "SENTRY_TRACES_SAMPLE_RATE", + "inherited from the SDK when spawning subprocesses, not a CLI config knob", + ], + ["SENTRY_DIST", "react-native xcode build var, mirrors legacy sentry-cli"], + [ + "SENTRY_SCAN_DISABLE_WORKERS", + "internal scanner performance tuning, not user-facing", + ], + [ + "SENTRY_DASHBOARD_SIXEL", + "internal sixel debug toggle; users use the --sixel flag", + ], + [ + "SENTRY_CLI_INTEGRATION_TEST_VERSION_OVERRIDE", + "integration-test-only version override", + ], + ["SENTRY_RN_BUNDLE_COMMAND", "internal react-native wrapper plumbing"], + ["SENTRY_RN_NO_DEBUG_ID", "internal react-native wrapper plumbing"], + ["SENTRY_RN_REAL_HERMES_CLI_PATH", "internal react-native wrapper plumbing"], + ["SENTRY_RN_REAL_NODE_BINARY", "internal react-native wrapper plumbing"], + ["SENTRY_RN_SOURCEMAP_REPORT", "internal react-native wrapper plumbing"], +]); + +const registryNames = new Set(ENV_VAR_REGISTRY.map((entry) => entry.name)); + +/** + * Env-var-looking names read off an env object via member access, e.g. + * `getEnv().SENTRY_ORG`, `process.env.NO_COLOR`, `ctx.env.SENTRY_HOST`, + * `someEnv.SENTRY_DSN`. The leading alternation matches the standard accessors + * plus any identifier ending in `env`/`Env` (the repo's env-copy convention). + */ +const ENV_MEMBER_RE = + /(?:getEnv\(\)|process\.env|ctx\.env|\b\w*[eE]nv)\??\.([A-Za-z_][A-Za-z0-9_]*)/g; + +/** + * Env-var reads via bracket access, e.g. `getEnv()["SENTRY_LOG_LEVEL"]` or + * `getEnv()[CONFIG_DIR_ENV_VAR]`. The index is either a string literal or an + * identifier resolved through the constant map built below. + */ +const ENV_BRACKET_RE = + /(?:getEnv\(\)|process\.env|ctx\.env|\b\w*[eE]nv)\??\[\s*(?:"([^"]+)"|'([^']+)'|([A-Za-z_][A-Za-z0-9_]*))\s*\]/g; + +/** `const FOO_ENV_VAR = "SENTRY_FOO";` — resolves bracket reads by constant. */ +const ENV_CONST_RE = + /\bconst\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:"([^"]+)"|'([^']+)')/g; + +function isUserFacing(name: string): boolean { + return name.startsWith("SENTRY_") || PUBLIC_ENV_VARS.has(name); +} + +const files = await glob("src/**/*.ts", { + ignore: ["src/generated/**", "**/*.test.ts"], +}); + +// Pass 1: resolve `const X = "SENTRY_..."` declarations across the tree so that +// bracket reads indexed by a constant can be mapped back to a literal name. +const constMap = new Map(); +const fileContents = new Map(); +for (const file of files) { + const content = await readFile(file, "utf-8"); + fileContents.set(file, content); + for (const match of content.matchAll(ENV_CONST_RE)) { + const value = match[2] ?? match[3]; + if (value && isUserFacing(value)) { + constMap.set(match[1], value); + } + } +} + +/** name -> first "file:line" where it is read (for actionable error output). */ +const referenced = new Map(); + +function record(name: string, file: string, content: string, index: number) { + if (!isUserFacing(name) || referenced.has(name)) { + return; + } + const line = content.slice(0, index).split("\n").length; + referenced.set(name, `${file}:${line}`); +} + +for (const [file, content] of fileContents) { + for (const match of content.matchAll(ENV_MEMBER_RE)) { + record(match[1], file, content, match.index); + } + for (const match of content.matchAll(ENV_BRACKET_RE)) { + const ident = match[3]; + const name = + match[1] ?? match[2] ?? (ident ? constMap.get(ident) : undefined); + if (name) { + record(name, file, content, match.index); + } + } +} + +const errors: string[] = []; + +// A user-facing var read in code must be documented or explicitly internal. +for (const [name, location] of referenced) { + if (registryNames.has(name) || INTERNAL_ENV_VARS.has(name)) { + continue; + } + errors.push( + `Undocumented env var: ${name} (read at ${location})\n` + + " Add it to ENV_VAR_REGISTRY in src/lib/env-registry.ts so it appears in configuration.md,\n" + + " or add it to INTERNAL_ENV_VARS in this script with a reason if it is internal-only." + ); +} + +// A name can't be both documented and internal. +for (const name of INTERNAL_ENV_VARS.keys()) { + if (registryNames.has(name)) { + errors.push( + `Contradictory entry: ${name} is in both ENV_VAR_REGISTRY and INTERNAL_ENV_VARS. ` + + "Remove it from INTERNAL_ENV_VARS." + ); + } +} + +// Keep the allowlist honest: drop entries no longer read anywhere. +for (const name of INTERNAL_ENV_VARS.keys()) { + if (!referenced.has(name)) { + errors.push( + `Stale INTERNAL_ENV_VARS entry: ${name} is no longer read in src/. Remove it.` + ); + } +} + +if (errors.length > 0) { + console.error(`\nFound ${errors.length} env var coverage issue(s):\n`); + for (const err of errors) { + console.error(` ✗ ${err}\n`); + } + process.exit(1); +} + +console.log( + `✓ All ${referenced.size} user-facing env var(s) read in src/ are documented ` + + `(${registryNames.size} in registry, ${INTERNAL_ENV_VARS.size} internal).` +); diff --git a/packages/cli/script/generate-docs-sections.ts b/packages/cli/script/generate-docs-sections.ts index d8d74d355..782cf2718 100644 --- a/packages/cli/script/generate-docs-sections.ts +++ b/packages/cli/script/generate-docs-sections.ts @@ -207,7 +207,7 @@ function generateProjectStructure(allRoutes: RouteInfo[]): string { } lines.push("│ ├── lib/ # Shared utilities"); - lines.push("│ └── types/ # TypeScript types and Zod schemas"); + lines.push("│ └── types/ # TypeScript types and Valibot schemas"); lines.push("├── test/ # Test files (mirrors src/ structure)"); lines.push("├── script/ # Build and utility scripts"); lines.push("├── plugins/ # Agent skill files"); diff --git a/packages/cli/src/commands/explore.ts b/packages/cli/src/commands/explore.ts index 41b23f957..a7902ce16 100644 --- a/packages/cli/src/commands/explore.ts +++ b/packages/cli/src/commands/explore.ts @@ -682,8 +682,7 @@ export const exploreCommand = buildListCommand("explore", { environment: { kind: "parsed", parse: String, - brief: - "Replay environment filter for --dataset replays (repeatable, comma-separated)", + brief: "Environment filter (repeatable, comma-separated)", variadic: true, optional: true, }, diff --git a/packages/cli/src/lib/env-registry.ts b/packages/cli/src/lib/env-registry.ts index e73a8075a..38130ecf5 100644 --- a/packages/cli/src/lib/env-registry.ts +++ b/packages/cli/src/lib/env-registry.ts @@ -244,6 +244,17 @@ export const ENV_VAR_REGISTRY: readonly EnvVarEntry[] = [ example: "1", devGuide: "Disable CLI telemetry (error tracking)", }, + { + name: "DO_NOT_TRACK", + description: + "Industry-standard opt-out for telemetry ([consoledonottrack.com](https://consoledonottrack.com/)). When set to `1`, the CLI disables all telemetry, equivalent to `SENTRY_CLI_NO_TELEMETRY=1`.", + example: "1", + }, + { + name: "SENTRY_PIPELINE", + description: + "Identifies the Sentry build plugin that invoked the CLI (e.g., `sentry-webpack-plugin/3.0.0`). Used internally by `sentry build upload` to attribute uploads to the correct plugin. You typically don't need to set this manually — it is set automatically by Sentry build integrations.", + }, { name: "SENTRY_CLI_NO_UPDATE_CHECK", description: diff --git a/packages/cli/test/script/check-env-coverage.test.ts b/packages/cli/test/script/check-env-coverage.test.ts new file mode 100644 index 000000000..7e3a8596b --- /dev/null +++ b/packages/cli/test/script/check-env-coverage.test.ts @@ -0,0 +1,30 @@ +/** + * Tests for the env var documentation coverage check. + * + * The check (script/check-env-coverage.ts) fails CI when the CLI reads a + * user-facing env var that isn't in ENV_VAR_REGISTRY or the internal allowlist. + * It runs as a standalone script with top-level await and process.exit, so we + * exercise it as a subprocess against the real source tree. + */ + +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; + +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +function runCheck() { + return spawnSync("pnpm", ["tsx", "script/check-env-coverage.ts"], { + cwd: pkgRoot, + encoding: "utf-8", + }); +} + +describe("check-env-coverage", () => { + test("passes against the current source tree", () => { + const result = runCheck(); + expect(result.status, result.stdout + result.stderr).toBe(0); + expect(result.stdout).toContain("user-facing env var(s) read in src/"); + }); +});