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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import {
DB_COLLECTION_NAME,
DB_NAMESPACE,
DB_OPERATION_BATCH_SIZE,
DB_OPERATION_NAME,
DB_QUERY_TEXT,
DB_SYSTEM_NAME,
SENTRY_OP,
SENTRY_ORIGIN,
SENTRY_TRACE_LIFECYCLE,
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';
Expand All @@ -24,83 +37,50 @@ describe('Mongoose tracing channel Test', () => {
cleanupChildProcesses();
});

const expectedSpan = (operation: string, extraData: Record<string, unknown> = {}) =>
expect.objectContaining({
data: expect.objectContaining({
'db.system.name': 'mongodb',
'db.namespace': 'test',
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'server.address': expect.any(String),
'server.port': expect.any(Number),
...extraData,
}),
description: `mongoose.blogposts.${operation}`,
op: 'db',
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' },
[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' },
[SERVER_ADDRESS]: { type: 'string', value: expect.any(String) },
[SERVER_PORT]: { type: 'integer', value: expect.any(Number) },
...extraAttributes,
}),
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
// filter values are redacted out of `db.query.text`
expectedSpan('findOne', { 'db.query.text': '{"title":"?"}' }),
expectedSpan('aggregate', { 'db.query.text': '[{"$match":{"title":"?"}}]' }),
expectedSpan('insertMany', { 'db.operation.batch.size': 2 }),
expectedSpan('bulkWrite', { 'db.operation.batch.size': 2 }),
// a cursor iteration emits a span per `.next()` via the `mongoose:cursor:next` channel
expectedSpan('find'),
]),
};

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('subscribes to mongoose >= 9.7 diagnostics channels with stable semconv attributes', async () => {
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) => {
span: container => {
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":"?"}' } }),
expectedStreamedSpan('findOne', { [DB_QUERY_TEXT]: { type: 'string', value: '{"title":"?"}' } }),
);
expect(container.items).toContainEqual(
expectedStreamedSpan('aggregate', {
'db.query.text': { type: 'string', value: '[{"$match":{"title":"?"}}]' },
[DB_QUERY_TEXT]: { type: 'string', value: '[{"$match":{"title":"?"}}]' },
}),
);
expect(container.items).toContainEqual(
expectedStreamedSpan('insertMany', { 'db.operation.batch.size': { type: 'integer', value: 2 } }),
expectedStreamedSpan('insertMany', { [DB_OPERATION_BATCH_SIZE]: { type: 'integer', value: 2 } }),
);
expect(container.items).toContainEqual(
expectedStreamedSpan('bulkWrite', { 'db.operation.batch.size': { type: 'integer', value: 2 } }),
expectedStreamedSpan('bulkWrite', { [DB_OPERATION_BATCH_SIZE]: { type: 'integer', value: 2 } }),
);
expect(container.items).toContainEqual(expectedStreamedSpan('find'));
},
Expand All @@ -112,12 +92,14 @@ describe('Mongoose tracing channel Test', () => {
test('does not double-instrument: the legacy IITM mongoose patcher does not fire on 9.7', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
span: container => {
const spans = container.items;
// The monkey-patch path (origin `auto.db.mongoose`) must be inactive on 9.7+.
expect(spans.find(span => span.origin === 'auto.db.mongoose')).toBeUndefined();
expect(spans.find(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose')).toBeUndefined();
// ...while the diagnostics-channel path is active.
expect(spans.find(span => span.origin === 'auto.db.mongoose.diagnostic_channel')).toBeDefined();
expect(
spans.find(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose.diagnostic_channel'),
).toBeDefined();
},
})
.start()
Expand All @@ -127,10 +109,10 @@ describe('Mongoose tracing channel Test', () => {
test('never leaks raw filter values into db.query.text', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
span: container => {
const spans = container.items;
for (const span of spans) {
const queryText = span.data?.['db.query.text'];
const queryText = span.attributes[DB_QUERY_TEXT]?.value;
if (typeof queryText === 'string') {
expect(queryText).not.toContain('Test');
}
Expand All @@ -144,14 +126,20 @@ describe('Mongoose tracing channel Test', () => {
test('nests the mongodb driver span under the mongoose channel span', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const mongooseSave = spans.find(span => span.description === 'mongoose.blogposts.save');
span: container => {
const spans = container.items;
const mongooseSave = spans.find(
span =>
span.name === 'save blogposts' &&
span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose.diagnostic_channel',
);
expect(mongooseSave).toBeDefined();
// the underlying mongodb driver span must parent to the mongoose channel span,
// proving the channel span is the active async context for the traced operation
const driverChild = spans.find(
span => span.parent_span_id === mongooseSave?.span_id && span.origin === driverOrigin,
span =>
span.parent_span_id === mongooseSave?.span_id &&
span.attributes[SENTRY_ORIGIN]?.value === driverOrigin,
);
expect(driverChild).toBeDefined();
},
Expand All @@ -163,14 +151,18 @@ describe('Mongoose tracing channel Test', () => {
test('omits db.query.text for the empty-filter cursor and does not treat the cursor batchSize as a batch', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
span: container => {
const spans = container.items;
// the `.find().cursor()` iteration runs with no filter, so there is no query text to emit
const cursorFind = spans.find(span => span.description === 'mongoose.blogposts.find');
const cursorFind = spans.find(
span =>
span.name === 'find blogposts' &&
span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose.diagnostic_channel',
);
expect(cursorFind).toBeDefined();
expect(cursorFind?.data?.['db.query.text']).toBeUndefined();
expect(cursorFind?.attributes[DB_QUERY_TEXT]).toBeUndefined();
// a cursor's `batchSize` is a fetch-tuning option, not a batch-operation size
expect(cursorFind?.data?.['db.operation.batch.size']).toBeUndefined();
expect(cursorFind?.attributes[DB_OPERATION_BATCH_SIZE]).toBeUndefined();
},
})
.start()
Expand All @@ -180,8 +172,6 @@ describe('Mongoose tracing channel Test', () => {
{ additionalDependencies: { mongoose: '^9.7' } },
);

// A failed operation must flag the mongoose channel span as errored. mongodb error statuses map to
// `internal_error` through the OTel pipeline (same as the postgres/redis suites).
createEsmAndCjsTests(
__dirname,
'scenario-error.mjs',
Expand All @@ -190,23 +180,12 @@ describe('Mongoose tracing channel Test', () => {
test('flags the mongoose channel span as errored when the operation fails', async () => {
await createTestRunner()
.expect({
transaction: event => {
const spans = event.spans || [];
const aggregateSpan = spans.find(span => span.description === 'mongoose.blogposts.aggregate');
expect(aggregateSpan).toBeDefined();
expect(aggregateSpan?.status).toBe('internal_error');
},
})
.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');
span: container => {
const aggregateSpan = container.items.find(
item =>
item.name === 'aggregate blogposts' &&
item.attributes[SENTRY_ORIGIN]?.value === 'auto.db.mongoose.diagnostic_channel',
);
expect(aggregateSpan).toBeDefined();
expect(aggregateSpan?.status).toBe('error');
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import {
DB_COLLECTION_NAME,
DB_OPERATION_NAME,
DB_SYSTEM_NAME,
SENTRY_OP,
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';
Expand All @@ -22,41 +30,18 @@ describe('Mongoose v5 Test', () => {
cleanupChildProcesses();
});

const expectedSpan = (operation: string) =>
expect.objectContaining({
data: expect.objectContaining({
'db.collection.name': 'blogposts',
'db.operation.name': operation,
'db.system.name': 'mongodb',
}),
description: `mongoose.BlogPost.${operation}`,
op: 'db',
origin,
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expectedSpan('save'),
expectedSpan('findOne'),
expectedSpan('aggregate'),
expectedSpan('insertMany'),
expectedSpan('bulkWrite'),
]),
};

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' },
[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' },
}),
});

Expand All @@ -68,14 +53,9 @@ describe('Mongoose v5 Test', () => {
'instrument.mjs',
(createTestRunner, 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) => {
span: container => {
expect(container.items.find(item => item.is_segment)?.name).toBe('Test Transaction');

for (const operation of STREAMED_OPERATIONS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading