Skip to content

Commit c84e457

Browse files
authored
Merge c4246d7 into 279b328
2 parents 279b328 + c4246d7 commit c84e457

File tree

11 files changed

+173
-6
lines changed

11 files changed

+173
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
### Fixes
3535

36+
- Prevents exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124))
3637
- Ignore JavascriptException to filter out obfuscated duplicate JS Errors on Android ([#4232](https://github.com/getsentry/sentry-react-native/pull/4232))
3738
- Skips ignoring require cycle logs for RN 0.70 or newer ([#4214](https://github.com/getsentry/sentry-react-native/pull/4214))
3839
- Enhanced accuracy of time-to-display spans. ([#4189](https://github.com/getsentry/sentry-react-native/pull/4189))

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

+33
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

+2
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

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import XCTest
22
import Sentry
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

+6
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

+13
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;
@@ -898,6 +900,17 @@ public void fetchNativeDeviceContexts(Promise promise) {
898900
}
899901

900902
final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope();
903+
if (currentScope != null) {
904+
// Remove react-native breadcrumbs
905+
Iterator<Breadcrumb> breadcrumbsIterator = currentScope.getBreadcrumbs().iterator();
906+
while (breadcrumbsIterator.hasNext()) {
907+
Breadcrumb breadcrumb = breadcrumbsIterator.next();
908+
if ("react-native".equals(breadcrumb.getOrigin())) {
909+
breadcrumbsIterator.remove();
910+
}
911+
}
912+
}
913+
901914
final @NotNull Map<String, Object> serialized =
902915
InternalSentrySdk.serializeScope(context, (SentryAndroidOptions) options, currentScope);
903916
final @Nullable Object deviceContext = RNSentryMapConverter.convertToWritable(serialized);

packages/core/ios/RNSentry.mm

+10
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,16 @@ - (NSDictionary *)fetchNativeStackFramesBy:(NSArray<NSNumber *> *)instructionsAd
439439

440440
[serializedScope setValue:contexts forKey:@"contexts"];
441441
[serializedScope removeObjectForKey:@"context"];
442+
443+
// Remove react-native breadcrumbs
444+
NSPredicate *removeRNBreadcrumbsPredicate =
445+
[NSPredicate predicateWithBlock:^BOOL(NSDictionary *breadcrumb, NSDictionary *bindings) {
446+
return ![breadcrumb[@"origin"] isEqualToString:@"react-native"];
447+
}];
448+
NSArray *breadcrumbs = [[serializedScope[@"breadcrumbs"] mutableCopy]
449+
filteredArrayUsingPredicate:removeRNBreadcrumbsPredicate];
450+
[serializedScope setValue:breadcrumbs forKey:@"breadcrumbs"];
451+
442452
resolve(serializedScope);
443453
}
444454

packages/core/ios/RNSentryBreadcrumb.m

+6
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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable complexity */
2+
import { getClient } from '@sentry/core';
23
import type { Event, Integration } from '@sentry/types';
34
import { logger, severityLevelFromString } from '@sentry/utils';
45
import { AppState } from 'react-native';
@@ -83,7 +84,11 @@ async function processEvent(event: Event): Promise<Event> {
8384
? native['breadcrumbs'].map(breadcrumbFromObject)
8485
: undefined;
8586
if (nativeBreadcrumbs) {
86-
event.breadcrumbs = nativeBreadcrumbs;
87+
const maxBreadcrumbs = getClient()?.getOptions().maxBreadcrumbs ?? 100; // Default is 100.
88+
event.breadcrumbs = nativeBreadcrumbs
89+
.concat(event.breadcrumbs || []) // concatenate the native and js breadcrumbs
90+
.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)) // sort by timestamp
91+
.slice(-maxBreadcrumbs); // keep the last maxBreadcrumbs
8792
}
8893

8994
return event;

packages/core/test/integrations/devicecontext.test.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getClient } from '@sentry/core';
12
import type { Client, Event, EventHint, SeverityLevel } from '@sentry/types';
23

34
import { deviceContextIntegration } from '../../src/js/integrations/devicecontext';
@@ -12,6 +13,13 @@ jest.mock('react-native', () => ({
1213
NativeModules: {},
1314
Platform: {},
1415
}));
16+
jest.mock('@sentry/core', () => ({
17+
getClient: jest.fn().mockReturnValue({
18+
getOptions: jest.fn().mockReturnValue({
19+
maxBreadcrumbs: 100, // Default value
20+
}),
21+
}),
22+
}));
1523

1624
describe('Device Context Integration', () => {
1725
it('add native user', async () => {
@@ -158,13 +166,70 @@ describe('Device Context Integration', () => {
158166
).expectEvent.toStrictEqualMockEvent();
159167
});
160168

161-
it('use only native breadcrumbs', async () => {
169+
it('merge native and event breadcrumbs', async () => {
170+
getClient().getOptions().maxBreadcrumbs = undefined; // Default 100
162171
const { processedEvent } = await processEventWith({
163-
nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] },
164-
mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] },
172+
nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb-1' }, { message: 'native-breadcrumb-2' }] },
173+
mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb-1' }, { message: 'event-breadcrumb-2' }] },
174+
});
175+
expect(processedEvent).toStrictEqual({
176+
breadcrumbs: [
177+
{ message: 'native-breadcrumb-1' },
178+
{ message: 'native-breadcrumb-2' },
179+
{ message: 'event-breadcrumb-1' },
180+
{ message: 'event-breadcrumb-2' },
181+
],
182+
});
183+
});
184+
185+
it('respect breadcrumb order when merging', async () => {
186+
getClient().getOptions().maxBreadcrumbs = undefined; // Default 100
187+
const { processedEvent } = await processEventWith({
188+
nativeContexts: {
189+
breadcrumbs: [
190+
{ message: 'native-breadcrumb-3', timestamp: 'Thursday, November 7, 2024 3:24:59 PM GMT+02:00' }, // 1730985899
191+
{ message: 'native-breadcrumb-1', timestamp: 'Thursday, November 7, 2024 3:24:57 PM GMT+02:00' }, // 1730985897
192+
],
193+
},
194+
mockEvent: {
195+
breadcrumbs: [
196+
{ message: 'event-breadcrumb-4', timestamp: 1730985999 },
197+
{ message: 'event-breadcrumb-2', timestamp: 1730985898 },
198+
],
199+
},
200+
});
201+
expect(processedEvent).toStrictEqual({
202+
breadcrumbs: [
203+
{ message: 'native-breadcrumb-1', timestamp: 1730985897 },
204+
{ message: 'event-breadcrumb-2', timestamp: 1730985898 },
205+
{ message: 'native-breadcrumb-3', timestamp: 1730985899 },
206+
{ message: 'event-breadcrumb-4', timestamp: 1730985999 },
207+
],
208+
});
209+
});
210+
211+
it('keep the last maxBreadcrumbs when merging', async () => {
212+
getClient().getOptions().maxBreadcrumbs = 3;
213+
const { processedEvent } = await processEventWith({
214+
nativeContexts: {
215+
breadcrumbs: [
216+
{ message: 'native-breadcrumb-3', timestamp: 'Thursday, November 7, 2024 3:24:59 PM GMT+02:00' }, // 1730985899
217+
{ message: 'native-breadcrumb-1', timestamp: 'Thursday, November 7, 2024 3:24:57 PM GMT+02:00' }, // 1730985897
218+
],
219+
},
220+
mockEvent: {
221+
breadcrumbs: [
222+
{ message: 'event-breadcrumb-4', timestamp: 1730985999 },
223+
{ message: 'event-breadcrumb-2', timestamp: 1730985898 },
224+
],
225+
},
165226
});
166227
expect(processedEvent).toStrictEqual({
167-
breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }],
228+
breadcrumbs: [
229+
{ message: 'event-breadcrumb-2', timestamp: 1730985898 },
230+
{ message: 'native-breadcrumb-3', timestamp: 1730985899 },
231+
{ message: 'event-breadcrumb-4', timestamp: 1730985999 },
232+
],
168233
});
169234
});
170235

samples/react-native/src/Screens/ErrorsScreen.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ const ErrorsScreen = (_props: Props) => {
8888
Sentry.captureException(error);
8989
}}
9090
/>
91+
<Button
92+
title="Capture exception with breadcrumb"
93+
onPress={() => {
94+
Sentry.captureException(new Error('Captured exception with breadcrumb'),
95+
context => context.addBreadcrumb({ message: 'error with breadcrumb' }));
96+
}}
97+
/>
9198
<Button
9299
title="Uncaught Thrown Error"
93100
onPress={() => {

0 commit comments

Comments
 (0)