Skip to content

Commit 9cee3c7

Browse files
authored
Merge f828fc7 into d2c32bb
2 parents d2c32bb + f828fc7 commit 9cee3c7

File tree

11 files changed

+155
-6
lines changed

11 files changed

+155
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
### Fixes
3434

35+
- Prevents exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124))
3536
- Skips ignoring require cycle logs for RN 0.70 or newer ([#4214](https://github.com/getsentry/sentry-react-native/pull/4214))
3637
- Enhanced accuracy of time-to-display spans. ([#4189](https://github.com/getsentry/sentry-react-native/pull/4189))
3738

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
@@ -12,6 +12,7 @@
1212
33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; };
1313
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; };
1414
33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; };
15+
AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; };
1516
B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; };
1617
/* End PBXBuildFile section */
1718

@@ -212,6 +213,7 @@
212213
isa = PBXSourcesBuildPhase;
213214
buildActionMask = 2147483647;
214215
files = (
216+
AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */,
215217
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */,
216218
336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */,
217219
33F58AD02977037D008F60EA /* RNSentryTests.mm 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
@@ -27,6 +27,7 @@
2727
import com.facebook.react.bridge.WritableNativeArray;
2828
import com.facebook.react.bridge.WritableNativeMap;
2929
import com.facebook.react.modules.core.DeviceEventManagerModule;
30+
import io.sentry.Breadcrumb;
3031
import io.sentry.HubAdapter;
3132
import io.sentry.ILogger;
3233
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;
@@ -905,6 +907,17 @@ public void fetchNativeDeviceContexts(Promise promise) {
905907
}
906908

907909
final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope();
910+
if (currentScope != null) {
911+
// Remove react-native breadcrumbs
912+
Iterator<Breadcrumb> breadcrumbsIterator = currentScope.getBreadcrumbs().iterator();
913+
while (breadcrumbsIterator.hasNext()) {
914+
Breadcrumb breadcrumb = breadcrumbsIterator.next();
915+
if ("react-native".equals(breadcrumb.getOrigin())) {
916+
breadcrumbsIterator.remove();
917+
}
918+
}
919+
}
920+
908921
final @NotNull Map<String, Object> serialized =
909922
InternalSentrySdk.serializeScope(context, (SentryAndroidOptions) options, currentScope);
910923
final @Nullable Object deviceContext = RNSentryMapConverter.convertToWritable(serialized);

packages/core/ios/RNSentry.mm

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

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

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

+3-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,8 @@ 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.concat(event.breadcrumbs || []).slice(0, maxBreadcrumbs);
8789
}
8890

8991
return event;

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

+54-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,55 @@ 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+
const { processedEvent } = await processEventWith({
171+
nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb-1' }, { message: 'native-breadcrumb-2' }] },
172+
mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb-1' }, { message: 'event-breadcrumb-2' }] },
173+
});
174+
expect(processedEvent).toStrictEqual({
175+
breadcrumbs: [
176+
{ message: 'native-breadcrumb-1' },
177+
{ message: 'native-breadcrumb-2' },
178+
{ message: 'event-breadcrumb-1' },
179+
{ message: 'event-breadcrumb-2' },
180+
],
181+
});
182+
});
183+
184+
it('use only native breadcrumb if the maxBreadcrumbs limit does not leave space for event breadcrumbs', async () => {
185+
getClient().getOptions().maxBreadcrumbs = 1;
186+
const { processedEvent } = await processEventWith({
187+
nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb' }] },
188+
mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb' }] },
189+
});
190+
expect(processedEvent).toStrictEqual({
191+
breadcrumbs: [{ message: 'native-breadcrumb' }],
192+
});
193+
});
194+
195+
it('use native breadcrumbs and pick only the event breadcrumbs that fit the maxBreadcrumbs limit', async () => {
196+
getClient().getOptions().maxBreadcrumbs = 3;
197+
const { processedEvent } = await processEventWith({
198+
nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb-1' }, { message: 'native-breadcrumb-2' }] },
199+
mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb-1' }, { message: 'event-breadcrumb-2' }] },
200+
});
201+
expect(processedEvent).toStrictEqual({
202+
breadcrumbs: [
203+
{ message: 'native-breadcrumb-1' },
204+
{ message: 'native-breadcrumb-2' },
205+
{ message: 'event-breadcrumb-1' },
206+
],
207+
});
208+
});
209+
210+
it('do not use any breadcrumbs if the maxBreadcrumbs limit is set to zero', async () => {
211+
getClient().getOptions().maxBreadcrumbs = 0;
162212
const { processedEvent } = await processEventWith({
163-
nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] },
164-
mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] },
213+
nativeContexts: { breadcrumbs: [{ message: 'native-breadcrumb' }] },
214+
mockEvent: { breadcrumbs: [{ message: 'event-breadcrumb' }] },
165215
});
166216
expect(processedEvent).toStrictEqual({
167-
breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }],
217+
breadcrumbs: [],
168218
});
169219
});
170220

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)