From 41b15cfc754415a1aa1a05fad080196b53af51ab Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 5 Feb 2026 15:08:40 +0100 Subject: [PATCH 1/2] An (ongoing) experiment with adding native logs listener --- packages/core/android/libs/replay-stubs.jar | Bin 1200 -> 1198 bytes .../core/ios/RNSentryExperimentalOptions.m | 2 +- packages/core/src/js/NativeLogListener.ts | 58 ++++++++++++++++++ packages/core/src/js/client.ts | 14 +++++ packages/core/src/js/index.ts | 2 +- packages/core/src/js/options.ts | 39 ++++++++++++ packages/core/src/js/wrapper.ts | 1 + samples/expo/app.json | 6 +- .../project.pbxproj | 34 ++++------ samples/react-native/src/App.tsx | 3 + 10 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 packages/core/src/js/NativeLogListener.ts diff --git a/packages/core/android/libs/replay-stubs.jar b/packages/core/android/libs/replay-stubs.jar index 54eb1a20d31828dabb74dc4c4d045668032d00b1..565d1180da5a7c03e7339882f752d051e661a695 100644 GIT binary patch delta 516 zcmdnMxsH=Jz?+#xgn@yBgMq!sbt11C%hV+I=!xc@>L8i{2$(2j!+i+X|%LDn%Kr7$g$lLc91ChIWraDrqwpFhYkpX|u!Q}64|6ewbQeZApU9qsRe zjoV)2s&FiJ4b#@#u;J_MnbEd0(;it${aYQys{4!a?}``aG&BQQa^F>czf=5fcfmdT zxcdxS0`wJ|*EGklR*G<}X12Yib>ZRKT?OoKj8s@ExYTX#?C7f4&U&b z!dELT86R24@x^8ChhDjQ)_0YFF|5i_!@Y=;rcvYmi%sDu50f+abs3xIlS2nqocuipW zyjy?4^OBf}Q649irY#Ly#y8W~kIQW*cT;;7|U_?1B^tPAnO0hrqD} E0B5MgbN~PV delta 500 zcmZ3-xq*{6z?+#xgn@yBgW+hm=|o;Nme73tn2F||>R*BlVioYST zbbyphT&~6frZ4I-15KFB#AwC|q+>i2*!V0bJ2LvzpY~&N6j<|YD~CqMsk;)wA0EVX zbRMh_`08Tf*!=wF-LT_pcBe%c{Fyq>$L0rP-IR^xicUSpj(A@W@eTJqf9Ch`*U}AU zQmQF|ZdMsyWTd!rkdCR>lbbtNPcPArMI5l?0uWdPYUhq<>VDFma%he=ney`tI_Wnym z_yqUH4gcTY7R>VO<0`3GeD1FGwm^d?BP+`qUk7f=KY?A zd6(ZVy5giIwozrPQLY1Ls-w_88{_w!X+r$AwZ+qd)01~u7ptun4Y|ymtr`87CBU1J zNrV|5Ig>9knSf(wGAFYgILs$|GwVVaHOwXu#ujE9a5Q{oc0q~;2bK)BBjAVx0LH+_ AFaQ7m diff --git a/packages/core/ios/RNSentryExperimentalOptions.m b/packages/core/ios/RNSentryExperimentalOptions.m index 084ed36309..d0feeb9981 100644 --- a/packages/core/ios/RNSentryExperimentalOptions.m +++ b/packages/core/ios/RNSentryExperimentalOptions.m @@ -34,7 +34,7 @@ + (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled if (sentryOptions == nil) { return; } - sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled; + // sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled; } + (void)configureProfilingWithOptions:(NSDictionary *)profilingOptions diff --git a/packages/core/src/js/NativeLogListener.ts b/packages/core/src/js/NativeLogListener.ts new file mode 100644 index 0000000000..f156c0a09e --- /dev/null +++ b/packages/core/src/js/NativeLogListener.ts @@ -0,0 +1,58 @@ +import { debug } from '@sentry/core'; +import { Platform } from 'react-native'; +import type { NativeLogEntry } from './options'; + +/** + * Sets up the native log listener that forwards logs from the native SDK to JS. + * This only works when `debug: true` is set in Sentry options. + * + * Note: Native log forwarding is not yet implemented. This function is a placeholder + * for future implementation. Currently, native SDK logs appear in Xcode console (iOS) + * or Logcat (Android) when `debug: true` is set. + * + * @param _callback - The callback to invoke when a native log is received. + * @returns A function to remove the listener, or undefined if setup failed. + */ +export function setupNativeLogListener(_callback: (log: NativeLogEntry) => void): (() => void) | undefined { + if (Platform.OS !== 'ios' && Platform.OS !== 'android') { + debug.log('Native log listener is only supported on iOS and Android.'); + return undefined; + } + + // Native log forwarding is not yet implemented. + // The infrastructure is in place for when native SDKs support log callbacks. + debug.log( + 'Native log forwarding is not yet implemented. Native SDK logs will appear in Xcode console (iOS) or Logcat (Android) when debug mode is enabled.', + ); + + return undefined; +} + +/** + * Default handler for native logs that logs to the JS console. + */ +export function defaultNativeLogHandler(log: NativeLogEntry): void { + const prefix = `[Sentry] [${log.level.toUpperCase()}] [${log.component}]`; + const message = `${prefix} ${log.message}`; + + switch (log.level.toLowerCase()) { + case 'fatal': + case 'error': + // eslint-disable-next-line no-console + console.error(message); + break; + case 'warning': + // eslint-disable-next-line no-console + console.warn(message); + break; + case 'info': + // eslint-disable-next-line no-console + console.info(message); + break; + case 'debug': + default: + // eslint-disable-next-line no-console + console.log(message); + break; + } +} diff --git a/packages/core/src/js/client.ts b/packages/core/src/js/client.ts index 74a834090a..d481d96b52 100644 --- a/packages/core/src/js/client.ts +++ b/packages/core/src/js/client.ts @@ -22,6 +22,7 @@ import { Alert } from 'react-native'; import { getDevServer } from './integrations/debugsymbolicatorutils'; import { defaultSdkInfo } from './integrations/sdkinfo'; import { getDefaultSidecarUrl } from './integrations/spotlight'; +import { defaultNativeLogHandler, setupNativeLogListener } from './NativeLogListener'; import type { ReactNativeClientOptions } from './options'; import type { mobileReplayIntegration } from './replay/mobilereplay'; import { MOBILE_REPLAY_INTEGRATION_NAME } from './replay/mobilereplay'; @@ -42,6 +43,7 @@ const DEFAULT_FLUSH_INTERVAL = 5000; export class ReactNativeClient extends Client { private _outcomesBuffer: Outcome[]; private _logFlushIdleTimeout: ReturnType | undefined; + private _removeNativeLogListener: (() => void) | undefined; /** * Creates a new React Native SDK instance. @@ -127,6 +129,12 @@ export class ReactNativeClient extends Client { * @inheritDoc */ public close(): PromiseLike { + // Clean up native log listener + if (this._removeNativeLogListener) { + this._removeNativeLogListener(); + this._removeNativeLogListener = undefined; + } + // As super.close() flushes queued events, we wait for that to finish before closing the native SDK. return super.close().then((result: boolean) => { return NATIVE.closeNativeSdk().then(() => result); @@ -215,6 +223,12 @@ export class ReactNativeClient extends Client { * Starts native client with dsn and options */ private _initNativeSdk(): void { + // Set up native log listener if debug is enabled + if (this._options.debug) { + const logHandler = this._options.onNativeLog ?? defaultNativeLogHandler; + this._removeNativeLogListener = setupNativeLogListener(logHandler); + } + NATIVE.initNativeSdk({ ...this._options, defaultSidecarUrl: getDefaultSidecarUrl(), diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 19ba331003..cb92e30771 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -69,7 +69,7 @@ export { export * from './integrations/exports'; export { SDK_NAME, SDK_VERSION } from './version'; -export type { ReactNativeOptions } from './options'; +export type { ReactNativeOptions, NativeLogEntry } from './options'; export { ReactNativeClient } from './client'; export { init, wrap, nativeCrash, flush, close, withScope, crashedLastRun } from './sdk'; diff --git a/packages/core/src/js/options.ts b/packages/core/src/js/options.ts index 20f2e7207d..40afcc33a1 100644 --- a/packages/core/src/js/options.ts +++ b/packages/core/src/js/options.ts @@ -347,6 +347,45 @@ export interface BaseReactNativeOptions { * @default 'all' */ logsOrigin?: 'all' | 'js' | 'native'; + + /** + * A callback that is invoked when the native SDK emits a log message. + * This is useful for surfacing native SDK logs (e.g., transport errors like HTTP 413) + * in the JavaScript console. + * + * Only works when `debug: true` is set. + * + * @example + * ```typescript + * Sentry.init({ + * debug: true, + * onNativeLog: ({ level, component, message }) => { + * console.log(`[Sentry Native] [${level}] [${component}] ${message}`); + * }, + * }); + * ``` + */ + onNativeLog?: (log: NativeLogEntry) => void; +} + +/** + * Represents a log entry from the native SDK. + */ +export interface NativeLogEntry { + /** + * The log level (e.g., 'debug', 'info', 'warning', 'error', 'fatal'). + */ + level: string; + + /** + * The component or module that emitted the log (e.g., 'Sentry', 'SentryHttpTransport'). + */ + component: string; + + /** + * The log message. + */ + message: string; } export type SentryReplayQuality = 'low' | 'medium' | 'high'; diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 35d686e39d..def9e48960 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -293,6 +293,7 @@ export const NATIVE: SentryNativeWrapper = { logsOrigin, profilingOptions, androidProfilingOptions, + onNativeLog, ...filteredOptions } = options; /* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */ diff --git a/samples/expo/app.json b/samples/expo/app.json index d80e040cf0..9c5dbe0d63 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -14,9 +14,7 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", @@ -90,4 +88,4 @@ "url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000" } } -} \ No newline at end of file +} diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj index e98d264df9..c66b30e41e 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj @@ -282,14 +282,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks.sh\"\n"; @@ -317,14 +313,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources.sh\"\n"; @@ -382,14 +374,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks.sh\"\n"; @@ -403,14 +391,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources.sh\"\n"; @@ -501,7 +485,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; @@ -523,7 +507,7 @@ PRODUCT_NAME = sentryreactnativesample; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample"; - RCT_NEW_ARCH_ENABLED = 1; + RCT_NEW_ARCH_ENABLED = 1; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -538,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; @@ -559,7 +543,7 @@ PRODUCT_NAME = sentryreactnativesample; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample"; - RCT_NEW_ARCH_ENABLED = 1; + RCT_NEW_ARCH_ENABLED = 1; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -640,7 +624,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -713,7 +700,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index f9436b8ed2..38c7e96fcb 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -47,6 +47,9 @@ const BottomTabNavigator = createBottomTabNavigator(); Sentry.init({ // Replace the example DSN below with your own DSN: dsn: getDsn(), + onNativeLog: ({ level, component, message }) => { + console.log(`ALWX [Sentry Native] [${level}] [${component}] ${message}`); + }, debug: true, environment: 'dev', beforeSend: (event: Sentry.ErrorEvent) => { From 87a35b9038a847b6fef8c511ce2c1b78404f6442 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 5 Feb 2026 18:28:51 +0100 Subject: [PATCH 2/2] NativeLogListener.ts --- packages/core/ios/RNSentry.mm | 5 +- packages/core/ios/RNSentryEvents.h | 1 + packages/core/ios/RNSentryEvents.m | 1 + packages/core/src/js/NativeLogListener.ts | 58 ++++++-- packages/core/test/NativeLogListener.test.ts | 144 +++++++++++++++++++ samples/react-native/src/App.tsx | 6 +- 6 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 packages/core/test/NativeLogListener.test.ts diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 3461af87af..cfc274aa76 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -41,6 +41,7 @@ #import "RNSentryDependencyContainer.h" #import "RNSentryEvents.h" +#import "RNSentryNativeLogsForwarder.h" #if SENTRY_TARGET_REPLAY_SUPPORTED # import "RNSentryReplay.h" @@ -311,17 +312,19 @@ - (void)initFramesTracking - (void)startObserving { hasListeners = YES; + [[RNSentryNativeLogsForwarder shared] configureWithEventEmitter:self]; } // Will be called when this module's last listener is removed, or on dealloc. - (void)stopObserving { hasListeners = NO; + [[RNSentryNativeLogsForwarder shared] stopForwarding]; } - (NSArray *)supportedEvents { - return @[ RNSentryNewFrameEvent ]; + return @[ RNSentryNewFrameEvent, RNSentryNativeLogEvent ]; } RCT_EXPORT_METHOD( diff --git a/packages/core/ios/RNSentryEvents.h b/packages/core/ios/RNSentryEvents.h index ee9f5e2088..6f1a5f0540 100644 --- a/packages/core/ios/RNSentryEvents.h +++ b/packages/core/ios/RNSentryEvents.h @@ -1,3 +1,4 @@ #import extern NSString *const RNSentryNewFrameEvent; +extern NSString *const RNSentryNativeLogEvent; diff --git a/packages/core/ios/RNSentryEvents.m b/packages/core/ios/RNSentryEvents.m index 13e3669cdd..bb3e842d73 100644 --- a/packages/core/ios/RNSentryEvents.m +++ b/packages/core/ios/RNSentryEvents.m @@ -1,3 +1,4 @@ #import "RNSentryEvents.h" NSString *const RNSentryNewFrameEvent = @"rn_sentry_new_frame"; +NSString *const RNSentryNativeLogEvent = @"SentryNativeLog"; diff --git a/packages/core/src/js/NativeLogListener.ts b/packages/core/src/js/NativeLogListener.ts index f156c0a09e..8d07e7bdc2 100644 --- a/packages/core/src/js/NativeLogListener.ts +++ b/packages/core/src/js/NativeLogListener.ts @@ -1,31 +1,63 @@ import { debug } from '@sentry/core'; -import { Platform } from 'react-native'; +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import type { NativeLogEntry } from './options'; +const NATIVE_LOG_EVENT_NAME = 'SentryNativeLog'; + +let nativeLogListener: ReturnType | null = null; + /** * Sets up the native log listener that forwards logs from the native SDK to JS. * This only works when `debug: true` is set in Sentry options. * - * Note: Native log forwarding is not yet implemented. This function is a placeholder - * for future implementation. Currently, native SDK logs appear in Xcode console (iOS) - * or Logcat (Android) when `debug: true` is set. - * - * @param _callback - The callback to invoke when a native log is received. + * @param callback - The callback to invoke when a native log is received. * @returns A function to remove the listener, or undefined if setup failed. */ -export function setupNativeLogListener(_callback: (log: NativeLogEntry) => void): (() => void) | undefined { +export function setupNativeLogListener(callback: (log: NativeLogEntry) => void): (() => void) | undefined { if (Platform.OS !== 'ios' && Platform.OS !== 'android') { debug.log('Native log listener is only supported on iOS and Android.'); return undefined; } - // Native log forwarding is not yet implemented. - // The infrastructure is in place for when native SDKs support log callbacks. - debug.log( - 'Native log forwarding is not yet implemented. Native SDK logs will appear in Xcode console (iOS) or Logcat (Android) when debug mode is enabled.', - ); + if (!NativeModules.RNSentry) { + debug.warn('Could not set up native log listener: RNSentry module not found.'); + return undefined; + } + + try { + // Remove existing listener if any + if (nativeLogListener) { + nativeLogListener.remove(); + nativeLogListener = null; + } - return undefined; + const eventEmitter = new NativeEventEmitter(NativeModules.RNSentry); + + nativeLogListener = eventEmitter.addListener( + NATIVE_LOG_EVENT_NAME, + (event: { level?: string; component?: string; message?: string }) => { + const logEntry: NativeLogEntry = { + level: event.level ?? 'info', + component: event.component ?? 'Sentry', + message: event.message ?? '', + }; + callback(logEntry); + }, + ); + + debug.log('Native log listener set up successfully.'); + + return () => { + if (nativeLogListener) { + nativeLogListener.remove(); + nativeLogListener = null; + debug.log('Native log listener removed.'); + } + }; + } catch (error) { + debug.warn('Failed to set up native log listener:', error); + return undefined; + } } /** diff --git a/packages/core/test/NativeLogListener.test.ts b/packages/core/test/NativeLogListener.test.ts new file mode 100644 index 0000000000..923c08347a --- /dev/null +++ b/packages/core/test/NativeLogListener.test.ts @@ -0,0 +1,144 @@ +/* eslint-disable no-console */ +import { defaultNativeLogHandler, setupNativeLogListener } from '../src/js/NativeLogListener'; +import type { NativeLogEntry } from '../src/js/options'; + +jest.mock('react-native', () => ({ + NativeModules: { + RNSentry: {}, + }, + NativeEventEmitter: jest.fn().mockImplementation(() => ({ + addListener: jest.fn().mockReturnValue({ + remove: jest.fn(), + }), + })), + Platform: { + OS: 'ios', + }, +})); + +describe('NativeLogListener', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('setupNativeLogListener', () => { + it('returns a cleanup function on success', () => { + const callback = jest.fn(); + const cleanup = setupNativeLogListener(callback); + + expect(cleanup).toBeDefined(); + expect(typeof cleanup).toBe('function'); + }); + + it('returns undefined when platform is not ios or android', () => { + jest.resetModules(); + jest.doMock('react-native', () => ({ + NativeModules: { + RNSentry: {}, + }, + NativeEventEmitter: jest.fn(), + Platform: { + OS: 'web', + }, + })); + + // Need to re-import after mocking + const { setupNativeLogListener: setupNativeLogListenerWeb } = jest.requireActual('../src/js/NativeLogListener'); + + const callback = jest.fn(); + const cleanup = setupNativeLogListenerWeb(callback); + + expect(cleanup).toBeUndefined(); + }); + }); + + describe('defaultNativeLogHandler', () => { + const originalConsole = { ...console }; + + beforeEach(() => { + console.log = jest.fn(); + console.info = jest.fn(); + console.warn = jest.fn(); + console.error = jest.fn(); + }); + + afterEach(() => { + console.log = originalConsole.log; + console.info = originalConsole.info; + console.warn = originalConsole.warn; + console.error = originalConsole.error; + }); + + it('logs error level to console.error', () => { + const log: NativeLogEntry = { + level: 'error', + component: 'TestComponent', + message: 'Test error message', + }; + + defaultNativeLogHandler(log); + + expect(console.error).toHaveBeenCalledWith('[Sentry] [ERROR] [TestComponent] Test error message'); + }); + + it('logs fatal level to console.error', () => { + const log: NativeLogEntry = { + level: 'fatal', + component: 'TestComponent', + message: 'Test fatal message', + }; + + defaultNativeLogHandler(log); + + expect(console.error).toHaveBeenCalledWith('[Sentry] [FATAL] [TestComponent] Test fatal message'); + }); + + it('logs warning level to console.warn', () => { + const log: NativeLogEntry = { + level: 'warning', + component: 'TestComponent', + message: 'Test warning message', + }; + + defaultNativeLogHandler(log); + + expect(console.warn).toHaveBeenCalledWith('[Sentry] [WARNING] [TestComponent] Test warning message'); + }); + + it('logs info level to console.info', () => { + const log: NativeLogEntry = { + level: 'info', + component: 'TestComponent', + message: 'Test info message', + }; + + defaultNativeLogHandler(log); + + expect(console.info).toHaveBeenCalledWith('[Sentry] [INFO] [TestComponent] Test info message'); + }); + + it('logs debug level to console.log', () => { + const log: NativeLogEntry = { + level: 'debug', + component: 'TestComponent', + message: 'Test debug message', + }; + + defaultNativeLogHandler(log); + + expect(console.log).toHaveBeenCalledWith('[Sentry] [DEBUG] [TestComponent] Test debug message'); + }); + + it('logs unknown level to console.log', () => { + const log: NativeLogEntry = { + level: 'unknown', + component: 'TestComponent', + message: 'Test unknown message', + }; + + defaultNativeLogHandler(log); + + expect(console.log).toHaveBeenCalledWith('[Sentry] [UNKNOWN] [TestComponent] Test unknown message'); + }); + }); +}); diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 38c7e96fcb..555a852257 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -48,9 +48,11 @@ Sentry.init({ // Replace the example DSN below with your own DSN: dsn: getDsn(), onNativeLog: ({ level, component, message }) => { - console.log(`ALWX [Sentry Native] [${level}] [${component}] ${message}`); + if (level === 'fatal') { + console.log(`ALWX [Sentry Native] [${level}] [${component}] ${message}`); + } }, - debug: true, + debug: false, environment: 'dev', beforeSend: (event: Sentry.ErrorEvent) => { logWithoutTracing('Event beforeSend:', event.event_id);