Skip to content

feat(local): add command to run a local Spotlight sidecar#888

Open
MathurAditya724 wants to merge 1 commit intomainfrom
feat/local-spotlight-command
Open

feat(local): add command to run a local Spotlight sidecar#888
MathurAditya724 wants to merge 1 commit intomainfrom
feat/local-spotlight-command

Conversation

@MathurAditya724
Copy link
Copy Markdown
Member

Summary

Adds sentry local, a long-running command that starts a minimal Hono HTTP server wire-compatible with the Spotlight sidecar protocol. It uses @spotlightjs/spotlight/sdk's createSpotlightBuffer and pushToSpotlightBuffer helpers to ingest envelopes from any Sentry SDK running in the user's dev stack and tails them to the terminal.

This gives users a one-command path to "Sentry for development" without leaving the CLI: errors, traces, and logs from their dev stack stream straight into their shell, and the SSE endpoint stays compatible with the Spotlight overlay for richer rendering.

What's new

  • src/commands/local.ts — new command. Flags: --port / -p (default 8969), --host / -H (default localhost), --open / -o, --quiet / -q. Runs without auth.
  • src/app.ts — wires local into the top-level route map.
  • package.json — adds @spotlightjs/spotlight, hono, and @hono/node-server as devDependencies (per the no-runtime-deps rule; everything is bundled at build time).
  • docs/src/fragments/commands/local.md — hand-written examples + endpoint table.

Endpoints exposed

Method Path Description
POST /stream Spotlight-compatible envelope ingest
POST /api/{projectId}/envelope/ Sentry SDK ingest path
GET /stream Server-Sent Events feed (Spotlight overlay)
GET /health Liveness check

Why a thin in-tree server instead of spawning npx @spotlightjs/spotlight

The SDK helpers give us decompression (gzip/deflate/br) + lazy envelope parsing for free, while keeping the command's surface focused on a CLI-friendly tail UX. Bundling through esbuild also keeps the published binary self-contained per the no-runtime-dependencies rule — users don't need a separate npx install or a network connection on first run.

Testing

  • bun run typecheck
  • bun x ultracite check (only the pre-existing unrelated markdown.ts warning)
  • bun run check:deps / check:fragments / check:errors / check:docs-sections
  • bun test --timeout 15000 --isolate --parallel test/lib test/commands test/types — 6343 pass / 0 fail
  • SENTRY_CLIENT_ID=test bun run script/bundle.ts — bundle builds cleanly
  • ✅ Manual smoke: started the sidecar, posted an envelope to /stream, observed 204 No Content and the tail line HH:MM:SS.sss • event appear; SIGTERM shut down gracefully. Same flow against the bundled node dist/bin.cjs.

Out of scope

  • The Spotlight overlay UI itself — setupSpotlight() from the upstream package can serve it but pulls in MCP / @sentry/node / a much larger dependency tree; this PR sticks to the small Hono server to keep the CLI lean.
  • Persistent storage of envelopes — the buffer is in-memory, capped at 500 items.

Adds 'sentry local', a long-running command that starts a minimal Hono
HTTP server wire-compatible with the Spotlight sidecar protocol. The
server uses @spotlightjs/spotlight/sdk's createSpotlightBuffer +
pushToSpotlightBuffer helpers to ingest envelopes from any Sentry SDK
running in the user's dev stack and tails them to the terminal.

Endpoints exposed:
  POST /stream                          - Spotlight ingest
  POST /api/{projectId}/envelope/       - Sentry SDK ingest path
  GET  /stream                          - SSE feed for the Spotlight overlay
  GET  /health                          - liveness check

Why a thin in-tree server instead of spawning npx @spotlightjs/spotlight:
the SDK helpers give us decompression + lazy parsing for free while
keeping the surface focused on a CLI-friendly tail UX, and bundling
through esbuild keeps the published binary self-contained per the
no-runtime-dependencies rule.

The command runs without auth (it's a local dev tool) and shuts down
gracefully on SIGINT/SIGTERM, force-closing keep-alive connections so
SSE subscribers don't block exit.
@github-actions
Copy link
Copy Markdown
Contributor

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-888/

Built to branch gh-pages at 2026-04-29 20:40 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions
Copy link
Copy Markdown
Contributor

Codecov Results 📊

6343 passed | Total: 6343 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

❌ Patch coverage is 25.10%. Project has 13230 uncovered lines.
❌ Project coverage is 75.8%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
src/commands/local.ts 24.50% ⚠️ 188 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    76.03%    75.80%    -0.23%
==========================================
  Files          294       295        +1
  Lines        54404     54659      +255
  Branches         0         0         —
==========================================
+ Hits         41362     41429       +67
- Misses       13042     13230      +188
- Partials         0         0         —

Generated by Codecov Action

Comment thread src/commands/local.ts
Comment on lines +261 to +262
process.once("SIGINT", () => shutdown("SIGINT"));
process.once("SIGTERM", () => shutdown("SIGTERM"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: process.once removes the SIGINT handler after one use, so the intended force-exit logic on a second signal is never triggered.
Severity: MEDIUM

Suggested Fix

Replace process.once('SIGINT', ...) with process.on('SIGINT', ...). This will keep the handler registered, allowing the if (shuttingDown) check to correctly detect a second signal and trigger the intended force-exit by calling process.exit(0).

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/commands/local.ts#L261-L262

Potential issue: The code uses `process.once('SIGINT', ...)` to handle shutdown signals.
This correctly initiates a graceful shutdown on the first signal. However, the handler
is removed after its first invocation. The code intends for a second signal to trigger a
force-exit via `if (shuttingDown) { process.exit(0); }`, bypassing shutdown hooks for
stuck connections. Because the handler is removed, a second `SIGINT` signal does not
trigger this logic, making the force-exit path unreachable. Instead, Node.js's default
exit behavior is invoked, which does not bypass the hooks as intended.

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant