Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 16 additions & 10 deletions apps/cli-docs/src/content/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions apps/cli-docs/src/fragments/commands/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions apps/cli-docs/src/fragments/commands/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",

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.

Missing root check script alias

Low Severity

check:env-coverage was added under packages/cli but not mirrored in the workspace root package.json, unlike every other check:* script. From the repo root, pnpm run check:env-coverage fails, so the new guard is harder to run the same way as check:deps, check:errors, and peers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ac61c1. Configure here.

"check:stale-refs": "pnpm tsx script/check-stale-references.ts"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key value...>`

View and manage default settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Query aggregate event data (Explore)
- `-d, --dataset <value> - Dataset to query (errors, spans, metrics, logs, replays) - (default: "errors")`
- `-q, --query <value> - Search query (Sentry search syntax)`
- `-s, --sort <value> - Sort field (prefix with - for desc, e.g., "-count()")`
- `-e, --environment <value>... - Replay environment filter for --dataset replays (repeatable, comma-separated)`
- `-e, --environment <value>... - Environment filter (repeatable, comma-separated)`
- `-n, --limit <value> - Number of rows (1-1000) - (default: "25")`
- `-t, --period <value> - 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`
Expand Down
192 changes: 192 additions & 0 deletions packages/cli/script/check-env-coverage.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>([
[
"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<string, string>();
const fileContents = new Map<string, string>();
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<string, string>();

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).`
);
2 changes: 1 addition & 1 deletion packages/cli/script/generate-docs-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/lib/env-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/test/script/check-env-coverage.test.ts
Original file line number Diff line number Diff line change
@@ -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/");
});
});
Loading