Skip to content

feat(core): Add startNewTrace API #12138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 27, 2024
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
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="oldTrace">Old Trace</button>
<button id="newTrace">new Trace</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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<EventAndTraceHeader>(
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<EventAndTraceHeader>(
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);
},
);
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getSpanDescendants,
continueTrace,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.bundle.tracing.replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.bundle.tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getSpanDescendants,
setMeasurement,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export {
startInactiveSpan,
startSpanManual,
withActiveSpan,
startNewTrace,
getSpanDescendants,
setMeasurement,
getSpanStatusFromHttpCode,
Expand Down
11 changes: 2 additions & 9 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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),
};
}
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
Expand Down
9 changes: 1 addition & 8 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
};
}
1 change: 1 addition & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
continueTrace,
withActiveSpan,
suppressTracing,
startNewTrace,
} from './trace';
export {
getDynamicSamplingContextFromClient,
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -212,6 +213,30 @@ export function suppressTracing<T>(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<T>(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,
Expand Down
30 changes: 30 additions & 0 deletions packages/core/test/lib/tracing/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
});
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
metricsDefault as metrics,
inboundFiltersIntegration,
linkedErrorsIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
getRootSpan,
getSpanDescendants,
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export {
startSpan,
startSpanManual,
startInactiveSpan,
startNewTrace,
getActiveSpan,
withActiveSpan,
getRootSpan,
Expand Down
1 change: 1 addition & 0 deletions packages/remix/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export {
startSpan,
startSpanManual,
startInactiveSpan,
startNewTrace,
withActiveSpan,
getSpanDescendants,
continueTrace,
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
startSpan,
startInactiveSpan,
startSpanManual,
startNewTrace,
withActiveSpan,
continueTrace,
cron,
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './eventbuilder';
export * from './anr';
export * from './lru';
export * from './buildPolyfills';
export * from './propagationContext';
12 changes: 12 additions & 0 deletions packages/utils/src/propagationContext.ts
Original file line number Diff line number Diff line change
@@ -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),
};
}
Loading
Loading