feat(local): render OTel semantic attributes in transaction output#1015
Conversation
Port trace-semantic-display from sentry-mcp (PR #981) to enrich sentry local tail output with OTel semantic attribute rendering. Transactions with GenAI, MCP, HTTP, database, and other OTel attributes now show rich labels instead of generic op + transaction name: [gen_ai] chat anthropic/claude-4-sonnet [1200ms] [5 spans] [mcp] tools/call search_files [320ms] [db] SELECT users [postgresql] [12ms] The semantic display module 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.
|
Codecov Results 📊❌ Patch coverage is 75.30%. Project has 4281 uncovered lines. Files with missing lines (4)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 81.87% 81.97% +0.1%
==========================================
Files 328 329 +1
Lines 23359 23744 +385
Branches 15114 15500 +386
==========================================
+ Hits 19123 19463 +340
- Misses 4236 4281 +45
- Partials 1621 1644 +23Generated by Codecov Action |
- Hoist URL scheme regex to module-level constant - Apply biome formatting fixes to test files - Use template literal for string concatenation in test
- Add logger import and log.debug in formatHttpTarget catch block (#1) - Document intentional inferSemanticOp domain omissions (#2) - Handle single-element OTel array attributes in truncate (#4) - Add numeric status code test (#5) - Add tests for GraphQL, RPC, Messaging, ObjectStore, CloudEvents, CICD, and FeatureFlag formatters (#8) - Simplify dedupeMetadata to skip redundant re-truncation (#10) - Use consistent split pattern in formatResourceTarget (#13)
S3 spans carry rpc.method/rpc.service attributes alongside aws.s3.* attributes. Check for S3 before RPC so the op tag shows [s3] instead of [rpc] for object store operations. Addresses Sentry Seer review.
- 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
…cate code - Reset retries and backoff delay after a successful connection that later drops, so transient disconnects don't exhaust the retry budget - Distinguish 'connected-then-lost' from 'error' in attemptSSEConnection - Remove duplicate unreachable code blocks in inferSemanticOp (biome format artifact)
Only treat 'no-connection' as fatal on the first attempt. If we've previously connected successfully, the server may be restarting — retry with backoff instead of giving up.
Review fixes: - Add stack frame info (filename, lineno, colno, function) to JSON error output for AI agent consumption - Add JSDoc to formatItemJson documenting sanitize design decision - Fix docs: SENTRY_TRACES_SAMPLE_RATE is '1 (unless already set)' - Fix ENOENT test to assert error message pattern, not just defined - Add comprehensive JSON format tests (error, transaction, log, source) Nice-to-haves: - Add startup banner with ingest URL, SSE endpoint, and connection hints - Add --filter ai to match transactions with GenAI/MCP OTel attributes
Security: - Add stripBidi() for JSON output — JSON.stringify escapes C0/C1 but leaves Unicode BiDi override chars (U+202A-202E, U+2066-2069) intact, which can reorder terminal text. JSON formatters now call stripBidi() on all user-controlled string values. - Extract BIDI_RE regex to module level, shared by sanitize() and stripBidi() SSE reconnection: - On first attempt, both 'no-connection' and 'error' are now fatal (server doesn't exist). Only retry after a previous successful connection (hasConnectedBefore).
Mid-stream errors (network reset, proxy timeout) now correctly report as 'connected-then-lost' instead of 'no-connection', enabling the reconnection loop. The onConnected callback fires as soon as the HTTP 200 response is received, before the stream read loop.
JSON.stringify only escapes C0 (U+0000-U+001F) per RFC 8259; C1 controls (U+0080-U+009F, e.g. CSI=U+009B) pass through unescaped. stripBidi() now removes both C1 and BiDi chars, preventing terminal injection when --format json output is displayed in a terminal.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit adcc086. Configure here.
| attrs: AttributeSource, | ||
| fallbackLabel: string | ||
| ): SemanticSpanDisplay | null { | ||
| const rpcSystem = getAttr(attrs, ["rpc.system.name"]); |
There was a problem hiding this comment.
rpc.system.name is not a valid OTel attribute — should be rpc.system
The code reads rpc.system.name to identify the RPC system, but the OTel semantic conventions define this attribute as rpc.system (values: "grpc", "java_rmi", "dotnet_wcf", etc.). Standard OTel-instrumented RPC spans will never populate rpcSystem, silently dropping the system label from metadata and causing inferSemanticOp to miss spans that only carry rpc.system without rpc.service.
Evidence
getAttr(attrs, ["rpc.system.name"])at line 317 will always returnundefinedfor spans instrumented by standard OTel SDKs, which emitrpc.system.db.system.name(used for databases at line 247) is the correct newer-semconv attribute, so the*.system.namepattern was incorrectly extended to RPC where no such rename occurred in the spec.inferSemanticOpat line 146 also uses"rpc.system.name"as its primary key; a span with only{"rpc.system": "grpc"}would not be tagged asrpc.- Unit tests at
test/lib/formatters/semantic-display.test.ts:285,504also userpc.system.name, confirming the wrong key propagated into test coverage rather than catching the mismatch.
Identified by Warden find-bugs · LHB-MGA

Summary
Enhance
sentry localfor the agent monitoring launch with OTel semantic attribute rendering, JSON output, SSE reconnection, and several quality improvements.OTel Semantic Display
Port
trace-semantic-displayfrom sentry-mcp (PR #981) to enrich tail output with OTel semantic attribute rendering. Transactions with GenAI, MCP, HTTP, database, and other OTel attributes now show rich labels: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 jsonflag for machine-readable output. Each envelope item produces one JSON line with structured fields including semantic labels, stack frames, and source detection:{"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 aiNew filter value that matches transactions with GenAI or MCP OTel attributes, so you can focus on agent activity:
sentry local -f aiSSE Reconnection
The SSE consumer now reconnects automatically with exponential backoff on connection loss, using
Last-Event-IDto 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
run.ts— named handler refs, cleanup infinally, settled flag for close/error raceparsePortdeduplicated — single implementation exported fromserver.tsSENTRY_TRACES_SAMPLE_RATEpreserved when already set (uses??fallback)Files Changed
src/lib/formatters/semantic-display.tssrc/lib/formatters/local.ts--filter ai,inferSourceNamesrc/commands/local/server.ts--format json,--filter ai,parsePortexport,feedSSELineid tracking, startup bannersrc/commands/local/run.tsparsePortimport, env var preservationdocs/src/fragments/commands/local.mdtest/lib/formatters/semantic-display.test.tstest/lib/formatters/local.test.tstest/commands/local/server.test.tstest/commands/local/run.test.ts