From cc910314d3e86ffbc120ebcc523390d5a59742f3 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 28 Oct 2026 18:12:13 +0100 Subject: [PATCH] fix(server-utils): Require a parent span for MySQL queries Co-Authored-By: GPT-6 --- .../tracing/mysql/scenario-no-parent.mjs | 44 +++++++++++++++++ .../suites/tracing/mysql/test.ts | 47 +++++++++++++++++++ .../server-utils/src/integrations/mysql.ts | 1 + 3 files changed, 92 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql/scenario-no-parent.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-no-parent.mjs new file mode 100644 index 000000000000..21e5928b3bbd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/scenario-no-parent.mjs @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/node'; +import mysql from 'mysql'; + +const connection = mysql.createConnection({ + port: Number(process.env.MYSQL_PORT), + user: 'root', + password: 'docker', +}); + +function query(sql) { + return new Promise((resolve, reject) => { + connection.query(sql, err => (err ? reject(err) : resolve())); + }); +} + +function streamQuery(sql) { + return new Promise((resolve, reject) => { + connection.query(sql).on('error', reject).on('end', resolve); + }); +} + +async function run() { + await query('SELECT 1 AS before_callback'); + await streamQuery('SELECT 1 AS before_stream'); + + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, async () => { + await query('SELECT 2 AS inside_callback'); + await streamQuery('SELECT 2 AS inside_stream'); + }); + + await query('SELECT 3 AS after_callback'); + await streamQuery('SELECT 3 AS after_stream'); + await new Promise((resolve, reject) => { + connection.end(err => (err ? reject(err) : resolve())); + }); + + // Flush every query before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts index 2a9cb3c3010a..3d4e73ffdcf4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpan } from '@sentry/core'; import { DB_CONNECTION_STRING, DB_QUERY_SUMMARY, @@ -215,6 +216,52 @@ describe('mysql auto instrumentation', () => { ); } + createEsmAndCjsTests( + __dirname, + 'scenario-no-parent.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('only instruments queries with an active parent span', async () => { + const runner = createTestRunner() + .withEnv({ ...env, MYSQL_PORT: String(mysqlPort) }) + .withFlags(...flags); + const dbSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes[SENTRY_ORIGIN]?.value === origin) { + dbSpans.set(span.span_id, span); + } + } + return spans.some(span => span.is_segment && span.name === 'parent operation'); + }); + + await runner.unordered().start().completed(); + + const spans = await spansPromise; + const parent = spans.find(span => span.is_segment && span.name === 'parent operation'); + expect(parent).toBeDefined(); + expect( + Array.from(dbSpans.values(), span => ({ + query: span.attributes[DB_QUERY_TEXT]?.value, + port: span.attributes[SERVER_PORT]?.value, + parentSpanId: span.parent_span_id, + traceId: span.trace_id, + isSegment: span.is_segment, + })), + ).toEqual( + ['SELECT ? AS inside_callback', 'SELECT ? AS inside_stream'].map(query => ({ + query, + port: mysqlPort, + parentSpanId: parent?.span_id, + traceId: parent?.trace_id, + isSegment: false, + })), + ); + }); + }, + { failsOnEsm }, + ); + createEsmAndCjsTests( __dirname, 'scenario-streamContext.mjs', diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index d3e8010cb1ba..0d38f4bbd18b 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -110,6 +110,7 @@ function instrumentMysql(): void { }); }, { + requiresParentSpan: true, // No-callback `query(sql)` returns a streamable `Query` emitter as `result`; it settles on the // emitter's `'end'`/`'error'`, not the channel, so defer ending to those. deferSpanEnd({ data, end }) {