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
4 changes: 2 additions & 2 deletions docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Best practices and operational guidance for AI coding agents using the Sentry CL
- **Use `sentry schema` to explore the API** — if you need to discover API endpoints, run `sentry schema` to browse interactively or `sentry schema <resource>` to search. This is faster than fetching OpenAPI specs externally.
- **Use `sentry issue view <id>` to investigate issues** — when asked about a specific issue (e.g., `CLI-G5`, `PROJECT-123`), use `sentry issue view` directly.
- **Use `--json` for machine-readable output** — pipe through `jq` for filtering. Human-readable output includes formatting that is hard to parse.
- **The CLI auto-detects org/project** — most commands work without explicit targets by scanning for DSNs in `.env` files, source code, config defaults, and directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
- **The CLI auto-detects org/project** — most commands work without explicit targets by checking `.sentryclirc` config files, scanning for DSNs in `.env` files and source code, and matching directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.

## Design Principles

Expand Down Expand Up @@ -213,7 +213,7 @@ When querying the Events API (directly or via `sentry api`), valid dataset value
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.
- **Pre-authenticating unnecessarily**: Don't run `sentry auth login` before every command. The CLI detects missing/expired auth and prompts automatically. Only run `sentry auth login` if you need to switch accounts.
- **Missing `--json` for piping**: Human-readable output includes formatting. Use `--json` when parsing output programmatically.
- **Specifying org/project when not needed**: Auto-detection resolves org/project from DSNs, env vars, config defaults, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
- **Specifying org/project when not needed**: Auto-detection resolves org/project from `.sentryclirc` config files, DSNs, env vars, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
- **Confusing `--query` syntax**: The `--query` flag uses Sentry search syntax (e.g., `is:unresolved`, `assigned:me`), not free text search.
- **Not using `--web`**: View commands support `-w`/`--web` to open the resource in the browser — useful for sharing links.
- **Fetching API schemas instead of using the CLI**: Prefer `sentry schema` to browse the API and `sentry api` to make requests — the CLI handles authentication and endpoint resolution, so there's rarely a need to download OpenAPI specs separately.
Expand Down
62 changes: 60 additions & 2 deletions docs/src/content/docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,67 @@
---
title: Configuration
description: Environment variables and configuration options for the Sentry CLI
description: Environment variables, config files, and configuration options for the Sentry CLI
---

The Sentry CLI can be configured through environment variables and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`.
The Sentry CLI can be configured through config files, environment variables, and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`.

## Configuration File (`.sentryclirc`)

The CLI supports a `.sentryclirc` config file using standard INI syntax. This is the same format used by the legacy `sentry-cli` tool, so existing config files are automatically picked up.

### How It Works

The CLI looks for `.sentryclirc` files by walking up from your current directory toward the filesystem root. If multiple files are found, values from the closest file take priority, with `~/.sentryclirc` serving as a global fallback.

```ini
[defaults]
org = my-org
project = my-project

[auth]
token = sntrys_...
```

### Supported Fields

| Section | Key | Description |
|---------|-----|-------------|
| `[defaults]` | `org` | Default organization slug |
| `[defaults]` | `project` | Default project slug |
| `[defaults]` | `url` | Sentry base URL (for self-hosted) |
| `[auth]` | `token` | Auth token (mapped to `SENTRY_AUTH_TOKEN`) |

### Monorepo Setup

In monorepos, place a `.sentryclirc` at the repo root with your org, then add per-package configs with just the project:

```
my-monorepo/
.sentryclirc # [defaults] org = my-company
packages/
frontend/
.sentryclirc # [defaults] project = frontend-web
backend/
.sentryclirc # [defaults] project = backend-api
```

When you run a command from `packages/frontend/`, the CLI resolves `org = my-company` from the root and `project = frontend-web` from the closest file.

### Resolution Priority

When the CLI needs to determine your org and project, it checks these sources in order:

1. **Explicit CLI arguments** — `sentry issue list my-org/my-project`
2. **Environment variables** — `SENTRY_ORG` / `SENTRY_PROJECT`
3. **`.sentryclirc` config file** — walked up from CWD, merged with `~/.sentryclirc`
4. **DSN auto-detection** — scans source code and `.env` files
5. **Directory name inference** — matches your directory name against project slugs

The first source that provides both org and project wins. For org-only commands, only the org is needed.

### Backward Compatibility

If you previously used the legacy `sentry-cli` and have a `~/.sentryclirc` file, the new CLI reads it automatically. The `[defaults]` and `[auth]` sections are fully compatible. The `[auth] token` value is mapped to the `SENTRY_AUTH_TOKEN` environment variable internally (only if the env var is not already set).

## Environment Variables

Expand Down
6 changes: 5 additions & 1 deletion docs/src/content/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The Sentry CLI includes several features designed to streamline your workflow, e

## DSN Auto-Detection

The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify the target for every command.
The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify the target for every command. DSN detection is one part of the [resolution priority chain](./configuration/#resolution-priority) — it runs after checking for explicit arguments, environment variables, and `.sentryclirc` config files.

### How It Works

Expand All @@ -19,6 +19,10 @@ DSN detection follows this priority order (highest first):

When a DSN is found, the CLI resolves it to your organization and project, then caches the result for fast subsequent lookups.

:::tip
For monorepos or when DSN detection picks up the wrong project, use a [`.sentryclirc` config file](./configuration/#configuration-file-sentryclirc) to pin your org/project explicitly.
:::

### Supported Languages

The CLI can detect DSNs from source code in these languages:
Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Best practices and operational guidance for AI coding agents using the Sentry CL
- **Use `sentry schema` to explore the API** — if you need to discover API endpoints, run `sentry schema` to browse interactively or `sentry schema <resource>` to search. This is faster than fetching OpenAPI specs externally.
- **Use `sentry issue view <id>` to investigate issues** — when asked about a specific issue (e.g., `CLI-G5`, `PROJECT-123`), use `sentry issue view` directly.
- **Use `--json` for machine-readable output** — pipe through `jq` for filtering. Human-readable output includes formatting that is hard to parse.
- **The CLI auto-detects org/project** — most commands work without explicit targets by scanning for DSNs in `.env` files, source code, config defaults, and directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
- **The CLI auto-detects org/project** — most commands work without explicit targets by checking `.sentryclirc` config files, scanning for DSNs in `.env` files and source code, and matching directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.

### Design Principles

Expand Down Expand Up @@ -223,7 +223,7 @@ When querying the Events API (directly or via `sentry api`), valid dataset value
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.
- **Pre-authenticating unnecessarily**: Don't run `sentry auth login` before every command. The CLI detects missing/expired auth and prompts automatically. Only run `sentry auth login` if you need to switch accounts.
- **Missing `--json` for piping**: Human-readable output includes formatting. Use `--json` when parsing output programmatically.
- **Specifying org/project when not needed**: Auto-detection resolves org/project from DSNs, env vars, config defaults, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
- **Specifying org/project when not needed**: Auto-detection resolves org/project from `.sentryclirc` config files, DSNs, env vars, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
- **Confusing `--query` syntax**: The `--query` flag uses Sentry search syntax (e.g., `is:unresolved`, `assigned:me`), not free text search.
- **Not using `--web`**: View commands support `-w`/`--web` to open the resource in the browser — useful for sharing links.
- **Fetching API schemas instead of using the CLI**: Prefer `sentry schema` to browse the API and `sentry api` to make requests — the CLI handles authentication and endpoint resolution, so there's rarely a need to download OpenAPI specs separately.
Expand Down
37 changes: 36 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@
*/

import { getEnv } from "./lib/env.js";
import { applySentryCliRcEnvShim } from "./lib/sentryclirc.js";

/**
* Preload project context: walk up from `cwd` once, finding both the
* project root (for DSN detection) and `.sentryclirc` config (for
* org/project defaults and env shim). Caches both results so later calls
* to `findProjectRoot` and `loadSentryCliRc` are cache hits.
*/
async function preloadProjectContext(cwd: string): Promise<void> {
// Dynamic import keeps the heavy DSN/DB modules out of the completion fast-path
const [{ findProjectRoot }, { setCachedProjectRoot }] = await Promise.all([
import("./lib/dsn/project-root.js"),
import("./lib/db/project-root-cache.js"),
]);

const result = await findProjectRoot(cwd);
await setCachedProjectRoot(cwd, {
projectRoot: result.projectRoot,
reason: result.reason,
});

// Apply .sentryclirc env shim (token, URL) — sentryclirc cache was
// populated as a side effect of findProjectRoot's walk
await applySentryCliRcEnvShim(cwd);
}

/**
* Fast-path: shell completion.
Expand Down Expand Up @@ -401,16 +426,26 @@ export async function runCli(cliArgs: string[]): Promise<void> {
* Reads `process.argv`, dispatches to the completion fast-path or the full
* CLI runner, and handles fatal errors. Called from `bin.ts`.
*/
export function startCli(): Promise<void> {
export async function startCli(): Promise<void> {
const args = process.argv.slice(2);

// Completions are a fast-path (~1ms) — skip .sentryclirc I/O.
if (args[0] === "__complete") {
return runCompletion(args.slice(1)).catch(() => {
// Completions should never crash — silently return no results
process.exitCode = 0;
});
}

// Walk up from CWD once to find project root AND .sentryclirc config.
// Caches both so later findProjectRoot / loadSentryCliRc calls are hits.
// Non-fatal — the CLI can still work via env vars and DSN detection.
try {
await preloadProjectContext(process.cwd());
} catch {
// Gracefully degrade: project context is optional for CLI operation.
}

return runCli(args).catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exitCode = 1;
Expand Down
117 changes: 64 additions & 53 deletions src/lib/dsn/project-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@

import { opendir, stat } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { join, resolve } from "node:path";
import { anyTrue } from "../promises.js";
import {
applyGlobalFallbacks,
applySentryCliRcDir,
createSentryCliRcConfig,
CONFIG_FILENAME as SENTRYCLIRC_FILENAME,
type SentryCliRcConfig,
setSentryCliRcCache,
} from "../sentryclirc.js";
import { withFsSpan, withTracingSpan } from "../telemetry.js";
import { walkUpFrom } from "../walk-up.js";
import { ENV_FILES, extractDsnFromEnvContent } from "./env-file.js";
import { handleFileError } from "./fs-utils.js";
import { createDetectedDsn } from "./parser.js";
Expand Down Expand Up @@ -74,6 +83,9 @@ const CI_MARKERS = [

/** Language/package markers - strong project boundary */
const LANGUAGE_MARKERS = [
// Sentry CLI config — treated as a project boundary (not definitive root,
// so the walk continues past it to find VCS markers in monorepos)
SENTRYCLIRC_FILENAME,
// JavaScript/Node ecosystem
"package.json",
"deno.json",
Expand Down Expand Up @@ -393,14 +405,6 @@ function selectProjectRoot(
return { projectRoot: fallback, reason: "fallback" };
}

/** State tracked during directory walk-up */
type WalkState = {
currentDir: string;
levelsTraversed: number;
languageMarkerAt: string | null;
buildSystemAt: string | null;
};

/**
* Create result when DSN is found in .env file
*/
Expand Down Expand Up @@ -447,90 +451,97 @@ function createRepoRootResult(
};
}

/**
* Finalize accumulated sentryclirc config: apply global fallbacks and cache.
*/
async function finalizeSentryCliRc(
cwd: string,
config: SentryCliRcConfig
): Promise<void> {
await applyGlobalFallbacks(config);
setSentryCliRcCache(cwd, config);
}

/**
* Walk up directories searching for project root.
*
* Loop logic:
* 1. Always process starting directory (do-while ensures this)
* 2. Stop at stopBoundary AFTER processing it (break before moving to parent)
* 3. Stop at filesystem root (parentDir === currentDir)
* Uses the shared {@link walkUpFrom} generator for directory traversal
* (with symlink cycle detection). Also reads `.sentryclirc` files at each
* level and populates the sentryclirc cache (with global fallbacks applied),
* so that a later `loadSentryCliRc` call for the same `cwd` is a cache hit
* instead of a second walk.
*
* Stops at the `stopBoundary` (home dir) after processing it, or when the
* generator reaches the filesystem root.
*/
async function walkUpDirectories(
resolvedStart: string,
stopBoundary: string
): Promise<ProjectRootResult> {
const state: WalkState = {
currentDir: resolvedStart,
levelsTraversed: 0,
languageMarkerAt: null,
buildSystemAt: null,
};
let levelsTraversed = 0;
let languageMarkerAt: string | null = null;
let buildSystemAt: string | null = null;
const rcConfig = createSentryCliRcConfig();

// do-while ensures starting directory is always checked,
// even when it equals the stop boundary (e.g., user runs from home dir)
do {
state.levelsTraversed += 1;
for await (const currentDir of walkUpFrom(resolvedStart)) {
levelsTraversed += 1;

const { dsnResult, repoRootResult, hasLang, hasBuild } =
await processDirectoryLevel(
state.currentDir,
state.languageMarkerAt,
state.buildSystemAt
);
// Check project-root markers AND .sentryclirc in parallel
const [{ dsnResult, repoRootResult, hasLang, hasBuild }] =
await Promise.all([
processDirectoryLevel(currentDir, languageMarkerAt, buildSystemAt),
applySentryCliRcDir(rcConfig, currentDir),
]);

// 1. Check for DSN in .env files - immediate return (unless at/above home directory)
// Don't use a .env in the home directory as a project root indicator,
// as users may have global configs that shouldn't define project boundaries
if (dsnResult && state.currentDir !== stopBoundary) {
return createDsnFoundResult(
state.currentDir,
dsnResult,
state.levelsTraversed
);
if (dsnResult && currentDir !== stopBoundary) {
await finalizeSentryCliRc(resolvedStart, rcConfig);
return createDsnFoundResult(currentDir, dsnResult, levelsTraversed);
}

// 2. Check for VCS/CI markers - definitive root, stop walking
if (repoRootResult.found) {
await finalizeSentryCliRc(resolvedStart, rcConfig);
return createRepoRootResult(
state.currentDir,
currentDir,
repoRootResult.type,
state.levelsTraversed,
state.languageMarkerAt
levelsTraversed,
languageMarkerAt
);
}

// 3. Remember language marker (closest to cwd wins)
if (!state.languageMarkerAt && hasLang) {
state.languageMarkerAt = state.currentDir;
if (!languageMarkerAt && hasLang) {
languageMarkerAt = currentDir;
}

// 4. Remember build system marker (last resort)
if (!state.buildSystemAt && hasBuild) {
state.buildSystemAt = state.currentDir;
if (!buildSystemAt && hasBuild) {
buildSystemAt = currentDir;
}

// Move to parent directory (or stop if at boundary/root)
const parentDir = dirname(state.currentDir);
const shouldStop =
state.currentDir === stopBoundary || parentDir === state.currentDir;
if (shouldStop) {
// Stop at boundary after processing it (e.g., home dir)
if (currentDir === stopBoundary) {
break;
}
state.currentDir = parentDir;
// biome-ignore lint/correctness/noConstantCondition: loop exits via break
} while (true);
}

// Populate sentryclirc cache from accumulated data
setSentryCliRcCache(resolvedStart, rcConfig);

// Determine project root from candidates (priority order)
const selected = selectProjectRoot(
state.languageMarkerAt,
state.buildSystemAt,
languageMarkerAt,
buildSystemAt,
resolvedStart
);

return {
projectRoot: selected.projectRoot,
reason: selected.reason,
levelsTraversed: state.levelsTraversed,
levelsTraversed,
};
}

Expand Down
Loading
Loading