diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ecdd4f3f0..fa379826b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Prevents exception capture context from being overwritten by native scope sync ([#4124](https://github.com/getsentry/sentry-react-native/pull/4124)) +- Excludes Dev Server and Sentry Dsn requests from Breadcrumbs ([#4240](https://github.com/getsentry/sentry-react-native/pull/4240)) ## 6.2.0 diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt index 448715d43d..b181625138 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt @@ -8,12 +8,14 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.common.JavascriptException +import io.sentry.Breadcrumb import io.sentry.ILogger import io.sentry.SentryLevel import io.sentry.android.core.SentryAndroidOptions import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -134,4 +136,109 @@ class RNSentryModuleImplTest { module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), logger) assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) } + + @Test + fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of( + "dsn", "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", "http://localhost:8081", + ) + module.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", "https://def.ingest.sentry.io/1234567") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { + val mockDevServerUrl = "http://localhost:8081" + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of( + "dsn", "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", mockDevServerUrl, + ) + module.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", mockDevServerUrl) + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of( + "dsn", "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", "http://localhost:8081", + ) + module.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { + val options = SentryAndroidOptions() + module.getSentryAndroidOptions(options, JavaOnlyMap(), logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") + module.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") + module.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } } diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index a72ed1e0bf..df720ee1af 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -238,6 +238,85 @@ - (void)testPassesErrorOnWrongDsn XCTAssertNotNil(error, @"Did not created error on invalid dsn"); } +- (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"devServerUrl" : @"http://localhost:8081" + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"https://def.ingest.sentry.io/1234567" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertNil(result, @"Breadcrumb should be filtered out"); +} + +- (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSString *mockDevServer = @"http://localhost:8081"; + + NSDictionary *_Nonnull mockedDictionary = + @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : mockDevServer }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : mockDevServer }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertNil(result, @"Breadcrumb should be filtered out"); +} + +- (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"devServerUrl" : @"http://localhost:8081" + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"http://testurl.com/service" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertEqual(breadcrumb, result); +} + +- (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567" + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"http://testurl.com/service" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertEqual(breadcrumb, result); +} + - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { RNSentry *rnSentry = [[RNSentry alloc] init]; 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 5ae47a0ae6..9366662cc6 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 @@ -75,6 +75,8 @@ import java.io.FileReader; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; @@ -277,6 +279,21 @@ protected void getSentryAndroidOptions( options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); } + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + String dsn = getURLFromDSN(rnOptions.getString("dsn")); + String devServerUrl = rnOptions.getString("devServerUrl"); + options.setBeforeBreadcrumb( + (breadcrumb, hint) -> { + Object urlObject = breadcrumb.getData("url"); + String url = urlObject instanceof String ? (String) urlObject : ""; + if ("http".equals(breadcrumb.getType()) + && ((dsn != null && url.startsWith(dsn)) + || (devServerUrl != null && url.startsWith(devServerUrl)))) { + return null; + } + return breadcrumb; + }); + // React native internally throws a JavascriptException. // we want to ignore it on the native side to avoid sending it twice. options.addIgnoredExceptionForType(JavascriptException.class); @@ -1001,4 +1018,17 @@ private boolean checkAndroidXAvailability() { private boolean isFrameMetricsAggregatorAvailable() { return androidXAvailable && frameMetricsAggregator != null; } + + public static @Nullable String getURLFromDSN(@Nullable String dsn) { + if (dsn == null) { + return null; + } + URI uri = null; + try { + uri = new URI(dsn); + } catch (URISyntaxException e) { + return null; + } + return uri.getScheme() + "://" + uri.getHost(); + } } diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 67803b8491..ab22df9ec8 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -162,6 +162,22 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) return nil; } + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; + sentryOptions.beforeBreadcrumb + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + { + NSString *url = breadcrumb.data[@"url"] ?: @""; + + if ([@"http" isEqualToString:breadcrumb.type] + && ((dsn != nil && [url hasPrefix:dsn]) + || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { + return nil; + } + return breadcrumb; + }; + if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; @@ -204,6 +220,15 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) return sentryOptions; } +- (NSString *_Nullable)getURLFromDSN:(NSString *)dsn +{ + NSURL *url = [NSURL URLWithString:dsn]; + if (!url) { + return nil; + } + return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; +} + - (void)setEventOriginTag:(SentryEvent *)event { if (event.sdk != nil) { diff --git a/packages/core/src/js/client.ts b/packages/core/src/js/client.ts index eba33d13d3..b9fe2ad27d 100644 --- a/packages/core/src/js/client.ts +++ b/packages/core/src/js/client.ts @@ -14,6 +14,7 @@ import type { import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils'; import { Alert } from 'react-native'; +import { getDevServer } from './integrations/debugsymbolicatorutils'; import { defaultSdkInfo } from './integrations/sdkinfo'; import { getDefaultSidecarUrl } from './integrations/spotlight'; import type { ReactNativeClientOptions } from './options'; @@ -146,6 +147,7 @@ export class ReactNativeClient extends BaseClient { NATIVE.initNativeSdk({ ...this._options, defaultSidecarUrl: getDefaultSidecarUrl(), + devServerUrl: getDevServer()?.url || '', mobileReplayOptions: this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] && 'options' in this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 70e74300cd..039b44850d 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -4,11 +4,12 @@ import { defaultStackParser, makeFetchTransport, } from '@sentry/react'; -import type { Integration, Scope,UserFeedback } from '@sentry/types'; +import type { Breadcrumb, BreadcrumbHint, Integration, Scope, UserFeedback } from '@sentry/types'; import { logger, stackParserFromStackParserOptions } from '@sentry/utils'; import * as React from 'react'; import { ReactNativeClient } from './client'; +import { getDevServer } from './integrations/debugsymbolicatorutils'; import { getDefaultIntegrations } from './integrations/default'; import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options'; import { shouldEnableNativeNagger } from './options'; @@ -62,6 +63,45 @@ export function init(passedOptions: ReactNativeOptions): void { enableSyncToNative(getIsolationScope()); } + const getURLFromDSN = (dsn: string | null): string | undefined => { + if (!dsn) { + return undefined; + } + try { + const url = new URL(dsn); + return `${url.protocol}//${url.host}`; + } catch (e) { + logger.error('Failed to extract url from DSN', e); + return undefined; + } + }; + + const userBeforeBreadcrumb = safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + const devServerUrl = getDevServer()?.url; + const dsn = getURLFromDSN(passedOptions.dsn); + const defaultBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb | null => { + const type = breadcrumb.type || ''; + const url = typeof breadcrumb.data?.url === 'string' ? breadcrumb.data.url : ''; + if (type === 'http' && ((devServerUrl && url.startsWith(devServerUrl)) || (dsn && url.startsWith(dsn)))) { + return null; + } + return breadcrumb; + }; + + const chainedBeforeBreadcrumb = (breadcrumb: Breadcrumb, hint?: BreadcrumbHint): Breadcrumb | null => { + let modifiedBreadcrumb = breadcrumb; + if (userBeforeBreadcrumb) { + const result = userBeforeBreadcrumb(breadcrumb, hint); + if (result === null) { + return null; + } + modifiedBreadcrumb = result; + } + return defaultBeforeBreadcrumb(modifiedBreadcrumb, hint); + }; + const options: ReactNativeClientOptions = { ...DEFAULT_OPTIONS, ...passedOptions, @@ -81,7 +121,7 @@ export function init(passedOptions: ReactNativeOptions): void { maxQueueSize, integrations: [], stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser), - beforeBreadcrumb: safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }), + beforeBreadcrumb: chainedBeforeBreadcrumb, initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), }; if ('tracesSampler' in options) { diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index f32652a0ce..ef7396f666 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -50,6 +50,7 @@ export interface Screenshot { } export type NativeSdkOptions = Partial & { + devServerUrl: string | undefined; defaultSidecarUrl: string | undefined; } & { mobileReplayOptions: MobileReplayOptions | undefined; diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index b5487340cd..2f9f6e4e69 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -1,8 +1,16 @@ import { initAndBind } from '@sentry/core'; import { makeFetchTransport } from '@sentry/react'; -import type { BaseTransportOptions, ClientOptions, Integration, Scope } from '@sentry/types'; +import type { + BaseTransportOptions, + Breadcrumb, + BreadcrumbHint, + ClientOptions, + Integration, + Scope, +} from '@sentry/types'; import { logger } from '@sentry/utils'; +import { getDevServer } from '../src/js/integrations/debugsymbolicatorutils'; import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; @@ -18,6 +26,9 @@ jest.mock('@sentry/core', () => ({ ...jest.requireActual('@sentry/core'), initAndBind: jest.fn(), })); +jest.mock('../src/js/integrations/debugsymbolicatorutils', () => ({ + getDevServer: jest.fn(), +})); describe('Tests the SDK functionality', () => { beforeEach(() => { @@ -292,6 +303,186 @@ describe('Tests the SDK functionality', () => { }); }); + describe('beforeBreadcrumb', () => { + it('should filters out dev server breadcrumbs', () => { + const devServerUrl = 'http://localhost:8081'; + (getDevServer as jest.Mock).mockReturnValue({ url: devServerUrl }); + + const mockBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint) => { + return breadcrumb; + }; + + const passedOptions = { + dsn: 'https://example@sentry.io/123', + beforeBreadcrumb: mockBeforeBreadcrumb, + }; + + init(passedOptions); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: devServerUrl }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toBeNull(); + }); + + it('should filters out dsn breadcrumbs', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: 'http://localhost:8081' }); + + const mockBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint) => { + return breadcrumb; + }; + + const passedOptions = { + dsn: 'https://abc@def.ingest.sentry.io/1234567', + beforeBreadcrumb: mockBeforeBreadcrumb, + }; + + init(passedOptions); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'https://def.ingest.sentry.io/1234567' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toBeNull(); + }); + + it('should keep breadcrumbs matching dsn if the url parsing fails for dsn', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: 'http://localhost:8081' }); + + const mockBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint) => { + return breadcrumb; + }; + + // Mock the URL constructor to throw an exception for this test case + const originalURL = (global as any).URL; + jest.spyOn(global as any, 'URL').mockImplementationOnce(() => { + throw new Error('Failed to parse DSN URL'); + }); + + const passedOptions = { + dsn: 'https://abc@def.ingest.sentry.io/1234567', + beforeBreadcrumb: mockBeforeBreadcrumb, + }; + + init(passedOptions); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'https://def.ingest.sentry.io/1234567' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toEqual(breadcrumb); + + // Restore the original URL constructor + (global as any).URL = originalURL; + }); + + it('should keep non dev server or dsn breadcrumbs', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: 'http://localhost:8081' }); + + const mockBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint) => { + return breadcrumb; + }; + + const passedOptions = { + dsn: 'https://example@sentry.io/123', + beforeBreadcrumb: mockBeforeBreadcrumb, + }; + + init(passedOptions); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'http://testurl.com/service' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toEqual(breadcrumb); + }); + + it('verify the user beforeBreadcrumb is chained', () => { + const devServerUrl = 'http://localhost:8081'; + + (getDevServer as jest.Mock).mockReturnValue({ url: devServerUrl }); + + const mockBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint) => { + breadcrumb.data = { url: devServerUrl }; // Set to an excluded url + return breadcrumb; + }; + + const passedOptions = { + dsn: 'https://example@sentry.io/123', + beforeBreadcrumb: mockBeforeBreadcrumb, + }; + + init(passedOptions); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'http://testurl.com/service' }, // Not an excluded url + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toBeNull(); + }); + }); + + it('should keep the breadcrumb when the dev server and dsn are undefined', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: undefined }); + + init({}); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'http://testurl.com/service' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toEqual(breadcrumb); + }); + + it('should keep the breadcrumb when the dev server does not match and the dsn is undefined', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: 'http://localhost:8081' }); + + init({}); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'http://testurl.com/service' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toEqual(breadcrumb); + }); + + it('should keep the breadcrumb when the dev server is undefined and the dsn does not match', () => { + (getDevServer as jest.Mock).mockReturnValue({ url: undefined }); + + init({ dsn: 'https://example@sentry.io/123' }); + + const breadcrumb: Breadcrumb = { + type: 'http', + data: { url: 'http://testurl.com/service' }, + }; + + const result = usedOptions()?.beforeBreadcrumb!(breadcrumb); + + expect(result).toEqual(breadcrumb); + }); + describe('withScope', () => { test('withScope callback does not throw', () => { const mockScopeCallback = jest.fn(() => { diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index ff54667211..34869b9f7c 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -99,6 +99,7 @@ describe('Tests Native Wrapper', () => { await NATIVE.initNativeSdk({ dsn: 'test', enableNative: true, + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -109,7 +110,12 @@ describe('Tests Native Wrapper', () => { test('warns if there is no dsn', async () => { logger.warn = jest.fn(); - await NATIVE.initNativeSdk({ enableNative: true, defaultSidecarUrl: undefined, mobileReplayOptions: undefined }); + await NATIVE.initNativeSdk({ + enableNative: true, + defaultSidecarUrl: undefined, + devServerUrl: undefined, + mobileReplayOptions: undefined, + }); expect(RNSentry.initNativeSdk).not.toBeCalled(); expect(logger.warn).toHaveBeenLastCalledWith( @@ -124,6 +130,7 @@ describe('Tests Native Wrapper', () => { dsn: 'test', enableNative: false, enableNativeNagger: true, + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -139,6 +146,7 @@ describe('Tests Native Wrapper', () => { enableNative: true, autoInitializeNativeSdk: true, beforeSend: jest.fn(), + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -156,6 +164,7 @@ describe('Tests Native Wrapper', () => { enableNative: true, autoInitializeNativeSdk: true, beforeBreadcrumb: jest.fn(), + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -173,6 +182,7 @@ describe('Tests Native Wrapper', () => { enableNative: true, autoInitializeNativeSdk: true, beforeSendTransaction: jest.fn(), + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -190,6 +200,7 @@ describe('Tests Native Wrapper', () => { enableNative: true, autoInitializeNativeSdk: true, integrations: [], + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -209,6 +220,7 @@ describe('Tests Native Wrapper', () => { dsn: 'test', enableNative: true, autoInitializeNativeSdk: false, + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -249,6 +261,7 @@ describe('Tests Native Wrapper', () => { logger.warn = jest.fn(); await NATIVE.initNativeSdk({ + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, dsn: 'test', @@ -335,6 +348,7 @@ describe('Tests Native Wrapper', () => { await NATIVE.initNativeSdk({ dsn: 'test-dsn', enableNative: false, + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, }); @@ -532,6 +546,7 @@ describe('Tests Native Wrapper', () => { await NATIVE.initNativeSdk({ dsn: 'test-dsn', enableNative: false, + devServerUrl: undefined, defaultSidecarUrl: undefined, mobileReplayOptions: undefined, });