From 3f399990edb0ef9e322d0826afc2f660138020c8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 14:12:22 +0100 Subject: [PATCH 1/5] 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) --- src/options.ts | 21 ++++++++++++++ test/sdk.test.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/options.ts b/src/options.ts index 2f074c4c..214019af 100644 --- a/src/options.ts +++ b/src/options.ts @@ -74,6 +74,27 @@ export interface BaseCapacitorOptions { appHangTimeoutInterval?: number; + /** + * If set to `true`, the SDK will only continue a trace if the `organization ID` of the incoming trace found in the + * `baggage` header matches the `organization ID` of the current Sentry client. + * + * The client's organization ID is extracted from the DSN or can be set with the `orgId` option. + * + * If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one. + * This is useful to prevent traces of unknown third-party services from being continued in your application. + * + * @default false + */ + strictTraceContinuation?: boolean; + + /** + * The organization ID for your Sentry project. + * + * The SDK will try to extract the organization ID from the DSN. If it cannot be found, or if you need to override it, + * you can provide the ID with this option. The organization ID is used for trace propagation and for features like `strictTraceContinuation`. + */ + orgId?: `${number}` | number; + /** * Only for Vue or Nuxt Client. * Allows the setup of sibling specific SDK. You are still allowed to set the same parameters diff --git a/test/sdk.test.ts b/test/sdk.test.ts index a05f09cc..c7e65e23 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -206,6 +206,79 @@ describe('SDK Init', () => { }); }); + test('passes strictTraceContinuation and orgId to browser options', () => { + NATIVE.platform = 'web'; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + strictTraceContinuation: true, + orgId: '12345', + }, mockOriginalInit); + + // Wait for async operations + return new Promise(resolve => { + setTimeout(() => { + expect(mockOriginalInit).toHaveBeenCalled(); + const browserOptions = mockOriginalInit.mock.calls[0][0]; + + expect(browserOptions.strictTraceContinuation).toBe(true); + expect(browserOptions.orgId).toBe('12345'); + + resolve(); + }, 0); + }); + }); + + test('passes strictTraceContinuation and orgId to native options', () => { + NATIVE.platform = 'ios'; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + strictTraceContinuation: true, + orgId: 67890, + }, mockOriginalInit); + + // Wait for async operations + return new Promise(resolve => { + setTimeout(() => { + expect(NATIVE.initNativeSdk).toHaveBeenCalled(); + const nativeOptions = (NATIVE.initNativeSdk as jest.Mock).mock.calls[0][0]; + + expect(nativeOptions.strictTraceContinuation).toBe(true); + expect(nativeOptions.orgId).toBe(67890); + + resolve(); + }, 0); + }); + }); + + test('strictTraceContinuation defaults to undefined when not set', () => { + NATIVE.platform = 'web'; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + }, mockOriginalInit); + + // Wait for async operations + return new Promise(resolve => { + setTimeout(() => { + expect(mockOriginalInit).toHaveBeenCalled(); + const browserOptions = mockOriginalInit.mock.calls[0][0]; + + expect(browserOptions.strictTraceContinuation).toBeUndefined(); + expect(browserOptions.orgId).toBeUndefined(); + + resolve(); + }, 0); + }); + }); + test('RewriteFrames to be added by default', async () => { NATIVE.platform = 'web'; const mockOriginalInit = jest.fn(); From 0074b0455b4f2aa452b59c102ccd92730bab27e2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 14:51:52 +0100 Subject: [PATCH 2/5] chore: Add changelog entry for strict trace continuation --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 291678df..894de1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > [migration guide](https://docs.sentry.io/platforms/javascript/guides/capacitor/migration/) first. +## Unreleased + +### Features + +- Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#1166](https://github.com/getsentry/sentry-capacitor/pull/1166)) + ## 3.1.0 ### Dependencies From f01d4ab34a87dc35ddf624ed9226d0a8adc4b732 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 15:06:03 +0100 Subject: [PATCH 3/5] 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. --- src/nativeOptions.ts | 2 ++ test/nativeOptions.test.ts | 10 ++++++++++ test/sdk.test.ts | 26 ++------------------------ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/nativeOptions.ts b/src/nativeOptions.ts index 019adacb..8f433d6c 100644 --- a/src/nativeOptions.ts +++ b/src/nativeOptions.ts @@ -44,6 +44,8 @@ export function FilterNativeOptions( tracesSampleRate: options.tracesSampleRate, // tunnel: options.tunnel: Only handled on the JavaScript Layer. enableCaptureFailedRequests: options.enableCaptureFailedRequests, + ...(options.strictTraceContinuation !== undefined && { strictTraceContinuation: options.strictTraceContinuation }), + ...(options.orgId !== undefined && { orgId: options.orgId }), ...iOSParameters(options), ...LogParameters(options), ...SpotlightParameters(), diff --git a/test/nativeOptions.test.ts b/test/nativeOptions.test.ts index 65fe7ae0..b0ff16ca 100644 --- a/test/nativeOptions.test.ts +++ b/test/nativeOptions.test.ts @@ -35,6 +35,16 @@ describe('nativeOptions', () => { expect(nativeOptions.enableWatchdogTerminationTracking).toBeTruthy(); }); + test('strictTraceContinuation and orgId are set when defined', () => { + const nativeOptions = FilterNativeOptions( + { + strictTraceContinuation: true, + orgId: '12345', + }); + expect(nativeOptions.strictTraceContinuation).toBe(true); + expect(nativeOptions.orgId).toBe('12345'); + }); + test('enableCaptureFailedRequests is set when defined', () => { const nativeOptions = FilterNativeOptions( { diff --git a/test/sdk.test.ts b/test/sdk.test.ts index c7e65e23..5f98a1b7 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -231,30 +231,8 @@ describe('SDK Init', () => { }); }); - test('passes strictTraceContinuation and orgId to native options', () => { - NATIVE.platform = 'ios'; - const mockOriginalInit = jest.fn(); - - init({ - dsn: 'test-dsn', - enabled: true, - strictTraceContinuation: true, - orgId: 67890, - }, mockOriginalInit); - - // Wait for async operations - return new Promise(resolve => { - setTimeout(() => { - expect(NATIVE.initNativeSdk).toHaveBeenCalled(); - const nativeOptions = (NATIVE.initNativeSdk as jest.Mock).mock.calls[0][0]; - - expect(nativeOptions.strictTraceContinuation).toBe(true); - expect(nativeOptions.orgId).toBe(67890); - - resolve(); - }, 0); - }); - }); + // Native option filtering for strictTraceContinuation and orgId + // is tested in nativeOptions.test.ts via FilterNativeOptions. test('strictTraceContinuation defaults to undefined when not set', () => { NATIVE.platform = 'web'; From 91e8a36fe5b3caf8e63ef88aed2d41d25db0c2df Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 19 May 2026 09:54:54 +0200 Subject: [PATCH 4/5] 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 --- .../java/io/sentry/capacitor/SentryCapacitor.java | 12 ++++++++++++ .../SentryCapacitorPlugin.swift | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java index 9ae2ab00..2391225c 100644 --- a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java +++ b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java @@ -154,6 +154,18 @@ public void initNativeSdk(final PluginCall call) { } } + if (capOptions.has("strictTraceContinuation")) { + options.setStrictTraceContinuation(capOptions.getBool("strictTraceContinuation")); + } + if (capOptions.has("orgId")) { + Object orgIdValue = capOptions.opt("orgId"); + if (orgIdValue instanceof String) { + options.setOrgId((String) orgIdValue); + } else if (orgIdValue instanceof Number) { + options.setOrgId(String.valueOf(((Number) orgIdValue).longValue())); + } + } + options.getLogs().setEnabled(Boolean.TRUE.equals(capOptions.getBoolean("enableLogs", false))); logger.log(SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); diff --git a/ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift b/ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift index a252295a..d2dc58e2 100644 --- a/ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift +++ b/ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift @@ -158,6 +158,17 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin { options.enableAutoPerformanceTracing = enableAutoPerformanceTracing } + if let strictTraceContinuation = dict["strictTraceContinuation"] as? Bool { + options.strictTraceContinuation = strictTraceContinuation + } + if let orgId = dict["orgId"] { + if let orgIdString = orgId as? String { + options.orgId = orgIdString + } else if let orgIdNumber = orgId as? NSNumber { + options.orgId = orgIdNumber.stringValue + } + } + return options } From 607ecf7fc11a6e5228b40f03ab13adf09927b00e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 19 May 2026 09:57:53 +0200 Subject: [PATCH 5/5] 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 --- test/wrapper.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts index 60bbaeca..b739e5df 100644 --- a/test/wrapper.test.ts +++ b/test/wrapper.test.ts @@ -170,6 +170,58 @@ describe('Tests Native Wrapper', () => { ); }); + test('passes strictTraceContinuation to native SDK', async () => { + const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk'); + + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + strictTraceContinuation: true, + }); + + const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options; + expect(nativeOption?.strictTraceContinuation).toBe(true); + }); + + test('passes orgId as string to native SDK', async () => { + const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk'); + + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + orgId: '12345', + }); + + const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options; + expect(nativeOption?.orgId).toBe('12345'); + }); + + test('passes numeric orgId to native SDK', async () => { + const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk'); + + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + orgId: 12345, + }); + + const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options; + expect(nativeOption?.orgId).toBe(12345); + }); + + test('does not include strictTraceContinuation when not set', async () => { + const initNativeSdk = jest.spyOn(SentryCapacitor, 'initNativeSdk'); + + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + }); + + const nativeOption = initNativeSdk.mock.calls[0]?.[0]?.options; + expect(nativeOption?.strictTraceContinuation).toBeUndefined(); + expect(nativeOption?.orgId).toBeUndefined(); + }); + test('sets enableNative: false when dsn is undefined', async () => { await NATIVE.initNativeSdk({ dsn: undefined,