From da7b8bdd11f8b456b585b82ba0abb92e19db9145 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:57:01 +0200 Subject: [PATCH] feat(tc): Add error status codes to mysql spans --- .../tests/transactions.test.ts | 25 +++++++++++++++++++ .../src/integrations/tracing-channel/mysql.ts | 11 ++++++++ 2 files changed, 36 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts index fb11235943b2..f44921b1c345 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts @@ -137,11 +137,24 @@ test('Instruments MySQL via Orchestrion', async ({ baseURL }) => { expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(200); const spans = transactionEvent.spans || []; + expect(spans).toContainEqual( expect.objectContaining({ op: 'db', origin: 'auto.db.orchestrion.mysql', description: 'SELECT 1 + 1 AS solution', + status: 'internal_error', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT 1 + 1 AS solution', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + // Test error codes - we deliberately don't start a Docker container so there's no DB connection + 'db.response.status_code': 'ECONNREFUSED', + 'error.type': 'AggregateError', + }), }), ); expect(spans).toContainEqual( @@ -149,6 +162,18 @@ test('Instruments MySQL via Orchestrion', async ({ baseURL }) => { op: 'db', origin: 'auto.db.orchestrion.mysql', description: 'SELECT NOW()', + status: 'internal_error', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT NOW()', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + // Test expected error codes (see comment above) + 'db.response.status_code': 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR', + 'error.type': 'Error', + }), }), ); }); diff --git a/packages/server-utils/src/integrations/tracing-channel/mysql.ts b/packages/server-utils/src/integrations/tracing-channel/mysql.ts index 03bb768cda2f..6047e885e237 100644 --- a/packages/server-utils/src/integrations/tracing-channel/mysql.ts +++ b/packages/server-utils/src/integrations/tracing-channel/mysql.ts @@ -189,6 +189,7 @@ const _mysqlChannelIntegration = (() => { code: SPAN_STATUS_ERROR, message: err instanceof Error ? err.message : 'unknown_error', }); + setErrorAttributes(span, err); // Defensive: end the span here too in case `'end'` never fires // (e.g. abrupt socket destruction). `finishSpan` is idempotent — // `spans.delete` makes the subsequent `'end'` listener a no-op. @@ -209,6 +210,7 @@ const _mysqlChannelIntegration = (() => { code: SPAN_STATUS_ERROR, message: ctx.error instanceof Error ? ctx.error.message : 'unknown_error', }); + setErrorAttributes(span, ctx.error); }, asyncStart() { @@ -235,6 +237,15 @@ function hasOnMethod(obj: object): obj is { on: (event: string, listener: (arg?: return 'on' in obj && typeof (obj as { on?: unknown }).on === 'function'; } +// The status message set via `setStatus` is discarded by the OTel->Sentry status mapping in mapStatus.ts (only canonical gRPC strings survive). +// For a refused connection these resolve to e.g. `db.response.status_code: 'ECONNREFUSED'` and `error.type: 'AggregateError'`. +// Mirrors the postgres.js integration. +function setErrorAttributes(span: Span, error: unknown): void { + const err = error as { code?: string | number; name?: string } | undefined; + span.setAttribute('db.response.status_code', err?.code !== undefined ? String(err.code) : 'unknown'); + span.setAttribute('error.type', err?.name ?? 'unknown'); +} + function extractSql(firstArg: unknown): string | undefined { if (typeof firstArg === 'string') { return firstArg;