diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts index ec3947f54f4a..2f216f3ab7ed 100644 --- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts @@ -88,7 +88,7 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di await createRunner() .expect({ span: container => { - expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + expect(container.items.find(span => span.is_segment && span.name === 'Test Transaction')).toBeDefined(); const knexSpans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === ORIGIN); expect(knexSpans).toEqual(EXPECTED_SPANS); }, 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 cffe56665a4c..6ea956038d06 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 @@ -66,7 +66,7 @@ describe('mysql2 tracing channel Test', () => { await createTestRunner() .expect({ span: container => { - expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + expect(container.items.find(span => span.is_segment && span.name === 'Test Transaction')).toBeDefined(); 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.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mysql2')).toBeUndefined(); @@ -84,7 +84,7 @@ describe('mysql2 tracing channel Test', () => { await createTestRunner() .expect({ span: container => { - expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction'); + expect(container.items.find(span => span.is_segment && span.name === 'Test Transaction')).toBeDefined(); const spans = container.items.filter(span => !span.is_segment); for (const span of spans) { const queryText = span.attributes[DB_QUERY_TEXT]?.value; diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-no-parent.mjs index 9f872a69b694..5eed9f14064c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-no-parent.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-no-parent.mjs @@ -13,8 +13,7 @@ async function run() { await probe.end(); }); - // No active span here: `requireParentSpan` means the connect and this query - // must NOT produce spans. + // Connect can produce a root span; the query still requires a parent. await client.connect(); await client.query('SELECT 1 AS unparented'); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 4dd383254d1f..e9b55b95e7f1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -167,7 +167,9 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ .expect({ span: container => { expect(container).toMatchObject(EXPECTED_SPANS); - const dbSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === DB); + const dbSpans = container.items.filter( + span => !span.is_segment && span.attributes[SENTRY_OP]?.value === DB, + ); expect(dbSpans.map(span => span.name)).toEqual([ 'pg.connect', 'CREATE TABLE "User"', @@ -375,16 +377,15 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ describe('requireParentSpan', () => { createEsmAndCjsTests(__dirname, 'scenario-no-parent.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('does not instrument queries or connects without an active parent span', { timeout: 90_000 }, async () => { + test('does not instrument queries without an active parent span', { timeout: 90_000 }, async () => { await createTestRunner() .expect({ span: container => { - const descriptions = container.items.map(span => span.name); - // The unparented connect + query must not have produced spans + // The unparented query must not have produced a span expect(container.items.map(span => span.attributes[DB_QUERY_TEXT]?.value)).not.toContain( 'SELECT ? AS unparented', ); - expect(descriptions.find(name => name?.includes('connect'))).toBeUndefined(); + expect(container.items.find(span => span.is_segment && span.name === 'pg.connect')).toBeDefined(); // Only the parented query is instrumented expect(container).toMatchObject({ items: expect.arrayContaining([ @@ -521,7 +522,9 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ .expect({ span: container => { expect(container).toMatchObject(EXPECTED_SPANS); - const dbSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === DB); + const dbSpans = container.items.filter( + span => !span.is_segment && span.attributes[SENTRY_OP]?.value === DB, + ); expect(dbSpans.map(span => span.name)).toEqual([ 'pg.connect', 'CREATE TABLE "NativeUser"', @@ -679,17 +682,16 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ 'instrument-orchestrion.mjs', (createTestRunner, test) => { test( - 'does not instrument queries or connects without an active parent span', + 'does not instrument queries without an active parent span (orchestrion)', { timeout: 90_000 }, async () => { await createTestRunner() .expect({ span: container => { - const descriptions = container.items.map(span => span.name); expect(container.items.map(span => span.attributes[DB_QUERY_TEXT]?.value)).not.toContain( 'SELECT ? AS unparented', ); - expect(descriptions.find(name => name?.includes('connect'))).toBeUndefined(); + expect(container.items.find(span => span.is_segment && span.name === 'pg.connect')).toBeDefined(); expect(container).toMatchObject({ items: expect.arrayContaining([ expect.objectContaining({ name: 'Test Transaction', is_segment: true }), diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index a6f3271d2356..87f0913a2780 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -153,7 +153,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument-ioredis.mjs', (createTestRunner, test) => { test('should create cache spans for prefixed keys (ioredis)', { timeout: 60_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner().unordered().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); }); }); @@ -238,9 +238,14 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory .withEnv({ STREAMED: 'true' }) .expect({ span: container => { - expect(container.items.find(item => item.is_segment)?.name).toBe(segmentName); + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'connect', + segmentName, + ]); - expect(childSpans(container)).toEqual([ + expect( + childSpans(container).filter(item => item.attributes[SENTRY_SEGMENT_NAME]?.value === segmentName), + ).toEqual([ span('set localhost:6383', redisSpanOp, { [DB_OPERATION_NAME]: 'set', [DB_QUERY_TEXT]: 'set test-key [1 other arguments]', diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 36decf4b6001..22111b91a006 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -92,10 +92,16 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d await createTestRunner() .expect({ span: container => { - const segmentSpan = container.items.find(item => item.is_segment); - expect(segmentSpan?.name).toBe('Test Span'); + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'connect', + 'Test Span', + ]); - const dbSpans = container.items.filter(item => item.attributes[SENTRY_OP]?.value === redisSpanOp); + const dbSpans = container.items.filter( + item => + item.attributes[SENTRY_OP]?.value === redisSpanOp && + item.attributes[SENTRY_SEGMENT_NAME]?.value === 'Test Span', + ); expect(dbSpans).toEqual([ expectedDbSpan({ operation: 'set', statement: 'set test-key [1 other arguments]' }), diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 2a5cc75fd56c..41f77b64c6e2 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -127,7 +127,6 @@ function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channe bindTracingChannelToSpan(tracingChannel(channelName), data => { return startInactiveSpan({ name: spanName, - onlyIfParent: true, attributes: { [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 3231e0e60a06..fd9ae4a656f4 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -141,7 +141,7 @@ function subscribeQueryLikeChannel( const spanOptions = getSpanOptions(data); return startInactiveSpan({ ...spanOptions, - onlyIfParent: true, + onlyIfParent: channelName !== CHANNELS.PG_CONNECT && channelName !== CHANNELS.PGPOOL_CONNECT, attributes: { ...spanOptions.attributes, [SENTRY_KIND]: 'client' }, }); }, diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index 9426e26c6add..6cb894039143 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -124,7 +124,6 @@ export function instrumentIoredis(options: RedisCacheOptions): void { const { host, port } = getConnectionOptions(data.self); return startInactiveSpan({ name: 'connect', - onlyIfParent: true, attributes: { [SENTRY_KIND]: 'client', ...connectionAttributes(host, port), diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index dec09eb63d28..571f8b65a309 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -102,8 +102,7 @@ function installTestAsyncContextStrategy(): void { /** * Drives a channel's `tracePromise` inside an enclosing span and captures the span bound by the - * subscriber. The subscriber only creates a recording span when there is an enclosing active span, so the - * helper always establishes one and returns its id for parenting assertions. + * subscriber, returning the enclosing span's id for parenting assertions. */ async function traceOperation( channelName: string, @@ -350,12 +349,17 @@ describe('subscribeMysql2DiagnosticChannels', () => { describe('connect channels', () => { it('creates a connect span without db.query.text', async () => { - const { span } = await traceOperation( - MYSQL2_DC_CHANNEL_CONNECT, + let span: Span | undefined; + await tracingChannel(MYSQL2_DC_CHANNEL_CONNECT).tracePromise( + async () => { + span = getActiveSpan(); + expect(span?.isRecording()).toBe(true); + }, { database: 'test', serverAddress: '127.0.0.1', serverPort: 3306, user: 'root' }, - { result: undefined }, ); + expect(spanToJSON(span!).parent_span_id).toBeUndefined(); + const json = spanToJSON(span!); expect(json.name).toBe('mysql2.connect'); expect(json.attributes['sentry.op']).toBe('db'); @@ -368,12 +372,17 @@ describe('subscribeMysql2DiagnosticChannels', () => { }); it('names the pool connect span distinctly', async () => { - const { span } = await traceOperation( - MYSQL2_DC_CHANNEL_POOL_CONNECT, + let span: Span | undefined; + await tracingChannel(MYSQL2_DC_CHANNEL_POOL_CONNECT).tracePromise( + async () => { + span = getActiveSpan(); + expect(span?.isRecording()).toBe(true); + }, { database: 'test', serverAddress: '127.0.0.1', serverPort: 3306 }, - { result: undefined }, ); + expect(spanToJSON(span!).parent_span_id).toBeUndefined(); + expect(spanToJSON(span!).name).toBe('mysql2.pool.connect'); }); diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index 9af1c1a5c851..78556a41884f 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -170,6 +170,7 @@ describe('postgresIntegration', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'pg.connect', + onlyIfParent: false, attributes: expect.objectContaining({ 'sentry.op': 'db', 'db.system.name': 'postgresql', @@ -201,6 +202,7 @@ describe('postgresIntegration', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'pg-pool.connect', + onlyIfParent: false, attributes: expect.objectContaining({ 'sentry.op': 'db', 'db.system.name': 'postgresql',