From 9b7787397a25f6292c0f325ae7bf95ba0457912d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 21 Dec 2022 23:47:09 +0100 Subject: [PATCH 01/16] feat(contexts): Sync native iOS contexts to JS --- ios/RNSentry.m | 26 ++-- src/js/definitions.ts | 2 +- src/js/integrations/devicecontext.ts | 71 +++++++-- test/integrations/devicecontext.test.ts | 194 ++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 28 deletions(-) create mode 100644 test/integrations/devicecontext.test.ts diff --git a/ios/RNSentry.m b/ios/RNSentry.m index 9c9eed2110..b0ab7e0d2e 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -171,33 +171,27 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event rejecter:(RCTPromiseRejectBlock)reject) { NSLog(@"Bridge call to: deviceContexts"); - NSMutableDictionary *contexts = [NSMutableDictionary new]; + __block NSMutableDictionary *contexts; // Temp work around until sorted out this API in sentry-cocoa. // TODO: If the callback isnt' executed the promise wouldn't be resolved. [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - NSDictionary *serializedScope = [scope serialize]; - // Scope serializes as 'context' instead of 'contexts' as it does for the event. - NSDictionary *tempContexts = [serializedScope valueForKey:@"context"]; - - NSMutableDictionary *user = [NSMutableDictionary new]; - - NSDictionary *tempUser = [serializedScope valueForKey:@"user"]; - if (tempUser != nil) { - [user addEntriesFromDictionary:[tempUser valueForKey:@"user"]]; - } else { - [user setValue:PrivateSentrySDKOnly.installationID forKey:@"id"]; + NSDictionary *serializedScope = [scope serialize]; + contexts = [serializedScope mutableCopy]; + + NSDictionary *user = [contexts valueForKey:@"user"]; + if (user == nil) { + [contexts + setValue:@{ @"id": PrivateSentrySDKOnly.installationID } + forKey:@"user"]; } - [contexts setValue:user forKey:@"user"]; - if (tempContexts != nil) { - [contexts setValue:tempContexts forKey:@"context"]; - } if (PrivateSentrySDKOnly.options.debug) { NSData *data = [NSJSONSerialization dataWithJSONObject:contexts options:0 error:nil]; NSString *debugContext = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Contexts: %@", debugContext); } }]; + resolve(contexts); } diff --git a/src/js/definitions.ts b/src/js/definitions.ts index 4058148b71..3579b6567c 100644 --- a/src/js/definitions.ts +++ b/src/js/definitions.ts @@ -21,7 +21,7 @@ export type NativeReleaseResponse = { }; export type NativeDeviceContextsResponse = { - [key: string]: Record; + [key: string]: Record | undefined; }; export interface NativeScreenshot { diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index 00fc67864f..dc5122c89e 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,7 +1,7 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Contexts, Event, Integration } from '@sentry/types'; +import { Breadcrumb, Event, EventProcessor, Hub, Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { NativeDeviceContextsResponse } from '../definitions'; import { NATIVE } from '../wrapper'; /** Load device context from native. */ @@ -19,26 +19,75 @@ export class DeviceContext implements Integration { /** * @inheritDoc */ - public setupOnce(): void { + public setupOnce( + addGlobalEventProcessor: (callback: EventProcessor) => void, + getCurrentHub: () => Hub, + ): void { addGlobalEventProcessor(async (event: Event) => { const self = getCurrentHub().getIntegration(DeviceContext); if (!self) { return event; } + let contexts: NativeDeviceContextsResponse | null = null; try { - const contexts = await NATIVE.fetchNativeDeviceContexts(); + contexts = await NATIVE.fetchNativeDeviceContexts(); + } catch (e) { + logger.log(`Failed to get device context from native: ${e}`); + } + + if (!contexts) { + return event; + } + + const nativeUser = contexts['user'] as Event['user']; + if (!event.user && nativeUser) { + event.user = nativeUser; + } + + const nativeContext = contexts['context'] as Event['contexts']; + if (nativeContext) { + event.contexts = { ...nativeContext, ...event.contexts }; + } - const context = contexts['context'] as Contexts ?? {}; - const user = contexts['user'] ?? {}; + const nativeTags = contexts['tags'] as Event['tags']; + if (nativeTags) { + event.tags = { ...nativeTags, ...event.tags }; + } + + const nativeExtra = contexts['extra'] as Event['extra']; + if (nativeExtra) { + event.extra = { ...nativeExtra, ...event.extra }; + } + + const nativeFingerprint = contexts['fingerprint'] as Event['fingerprint']; + if (nativeFingerprint) { + event.fingerprint = (event.fingerprint ?? []).concat( + nativeFingerprint.filter((item) => (event.fingerprint ?? []).indexOf(item) < 0), + ) + } - event.contexts = { ...context, ...event.contexts }; + const nativeLevel = contexts['level'] as Event['level']; + if (!event.level && nativeLevel) { + event.level = nativeLevel; + } + + const nativeEnvironment = contexts['environment'] as Event['environment']; + if (!event.environment && nativeEnvironment) { + event.environment = nativeEnvironment; + } - if (!event.user) { - event.user = { ...user }; + const nativeBreadcrumbs = contexts['breadcrumbs'] as Event['breadcrumbs']; + if (nativeBreadcrumbs) { + event.breadcrumbs = event.breadcrumbs ?? [] + for (const breadcrumb of nativeBreadcrumbs ?? []) { + const equals = (i: Breadcrumb): boolean => JSON.stringify(i) === JSON.stringify(breadcrumb) + const exists = event.breadcrumbs.findIndex(equals) >= 0; + if (!exists) { + event.breadcrumbs.push(breadcrumb); + } } - } catch (e) { - logger.log(`Failed to get device context from native: ${e}`); + event.breadcrumbs = event.breadcrumbs.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)); } return event; diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts new file mode 100644 index 0000000000..dbff474ec2 --- /dev/null +++ b/test/integrations/devicecontext.test.ts @@ -0,0 +1,194 @@ +import { Hub } from '@sentry/core'; +import { Event } from '@sentry/types'; + +import { NativeDeviceContextsResponse } from '../../src/js/definitions'; +import { DeviceContext } from '../../src/js/integrations'; +import { NATIVE } from '../../src/js/wrapper'; + +jest.mock('../../src/js/wrapper'); + +describe('Device Context Integration', () => { + let integration: DeviceContext; + + const mockGetCurrentHub = () => ({ + getIntegration: () => integration, + } as unknown as Hub); + + beforeEach(() => { + integration = new DeviceContext(); + }); + + it('add native user', async () => { + (await executeIntegrationWith({ + nativeContexts: { user: { id: 'native-user' } }, + })).expectEvent.toStrictEqualToNativeContexts(); + }); + + it('do not overwrite event user', async () => { + (await executeIntegrationWith({ + nativeContexts: { user: { id: 'native-user' } }, + mockEvent: { user: { id: 'event-user' } }, + })).expectEvent.toStrictEqualMockEvent(); + }); + + it('merge event and native contexts', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { context: { duplicate: { context: 'value' }, native: { context: 'value' } } }, + mockEvent: { contexts: { duplicate: { context: 'value' }, event: { context: 'value' } } }, + }); + expect(processedEvent).toStrictEqual({ + contexts: { + duplicate: { context: 'value' }, + native: { context: 'value' }, + event: { context: 'value' }, + }, + }); + }); + + it('merge native tags', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { tags: { duplicate: 'tag', native: 'tag' } }, + mockEvent: { tags: { duplicate: 'tag', event: 'tag' } }, + }); + expect(processedEvent).toStrictEqual({ + tags: { + duplicate: 'tag', + native: 'tag', + event: 'tag', + }, + }); + }); + + it('merge native extra', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { extra: { duplicate: 'extra', native: 'extra' } }, + mockEvent: { extra: { duplicate: 'extra', event: 'extra' } }, + }); + expect(processedEvent).toStrictEqual({ + extra: { + duplicate: 'extra', + native: 'extra', + event: 'extra', + }, + }); + }); + + it('merge fingerprints', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { fingerprint: ['duplicate-fingerprint', 'native-fingerprint'] }, + mockEvent: { fingerprint: ['duplicate-fingerprint', 'event-fingerprint'] }, + }); + expect(processedEvent).toStrictEqual({ + fingerprint: ['duplicate-fingerprint', 'event-fingerprint', 'native-fingerprint'], + }); + }); + + it('add native level', async () => { + (await executeIntegrationWith({ + nativeContexts: { level: 'native-level' }, + })).expectEvent.toStrictEqualToNativeContexts(); + }); + + it('do not overwrite event level', async () => { + (await executeIntegrationWith({ + nativeContexts: { level: 'native-level' }, + mockEvent: { level: 'info' }, + })).expectEvent.toStrictEqualMockEvent(); + }); + + it('add native environment', async () => { + (await executeIntegrationWith({ + nativeContexts: { environment: 'native-environment' }, + })).expectEvent.toStrictEqualToNativeContexts(); + }); + + it('do not overwrite event environment', async () => { + (await executeIntegrationWith({ + nativeContexts: { environment: 'native-environment' }, + mockEvent: { environment: 'event-environment' }, + })).expectEvent.toStrictEqualMockEvent(); + }); + + it('merge breadcrumbs', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] }, + mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] }, + }); + expect(processedEvent).toStrictEqual({ + breadcrumbs: [ + { message: 'duplicate-breadcrumb' }, + { message: 'event-breadcrumb' }, + { message: 'native-breadcrumb' }, + ], + }); + }); + + it('asc oder breadcrumbs by timestamp', async () => { + const { processedEvent } = await executeIntegrationWith({ + nativeContexts: { + breadcrumbs: [ + { message: 'breadcrumb-0' }, + { message: 'breadcrumb-2', timestamp: 2 }, + { message: 'breadcrumb-1' }, + ] + }, + mockEvent: { + breadcrumbs: [ + { message: 'breadcrumb-4', timestamp: 4 }, + { message: 'breadcrumb-3', timestamp: 3 }, + ] + }, + }); + expect(processedEvent).toStrictEqual({ + breadcrumbs: [ + { message: 'breadcrumb-0' }, + { message: 'breadcrumb-1' }, + { message: 'breadcrumb-2', timestamp: 2 }, + { message: 'breadcrumb-3', timestamp: 3 }, + { message: 'breadcrumb-4', timestamp: 4 }, + ], + }); + }); + + async function executeIntegrationWith({ nativeContexts, mockEvent }: { + nativeContexts: Record; + mockEvent?: Event; + }): Promise<{ + processedEvent: Event | null; + expectEvent: { + toStrictEqualToNativeContexts: () => void; + toStrictEqualMockEvent: () => void; + }; + }> { + (NATIVE.fetchNativeDeviceContexts as jest.MockedFunction) + .mockImplementation( + () => Promise.resolve(nativeContexts as NativeDeviceContextsResponse), + ); + const originalNativeContexts = { ...nativeContexts }; + const originalMockEvent = { ...mockEvent }; + const processedEvent = await executeIntegrationFor(mockEvent ?? {}); + return { + processedEvent, + expectEvent: { + toStrictEqualToNativeContexts: () => expect(processedEvent).toStrictEqual(originalNativeContexts), + toStrictEqualMockEvent: () => expect(processedEvent).toStrictEqual(originalMockEvent), + }, + }; + } + + function executeIntegrationFor(mockedEvent: Event): Promise { + return new Promise((resolve, reject) => { + integration.setupOnce( + async (eventProcessor) => { + try { + const processedEvent = await eventProcessor(mockedEvent, {}); + resolve(processedEvent); + } catch (e) { + reject(e); + } + }, + mockGetCurrentHub, + ); + }); + } +}); From 60724381e96813179fd0ace8b10bddf569bd397e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 21 Dec 2022 23:54:28 +0100 Subject: [PATCH 02/16] Add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a23679c0..6a03429294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. + ### Dependencies - Bump Cocoa SDK from v7.31.3 to v7.31.4 ([#2699](https://github.com/getsentry/sentry-react-native/pull/2699)) From a99a5363b99951b1455982e3c0e36e369391a539 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 21 Dec 2022 23:56:08 +0100 Subject: [PATCH 03/16] Clearly show event values are preferred over native --- test/integrations/devicecontext.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts index dbff474ec2..5bcdf78dba 100644 --- a/test/integrations/devicecontext.test.ts +++ b/test/integrations/devicecontext.test.ts @@ -33,12 +33,12 @@ describe('Device Context Integration', () => { it('merge event and native contexts', async () => { const { processedEvent } = await executeIntegrationWith({ - nativeContexts: { context: { duplicate: { context: 'value' }, native: { context: 'value' } } }, - mockEvent: { contexts: { duplicate: { context: 'value' }, event: { context: 'value' } } }, + nativeContexts: { context: { duplicate: { context: 'native-value' }, native: { context: 'value' } } }, + mockEvent: { contexts: { duplicate: { context: 'event-value' }, event: { context: 'value' } } }, }); expect(processedEvent).toStrictEqual({ contexts: { - duplicate: { context: 'value' }, + duplicate: { context: 'event-value' }, native: { context: 'value' }, event: { context: 'value' }, }, @@ -47,12 +47,12 @@ describe('Device Context Integration', () => { it('merge native tags', async () => { const { processedEvent } = await executeIntegrationWith({ - nativeContexts: { tags: { duplicate: 'tag', native: 'tag' } }, - mockEvent: { tags: { duplicate: 'tag', event: 'tag' } }, + nativeContexts: { tags: { duplicate: 'native-tag', native: 'tag' } }, + mockEvent: { tags: { duplicate: 'event-tag', event: 'tag' } }, }); expect(processedEvent).toStrictEqual({ tags: { - duplicate: 'tag', + duplicate: 'event-tag', native: 'tag', event: 'tag', }, @@ -61,12 +61,12 @@ describe('Device Context Integration', () => { it('merge native extra', async () => { const { processedEvent } = await executeIntegrationWith({ - nativeContexts: { extra: { duplicate: 'extra', native: 'extra' } }, - mockEvent: { extra: { duplicate: 'extra', event: 'extra' } }, + nativeContexts: { extra: { duplicate: 'native-extra', native: 'extra' } }, + mockEvent: { extra: { duplicate: 'event-extra', event: 'extra' } }, }); expect(processedEvent).toStrictEqual({ extra: { - duplicate: 'extra', + duplicate: 'event-extra', native: 'extra', event: 'extra', }, From 4cb42bf3e55d2fbd94da41cf91b405db58ada00a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 21 Dec 2022 23:56:28 +0100 Subject: [PATCH 04/16] Add todo for breadcrumbs timestamps --- src/js/integrations/devicecontext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index dc5122c89e..2e52d5392c 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -87,6 +87,7 @@ export class DeviceContext implements Integration { event.breadcrumbs.push(breadcrumb); } } + // TODO: native breadcrumbs timestamp is ISO string not a number event.breadcrumbs = event.breadcrumbs.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)); } From 13b8f336bdf541f526a31a92efd03b1c56d15a01 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 3 Jan 2023 14:47:44 +0100 Subject: [PATCH 05/16] Add breadcrumb factory and equals function --- src/js/breadcrumb.ts | 55 +++++++++++++ src/js/integrations/devicecontext.ts | 14 ++-- test/breadcrumb.test.ts | 105 ++++++++++++++++++++++++ test/integrations/devicecontext.test.ts | 23 +++--- 4 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 src/js/breadcrumb.ts create mode 100644 test/breadcrumb.test.ts diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts new file mode 100644 index 0000000000..c65c3c037b --- /dev/null +++ b/src/js/breadcrumb.ts @@ -0,0 +1,55 @@ +import { Breadcrumb } from '@sentry/types'; +import { normalize, severityLevelFromString } from '@sentry/utils'; + +type BreadcrumbCandidate = { + [K in keyof Partial]: unknown; +} + +/** + * Convert plain object to a valid Breadcrumb + */ +export function breadcrumbFromObject(candidate: BreadcrumbCandidate) { + const breadcrumb: Breadcrumb = {}; + + if (typeof candidate.type === 'string') { + breadcrumb.type = candidate.type; + } + if (typeof candidate.level === 'string') { + breadcrumb.level = severityLevelFromString(candidate.level); + } + if (typeof candidate.event_id === 'string') { + breadcrumb.event_id = candidate.event_id; + } + if (typeof candidate.category === 'string') { + breadcrumb.category = candidate.category; + } + if (typeof candidate.message === 'string') { + breadcrumb.message = candidate.message; + } + if (typeof candidate.data === 'object' && candidate.data !== null) { + breadcrumb.data = candidate.data; + } + if (typeof candidate.timestamp === 'string') { + const timestamp = Date.parse(candidate.timestamp) + if (!isNaN(timestamp)) { + breadcrumb.timestamp = timestamp; + } + } + + return breadcrumb; +} + +/** + * Compares two breadcrumbs and returns true if they are equal + * Warning: This function does not check timestamp + */ +export function breadcrumbEquals(breadcrumb: Breadcrumb, other: Breadcrumb): boolean { + return ( + breadcrumb.category === other.category && + JSON.stringify(normalize(breadcrumb.data)) === JSON.stringify(normalize(other.data)) && + breadcrumb.event_id === other.event_id && + breadcrumb.level === other.level && + breadcrumb.message === other.message && + breadcrumb.type === other.type + ); +} diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index 2e52d5392c..c600be9bd6 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,5 +1,6 @@ import { Breadcrumb, Event, EventProcessor, Hub, Integration } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, severityLevelFromString } from '@sentry/utils'; +import { breadcrumbEquals, breadcrumbFromObject } from '../breadcrumb'; import { NativeDeviceContextsResponse } from '../definitions'; import { NATIVE } from '../wrapper'; @@ -67,7 +68,9 @@ export class DeviceContext implements Integration { ) } - const nativeLevel = contexts['level'] as Event['level']; + const nativeLevel = typeof contexts['level'] === 'string' + ? severityLevelFromString(contexts['level']) + : undefined; if (!event.level && nativeLevel) { event.level = nativeLevel; } @@ -77,17 +80,18 @@ export class DeviceContext implements Integration { event.environment = nativeEnvironment; } - const nativeBreadcrumbs = contexts['breadcrumbs'] as Event['breadcrumbs']; + const nativeBreadcrumbs = Array.isArray(contexts['breadcrumbs']) + ? contexts['breadcrumbs'].map(breadcrumbFromObject) + : undefined; if (nativeBreadcrumbs) { event.breadcrumbs = event.breadcrumbs ?? [] for (const breadcrumb of nativeBreadcrumbs ?? []) { - const equals = (i: Breadcrumb): boolean => JSON.stringify(i) === JSON.stringify(breadcrumb) + const equals = (b: Breadcrumb): boolean => breadcrumbEquals(b, breadcrumb); const exists = event.breadcrumbs.findIndex(equals) >= 0; if (!exists) { event.breadcrumbs.push(breadcrumb); } } - // TODO: native breadcrumbs timestamp is ISO string not a number event.breadcrumbs = event.breadcrumbs.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)); } diff --git a/test/breadcrumb.test.ts b/test/breadcrumb.test.ts new file mode 100644 index 0000000000..4456e3e985 --- /dev/null +++ b/test/breadcrumb.test.ts @@ -0,0 +1,105 @@ +import { Breadcrumb } from '@sentry/types'; +import { breadcrumbEquals, breadcrumbFromObject } from '../src/js/breadcrumb'; + +describe('Breadcrumb', () => { + describe('breadcrumbFromObject', () => { + it('convert a plain object to a valid Breadcrumb', () => { + const candidate = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: '2020-01-01T00:00:00.000Z', + }; + const breadcrumb = breadcrumbFromObject(candidate); + expect(breadcrumb).toEqual({ + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }); + }); + + it('convert plain object with invalid timestamp to a valid Breadcrumb', () => { + const candidate = { + type: 'test', + level: 'info', + timestamp: 'invalid', + }; + const breadcrumb = breadcrumbFromObject(candidate); + expect(breadcrumb).toEqual({ + type: 'test', + level: 'info', + }); + }); + + it('convert empty object to a valid Breadcrumb', () => { + const candidate = {}; + const breadcrumb = breadcrumbFromObject(candidate); + expect(breadcrumb).toEqual({}); + }); + }); + + describe('breadcrumbEquals', () => { + it('returns true if two breadcrumbs with different timestamp are equal', () => { + const breadcrumb: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + const other: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800001, + }; + expect(breadcrumbEquals(breadcrumb, other)).toBe(true); + }); + + it('returns false if breadcrumbs message is different', () => { + const breadcrumb: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + const other: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test2', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + expect(breadcrumbEquals(breadcrumb, other)).toBe(false); + }); + }); +}); diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts index 5bcdf78dba..0b54d734c3 100644 --- a/test/integrations/devicecontext.test.ts +++ b/test/integrations/devicecontext.test.ts @@ -1,5 +1,5 @@ import { Hub } from '@sentry/core'; -import { Event } from '@sentry/types'; +import { Event, SeverityLevel } from '@sentry/types'; import { NativeDeviceContextsResponse } from '../../src/js/definitions'; import { DeviceContext } from '../../src/js/integrations'; @@ -85,7 +85,7 @@ describe('Device Context Integration', () => { it('add native level', async () => { (await executeIntegrationWith({ - nativeContexts: { level: 'native-level' }, + nativeContexts: { level: 'fatal' }, })).expectEvent.toStrictEqualToNativeContexts(); }); @@ -127,25 +127,28 @@ describe('Device Context Integration', () => { const { processedEvent } = await executeIntegrationWith({ nativeContexts: { breadcrumbs: [ - { message: 'breadcrumb-0' }, - { message: 'breadcrumb-2', timestamp: 2 }, { message: 'breadcrumb-1' }, + { message: 'breadcrumb-2', timestamp: '2023-10-01T11:00:00.000Z' }, + { message: 'breadcrumb-0' }, ] }, mockEvent: { breadcrumbs: [ - { message: 'breadcrumb-4', timestamp: 4 }, - { message: 'breadcrumb-3', timestamp: 3 }, + { message: 'breadcrumb-4', timestamp: Date.parse('2025-10-01T11:00:00.000Z') }, + { message: 'breadcrumb-3', timestamp: Date.parse('2024-10-01T11:00:00.000Z') }, + { message: 'breadcrumb-5' }, ] }, }); + expect(processedEvent).toStrictEqual({ breadcrumbs: [ - { message: 'breadcrumb-0' }, + { message: 'breadcrumb-5' }, { message: 'breadcrumb-1' }, - { message: 'breadcrumb-2', timestamp: 2 }, - { message: 'breadcrumb-3', timestamp: 3 }, - { message: 'breadcrumb-4', timestamp: 4 }, + { message: 'breadcrumb-0' }, + { message: 'breadcrumb-2', timestamp: Date.parse('2023-10-01T11:00:00.000Z') }, + { message: 'breadcrumb-3', timestamp: Date.parse('2024-10-01T11:00:00.000Z') }, + { message: 'breadcrumb-4', timestamp: Date.parse('2025-10-01T11:00:00.000Z') }, ], }); }); From 51e22424c8d38748e9b99e8d414c8456f49da764 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 3 Jan 2023 14:51:31 +0100 Subject: [PATCH 06/16] fix lint --- src/js/breadcrumb.ts | 2 +- src/js/integrations/devicecontext.ts | 3 ++- test/breadcrumb.test.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts index c65c3c037b..9794748c47 100644 --- a/src/js/breadcrumb.ts +++ b/src/js/breadcrumb.ts @@ -8,7 +8,7 @@ type BreadcrumbCandidate = { /** * Convert plain object to a valid Breadcrumb */ -export function breadcrumbFromObject(candidate: BreadcrumbCandidate) { +export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb { const breadcrumb: Breadcrumb = {}; if (typeof candidate.type === 'string') { diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index c600be9bd6..21513331c4 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,7 +1,8 @@ +/* eslint-disable complexity */ import { Breadcrumb, Event, EventProcessor, Hub, Integration } from '@sentry/types'; import { logger, severityLevelFromString } from '@sentry/utils'; -import { breadcrumbEquals, breadcrumbFromObject } from '../breadcrumb'; +import { breadcrumbEquals, breadcrumbFromObject } from '../breadcrumb'; import { NativeDeviceContextsResponse } from '../definitions'; import { NATIVE } from '../wrapper'; diff --git a/test/breadcrumb.test.ts b/test/breadcrumb.test.ts index 4456e3e985..1ac5226174 100644 --- a/test/breadcrumb.test.ts +++ b/test/breadcrumb.test.ts @@ -1,4 +1,5 @@ import { Breadcrumb } from '@sentry/types'; + import { breadcrumbEquals, breadcrumbFromObject } from '../src/js/breadcrumb'; describe('Breadcrumb', () => { From 2fc009dda15f33cb8d83d9efd19e9a066ea237db Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 3 Jan 2023 15:21:10 +0100 Subject: [PATCH 07/16] Make breadcrumb comparison more loose --- src/js/breadcrumb.ts | 4 +--- test/breadcrumb.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts index 9794748c47..05dd89dee0 100644 --- a/src/js/breadcrumb.ts +++ b/src/js/breadcrumb.ts @@ -41,14 +41,12 @@ export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb /** * Compares two breadcrumbs and returns true if they are equal - * Warning: This function does not check timestamp + * Warning: This function does not check timestamp and level, data */ export function breadcrumbEquals(breadcrumb: Breadcrumb, other: Breadcrumb): boolean { return ( breadcrumb.category === other.category && - JSON.stringify(normalize(breadcrumb.data)) === JSON.stringify(normalize(other.data)) && breadcrumb.event_id === other.event_id && - breadcrumb.level === other.level && breadcrumb.message === other.message && breadcrumb.type === other.type ); diff --git a/test/breadcrumb.test.ts b/test/breadcrumb.test.ts index 1ac5226174..4a93275f72 100644 --- a/test/breadcrumb.test.ts +++ b/test/breadcrumb.test.ts @@ -77,6 +77,58 @@ describe('Breadcrumb', () => { expect(breadcrumbEquals(breadcrumb, other)).toBe(true); }); + it('returns true if two breadcrumbs with different level are equal', () => { + const breadcrumb: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + const other: Breadcrumb = { + type: 'test', + level: 'warning', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + expect(breadcrumbEquals(breadcrumb, other)).toBe(true); + }); + + it('returns true if two breadcrumbs with different data are equal', () => { + const breadcrumb: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test', + }, + timestamp: 1577836800000, + }; + const other: Breadcrumb = { + type: 'test', + level: 'info', + event_id: '1234', + category: 'test', + message: 'test', + data: { + test: 'test2', + }, + timestamp: 1577836800000, + }; + expect(breadcrumbEquals(breadcrumb, other)).toBe(true); + }); + it('returns false if breadcrumbs message is different', () => { const breadcrumb: Breadcrumb = { type: 'test', From 2acead79bab1c8bb91d0c384b14af0f01a523120 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 3 Jan 2023 15:21:50 +0100 Subject: [PATCH 08/16] fix lint --- src/js/breadcrumb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts index 05dd89dee0..4ca87b6ed8 100644 --- a/src/js/breadcrumb.ts +++ b/src/js/breadcrumb.ts @@ -1,5 +1,5 @@ import { Breadcrumb } from '@sentry/types'; -import { normalize, severityLevelFromString } from '@sentry/utils'; +import { severityLevelFromString } from '@sentry/utils'; type BreadcrumbCandidate = { [K in keyof Partial]: unknown; From ffe362a63a0adcafb795e2a0b7e0b4fc833d30e2 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 3 Jan 2023 15:28:43 +0100 Subject: [PATCH 09/16] Add pr id --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a12b144f7..d0bae9591b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. +- Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. ([#2713](https://github.com/getsentry/sentry-react-native/pull/2713)) ### Fixes From 0ec3efc15e261ff3a93b9789f66a97deb12c4adb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 4 Jan 2023 14:22:23 +0100 Subject: [PATCH 10/16] Use only native breadcrumbs is available --- CHANGELOG.md | 3 + .../io/sentry/react/RNSentryModuleImpl.java | 8 +- ios/RNSentry.mm | 4 +- sample/src/App.tsx | 2 +- src/js/NativeRNSentry.ts | 29 ++++- src/js/breadcrumb.ts | 17 +-- src/js/integrations/devicecontext.ts | 42 +++---- src/js/scope.ts | 9 +- src/js/wrapper.ts | 5 - test/breadcrumb.test.ts | 108 +----------------- test/integrations/devicecontext.test.ts | 35 +----- test/scope.test.ts | 25 ++++ test/wrapper.test.ts | 4 - 13 files changed, 92 insertions(+), 199 deletions(-) create mode 100644 test/scope.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d933e8bab..522da4fe8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ ### Features - Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. ([#2713](https://github.com/getsentry/sentry-react-native/pull/2713)) + - `breadcrumb.level` value `log` is transformed to `debug` when syncing with native layers. + - Deprecated `breadcrumb.level` value `critical` is removed and not transformed. + - Default `breadcrumb.level` is `error` ### Breaking changes diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 4f678a5d7c..e34cf749fd 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -444,13 +444,13 @@ public void addBreadcrumb(final ReadableMap breadcrumb) { case "debug": breadcrumbInstance.setLevel(SentryLevel.DEBUG); break; - case "error": - breadcrumbInstance.setLevel(SentryLevel.ERROR); - break; case "info": - default: breadcrumbInstance.setLevel(SentryLevel.INFO); break; + case "error": + default: + breadcrumbInstance.setLevel(SentryLevel.ERROR); + break; } } diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 2e38d3cd2f..a080158ff6 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -377,12 +377,10 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event [breadcrumbInstance setLevel:sentryLevel]; [breadcrumbInstance setCategory:breadcrumb[@"category"]]; - [breadcrumbInstance setType:breadcrumb[@"type"]]; - [breadcrumbInstance setMessage:breadcrumb[@"message"]]; - [breadcrumbInstance setData:breadcrumb[@"data"]]; + [breadcrumbInstance setTimestamp:[NSDate dateWithTimeIntervalSince1970:[breadcrumb[@"timestamp"] doubleValue]]]; [scope addBreadcrumb:breadcrumbInstance]; }]; diff --git a/sample/src/App.tsx b/sample/src/App.tsx index e620a6bae8..f4df6bdfe8 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -28,7 +28,7 @@ Sentry.init({ dsn: SENTRY_INTERNAL_DSN, debug: true, beforeSend: (e, hint) => { - console.log('Event beforeSend:', e, 'hint:', hint); + // console.log('Event beforeSend:', e, 'hint:', hint); return e; }, // This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted. diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index 64bcefebb6..2efeff9fc0 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -52,8 +52,35 @@ export type NativeReleaseResponse = { version: string; }; +/** + * This type describes serialized scope from sentry-cocoa. (This is not used for Android) + * https://github.com/getsentry/sentry-cocoa/blob/master/Sources/Sentry/SentryScope.m + */ export type NativeDeviceContextsResponse = { - [key: string]: Record; + [key: string]: unknown; + tags?: Record; + extra?: Record; + context?: Record>; + user?: { + userId?: string; + email?: string; + username?: string; + ipAddress?: string; + segment?: string; + data?: Record; + }; + dist?: string; + environment?: string; + fingerprint?: string[]; + level?: string; + breadcrumbs?: { + level?: string; + timestamp?: string; + category?: string; + type?: string; + message?: string; + data?: Record; + }[]; }; export type NativeScreenshot = { diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts index 4ca87b6ed8..2432628171 100644 --- a/src/js/breadcrumb.ts +++ b/src/js/breadcrumb.ts @@ -1,6 +1,8 @@ -import { Breadcrumb } from '@sentry/types'; +import { Breadcrumb, SeverityLevel } from '@sentry/types'; import { severityLevelFromString } from '@sentry/utils'; +export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'error'; + type BreadcrumbCandidate = { [K in keyof Partial]: unknown; } @@ -38,16 +40,3 @@ export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb return breadcrumb; } - -/** - * Compares two breadcrumbs and returns true if they are equal - * Warning: This function does not check timestamp and level, data - */ -export function breadcrumbEquals(breadcrumb: Breadcrumb, other: Breadcrumb): boolean { - return ( - breadcrumb.category === other.category && - breadcrumb.event_id === other.event_id && - breadcrumb.message === other.message && - breadcrumb.type === other.type - ); -} diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index 21513331c4..f8650330ba 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,9 +1,9 @@ /* eslint-disable complexity */ -import { Breadcrumb, Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; import { logger, severityLevelFromString } from '@sentry/utils'; -import { breadcrumbEquals, breadcrumbFromObject } from '../breadcrumb'; -import { NativeDeviceContextsResponse } from '../definitions'; +import { breadcrumbFromObject } from '../breadcrumb'; +import { NativeDeviceContextsResponse } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; /** Load device context from native. */ @@ -31,69 +31,61 @@ export class DeviceContext implements Integration { return event; } - let contexts: NativeDeviceContextsResponse | null = null; + let native: NativeDeviceContextsResponse | null = null; try { - contexts = await NATIVE.fetchNativeDeviceContexts(); + native = await NATIVE.fetchNativeDeviceContexts(); } catch (e) { logger.log(`Failed to get device context from native: ${e}`); } - if (!contexts) { + if (!native) { return event; } - const nativeUser = contexts['user'] as Event['user']; + const nativeUser = native.user; if (!event.user && nativeUser) { event.user = nativeUser; } - const nativeContext = contexts['context'] as Event['contexts']; + const nativeContext = native.context; if (nativeContext) { event.contexts = { ...nativeContext, ...event.contexts }; } - const nativeTags = contexts['tags'] as Event['tags']; + const nativeTags = native.tags; if (nativeTags) { event.tags = { ...nativeTags, ...event.tags }; } - const nativeExtra = contexts['extra'] as Event['extra']; + const nativeExtra = native.extra; if (nativeExtra) { event.extra = { ...nativeExtra, ...event.extra }; } - const nativeFingerprint = contexts['fingerprint'] as Event['fingerprint']; + const nativeFingerprint = native.fingerprint; if (nativeFingerprint) { event.fingerprint = (event.fingerprint ?? []).concat( nativeFingerprint.filter((item) => (event.fingerprint ?? []).indexOf(item) < 0), ) } - const nativeLevel = typeof contexts['level'] === 'string' - ? severityLevelFromString(contexts['level']) + const nativeLevel = typeof native['level'] === 'string' + ? severityLevelFromString(native['level']) : undefined; if (!event.level && nativeLevel) { event.level = nativeLevel; } - const nativeEnvironment = contexts['environment'] as Event['environment']; + const nativeEnvironment = native['environment']; if (!event.environment && nativeEnvironment) { event.environment = nativeEnvironment; } - const nativeBreadcrumbs = Array.isArray(contexts['breadcrumbs']) - ? contexts['breadcrumbs'].map(breadcrumbFromObject) + const nativeBreadcrumbs = Array.isArray(native['breadcrumbs']) + ? native['breadcrumbs'].map(breadcrumbFromObject) : undefined; if (nativeBreadcrumbs) { - event.breadcrumbs = event.breadcrumbs ?? [] - for (const breadcrumb of nativeBreadcrumbs ?? []) { - const equals = (b: Breadcrumb): boolean => breadcrumbEquals(b, breadcrumb); - const exists = event.breadcrumbs.findIndex(equals) >= 0; - if (!exists) { - event.breadcrumbs.push(breadcrumb); - } - } - event.breadcrumbs = event.breadcrumbs.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)); + event.breadcrumbs = nativeBreadcrumbs; } return event; diff --git a/src/js/scope.ts b/src/js/scope.ts index 75ca713396..d7254b8213 100644 --- a/src/js/scope.ts +++ b/src/js/scope.ts @@ -1,6 +1,7 @@ import { Scope } from '@sentry/core'; import { Attachment, Breadcrumb, User } from '@sentry/types'; +import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb'; import { NATIVE } from './wrapper'; /** @@ -58,8 +59,12 @@ export class ReactNativeScope extends Scope { * @inheritDoc */ public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { - NATIVE.addBreadcrumb(breadcrumb); - return super.addBreadcrumb(breadcrumb, maxBreadcrumbs); + const mergedBreadcrumb = { + ...breadcrumb, + level: breadcrumb.level || DEFAULT_BREADCRUMB_LEVEL, + }; + NATIVE.addBreadcrumb(mergedBreadcrumb); + return super.addBreadcrumb(mergedBreadcrumb, maxBreadcrumbs); } /** diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 96e25db2b1..03e9d06ee1 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -566,11 +566,6 @@ export const NATIVE: SentryNativeWrapper = { if (level == 'log' as SeverityLevel) { return 'debug' as SeverityLevel; } - else if (level == 'critical' as SeverityLevel) { - return 'fatal' as SeverityLevel; - } - - return level; }, diff --git a/test/breadcrumb.test.ts b/test/breadcrumb.test.ts index 4a93275f72..3cde563b08 100644 --- a/test/breadcrumb.test.ts +++ b/test/breadcrumb.test.ts @@ -1,6 +1,6 @@ import { Breadcrumb } from '@sentry/types'; -import { breadcrumbEquals, breadcrumbFromObject } from '../src/js/breadcrumb'; +import { breadcrumbFromObject } from '../src/js/breadcrumb'; describe('Breadcrumb', () => { describe('breadcrumbFromObject', () => { @@ -49,110 +49,4 @@ describe('Breadcrumb', () => { expect(breadcrumb).toEqual({}); }); }); - - describe('breadcrumbEquals', () => { - it('returns true if two breadcrumbs with different timestamp are equal', () => { - const breadcrumb: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - const other: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800001, - }; - expect(breadcrumbEquals(breadcrumb, other)).toBe(true); - }); - - it('returns true if two breadcrumbs with different level are equal', () => { - const breadcrumb: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - const other: Breadcrumb = { - type: 'test', - level: 'warning', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - expect(breadcrumbEquals(breadcrumb, other)).toBe(true); - }); - - it('returns true if two breadcrumbs with different data are equal', () => { - const breadcrumb: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - const other: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test2', - }, - timestamp: 1577836800000, - }; - expect(breadcrumbEquals(breadcrumb, other)).toBe(true); - }); - - it('returns false if breadcrumbs message is different', () => { - const breadcrumb: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - const other: Breadcrumb = { - type: 'test', - level: 'info', - event_id: '1234', - category: 'test', - message: 'test2', - data: { - test: 'test', - }, - timestamp: 1577836800000, - }; - expect(breadcrumbEquals(breadcrumb, other)).toBe(false); - }); - }); }); diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts index 0b54d734c3..39ee7526ce 100644 --- a/test/integrations/devicecontext.test.ts +++ b/test/integrations/devicecontext.test.ts @@ -1,8 +1,8 @@ import { Hub } from '@sentry/core'; import { Event, SeverityLevel } from '@sentry/types'; -import { NativeDeviceContextsResponse } from '../../src/js/definitions'; import { DeviceContext } from '../../src/js/integrations'; +import { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; jest.mock('../../src/js/wrapper'); @@ -109,7 +109,7 @@ describe('Device Context Integration', () => { })).expectEvent.toStrictEqualMockEvent(); }); - it('merge breadcrumbs', async () => { + it('use only native breadcrumbs', async () => { const { processedEvent } = await executeIntegrationWith({ nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] }, mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] }, @@ -117,42 +117,11 @@ describe('Device Context Integration', () => { expect(processedEvent).toStrictEqual({ breadcrumbs: [ { message: 'duplicate-breadcrumb' }, - { message: 'event-breadcrumb' }, { message: 'native-breadcrumb' }, ], }); }); - it('asc oder breadcrumbs by timestamp', async () => { - const { processedEvent } = await executeIntegrationWith({ - nativeContexts: { - breadcrumbs: [ - { message: 'breadcrumb-1' }, - { message: 'breadcrumb-2', timestamp: '2023-10-01T11:00:00.000Z' }, - { message: 'breadcrumb-0' }, - ] - }, - mockEvent: { - breadcrumbs: [ - { message: 'breadcrumb-4', timestamp: Date.parse('2025-10-01T11:00:00.000Z') }, - { message: 'breadcrumb-3', timestamp: Date.parse('2024-10-01T11:00:00.000Z') }, - { message: 'breadcrumb-5' }, - ] - }, - }); - - expect(processedEvent).toStrictEqual({ - breadcrumbs: [ - { message: 'breadcrumb-5' }, - { message: 'breadcrumb-1' }, - { message: 'breadcrumb-0' }, - { message: 'breadcrumb-2', timestamp: Date.parse('2023-10-01T11:00:00.000Z') }, - { message: 'breadcrumb-3', timestamp: Date.parse('2024-10-01T11:00:00.000Z') }, - { message: 'breadcrumb-4', timestamp: Date.parse('2025-10-01T11:00:00.000Z') }, - ], - }); - }); - async function executeIntegrationWith({ nativeContexts, mockEvent }: { nativeContexts: Record; mockEvent?: Event; diff --git a/test/scope.test.ts b/test/scope.test.ts new file mode 100644 index 0000000000..af5b48b2bc --- /dev/null +++ b/test/scope.test.ts @@ -0,0 +1,25 @@ +import { Breadcrumb } from '@sentry/types'; + +import { ReactNativeScope } from '../src/js/scope'; +import { NATIVE } from '../src/js/wrapper'; + +jest.mock('../src/js/wrapper'); + +describe('Scope', () => { + describe('addBreadcrumb', () => { + it('addBreadcrumb without level', () => { + (NATIVE.addBreadcrumb as jest.Mock).mockImplementationOnce(() => { return; }); + const scope = new ReactNativeScope() as ReactNativeScope & { _breadcrumbs: Breadcrumb[] }; + const breadcrumb = { + message: 'test', + timestamp: 1234, + }; + scope.addBreadcrumb(breadcrumb); + expect(scope._breadcrumbs).toEqual([{ + message: 'test', + timestamp: 1234, + level: 'error', + }]); + }); + }); +}); diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts index 9a70ec703e..603acb3616 100644 --- a/test/wrapper.test.ts +++ b/test/wrapper.test.ts @@ -474,10 +474,6 @@ describe('Tests Native Wrapper', () => { }); describe('_processLevel', () => { - test('converts deprecated levels', () => { - expect(NATIVE._processLevel('log' as SeverityLevel)).toBe('debug' as SeverityLevel); - expect(NATIVE._processLevel('critical' as SeverityLevel)).toBe('fatal' as SeverityLevel); - }); test('returns non-deprecated levels', () => { expect(NATIVE._processLevel('debug' as SeverityLevel)).toBe('debug' as SeverityLevel); expect(NATIVE._processLevel('fatal' as SeverityLevel)).toBe('fatal' as SeverityLevel); From d158fe0c9a71f02b021c394c560241b7bf1681cf Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 4 Jan 2023 14:27:50 +0100 Subject: [PATCH 11/16] Add specific nsdictionary types back --- ios/RNSentry.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index a080158ff6..8f913a342d 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -175,14 +175,14 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event rejecter:(RCTPromiseRejectBlock)reject) { NSLog(@"Bridge call to: deviceContexts"); - __block NSMutableDictionary *contexts; + __block NSMutableDictionary *contexts; // Temp work around until sorted out this API in sentry-cocoa. // TODO: If the callback isnt' executed the promise wouldn't be resolved. [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - NSDictionary *serializedScope = [scope serialize]; + NSDictionary *serializedScope = [scope serialize]; contexts = [serializedScope mutableCopy]; - NSDictionary *user = [contexts valueForKey:@"user"]; + NSDictionary *user = [contexts valueForKey:@"user"]; if (user == nil) { [contexts setValue:@{ @"id": PrivateSentrySDKOnly.installationID } From 1d6b6aeae45a79d466cbc3addf07cf3cbecb6e3f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 4 Jan 2023 16:23:04 +0100 Subject: [PATCH 12/16] Revert sample before send clg del --- sample/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/src/App.tsx b/sample/src/App.tsx index f4df6bdfe8..e620a6bae8 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -28,7 +28,7 @@ Sentry.init({ dsn: SENTRY_INTERNAL_DSN, debug: true, beforeSend: (e, hint) => { - // console.log('Event beforeSend:', e, 'hint:', hint); + console.log('Event beforeSend:', e, 'hint:', hint); return e; }, // This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted. From 566058c4367cec9ba5cf62fe584f4ce555a8416f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 4 Jan 2023 17:41:56 +0100 Subject: [PATCH 13/16] Remove add breadcrumb timestamp recreation ios --- ios/RNSentry.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 8f913a342d..dcbad4ccb7 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -377,10 +377,12 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event [breadcrumbInstance setLevel:sentryLevel]; [breadcrumbInstance setCategory:breadcrumb[@"category"]]; + [breadcrumbInstance setType:breadcrumb[@"type"]]; + [breadcrumbInstance setMessage:breadcrumb[@"message"]]; + [breadcrumbInstance setData:breadcrumb[@"data"]]; - [breadcrumbInstance setTimestamp:[NSDate dateWithTimeIntervalSince1970:[breadcrumb[@"timestamp"] doubleValue]]]; [scope addBreadcrumb:breadcrumbInstance]; }]; From 5a4cef034f21fb8186f855886fb57014c054fa72 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 9 Jan 2023 12:08:53 +0100 Subject: [PATCH 14/16] Return log to debug test --- test/wrapper.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts index 603acb3616..66122e33c8 100644 --- a/test/wrapper.test.ts +++ b/test/wrapper.test.ts @@ -474,6 +474,9 @@ describe('Tests Native Wrapper', () => { }); describe('_processLevel', () => { + test('converts deprecated levels', () => { + expect(NATIVE._processLevel('log' as SeverityLevel)).toBe('debug' as SeverityLevel); + }); test('returns non-deprecated levels', () => { expect(NATIVE._processLevel('debug' as SeverityLevel)).toBe('debug' as SeverityLevel); expect(NATIVE._processLevel('fatal' as SeverityLevel)).toBe('fatal' as SeverityLevel); From 83690ad2d94ec56487d8ce02c8b0947761b68ccd Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 9 Jan 2023 12:25:47 +0100 Subject: [PATCH 15/16] Set breadcrumbs default level to info --- CHANGELOG.md | 4 ++-- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 8 ++++---- ios/RNSentry.mm | 6 +++--- src/js/breadcrumb.ts | 2 +- test/scope.test.ts | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522da4fe8d..a725cc631e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ - Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. ([#2713](https://github.com/getsentry/sentry-react-native/pull/2713)) - `breadcrumb.level` value `log` is transformed to `debug` when syncing with native layers. - - Deprecated `breadcrumb.level` value `critical` is removed and not transformed. - - Default `breadcrumb.level` is `error` + - Remove `breadcrumb.level` value `critical` transformation to `fatal`. + - Default `breadcrumb.level` is `info` ### Breaking changes diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index e34cf749fd..4f678a5d7c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -444,13 +444,13 @@ public void addBreadcrumb(final ReadableMap breadcrumb) { case "debug": breadcrumbInstance.setLevel(SentryLevel.DEBUG); break; - case "info": - breadcrumbInstance.setLevel(SentryLevel.INFO); - break; case "error": - default: breadcrumbInstance.setLevel(SentryLevel.ERROR); break; + case "info": + default: + breadcrumbInstance.setLevel(SentryLevel.INFO); + break; } } diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index dcbad4ccb7..7d83f3a2e5 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -367,12 +367,12 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event sentryLevel = kSentryLevelFatal; } else if ([levelString isEqualToString:@"warning"]) { sentryLevel = kSentryLevelWarning; - } else if ([levelString isEqualToString:@"info"]) { - sentryLevel = kSentryLevelInfo; + } else if ([levelString isEqualToString:@"error"]) { + sentryLevel = kSentryLevelError; } else if ([levelString isEqualToString:@"debug"]) { sentryLevel = kSentryLevelDebug; } else { - sentryLevel = kSentryLevelError; + sentryLevel = kSentryLevelInfo; } [breadcrumbInstance setLevel:sentryLevel]; diff --git a/src/js/breadcrumb.ts b/src/js/breadcrumb.ts index 2432628171..0f3b6ea4db 100644 --- a/src/js/breadcrumb.ts +++ b/src/js/breadcrumb.ts @@ -1,7 +1,7 @@ import { Breadcrumb, SeverityLevel } from '@sentry/types'; import { severityLevelFromString } from '@sentry/utils'; -export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'error'; +export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'info'; type BreadcrumbCandidate = { [K in keyof Partial]: unknown; diff --git a/test/scope.test.ts b/test/scope.test.ts index af5b48b2bc..ccea73e144 100644 --- a/test/scope.test.ts +++ b/test/scope.test.ts @@ -18,7 +18,7 @@ describe('Scope', () => { expect(scope._breadcrumbs).toEqual([{ message: 'test', timestamp: 1234, - level: 'error', + level: 'info', }]); }); }); From 1b979e1c62016e93daa1a169ba6253b8511ee6f5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 12 Jan 2023 11:18:53 +0100 Subject: [PATCH 16/16] Fix changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2901f83306..0289beed67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog -## 5.0.0-alpha.11 - -- Latest changes from 4.13.0 +## Unreleased ### Features @@ -11,6 +9,10 @@ - Remove `breadcrumb.level` value `critical` transformation to `fatal`. - Default `breadcrumb.level` is `info` +## 5.0.0-alpha.11 + +- Latest changes from 4.13.0 + ### Breaking changes - Message event current stack trace moved from exception to threads ([#2694](https://github.com/getsentry/sentry-react-native/pull/2694))