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
20 changes: 20 additions & 0 deletions docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ sentry log list --follow
sentry log list --query "severity:error"
```

### Capture Events Locally (Spotlight)

```bash
# Run the app with the local server auto-enabled; tail errors/traces/logs.
# No DSN needed — with no DSN, events go ONLY to the local server (nothing
# reaches the user's Sentry org, no production quota). With a DSN set, the
# SDK sends to both.
sentry local run -- npm run dev # or: python manage.py runserver, etc.

# Watch only AI/agent (gen_ai, mcp) spans while iterating on an agent.
sentry local -f ai

# Server-side SDKs read SENTRY_SPOTLIGHT automatically. The CLI also injects
# the URL under every framework client prefix (NEXT_PUBLIC_, VITE_, PUBLIC_,
# NUXT_PUBLIC_, REACT_APP_, VUE_APP_, GATSBY_). Until the browser SDK reads
# these automatically (getsentry/sentry-javascript#18198), reference the var
# matching your framework in the client config:
# Sentry.init({ spotlight: process.env.NEXT_PUBLIC_SENTRY_SPOTLIGHT ?? false })
```

### Explore the API Schema

```bash
Expand Down
23 changes: 22 additions & 1 deletion docs/src/fragments/commands/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

`sentry local` runs a local development server that captures Sentry SDK envelopes from your dev stack and surfaces errors, traces, and logs in real time — right in your terminal. No authentication required.

No DSN is required either. If your app has no DSN configured, events flow **only** to the local server — nothing reaches your Sentry organization and no production quota is used. If a DSN *is* set, the SDK sends to both Sentry and the local server.

If a server is already running on the port, the command attaches as an SSE consumer instead of starting a duplicate.

## Examples
Expand Down Expand Up @@ -33,9 +35,21 @@ Env vars injected into the child process:
| Variable | Value |
|----------|-------|
| `SENTRY_SPOTLIGHT` | `http://localhost:<port>/stream` |
| `NEXT_PUBLIC_SENTRY_SPOTLIGHT` | `http://localhost:<port>/stream` |
| `<PREFIX>SENTRY_SPOTLIGHT` | `http://localhost:<port>/stream` |
| `SENTRY_TRACES_SAMPLE_RATE` | `1` (unless already set) |

The `<PREFIX>` variants cover every common framework client prefix so the spotlight URL is inlined into your browser bundle no matter which bundler you use: `PUBLIC_` (SvelteKit, Astro, Qwik), `NEXT_PUBLIC_` (Next.js), `VITE_` (Vite), `NUXT_PUBLIC_` (Nuxt), `REACT_APP_` (Create React App), `VUE_APP_` (Vue CLI), and `GATSBY_` (Gatsby).

**Server vs. client.** Server-side SDKs (`@sentry/node`, Python, and friends) read `SENTRY_SPOTLIGHT` automatically — no code changes needed.

For browser/client events, the CLI exposes the spotlight URL under every framework client prefix above. Once the [browser SDK reads these variables automatically](https://github.com/getsentry/sentry-javascript/pull/18198), client-side capture will be zero-config too. **Until then**, reference the variable matching your framework in your client config:

```ts
// Next.js example — other frameworks use their own env access pattern
// (e.g. import.meta.env.VITE_SENTRY_SPOTLIGHT for Vite-based frameworks).
Sentry.init({ spotlight: process.env.NEXT_PUBLIC_SENTRY_SPOTLIGHT ?? false });
```

## Endpoints

| Method | Path | Description |
Expand Down Expand Up @@ -78,6 +92,13 @@ Use `--quiet` to suppress tail output entirely if you only need the SSE stream.

GenAI operations show the model name, MCP tool calls show the tool being invoked, and database queries show the system and query summary. This works automatically when your Sentry SDK is configured with AI/LLM integrations.

To watch only agent activity, filter to the `ai` item type:

```bash
sentry local -f ai # only AI/agent spans
sentry local -f ai -f error # agent spans and errors
```

## JSON output

Use `--format json` (or `-F json`) for machine-readable NDJSON output, one JSON object per envelope item:
Expand Down
20 changes: 20 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ sentry log list --follow
sentry log list --query "severity:error"
```

#### Capture Events Locally (Spotlight)

```bash
# Run the app with the local server auto-enabled; tail errors/traces/logs.
# No DSN needed — with no DSN, events go ONLY to the local server (nothing
# reaches the user's Sentry org, no production quota). With a DSN set, the
# SDK sends to both.
sentry local run -- npm run dev # or: python manage.py runserver, etc.

# Watch only AI/agent (gen_ai, mcp) spans while iterating on an agent.
sentry local -f ai

# Server-side SDKs read SENTRY_SPOTLIGHT automatically. The CLI also injects
# the URL under every framework client prefix (NEXT_PUBLIC_, VITE_, PUBLIC_,
# NUXT_PUBLIC_, REACT_APP_, VUE_APP_, GATSBY_). Until the browser SDK reads
# these automatically (getsentry/sentry-javascript#18198), reference the var
# matching your framework in the client config:
# Sentry.init({ spotlight: process.env.NEXT_PUBLIC_SENTRY_SPOTLIGHT ?? false })
```

#### Explore the API Schema

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ View a dashboard
- `-w, --web - Open in browser`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-r, --refresh <value> - Auto-refresh interval in seconds (default: 60, min: 10)`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01"`

**Examples:**

Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/event.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ List events for an issue
- `-n, --limit <value> - Number of events (1-1000) - (default: "25")`
- `-q, --query <value> - Search query (Sentry search syntax)`
- `--full - Include full event body (stacktraces)`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/explore.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Query aggregate event data (Explore)
- `-s, --sort <value> - Sort field (prefix with - for desc, e.g., "-count()")`
- `-e, --environment <value>... - Replay environment filter for --dataset replays (repeatable, comma-separated)`
- `-n, --limit <value> - Number of rows (1-1000) - (default: "25")`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "24h")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "24h")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ List issues in a project
- `-q, --query <value> - Search query (Sentry syntax, implicit AND, no OR operator)`
- `-n, --limit <value> - Maximum number of issues to list - (default: "25")`
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "90d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "90d")`
- `-c, --cursor <value> - Pagination cursor (use "next" for next page, "prev" for previous)`
- `--compact - Single-line rows for compact output (auto-detects if omitted)`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
Expand Down Expand Up @@ -87,7 +87,7 @@ List events for a specific issue
- `-n, --limit <value> - Number of events (1-1000) - (default: "25")`
- `-q, --query <value> - Search query (Sentry search syntax)`
- `--full - Include full event body (stacktraces)`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down
3 changes: 3 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ sentry local --quiet

sentry local -f error -f log # only errors and logs

sentry local -f ai # only AI/agent spans
sentry local -f ai -f error # agent spans and errors

sentry local --format json
```

Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ List logs from a project
- `-n, --limit <value> - Number of log entries (1-1000) - (default: "100")`
- `-q, --query <value> - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")`
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01"`
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
- `--fresh - Bypass cache, re-detect projects, and fetch fresh data`

Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ List recent Session Replays
- `-q, --query <value> - Search query (Sentry replay search syntax)`
- `-e, --environment <value>... - Filter by environment (repeatable, comma-separated)`
- `-s, --sort <value> - Sort by: date, oldest, duration, errors, activity, or a raw replay sort field - (default: "date")`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down
2 changes: 1 addition & 1 deletion plugins/sentry-cli/skills/sentry-cli/references/span.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ List spans in a project or trace
- `-n, --limit <value> - Number of spans (<=1000) - (default: "25")`
- `-q, --query <value> - Filter spans (e.g., "op:db", "project:backend", "project:[cli,api]")`
- `-s, --sort <value> - Sort order: date, duration - (default: "date")`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/trace.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ List recent traces in a project
- `-n, --limit <value> - Number of traces (1-1000) - (default: "25")`
- `-q, --query <value> - Search query (Sentry search syntax)`
- `-s, --sort <value> - Sort by: date, duration - (default: "date")`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "7d")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

Expand Down Expand Up @@ -91,7 +91,7 @@ View logs associated with a trace

**Flags:**
- `-w, --web - Open trace in browser`
- `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "14d")`
- `-t, --period <value> - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "14d")`
- `-n, --limit <value> - Number of log entries (<=1000) - (default: "100")`
- `-q, --query <value> - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")`
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
Expand Down
43 changes: 39 additions & 4 deletions src/commands/local/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
* sentry local run
*
* Run a command with the local dev server enabled. Injects
* `SENTRY_SPOTLIGHT` into the child process environment so the Sentry SDK
* auto-sends envelopes to the local server.
* `SENTRY_SPOTLIGHT` (read automatically by server-side SDKs) plus the
* framework-prefixed client variants (`NEXT_PUBLIC_SENTRY_SPOTLIGHT`,
* `VITE_SENTRY_SPOTLIGHT`, etc.) so the spotlight URL also reaches
* browser bundles regardless of bundler.
*
* If no server is already running on the target port, one is started
* automatically in the background and shut down when the child exits.
Expand Down Expand Up @@ -33,6 +35,27 @@ type RunFlags = {
/** Buffer size for the auto-started background server. */
const BUFFER_SIZE = 500;

/**
* Client-side env var prefixes that frameworks inline into browser bundles
* at build time. We inject `<PREFIX>SENTRY_SPOTLIGHT` for every variant so the
* spotlight URL reaches whichever bundler the user's app uses.
*
* Mirrors the prefixes the Sentry browser SDK is intended to read for
* Spotlight configuration (see getsentry/sentry-javascript#18198). Note this
* set differs from the DSN-detection prefixes in `src/lib/dsn/env.ts`: it adds
* `PUBLIC_` (SvelteKit/Astro/Qwik), `VUE_APP_` (Vue CLI), and `GATSBY_`
* (Gatsby), and omits `EXPO_PUBLIC_` (React Native has no browser bundle).
*/
export const CLIENT_SPOTLIGHT_PREFIXES = [
"PUBLIC_", // SvelteKit, Astro, Qwik
"NEXT_PUBLIC_", // Next.js
"VITE_", // Vite
"NUXT_PUBLIC_", // Nuxt
"REACT_APP_", // Create React App
"VUE_APP_", // Vue CLI
"GATSBY_", // Gatsby
] as const;

/**
* Shut down a background server, closing all connections so keep-alive
* sockets (e.g. SSE subscribers) don't block exit.
Expand All @@ -56,7 +79,9 @@ export const runCommand = buildCommand({
"If no server is already listening on the port, one is started\n" +
"automatically and shut down when the child process exits.\n\n" +
"The child process inherits all current env vars plus\n" +
"SENTRY_SPOTLIGHT and SENTRY_TRACES_SAMPLE_RATE=1.\n\n" +
"SENTRY_SPOTLIGHT (server-side SDKs read this automatically), the\n" +
"framework-prefixed client variants (NEXT_PUBLIC_, VITE_, etc.), and\n" +
"SENTRY_TRACES_SAMPLE_RATE=1.\n\n" +
"Example:\n" +
" sentry local run -- npm run dev\n" +
" sentry local run -- python manage.py runserver",
Expand Down Expand Up @@ -124,11 +149,21 @@ export const runCommand = buildCommand({
let child: ChildProcess;
try {
const [cmd = "", ...cmdArgs] = args;
// Expose the spotlight URL under every framework client prefix so it's
// inlined into browser bundles regardless of bundler. `SENTRY_SPOTLIGHT`
// is set last so the base name (read by server-side SDKs) is never
// shadowed by a client variant.
const clientSpotlightVars = Object.fromEntries(
CLIENT_SPOTLIGHT_PREFIXES.map((prefix) => [
`${prefix}SENTRY_SPOTLIGHT`,
spotlightUrl,
])
);
child = spawn(cmd, cmdArgs, {
env: {
...process.env,
...clientSpotlightVars,
SENTRY_SPOTLIGHT: spotlightUrl,
NEXT_PUBLIC_SENTRY_SPOTLIGHT: spotlightUrl,
SENTRY_TRACES_SAMPLE_RATE:
process.env.SENTRY_TRACES_SAMPLE_RATE ?? "1",
},
Expand Down
56 changes: 54 additions & 2 deletions test/commands/local/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,36 @@
* exit code propagation, signal handling, and error cases.
*/

import { describe, expect, test, vi } from "vitest";
import { runCommand } from "../../../src/commands/local/run.js";
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
CLIENT_SPOTLIGHT_PREFIXES,
runCommand,
} from "../../../src/commands/local/run.js";
import { CliError, ValidationError } from "../../../src/lib/errors.js";

/**
* Records the env passed to the most recent `spawn` call so tests can assert
* which variables were injected into the child process. The mock below still
* delegates to the real `spawn`, so commands like `printenv`/`true` run for
* real and exit codes propagate normally.
*/
const spawnCapture: { env?: NodeJS.ProcessEnv } = {};

vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (
cmd: string,
args: readonly string[],
options: Parameters<typeof actual.spawn>[2]
) => {
spawnCapture.env = (options as { env?: NodeJS.ProcessEnv })?.env;
return actual.spawn(cmd, args as string[], options);
},
};
});

type RunFunc = (
this: unknown,
flags: { port: number; host: string },
Expand All @@ -24,6 +50,10 @@ function makeContext() {
}

describe("sentry local run", () => {
beforeEach(() => {
spawnCapture.env = undefined;
});

test("throws ValidationError when no command provided", async () => {
const func = (await runCommand.loader()) as unknown as RunFunc;
const ctx = makeContext();
Expand Down Expand Up @@ -123,4 +153,26 @@ describe("sentry local run", () => {
// "-- true" should strip "--" and run "true" successfully
await func.call(ctx, { port: 19_880, host: "127.0.0.1" }, "--", "true");
});

test("injects spotlight URL under every framework client prefix", async () => {
const func = (await runCommand.loader()) as unknown as RunFunc;
const ctx = makeContext();

const port = 19_881;
const host = "127.0.0.1";
const expectedUrl = `http://${host}:${port}/stream`;

// `node:child_process` is mocked at module scope (see vi.mock below). The
// mock records the env handed to spawn so we can assert against it.
await func.call(ctx, { port, host }, "printenv");

const capturedEnv = spawnCapture.env;
expect(capturedEnv).toBeDefined();
// Base name read by server-side SDKs.
expect(capturedEnv?.SENTRY_SPOTLIGHT).toBe(expectedUrl);
// Every framework client variant points at the same URL.
for (const prefix of CLIENT_SPOTLIGHT_PREFIXES) {
expect(capturedEnv?.[`${prefix}SENTRY_SPOTLIGHT`]).toBe(expectedUrl);
}
});
});
Loading