diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3e22ad..5a4307b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#1166](https://github.com/getsentry/sentry-capacitor/pull/1166)) + ### Dependencies - Bump Android SDK from v8.35.0 to v8.41.0 ([#1247](https://github.com/getsentry/sentry-capacitor/pull/1247)) 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 } diff --git a/src/nativeOptions.ts b/src/nativeOptions.ts index 99416380..af428f5c 100644 --- a/src/nativeOptions.ts +++ b/src/nativeOptions.ts @@ -42,6 +42,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/src/options.ts b/src/options.ts index 8f1ed1b6..ae7de96f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -73,6 +73,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/nativeOptions.test.ts b/test/nativeOptions.test.ts index b861f192..c98d98f7 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 a05f09cc..5f98a1b7 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -206,6 +206,57 @@ 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); + }); + }); + + // 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'; + 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(); 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,