diff --git a/CHANGELOG.md b/CHANGELOG.md index 06273210b1..961459b325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#5828](https://github.com/getsentry/sentry-react-native/pull/5828)) - Support `SENTRY_ENVIRONMENT` in bare React Native builds ([#5823](https://github.com/getsentry/sentry-react-native/pull/5823)) ### Fixes diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index 66cf4a927b..2143beb795 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -389,6 +389,34 @@ describe('Tests the SDK functionality', () => { }); }); + describe('strictTraceContinuation', () => { + it('passes strictTraceContinuation option through to client options', () => { + init({ + strictTraceContinuation: true, + }); + expect(usedOptions()?.strictTraceContinuation).toBe(true); + }); + + it('passes orgId option through to client options', () => { + init({ + orgId: '12345', + }); + expect(usedOptions()?.orgId).toBe('12345'); + }); + + it('passes numeric orgId option through to client options', () => { + init({ + orgId: 12345, + }); + expect(usedOptions()?.orgId).toBe(12345); + }); + + it('defaults strictTraceContinuation to undefined when not set', () => { + init({}); + expect(usedOptions()?.strictTraceContinuation).toBeUndefined(); + }); + }); + describe('beforeBreadcrumb', () => { it('should filters out dev server breadcrumbs', () => { const devServerUrl = 'http://localhost:8081'; diff --git a/packages/core/test/strictTraceContinuation.test.ts b/packages/core/test/strictTraceContinuation.test.ts new file mode 100644 index 0000000000..e50deaf885 --- /dev/null +++ b/packages/core/test/strictTraceContinuation.test.ts @@ -0,0 +1,261 @@ +import { continueTrace, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { getDefaultTestClientOptions, TestClient } from './mocks/client'; + +describe('strictTraceContinuation', () => { + let client: TestClient; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('with matching org IDs', () => { + beforeEach(() => { + client = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@o123.ingest.sentry.io/1234', + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('continues trace when baggage org_id matches DSN org ID', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + }); + + describe('with mismatching org IDs', () => { + beforeEach(() => { + client = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@o123.ingest.sentry.io/1234', + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('starts new trace when baggage org_id does not match DSN org ID', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=456', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + }); + + describe('with orgId option override', () => { + beforeEach(() => { + client = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@o123.ingest.sentry.io/1234', + orgId: '999', + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('uses orgId option over DSN-extracted org ID', () => { + // baggage org_id=123 matches DSN but NOT the orgId option (999) + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + // Should start new trace because orgId option (999) != baggage org_id (123) + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + it('continues trace when baggage matches orgId option', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=999', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + }); + + describe('strictTraceContinuation=true', () => { + beforeEach(() => { + client = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@o123.ingest.sentry.io/1234', + strictTraceContinuation: true, + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('starts new trace when baggage has no org_id', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + it('starts new trace when SDK has no org_id but baggage does', () => { + // Use a DSN without org ID in hostname + const clientWithoutOrgId = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@sentry.example.com/1234', + strictTraceContinuation: true, + }), + ); + setCurrentClient(clientWithoutOrgId); + clientWithoutOrgId.init(); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + it('continues trace when both org IDs are missing', () => { + const clientWithoutOrgId = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@sentry.example.com/1234', + strictTraceContinuation: true, + }), + ); + setCurrentClient(clientWithoutOrgId); + clientWithoutOrgId.init(); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + }); + + describe('strictTraceContinuation=false (default)', () => { + beforeEach(() => { + client = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@o123.ingest.sentry.io/1234', + strictTraceContinuation: false, + }), + ); + setCurrentClient(client); + client.init(); + }); + + it('continues trace when baggage has no org_id', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + + it('continues trace when SDK has no org_id but baggage does', () => { + const clientWithoutOrgId = new TestClient( + getDefaultTestClientOptions({ + tracesSampleRate: 1.0, + dsn: 'https://abc@sentry.example.com/1234', + strictTraceContinuation: false, + }), + ); + setCurrentClient(clientWithoutOrgId); + clientWithoutOrgId.init(); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + + it('still starts new trace when org IDs mismatch', () => { + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=456', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + }); +}); diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index cc7b79c0d9..65880888ff 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -377,6 +377,38 @@ describe('Tests Native Wrapper', () => { expect(initParameter.enableLogs).toBe(expectedEnableLogs); expect(initParameter.logsOrigin).toBeUndefined(); }); + + test('passes strictTraceContinuation option to native SDK', async () => { + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + autoInitializeNativeSdk: true, + strictTraceContinuation: true, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + + expect(RNSentry.initNativeSdk).toHaveBeenCalled(); + const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction).mock.calls[0][0]; + expect(initParameter.strictTraceContinuation).toBe(true); + }); + + test('passes orgId option to native SDK', async () => { + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + autoInitializeNativeSdk: true, + orgId: '12345', + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + + expect(RNSentry.initNativeSdk).toHaveBeenCalled(); + const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction).mock.calls[0][0]; + expect(initParameter.orgId).toBe('12345'); + }); }); describe('sendEnvelope', () => {