diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js
new file mode 100644
index 000000000000..5b28df9da5e8
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/subject.js
@@ -0,0 +1,15 @@
+const newTraceBtn = document.getElementById('newTrace');
+newTraceBtn.addEventListener('click', async () => {
+ Sentry.startNewTrace(() => {
+ Sentry.startSpan({ op: 'ui.interaction.click', name: 'new-trace' }, async () => {
+ await fetch('http://example.com');
+ });
+ });
+});
+
+const oldTraceBtn = document.getElementById('oldTrace');
+oldTraceBtn.addEventListener('click', async () => {
+ Sentry.startSpan({ op: 'ui.interaction.click', name: 'old-trace' }, async () => {
+ await fetch('http://example.com');
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html
new file mode 100644
index 000000000000..7d3c25bf7b84
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts
new file mode 100644
index 000000000000..3ddca4787aee
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts
@@ -0,0 +1,106 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../../utils/fixtures';
+import type { EventAndTraceHeader } from '../../../../utils/helpers';
+import {
+ eventAndTraceHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback',
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ await page.route('http://example.com/**', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({}),
+ });
+ });
+
+ const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest(
+ page,
+ url,
+ eventAndTraceHeaderRequestParser,
+ );
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+
+ expect(pageloadEvent.type).toEqual('transaction');
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ expect(pageloadTraceHeaders).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: pageloadTraceContext?.trace_id,
+ });
+
+ const transactionPromises = getMultipleSentryEnvelopeRequests(
+ page,
+ 2,
+ { envelopeType: 'transaction' },
+ eventAndTraceHeaderRequestParser,
+ );
+
+ await page.locator('#newTrace').click();
+ await page.locator('#oldTrace').click();
+
+ const [txnEvent1, txnEvent2] = await transactionPromises;
+
+ const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] =
+ txnEvent1[0].transaction === 'new-trace' ? txnEvent1 : txnEvent2;
+ const [oldTraceTransactionEvent, oldTraceTransactionTraceHeaders] =
+ txnEvent1[0].transaction === 'old-trace' ? txnEvent1 : txnEvent2;
+
+ const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace;
+ expect(newTraceTransactionTraceContext).toMatchObject({
+ op: 'ui.interaction.click',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ expect(newTraceTransactionTraceHeaders).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: newTraceTransactionTraceContext?.trace_id,
+ transaction: 'new-trace',
+ });
+
+ const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace;
+ expect(oldTraceTransactionEventTraceContext).toMatchObject({
+ op: 'ui.interaction.click',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+
+ expect(oldTraceTransactionTraceHeaders).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: oldTraceTransactionTraceHeaders?.trace_id,
+ // transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction
+ // which does not have a `transaction` field because its source is URL.
+ });
+
+ expect(oldTraceTransactionEventTraceContext?.trace_id).toEqual(pageloadTraceContext?.trace_id);
+ expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
+ },
+);
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index a4f3ca59fb1f..6657b3030cb1 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -64,6 +64,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
continueTrace,
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 1d2323df06e5..62165a710127 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -62,6 +62,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
index de8453db8784..b9dc457640be 100644
--- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
@@ -10,6 +10,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts
index 3b8a51e661dc..a0e4d4736384 100644
--- a/packages/browser/src/index.bundle.tracing.replay.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.ts
@@ -10,6 +10,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts
index e93bf68994e3..d540ff0bd6f9 100644
--- a/packages/browser/src/index.bundle.tracing.ts
+++ b/packages/browser/src/index.bundle.tracing.ts
@@ -11,6 +11,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 86e6ea20fe81..245eaa966859 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -59,6 +59,7 @@ export {
startInactiveSpan,
startSpanManual,
withActiveSpan,
+ startNewTrace,
getSpanDescendants,
setMeasurement,
getSpanStatusFromHttpCode,
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index b3d530ee3653..f6528e4d155d 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -27,10 +27,10 @@ import type { Client, IntegrationFn, StartSpanOptions, TransactionSource } from
import type { Span } from '@sentry/types';
import {
browserPerformanceTimeOrigin,
+ generatePropagationContext,
getDomElement,
logger,
propagationContextFromHeaders,
- uuid4,
} from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
@@ -412,8 +412,8 @@ export function startBrowserTracingPageLoadSpan(
* This will only do something if a browser tracing integration has been setup.
*/
export function startBrowserTracingNavigationSpan(client: Client, spanOptions: StartSpanOptions): Span | undefined {
- getCurrentScope().setPropagationContext(generatePropagationContext());
getIsolationScope().setPropagationContext(generatePropagationContext());
+ getCurrentScope().setPropagationContext(generatePropagationContext());
client.emit('startNavigationSpan', spanOptions);
@@ -487,10 +487,3 @@ function registerInteractionListener(
addEventListener('click', registerInteractionTransaction, { once: false, capture: true });
}
}
-
-function generatePropagationContext(): { traceId: string; spanId: string } {
- return {
- traceId: uuid4(),
- spanId: uuid4().substring(16),
- };
-}
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index fd7671b34b09..c3e8eff8beac 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -82,6 +82,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts
index 724c9b621ce9..ff89c0d593a9 100644
--- a/packages/core/src/scope.ts
+++ b/packages/core/src/scope.ts
@@ -21,7 +21,7 @@ import type {
SeverityLevel,
User,
} from '@sentry/types';
-import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils';
+import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils';
import { updateSession } from './session';
import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope';
@@ -600,10 +600,3 @@ export const Scope = ScopeClass;
* Holds additional event information.
*/
export type Scope = ScopeInterface;
-
-function generatePropagationContext(): PropagationContext {
- return {
- traceId: uuid4(),
- spanId: uuid4().substring(16),
- };
-}
diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts
index 90a5ac737aa1..0c08101acb68 100644
--- a/packages/core/src/tracing/index.ts
+++ b/packages/core/src/tracing/index.ts
@@ -17,6 +17,7 @@ export {
continueTrace,
withActiveSpan,
suppressTracing,
+ startNewTrace,
} from './trace';
export {
getDynamicSamplingContextFromClient,
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index 4d910f54e996..e34c2c1a62d3 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -1,11 +1,12 @@
import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, StartSpanOptions } from '@sentry/types';
-import { propagationContextFromHeaders } from '@sentry/utils';
+import { generatePropagationContext, logger, propagationContextFromHeaders } from '@sentry/utils';
import type { AsyncContextStrategy } from '../asyncContext/types';
import { getMainCarrier } from '../carrier';
import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes';
import { getAsyncContextStrategy } from '../asyncContext';
+import { DEBUG_BUILD } from '../debug-build';
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
@@ -212,6 +213,30 @@ export function suppressTracing(callback: () => T): T {
});
}
+/**
+ * Starts a new trace for the duration of the provided callback. Spans started within the
+ * callback will be part of the new trace instead of a potentially previously started trace.
+ *
+ * Important: Only use this function if you want to override the default trace lifetime and
+ * propagation mechanism of the SDK for the duration and scope of the provided callback.
+ * The newly created trace will also be the root of a new distributed trace, for example if
+ * you make http requests within the callback.
+ * This function might be useful if the operation you want to instrument should not be part
+ * of a potentially ongoing trace.
+ *
+ * Default behavior:
+ * - Server-side: A new trace is started for each incoming request.
+ * - Browser: A new trace is started for each page our route. Navigating to a new route
+ * or page will automatically create a new trace.
+ */
+export function startNewTrace(callback: () => T): T {
+ return withScope(scope => {
+ scope.setPropagationContext(generatePropagationContext());
+ DEBUG_BUILD && logger.info(`Starting a new trace with id ${scope.getPropagationContext().traceId}`);
+ return withActiveSpan(null, callback);
+ });
+}
+
function createChildOrRootSpan({
parentSpan,
spanContext,
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
index f2aa8460dba4..9399aa6b5a57 100644
--- a/packages/core/test/lib/tracing/trace.test.ts
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -24,6 +24,7 @@ import {
withActiveSpan,
} from '../../../src/tracing';
import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan';
+import { startNewTrace } from '../../../src/tracing/trace';
import { _setSpanForScope } from '../../../src/utils/spanOnScope';
import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils';
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
@@ -1590,3 +1591,32 @@ describe('suppressTracing', () => {
});
});
});
+
+describe('startNewTrace', () => {
+ beforeEach(() => {
+ getCurrentScope().clear();
+ getIsolationScope().clear();
+ });
+
+ it('creates a new propagation context on the current scope', () => {
+ const oldCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId;
+
+ startNewTrace(() => {
+ const newCurrentScopeItraceId = getCurrentScope().getPropagationContext().traceId;
+
+ expect(newCurrentScopeItraceId).toMatch(/^[a-f0-9]{32}$/);
+ expect(newCurrentScopeItraceId).not.toEqual(oldCurrentScopeItraceId);
+ });
+ });
+
+ it('keeps the propagation context on the isolation scope as-is', () => {
+ const oldIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId;
+
+ startNewTrace(() => {
+ const newIsolationScopeTraceId = getIsolationScope().getPropagationContext().traceId;
+
+ expect(newIsolationScopeTraceId).toMatch(/^[a-f0-9]{32}$/);
+ expect(newIsolationScopeTraceId).toEqual(oldIsolationScopeTraceId);
+ });
+ });
+});
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
index 1857de352798..aa30c762d624 100644
--- a/packages/deno/src/index.ts
+++ b/packages/deno/src/index.ts
@@ -58,6 +58,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
metricsDefault as metrics,
inboundFiltersIntegration,
linkedErrorsIntegration,
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 067c27818a10..6affee429e1f 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -62,6 +62,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 5cc16772189b..47d3d5d7735f 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -111,6 +111,7 @@ export {
startSpan,
startSpanManual,
startInactiveSpan,
+ startNewTrace,
getActiveSpan,
withActiveSpan,
getRootSpan,
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index ab35b4bf4847..a6476b692fbf 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -67,6 +67,7 @@ export {
startSpan,
startSpanManual,
startInactiveSpan,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
continueTrace,
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index d7d50f64481e..c8b97029e456 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -60,6 +60,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
continueTrace,
cron,
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index a0649cef48ad..2fb6f420ab58 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -35,3 +35,4 @@ export * from './eventbuilder';
export * from './anr';
export * from './lru';
export * from './buildPolyfills';
+export * from './propagationContext';
diff --git a/packages/utils/src/propagationContext.ts b/packages/utils/src/propagationContext.ts
new file mode 100644
index 000000000000..745531c8aa98
--- /dev/null
+++ b/packages/utils/src/propagationContext.ts
@@ -0,0 +1,12 @@
+import type { PropagationContext } from '@sentry/types';
+import { uuid4 } from './misc';
+
+/**
+ * Returns a new minimal propagation context
+ */
+export function generatePropagationContext(): PropagationContext {
+ return {
+ traceId: uuid4(),
+ spanId: uuid4().substring(16),
+ };
+}
diff --git a/packages/utils/test/proagationContext.test.ts b/packages/utils/test/proagationContext.test.ts
new file mode 100644
index 000000000000..01c8569bde9b
--- /dev/null
+++ b/packages/utils/test/proagationContext.test.ts
@@ -0,0 +1,10 @@
+import { generatePropagationContext } from '../src/propagationContext';
+
+describe('generatePropagationContext', () => {
+ it('generates a new minimal propagation context', () => {
+ expect(generatePropagationContext()).toEqual({
+ traceId: expect.stringMatching(/^[0-9a-f]{32}$/),
+ spanId: expect.stringMatching(/^[0-9a-f]{16}$/),
+ });
+ });
+});
diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts
index ce4ef113908b..79c6d77c9d21 100644
--- a/packages/vercel-edge/src/index.ts
+++ b/packages/vercel-edge/src/index.ts
@@ -58,6 +58,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
+ startNewTrace,
withActiveSpan,
getSpanDescendants,
continueTrace,