diff --git a/docs/src/content/docs/agent-guidance.md b/docs/src/content/docs/agent-guidance.md index e64da068e..abed4318c 100644 --- a/docs/src/content/docs/agent-guidance.md +++ b/docs/src/content/docs/agent-guidance.md @@ -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 diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index 343de505d..9badf81d9 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -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 @@ -33,9 +35,21 @@ Env vars injected into the child process: | Variable | Value | |----------|-------| | `SENTRY_SPOTLIGHT` | `http://localhost:/stream` | -| `NEXT_PUBLIC_SENTRY_SPOTLIGHT` | `http://localhost:/stream` | +| `SENTRY_SPOTLIGHT` | `http://localhost:/stream` | | `SENTRY_TRACES_SAMPLE_RATE` | `1` (unless already set) | +The `` 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 | @@ -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: diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 410349511..a7cfe336d 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -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 diff --git a/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md b/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md index 4464f607b..54c0d3e27 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md @@ -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 - Auto-refresh interval in seconds (default: 60, min: 10)` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"` +- `-t, --period - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01"` **Examples:** diff --git a/plugins/sentry-cli/skills/sentry-cli/references/event.md b/plugins/sentry-cli/skills/sentry-cli/references/event.md index e17fb3f64..794369af3 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/event.md @@ -37,7 +37,7 @@ List events for an issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/plugins/sentry-cli/skills/sentry-cli/references/explore.md index aa0dc82fd..a2d597693 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -24,7 +24,7 @@ Query aggregate event data (Explore) - `-s, --sort - Sort field (prefix with - for desc, e.g., "-count()")` - `-e, --environment ... - Replay environment filter for --dataset replays (repeatable, comma-separated)` - `-n, --limit - Number of rows (1-1000) - (default: "25")` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "24h")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 8930e772a..51f74aa71 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -19,7 +19,7 @@ List issues in a project - `-q, --query - Search query (Sentry syntax, implicit AND, no OR operator)` - `-n, --limit - Maximum number of issues to list - (default: "25")` - `-s, --sort - Sort by: date, new, freq, user - (default: "date")` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "90d")` +- `-t, --period - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "90d")` - `-c, --cursor - 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` @@ -87,7 +87,7 @@ List events for a specific issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 647aed124..d9d602eae 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -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 ``` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/log.md b/plugins/sentry-cli/skills/sentry-cli/references/log.md index 54355ca2e..f007ccb23 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/log.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/log.md @@ -19,7 +19,7 @@ List logs from a project - `-n, --limit - Number of log entries (1-1000) - (default: "100")` - `-q, --query - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")` - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"` +- `-t, --period - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01"` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` - `--fresh - Bypass cache, re-detect projects, and fetch fresh data` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/replay.md b/plugins/sentry-cli/skills/sentry-cli/references/replay.md index 224b9902b..8da82836f 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/replay.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/replay.md @@ -20,7 +20,7 @@ List recent Session Replays - `-q, --query - Search query (Sentry replay search syntax)` - `-e, --environment ... - Filter by environment (repeatable, comma-separated)` - `-s, --sort - Sort by: date, oldest, duration, errors, activity, or a raw replay sort field - (default: "date")` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/span.md b/plugins/sentry-cli/skills/sentry-cli/references/span.md index fe03b4aa0..aa67d265d 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/span.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/span.md @@ -19,7 +19,7 @@ List spans in a project or trace - `-n, --limit - Number of spans (<=1000) - (default: "25")` - `-q, --query - Filter spans (e.g., "op:db", "project:backend", "project:[cli,api]")` - `-s, --sort - Sort order: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 1baaf028a..74386e5a8 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -19,7 +19,7 @@ List recent traces in a project - `-n, --limit - Number of traces (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort by: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` +- `-t, --period - 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 - Navigate pages: "next", "prev", "first" (or raw cursor string)` @@ -91,7 +91,7 @@ View logs associated with a trace **Flags:** - `-w, --web - Open trace in browser` -- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "14d")` +- `-t, --period - Time range: "7d", "2026-05-01..2026-06-01", ">=2026-05-01" - (default: "14d")` - `-n, --limit - Number of log entries (<=1000) - (default: "100")` - `-q, --query - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index aec2773c6..3411b3e35 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -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. @@ -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 `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. @@ -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", @@ -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", }, diff --git a/test/commands/local/run.test.ts b/test/commands/local/run.test.ts index f2fbc2a57..7d2e9650b 100644 --- a/test/commands/local/run.test.ts +++ b/test/commands/local/run.test.ts @@ -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(); + return { + ...actual, + spawn: ( + cmd: string, + args: readonly string[], + options: Parameters[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 }, @@ -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(); @@ -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); + } + }); });