From 4ba246d67ec5e757da2f1d4025ff3947b2f35b18 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 7 Oct 2026 18:25:35 +0200 Subject: [PATCH 1/2] test(node): Port MongoDB integration tests to span streaming Co-Authored-By: GPT-6 --- .../suites/tracing/mongodb-v4/instrument.mjs | 1 - .../suites/tracing/mongodb-v4/test.ts | 52 +-- .../suites/tracing/mongodb-v5/instrument.mjs | 1 - .../suites/tracing/mongodb-v5/test.ts | 52 +-- .../suites/tracing/mongodb-v6/instrument.mjs | 1 - .../suites/tracing/mongodb-v6/test.ts | 46 +-- .../suites/tracing/mongodb-v7/instrument.mjs | 1 - .../suites/tracing/mongodb-v7/test.ts | 46 +-- .../suites/tracing/mongodb/instrument.mjs | 1 - .../suites/tracing/mongodb/test.ts | 379 +++++------------- 10 files changed, 120 insertions(+), 460 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : '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/mongodb-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts index 079e8565d3c5..a00013c8470d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts @@ -1,5 +1,6 @@ import { DB_COLLECTION_NAME, + DB_CONNECTION_STRING, DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, @@ -8,7 +9,6 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer, TransactionEvent } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -32,24 +32,6 @@ describe('MongoDB v4 auto-instrumentation', () => { const origin = 'auto.db.mongo'; - const spanFor = (operation: string): unknown => - expect.objectContaining({ - data: expect.objectContaining({ - [SENTRY_ORIGIN]: origin, - [SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: 'mongodb', - [DB_NAMESPACE]: 'admin', - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: operation, - // `db.connection_string` has no `@sentry/conventions` constant — it stays inlined to match - // what `@opentelemetry/instrumentation-mongodb` emitted. - 'db.connection_string': expect.any(String), - [DB_QUERY_TEXT]: expect.any(String), - }), - op: 'db', - origin, - }); - const streamedSpanFor = (operation: string): unknown => expect.objectContaining({ name: `${operation} movies`, @@ -63,7 +45,7 @@ describe('MongoDB v4 auto-instrumentation', () => { [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, [DB_OPERATION_NAME]: { type: 'string', value: operation }, - 'db.connection_string': { type: 'string', value: expect.any(String) }, + [DB_CONNECTION_STRING]: { type: 'string', value: expect.any(String) }, [DB_QUERY_TEXT]: { type: 'string', value: expect.any(String) }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, }), @@ -77,35 +59,7 @@ describe('MongoDB v4 auto-instrumentation', () => { test('auto-instruments `mongodb` (>= 4.0 < 6.4 callback command) and parents pooled ops correctly.', async () => { await createTestRunner() .expect({ - transaction: (event: TransactionEvent) => { - const spans = event.spans || []; - expect(spans).toContainEqual(spanFor('insert')); - expect(spans).toContainEqual(spanFor('find')); - expect(spans).toContainEqual(spanFor('update')); - - // Checkout context propagation: each `op-*` span must be the - // parent of its own find command. A lost context would collapse - // them onto one parent (or the transaction). - const opIds = new Set(spans.filter(s => /^op-[abc]$/.test(s.description ?? '')).map(s => s.span_id)); - expect(opIds.size).toBe(3); - const pooledFinds = spans.filter( - s => - s.origin === origin && - (s.data as Record)?.[DB_OPERATION_NAME] === 'find' && - opIds.has(s.parent_span_id as string), - ); - expect(new Set(pooledFinds.map(s => s.parent_span_id)).size).toBe(3); - }, - }) - .start() - .completed(); - }); - - test('auto-instruments `mongodb` and parents pooled ops correctly with span streaming enabled.', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const spans = container.items; expect(spans).toContainEqual(streamedSpanFor('insert')); expect(spans).toContainEqual(streamedSpanFor('find')); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : '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/mongodb-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts index c250d3375b04..7e36852dae00 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts @@ -1,5 +1,6 @@ import { DB_COLLECTION_NAME, + DB_CONNECTION_STRING, DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, @@ -8,7 +9,6 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer, TransactionEvent } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -32,24 +32,6 @@ describe('MongoDB v5 auto-instrumentation', () => { const origin = 'auto.db.mongo'; - const spanFor = (operation: string): unknown => - expect.objectContaining({ - data: expect.objectContaining({ - [SENTRY_ORIGIN]: origin, - [SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: 'mongodb', - [DB_NAMESPACE]: 'admin', - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: operation, - // `db.connection_string` has no `@sentry/conventions` constant — it stays inlined to match - // what `@opentelemetry/instrumentation-mongodb` emitted. - 'db.connection_string': expect.any(String), - [DB_QUERY_TEXT]: expect.any(String), - }), - op: 'db', - origin, - }); - const streamedSpanFor = (operation: string): unknown => expect.objectContaining({ name: `${operation} movies`, @@ -63,7 +45,7 @@ describe('MongoDB v5 auto-instrumentation', () => { [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, [DB_OPERATION_NAME]: { type: 'string', value: operation }, - 'db.connection_string': { type: 'string', value: expect.any(String) }, + [DB_CONNECTION_STRING]: { type: 'string', value: expect.any(String) }, [DB_QUERY_TEXT]: { type: 'string', value: expect.any(String) }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, }), @@ -77,35 +59,7 @@ describe('MongoDB v5 auto-instrumentation', () => { test('auto-instruments `mongodb` (>= 4.0 < 6.4 callback command) and parents pooled ops correctly.', async () => { await createTestRunner() .expect({ - transaction: (event: TransactionEvent) => { - const spans = event.spans || []; - expect(spans).toContainEqual(spanFor('insert')); - expect(spans).toContainEqual(spanFor('find')); - expect(spans).toContainEqual(spanFor('update')); - - // Checkout context propagation: each `op-*` span must be the - // parent of its own find command. A lost context would collapse - // them onto one parent (or the transaction). - const opIds = new Set(spans.filter(s => /^op-[abc]$/.test(s.description ?? '')).map(s => s.span_id)); - expect(opIds.size).toBe(3); - const pooledFinds = spans.filter( - s => - s.origin === origin && - (s.data as Record)?.[DB_OPERATION_NAME] === 'find' && - opIds.has(s.parent_span_id as string), - ); - expect(new Set(pooledFinds.map(s => s.parent_span_id)).size).toBe(3); - }, - }) - .start() - .completed(); - }); - - test('auto-instruments `mongodb` and parents pooled ops correctly with span streaming enabled.', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const spans = container.items; expect(spans).toContainEqual(streamedSpanFor('insert')); expect(spans).toContainEqual(streamedSpanFor('find')); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : '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/mongodb-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts index 32bb93fa4af7..cd38ee2d7e57 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts @@ -1,5 +1,6 @@ import { DB_COLLECTION_NAME, + DB_CONNECTION_STRING, DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, @@ -8,7 +9,6 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer, TransactionEvent } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -35,24 +35,6 @@ describe('MongoDB v6 auto-instrumentation', () => { // `db.statement` (scrubbed full command doc) and `db.connection_string` vary // by driver version, so assert their presence rather than exact content; // the operation-identifying attributes are exact. - const spanFor = (operation: string): unknown => - expect.objectContaining({ - data: expect.objectContaining({ - [SENTRY_ORIGIN]: origin, - [SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: 'mongodb', - [DB_NAMESPACE]: 'admin', - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: operation, - // `db.connection_string` has no `@sentry/conventions` constant — it stays inlined to match - // what `@opentelemetry/instrumentation-mongodb` emitted. - 'db.connection_string': expect.any(String), - [DB_QUERY_TEXT]: expect.any(String), - }), - op: 'db', - origin, - }); - const streamedSpanFor = (operation: string): unknown => expect.objectContaining({ name: `${operation} movies`, @@ -66,7 +48,7 @@ describe('MongoDB v6 auto-instrumentation', () => { [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, [DB_OPERATION_NAME]: { type: 'string', value: operation }, - 'db.connection_string': { type: 'string', value: expect.any(String) }, + [DB_CONNECTION_STRING]: { type: 'string', value: expect.any(String) }, [DB_QUERY_TEXT]: { type: 'string', value: expect.any(String) }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, }), @@ -80,29 +62,7 @@ describe('MongoDB v6 auto-instrumentation', () => { test('auto-instruments modern `mongodb` (>= 6.4 promise command).', async () => { await createTestRunner() .expect({ - transaction: (event: TransactionEvent) => { - const spans = event.spans || []; - expect(spans).toContainEqual(spanFor('insert')); - expect(spans).toContainEqual(spanFor('find')); - expect(spans).toContainEqual(spanFor('update')); - // No orphaned handshake/heartbeat spans! - // every command span has a parent. - const mongoSpans = spans.filter(s => s.origin === origin); - expect(mongoSpans.length).toBeGreaterThan(0); - for (const s of mongoSpans) { - expect(s.parent_span_id).toBeTruthy(); - } - }, - }) - .start() - .completed(); - }); - - test('auto-instruments modern `mongodb` with span streaming enabled.', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const spans = container.items; expect(spans).toContainEqual(streamedSpanFor('insert')); expect(spans).toContainEqual(streamedSpanFor('find')); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : '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/mongodb-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts index f367ae15ae4c..bcaab2e92bd0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts @@ -1,5 +1,6 @@ import { DB_COLLECTION_NAME, + DB_CONNECTION_STRING, DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, @@ -8,7 +9,6 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer, TransactionEvent } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -35,24 +35,6 @@ describe('MongoDB v7 auto-instrumentation', () => { // `db.statement` (scrubbed full command doc) and `db.connection_string` vary // by driver version, so assert their presence rather than exact content; // the operation-identifying attributes are exact. - const spanFor = (operation: string): unknown => - expect.objectContaining({ - data: expect.objectContaining({ - [SENTRY_ORIGIN]: origin, - [SENTRY_OP]: 'db', - [DB_SYSTEM_NAME]: 'mongodb', - [DB_NAMESPACE]: 'admin', - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: operation, - // `db.connection_string` has no `@sentry/conventions` constant — it stays inlined to match - // what `@opentelemetry/instrumentation-mongodb` emitted. - 'db.connection_string': expect.any(String), - [DB_QUERY_TEXT]: expect.any(String), - }), - op: 'db', - origin, - }); - const streamedSpanFor = (operation: string): unknown => expect.objectContaining({ name: `${operation} movies`, @@ -66,7 +48,7 @@ describe('MongoDB v7 auto-instrumentation', () => { [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, [DB_OPERATION_NAME]: { type: 'string', value: operation }, - 'db.connection_string': { type: 'string', value: expect.any(String) }, + [DB_CONNECTION_STRING]: { type: 'string', value: expect.any(String) }, [DB_QUERY_TEXT]: { type: 'string', value: expect.any(String) }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, }), @@ -80,29 +62,7 @@ describe('MongoDB v7 auto-instrumentation', () => { test('auto-instruments modern `mongodb` (>= 6.4 promise command).', async () => { await createTestRunner() .expect({ - transaction: (event: TransactionEvent) => { - const spans = event.spans || []; - expect(spans).toContainEqual(spanFor('insert')); - expect(spans).toContainEqual(spanFor('find')); - expect(spans).toContainEqual(spanFor('update')); - // No orphaned handshake/heartbeat spans! - // every command span has a parent. - const mongoSpans = spans.filter(s => s.origin === origin); - expect(mongoSpans.length).toBeGreaterThan(0); - for (const s of mongoSpans) { - expect(s.parent_span_id).toBeTruthy(); - } - }, - }) - .start() - .completed(); - }); - - test('auto-instruments modern `mongodb` with span streaming enabled.', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const spans = container.items; expect(spans).toContainEqual(streamedSpanFor('insert')); expect(spans).toContainEqual(streamedSpanFor('find')); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongodb/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : '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/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index f0f45e51f35c..48a74b770306 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -1,11 +1,13 @@ import { DB_COLLECTION_NAME, + DB_CONNECTION_STRING, DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ERROR_TYPE, SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, SENTRY_KIND, SENTRY_OP, SENTRY_ORIGIN, @@ -17,11 +19,10 @@ import { SENTRY_STATUS_MESSAGE, SENTRY_TRACE_LIFECYCLE, SERVER_ADDRESS, + SERVER_PORT, } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer, TransactionEvent } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; -import { assertSentryTransaction } from '../../../utils/assertions'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; describe('MongoDB auto-instrumentation', () => { @@ -40,144 +41,131 @@ describe('MongoDB auto-instrumentation', () => { cleanupChildProcesses(); }); - const SPAN_FIND_MATCHER = expect.objectContaining({ - data: { - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.namespace': 'admin', - 'db.collection.name': 'movies', - 'db.operation.name': 'find', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': expect.any(Number), - 'db.query.text': '{"title":"?"}', - 'sentry.kind': 'client', + const streamAttributes = (values: Record): Record => + Object.fromEntries(Object.entries(values).map(([key, value]) => [key, { type: 'string', value }])); + + function streamedSpan({ + name, + status = 'ok', + attributes, + }: { + name: string; + status?: string; + attributes: Record; + }): unknown { + return { + name, + attributes: { + ...streamAttributes({ + [DB_CONNECTION_STRING]: expect.any(String), + [DB_NAMESPACE]: 'admin', + [DB_SYSTEM_NAME]: 'mongodb', + [SENTRY_ENVIRONMENT]: 'production', + [SENTRY_KIND]: 'client', + [SENTRY_OP]: 'db', + [SENTRY_ORIGIN]: origin, + [SENTRY_RELEASE]: '1.0', + [SENTRY_SDK_NAME]: 'sentry.javascript.node', + [SENTRY_SDK_VERSION]: expect.any(String), + [SENTRY_SEGMENT_ID]: expect.stringMatching(/^[\da-f]{16}$/), + [SENTRY_SEGMENT_NAME]: 'Test Transaction', + [SERVER_ADDRESS]: expect.any(String), + [SENTRY_TRACE_LIFECYCLE]: 'stream', + ...attributes, + }), + [SERVER_PORT]: { type: 'integer', value: expect.any(Number) }, + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + }, + 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, + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }; + } + + const STREAMED_FIND_MATCHER = streamedSpan({ + name: 'find movies', + attributes: { + [DB_COLLECTION_NAME]: 'movies', + [DB_OPERATION_NAME]: 'find', + [DB_QUERY_TEXT]: '{"title":"?"}', }, - description: '{"title":"?"}', - op: 'db', - origin, }); - const SPAN_INSERT_MATCHER = expect.objectContaining({ - data: { - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.namespace': 'admin', - 'db.collection.name': 'movies', - 'db.operation.name': 'insert', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': expect.any(Number), - 'db.query.text': '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', - 'sentry.kind': 'client', + const STREAMED_INSERT_MATCHER = streamedSpan({ + name: 'insert movies', + attributes: { + [DB_COLLECTION_NAME]: 'movies', + [DB_OPERATION_NAME]: 'insert', + [DB_QUERY_TEXT]: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', }, - description: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', - op: 'db', - origin, }); - const SPAN_ISMASTER_MATCHER = expect.objectContaining({ - data: { - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.namespace': 'admin', - 'db.collection.name': '$cmd', - 'db.operation.name': 'isMaster', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': expect.any(Number), - 'db.query.text': + const STREAMED_ISMASTER_MATCHER = streamedSpan({ + name: 'isMaster $cmd', + attributes: { + [DB_COLLECTION_NAME]: '$cmd', + [DB_OPERATION_NAME]: 'isMaster', + [DB_QUERY_TEXT]: '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}', - 'sentry.kind': 'client', }, - description: - '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}', - op: 'db', - origin, }); - const SPAN_UPDATE_MATCHER = expect.objectContaining({ - data: { - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.namespace': 'admin', - 'db.collection.name': 'movies', - 'db.operation.name': 'update', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': expect.any(Number), - 'db.query.text': '{"title":"?"}', - 'sentry.kind': 'client', + const STREAMED_UPDATE_MATCHER = streamedSpan({ + name: 'update movies', + attributes: { + [DB_COLLECTION_NAME]: 'movies', + [DB_OPERATION_NAME]: 'update', + [DB_QUERY_TEXT]: '{"title":"?"}', }, - description: '{"title":"?"}', - op: 'db', - origin, }); // A query the server rejects: same attributes as a successful find, but with an error status. - const SPAN_FIND_ERROR_MATCHER = expect.objectContaining({ - data: expect.objectContaining({ - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.operation.name': 'find', - 'db.query.text': '{"$thisOperatorDoesNotExist":"?"}', - 'sentry.kind': 'client', - }), - description: '{"$thisOperatorDoesNotExist":"?"}', - op: 'db', - origin, - status: 'internal_error', + const STREAMED_FIND_ERROR_MATCHER = streamedSpan({ + name: 'find movies', + status: 'error', + attributes: { + [DB_COLLECTION_NAME]: 'movies', + [DB_OPERATION_NAME]: 'find', + [DB_QUERY_TEXT]: '{"$thisOperatorDoesNotExist":"?"}', + [ERROR_TYPE]: 'MongoError', + [SENTRY_STATUS_MESSAGE]: expect.any(String), + }, }); - const SPAN_ENDSESSIONS_MATCHER = expect.objectContaining({ - data: { - 'sentry.origin': origin, - 'sentry.op': 'db', - 'db.system.name': 'mongodb', - 'db.namespace': 'admin', - 'db.collection.name': '$cmd', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': expect.any(Number), - 'db.query.text': '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', - 'sentry.kind': 'client', + // `endSessions` exposes no operation name, so the span falls back to the collection alone. + const STREAMED_ENDSESSIONS_MATCHER = streamedSpan({ + name: '$cmd', + attributes: { + [DB_COLLECTION_NAME]: '$cmd', + [DB_QUERY_TEXT]: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', }, - description: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', - op: 'db', - origin, }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('should auto-instrument `mongodb` package.', async () => { await createTestRunner() .expect({ - transaction: (txn: TransactionEvent) => { - assertSentryTransaction(txn, { transaction: 'Test Transaction' }); - const spans = txn.spans || []; + span: container => { + expect(container.items.find(item => item.is_segment)).toMatchObject({ + name: 'Test Transaction', + span_id: expect.any(String), + start_timestamp: expect.anything(), + end_timestamp: expect.anything(), + }); + + const spans = container.items.filter(item => !item.is_segment); - // Assert the per-operation breakdown rather than just a total span - // count. When the driver occasionally emits an extra command - // (e.g. a stray `isMaster` from a reconnect, a `ping`, or a - // heartbeat), `toEqual` shows a clear per-operation diff like - // "isMaster: 2 → 3" instead of an opaque "8 vs 9" length - // mismatch — making future flakes self-diagnosing. - // - // `db.operation` isn't set on every span — the `endSessions` - // command exposes its name only via `db.statement` — so derive - // the operation by parsing the leading command name out of - // `db.statement` as a fallback. + // Count by operation so an extra driver command produces a readable diff. const operationCounts = spans.reduce>((acc, span) => { - const data = (span.data ?? {}) as Record; - let op = - typeof data['db.operation.name'] === 'string' ? (data['db.operation.name'] as string) : undefined; + const operation = span.attributes[DB_OPERATION_NAME]?.value; + let op = typeof operation === 'string' ? operation : undefined; if (!op) { - const stmt = data['db.query.text']; - const match = typeof stmt === 'string' ? stmt.match(/^\{"(\w+)"/) : null; + const statement = span.attributes[DB_QUERY_TEXT]?.value; + const match = typeof statement === 'string' ? statement.match(/^\{"(\w+)"/) : null; op = match ? match[1] : 'unknown'; } acc[op] = (acc[op] || 0) + 1; @@ -192,167 +180,16 @@ describe('MongoDB auto-instrumentation', () => { endSessions: 1, }); - expect(spans).toContainEqual(SPAN_FIND_MATCHER); - expect(spans).toContainEqual(SPAN_INSERT_MATCHER); - expect(spans).toContainEqual(SPAN_ISMASTER_MATCHER); - expect(spans).toContainEqual(SPAN_UPDATE_MATCHER); - expect(spans).toContainEqual(SPAN_FIND_ERROR_MATCHER); - expect(spans).toContainEqual(SPAN_ENDSESSIONS_MATCHER); + expect(spans).toContainEqual(STREAMED_FIND_MATCHER); + expect(spans).toContainEqual(STREAMED_INSERT_MATCHER); + expect(spans).toContainEqual(STREAMED_ISMASTER_MATCHER); + expect(spans).toContainEqual(STREAMED_UPDATE_MATCHER); + expect(spans).toContainEqual(STREAMED_FIND_ERROR_MATCHER); + expect(spans).toContainEqual(STREAMED_ENDSESSIONS_MATCHER); }, }) .start() .completed(); }); }); - - describe('streamed', () => { - const streamAttributes = (values: Record): Record => - Object.fromEntries(Object.entries(values).map(([key, value]) => [key, { type: 'string', value }])); - - function streamedSpan({ - name, - status = 'ok', - attributes, - }: { - name: string; - status?: string; - attributes: Record; - }): unknown { - return { - name, - attributes: { - ...streamAttributes({ - 'db.connection_string': expect.any(String), - [DB_NAMESPACE]: 'admin', - [DB_SYSTEM_NAME]: 'mongodb', - [SENTRY_ENVIRONMENT]: 'production', - [SENTRY_KIND]: 'client', - [SENTRY_OP]: 'db', - [SENTRY_ORIGIN]: origin, - [SENTRY_RELEASE]: '1.0', - [SENTRY_SDK_NAME]: 'sentry.javascript.node', - [SENTRY_SDK_VERSION]: expect.any(String), - [SENTRY_SEGMENT_ID]: expect.stringMatching(/^[\da-f]{16}$/), - [SENTRY_SEGMENT_NAME]: 'Test Transaction', - [SERVER_ADDRESS]: expect.any(String), - [SENTRY_TRACE_LIFECYCLE]: 'stream', - ...attributes, - }), - 'server.port': { type: 'integer', value: expect.any(Number) }, - 'sentry.is_localhost': { type: 'boolean', value: false }, - }, - 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, - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }; - } - - const STREAMED_FIND_MATCHER = streamedSpan({ - name: 'find movies', - attributes: { - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: 'find', - [DB_QUERY_TEXT]: '{"title":"?"}', - }, - }); - - const STREAMED_INSERT_MATCHER = streamedSpan({ - name: 'insert movies', - attributes: { - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: 'insert', - [DB_QUERY_TEXT]: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', - }, - }); - - const STREAMED_ISMASTER_MATCHER = streamedSpan({ - name: 'isMaster $cmd', - attributes: { - [DB_COLLECTION_NAME]: '$cmd', - [DB_OPERATION_NAME]: 'isMaster', - [DB_QUERY_TEXT]: - '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}', - }, - }); - - const STREAMED_UPDATE_MATCHER = streamedSpan({ - name: 'update movies', - attributes: { - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: 'update', - [DB_QUERY_TEXT]: '{"title":"?"}', - }, - }); - - // A query the server rejects: same attributes as a successful find, but with an error status. - const STREAMED_FIND_ERROR_MATCHER = streamedSpan({ - name: 'find movies', - status: 'error', - attributes: { - [DB_COLLECTION_NAME]: 'movies', - [DB_OPERATION_NAME]: 'find', - [DB_QUERY_TEXT]: '{"$thisOperatorDoesNotExist":"?"}', - [ERROR_TYPE]: 'MongoError', - [SENTRY_STATUS_MESSAGE]: expect.any(String), - }, - }); - - // `endSessions` exposes no operation name, so the span falls back to the collection alone. - const STREAMED_ENDSESSIONS_MATCHER = streamedSpan({ - name: '$cmd', - attributes: { - [DB_COLLECTION_NAME]: '$cmd', - [DB_QUERY_TEXT]: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', - }, - }); - - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `mongodb` package with span streaming enabled.', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { - expect(container.items.find(item => item.is_segment)?.name).toBe('Test Transaction'); - - const spans = container.items.filter(item => !item.is_segment); - - // Same per-operation breakdown as the transaction-based test above, so an extra - // driver command shows up as a readable per-operation diff. - const operationCounts = spans.reduce>((acc, span) => { - const operation = span.attributes['db.operation.name']?.value; - let op = typeof operation === 'string' ? operation : undefined; - if (!op) { - const statement = span.attributes['db.query.text']?.value; - const match = typeof statement === 'string' ? statement.match(/^\{"(\w+)"/) : null; - op = match ? match[1] : 'unknown'; - } - acc[op] = (acc[op] || 0) + 1; - return acc; - }, {}); - - expect(operationCounts).toEqual({ - find: 4, - isMaster: 2, - insert: 1, - update: 1, - endSessions: 1, - }); - - expect(spans).toContainEqual(STREAMED_FIND_MATCHER); - expect(spans).toContainEqual(STREAMED_INSERT_MATCHER); - expect(spans).toContainEqual(STREAMED_ISMASTER_MATCHER); - expect(spans).toContainEqual(STREAMED_UPDATE_MATCHER); - expect(spans).toContainEqual(STREAMED_FIND_ERROR_MATCHER); - expect(spans).toContainEqual(STREAMED_ENDSESSIONS_MATCHER); - }, - }) - .start() - .completed(); - }); - }); - }); }); From 6b9a3ac2384919052a439a5819145dfc4bf9152a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 11:04:37 +0200 Subject: [PATCH 2/2] test(node): Use conventions constants for span ops Co-Authored-By: GPT-6 --- .../node-integration-tests/suites/tracing/mongodb-v4/test.ts | 3 ++- .../node-integration-tests/suites/tracing/mongodb-v5/test.ts | 3 ++- .../node-integration-tests/suites/tracing/mongodb-v6/test.ts | 3 ++- .../node-integration-tests/suites/tracing/mongodb-v7/test.ts | 3 ++- .../node-integration-tests/suites/tracing/mongodb/test.ts | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts index a00013c8470d..1aa22d8230fe 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v4/test.ts @@ -9,6 +9,7 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -40,7 +41,7 @@ describe('MongoDB v4 auto-instrumentation', () => { status: 'ok', attributes: expect.objectContaining({ [SENTRY_ORIGIN]: { type: 'string', value: origin }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [DB_SYSTEM_NAME]: { type: 'string', value: 'mongodb' }, [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts index 7e36852dae00..ac3d08712184 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v5/test.ts @@ -9,6 +9,7 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -40,7 +41,7 @@ describe('MongoDB v5 auto-instrumentation', () => { status: 'ok', attributes: expect.objectContaining({ [SENTRY_ORIGIN]: { type: 'string', value: origin }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [DB_SYSTEM_NAME]: { type: 'string', value: 'mongodb' }, [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts index cd38ee2d7e57..5033b4bbe039 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v6/test.ts @@ -9,6 +9,7 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -43,7 +44,7 @@ describe('MongoDB v6 auto-instrumentation', () => { status: 'ok', attributes: expect.objectContaining({ [SENTRY_ORIGIN]: { type: 'string', value: origin }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [DB_SYSTEM_NAME]: { type: 'string', value: 'mongodb' }, [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts index bcaab2e92bd0..f38fed0f1692 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb-v7/test.ts @@ -9,6 +9,7 @@ import { SENTRY_ORIGIN, SENTRY_TRACE_LIFECYCLE, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -43,7 +44,7 @@ describe('MongoDB v7 auto-instrumentation', () => { status: 'ok', attributes: expect.objectContaining({ [SENTRY_ORIGIN]: { type: 'string', value: origin }, - [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: DB }, [DB_SYSTEM_NAME]: { type: 'string', value: 'mongodb' }, [DB_NAMESPACE]: { type: 'string', value: 'admin' }, [DB_COLLECTION_NAME]: { type: 'string', value: 'movies' }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index 48a74b770306..c24482f7fa24 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -21,6 +21,7 @@ import { SERVER_ADDRESS, SERVER_PORT, } from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -62,7 +63,7 @@ describe('MongoDB auto-instrumentation', () => { [DB_SYSTEM_NAME]: 'mongodb', [SENTRY_ENVIRONMENT]: 'production', [SENTRY_KIND]: 'client', - [SENTRY_OP]: 'db', + [SENTRY_OP]: DB, [SENTRY_ORIGIN]: origin, [SENTRY_RELEASE]: '1.0', [SENTRY_SDK_NAME]: 'sentry.javascript.node',