From e7b8a1610983b318330a1a34013cc2caf5cd9e97 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 17:22:41 +0200 Subject: [PATCH 1/6] test(node): Port MySQL integration tests to span streaming Co-Authored-By: GPT-6 --- .../node-suites/excludes.ts | 1 + .../suites/tracing/mysql/instrument.mjs | 1 - .../suites/tracing/mysql/test.ts | 333 +++++++----------- .../tracing/mysql2-static/docker-compose.yml | 14 + .../tracing/mysql2-static/instrument.mjs | 10 + .../suites/tracing/mysql2-static/scenario.mjs | 38 ++ .../suites/tracing/mysql2-static/test.ts | 89 +++++ .../mysql2-tracing-channel/instrument.mjs | 1 - .../tracing/mysql2-tracing-channel/test.ts | 67 ++-- .../suites/tracing/mysql2/docker-compose.yml | 2 +- .../suites/tracing/mysql2/instrument.mjs | 1 - .../suites/tracing/mysql2/scenario.mjs | 2 +- .../suites/tracing/mysql2/test.ts | 91 ++--- 13 files changed, 374 insertions(+), 276 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index d27790a79a0f..65cf09ddd1c3 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -120,6 +120,7 @@ export const NO_AUTO_INSTRUMENTATION = [ 'suites/tracing/mysql/test.ts', 'suites/tracing/mysql2-tracing-channel/test.ts', 'suites/tracing/mysql2/test.ts', + 'suites/tracing/mysql2-static/test.ts', 'suites/tracing/openai/test.ts', 'suites/tracing/openai/v6/test.ts', 'suites/tracing/openai/v7/test.ts', diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql/instrument.mjs index 450e0dd3ebfe..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', }); 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 e0db6de0be75..151dd03a118d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -1,18 +1,34 @@ +import { + DB_CONNECTION_STRING, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + DB_USER, + SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; import type { AddressInfo, Server } from 'node:net'; import { afterAll, beforeAll, describe, expect } from 'vitest'; -import { cleanupChildProcesses, createCjsTests, createEsmAndCjsTests } from '../../../utils/runner'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; import { startMysqlTestServer } from './mysql-test-server'; -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; describe('mysql auto instrumentation', () => { // A minimal in-process MySQL server (on a random free port) so the client's // connection handshake succeeds. Without it, `createPool()` queries fail at // connection acquisition — before `connection.query` runs — so the // diagnostics-channel instrumentation (which hooks `connection.query`) never - // sees them. Queries still error (the server rejects them), so spans keep - // `status: internal_error` as the assertions expect. The port is passed to - // each scenario via the `MYSQL_PORT` env var. + // sees them. The port is passed to each scenario via the `MYSQL_PORT` env var. let mysqlServer: Server; let mysqlPort: number; beforeAll(async () => { @@ -26,36 +42,97 @@ describe('mysql auto instrumentation', () => { cleanupChildProcesses(); }); - // Builds the expected transaction. When `origin` is given, the spans must also - // carry that `sentry.origin`, which is how we assert that the - // diagnostics-channel instrumentation (not the OTel one) produced them. A - // scenario can pass `override` to replace the default transaction expectation - // (e.g. the streamed-error scenario, which runs a different, failing query). - function expectedTransaction( + function expectedSpans( port: number, origin: string | undefined, override: Record | undefined, - ): Record { - const span = (description: string): ReturnType => - expect.objectContaining({ - description, - op: 'db', - ...(origin ? { origin } : {}), - data: expect.objectContaining({ - ...(origin ? { 'sentry.origin': origin } : {}), - 'db.system.name': 'mysql', - 'server.address': 'localhost', - 'server.port': port, - 'db.user': 'root', - }), - status: 'ok', - }); + ): unknown { + const COMMON_ATTRIBUTES = { + // These spans belong to a script with no incoming request, so there is nothing to judge. + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + [DB_CONNECTION_STRING]: { + type: 'string', + value: expect.stringMatching(/^jdbc:mysql:\/\/localhost:.*/), + }, + [DB_SYSTEM_NAME]: { + type: 'string', + value: 'mysql', + }, + [DB_USER]: { + type: 'string', + value: 'root', + }, + [SERVER_ADDRESS]: { + type: 'string', + value: 'localhost', + }, + [SERVER_PORT]: { + type: 'integer', + value: port, + }, + [SENTRY_KIND]: { + type: 'string', + value: 'client', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_OP]: { + type: 'string', + value: 'db', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: origin, + }, + [SENTRY_RELEASE]: { + type: 'string', + value: '1.0', + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.node', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: expect.any(String), + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: expect.stringMatching(/^[\da-f]{16}$/), + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'Test Transaction', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + }; - return { - transaction: 'Test Transaction', - spans: expect.arrayContaining([span('SELECT ? + ? AS solution'), span('SELECT NOW()')]), - ...(override ?? {}), + const COMMON_SPAN_PROPS = { + end_timestamp: expect.any(Number), + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), }; + + const span = (queryText: string) => ({ + name: 'SELECT', + attributes: { + ...COMMON_ATTRIBUTES, + [DB_QUERY_TEXT]: { type: 'string', value: queryText }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'SELECT' }, + }, + ...COMMON_SPAN_PROPS, + }); + + return override?.spans ?? [span('SELECT ? + ? AS solution'), span('SELECT NOW()')]; } const CHANNEL_ORIGIN = 'auto.db.mysql'; @@ -93,13 +170,14 @@ describe('mysql auto instrumentation', () => { // The transaction itself succeeds (status `ok`); only the failing query's child span is errored. spans: expect.arrayContaining([ expect.objectContaining({ - description: 'SELECT * FROM does_not_exist', - op: 'db', + name: 'SELECT does_not_exist', // A failing streamed query emits `error`, which marks the span as errored - status: 'internal_error', - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.user': 'root', + status: 'error', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'db' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_USER]: { type: 'string', value: 'root' }, }), }), ]), @@ -119,7 +197,13 @@ describe('mysql auto instrumentation', () => { await createRunner() .withEnv({ ...env, MYSQL_PORT: String(mysqlPort) }) .withFlags(...flags) - .expect({ transaction: expectedTransaction(mysqlPort, origin, transactionOverride) }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === 'db'); + expect(spans).toEqual(expectedSpans(mysqlPort, origin, transactionOverride)); + }, + }) .start() .completed(); }); @@ -140,12 +224,14 @@ describe('mysql auto instrumentation', () => { .withFlags(...flags) .withEnv({ ...env, MYSQL_PORT: String(mysqlPort) }) .expect({ - transaction: (transaction): void => { - const transactionSpanId = transaction.contexts?.trace?.span_id; - const spans = transaction.spans ?? []; - const mysqlSpan = spans.find(span => span.description === 'SELECT ? + ? AS solution'); - const listenerSpan = spans.find(span => span.description === 'listener-child'); - const innerSpan = spans.find(span => span.description === 'inner-span'); + span: (container): void => { + const transactionSpanId = container.items.find(span => span.is_segment)?.span_id; + const spans = container.items; + const mysqlSpan = spans.find( + span => span.attributes[DB_QUERY_TEXT]?.value === 'SELECT ? + ? AS solution', + ); + const listenerSpan = spans.find(span => span.name === 'listener-child'); + const innerSpan = spans.find(span => span.name === 'inner-span'); expect(transactionSpanId).toBeDefined(); expect(mysqlSpan).toBeDefined(); @@ -169,163 +255,4 @@ describe('mysql auto instrumentation', () => { ); }); } - - describe('streamed', () => { - const assertMysqlSpans = (container: SerializedStreamedSpanContainer): void => { - const segmentSpan = container.items.find(item => item.is_segment); - expect(segmentSpan?.name).toBe('Test Transaction'); - - const dbSpans = container.items.filter(spanItem => spanItem.attributes[SENTRY_OP]?.value === 'db'); - - expect(dbSpans.length).toBe(2); - - const COMMON_ATTRIBUTES = { - // These spans belong to a script with no incoming request, so there is nothing to judge. - 'sentry.is_localhost': { type: 'boolean', value: false }, - 'db.connection_string': { - type: 'string', - value: expect.stringMatching(/^jdbc:mysql:\/\/localhost:.*/), - }, - 'db.system.name': { - type: 'string', - value: 'mysql', - }, - 'db.user': { - type: 'string', - value: 'root', - }, - 'server.address': { - type: 'string', - value: 'localhost', - }, - 'server.port': { - type: 'integer', - value: expect.any(Number), - }, - 'sentry.kind': { - type: 'string', - value: 'client', - }, - 'sentry.environment': { - type: 'string', - value: 'production', - }, - 'sentry.op': { - type: 'string', - value: 'db', - }, - 'sentry.origin': { - type: 'string', - value: 'auto.db.mysql', - }, - 'sentry.release': { - type: 'string', - value: '1.0', - }, - 'sentry.sdk.name': { - type: 'string', - value: 'sentry.javascript.node', - }, - 'sentry.sdk.version': { - type: 'string', - value: expect.any(String), - }, - 'sentry.segment.id': { - type: 'string', - value: expect.stringMatching(/^[\da-f]{16}$/), - }, - 'sentry.segment.name': { - type: 'string', - value: 'Test Transaction', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - }; - - const COMMON_SPAN_PROPS = { - end_timestamp: expect.any(Number), - is_segment: false, - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }; - - expect(dbSpans).toEqual([ - { - attributes: { - ...COMMON_ATTRIBUTES, - 'db.query.text': { - type: 'string', - value: 'SELECT ? + ? AS solution', - }, - 'db.query.summary': { - type: 'string', - value: 'SELECT', - }, - }, - name: 'SELECT', - ...COMMON_SPAN_PROPS, - }, - { - attributes: { - ...COMMON_ATTRIBUTES, - 'db.query.text': { - type: 'string', - value: 'SELECT NOW()', - }, - 'db.query.summary': { - type: 'string', - value: 'SELECT', - }, - }, - name: 'SELECT', - ...COMMON_SPAN_PROPS, - }, - ]); - }; - - describe('with connection.connect()', () => { - createCjsTests(__dirname, 'scenario-withConnect.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `mysql` package when using connection.connect()', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true', MYSQL_PORT: String(mysqlPort) }) - .expect({ - span: assertMysqlSpans, - }) - .start() - .completed(); - }); - }); - }); - - describe('query without callback', () => { - createCjsTests(__dirname, 'scenario-withoutCallback.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `mysql` package when using query without callback', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true', MYSQL_PORT: String(mysqlPort) }) - .expect({ span: assertMysqlSpans }) - .start() - .completed(); - }); - }); - }); - - describe('without connection.connect()', () => { - createCjsTests(__dirname, 'scenario-withoutConnect.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `mysql` package without connection.connect()', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true', MYSQL_PORT: String(mysqlPort) }) - .expect({ - span: assertMysqlSpans, - }) - .start() - .completed(); - }); - }); - }); - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml new file mode 100644 index 000000000000..740d3a25ca80 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml @@ -0,0 +1,14 @@ +services: + db: + image: mysql:8 + restart: always + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: password + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -ppassword'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 10s diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs new file mode 100644 index 000000000000..170ad6f6a702 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs new file mode 100644 index 000000000000..b895589b373f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/node'; +import { waitForConnection } from '@sentry-internal/node-integration-tests'; +import mysql from 'mysql2/promise'; + +const CONNECT_CONFIG = { + user: 'root', + password: 'password', + host: 'localhost', + port: 3306, +}; + +async function run() { + // Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`). + await waitForConnection(async () => { + const probe = await mysql.createConnection(CONNECT_CONFIG); + await probe.end(); + }); + + const connection = await mysql.createConnection(CONNECT_CONFIG); + + await Sentry.startSpan( + { + op: 'transaction', + name: 'Test Transaction', + }, + async _ => { + await connection.query('SELECT 1 + 1 AS solution'); + await connection.query('SELECT ? as a, ? as b, NOW() as c', ['1', '2']); + await connection.query('SELECT ? AS scalar_value', 42); + // `execute` is instrumented the same way as `query` + await connection.execute('SELECT 42 AS answer'); + // a failing query should produce a span with an error status + await connection.query('SELECT * FROM does_not_exist').catch(() => {}); + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts new file mode 100644 index 000000000000..9733d4c7b614 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts @@ -0,0 +1,89 @@ +import { afterAll, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; + +describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__dirname] }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + // With orchestrion injection enabled (`INJECT_ORCHESTRION`), the diagnostics-channel integration + // records the spans instead of the OTel patcher, so they carry a different `sentry.origin`. + const ORIGIN = 'auto.db.mysql2'; + + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'SELECT ? + ? AS solution', + op: 'db', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? + ? AS solution', + 'server.address': 'localhost', + 'server.port': 3306, + 'db.user': 'root', + }), + }), + // bind values are left as `?` placeholders in `db.statement` (not inlined) + expect.objectContaining({ + description: 'SELECT ? as a, ? as b, NOW() as c', + op: 'db', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? as a, ? as b, NOW() as c', + 'server.address': 'localhost', + 'server.port': 3306, + 'db.user': 'root', + }), + }), + // a single non-array bind value is also left as a `?` placeholder in `db.statement` + expect.objectContaining({ + description: 'SELECT ? AS scalar_value', + op: 'db', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? AS scalar_value', + }), + }), + // `execute` is instrumented the same way as `query` + expect.objectContaining({ + description: 'SELECT ? AS answer', + op: 'db', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? AS answer', + }), + }), + // a failing query produces a span with an error status + expect.objectContaining({ + description: 'SELECT * FROM does_not_exist', + op: 'db', + status: 'internal_error', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT * FROM does_not_exist', + }), + }), + ]), + }; + + createEsmAndCjsTests( + __dirname, + 'scenario.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('should auto-instrument `mysql2` package without connection.connect()', { timeout: 75_000 }, async () => { + await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + }); + }, + // mysql2 >= 3.20.0 publishes its own diagnostics channels, which the SDK subscribes to instead + // of the orchestrion path asserted here. That range is covered by `mysql2-tracing-channel`, so + // this suite pins a version below the boundary regardless of the version the workspace installs. + { additionalDependencies: { mysql2: '3.19.1' } }, + ); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts index b243da576bb3..6a8f420befa8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts @@ -1,3 +1,12 @@ +import { + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -13,22 +22,21 @@ describe('mysql2 tracing channel Test', () => { const expectedQuerySpan = (queryText: string) => expect.objectContaining({ - description: queryText, - op: 'db', - origin: 'auto.db.mysql2.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.mysql2.diagnostic_channel', - 'db.system.name': 'mysql', - 'db.operation.name': 'SELECT', - 'db.query.text': queryText, - 'server.address': 'localhost', - 'server.port': 3308, + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.mysql2.diagnostic_channel' }, + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_OPERATION_NAME]: { type: 'string', value: 'SELECT' }, + [DB_QUERY_TEXT]: { type: 'string', value: queryText }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, + [SERVER_PORT]: { type: 'integer', value: 3308 }, + [SENTRY_OP]: { type: 'string', value: 'db' }, }), + name: 'SELECT', }); - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ + const EXPECTED_SPANS = { + items: expect.arrayContaining([ + expect.objectContaining({ name: 'Test Transaction', is_segment: true }), expectedQuerySpan('SELECT ? + ? AS solution'), // the inlined literal is redacted out of `db.query.text` expectedQuerySpan('SELECT ? AS leaked'), @@ -36,10 +44,13 @@ describe('mysql2 tracing channel Test', () => { expectedQuerySpan('SELECT ? AS answer'), // a failing query produces a span with an error status expect.objectContaining({ - description: 'SELECT * FROM does_not_exist', - op: 'db', - status: 'internal_error', - origin: 'auto.db.mysql2.diagnostic_channel', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.mysql2.diagnostic_channel' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, + }), + name: 'SELECT does_not_exist', + status: 'error', }), ]), }; @@ -47,19 +58,21 @@ describe('mysql2 tracing channel Test', () => { describeWithDockerCompose('with pg docker compose', { workingDirectory: [__dirname] }, () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('subscribes to mysql2 >= 3.20.0 diagnostics channels with stable semconv attributes', async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed(); }, 30_000); test('does not double-instrument: the legacy IITM mysql2 patcher does not fire on 3.20.0+', async () => { await createTestRunner() .expect({ - transaction: event => { - expect(event.transaction).toBe('Test Transaction'); - const spans = event.spans || []; + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); // The monkey-patch path (origin `auto.db.mysql2`) must be inactive on 3.20.0+. - expect(spans.find(span => span.origin === 'auto.db.mysql2')).toBeUndefined(); + expect(spans.find(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mysql2')).toBeUndefined(); // ...while the diagnostics-channel path is active. - expect(spans.find(span => span.origin === 'auto.db.mysql2.diagnostic_channel')).toBeDefined(); + expect( + spans.find(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mysql2.diagnostic_channel'), + ).toBeDefined(); }, }) .start() @@ -69,11 +82,11 @@ describe('mysql2 tracing channel Test', () => { test('never leaks raw values into db.query.text', async () => { await createTestRunner() .expect({ - transaction: event => { - expect(event.transaction).toBe('Test Transaction'); - const spans = event.spans || []; + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); for (const span of spans) { - const queryText = span.data?.['db.query.text']; + const queryText = span.attributes[DB_QUERY_TEXT]?.value; if (typeof queryText === 'string') { expect(queryText).not.toContain('super-secret'); } diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml index 740d3a25ca80..e201ea2e4390 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml @@ -3,7 +3,7 @@ services: image: mysql:8 restart: always ports: - - '3306:3306' + - '3344:3306' environment: MYSQL_ROOT_PASSWORD: password healthcheck: diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs index b895589b373f..ebc8f811a20b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs @@ -6,7 +6,7 @@ const CONNECT_CONFIG = { user: 'root', password: 'password', host: 'localhost', - port: 3306, + port: 3344, }; async function run() { diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts index 9733d4c7b614..56d7b30d65f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts @@ -1,3 +1,12 @@ +import { + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + DB_USER, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -10,64 +19,64 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // records the spans instead of the OTel patcher, so they carry a different `sentry.origin`. const ORIGIN = 'auto.db.mysql2'; - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ + const EXPECTED_SPANS = { + items: expect.arrayContaining([ + expect.objectContaining({ name: 'Test Transaction', is_segment: true }), expect.objectContaining({ - description: 'SELECT ? + ? AS solution', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? + ? AS solution', - 'server.address': 'localhost', - 'server.port': 3306, - 'db.user': 'root', + attributes: expect.objectContaining({ + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? + ? AS solution' }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, + [SERVER_PORT]: { type: 'integer', value: 3344 }, + [DB_USER]: { type: 'string', value: 'root' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), + name: 'SELECT', }), // bind values are left as `?` placeholders in `db.statement` (not inlined) expect.objectContaining({ - description: 'SELECT ? as a, ? as b, NOW() as c', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? as a, ? as b, NOW() as c', - 'server.address': 'localhost', - 'server.port': 3306, - 'db.user': 'root', + attributes: expect.objectContaining({ + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? as a, ? as b, NOW() as c' }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, + [SERVER_PORT]: { type: 'integer', value: 3344 }, + [DB_USER]: { type: 'string', value: 'root' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), + name: 'SELECT', }), // a single non-array bind value is also left as a `?` placeholder in `db.statement` expect.objectContaining({ - description: 'SELECT ? AS scalar_value', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? AS scalar_value', + attributes: expect.objectContaining({ + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS scalar_value' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), + name: 'SELECT', }), // `execute` is instrumented the same way as `query` expect.objectContaining({ - description: 'SELECT ? AS answer', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? AS answer', + attributes: expect.objectContaining({ + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS answer' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), + name: 'SELECT', }), // a failing query produces a span with an error status expect.objectContaining({ - description: 'SELECT * FROM does_not_exist', - op: 'db', - status: 'internal_error', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT * FROM does_not_exist', + attributes: expect.objectContaining({ + [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, + [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), + name: 'SELECT does_not_exist', + status: 'error', }), ]), }; @@ -78,7 +87,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ 'instrument.mjs', (createTestRunner, test) => { test('should auto-instrument `mysql2` package without connection.connect()', { timeout: 75_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed(); }); }, // mysql2 >= 3.20.0 publishes its own diagnostics channels, which the SDK subscribes to instead From 6aacd8ad32200094ca36d55a0ba0ddf3bed92228 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 11:04:41 +0200 Subject: [PATCH 2/6] test(node): Use conventions constants for span ops Co-Authored-By: GPT-6 --- .../suites/tracing/mysql/test.ts | 7 ++++--- .../suites/tracing/mysql2-static/test.ts | 11 ++++++----- .../suites/tracing/mysql2-tracing-channel/test.ts | 5 +++-- .../suites/tracing/mysql2/test.ts | 11 ++++++----- 4 files changed, 19 insertions(+), 15 deletions(-) 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 151dd03a118d..05fbd28601a2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -18,6 +18,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import type { AddressInfo, Server } from 'node:net'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -80,7 +81,7 @@ describe('mysql auto instrumentation', () => { }, [SENTRY_OP]: { type: 'string', - value: 'db', + value: DB, }, [SENTRY_ORIGIN]: { type: 'string', @@ -174,7 +175,7 @@ describe('mysql auto instrumentation', () => { // A failing streamed query emits `error`, which marks the span as errored status: 'error', attributes: expect.objectContaining({ - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, [DB_USER]: { type: 'string', value: 'root' }, @@ -200,7 +201,7 @@ describe('mysql auto instrumentation', () => { .expect({ span: container => { expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); - const spans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === 'db'); + const spans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === DB); expect(spans).toEqual(expectedSpans(mysqlPort, origin, transactionOverride)); }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts index 9733d4c7b614..619fd607a06e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts @@ -1,3 +1,4 @@ +import { DB } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -15,7 +16,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ spans: expect.arrayContaining([ expect.objectContaining({ description: 'SELECT ? + ? AS solution', - op: 'db', + op: DB, origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', @@ -28,7 +29,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // bind values are left as `?` placeholders in `db.statement` (not inlined) expect.objectContaining({ description: 'SELECT ? as a, ? as b, NOW() as c', - op: 'db', + op: DB, origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', @@ -41,7 +42,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // a single non-array bind value is also left as a `?` placeholder in `db.statement` expect.objectContaining({ description: 'SELECT ? AS scalar_value', - op: 'db', + op: DB, origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', @@ -51,7 +52,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // `execute` is instrumented the same way as `query` expect.objectContaining({ description: 'SELECT ? AS answer', - op: 'db', + op: DB, origin: ORIGIN, data: expect.objectContaining({ 'db.system.name': 'mysql', @@ -61,7 +62,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // a failing query produces a span with an error status expect.objectContaining({ description: 'SELECT * FROM does_not_exist', - op: 'db', + op: DB, status: 'internal_error', origin: ORIGIN, data: expect.objectContaining({ diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts index 6a8f420befa8..6e67cc51482d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2-tracing-channel/test.ts @@ -7,6 +7,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -29,7 +30,7 @@ describe('mysql2 tracing channel Test', () => { [DB_QUERY_TEXT]: { type: 'string', value: queryText }, [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, [SERVER_PORT]: { type: 'integer', value: 3308 }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, }), name: 'SELECT', }); @@ -45,7 +46,7 @@ describe('mysql2 tracing channel Test', () => { // a failing query produces a span with an error status expect.objectContaining({ attributes: expect.objectContaining({ - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.mysql2.diagnostic_channel' }, [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, }), diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts index 56d7b30d65f7..168163b6953f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts @@ -7,6 +7,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -29,7 +30,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, [SERVER_PORT]: { type: 'integer', value: 3344 }, [DB_USER]: { type: 'string', value: 'root' }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), name: 'SELECT', @@ -42,7 +43,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, [SERVER_PORT]: { type: 'integer', value: 3344 }, [DB_USER]: { type: 'string', value: 'root' }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), name: 'SELECT', @@ -52,7 +53,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ attributes: expect.objectContaining({ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS scalar_value' }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), name: 'SELECT', @@ -62,7 +63,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ attributes: expect.objectContaining({ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS answer' }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), name: 'SELECT', @@ -72,7 +73,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ attributes: expect.objectContaining({ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, }), name: 'SELECT does_not_exist', From 0bbb46559fed2a7639793490168b6576e00bb72e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 11:24:14 +0200 Subject: [PATCH 3/6] test(node): Keep the original MySQL2 suite static Co-Authored-By: GPT-6 --- .../node-suites/excludes.ts | 1 - .../tracing/mysql2-static/docker-compose.yml | 14 --- .../tracing/mysql2-static/instrument.mjs | 10 -- .../suites/tracing/mysql2-static/scenario.mjs | 38 -------- .../suites/tracing/mysql2-static/test.ts | 90 ------------------ .../suites/tracing/mysql2/docker-compose.yml | 2 +- .../suites/tracing/mysql2/instrument.mjs | 1 + .../suites/tracing/mysql2/scenario.mjs | 2 +- .../suites/tracing/mysql2/test.ts | 91 +++++++++---------- 9 files changed, 44 insertions(+), 205 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml delete mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 65cf09ddd1c3..d27790a79a0f 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -120,7 +120,6 @@ export const NO_AUTO_INSTRUMENTATION = [ 'suites/tracing/mysql/test.ts', 'suites/tracing/mysql2-tracing-channel/test.ts', 'suites/tracing/mysql2/test.ts', - 'suites/tracing/mysql2-static/test.ts', 'suites/tracing/openai/test.ts', 'suites/tracing/openai/v6/test.ts', 'suites/tracing/openai/v7/test.ts', diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml deleted file mode 100644 index 740d3a25ca80..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - db: - image: mysql:8 - restart: always - ports: - - '3306:3306' - environment: - MYSQL_ROOT_PASSWORD: password - healthcheck: - test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -ppassword'] - interval: 2s - timeout: 3s - retries: 30 - start_period: 10s diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs deleted file mode 100644 index 170ad6f6a702..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/instrument.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - traceLifecycle: 'static', - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs deleted file mode 100644 index b895589b373f..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/scenario.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { waitForConnection } from '@sentry-internal/node-integration-tests'; -import mysql from 'mysql2/promise'; - -const CONNECT_CONFIG = { - user: 'root', - password: 'password', - host: 'localhost', - port: 3306, -}; - -async function run() { - // Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`). - await waitForConnection(async () => { - const probe = await mysql.createConnection(CONNECT_CONFIG); - await probe.end(); - }); - - const connection = await mysql.createConnection(CONNECT_CONFIG); - - await Sentry.startSpan( - { - op: 'transaction', - name: 'Test Transaction', - }, - async _ => { - await connection.query('SELECT 1 + 1 AS solution'); - await connection.query('SELECT ? as a, ? as b, NOW() as c', ['1', '2']); - await connection.query('SELECT ? AS scalar_value', 42); - // `execute` is instrumented the same way as `query` - await connection.execute('SELECT 42 AS answer'); - // a failing query should produce a span with an error status - await connection.query('SELECT * FROM does_not_exist').catch(() => {}); - }, - ); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts deleted file mode 100644 index 619fd607a06e..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2-static/test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { DB } from '@sentry/conventions/op'; -import { afterAll, expect } from 'vitest'; -import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; - -describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__dirname] }, () => { - afterAll(() => { - cleanupChildProcesses(); - }); - - // With orchestrion injection enabled (`INJECT_ORCHESTRION`), the diagnostics-channel integration - // records the spans instead of the OTel patcher, so they carry a different `sentry.origin`. - const ORIGIN = 'auto.db.mysql2'; - - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'SELECT ? + ? AS solution', - op: DB, - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? + ? AS solution', - 'server.address': 'localhost', - 'server.port': 3306, - 'db.user': 'root', - }), - }), - // bind values are left as `?` placeholders in `db.statement` (not inlined) - expect.objectContaining({ - description: 'SELECT ? as a, ? as b, NOW() as c', - op: DB, - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? as a, ? as b, NOW() as c', - 'server.address': 'localhost', - 'server.port': 3306, - 'db.user': 'root', - }), - }), - // a single non-array bind value is also left as a `?` placeholder in `db.statement` - expect.objectContaining({ - description: 'SELECT ? AS scalar_value', - op: DB, - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? AS scalar_value', - }), - }), - // `execute` is instrumented the same way as `query` - expect.objectContaining({ - description: 'SELECT ? AS answer', - op: DB, - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? AS answer', - }), - }), - // a failing query produces a span with an error status - expect.objectContaining({ - description: 'SELECT * FROM does_not_exist', - op: DB, - status: 'internal_error', - origin: ORIGIN, - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT * FROM does_not_exist', - }), - }), - ]), - }; - - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('should auto-instrument `mysql2` package without connection.connect()', { timeout: 75_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); - }, - // mysql2 >= 3.20.0 publishes its own diagnostics channels, which the SDK subscribes to instead - // of the orchestrion path asserted here. That range is covered by `mysql2-tracing-channel`, so - // this suite pins a version below the boundary regardless of the version the workspace installs. - { additionalDependencies: { mysql2: '3.19.1' } }, - ); -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml index e201ea2e4390..740d3a25ca80 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml @@ -3,7 +3,7 @@ services: image: mysql:8 restart: always ports: - - '3344:3306' + - '3306:3306' environment: MYSQL_ROOT_PASSWORD: password healthcheck: diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs index 46a27dd03b74..170ad6f6a702 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/instrument.mjs @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs index ebc8f811a20b..b895589b373f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/scenario.mjs @@ -6,7 +6,7 @@ const CONNECT_CONFIG = { user: 'root', password: 'password', host: 'localhost', - port: 3344, + port: 3306, }; async function run() { diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts index 168163b6953f..619fd607a06e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts @@ -1,12 +1,3 @@ -import { - DB_QUERY_TEXT, - DB_SYSTEM_NAME, - DB_USER, - SENTRY_OP, - SENTRY_ORIGIN, - SERVER_ADDRESS, - SERVER_PORT, -} from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -20,64 +11,64 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ // records the spans instead of the OTel patcher, so they carry a different `sentry.origin`. const ORIGIN = 'auto.db.mysql2'; - const EXPECTED_SPANS = { - items: expect.arrayContaining([ - expect.objectContaining({ name: 'Test Transaction', is_segment: true }), + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ expect.objectContaining({ - attributes: expect.objectContaining({ - [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, - [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? + ? AS solution' }, - [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, - [SERVER_PORT]: { type: 'integer', value: 3344 }, - [DB_USER]: { type: 'string', value: 'root' }, - [SENTRY_OP]: { type: 'string', value: DB }, - [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, + description: 'SELECT ? + ? AS solution', + op: DB, + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? + ? AS solution', + 'server.address': 'localhost', + 'server.port': 3306, + 'db.user': 'root', }), - name: 'SELECT', }), // bind values are left as `?` placeholders in `db.statement` (not inlined) expect.objectContaining({ - attributes: expect.objectContaining({ - [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, - [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? as a, ? as b, NOW() as c' }, - [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, - [SERVER_PORT]: { type: 'integer', value: 3344 }, - [DB_USER]: { type: 'string', value: 'root' }, - [SENTRY_OP]: { type: 'string', value: DB }, - [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, + description: 'SELECT ? as a, ? as b, NOW() as c', + op: DB, + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? as a, ? as b, NOW() as c', + 'server.address': 'localhost', + 'server.port': 3306, + 'db.user': 'root', }), - name: 'SELECT', }), // a single non-array bind value is also left as a `?` placeholder in `db.statement` expect.objectContaining({ - attributes: expect.objectContaining({ - [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, - [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS scalar_value' }, - [SENTRY_OP]: { type: 'string', value: DB }, - [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, + description: 'SELECT ? AS scalar_value', + op: DB, + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? AS scalar_value', }), - name: 'SELECT', }), // `execute` is instrumented the same way as `query` expect.objectContaining({ - attributes: expect.objectContaining({ - [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, - [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT ? AS answer' }, - [SENTRY_OP]: { type: 'string', value: DB }, - [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, + description: 'SELECT ? AS answer', + op: DB, + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT ? AS answer', }), - name: 'SELECT', }), // a failing query produces a span with an error status expect.objectContaining({ - attributes: expect.objectContaining({ - [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql' }, - [DB_QUERY_TEXT]: { type: 'string', value: 'SELECT * FROM does_not_exist' }, - [SENTRY_OP]: { type: 'string', value: DB }, - [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN }, + description: 'SELECT * FROM does_not_exist', + op: DB, + status: 'internal_error', + origin: ORIGIN, + data: expect.objectContaining({ + 'db.system.name': 'mysql', + 'db.query.text': 'SELECT * FROM does_not_exist', }), - name: 'SELECT does_not_exist', - status: 'error', }), ]), }; @@ -88,7 +79,7 @@ describeWithDockerCompose('mysql2 auto instrumentation', { workingDirectory: [__ 'instrument.mjs', (createTestRunner, test) => { test('should auto-instrument `mysql2` package without connection.connect()', { timeout: 75_000 }, async () => { - await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed(); + await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); }, // mysql2 >= 3.20.0 publishes its own diagnostics channels, which the SDK subscribes to instead From 60a796f075b0298c8fe16413e49e80034f9a150c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 11:38:07 +0200 Subject: [PATCH 4/6] test(node): Rename MySQL span expectation override Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mysql/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 05fbd28601a2..e30185f7897b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -188,7 +188,7 @@ describe('mysql auto instrumentation', () => { for (const { label, env, flags, origin, failsOnEsm } of CASES) { describe(label, () => { - for (const [scenario, description, transactionOverride] of SCENARIOS) { + for (const [scenario, description, spanOverride] of SCENARIOS) { createEsmAndCjsTests( __dirname, scenario, @@ -202,7 +202,7 @@ describe('mysql auto instrumentation', () => { span: container => { expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); const spans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === DB); - expect(spans).toEqual(expectedSpans(mysqlPort, origin, transactionOverride)); + expect(spans).toEqual(expectedSpans(mysqlPort, origin, spanOverride)); }, }) .start() From f8d41e5a3029c8591db48bdc988e07743bc1d059 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 11:38:47 +0200 Subject: [PATCH 5/6] test(node): Update MySQL segment span comment Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mysql/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e30185f7897b..4717e90ef630 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -168,7 +168,7 @@ describe('mysql auto instrumentation', () => { 'scenario-streamError.mjs', 'streamed query error', { - // The transaction itself succeeds (status `ok`); only the failing query's child span is errored. + // The segment span succeeds (status `ok`); only the failing query span is errored. spans: expect.arrayContaining([ expect.objectContaining({ name: 'SELECT does_not_exist', From 1103e22bff23df8720f5e656db6cfb19c20935d1 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 14:02:40 +0200 Subject: [PATCH 6/6] test(node): require one failed MySQL query span Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mysql/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4717e90ef630..2a9cb3c3010a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -169,7 +169,7 @@ describe('mysql auto instrumentation', () => { 'streamed query error', { // The segment span succeeds (status `ok`); only the failing query span is errored. - spans: expect.arrayContaining([ + spans: [ expect.objectContaining({ name: 'SELECT does_not_exist', // A failing streamed query emits `error`, which marks the span as errored @@ -181,7 +181,7 @@ describe('mysql auto instrumentation', () => { [DB_USER]: { type: 'string', value: 'root' }, }), }), - ]), + ], }, ], ] as const;