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 170ad6f6a702..22bf57f14364 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,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', + 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 a5e06557bcd9..079e8565d3c5 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,4 +1,14 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_COLLECTION_NAME, + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + 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'; @@ -25,19 +35,40 @@ describe('MongoDB v4 auto-instrumentation', () => { 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, + [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), + [DB_QUERY_TEXT]: expect.any(String), }), op: 'db', origin, }); + const streamedSpanFor = (operation: string): unknown => + expect.objectContaining({ + name: `${operation} movies`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: origin }, + [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' }, + [DB_OPERATION_NAME]: { type: 'string', value: operation }, + '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' }, + }), + }); + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -60,7 +91,7 @@ describe('MongoDB v4 auto-instrumentation', () => { const pooledFinds = spans.filter( s => s.origin === origin && - (s.data as Record)?.['db.operation.name'] === 'find' && + (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); @@ -69,6 +100,31 @@ describe('MongoDB v4 auto-instrumentation', () => { .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) => { + const spans = container.items; + expect(spans).toContainEqual(streamedSpanFor('insert')); + expect(spans).toContainEqual(streamedSpanFor('find')); + expect(spans).toContainEqual(streamedSpanFor('update')); + + const opIds = new Set(spans.filter(span => /^op-[abc]$/.test(span.name)).map(span => span.span_id)); + expect(opIds.size).toBe(3); + const pooledFinds = spans.filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === origin && + span.attributes[DB_OPERATION_NAME]?.value === 'find' && + opIds.has(span.parent_span_id as string), + ); + expect(new Set(pooledFinds.map(span => span.parent_span_id)).size).toBe(3); + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongodb: '4.17.2' } }, ); 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 170ad6f6a702..22bf57f14364 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,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', + 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 c6209f81afbb..c250d3375b04 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,4 +1,14 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_COLLECTION_NAME, + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + 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'; @@ -25,19 +35,40 @@ describe('MongoDB v5 auto-instrumentation', () => { 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, + [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), + [DB_QUERY_TEXT]: expect.any(String), }), op: 'db', origin, }); + const streamedSpanFor = (operation: string): unknown => + expect.objectContaining({ + name: `${operation} movies`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: origin }, + [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' }, + [DB_OPERATION_NAME]: { type: 'string', value: operation }, + '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' }, + }), + }); + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -60,7 +91,7 @@ describe('MongoDB v5 auto-instrumentation', () => { const pooledFinds = spans.filter( s => s.origin === origin && - (s.data as Record)?.['db.operation.name'] === 'find' && + (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); @@ -69,6 +100,31 @@ describe('MongoDB v5 auto-instrumentation', () => { .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) => { + const spans = container.items; + expect(spans).toContainEqual(streamedSpanFor('insert')); + expect(spans).toContainEqual(streamedSpanFor('find')); + expect(spans).toContainEqual(streamedSpanFor('update')); + + const opIds = new Set(spans.filter(span => /^op-[abc]$/.test(span.name)).map(span => span.span_id)); + expect(opIds.size).toBe(3); + const pooledFinds = spans.filter( + span => + span.attributes[SENTRY_ORIGIN]?.value === origin && + span.attributes[DB_OPERATION_NAME]?.value === 'find' && + opIds.has(span.parent_span_id as string), + ); + expect(new Set(pooledFinds.map(span => span.parent_span_id)).size).toBe(3); + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongodb: '5.9.2' } }, ); 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 170ad6f6a702..22bf57f14364 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,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', + 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 b3a89f4bfdda..32bb93fa4af7 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,4 +1,14 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_COLLECTION_NAME, + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + 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'; @@ -28,19 +38,40 @@ describe('MongoDB v6 auto-instrumentation', () => { 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, + [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), + [DB_QUERY_TEXT]: expect.any(String), }), op: 'db', origin, }); + const streamedSpanFor = (operation: string): unknown => + expect.objectContaining({ + name: `${operation} movies`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: origin }, + [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' }, + [DB_OPERATION_NAME]: { type: 'string', value: operation }, + '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' }, + }), + }); + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -66,6 +97,30 @@ describe('MongoDB v6 auto-instrumentation', () => { .start() .completed(); }); + + test('auto-instruments modern `mongodb` with span streaming enabled.', async () => { + await createTestRunner() + .withEnv({ STREAMED: 'true' }) + .expect({ + span: (container: SerializedStreamedSpanContainer) => { + const spans = container.items; + expect(spans).toContainEqual(streamedSpanFor('insert')); + expect(spans).toContainEqual(streamedSpanFor('find')); + expect(spans).toContainEqual(streamedSpanFor('update')); + + expect(container.items.find(item => item.is_segment)?.name).toBe('Test Transaction'); + + const mongoSpans = spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === origin); + expect(mongoSpans.length).toBeGreaterThan(0); + for (const span of mongoSpans) { + expect(span.is_segment).toBe(false); + expect(span.parent_span_id).toBeTruthy(); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongodb: '6.21.0' } }, ); 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 170ad6f6a702..22bf57f14364 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,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', + 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 79b8f460af0d..f367ae15ae4c 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,4 +1,14 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_COLLECTION_NAME, + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + 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'; @@ -28,19 +38,40 @@ describe('MongoDB v7 auto-instrumentation', () => { 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, + [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), + [DB_QUERY_TEXT]: expect.any(String), }), op: 'db', origin, }); + const streamedSpanFor = (operation: string): unknown => + expect.objectContaining({ + name: `${operation} movies`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status: 'ok', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: origin }, + [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' }, + [DB_OPERATION_NAME]: { type: 'string', value: operation }, + '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' }, + }), + }); + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -66,6 +97,30 @@ describe('MongoDB v7 auto-instrumentation', () => { .start() .completed(); }); + + test('auto-instruments modern `mongodb` with span streaming enabled.', async () => { + await createTestRunner() + .withEnv({ STREAMED: 'true' }) + .expect({ + span: (container: SerializedStreamedSpanContainer) => { + const spans = container.items; + expect(spans).toContainEqual(streamedSpanFor('insert')); + expect(spans).toContainEqual(streamedSpanFor('find')); + expect(spans).toContainEqual(streamedSpanFor('update')); + + expect(container.items.find(item => item.is_segment)?.name).toBe('Test Transaction'); + + const mongoSpans = spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === origin); + expect(mongoSpans.length).toBeGreaterThan(0); + for (const span of mongoSpans) { + expect(span.is_segment).toBe(false); + expect(span.parent_span_id).toBeTruthy(); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongodb: '7.5.0' } }, ); 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 170ad6f6a702..22bf57f14364 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,7 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', + 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 86648b0b71c4..d0c7647bf782 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -1,4 +1,24 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_COLLECTION_NAME, + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + ERROR_TYPE, + SENTRY_ENVIRONMENT, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_STATUS_MESSAGE, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, +} 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'; @@ -184,4 +204,154 @@ describe('MongoDB auto-instrumentation', () => { .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) }, + }, + 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(); + }); + }); + }); }); diff --git a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts index b3b8ca98c4a9..c01b15396727 100644 --- a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts @@ -9,7 +9,13 @@ import { SERVER_PORT, } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; -import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { + getClient, + hasSpanStreamingEnabled, + isObjectLike, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + startInactiveSpan, +} from '@sentry/core'; // `db.connection_string` is not part of `@sentry/conventions`, so it stays inlined to match // what `@opentelemetry/instrumentation-mongodb` emitted. @@ -220,8 +226,19 @@ export function getV3SpanAttributes( * to support platforms that lack it (ie, Deno). */ export function startMongoSpan(attributes: SpanAttributes): Span { + const operation = attributes[DB_OPERATION_NAME] as string | undefined; + const target = (attributes[DB_COLLECTION_NAME] || attributes[DB_NAMESPACE]) as string | undefined; + + const client = getClient(); + const name = + client && hasSpanStreamingEnabled(client) + ? operation && target + ? `${operation} ${target}` + : target || DB_SYSTEM_VALUE_MONGODB + : (attributes[DB_QUERY_TEXT] as string) || `mongodb.${attributes[DB_OPERATION_NAME] || 'command'}`; + return startInactiveSpan({ - name: (attributes[DB_QUERY_TEXT] as string) || `mongodb.${attributes[DB_OPERATION_NAME] || 'command'}`, + name, op: 'db', attributes: { [SENTRY_KIND]: 'client',