Skip to content

Commit 91e84db

Browse files
antonisclaude
andauthored
feat: Implement strict trace continuation (#1166)
* feat: Implement strict trace continuation Expose `strictTraceContinuation` and `orgId` options in the Capacitor SDK. These options pass through to @sentry/core which handles the actual trace continuation validation logic. Spec: https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: Add changelog entry for strict trace continuation * fix: Add strictTraceContinuation and orgId to FilterNativeOptions Address Cursor Bugbot feedback: the options were not included in the FilterNativeOptions whitelist, so they would be silently dropped on native platforms. * feat: Map strictTraceContinuation and orgId to native SDKs The options were being passed through FilterNativeOptions to the bridge but not mapped to the native SDK options on Android and iOS. Aligned with sentry-react-native implementation (PR #5829). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: Add wrapper tests for strictTraceContinuation and orgId Verify that strictTraceContinuation and orgId options pass through the native bridge via initNativeSdk, aligned with RN wrapper tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 271ada7 commit 91e84db

8 files changed

Lines changed: 163 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#1166](https://github.com/getsentry/sentry-capacitor/pull/1166))
14+
1115
### Dependencies
1216

1317
- Bump Android SDK from v8.35.0 to v8.41.0 ([#1247](https://github.com/getsentry/sentry-capacitor/pull/1247))

android/src/main/java/io/sentry/capacitor/SentryCapacitor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ public void initNativeSdk(final PluginCall call) {
154154
}
155155
}
156156

157+
if (capOptions.has("strictTraceContinuation")) {
158+
options.setStrictTraceContinuation(capOptions.getBool("strictTraceContinuation"));
159+
}
160+
if (capOptions.has("orgId")) {
161+
Object orgIdValue = capOptions.opt("orgId");
162+
if (orgIdValue instanceof String) {
163+
options.setOrgId((String) orgIdValue);
164+
} else if (orgIdValue instanceof Number) {
165+
options.setOrgId(String.valueOf(((Number) orgIdValue).longValue()));
166+
}
167+
}
168+
157169
options.getLogs().setEnabled(Boolean.TRUE.equals(capOptions.getBoolean("enableLogs", false)));
158170

159171
logger.log(SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));

ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
158158
options.enableAutoPerformanceTracing = enableAutoPerformanceTracing
159159
}
160160

161+
if let strictTraceContinuation = dict["strictTraceContinuation"] as? Bool {
162+
options.strictTraceContinuation = strictTraceContinuation
163+
}
164+
if let orgId = dict["orgId"] {
165+
if let orgIdString = orgId as? String {
166+
options.orgId = orgIdString
167+
} else if let orgIdNumber = orgId as? NSNumber {
168+
options.orgId = orgIdNumber.stringValue
169+
}
170+
}
171+
161172
return options
162173
}
163174

src/nativeOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export function FilterNativeOptions(
4242
tracesSampleRate: options.tracesSampleRate,
4343
// tunnel: options.tunnel: Only handled on the JavaScript Layer.
4444
enableCaptureFailedRequests: options.enableCaptureFailedRequests,
45+
...(options.strictTraceContinuation !== undefined && { strictTraceContinuation: options.strictTraceContinuation }),
46+
...(options.orgId !== undefined && { orgId: options.orgId }),
4547
...iOSParameters(options),
4648
...LogParameters(options),
4749
...SpotlightParameters(),

src/options.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,27 @@ export interface BaseCapacitorOptions {
7373
*/
7474
appHangTimeoutInterval?: number;
7575

76+
/**
77+
* If set to `true`, the SDK will only continue a trace if the `organization ID` of the incoming trace found in the
78+
* `baggage` header matches the `organization ID` of the current Sentry client.
79+
*
80+
* The client's organization ID is extracted from the DSN or can be set with the `orgId` option.
81+
*
82+
* If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one.
83+
* This is useful to prevent traces of unknown third-party services from being continued in your application.
84+
*
85+
* @default false
86+
*/
87+
strictTraceContinuation?: boolean;
88+
89+
/**
90+
* The organization ID for your Sentry project.
91+
*
92+
* The SDK will try to extract the organization ID from the DSN. If it cannot be found, or if you need to override it,
93+
* you can provide the ID with this option. The organization ID is used for trace propagation and for features like `strictTraceContinuation`.
94+
*/
95+
orgId?: `${number}` | number;
96+
7697
/**
7798
* Only for Vue or Nuxt Client.
7899
* Allows the setup of sibling specific SDK. You are still allowed to set the same parameters

test/nativeOptions.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ describe('nativeOptions', () => {
3535
expect(nativeOptions.enableWatchdogTerminationTracking).toBeTruthy();
3636
});
3737

38+
test('strictTraceContinuation and orgId are set when defined', () => {
39+
const nativeOptions = FilterNativeOptions(
40+
{
41+
strictTraceContinuation: true,
42+
orgId: '12345',
43+
});
44+
expect(nativeOptions.strictTraceContinuation).toBe(true);
45+
expect(nativeOptions.orgId).toBe('12345');
46+
});
47+
3848
test('enableCaptureFailedRequests is set when defined', () => {
3949
const nativeOptions = FilterNativeOptions(
4050
{

test/sdk.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,57 @@ describe('SDK Init', () => {
206206
});
207207
});
208208

209+
test('passes strictTraceContinuation and orgId to browser options', () => {
210+
NATIVE.platform = 'web';
211+
const mockOriginalInit = jest.fn();
212+
213+
init({
214+
dsn: 'test-dsn',
215+
enabled: true,
216+
strictTraceContinuation: true,
217+
orgId: '12345',
218+
}, mockOriginalInit);
219+
220+
// Wait for async operations
221+
return new Promise<void>(resolve => {
222+
setTimeout(() => {
223+
expect(mockOriginalInit).toHaveBeenCalled();
224+
const browserOptions = mockOriginalInit.mock.calls[0][0];
225+
226+
expect(browserOptions.strictTraceContinuation).toBe(true);
227+
expect(browserOptions.orgId).toBe('12345');
228+
229+
resolve();
230+
}, 0);
231+
});
232+
});
233+
234+
// Native option filtering for strictTraceContinuation and orgId
235+
// is tested in nativeOptions.test.ts via FilterNativeOptions.
236+
237+
test('strictTraceContinuation defaults to undefined when not set', () => {
238+
NATIVE.platform = 'web';
239+
const mockOriginalInit = jest.fn();
240+
241+
init({
242+
dsn: 'test-dsn',
243+
enabled: true,
244+
}, mockOriginalInit);
245+
246+
// Wait for async operations
247+
return new Promise<void>(resolve => {
248+
setTimeout(() => {
249+
expect(mockOriginalInit).toHaveBeenCalled();
250+
const browserOptions = mockOriginalInit.mock.calls[0][0];
251+
252+
expect(browserOptions.strictTraceContinuation).toBeUndefined();
253+
expect(browserOptions.orgId).toBeUndefined();
254+
255+
resolve();
256+
}, 0);
257+
});
258+
});
259+
209260
test('RewriteFrames to be added by default', async () => {
210261
NATIVE.platform = 'web';
211262
const mockOriginalInit = jest.fn();

test/wrapper.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,58 @@ describe('Tests Native Wrapper', () => {
170170
);
171171
});
172172

173+
test('passes strictTraceContinuation to native SDK', async () => {
174+
const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk');
175+
176+
await NATIVE.initNativeSdk({
177+
dsn: 'test',
178+
enableNative: true,
179+
strictTraceContinuation: true,
180+
});
181+
182+
const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options;
183+
expect(nativeOption?.strictTraceContinuation).toBe(true);
184+
});
185+
186+
test('passes orgId as string to native SDK', async () => {
187+
const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk');
188+
189+
await NATIVE.initNativeSdk({
190+
dsn: 'test',
191+
enableNative: true,
192+
orgId: '12345',
193+
});
194+
195+
const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options;
196+
expect(nativeOption?.orgId).toBe('12345');
197+
});
198+
199+
test('passes numeric orgId to native SDK', async () => {
200+
const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk');
201+
202+
await NATIVE.initNativeSdk({
203+
dsn: 'test',
204+
enableNative: true,
205+
orgId: 12345,
206+
});
207+
208+
const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options;
209+
expect(nativeOption?.orgId).toBe(12345);
210+
});
211+
212+
test('does not include strictTraceContinuation when not set', async () => {
213+
const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk');
214+
215+
await NATIVE.initNativeSdk({
216+
dsn: 'test',
217+
enableNative: true,
218+
});
219+
220+
const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options;
221+
expect(nativeOption?.strictTraceContinuation).toBeUndefined();
222+
expect(nativeOption?.orgId).toBeUndefined();
223+
});
224+
173225
test('sets enableNative: false when dsn is undefined', async () => {
174226
await NATIVE.initNativeSdk({
175227
dsn: undefined,

0 commit comments

Comments
 (0)