Skip to content

Commit 112c4f8

Browse files
antonislucas-zimermankrystofwoldrich
authored
feat(core): Exclude Dev Server and Sentry Dsn request from Breadcrumbs (#4240)
* Adds breadcrumb origin in RNSentryBreadcrumb dictionary parsing * Use 8.38.0-beta.1 Cocoa SDK that has the new origin field * Adds changelog * Fixes sentry-java breaking changes * Adds origin native tests * Adds Capture exception with breadcrumb in the sample * Set react native as event origin * Filter out events with react-native origin from the native layer * Merge event breadcrumbs with native context * Lint: removes empty line * Use predicate to filter breadcrumbs * Respect max breadcrumbs limit * Updates changelog * Update test names * Fixes lint issue * Filter out DevServer and DSN related breadcrumbs * Adds changelog * Keep the last maxBreadcrumbs (default 100) when merging native and js breadcrumbs * Use client from function parameter * Refactor and test RNSentryModuleImpl.fetchNativeDeviceContexts (#4253) * Refactor fetchNativeDeviceContexts for testability * Test fetchNativeDeviceContexts * Adds new line at the end * Revert "Filter out DevServer and DSN related breadcrumbs" This reverts commit 87bdc77. * Passes development server url as an option to the native sdks * Filter out Dev Server and Sentry Dsn breadcrumbs on Android * Filter out Dev Server and Sentry Dsn breadcrumbs on iOS * Filter out Dev Server and Sentry Dsn breadcrumbs on JS * Adds Java tests * Adds Cocoa tests * Adds JS tests * Sets correct spacing in import Co-authored-by: LucasZF <[email protected]> * Fixes changelog typo Co-authored-by: LucasZF <[email protected]> * Handles undefined dev server urls Co-authored-by: LucasZF <[email protected]> * Handles undefined dsns * Adds tests for undefined dev servers and dsns * Handles undefined dev server urls in native code * Updates test cases * Uses the url from the passed dsn to filter breadcrumbs * Handles nil dsn though this state should never be reached due to SentryOptions validation * Use startsWith to check url matching Co-authored-by: LucasZF <[email protected]> * Check url with prefix instead of contains for efficiency * Fix comment typo Co-authored-by: LucasZF <[email protected]> * Fix comment typo Co-authored-by: LucasZF <[email protected]> * Fix comment typo Co-authored-by: LucasZF <[email protected]> * Update CHANGELOG.md * Safely parses dsn url * Adds test case for url exception --------- Co-authored-by: LucasZF <[email protected]> Co-authored-by: Krystof Woldrich <[email protected]>
1 parent fa2ef81 commit 112c4f8

File tree

10 files changed

+495
-4
lines changed

10 files changed

+495
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Fixes
1212

1313
- Prevents exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124))
14+
- Excludes Dev Server and Sentry Dsn requests from Breadcrumbs ([#4240](https://github.com/getsentry/sentry-react-native/pull/4240))
1415

1516
### Dependencies
1617

packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import com.facebook.react.bridge.Promise
88
import com.facebook.react.bridge.ReactApplicationContext
99
import com.facebook.react.bridge.WritableMap
1010
import com.facebook.react.common.JavascriptException
11+
import io.sentry.Breadcrumb
1112
import io.sentry.ILogger
1213
import io.sentry.SentryLevel
1314
import io.sentry.android.core.SentryAndroidOptions
1415
import org.junit.After
1516
import org.junit.Assert.assertEquals
1617
import org.junit.Assert.assertFalse
18+
import org.junit.Assert.assertNull
1719
import org.junit.Assert.assertTrue
1820
import org.junit.Before
1921
import org.junit.Test
@@ -134,4 +136,109 @@ class RNSentryModuleImplTest {
134136
module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), logger)
135137
assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java))
136138
}
139+
140+
@Test
141+
fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() {
142+
val options = SentryAndroidOptions()
143+
val rnOptions = JavaOnlyMap.of(
144+
"dsn", "https://[email protected]/1234567",
145+
"devServerUrl", "http://localhost:8081",
146+
)
147+
module.getSentryAndroidOptions(options, rnOptions, logger)
148+
149+
val breadcrumb = Breadcrumb().apply {
150+
type = "http"
151+
setData("url", "https://def.ingest.sentry.io/1234567")
152+
}
153+
154+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
155+
156+
assertNull("Breadcrumb should be filtered out", result)
157+
}
158+
159+
@Test
160+
fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() {
161+
val mockDevServerUrl = "http://localhost:8081"
162+
val options = SentryAndroidOptions()
163+
val rnOptions = JavaOnlyMap.of(
164+
"dsn", "https://[email protected]/1234567",
165+
"devServerUrl", mockDevServerUrl,
166+
)
167+
module.getSentryAndroidOptions(options, rnOptions, logger)
168+
169+
val breadcrumb = Breadcrumb().apply {
170+
type = "http"
171+
setData("url", mockDevServerUrl)
172+
}
173+
174+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
175+
176+
assertNull("Breadcrumb should be filtered out", result)
177+
}
178+
179+
@Test
180+
fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() {
181+
val options = SentryAndroidOptions()
182+
val rnOptions = JavaOnlyMap.of(
183+
"dsn", "https://[email protected]/1234567",
184+
"devServerUrl", "http://localhost:8081",
185+
)
186+
module.getSentryAndroidOptions(options, rnOptions, logger)
187+
188+
val breadcrumb = Breadcrumb().apply {
189+
type = "http"
190+
setData("url", "http://testurl.com/service")
191+
}
192+
193+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
194+
195+
assertEquals(breadcrumb, result)
196+
}
197+
198+
@Test
199+
fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() {
200+
val options = SentryAndroidOptions()
201+
module.getSentryAndroidOptions(options, JavaOnlyMap(), logger)
202+
203+
val breadcrumb = Breadcrumb().apply {
204+
type = "http"
205+
setData("url", "http://testurl.com/service")
206+
}
207+
208+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
209+
210+
assertEquals(breadcrumb, result)
211+
}
212+
213+
@Test
214+
fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() {
215+
val options = SentryAndroidOptions()
216+
val rnOptions = JavaOnlyMap.of("dsn", "https://[email protected]/1234567")
217+
module.getSentryAndroidOptions(options, rnOptions, logger)
218+
219+
val breadcrumb = Breadcrumb().apply {
220+
type = "http"
221+
setData("url", "http://testurl.com/service")
222+
}
223+
224+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
225+
226+
assertEquals(breadcrumb, result)
227+
}
228+
229+
@Test
230+
fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() {
231+
val options = SentryAndroidOptions()
232+
val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081")
233+
module.getSentryAndroidOptions(options, rnOptions, logger)
234+
235+
val breadcrumb = Breadcrumb().apply {
236+
type = "http"
237+
setData("url", "http://testurl.com/service")
238+
}
239+
240+
val result = options.beforeBreadcrumb?.execute(breadcrumb, mock())
241+
242+
assertEquals(breadcrumb, result)
243+
}
137244
}

packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,85 @@ - (void)testPassesErrorOnWrongDsn
238238
XCTAssertNotNil(error, @"Did not created error on invalid dsn");
239239
}
240240

241+
- (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs
242+
{
243+
RNSentry *rnSentry = [[RNSentry alloc] init];
244+
NSError *error = nil;
245+
246+
NSDictionary *_Nonnull mockedDictionary = @{
247+
@"dsn" : @"https://[email protected]/1234567",
248+
@"devServerUrl" : @"http://localhost:8081"
249+
};
250+
SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error];
251+
252+
SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init];
253+
breadcrumb.type = @"http";
254+
breadcrumb.data = @{ @"url" : @"https://def.ingest.sentry.io/1234567" };
255+
256+
SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb);
257+
258+
XCTAssertNil(result, @"Breadcrumb should be filtered out");
259+
}
260+
261+
- (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs
262+
{
263+
RNSentry *rnSentry = [[RNSentry alloc] init];
264+
NSError *error = nil;
265+
266+
NSString *mockDevServer = @"http://localhost:8081";
267+
268+
NSDictionary *_Nonnull mockedDictionary =
269+
@{ @"dsn" : @"https://[email protected]/1234567", @"devServerUrl" : mockDevServer };
270+
SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error];
271+
272+
SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init];
273+
breadcrumb.type = @"http";
274+
breadcrumb.data = @{ @"url" : mockDevServer };
275+
276+
SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb);
277+
278+
XCTAssertNil(result, @"Breadcrumb should be filtered out");
279+
}
280+
281+
- (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs
282+
{
283+
RNSentry *rnSentry = [[RNSentry alloc] init];
284+
NSError *error = nil;
285+
286+
NSDictionary *_Nonnull mockedDictionary = @{
287+
@"dsn" : @"https://[email protected]/1234567",
288+
@"devServerUrl" : @"http://localhost:8081"
289+
};
290+
SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error];
291+
292+
SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init];
293+
breadcrumb.type = @"http";
294+
breadcrumb.data = @{ @"url" : @"http://testurl.com/service" };
295+
296+
SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb);
297+
298+
XCTAssertEqual(breadcrumb, result);
299+
}
300+
301+
- (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch
302+
{
303+
RNSentry *rnSentry = [[RNSentry alloc] init];
304+
NSError *error = nil;
305+
306+
NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization
307+
@"dsn" : @"https://[email protected]/1234567"
308+
};
309+
SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error];
310+
311+
SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init];
312+
breadcrumb.type = @"http";
313+
breadcrumb.data = @{ @"url" : @"http://testurl.com/service" };
314+
315+
SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb);
316+
317+
XCTAssertEqual(breadcrumb, result);
318+
}
319+
241320
- (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags
242321
{
243322
RNSentry *rnSentry = [[RNSentry alloc] init];

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
import java.io.FileReader;
7676
import java.io.IOException;
7777
import java.io.InputStream;
78+
import java.net.URI;
79+
import java.net.URISyntaxException;
7880
import java.nio.charset.Charset;
7981
import java.util.HashMap;
8082
import java.util.Iterator;
@@ -277,6 +279,21 @@ protected void getSentryAndroidOptions(
277279
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
278280
}
279281

282+
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
283+
String dsn = getURLFromDSN(rnOptions.getString("dsn"));
284+
String devServerUrl = rnOptions.getString("devServerUrl");
285+
options.setBeforeBreadcrumb(
286+
(breadcrumb, hint) -> {
287+
Object urlObject = breadcrumb.getData("url");
288+
String url = urlObject instanceof String ? (String) urlObject : "";
289+
if ("http".equals(breadcrumb.getType())
290+
&& ((dsn != null && url.startsWith(dsn))
291+
|| (devServerUrl != null && url.startsWith(devServerUrl)))) {
292+
return null;
293+
}
294+
return breadcrumb;
295+
});
296+
280297
// React native internally throws a JavascriptException.
281298
// we want to ignore it on the native side to avoid sending it twice.
282299
options.addIgnoredExceptionForType(JavascriptException.class);
@@ -1001,4 +1018,17 @@ private boolean checkAndroidXAvailability() {
10011018
private boolean isFrameMetricsAggregatorAvailable() {
10021019
return androidXAvailable && frameMetricsAggregator != null;
10031020
}
1021+
1022+
public static @Nullable String getURLFromDSN(@Nullable String dsn) {
1023+
if (dsn == null) {
1024+
return null;
1025+
}
1026+
URI uri = null;
1027+
try {
1028+
uri = new URI(dsn);
1029+
} catch (URISyntaxException e) {
1030+
return null;
1031+
}
1032+
return uri.getScheme() + "://" + uri.getHost();
1033+
}
10041034
}

packages/core/ios/RNSentry.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,22 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
162162
return nil;
163163
}
164164

165+
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
166+
NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]];
167+
NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"];
168+
sentryOptions.beforeBreadcrumb
169+
= ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb)
170+
{
171+
NSString *url = breadcrumb.data[@"url"] ?: @"";
172+
173+
if ([@"http" isEqualToString:breadcrumb.type]
174+
&& ((dsn != nil && [url hasPrefix:dsn])
175+
|| (devServerUrl != nil && [url hasPrefix:devServerUrl]))) {
176+
return nil;
177+
}
178+
return breadcrumb;
179+
};
180+
165181
if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) {
166182
BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue];
167183

@@ -204,6 +220,15 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
204220
return sentryOptions;
205221
}
206222

223+
- (NSString *_Nullable)getURLFromDSN:(NSString *)dsn
224+
{
225+
NSURL *url = [NSURL URLWithString:dsn];
226+
if (!url) {
227+
return nil;
228+
}
229+
return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host];
230+
}
231+
207232
- (void)setEventOriginTag:(SentryEvent *)event
208233
{
209234
if (event.sdk != nil) {

packages/core/src/js/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils';
1515
import { Alert } from 'react-native';
1616

17+
import { getDevServer } from './integrations/debugsymbolicatorutils';
1718
import { defaultSdkInfo } from './integrations/sdkinfo';
1819
import { getDefaultSidecarUrl } from './integrations/spotlight';
1920
import type { ReactNativeClientOptions } from './options';
@@ -146,6 +147,7 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
146147
NATIVE.initNativeSdk({
147148
...this._options,
148149
defaultSidecarUrl: getDefaultSidecarUrl(),
150+
devServerUrl: getDevServer()?.url || '',
149151
mobileReplayOptions:
150152
this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] &&
151153
'options' in this._integrations[MOBILE_REPLAY_INTEGRATION_NAME]

packages/core/src/js/sdk.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import {
44
defaultStackParser,
55
makeFetchTransport,
66
} from '@sentry/react';
7-
import type { Integration, Scope,UserFeedback } from '@sentry/types';
7+
import type { Breadcrumb, BreadcrumbHint, Integration, Scope, UserFeedback } from '@sentry/types';
88
import { logger, stackParserFromStackParserOptions } from '@sentry/utils';
99
import * as React from 'react';
1010

1111
import { ReactNativeClient } from './client';
12+
import { getDevServer } from './integrations/debugsymbolicatorutils';
1213
import { getDefaultIntegrations } from './integrations/default';
1314
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
1415
import { shouldEnableNativeNagger } from './options';
@@ -62,6 +63,45 @@ export function init(passedOptions: ReactNativeOptions): void {
6263
enableSyncToNative(getIsolationScope());
6364
}
6465

66+
const getURLFromDSN = (dsn: string | null): string | undefined => {
67+
if (!dsn) {
68+
return undefined;
69+
}
70+
try {
71+
const url = new URL(dsn);
72+
return `${url.protocol}//${url.host}`;
73+
} catch (e) {
74+
logger.error('Failed to extract url from DSN', e);
75+
return undefined;
76+
}
77+
};
78+
79+
const userBeforeBreadcrumb = safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' });
80+
81+
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
82+
const devServerUrl = getDevServer()?.url;
83+
const dsn = getURLFromDSN(passedOptions.dsn);
84+
const defaultBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb | null => {
85+
const type = breadcrumb.type || '';
86+
const url = typeof breadcrumb.data?.url === 'string' ? breadcrumb.data.url : '';
87+
if (type === 'http' && ((devServerUrl && url.startsWith(devServerUrl)) || (dsn && url.startsWith(dsn)))) {
88+
return null;
89+
}
90+
return breadcrumb;
91+
};
92+
93+
const chainedBeforeBreadcrumb = (breadcrumb: Breadcrumb, hint?: BreadcrumbHint): Breadcrumb | null => {
94+
let modifiedBreadcrumb = breadcrumb;
95+
if (userBeforeBreadcrumb) {
96+
const result = userBeforeBreadcrumb(breadcrumb, hint);
97+
if (result === null) {
98+
return null;
99+
}
100+
modifiedBreadcrumb = result;
101+
}
102+
return defaultBeforeBreadcrumb(modifiedBreadcrumb, hint);
103+
};
104+
65105
const options: ReactNativeClientOptions = {
66106
...DEFAULT_OPTIONS,
67107
...passedOptions,
@@ -81,7 +121,7 @@ export function init(passedOptions: ReactNativeOptions): void {
81121
maxQueueSize,
82122
integrations: [],
83123
stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser),
84-
beforeBreadcrumb: safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }),
124+
beforeBreadcrumb: chainedBeforeBreadcrumb,
85125
initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }),
86126
};
87127
if ('tracesSampler' in options) {

packages/core/src/js/wrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface Screenshot {
5050
}
5151

5252
export type NativeSdkOptions = Partial<ReactNativeClientOptions> & {
53+
devServerUrl: string | undefined;
5354
defaultSidecarUrl: string | undefined;
5455
} & {
5556
mobileReplayOptions: MobileReplayOptions | undefined;

0 commit comments

Comments
 (0)