From 3f399990edb0ef9e322d0826afc2f660138020c8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 17 Mar 2026 14:12:22 +0100 Subject: [PATCH 1/3] 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/3] 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/3] 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';