Skip to content

Commit 33385ff

Browse files
committed
feat(core): Implement strictTraceContinuation
1 parent c3a9682 commit 33385ff

File tree

3 files changed

+201
-0
lines changed

3 files changed

+201
-0
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { hasSpansEnabled } from '../utils/hasSpansEnabled';
1616
import { parseSampleRate } from '../utils/parseSampleRate';
1717
import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope';
1818
import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
19+
import { baggageHeaderToDynamicSamplingContext } from '../utils-hoist/baggage';
20+
import { extractOrgIdFromDsnHost } from '../utils-hoist/dsn';
1921
import { logger } from '../utils-hoist/logger';
2022
import { generateTraceId } from '../utils-hoist/propagationContext';
2123
import { propagationContextFromHeaders } from '../utils-hoist/tracing';
@@ -215,6 +217,47 @@ export const continueTrace = <V>(
215217
}
216218

217219
const { sentryTrace, baggage } = options;
220+
const client = getClient();
221+
222+
const clientOptions = client?.getOptions();
223+
const strictTraceContinuation = clientOptions?.strictTraceContinuation || false; // default for `strictTraceContinuation` is `false` todo(v10): set default to `true`
224+
225+
const incomingDsc = baggageHeaderToDynamicSamplingContext(baggage);
226+
const baggageOrgId = incomingDsc?.org_id;
227+
228+
let sdkOrgId: string | undefined;
229+
const dsn = client?.getDsn();
230+
if (dsn?.host) {
231+
sdkOrgId = extractOrgIdFromDsnHost(dsn.host);
232+
}
233+
234+
const shouldStartNewTrace = (): boolean => {
235+
// Case: baggage org ID and SDK org ID don't match - always start new trace
236+
if (baggageOrgId && sdkOrgId && baggageOrgId !== sdkOrgId) {
237+
DEBUG_BUILD &&
238+
logger.info(`Starting a new trace because org IDs don't match (incoming: ${baggageOrgId}, sdk: ${sdkOrgId})`);
239+
return true;
240+
}
241+
242+
if (strictTraceContinuation) {
243+
// With strict continuation enabled, start new trace if:
244+
// - Baggage has org ID but SDK doesn't have one
245+
// - SDK has org ID but baggage doesn't have one
246+
if ((baggageOrgId && !sdkOrgId) || (!baggageOrgId && sdkOrgId)) {
247+
DEBUG_BUILD &&
248+
logger.info(
249+
`Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming: ${baggageOrgId}, sdk: ${sdkOrgId})`,
250+
);
251+
return true;
252+
}
253+
}
254+
255+
return false;
256+
};
257+
258+
if (shouldStartNewTrace()) {
259+
return startNewTrace(callback);
260+
}
218261

219262
return withScope(scope => {
220263
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);

packages/core/src/types-hoist/options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,18 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
320320
*/
321321
tracePropagationTargets?: TracePropagationTargets;
322322

323+
/**
324+
* Controls whether trace continuation should be strict about matching organization IDs.
325+
*
326+
* When set to `true`, the SDK will only continue a trace if the organization ID in the incoming baggage
327+
* matches the organization ID of the current SDK (extracted from the DSN).
328+
*
329+
* If there is no match, the SDK will start a new trace instead of continuing the incoming one.
330+
*
331+
* @default false
332+
*/
333+
strictTraceContinuation?: boolean;
334+
323335
/**
324336
* The organization ID of the current SDK. The organization ID is a string containing only numbers. This ID is used to
325337
* propagate traces to other Sentry services.

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { Span } from '../../../src/types-hoist/span';
3131
import type { StartSpanOptions } from '../../../src/types-hoist/startSpanOptions';
3232
import { _setSpanForScope } from '../../../src/utils/spanOnScope';
3333
import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils';
34+
import { extractOrgIdFromDsnHost } from '../../../src/utils-hoist/dsn';
3435
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
3536

3637
const enum Type {
@@ -1733,6 +1734,151 @@ describe('continueTrace', () => {
17331734

17341735
expect(result).toEqual('aha');
17351736
});
1737+
1738+
describe('strictTraceContinuation', () => {
1739+
const creatOrgIdInDsn = (orgId: number) => {
1740+
vi.spyOn(client, 'getDsn').mockReturnValue({
1741+
host: `o${orgId}.ingest.sentry.io`,
1742+
protocol: 'https',
1743+
projectId: 'projId',
1744+
});
1745+
};
1746+
1747+
afterEach(() => {
1748+
vi.clearAllMocks();
1749+
});
1750+
1751+
it('continues trace when org IDs match', () => {
1752+
creatOrgIdInDsn(123);
1753+
1754+
const scope = continueTrace(
1755+
{
1756+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1757+
baggage: 'sentry-org_id=123',
1758+
},
1759+
() => {
1760+
return getCurrentScope();
1761+
},
1762+
);
1763+
1764+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
1765+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
1766+
});
1767+
1768+
it('starts new trace when both SDK and baggage org IDs are set and do not match', () => {
1769+
creatOrgIdInDsn(123);
1770+
1771+
const scope = continueTrace(
1772+
{
1773+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1774+
baggage: 'sentry-org_id=456',
1775+
},
1776+
() => {
1777+
return getCurrentScope();
1778+
},
1779+
);
1780+
1781+
// Should start a new trace with a different trace ID
1782+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
1783+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
1784+
});
1785+
1786+
describe('when strictTraceContinuation is true', () => {
1787+
it('starts new trace when baggage org ID is missing', () => {
1788+
client.getOptions().strictTraceContinuation = true;
1789+
1790+
creatOrgIdInDsn(123);
1791+
1792+
const scope = continueTrace(
1793+
{
1794+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1795+
baggage: 'sentry-environment=production',
1796+
},
1797+
() => {
1798+
return getCurrentScope();
1799+
},
1800+
);
1801+
1802+
// Should start a new trace with a different trace ID
1803+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
1804+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
1805+
});
1806+
1807+
it('starts new trace when SDK org ID is missing', () => {
1808+
client.getOptions().strictTraceContinuation = true;
1809+
1810+
const scope = continueTrace(
1811+
{
1812+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1813+
baggage: 'sentry-org_id=123',
1814+
},
1815+
() => {
1816+
return getCurrentScope();
1817+
},
1818+
);
1819+
1820+
// Should start a new trace with a different trace ID
1821+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
1822+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
1823+
});
1824+
1825+
it('continues trace when both org IDs are missing', () => {
1826+
client.getOptions().strictTraceContinuation = true;
1827+
1828+
const scope = continueTrace(
1829+
{
1830+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1831+
baggage: 'sentry-environment=production',
1832+
},
1833+
() => {
1834+
return getCurrentScope();
1835+
},
1836+
);
1837+
1838+
// Should continue the trace
1839+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
1840+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
1841+
});
1842+
});
1843+
1844+
describe('when strictTraceContinuation is false', () => {
1845+
it('continues trace when baggage org ID is missing', () => {
1846+
client.getOptions().strictTraceContinuation = false;
1847+
1848+
creatOrgIdInDsn(123);
1849+
1850+
const scope = continueTrace(
1851+
{
1852+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1853+
baggage: 'sentry-environment=production',
1854+
},
1855+
() => {
1856+
return getCurrentScope();
1857+
},
1858+
);
1859+
1860+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
1861+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
1862+
});
1863+
1864+
it('SDK org ID is missing', () => {
1865+
client.getOptions().strictTraceContinuation = false;
1866+
1867+
const scope = continueTrace(
1868+
{
1869+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
1870+
baggage: 'sentry-org_id=123',
1871+
},
1872+
() => {
1873+
return getCurrentScope();
1874+
},
1875+
);
1876+
1877+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
1878+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
1879+
});
1880+
});
1881+
});
17361882
});
17371883

17381884
describe('getActiveSpan', () => {

0 commit comments

Comments
 (0)