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 @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: '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,4 @@
import type { TransactionEvent } from '@sentry/core';
import { SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes';
import { afterAll, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

Expand All @@ -7,36 +7,35 @@ afterAll(() => {
});

test('bindScopeToEmitter preserves the active span for listeners firing in a different async context', async () => {
// Collect both transactions regardless of the order they are flushed in.
const transactions: Record<string, TransactionEvent> = {};
const collect = (event: TransactionEvent): void => {
transactions[event.transaction as string] = event;
};

await createRunner(__dirname, 'scenario.ts')
.expect({ transaction: collect })
.expect({ transaction: collect })
.expect({
span: container => {
const parent = container.items.find(span => span.name === 'parent');
const childUnbound = container.items.find(span => span.name === 'child-unbound');

expect(parent?.is_segment).toBe(true);
expect(childUnbound?.is_segment).toBe(true);

const parentTraceId = parent?.trace_id;
const parentSpanId = parent?.span_id;

// The bound emitter's listener ran inside the parent span context -> nested child span.
const childBound = container.items.find(span => span.name === 'child-bound');
expect(childBound?.is_segment).toBe(false);
expect(childBound?.parent_span_id).toBe(parentSpanId);
expect(childBound?.trace_id).toBe(parentTraceId);

// The unbound emitter's listener ran without the parent active -> its own root span,
// not nested under the parent span. It still shares the isolation scope's propagation context
// trace, matching the core SDK behavior for root spans.
expect(
container.items.filter(
span => !span.is_segment && span.attributes[SENTRY_SEGMENT_ID]?.value === childUnbound?.span_id,
),
).toEqual([]);
expect(childUnbound?.parent_span_id).toBeUndefined();
},
})
.start()
.completed();

const parent = transactions['parent'];
const childUnbound = transactions['child-unbound'];

expect(parent).toBeDefined();
expect(childUnbound).toBeDefined();

const parentTraceId = parent?.contexts?.trace?.trace_id;
const parentSpanId = parent?.contexts?.trace?.span_id;

// The bound emitter's listener ran inside the parent span context -> nested child span.
const childBound = parent?.spans?.find(span => span.description === 'child-bound');
expect(childBound).toBeDefined();
expect(childBound?.parent_span_id).toBe(parentSpanId);
expect(childBound?.trace_id).toBe(parentTraceId);

// The unbound emitter's listener ran without the parent active -> its own root transaction,
// not nested under the parent span. It still shares the isolation scope's propagation context
// trace, matching the core SDK behavior for root spans.
expect(childUnbound?.spans).toEqual([]);
expect(childUnbound?.contexts?.trace?.parent_span_id).toBeUndefined();
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests';
const tracesSampleRate = process.env.TRACES_SAMPLE_RATE;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
...(tracesSampleRate !== undefined ? { tracesSampleRate: Number(tracesSampleRate) } : {}),
integrations: [],
transport: loggingTransport,
// Stash the outgoing propagation data as observed inside the continueTrace callback onto every
// error event, so we can assert on it uniformly across all sampling configs (even when no
// transaction is emitted).
// span is emitted).
beforeSend(event) {
event.contexts = {
...event.contexts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const baggage = process.env.INCOMING_BAGGAGE || undefined;

Sentry.continueTrace({ sentryTrace, baggage }, () => {
Sentry.startSpan({ name: 'continued-root-span' }, () => {
// Captured while the root span is active. The error is emitted before the span ends, so the
// error envelope always precedes the transaction envelope (ordered assertions rely on this).
// Capture while the root span is active so the error carries its trace context.
Sentry.captureException(new Error('continued-trace-error'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const CONFIGS: { name: string; rate?: string }[] = [
* - incoming parentSampled true/false -> overrides local rate.
* - deferred/none -> local tracesSampleRate decides.
*/
function expectsTransaction(rate: string | undefined, variant: Variant): boolean {
function expectsSpan(rate: string | undefined, variant: Variant): boolean {
if (rate === undefined) return false; // TwP: span recording disabled
if (variant.parentSampled === true) return true; // positive parent decision wins
if (variant.parentSampled === false) return false; // negative parent decision wins
Expand All @@ -87,7 +87,7 @@ describe('continueTrace', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
describe.each(CONFIGS)('$name', config => {
test.each(VARIANTS)('continues the $key', async variant => {
const wantsTransaction = expectsTransaction(config.rate, variant);
const wantsSpan = expectsSpan(config.rate, variant);

const runner = createRunner().withEnv({
TRACES_SAMPLE_RATE: config.rate,
Expand All @@ -96,10 +96,10 @@ describe('continueTrace', () => {
});

let observedErrorTraceId: string | undefined;
let observedTxTraceId: string | undefined;
let observedSpanTraceId: string | undefined;

// The error event is delayed by async enrichment (context lines, local variables) while the
// transaction flushes synchronously on span end, so the two envelopes can arrive in either
// span buffer flushes after span end, so the two envelopes can arrive in either
// order. Match them by type rather than by position.
runner.unordered();

Expand All @@ -124,30 +124,30 @@ describe('continueTrace', () => {
},
});

if (wantsTransaction) {
if (wantsSpan) {
runner.expect({
transaction: transaction => {
const trace = transaction.contexts?.trace;
observedTxTraceId = trace?.trace_id;
span: container => {
const segment = container.items.find(span => span.is_segment);
observedSpanTraceId = segment?.trace_id;

if (variant.traceId) {
expect(trace?.trace_id).toBe(variant.traceId);
expect(segment?.trace_id).toBe(variant.traceId);
} else {
expect(trace?.trace_id).toMatch(/^[a-f0-9]{32}$/);
expect(segment?.trace_id).toMatch(/^[a-f0-9]{32}$/);
}
if (variant.parentSpanId) {
expect(trace?.parent_span_id).toBe(variant.parentSpanId);
expect(segment?.parent_span_id).toBe(variant.parentSpanId);
}
expect(transaction.transaction).toBe('continued-root-span');
expect(segment?.name).toBe('continued-root-span');
},
});
}

await runner.start().completed();

if (wantsTransaction) {
// Error and transaction share the (continued or freshly generated) trace id.
expect(observedTxTraceId).toBe(observedErrorTraceId);
if (wantsSpan) {
// Error and span share the (continued or freshly generated) trace id.
expect(observedSpanTraceId).toBe(observedErrorTraceId);
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests';
const tracesSampleRate = process.env.TRACES_SAMPLE_RATE;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
...(tracesSampleRate !== undefined ? { tracesSampleRate: Number(tracesSampleRate) } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ describe('startNewTrace', () => {
test.each(CONFIGS)('starts a fresh trace shared by every root span in the callback [%s]', async (_name, rate) => {
await createRunner()
.withEnv({ TRACES_SAMPLE_RATE: rate })
// Transactions (if any, in rate=1) are irrelevant here and their ordering vs. the error is
// Spans (if any, in rate=1) are irrelevant here and their ordering vs. the error is
// not deterministic, so we ignore them and rely entirely on the stashed error context.
.ignore('transaction')
.ignore('span')
.expect({
event: event => {
const trace = event.contexts?.trace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node');
const { loggingTransport } = require('@sentry-internal/node-integration-tests');

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
transport: loggingTransport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const app = express();

app.get('/test', async (_req, res) => {
Sentry.captureException(new Error('test error'));
// calling Sentry.flush() here to ensure that the order in which we send transaction and errors
// is guaranteed to be 1. error, 2. transaction (repeated 3x in test)
// calling Sentry.flush() here to ensure that the order in which we send root spans and errors
// is guaranteed to be 1. error, 2. root span (repeated 3x in test)
await Sentry.flush();
res.json({ success: true });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ afterAll(() => {
cleanupChildProcesses();
});

test('errors and transactions get a unique traceId per request, when tracing is enabled', async () => {
test('errors and spans get a unique traceId per request, when tracing is enabled', async () => {
const eventTraceIds: string[] = [];
const transactionTraceIds: string[] = [];

const runner = createRunner(__dirname, 'server.js')
.withFlags('--import', join(__dirname, 'instrument.cjs'))
Expand All @@ -17,43 +16,42 @@ test('errors and transactions get a unique traceId per request, when tracing is
eventTraceIds.push(event.contexts?.trace?.trace_id || '');
},
})
.expect({
transaction: transaction => {
transactionTraceIds.push(transaction.spans?.[0]?.trace_id || '');
},
})
.expect({
event: event => {
eventTraceIds.push(event.contexts?.trace?.trace_id || '');
},
})
.expect({
transaction: transaction => {
transactionTraceIds.push(transaction.spans?.[0]?.trace_id || '');
},
})
.expect({
event: event => {
eventTraceIds.push(event.contexts?.trace?.trace_id || '');
},
})
.expect({
transaction: transaction => {
transactionTraceIds.push(transaction.spans?.[0]?.trace_id || '');
},
})
.start();
});

const seenSegmentIds = new Set<string>();
const spansPromises = Array.from({ length: 3 }, () =>
runner.collectStreamedSpansUntilSegment(segment => {
if (seenSegmentIds.has(segment.span_id)) return false;
seenSegmentIds.add(segment.span_id);
return true;
}),
);

const started = runner.start();

await started.makeRequest('get', '/test');
await started.makeRequest('get', '/test');
await started.makeRequest('get', '/test');

await runner.makeRequest('get', '/test');
await runner.makeRequest('get', '/test');
await runner.makeRequest('get', '/test');
await started.completed();

await runner.completed();
const spanTraceIds = (await Promise.all(spansPromises)).map(
spans => spans.find(span => !span.is_segment)?.trace_id || '',
);

expect(new Set(transactionTraceIds).size).toBe(3);
for (const traceId of transactionTraceIds) {
expect(new Set(spanTraceIds).size).toBe(3);
for (const traceId of spanTraceIds) {
expect(traceId).toMatch(/^[a-f\d]{32}$/);
}

expect(eventTraceIds.sort()).toEqual(transactionTraceIds.sort());
expect(eventTraceIds.sort()).toEqual(spanTraceIds.sort());
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
transport: loggingTransport,
Expand Down
Loading