Skip to content

Commit 0559f1e

Browse files
committed
feat(local): address remaining important gaps
- Deduplicate parsePort: export from server.ts, import in run.ts - Fix signal handler leak in run.ts: store refs, remove in finally, add settled flag to prevent close/error race - Add SSE reconnection with exponential backoff and Last-Event-ID resume support in the SSE consumer - Add --format json flag for NDJSON output (machine-readable output for AI coding agents and automation tools) - Preserve existing SENTRY_TRACES_SAMPLE_RATE (use ?? fallback) - Add agent monitoring section to docs with gen_ai/mcp examples - Add JSON output section to docs - Add server.test.ts: parsePort, feedSSELine (with id tracking), buildApp (health, ingest, CORS, SSE, 413 rejection), isServerRunning - Expand run.test.ts: -- separator stripping, ENOENT handling, SENTRY_TRACES_SAMPLE_RATE preservation
1 parent 344ab58 commit 0559f1e

6 files changed

Lines changed: 717 additions & 54 deletions

File tree

docs/src/fragments/commands/local.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,32 @@ sentry local -f error -f log # only errors and logs
6464
```
6565

6666
Use `--quiet` to suppress tail output entirely if you only need the SSE stream.
67+
68+
## Agent monitoring
69+
70+
`sentry local` shows rich output for AI agent spans when your SDK instruments with [OpenTelemetry semantic attributes](https://opentelemetry.io/docs/specs/semconv/gen-ai/):
71+
72+
```
73+
14:32:01 [TRACE] [SERVER] [gen_ai] chat anthropic/claude-4-sonnet [1200ms] [5 spans]
74+
14:32:02 [TRACE] [SERVER] [mcp] tools/call search_files [320ms]
75+
14:32:03 [TRACE] [SERVER] [db] SELECT users [postgresql] [12ms]
76+
14:32:04 [ERROR] [SERVER] RateLimitError: API quota exceeded [api_client.py:42]
77+
```
78+
79+
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.
80+
81+
## JSON output
82+
83+
Use `--format json` (or `-F json`) for machine-readable NDJSON output, one JSON object per envelope item:
84+
85+
```bash
86+
sentry local --format json
87+
```
88+
89+
```json
90+
{"type":"transaction","timestamp":1700000001,"op":"gen_ai","label":"chat anthropic/claude-4-sonnet","duration_ms":1200,"span_count":5,"source":"server"}
91+
{"type":"error","timestamp":1700000002,"error_type":"RateLimitError","message":"API quota exceeded","source":"server"}
92+
{"type":"log","timestamp":1700000003,"level":"info","message":"User logged in","attributes":{"user_id":1234},"source":"server"}
93+
```
94+
95+
This is useful for AI coding agents and automation tools that need to consume Sentry events programmatically.

src/commands/local/run.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
buildApp,
2222
DEFAULT_PORT,
2323
isServerRunning,
24+
parsePort,
2425
tryListen,
2526
} from "./server.js";
2627

@@ -29,18 +30,6 @@ type RunFlags = {
2930
readonly host: string;
3031
};
3132

32-
/** Parse and validate a port number. */
33-
function parsePort(value: string): number {
34-
const port = Number(value);
35-
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
36-
throw new ValidationError(
37-
`Invalid port: ${value}. Must be an integer between 0 and 65535.`,
38-
"port"
39-
);
40-
}
41-
return port;
42-
}
43-
4433
/** Buffer size for the auto-started background server. */
4534
const BUFFER_SIZE = 500;
4635

@@ -140,7 +129,8 @@ export const runCommand = buildCommand({
140129
...process.env,
141130
SENTRY_SPOTLIGHT: spotlightUrl,
142131
NEXT_PUBLIC_SENTRY_SPOTLIGHT: spotlightUrl,
143-
SENTRY_TRACES_SAMPLE_RATE: "1",
132+
SENTRY_TRACES_SAMPLE_RATE:
133+
process.env.SENTRY_TRACES_SAMPLE_RATE ?? "1",
144134
},
145135
stdio: "inherit",
146136
});
@@ -155,23 +145,34 @@ export const runCommand = buildCommand({
155145
}
156146

157147
// Forward signals to the child so the whole process tree shuts down.
158-
const forwardSignal = (signal: NodeJS.Signals) => {
159-
child.kill(signal);
160-
};
161-
process.once("SIGINT", () => forwardSignal("SIGINT"));
162-
process.once("SIGTERM", () => forwardSignal("SIGTERM"));
148+
// Store references so handlers can be removed in finally.
149+
const onSigint = () => child.kill("SIGINT");
150+
const onSigterm = () => child.kill("SIGTERM");
151+
process.once("SIGINT", onSigint);
152+
process.once("SIGTERM", onSigterm);
163153

164154
let exitCode: number;
165155
try {
166156
exitCode = await new Promise<number>((resolve, reject) => {
167-
child.on("close", (code) => resolve(code ?? 1));
157+
let settled = false;
158+
child.on("close", (code) => {
159+
if (!settled) {
160+
settled = true;
161+
resolve(code ?? 1);
162+
}
163+
});
168164
// If spawn itself fails (e.g. ENOENT), 'close' may never fire.
169165
child.on("error", (err) => {
170166
logger.debug(`Child process error: ${err.message}`);
171-
reject(err);
167+
if (!settled) {
168+
settled = true;
169+
reject(err);
170+
}
172171
});
173172
});
174173
} finally {
174+
process.removeListener("SIGINT", onSigint);
175+
process.removeListener("SIGTERM", onSigterm);
175176
if (bgServer) {
176177
logger.info("Stopping background server...");
177178
await shutdownServer(bgServer);

0 commit comments

Comments
 (0)