diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8022f860..bc37b878a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Adds breadcrumb origin field to prevent exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124)) - Skips ignoring require cycle logs for RN 0.70 or newer ([#4214](https://github.com/getsentry/sentry-react-native/pull/4214)) - Enhanced accuracy of time-to-display spans. ([#4189](https://github.com/getsentry/sentry-react-native/pull/4189)) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt index a41c3b964d..e2eceab44a 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt @@ -1,6 +1,7 @@ package io.sentry.rnsentryandroidtester import com.facebook.react.bridge.JavaOnlyMap +import io.sentry.SentryLevel import io.sentry.react.RNSentryBreadcrumb import junit.framework.TestCase.assertEquals import org.junit.Test @@ -10,6 +11,38 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class RNSentryBreadcrumbTest { + @Test + fun generatesSentryBreadcrumbFromMap() { + val testData = JavaOnlyMap.of( + "test", "data", + ) + val map = JavaOnlyMap.of( + "level", "error", + "category", "testCategory", + "origin", "testOrigin", + "type", "testType", + "message", "testMessage", + "data", testData, + ) + val actual = RNSentryBreadcrumb.fromMap(map) + assertEquals(SentryLevel.ERROR, actual.level) + assertEquals("testCategory", actual.category) + assertEquals("testOrigin", actual.origin) + assertEquals("testType", actual.type) + assertEquals("testMessage", actual.message) + assertEquals(testData.toHashMap(), actual.data) + } + + @Test + fun reactNativeForMissingOrigin() { + val map = JavaOnlyMap.of( + "message", "testMessage", + ) + val actual = RNSentryBreadcrumb.fromMap(map) + assertEquals("testMessage", actual.message) + assertEquals("react-native", actual.origin) + } + @Test fun nullForMissingCategory() { val map = JavaOnlyMap.of() diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index b1cc2aa6f4..a473ae7d57 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; + AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; }; B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; }; /* End PBXBuildFile section */ @@ -212,6 +213,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.swift index f4bd3ce8e9..397a18ea2d 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.swift @@ -1,12 +1,13 @@ import XCTest import Sentry -class RNSentryBreadcrumbTests: XCTestCase { +final class RNSentryBreadcrumbTests: XCTestCase { func testGeneratesSentryBreadcrumbFromNSDictionary() { let actualCrumb = RNSentryBreadcrumb.from([ "level": "error", "category": "testCategory", + "origin": "testOrigin", "type": "testType", "message": "testMessage", "data": [ @@ -16,11 +17,29 @@ class RNSentryBreadcrumbTests: XCTestCase { XCTAssertEqual(actualCrumb!.level, SentryLevel.error) XCTAssertEqual(actualCrumb!.category, "testCategory") + XCTAssertEqual(actualCrumb!.origin, "testOrigin") XCTAssertEqual(actualCrumb!.type, "testType") XCTAssertEqual(actualCrumb!.message, "testMessage") XCTAssertEqual((actualCrumb!.data)!["test"] as! String, "data") } + func testUsesReactNativeAsDefaultOrigin() { + let actualCrumb = RNSentryBreadcrumb.from([ + "message": "testMessage" + ]) + + XCTAssertEqual(actualCrumb!.origin, "react-native") + } + + func testKeepsOriginIfSet() { + let actualCrumb = RNSentryBreadcrumb.from([ + "message": "testMessage", + "origin": "someOrigin" + ]) + + XCTAssertEqual(actualCrumb!.origin, "someOrigin") + } + func testUsesInfoAsDefaultSentryLevel() { let actualCrumb = RNSentryBreadcrumb.from([ "message": "testMessage" diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java index cc5bf0fb2b..45885adc9c 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java @@ -51,6 +51,12 @@ public static Breadcrumb fromMap(ReadableMap from) { breadcrumb.setCategory(from.getString("category")); } + if (from.hasKey("origin")) { + breadcrumb.setOrigin(from.getString("origin")); + } else { + breadcrumb.setOrigin("react-native"); + } + if (from.hasKey("level")) { switch (from.getString("level")) { case "fatal": diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 7472187dd4..ddcdf5e8ba 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -26,6 +26,7 @@ import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import io.sentry.Breadcrumb; import io.sentry.HubAdapter; import io.sentry.ILogger; import io.sentry.IScope; @@ -75,6 +76,7 @@ import java.io.InputStream; import java.nio.charset.Charset; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -896,6 +898,17 @@ public void fetchNativeDeviceContexts(Promise promise) { } final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope(); + if (currentScope != null) { + // Remove react-native breadcrumbs + Iterator breadcrumbsIterator = currentScope.getBreadcrumbs().iterator(); + while (breadcrumbsIterator.hasNext()) { + Breadcrumb breadcrumb = breadcrumbsIterator.next(); + if ("react-native".equals(breadcrumb.getOrigin())) { + breadcrumbsIterator.remove(); + } + } + } + final @NotNull Map serialized = InternalSentrySdk.serializeScope(context, (SentryAndroidOptions) options, currentScope); final @Nullable Object deviceContext = RNSentryMapConverter.convertToWritable(serialized); diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 3009e46c09..1bcef33eea 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -386,6 +386,17 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd [serializedScope setValue:contexts forKey:@"contexts"]; [serializedScope removeObjectForKey:@"context"]; + + // Remove react-native breadcrumbs + NSMutableArray *> *breadcrumbs = [serializedScope[@"breadcrumbs"] mutableCopy]; + for (NSInteger i = breadcrumbs.count - 1; i >= 0; i--) { + NSDictionary *breadcrumb = breadcrumbs[i]; + if ([breadcrumb[@"origin"] isEqualToString:@"react-native"]) { + [breadcrumbs removeObjectAtIndex:i]; + } + } + [serializedScope setValue:breadcrumbs forKey:@"breadcrumbs"]; + resolve(serializedScope); } diff --git a/packages/core/ios/RNSentryBreadcrumb.m b/packages/core/ios/RNSentryBreadcrumb.m index e900ba4833..f002ba74a2 100644 --- a/packages/core/ios/RNSentryBreadcrumb.m +++ b/packages/core/ios/RNSentryBreadcrumb.m @@ -23,6 +23,12 @@ +(SentryBreadcrumb*) from: (NSDictionary *) dict [crumb setLevel:sentryLevel]; [crumb setCategory:dict[@"category"]]; + id origin = dict[@"origin"]; + if (origin != nil) { + [crumb setOrigin:origin]; + } else { + [crumb setOrigin:@"react-native"]; + } [crumb setType:dict[@"type"]]; [crumb setMessage:dict[@"message"]]; [crumb setData:dict[@"data"]]; diff --git a/packages/core/src/js/integrations/devicecontext.ts b/packages/core/src/js/integrations/devicecontext.ts index 942ca5210d..eeb6b87b8a 100644 --- a/packages/core/src/js/integrations/devicecontext.ts +++ b/packages/core/src/js/integrations/devicecontext.ts @@ -83,7 +83,7 @@ async function processEvent(event: Event): Promise { ? native['breadcrumbs'].map(breadcrumbFromObject) : undefined; if (nativeBreadcrumbs) { - event.breadcrumbs = nativeBreadcrumbs; + event.breadcrumbs = (event.breadcrumbs || []).concat(nativeBreadcrumbs); } return event; diff --git a/packages/core/test/integrations/devicecontext.test.ts b/packages/core/test/integrations/devicecontext.test.ts index ff46e5f3c1..cb641064c7 100644 --- a/packages/core/test/integrations/devicecontext.test.ts +++ b/packages/core/test/integrations/devicecontext.test.ts @@ -158,13 +158,13 @@ describe('Device Context Integration', () => { ).expectEvent.toStrictEqualMockEvent(); }); - it('use only native breadcrumbs', async () => { + it('merge native and event breadcrumbs', async () => { const { processedEvent } = await processEventWith({ - nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] }, - mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] }, + nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb' }] }, + mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb' }] }, }); expect(processedEvent).toStrictEqual({ - breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }], + breadcrumbs: [{ message: 'event-breadcrumb' }, { message: 'native-breadcrumb' }], }); }); diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx index 6430440295..2890f8d19d 100644 --- a/samples/react-native/src/Screens/ErrorsScreen.tsx +++ b/samples/react-native/src/Screens/ErrorsScreen.tsx @@ -88,6 +88,13 @@ const ErrorsScreen = (_props: Props) => { Sentry.captureException(error); }} /> +