diff --git a/MIGRATION.md b/MIGRATION.md index 44f29afe32c4..0dee0ad59433 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -742,6 +742,7 @@ Attribute availability remains runtime-dependent. For example, browser and Worke - Legacy messaging (`messaging.*`) span attributes on the AMQP instrumentation were replaced by their current semantic-convention equivalents: `messaging.destination.name`, `messaging.rabbitmq.destination.routing_key`, `messaging.message.id`, `messaging.message.conversation_id`, `messaging.operation.name`, `network.protocol.name`, `network.protocol.version`, and `url.full`. `messaging.destination_kind` is no longer emitted. - The database span attributes `db.system`, `db.name`, `db.operation`, `db.statement` and `db.mongodb.collection` were renamed to `db.system.name`, `db.namespace`, `db.operation.name`, `db.query.text` and `db.collection.name`. +- Mongoose spans report `db.system.name: 'mongodb'` instead of `'mongoose'`. Mongoose is an ODM, not a database system. - The Redis and ioredis instrumentations no longer emit `db.connection_string`. The connection is described by `server.address` and `server.port` instead. #### GenAI attributes @@ -922,6 +923,7 @@ The following span names were adjusted: | `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) | | `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) | | `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) | +| `db` (mongoose) | `mongoose..` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither | `navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names. @@ -953,6 +955,8 @@ Only the Express, Koa and Hapi integrations resolve a route template for `router The Express, Fastify, Hapi and Elysia integrations resolve a route template for `handler` spans. NestJS has none when the span starts, so its request handler spans are named `Request handler`. The handler function name is no longer part of these span names. It stays on an attribute: `nestjs.callback` for NestJS, and `code.function.name` for Elysia, which now sets it on its handler spans. Elysia request handler spans also carry `http.route` now. Both attributes are set in both trace lifecycles. +A mongoose span's name is built from `db.collection.name`, so it holds the collection (`blogposts`) rather than the model (`BlogPost`). The related [`db.system.name` change](#messaging-and-database-attributes) from `mongoose` to `mongodb` applies in both trace lifecycles. + Messaging span names now read ` ` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`. AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/ send`). The full ARN remains on `messaging.destination.name`. diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts index 3e8488a569fe..09c08d4d8efc 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts @@ -56,7 +56,7 @@ Deno.test('mongoose instrumentation: orchestrion:mongoose:model_save channel pro const mongooseSpan = parent.spans?.find(s => s.op === 'db'); assertExists(mongooseSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); assertEquals(mongooseSpan!.description, 'mongoose.BlogPost.save'); - assertEquals(mongooseSpan!.data?.['db.system.name'], 'mongoose'); + assertEquals(mongooseSpan!.data?.['db.system.name'], 'mongodb'); assertEquals(mongooseSpan!.data?.['db.namespace'], 'mydb'); assertEquals(mongooseSpan!.data?.['db.collection.name'], 'blogposts'); assertEquals(mongooseSpan!.data?.['db.operation.name'], 'save'); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/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/mongoose-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts index cbb068cf7674..2a0cd427a3bb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-tracing-channel/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -39,6 +40,23 @@ describe('Mongoose tracing channel Test', () => { origin: 'auto.db.mongoose.diagnostic_channel', }); + const expectedStreamedSpan = (operation: string, extraAttributes: Record = {}) => + expect.objectContaining({ + name: `${operation} blogposts`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: 'blogposts' }, + 'db.namespace': { type: 'string', value: 'test' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.mongoose.diagnostic_channel' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + ...extraAttributes, + }), + }); + const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', spans: expect.arrayContaining([ @@ -62,6 +80,35 @@ describe('Mongoose tracing channel Test', () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); + test('names channel spans after the operation and collection 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'); + + expect(container.items).toContainEqual(expectedStreamedSpan('save')); + expect(container.items).toContainEqual( + expectedStreamedSpan('findOne', { 'db.query.text': { type: 'string', value: '{"title":"?"}' } }), + ); + expect(container.items).toContainEqual( + expectedStreamedSpan('aggregate', { + 'db.query.text': { type: 'string', value: '[{"$match":{"title":"?"}}]' }, + }), + ); + expect(container.items).toContainEqual( + expectedStreamedSpan('insertMany', { 'db.operation.batch.size': { type: 'integer', value: 2 } }), + ); + expect(container.items).toContainEqual( + expectedStreamedSpan('bulkWrite', { 'db.operation.batch.size': { type: 'integer', value: 2 } }), + ); + expect(container.items).toContainEqual(expectedStreamedSpan('find')); + }, + }) + .start() + .completed(); + }); + test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => { await createTestRunner() .expect({ @@ -153,6 +200,20 @@ describe('Mongoose tracing channel Test', () => { .start() .completed(); }); + + test('flags the streamed mongoose channel span as errored when the operation fails', async () => { + await createTestRunner() + .withEnv({ STREAMED: 'true' }) + .expect({ + span: (container: SerializedStreamedSpanContainer) => { + const aggregateSpan = container.items.find(item => item.name === 'aggregate blogposts'); + expect(aggregateSpan).toBeDefined(); + expect(aggregateSpan?.status).toBe('error'); + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongoose: '^9.7' } }, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-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/mongoose-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/test.ts index d99b38ff9e33..987f278f8d7e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v5/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -26,7 +27,7 @@ describe('Mongoose v5 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': operation, - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: `mongoose.BlogPost.${operation}`, op: 'db', @@ -44,6 +45,23 @@ describe('Mongoose v5 Test', () => { ]), }; + const expectedStreamedSpan = (operation: string) => + expect.objectContaining({ + name: `${operation} blogposts`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: 'blogposts' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: origin }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }), + }); + + const STREAMED_OPERATIONS = ['save', 'findOne', 'aggregate', 'insertMany', 'bulkWrite']; + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -52,6 +70,22 @@ describe('Mongoose v5 Test', () => { test('auto-instruments `mongoose` v5.', async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); + + test('auto-instruments `mongoose` v5 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'); + + for (const operation of STREAMED_OPERATIONS) { + expect(container.items).toContainEqual(expectedStreamedSpan(operation)); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongoose: '^5.9.7' } }, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-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/mongoose-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/test.ts index 426566a49825..97c253d412ac 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v7/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -24,7 +25,7 @@ describe('Mongoose v7 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': operation, - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: `mongoose.BlogPost.${operation}`, op: 'db', @@ -42,6 +43,23 @@ describe('Mongoose v7 Test', () => { ]), }; + const expectedStreamedSpan = (operation: string) => + expect.objectContaining({ + name: `${operation} blogposts`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: 'blogposts' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: origin }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }), + }); + + const STREAMED_OPERATIONS = ['save', 'findOne', 'aggregate', 'insertMany', 'bulkWrite']; + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -50,6 +68,22 @@ describe('Mongoose v7 Test', () => { test('auto-instruments `mongoose` v7.', async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); + + test('auto-instruments `mongoose` v7 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'); + + for (const operation of STREAMED_OPERATIONS) { + expect(container.items).toContainEqual(expectedStreamedSpan(operation)); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongoose: '^7' } }, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/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/mongoose-v8/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/test.ts index 50ce268bb8c4..4109d26a70f4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v8/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -27,7 +28,7 @@ describe('Mongoose v8 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': 'save', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.save', op: 'db', @@ -37,7 +38,7 @@ describe('Mongoose v8 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': 'updateOne', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.updateOne', op: 'db', @@ -47,7 +48,7 @@ describe('Mongoose v8 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': 'deleteOne', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.deleteOne', op: 'db', @@ -56,6 +57,21 @@ describe('Mongoose v8 Test', () => { ]), }; + const expectedStreamedSpan = (operation: string) => + expect.objectContaining({ + name: `${operation} blogposts`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: 'blogposts' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: origin }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }), + }); + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -64,6 +80,22 @@ describe('Mongoose v8 Test', () => { test('auto-instruments `mongoose` v8 document methods.', async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); + + test('auto-instruments `mongoose` v8 document methods 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'); + + for (const operation of ['save', 'updateOne', 'deleteOne']) { + expect(container.items).toContainEqual(expectedStreamedSpan(operation)); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongoose: '^8' } }, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/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/mongoose-v9/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/test.ts index 9a477adc8304..55a80b82e9b9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose-v9/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -26,7 +27,7 @@ describe('Mongoose v9 Test', () => { data: expect.objectContaining({ 'db.collection.name': 'blogposts', 'db.operation.name': operation, - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: `mongoose.BlogPost.${operation}`, op: 'db', @@ -47,6 +48,23 @@ describe('Mongoose v9 Test', () => { ]), }; + const expectedStreamedSpan = (operation: string) => + expect.objectContaining({ + name: `${operation} blogposts`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: 'blogposts' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: origin }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }), + }); + + const STREAMED_OPERATIONS = ['save', 'findOne', 'aggregate', 'insertMany', 'bulkWrite', 'updateOne', 'deleteOne']; + createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -55,6 +73,22 @@ describe('Mongoose v9 Test', () => { test('auto-instruments `mongoose` v9.', async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); + + test('auto-instruments `mongoose` v9 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'); + + for (const operation of STREAMED_OPERATIONS) { + expect(container.items).toContainEqual(expectedStreamedSpan(operation)); + } + }, + }) + .start() + .completed(); + }); }, { additionalDependencies: { mongoose: '>=9 <9.7' } }, ); diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mongoose/instrument.mjs index 170ad6f6a702..22bf57f14364 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose/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/mongoose/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts index db12a04ab22e..cb3012ede40a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -27,7 +28,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'save', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.save', op: 'db', @@ -38,7 +39,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'findOne', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.findOne', op: 'db', @@ -49,7 +50,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'aggregate', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.aggregate', op: 'db', @@ -60,7 +61,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'insertMany', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.insertMany', op: 'db', @@ -71,7 +72,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'bulkWrite', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.bulkWrite', op: 'db', @@ -83,7 +84,7 @@ describe('Mongoose experimental Test', () => { 'db.collection.name': 'blogposts', 'db.namespace': 'test', 'db.operation.name': 'remove', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.BlogPost.remove', op: 'db', @@ -93,7 +94,7 @@ describe('Mongoose experimental Test', () => { expect.objectContaining({ data: expect.objectContaining({ 'db.operation.name': 'save', - 'db.system.name': 'mongoose', + 'db.system.name': 'mongodb', }), description: 'mongoose.RequiredDoc.save', op: 'db', @@ -103,6 +104,23 @@ describe('Mongoose experimental Test', () => { ]), }; + const expectedStreamedSpan = (operation: string, collection = 'blogposts', status = 'ok') => + expect.objectContaining({ + name: `${operation} ${collection}`, + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status, + attributes: expect.objectContaining({ + 'db.collection.name': { type: 'string', value: collection }, + 'db.namespace': { type: 'string', value: 'test' }, + 'db.operation.name': { type: 'string', value: operation }, + 'db.system.name': { type: 'string', value: 'mongodb' }, + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: origin }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }), + }); + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('should auto-instrument `mongoose` package.', async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); @@ -126,6 +144,24 @@ describe('Mongoose experimental Test', () => { .completed(); }); + test('should auto-instrument `mongoose` 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'); + + for (const operation of ['save', 'findOne', 'aggregate', 'insertMany', 'bulkWrite', 'remove']) { + expect(container.items).toContainEqual(expectedStreamedSpan(operation)); + } + + expect(container.items).toContainEqual(expectedStreamedSpan('save', 'requireddocs', 'error')); + }, + }) + .start() + .completed(); + }); + test('parents a query to the span it was built in, not where it executes', async () => { await createTestRunner() .expect({ diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts index f2d2c3f24ffc..7e4230b84fbe 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts @@ -11,7 +11,14 @@ import { SERVER_PORT, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { isObjectLike, debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { + debug, + getClient, + hasSpanStreamingEnabled, + isObjectLike, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + startInactiveSpan, +} from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -117,8 +124,19 @@ function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName const queryText = redactMongoQuery(data.args?.pipeline ?? data.args?.filter); const batchSize = getBatchSize(data); + const client = getClient(); + const target = collection || data.database; + const name = + client && hasSpanStreamingEnabled(client) + ? target + ? `${data.operation} ${target}` + : DB_SYSTEM_NAME_VALUE_MONGODB + : collection + ? `mongoose.${collection}.${data.operation}` + : `mongoose.${data.operation}`; + return startInactiveSpan({ - name: collection ? `mongoose.${collection}.${data.operation}` : `mongoose.${data.operation}`, + name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts index f191dbb69d67..b279343435f8 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts @@ -11,7 +11,9 @@ import { } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; + +const DB_SYSTEM_NAME_VALUE_MONGODB = 'mongodb'; /** The subset of mongoose's `Collection` that the legacy span shape reads. */ export interface MongooseLegacyCollection { @@ -30,7 +32,8 @@ export interface StartMongooseLegacySpanOptions { /** * Start a mongoose client span reproducing the vendored * `@opentelemetry/instrumentation-mongoose` span shape, on the stable - * conventions. + * conventions. `db.system.name` deliberately deviates from what the OTel + * instrumentation emitted: mongoose is an ODM, the database system is mongodb. * * Shared by the vendored OTel/IITM instrumentation (`@sentry/node`) and the * orchestrion channel subscriber so the two emit an identical span shape, @@ -43,21 +46,33 @@ export function startMongooseLegacySpan({ origin, parentSpan, }: StartMongooseLegacySpanOptions): Span { + const collectionName = collection?.name; + const namespace = collection?.conn?.name; + const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [DB_COLLECTION_NAME]: collection?.name, - [DB_NAMESPACE]: collection?.conn?.name, + [DB_COLLECTION_NAME]: collectionName, + [DB_NAMESPACE]: namespace, [DB_USER]: collection?.conn?.user, [SERVER_ADDRESS]: collection?.conn?.host, [SERVER_PORT]: collection?.conn?.port, [DB_OPERATION_NAME]: operation, - [DB_SYSTEM_NAME]: 'mongoose', + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, }; + const client = getClient(); + const target = collectionName || namespace; + const name = + client && hasSpanStreamingEnabled(client) + ? target + ? `${operation} ${target}` + : DB_SYSTEM_NAME_VALUE_MONGODB + : `mongoose.${modelName}.${operation}`; + return startInactiveSpan({ - name: `mongoose.${modelName}.${operation}`, + name, // Set this explicitly, for platforms lacking `inferDbSpanData` attributes, parentSpan,