Skip to content

Commit 6f00b15

Browse files
antonisclaude
andcommitted
feat(tracing): Add strict trace continuation support
Add tests verifying that strictTraceContinuation and orgId options from @sentry/core are correctly passed through the React Native SDK init and native bridge. The core trace continuation logic is already implemented in @sentry/core v10.43.0 - this ensures the options are properly exposed and the behavior works end-to-end in the React Native context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 45df559 commit 6f00b15

3 files changed

Lines changed: 321 additions & 0 deletions

File tree

packages/core/test/sdk.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,34 @@ describe('Tests the SDK functionality', () => {
389389
});
390390
});
391391

392+
describe('strictTraceContinuation', () => {
393+
it('passes strictTraceContinuation option through to client options', () => {
394+
init({
395+
strictTraceContinuation: true,
396+
});
397+
expect(usedOptions()?.strictTraceContinuation).toBe(true);
398+
});
399+
400+
it('passes orgId option through to client options', () => {
401+
init({
402+
orgId: '12345',
403+
});
404+
expect(usedOptions()?.orgId).toBe('12345');
405+
});
406+
407+
it('passes numeric orgId option through to client options', () => {
408+
init({
409+
orgId: 12345,
410+
});
411+
expect(usedOptions()?.orgId).toBe(12345);
412+
});
413+
414+
it('defaults strictTraceContinuation to undefined when not set', () => {
415+
init({});
416+
expect(usedOptions()?.strictTraceContinuation).toBeUndefined();
417+
});
418+
});
419+
392420
describe('beforeBreadcrumb', () => {
393421
it('should filters out dev server breadcrumbs', () => {
394422
const devServerUrl = 'http://localhost:8081';
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { continueTrace, getCurrentScope, setCurrentClient } from '@sentry/core';
2+
import { getDefaultTestClientOptions, TestClient } from './mocks/client';
3+
4+
describe('strictTraceContinuation', () => {
5+
let client: TestClient;
6+
7+
afterEach(() => {
8+
jest.clearAllMocks();
9+
});
10+
11+
describe('with matching org IDs', () => {
12+
beforeEach(() => {
13+
client = new TestClient(
14+
getDefaultTestClientOptions({
15+
tracesSampleRate: 1.0,
16+
dsn: 'https://abc@o123.ingest.sentry.io/1234',
17+
}),
18+
);
19+
setCurrentClient(client);
20+
client.init();
21+
});
22+
23+
it('continues trace when baggage org_id matches DSN org ID', () => {
24+
const scope = continueTrace(
25+
{
26+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
27+
baggage: 'sentry-org_id=123',
28+
},
29+
() => {
30+
return getCurrentScope();
31+
},
32+
);
33+
34+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
35+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
36+
});
37+
});
38+
39+
describe('with mismatching org IDs', () => {
40+
beforeEach(() => {
41+
client = new TestClient(
42+
getDefaultTestClientOptions({
43+
tracesSampleRate: 1.0,
44+
dsn: 'https://abc@o123.ingest.sentry.io/1234',
45+
}),
46+
);
47+
setCurrentClient(client);
48+
client.init();
49+
});
50+
51+
it('starts new trace when baggage org_id does not match DSN org ID', () => {
52+
const scope = continueTrace(
53+
{
54+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
55+
baggage: 'sentry-org_id=456',
56+
},
57+
() => {
58+
return getCurrentScope();
59+
},
60+
);
61+
62+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
63+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
64+
});
65+
});
66+
67+
describe('with orgId option override', () => {
68+
beforeEach(() => {
69+
client = new TestClient(
70+
getDefaultTestClientOptions({
71+
tracesSampleRate: 1.0,
72+
dsn: 'https://abc@o123.ingest.sentry.io/1234',
73+
orgId: '999',
74+
}),
75+
);
76+
setCurrentClient(client);
77+
client.init();
78+
});
79+
80+
it('uses orgId option over DSN-extracted org ID', () => {
81+
// baggage org_id=123 matches DSN but NOT the orgId option (999)
82+
const scope = continueTrace(
83+
{
84+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
85+
baggage: 'sentry-org_id=123',
86+
},
87+
() => {
88+
return getCurrentScope();
89+
},
90+
);
91+
92+
// Should start new trace because orgId option (999) != baggage org_id (123)
93+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
94+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
95+
});
96+
97+
it('continues trace when baggage matches orgId option', () => {
98+
const scope = continueTrace(
99+
{
100+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
101+
baggage: 'sentry-org_id=999',
102+
},
103+
() => {
104+
return getCurrentScope();
105+
},
106+
);
107+
108+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
109+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
110+
});
111+
});
112+
113+
describe('strictTraceContinuation=true', () => {
114+
beforeEach(() => {
115+
client = new TestClient(
116+
getDefaultTestClientOptions({
117+
tracesSampleRate: 1.0,
118+
dsn: 'https://abc@o123.ingest.sentry.io/1234',
119+
strictTraceContinuation: true,
120+
}),
121+
);
122+
setCurrentClient(client);
123+
client.init();
124+
});
125+
126+
it('starts new trace when baggage has no org_id', () => {
127+
const scope = continueTrace(
128+
{
129+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
130+
baggage: 'sentry-environment=production',
131+
},
132+
() => {
133+
return getCurrentScope();
134+
},
135+
);
136+
137+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
138+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
139+
});
140+
141+
it('starts new trace when SDK has no org_id but baggage does', () => {
142+
// Use a DSN without org ID in hostname
143+
const clientWithoutOrgId = new TestClient(
144+
getDefaultTestClientOptions({
145+
tracesSampleRate: 1.0,
146+
dsn: 'https://abc@sentry.example.com/1234',
147+
strictTraceContinuation: true,
148+
}),
149+
);
150+
setCurrentClient(clientWithoutOrgId);
151+
clientWithoutOrgId.init();
152+
153+
const scope = continueTrace(
154+
{
155+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
156+
baggage: 'sentry-org_id=123',
157+
},
158+
() => {
159+
return getCurrentScope();
160+
},
161+
);
162+
163+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
164+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
165+
});
166+
167+
it('continues trace when both org IDs are missing', () => {
168+
const clientWithoutOrgId = new TestClient(
169+
getDefaultTestClientOptions({
170+
tracesSampleRate: 1.0,
171+
dsn: 'https://abc@sentry.example.com/1234',
172+
strictTraceContinuation: true,
173+
}),
174+
);
175+
setCurrentClient(clientWithoutOrgId);
176+
clientWithoutOrgId.init();
177+
178+
const scope = continueTrace(
179+
{
180+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
181+
baggage: 'sentry-environment=production',
182+
},
183+
() => {
184+
return getCurrentScope();
185+
},
186+
);
187+
188+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
189+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
190+
});
191+
});
192+
193+
describe('strictTraceContinuation=false (default)', () => {
194+
beforeEach(() => {
195+
client = new TestClient(
196+
getDefaultTestClientOptions({
197+
tracesSampleRate: 1.0,
198+
dsn: 'https://abc@o123.ingest.sentry.io/1234',
199+
strictTraceContinuation: false,
200+
}),
201+
);
202+
setCurrentClient(client);
203+
client.init();
204+
});
205+
206+
it('continues trace when baggage has no org_id', () => {
207+
const scope = continueTrace(
208+
{
209+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
210+
baggage: 'sentry-environment=production',
211+
},
212+
() => {
213+
return getCurrentScope();
214+
},
215+
);
216+
217+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
218+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
219+
});
220+
221+
it('continues trace when SDK has no org_id but baggage does', () => {
222+
const clientWithoutOrgId = new TestClient(
223+
getDefaultTestClientOptions({
224+
tracesSampleRate: 1.0,
225+
dsn: 'https://abc@sentry.example.com/1234',
226+
strictTraceContinuation: false,
227+
}),
228+
);
229+
setCurrentClient(clientWithoutOrgId);
230+
clientWithoutOrgId.init();
231+
232+
const scope = continueTrace(
233+
{
234+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
235+
baggage: 'sentry-org_id=123',
236+
},
237+
() => {
238+
return getCurrentScope();
239+
},
240+
);
241+
242+
expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012');
243+
expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012');
244+
});
245+
246+
it('still starts new trace when org IDs mismatch', () => {
247+
const scope = continueTrace(
248+
{
249+
sentryTrace: '12312012123120121231201212312012-1121201211212012-1',
250+
baggage: 'sentry-org_id=456',
251+
},
252+
() => {
253+
return getCurrentScope();
254+
},
255+
);
256+
257+
expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012');
258+
expect(scope.getPropagationContext().parentSpanId).toBeUndefined();
259+
});
260+
});
261+
});

packages/core/test/wrapper.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,38 @@ describe('Tests Native Wrapper', () => {
377377
expect(initParameter.enableLogs).toBe(expectedEnableLogs);
378378
expect(initParameter.logsOrigin).toBeUndefined();
379379
});
380+
381+
test('passes strictTraceContinuation option to native SDK', async () => {
382+
await NATIVE.initNativeSdk({
383+
dsn: 'test',
384+
enableNative: true,
385+
autoInitializeNativeSdk: true,
386+
strictTraceContinuation: true,
387+
devServerUrl: undefined,
388+
defaultSidecarUrl: undefined,
389+
mobileReplayOptions: undefined,
390+
});
391+
392+
expect(RNSentry.initNativeSdk).toHaveBeenCalled();
393+
const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
394+
expect(initParameter.strictTraceContinuation).toBe(true);
395+
});
396+
397+
test('passes orgId option to native SDK', async () => {
398+
await NATIVE.initNativeSdk({
399+
dsn: 'test',
400+
enableNative: true,
401+
autoInitializeNativeSdk: true,
402+
orgId: '12345',
403+
devServerUrl: undefined,
404+
defaultSidecarUrl: undefined,
405+
mobileReplayOptions: undefined,
406+
});
407+
408+
expect(RNSentry.initNativeSdk).toHaveBeenCalled();
409+
const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
410+
expect(initParameter.orgId).toBe('12345');
411+
});
380412
});
381413

382414
describe('sendEnvelope', () => {

0 commit comments

Comments
 (0)