Skip to content

Commit 330750d

Browse files
authored
feat(core): Add startNewTrace API (#12138)
Add a new `Sentry.startNewTrace` function that allows users to start a trace in isolation of a potentially still active trace. When this function is called, a new trace will be started on a forked scope which remains valid throughout the callback lifetime.
1 parent 5a1fae6 commit 330750d

File tree

24 files changed

+227
-18
lines changed

24 files changed

+227
-18
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const newTraceBtn = document.getElementById('newTrace');
2+
newTraceBtn.addEventListener('click', async () => {
3+
Sentry.startNewTrace(() => {
4+
Sentry.startSpan({ op: 'ui.interaction.click', name: 'new-trace' }, async () => {
5+
await fetch('http://example.com');
6+
});
7+
});
8+
});
9+
10+
const oldTraceBtn = document.getElementById('oldTrace');
11+
oldTraceBtn.addEventListener('click', async () => {
12+
Sentry.startSpan({ op: 'ui.interaction.click', name: 'old-trace' }, async () => {
13+
await fetch('http://example.com');
14+
});
15+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="oldTrace">Old Trace</button>
8+
<button id="newTrace">new Trace</button>
9+
</body>
10+
</html>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import type { EventAndTraceHeader } from '../../../../utils/helpers';
4+
import {
5+
eventAndTraceHeaderRequestParser,
6+
getFirstSentryEnvelopeRequest,
7+
getMultipleSentryEnvelopeRequests,
8+
shouldSkipTracingTest,
9+
} from '../../../../utils/helpers';
10+
11+
sentryTest(
12+
'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback',
13+
async ({ getLocalTestUrl, page }) => {
14+
if (shouldSkipTracingTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
const url = await getLocalTestUrl({ testDir: __dirname });
19+
20+
await page.route('http://example.com/**', route => {
21+
return route.fulfill({
22+
status: 200,
23+
contentType: 'application/json',
24+
body: JSON.stringify({}),
25+
});
26+
});
27+
28+
const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
29+
page,
30+
url,
31+
eventAndTraceHeaderRequestParser,
32+
);
33+
34+
const pageloadTraceContext = pageloadEvent.contexts?.trace;
35+
36+
expect(pageloadEvent.type).toEqual('transaction');
37+
38+
expect(pageloadTraceContext).toMatchObject({
39+
op: 'pageload',
40+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
41+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
42+
});
43+
expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
44+
45+
expect(pageloadTraceHeaders).toEqual({
46+
environment: 'production',
47+
public_key: 'public',
48+
sample_rate: '1',
49+
sampled: 'true',
50+
trace_id: pageloadTraceContext?.trace_id,
51+
});
52+
53+
const transactionPromises = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
54+
page,
55+
2,
56+
{ envelopeType: 'transaction' },
57+
eventAndTraceHeaderRequestParser,
58+
);
59+
60+
await page.locator('#newTrace').click();
61+
await page.locator('#oldTrace').click();
62+
63+
const [txnEvent1, txnEvent2] = await transactionPromises;
64+
65+
const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] =
66+
txnEvent1[0].transaction === 'new-trace' ? txnEvent1 : txnEvent2;
67+
const [oldTraceTransactionEvent, oldTraceTransactionTraceHeaders] =
68+
txnEvent1[0].transaction === 'old-trace' ? txnEvent1 : txnEvent2;
69+
70+
const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace;
71+
expect(newTraceTransactionTraceContext).toMatchObject({
72+
op: 'ui.interaction.click',
73+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
74+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
75+
});
76+
77+
expect(newTraceTransactionTraceHeaders).toEqual({
78+
environment: 'production',
79+
public_key: 'public',
80+
sample_rate: '1',
81+
sampled: 'true',
82+
trace_id: newTraceTransactionTraceContext?.trace_id,
83+
transaction: 'new-trace',
84+
});
85+
86+
const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace;
87+
expect(oldTraceTransactionEventTraceContext).toMatchObject({
88+
op: 'ui.interaction.click',
89+
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
90+
span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
91+
});
92+
93+
expect(oldTraceTransactionTraceHeaders).toEqual({
94+
environment: 'production',
95+
public_key: 'public',
96+
sample_rate: '1',
97+
sampled: 'true',
98+
trace_id: oldTraceTransactionTraceHeaders?.trace_id,
99+
// transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction
100+
// which does not have a `transaction` field because its source is URL.
101+
});
102+
103+
expect(oldTraceTransactionEventTraceContext?.trace_id).toEqual(pageloadTraceContext?.trace_id);
104+
expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
105+
},
106+
);

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export {
6464
startSpan,
6565
startInactiveSpan,
6666
startSpanManual,
67+
startNewTrace,
6768
withActiveSpan,
6869
getSpanDescendants,
6970
continueTrace,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export {
6262
startSpan,
6363
startInactiveSpan,
6464
startSpanManual,
65+
startNewTrace,
6566
withActiveSpan,
6667
getRootSpan,
6768
getSpanDescendants,

packages/browser/src/index.bundle.tracing.replay.feedback.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
startSpan,
1111
startInactiveSpan,
1212
startSpanManual,
13+
startNewTrace,
1314
withActiveSpan,
1415
getSpanDescendants,
1516
setMeasurement,

packages/browser/src/index.bundle.tracing.replay.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
startSpan,
1111
startInactiveSpan,
1212
startSpanManual,
13+
startNewTrace,
1314
withActiveSpan,
1415
getSpanDescendants,
1516
setMeasurement,

packages/browser/src/index.bundle.tracing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
startSpan,
1212
startInactiveSpan,
1313
startSpanManual,
14+
startNewTrace,
1415
withActiveSpan,
1516
getSpanDescendants,
1617
setMeasurement,

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export {
5959
startInactiveSpan,
6060
startSpanManual,
6161
withActiveSpan,
62+
startNewTrace,
6263
getSpanDescendants,
6364
setMeasurement,
6465
getSpanStatusFromHttpCode,

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ import type { Client, IntegrationFn, StartSpanOptions, TransactionSource } from
2727
import type { Span } from '@sentry/types';
2828
import {
2929
browserPerformanceTimeOrigin,
30+
generatePropagationContext,
3031
getDomElement,
3132
logger,
3233
propagationContextFromHeaders,
33-
uuid4,
3434
} from '@sentry/utils';
3535

3636
import { DEBUG_BUILD } from '../debug-build';
@@ -412,8 +412,8 @@ export function startBrowserTracingPageLoadSpan(
412412
* This will only do something if a browser tracing integration has been setup.
413413
*/
414414
export function startBrowserTracingNavigationSpan(client: Client, spanOptions: StartSpanOptions): Span | undefined {
415-
getCurrentScope().setPropagationContext(generatePropagationContext());
416415
getIsolationScope().setPropagationContext(generatePropagationContext());
416+
getCurrentScope().setPropagationContext(generatePropagationContext());
417417

418418
client.emit('startNavigationSpan', spanOptions);
419419

@@ -487,10 +487,3 @@ function registerInteractionListener(
487487
addEventListener('click', registerInteractionTransaction, { once: false, capture: true });
488488
}
489489
}
490-
491-
function generatePropagationContext(): { traceId: string; spanId: string } {
492-
return {
493-
traceId: uuid4(),
494-
spanId: uuid4().substring(16),
495-
};
496-
}

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export {
8282
startSpan,
8383
startInactiveSpan,
8484
startSpanManual,
85+
startNewTrace,
8586
withActiveSpan,
8687
getRootSpan,
8788
getSpanDescendants,

packages/core/src/scope.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {
2121
SeverityLevel,
2222
User,
2323
} from '@sentry/types';
24-
import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils';
24+
import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils';
2525

2626
import { updateSession } from './session';
2727
import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope';
@@ -600,10 +600,3 @@ export const Scope = ScopeClass;
600600
* Holds additional event information.
601601
*/
602602
export type Scope = ScopeInterface;
603-
604-
function generatePropagationContext(): PropagationContext {
605-
return {
606-
traceId: uuid4(),
607-
spanId: uuid4().substring(16),
608-
};
609-
}

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export {
1717
continueTrace,
1818
withActiveSpan,
1919
suppressTracing,
20+
startNewTrace,
2021
} from './trace';
2122
export {
2223
getDynamicSamplingContextFromClient,

packages/core/src/tracing/trace.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types';
2-
import { propagationContextFromHeaders } from '@sentry/utils';
2+
import { generatePropagationContext, logger, propagationContextFromHeaders } from '@sentry/utils';
33
import type { AsyncContextStrategy } from '../asyncContext/types';
44
import { getMainCarrier } from '../carrier';
55

66
import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes';
77

88
import { getAsyncContextStrategy } from '../asyncContext';
9+
import { DEBUG_BUILD } from '../debug-build';
910
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
1011
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
1112
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
@@ -212,6 +213,30 @@ export function suppressTracing<T>(callback: () => T): T {
212213
});
213214
}
214215

216+
/**
217+
* Starts a new trace for the duration of the provided callback. Spans started within the
218+
* callback will be part of the new trace instead of a potentially previously started trace.
219+
*
220+
* Important: Only use this function if you want to override the default trace lifetime and
221+
* propagation mechanism of the SDK for the duration and scope of the provided callback.
222+
* The newly created trace will also be the root of a new distributed trace, for example if
223+
* you make http requests within the callback.
224+
* This function might be useful if the operation you want to instrument should not be part
225+
* of a potentially ongoing trace.
226+
*
227+
* Default behavior:
228+
* - Server-side: A new trace is started for each incoming request.
229+
* - Browser: A new trace is started for each page our route. Navigating to a new route
230+
* or page will automatically create a new trace.
231+
*/
232+
export function startNewTrace<T>(callback: () => T): T {
233+
return withScope(scope => {
234+
scope.setPropagationContext(generatePropagationContext());
235+
DEBUG_BUILD && logger.info(`Starting a new trace with id ${scope.getPropagationContext().traceId}`);
236+
return withActiveSpan(null, callback);
237+
});
238+
}
239+
215240
function createChildOrRootSpan({
216241
parentSpan,
217242
spanContext,

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
withActiveSpan,
2525
} from '../../../src/tracing';
2626
import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan';
27+
import { startNewTrace } from '../../../src/tracing/trace';
2728
import { _setSpanForScope } from '../../../src/utils/spanOnScope';
2829
import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils';
2930
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
@@ -1590,3 +1591,32 @@ describe('suppressTracing', () => {
15901591
});
15911592
});
15921593
});
1594+
1595+
describe('startNewTrace', () => {
1596+
beforeEach(() => {
1597+
getCurrentScope().clear();
1598+
getIsolationScope().clear();
1599+
});
1600+
1601+
it('creates a new propagation context on the current scope', () => {
1602+
const oldCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId;
1603+
1604+
startNewTrace(() => {
1605+
const newCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId;
1606+
1607+
expect(newCurrentScopeItraceId).toMatch(/^[a-f0-9]{32}$/);
1608+
expect(newCurrentScopeItraceId).not.toEqual(oldCurrentScopeItraceId);
1609+
});
1610+
});
1611+
1612+
it('keeps the propagation context on the isolation scope as-is', () => {
1613+
const oldIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId;
1614+
1615+
startNewTrace(() => {
1616+
const newIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId;
1617+
1618+
expect(newIsolationScopeTraceId).toMatch(/^[a-f0-9]{32}$/);
1619+
expect(newIsolationScopeTraceId).toEqual(oldIsolationScopeTraceId);
1620+
});
1621+
});
1622+
});

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export {
5858
startSpan,
5959
startInactiveSpan,
6060
startSpanManual,
61+
startNewTrace,
6162
metricsDefault as metrics,
6263
inboundFiltersIntegration,
6364
linkedErrorsIntegration,

packages/google-cloud-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export {
6262
startSpan,
6363
startInactiveSpan,
6464
startSpanManual,
65+
startNewTrace,
6566
withActiveSpan,
6667
getRootSpan,
6768
getSpanDescendants,

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export {
113113
startSpan,
114114
startSpanManual,
115115
startInactiveSpan,
116+
startNewTrace,
116117
getActiveSpan,
117118
withActiveSpan,
118119
getRootSpan,

packages/remix/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export {
6767
startSpan,
6868
startSpanManual,
6969
startInactiveSpan,
70+
startNewTrace,
7071
withActiveSpan,
7172
getSpanDescendants,
7273
continueTrace,

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
startSpan,
6161
startInactiveSpan,
6262
startSpanManual,
63+
startNewTrace,
6364
withActiveSpan,
6465
continueTrace,
6566
cron,

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ export * from './eventbuilder';
3535
export * from './anr';
3636
export * from './lru';
3737
export * from './buildPolyfills';
38+
export * from './propagationContext';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { PropagationContext } from '@sentry/types';
2+
import { uuid4 } from './misc';
3+
4+
/**
5+
* Returns a new minimal propagation context
6+
*/
7+
export function generatePropagationContext(): PropagationContext {
8+
return {
9+
traceId: uuid4(),
10+
spanId: uuid4().substring(16),
11+
};
12+
}

0 commit comments

Comments
 (0)