Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.<Model>.<operation>` (`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.

Expand Down Expand Up @@ -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 `<operation type> <destination>` 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/<uuid> send`). The full ARN remains on `messaging.destination.name`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,6 +40,23 @@ describe('Mongoose tracing channel Test', () => {
origin: 'auto.db.mongoose.diagnostic_channel',
});

const expectedStreamedSpan = (operation: string, extraAttributes: Record<string, unknown> = {}) =>
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([
Expand All @@ -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({
Expand Down Expand Up @@ -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' } },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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' } },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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' } },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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' } },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading