Skip to content

Commit aa92006

Browse files
feat(local): render OTel semantic attributes in transaction output (#1015)
## Summary Enhance `sentry local` for the agent monitoring launch with OTel semantic attribute rendering, JSON output, SSE reconnection, and several quality improvements. ### OTel Semantic Display Port `trace-semantic-display` from sentry-mcp ([PR #981](getsentry/sentry-mcp#981)) to enrich tail output with OTel semantic attribute rendering. Transactions with GenAI, MCP, HTTP, database, and other OTel attributes now show rich labels: ``` [gen_ai] chat anthropic/claude-4-sonnet [1200ms] [5 spans] [mcp] tools/call search_files [320ms] [db] SELECT users [postgresql] [12ms] ``` Covers 15 attribute families: GenAI, MCP, HTTP, database, GraphQL, RPC/cloud, messaging, FaaS, object stores, CloudEvents, CICD, feature flags, process, exception, and error types. Falls back to existing behavior when no semantic attributes are present. ### `--format json` (NDJSON) New `--format json` / `-F json` flag for machine-readable output. Each envelope item produces one JSON line with structured fields including semantic labels, stack frames, and source detection: ```json {"type":"transaction","op":"gen_ai","label":"chat anthropic/claude-4-sonnet","duration_ms":1200,"source":"server"} {"type":"error","error_type":"TypeError","message":"x is not a function","filename":"app.ts","lineno":42,"source":"server"} ``` ### `--filter ai` New filter value that matches transactions with GenAI or MCP OTel attributes, so you can focus on agent activity: `sentry local -f ai` ### SSE Reconnection The SSE consumer now reconnects automatically with exponential backoff on connection loss, using `Last-Event-ID` to resume from where the stream left off. Retry counter resets after successful reconnection. Transient HTTP errors after a previous successful session are retried rather than treated as fatal. ### Quality Improvements - **Signal handler leak fixed** in `run.ts` — named handler refs, cleanup in `finally`, settled flag for close/error race - **`parsePort` deduplicated** — single implementation exported from `server.ts` - **`SENTRY_TRACES_SAMPLE_RATE`** preserved when already set (uses `??` fallback) - **Startup banner** with ingest URL, SSE endpoint, and connection hints - **Agent monitoring docs** with GenAI/MCP examples and JSON output section ## Files Changed | File | Change | |------|--------| | `src/lib/formatters/semantic-display.ts` | **New** — OTel semantic attribute rendering (15 families) | | `src/lib/formatters/local.ts` | Semantic display integration, JSON formatters, `--filter ai`, `inferSourceName` | | `src/commands/local/server.ts` | SSE reconnection, `--format json`, `--filter ai`, `parsePort` export, `feedSSELine` id tracking, startup banner | | `src/commands/local/run.ts` | Signal handler fix, `parsePort` import, env var preservation | | `docs/src/fragments/commands/local.md` | Agent monitoring section, JSON output section | | `test/lib/formatters/semantic-display.test.ts` | **New** — 70+ tests for all semantic formatters | | `test/lib/formatters/local.test.ts` | AI transaction tests, JSON format tests | | `test/commands/local/server.test.ts` | **New** — parsePort, feedSSELine, buildApp, CORS, isServerRunning | | `test/commands/local/run.test.ts` | Expanded — ENOENT, env preservation, separator handling | --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8edfa2a commit aa92006

10 files changed

Lines changed: 2525 additions & 69 deletions

File tree

docs/src/fragments/commands/local.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Env vars injected into the child process:
3434
|----------|-------|
3535
| `SENTRY_SPOTLIGHT` | `http://localhost:<port>/stream` |
3636
| `NEXT_PUBLIC_SENTRY_SPOTLIGHT` | `http://localhost:<port>/stream` |
37-
| `SENTRY_TRACES_SAMPLE_RATE` | `1` |
37+
| `SENTRY_TRACES_SAMPLE_RATE` | `1` (unless already set) |
3838

3939
## Endpoints
4040

@@ -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.

plugins/sentry-cli/skills/sentry-cli/references/local.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Start the local dev server and tail events
1919
- `-p, --port <value> - Port to listen on (default 8969) - (default: "8969")`
2020
- `-H, --host <value> - Hostname to bind to (default localhost) - (default: "localhost")`
2121
- `-q, --quiet - Suppress per-envelope tail output`
22-
- `-f, --filter <value>... - Only show items of this type (repeatable: error, transaction, log)`
22+
- `-f, --filter <value>... - Only show items of this type (repeatable: error, transaction, log, ai)`
23+
- `-F, --format <value> - Output format: human (default) or json (NDJSON) - (default: "human")`
2324

2425
### `sentry local run <command...>`
2526

@@ -49,6 +50,8 @@ sentry local -f error -f log
4950
sentry local --quiet
5051

5152
sentry local -f error -f log # only errors and logs
53+
54+
sentry local --format json
5255
```
5356

5457
All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.

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)