Skip to content
Draft
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 @@ -8,14 +8,15 @@ Sentry.pinoIntegration.untrackLogger(ignoredLogger);

ignoredLogger.info('this will not be tracked');

Sentry.withIsolationScope(() => {
// Each operation runs in its own trace, so logs emitted within them carry distinct trace ids.
Sentry.startNewTrace(() => {
Sentry.startSpan({ name: 'startup' }, () => {
logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world');
});
});

setTimeout(() => {
Sentry.withIsolationScope(() => {
Sentry.startNewTrace(() => {
Sentry.startSpan({ name: 'later' }, () => {
const child = logger.child({ module: 'authentication' });
child.error(new Error('oh no'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ test('bindScopeToEmitter preserves the active span for listeners firing in a dif
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, separate trace.
// 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?.trace_id).not.toBe(parentTraceId);
expect(childUnbound?.contexts?.trace?.parent_span_id).toBeUndefined();
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@ afterAll(() => {
});

test('sends manually started streamed parallel root spans in root context', async () => {
expect.assertions(7);
expect.assertions(6);

await createRunner(__dirname, 'scenario.ts')
.expect({ span: { items: [{ name: 'test_span_1' }] } })
.expect({
span: spanContainer => {
expect(spanContainer).toBeDefined();
const traceId = spanContainer.items[0]!.trace_id;
expect(traceId).toMatch(/^[0-9a-f]{32}$/);

// It ignores propagation context of the root context
expect(traceId).not.toBe('12345678901234567890123456789012');
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
expect(span1).toBeDefined();
expect(span2).toBeDefined();

// Different trace ID than the first span
const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
// Both root spans continue the scope's propagation context, matching the core SDK behavior.
expect(span1!.trace_id).toBe('12345678901234567890123456789012');
expect(span1!.parent_span_id).toBe('1234567890123456');

expect(trace1Id).not.toBe(traceId);
// Same trace ID for both spans
expect(span2!.trace_id).toBe(span1!.trace_id);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@ afterAll(() => {
});

test('should send manually started parallel root spans in root context', async () => {
expect.assertions(7);
expect.assertions(6);

await createRunner(__dirname, 'scenario.ts')
.expect({ transaction: { transaction: 'test_span_1' } })
.expect({
transaction: transaction => {
expect(transaction).toBeDefined();
const traceId = transaction.contexts?.trace?.trace_id;
expect(traceId).toBeDefined();

// It ignores propagation context of the root context
expect(traceId).not.toBe('12345678901234567890123456789012');
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
// Both root spans continue the scope's propagation context, matching the core SDK behavior.
expect(traceId).toBe('12345678901234567890123456789012');
expect(transaction.contexts?.trace?.parent_span_id).toBe('1234567890123456');

// Different trace ID than the first span
// Same trace ID as the first span
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
expect(trace1Id).toBeDefined();
expect(trace1Id).not.toBe(traceId);
expect(trace1Id).toBe('12345678901234567890123456789012');
expect(trace1Id).toBe(traceId);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ test('sends manually started streamed parallel root spans outside of root contex
expect.assertions(6);

await createRunner(__dirname, 'scenario.ts')
.expect({ span: { items: [{ name: 'test_span_1' }] } })
.expect({
span: spanContainer => {
expect(spanContainer).toBeDefined();
const traceId = spanContainer.items[0]!.trace_id;
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();

const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
expect(span1).toBeDefined();
expect(span2).toBeDefined();

// Different trace ID as the first span
expect(trace1Id).not.toBe(traceId);
expect(span1!.trace_id).toMatch(/^[0-9a-f]{32}$/);
expect(span1!.parent_span_id).toBeUndefined();

// Same trace ID for both spans - both root spans share the scope's propagation context
expect(span2!.trace_id).toBe(span1!.trace_id);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ test('sends manually started streamed parallel root spans outside of root contex
expect.assertions(6);

await createRunner(__dirname, 'scenario.ts')
.expect({ span: { items: [{ name: 'test_span_1' }] } })
.expect({
span: spanContainer => {
expect(spanContainer).toBeDefined();
const traceId = spanContainer.items[0]!.trace_id;
expect(traceId).toMatch(/^[0-9a-f]{32}$/);
expect(spanContainer.items[0]!.parent_span_id).toBeUndefined();

const trace1Id = spanContainer.items[0]!.attributes.spanIdTraceId?.value;
expect(trace1Id).toMatch(/^[0-9a-f]{32}$/);
const span1 = spanContainer.items.find(item => item.name === 'test_span_1');
const span2 = spanContainer.items.find(item => item.name === 'test_span_2');
expect(span1).toBeDefined();
expect(span2).toBeDefined();

// Different trace ID as the first span
expect(trace1Id).not.toBe(traceId);
// Both root spans continue the scope's propagation context, including the parentSpanId,
// matching the core SDK behavior.
expect(span1!.trace_id).toBe('12345678901234567890123456789012');
expect(span1!.parent_span_id).toBe('1234567890123456');

// Same trace ID for both spans
expect(span2!.trace_id).toBe(span1!.trace_id);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ test('should send manually started parallel root spans outside of root context w
transaction: transaction => {
expect(transaction).toBeDefined();
const traceId = transaction.contexts?.trace?.trace_id;
expect(traceId).toBeDefined();
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();

const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
expect(trace1Id).toBeDefined();
// Both root spans continue the scope's propagation context, including the parentSpanId,
// matching the core SDK behavior.
expect(traceId).toBe('12345678901234567890123456789012');
expect(transaction.contexts?.trace?.parent_span_id).toBe('1234567890123456');

// Different trace ID as the first span
expect(trace1Id).not.toBe(traceId);
// Same trace ID as the first span
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
expect(trace1Id).toBe(traceId);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ test('should send manually started parallel root spans outside of root context',
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
expect(trace1Id).toBeDefined();

// Different trace ID as the first span
expect(trace1Id).not.toBe(traceId);
// Same trace ID as the first span - both root spans share the scope's propagation context
expect(trace1Id).toBe(traceId);
},
})
.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ describe('outgoing fetch', () => {
const [SERVER_URL, closeTestServer] = await createTestServer()
.get('/api/v0', headers => {
expect(headers['baggage']).toEqual(expect.any(String));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
})
.get('/api/v1', headers => {
expect(headers['baggage']).toEqual(expect.any(String));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
})
.get('/api/v2', headers => {
expect(headers['baggage']).toBeUndefined();
Expand Down
17 changes: 9 additions & 8 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ export const continueTrace = <V>(
return withScope(scope => {
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);
scope.setPropagationContext(propagationContext);
_setSpanForScope(scope, undefined);
return callback();
return withActiveSpan(null, callback);
});
};

Expand Down Expand Up @@ -330,13 +329,15 @@ export function startNewTrace<T>(callback: () => T): T {
return acs.startNewTrace(callback);
}

return withScope(scope => {
scope.setPropagationContext({
traceId: generateTraceId(),
sampleRand: safeMathRandom(),
return withActiveSpan(null, () => {
return withScope(scope => {
scope.setPropagationContext({
traceId: generateTraceId(),
sampleRand: safeMathRandom(),
});
DEBUG_BUILD && debug.log(`Starting a new trace with id ${scope.getPropagationContext().traceId}`);
return callback();
});
DEBUG_BUILD && debug.log(`Starting a new trace with id ${scope.getPropagationContext().traceId}`);
return withActiveSpan(null, callback);
});
}

Expand Down
19 changes: 2 additions & 17 deletions packages/node/src/sdk/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as os from 'node:os';
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core';
import type { ServerRuntimeClientOptions } from '@sentry/core';
import {
_INTERNAL_clearAiProviderSkips,
_INTERNAL_flushLogsBuffer,
Expand All @@ -11,11 +11,7 @@ import {
SDK_VERSION,
ServerRuntimeClient,
} from '@sentry/core';
import {
type AsyncLocalStorageLookup,
getTraceContextForScope,
type SentryTracerProvider,
} from '@sentry/opentelemetry';
import { type AsyncLocalStorageLookup, type SentryTracerProvider } from '@sentry/opentelemetry';
import { isMainThread, threadId } from 'worker_threads';
import { DEBUG_BUILD } from '../debug-build';
import type { NodeClientOptions } from '../types';
Expand Down Expand Up @@ -170,15 +166,4 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
_INTERNAL_clearAiProviderSkips();
super._setupIntegrations();
}

/** Custom implementation for OTEL, so we can handle scope-span linking. */
protected _getTraceInfoFromScope(
scope: Scope | undefined,
): [dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined, traceContext: TraceContext | undefined] {
if (!scope) {
return [undefined, undefined];
}

return getTraceContextForScope(this, scope);
}
}
1 change: 0 additions & 1 deletion packages/opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ function setupSentry() {

// Initialize the provider
trace.setGlobalTracerProvider(provider);
propagation.setGlobalPropagator(new SentryPropagator());
context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager());

setOpenTelemetryContextAsyncContextStrategy();
Expand Down
8 changes: 2 additions & 6 deletions packages/opentelemetry/src/asyncContextStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ import {
SENTRY_FORK_SET_SCOPE_CONTEXT_KEY,
SENTRY_TRACE_STATE_CHILD_IGNORED,
} from './constants';
import { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual, withActiveSpan } from './trace';
import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace';
import type { CurrentScopes } from './types';
import { getContextFromScope, getScopesFromContext } from './utils/contextData';
import { getActiveSpan } from './utils/getActiveSpan';
import { getTraceData } from './utils/getTraceData';
import { AsyncLocalStorage } from 'node:async_hooks';
import type { AsyncLocalStorageLookup } from './asyncLocalStorageContextManager';
import { SentryAsyncLocalStorageContextManager } from './asyncLocalStorageContextManager';
import { AsyncLocalStorage } from 'node:async_hooks';

/**
* Sets the async context strategy to use follow the OTEL context under the hood.
Expand Down Expand Up @@ -119,9 +118,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(): AsyncLocalStorage
startSpanManual,
startInactiveSpan,
getActiveSpan,
getTraceData,
continueTrace,
startNewTrace,
Comment thread
cursor[bot] marked this conversation as resolved.
// The types here don't fully align, because our own `Span` type is narrower
// than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around
withActiveSpan: withActiveSpan,
Expand Down
1 change: 0 additions & 1 deletion packages/opentelemetry/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const SENTRY_BAGGAGE_HEADER = 'baggage';

export const SENTRY_TRACE_STATE_DSC = 'sentry.dsc';
export const SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING = 'sentry.sampled_not_recording';
export const SENTRY_TRACE_STATE_URL = 'sentry.url';

/**
* A flag marking a context as ignored because the span associated with the context
Expand Down
2 changes: 0 additions & 2 deletions packages/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export { getScopesFromContext } from './utils/contextData';

export { getTraceContextForScope } from './trace';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API export removed

Medium Severity

This violates the Breaking Changes review rule: publicly exported getTraceContextForScope was removed without a deprecation notice, and NodeClient’s related override was dropped in the same change.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 6535b4e. Configure here.


export { setupEventContextTrace } from './setupEventContextTrace';

export { SentryPropagator } from './propagator';
Expand Down
Loading
Loading