Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,43 @@ 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(
expect.objectContaining({
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',
}),
}),
);
});
11 changes: 11 additions & 0 deletions packages/server-utils/src/integrations/tracing-channel/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {
Expand All @@ -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;
Expand Down
Loading