Skip to content

Commit fd54b38

Browse files
authored
fix(core): Breadcrumbs added on forked context are now captured (#4124)
1 parent e22745e commit fd54b38

File tree

12 files changed

+297
-22
lines changed

12 files changed

+297
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
77
<!-- prettier-ignore-end -->
88
9+
## Unreleased
10+
11+
### Fixes
12+
13+
- Prevents exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124))
14+
915
## 6.2.0
1016

1117
### Features
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.sentry.react
2+
3+
import android.content.Context
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import com.facebook.react.bridge.PromiseImpl
6+
import com.facebook.react.bridge.ReactApplicationContext
7+
import com.facebook.react.bridge.WritableMap
8+
import com.facebook.soloader.SoLoader
9+
import io.sentry.Breadcrumb
10+
import io.sentry.Scope
11+
import io.sentry.SentryOptions
12+
import io.sentry.android.core.SentryAndroidOptions
13+
import org.junit.Assert.assertEquals
14+
import org.junit.Assert.fail
15+
import org.junit.Before
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
import org.junit.runners.JUnit4
19+
20+
@RunWith(JUnit4::class)
21+
class RNSentryModuleImplTest {
22+
23+
private lateinit var module: RNSentryModuleImpl
24+
private lateinit var context: Context
25+
26+
@Before
27+
fun setUp() {
28+
context = InstrumentationRegistry.getInstrumentation().targetContext
29+
SoLoader.init(context, false)
30+
val reactContext = ReactApplicationContext(context)
31+
module = RNSentryModuleImpl(reactContext)
32+
}
33+
34+
@Test
35+
fun fetchNativeDeviceContextsWithNullContext() {
36+
val options = SentryAndroidOptions()
37+
val scope = Scope(options)
38+
val promise = PromiseImpl({
39+
assertEquals(1, it.size)
40+
assertEquals(null, it[0])
41+
}, {
42+
fail("Promise was rejected unexpectedly")
43+
})
44+
module.fetchNativeDeviceContexts(promise, options, null, scope)
45+
}
46+
47+
@Test
48+
fun fetchNativeDeviceContextsWithInvalidSentryOptions() {
49+
class NotAndroidSentryOptions : SentryOptions()
50+
51+
val options = NotAndroidSentryOptions()
52+
val scope = Scope(options)
53+
val promise = PromiseImpl({
54+
assertEquals(1, it.size)
55+
assertEquals(null, it[0])
56+
}, {
57+
fail("Promise was rejected unexpectedly")
58+
})
59+
module.fetchNativeDeviceContexts(promise, options, context, scope)
60+
}
61+
62+
@Test
63+
fun fetchNativeDeviceContextsFiltersBreadcrumbs() {
64+
val options = SentryAndroidOptions().apply { maxBreadcrumbs = 5 }
65+
val scope = Scope(options)
66+
scope.addBreadcrumb(Breadcrumb("Breadcrumb1-RN").apply { origin = "react-native" })
67+
scope.addBreadcrumb(Breadcrumb("Breadcrumb2-Native"))
68+
scope.addBreadcrumb(Breadcrumb("Breadcrumb3-Native").apply { origin = "java" })
69+
scope.addBreadcrumb(Breadcrumb("Breadcrumb2-RN").apply { origin = "react-native" })
70+
scope.addBreadcrumb(Breadcrumb("Breadcrumb2-RN").apply { origin = "react-native" })
71+
72+
val promise = PromiseImpl({
73+
assertEquals(1, it.size)
74+
assertEquals(true, it[0] is WritableMap)
75+
val actual = it[0] as WritableMap
76+
val breadcrumbs = actual.getArray("breadcrumbs")
77+
assertEquals(2, breadcrumbs?.size())
78+
assertEquals("Breadcrumb2-Native", breadcrumbs?.getMap(0)?.getString("message"))
79+
assertEquals("Breadcrumb3-Native", breadcrumbs?.getMap(1)?.getString("message"))
80+
}, {
81+
fail("Promise was rejected unexpectedly")
82+
})
83+
84+
module.fetchNativeDeviceContexts(promise, options, context, scope)
85+
}
86+
}

packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryBreadcrumbTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.rnsentryandroidtester
22

33
import com.facebook.react.bridge.JavaOnlyMap
4+
import io.sentry.SentryLevel
45
import io.sentry.react.RNSentryBreadcrumb
56
import junit.framework.TestCase.assertEquals
67
import org.junit.Test
@@ -10,6 +11,38 @@ import org.junit.runners.JUnit4
1011
@RunWith(JUnit4::class)
1112
class RNSentryBreadcrumbTest {
1213

14+
@Test
15+
fun generatesSentryBreadcrumbFromMap() {
16+
val testData = JavaOnlyMap.of(
17+
"test", "data",
18+
)
19+
val map = JavaOnlyMap.of(
20+
"level", "error",
21+
"category", "testCategory",
22+
"origin", "testOrigin",
23+
"type", "testType",
24+
"message", "testMessage",
25+
"data", testData,
26+
)
27+
val actual = RNSentryBreadcrumb.fromMap(map)
28+
assertEquals(SentryLevel.ERROR, actual.level)
29+
assertEquals("testCategory", actual.category)
30+
assertEquals("testOrigin", actual.origin)
31+
assertEquals("testType", actual.type)
32+
assertEquals("testMessage", actual.message)
33+
assertEquals(testData.toHashMap(), actual.data)
34+
}
35+
36+
@Test
37+
fun reactNativeForMissingOrigin() {
38+
val map = JavaOnlyMap.of(
39+
"message", "testMessage",
40+
)
41+
val actual = RNSentryBreadcrumb.fromMap(map)
42+
assertEquals("testMessage", actual.message)
43+
assertEquals("react-native", actual.origin)
44+
}
45+
1346
@Test
1447
fun nullForMissingCategory() {
1548
val map = JavaOnlyMap.of()

packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; };
1414
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; };
1515
33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; };
16+
AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; };
1617
B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; };
1718
/* End PBXBuildFile section */
1819

@@ -221,6 +222,7 @@
221222
isa = PBXSourcesBuildPhase;
222223
buildActionMask = 2147483647;
223224
files = (
225+
AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */,
224226
332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */,
225227
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */,
226228
336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */,

packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Sentry
22
import XCTest
33

4-
class RNSentryBreadcrumbTests: XCTestCase {
4+
final class RNSentryBreadcrumbTests: XCTestCase {
55

66
func testGeneratesSentryBreadcrumbFromNSDictionary() {
77
let actualCrumb = RNSentryBreadcrumb.from([
88
"level": "error",
99
"category": "testCategory",
10+
"origin": "testOrigin",
1011
"type": "testType",
1112
"message": "testMessage",
1213
"data": [
@@ -16,11 +17,29 @@ class RNSentryBreadcrumbTests: XCTestCase {
1617

1718
XCTAssertEqual(actualCrumb!.level, SentryLevel.error)
1819
XCTAssertEqual(actualCrumb!.category, "testCategory")
20+
XCTAssertEqual(actualCrumb!.origin, "testOrigin")
1921
XCTAssertEqual(actualCrumb!.type, "testType")
2022
XCTAssertEqual(actualCrumb!.message, "testMessage")
2123
XCTAssertEqual((actualCrumb!.data)!["test"] as! String, "data")
2224
}
2325

26+
func testUsesReactNativeAsDefaultOrigin() {
27+
let actualCrumb = RNSentryBreadcrumb.from([
28+
"message": "testMessage"
29+
])
30+
31+
XCTAssertEqual(actualCrumb!.origin, "react-native")
32+
}
33+
34+
func testKeepsOriginIfSet() {
35+
let actualCrumb = RNSentryBreadcrumb.from([
36+
"message": "testMessage",
37+
"origin": "someOrigin"
38+
])
39+
40+
XCTAssertEqual(actualCrumb!.origin, "someOrigin")
41+
}
42+
2443
func testUsesInfoAsDefaultSentryLevel() {
2544
let actualCrumb = RNSentryBreadcrumb.from([
2645
"message": "testMessage"

packages/core/android/src/main/java/io/sentry/react/RNSentryBreadcrumb.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public static Breadcrumb fromMap(ReadableMap from) {
5151
breadcrumb.setCategory(from.getString("category"));
5252
}
5353

54+
if (from.hasKey("origin")) {
55+
breadcrumb.setOrigin(from.getString("origin"));
56+
} else {
57+
breadcrumb.setOrigin("react-native");
58+
}
59+
5460
if (from.hasKey("level")) {
5561
switch (from.getString("level")) {
5662
case "fatal":

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.facebook.react.bridge.WritableNativeMap;
2929
import com.facebook.react.common.JavascriptException;
3030
import com.facebook.react.modules.core.DeviceEventManagerModule;
31+
import io.sentry.Breadcrumb;
3132
import io.sentry.HubAdapter;
3233
import io.sentry.ILogger;
3334
import io.sentry.IScope;
@@ -76,6 +77,7 @@
7677
import java.io.InputStream;
7778
import java.nio.charset.Charset;
7879
import java.util.HashMap;
80+
import java.util.Iterator;
7981
import java.util.List;
8082
import java.util.Map;
8183
import java.util.Properties;
@@ -886,18 +888,35 @@ private String readStringFromFile(File path) throws IOException {
886888

887889
public void fetchNativeDeviceContexts(Promise promise) {
888890
final @NotNull SentryOptions options = HubAdapter.getInstance().getOptions();
891+
final @Nullable Context context = this.getReactApplicationContext().getApplicationContext();
892+
final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope();
893+
fetchNativeDeviceContexts(promise, options, context, currentScope);
894+
}
895+
896+
protected void fetchNativeDeviceContexts(
897+
Promise promise,
898+
final @NotNull SentryOptions options,
899+
final @Nullable Context context,
900+
final @Nullable IScope currentScope) {
889901
if (!(options instanceof SentryAndroidOptions)) {
890902
promise.resolve(null);
891903
return;
892904
}
893-
894-
final @Nullable Context context = this.getReactApplicationContext().getApplicationContext();
895905
if (context == null) {
896906
promise.resolve(null);
897907
return;
898908
}
909+
if (currentScope != null) {
910+
// Remove react-native breadcrumbs
911+
Iterator<Breadcrumb> breadcrumbsIterator = currentScope.getBreadcrumbs().iterator();
912+
while (breadcrumbsIterator.hasNext()) {
913+
Breadcrumb breadcrumb = breadcrumbsIterator.next();
914+
if ("react-native".equals(breadcrumb.getOrigin())) {
915+
breadcrumbsIterator.remove();
916+
}
917+
}
918+
}
899919

900-
final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope();
901920
final @NotNull Map<String, Object> serialized =
902921
InternalSentrySdk.serializeScope(context, (SentryAndroidOptions) options, currentScope);
903922
final @Nullable Object deviceContext = RNSentryMapConverter.convertToWritable(serialized);

packages/core/ios/RNSentry.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ - (NSDictionary *)fetchNativeStackFramesBy:(NSArray<NSNumber *> *)instructionsAd
442442

443443
[serializedScope setValue:contexts forKey:@"contexts"];
444444
[serializedScope removeObjectForKey:@"context"];
445+
446+
// Remove react-native breadcrumbs
447+
NSPredicate *removeRNBreadcrumbsPredicate =
448+
[NSPredicate predicateWithBlock:^BOOL(NSDictionary *breadcrumb, NSDictionary *bindings) {
449+
return ![breadcrumb[@"origin"] isEqualToString:@"react-native"];
450+
}];
451+
NSArray *breadcrumbs = [[serializedScope[@"breadcrumbs"] mutableCopy]
452+
filteredArrayUsingPredicate:removeRNBreadcrumbsPredicate];
453+
[serializedScope setValue:breadcrumbs forKey:@"breadcrumbs"];
454+
445455
resolve(serializedScope);
446456
}
447457

packages/core/ios/RNSentryBreadcrumb.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ + (SentryBreadcrumb *)from:(NSDictionary *)dict
2323

2424
[crumb setLevel:sentryLevel];
2525
[crumb setCategory:dict[@"category"]];
26+
id origin = dict[@"origin"];
27+
if (origin != nil) {
28+
[crumb setOrigin:origin];
29+
} else {
30+
[crumb setOrigin:@"react-native"];
31+
}
2632
[crumb setType:dict[@"type"]];
2733
[crumb setMessage:dict[@"message"]];
2834
[crumb setData:dict[@"data"]];

packages/core/src/js/integrations/devicecontext.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable complexity */
2-
import type { Event, Integration } from '@sentry/types';
2+
import type { Client, Event, EventHint, Integration } from '@sentry/types';
33
import { logger, severityLevelFromString } from '@sentry/utils';
44
import { AppState } from 'react-native';
55

@@ -20,7 +20,7 @@ export const deviceContextIntegration = (): Integration => {
2020
};
2121
};
2222

23-
async function processEvent(event: Event): Promise<Event> {
23+
async function processEvent(event: Event, _hint: EventHint, client: Client): Promise<Event> {
2424
let native: NativeDeviceContextsResponse | null = null;
2525
try {
2626
native = await NATIVE.fetchNativeDeviceContexts();
@@ -83,7 +83,11 @@ async function processEvent(event: Event): Promise<Event> {
8383
? native['breadcrumbs'].map(breadcrumbFromObject)
8484
: undefined;
8585
if (nativeBreadcrumbs) {
86-
event.breadcrumbs = nativeBreadcrumbs;
86+
const maxBreadcrumbs = client?.getOptions().maxBreadcrumbs ?? 100; // Default is 100.
87+
event.breadcrumbs = nativeBreadcrumbs
88+
.concat(event.breadcrumbs || []) // concatenate the native and js breadcrumbs
89+
.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)) // sort by timestamp
90+
.slice(-maxBreadcrumbs); // keep the last maxBreadcrumbs
8791
}
8892

8993
return event;

0 commit comments

Comments
 (0)