Skip to content

Commit 41b15cf

Browse files
committed
An (ongoing) experiment with adding native logs listener
1 parent b1579bc commit 41b15cf

10 files changed

Lines changed: 131 additions & 28 deletions

File tree

-2 Bytes
Binary file not shown.

packages/core/ios/RNSentryExperimentalOptions.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ + (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled
3434
if (sentryOptions == nil) {
3535
return;
3636
}
37-
sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled;
37+
// sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled;
3838
}
3939

4040
+ (void)configureProfilingWithOptions:(NSDictionary *)profilingOptions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { debug } from '@sentry/core';
2+
import { Platform } from 'react-native';
3+
import type { NativeLogEntry } from './options';
4+
5+
/**
6+
* Sets up the native log listener that forwards logs from the native SDK to JS.
7+
* This only works when `debug: true` is set in Sentry options.
8+
*
9+
* Note: Native log forwarding is not yet implemented. This function is a placeholder
10+
* for future implementation. Currently, native SDK logs appear in Xcode console (iOS)
11+
* or Logcat (Android) when `debug: true` is set.
12+
*
13+
* @param _callback - The callback to invoke when a native log is received.
14+
* @returns A function to remove the listener, or undefined if setup failed.
15+
*/
16+
export function setupNativeLogListener(_callback: (log: NativeLogEntry) => void): (() => void) | undefined {
17+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
18+
debug.log('Native log listener is only supported on iOS and Android.');
19+
return undefined;
20+
}
21+
22+
// Native log forwarding is not yet implemented.
23+
// The infrastructure is in place for when native SDKs support log callbacks.
24+
debug.log(
25+
'Native log forwarding is not yet implemented. Native SDK logs will appear in Xcode console (iOS) or Logcat (Android) when debug mode is enabled.',
26+
);
27+
28+
return undefined;
29+
}
30+
31+
/**
32+
* Default handler for native logs that logs to the JS console.
33+
*/
34+
export function defaultNativeLogHandler(log: NativeLogEntry): void {
35+
const prefix = `[Sentry] [${log.level.toUpperCase()}] [${log.component}]`;
36+
const message = `${prefix} ${log.message}`;
37+
38+
switch (log.level.toLowerCase()) {
39+
case 'fatal':
40+
case 'error':
41+
// eslint-disable-next-line no-console
42+
console.error(message);
43+
break;
44+
case 'warning':
45+
// eslint-disable-next-line no-console
46+
console.warn(message);
47+
break;
48+
case 'info':
49+
// eslint-disable-next-line no-console
50+
console.info(message);
51+
break;
52+
case 'debug':
53+
default:
54+
// eslint-disable-next-line no-console
55+
console.log(message);
56+
break;
57+
}
58+
}

packages/core/src/js/client.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Alert } from 'react-native';
2222
import { getDevServer } from './integrations/debugsymbolicatorutils';
2323
import { defaultSdkInfo } from './integrations/sdkinfo';
2424
import { getDefaultSidecarUrl } from './integrations/spotlight';
25+
import { defaultNativeLogHandler, setupNativeLogListener } from './NativeLogListener';
2526
import type { ReactNativeClientOptions } from './options';
2627
import type { mobileReplayIntegration } from './replay/mobilereplay';
2728
import { MOBILE_REPLAY_INTEGRATION_NAME } from './replay/mobilereplay';
@@ -42,6 +43,7 @@ const DEFAULT_FLUSH_INTERVAL = 5000;
4243
export class ReactNativeClient extends Client<ReactNativeClientOptions> {
4344
private _outcomesBuffer: Outcome[];
4445
private _logFlushIdleTimeout: ReturnType<typeof setTimeout> | undefined;
46+
private _removeNativeLogListener: (() => void) | undefined;
4547

4648
/**
4749
* Creates a new React Native SDK instance.
@@ -127,6 +129,12 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
127129
* @inheritDoc
128130
*/
129131
public close(): PromiseLike<boolean> {
132+
// Clean up native log listener
133+
if (this._removeNativeLogListener) {
134+
this._removeNativeLogListener();
135+
this._removeNativeLogListener = undefined;
136+
}
137+
130138
// As super.close() flushes queued events, we wait for that to finish before closing the native SDK.
131139
return super.close().then((result: boolean) => {
132140
return NATIVE.closeNativeSdk().then(() => result);
@@ -215,6 +223,12 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
215223
* Starts native client with dsn and options
216224
*/
217225
private _initNativeSdk(): void {
226+
// Set up native log listener if debug is enabled
227+
if (this._options.debug) {
228+
const logHandler = this._options.onNativeLog ?? defaultNativeLogHandler;
229+
this._removeNativeLogListener = setupNativeLogListener(logHandler);
230+
}
231+
218232
NATIVE.initNativeSdk({
219233
...this._options,
220234
defaultSidecarUrl: getDefaultSidecarUrl(),

packages/core/src/js/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export {
6969
export * from './integrations/exports';
7070

7171
export { SDK_NAME, SDK_VERSION } from './version';
72-
export type { ReactNativeOptions } from './options';
72+
export type { ReactNativeOptions, NativeLogEntry } from './options';
7373
export { ReactNativeClient } from './client';
7474

7575
export { init, wrap, nativeCrash, flush, close, withScope, crashedLastRun } from './sdk';

packages/core/src/js/options.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,45 @@ export interface BaseReactNativeOptions {
347347
* @default 'all'
348348
*/
349349
logsOrigin?: 'all' | 'js' | 'native';
350+
351+
/**
352+
* A callback that is invoked when the native SDK emits a log message.
353+
* This is useful for surfacing native SDK logs (e.g., transport errors like HTTP 413)
354+
* in the JavaScript console.
355+
*
356+
* Only works when `debug: true` is set.
357+
*
358+
* @example
359+
* ```typescript
360+
* Sentry.init({
361+
* debug: true,
362+
* onNativeLog: ({ level, component, message }) => {
363+
* console.log(`[Sentry Native] [${level}] [${component}] ${message}`);
364+
* },
365+
* });
366+
* ```
367+
*/
368+
onNativeLog?: (log: NativeLogEntry) => void;
369+
}
370+
371+
/**
372+
* Represents a log entry from the native SDK.
373+
*/
374+
export interface NativeLogEntry {
375+
/**
376+
* The log level (e.g., 'debug', 'info', 'warning', 'error', 'fatal').
377+
*/
378+
level: string;
379+
380+
/**
381+
* The component or module that emitted the log (e.g., 'Sentry', 'SentryHttpTransport').
382+
*/
383+
component: string;
384+
385+
/**
386+
* The log message.
387+
*/
388+
message: string;
350389
}
351390

352391
export type SentryReplayQuality = 'low' | 'medium' | 'high';

packages/core/src/js/wrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ export const NATIVE: SentryNativeWrapper = {
293293
logsOrigin,
294294
profilingOptions,
295295
androidProfilingOptions,
296+
onNativeLog,
296297
...filteredOptions
297298
} = options;
298299
/* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */

samples/expo/app.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
"resizeMode": "contain",
1515
"backgroundColor": "#ffffff"
1616
},
17-
"assetBundlePatterns": [
18-
"**/*"
19-
],
17+
"assetBundlePatterns": ["**/*"],
2018
"ios": {
2119
"supportsTablet": true,
2220
"bundleIdentifier": "io.sentry.expo.sample",
@@ -90,4 +88,4 @@
9088
"url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000"
9189
}
9290
}
93-
}
91+
}

samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,10 @@
282282
inputFileListPaths = (
283283
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
284284
);
285-
inputPaths = (
286-
);
287285
name = "[CP] Embed Pods Frameworks";
288286
outputFileListPaths = (
289287
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
290288
);
291-
outputPaths = (
292-
);
293289
runOnlyForDeploymentPostprocessing = 0;
294290
shellPath = /bin/sh;
295291
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks.sh\"\n";
@@ -317,14 +313,10 @@
317313
inputFileListPaths = (
318314
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-input-files.xcfilelist",
319315
);
320-
inputPaths = (
321-
);
322316
name = "[CP] Copy Pods Resources";
323317
outputFileListPaths = (
324318
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-output-files.xcfilelist",
325319
);
326-
outputPaths = (
327-
);
328320
runOnlyForDeploymentPostprocessing = 0;
329321
shellPath = /bin/sh;
330322
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources.sh\"\n";
@@ -382,14 +374,10 @@
382374
inputFileListPaths = (
383375
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
384376
);
385-
inputPaths = (
386-
);
387377
name = "[CP] Embed Pods Frameworks";
388378
outputFileListPaths = (
389379
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
390380
);
391-
outputPaths = (
392-
);
393381
runOnlyForDeploymentPostprocessing = 0;
394382
shellPath = /bin/sh;
395383
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks.sh\"\n";
@@ -403,14 +391,10 @@
403391
inputFileListPaths = (
404392
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-input-files.xcfilelist",
405393
);
406-
inputPaths = (
407-
);
408394
name = "[CP] Copy Pods Resources";
409395
outputFileListPaths = (
410396
"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-output-files.xcfilelist",
411397
);
412-
outputPaths = (
413-
);
414398
runOnlyForDeploymentPostprocessing = 0;
415399
shellPath = /bin/sh;
416400
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources.sh\"\n";
@@ -501,7 +485,7 @@
501485
CLANG_ENABLE_MODULES = YES;
502486
CODE_SIGN_IDENTITY = "Apple Development";
503487
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
504-
CODE_SIGN_STYLE = Manual;
488+
CODE_SIGN_STYLE = Manual;
505489
CURRENT_PROJECT_VERSION = 66;
506490
DEVELOPMENT_TEAM = "";
507491
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U;
@@ -523,7 +507,7 @@
523507
PRODUCT_NAME = sentryreactnativesample;
524508
PROVISIONING_PROFILE_SPECIFIER = "";
525509
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample";
526-
RCT_NEW_ARCH_ENABLED = 1;
510+
RCT_NEW_ARCH_ENABLED = 1;
527511
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
528512
SWIFT_VERSION = 5.0;
529513
VERSIONING_SYSTEM = "apple-generic";
@@ -538,7 +522,7 @@
538522
CLANG_ENABLE_MODULES = YES;
539523
CODE_SIGN_IDENTITY = "Apple Development";
540524
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
541-
CODE_SIGN_STYLE = Manual;
525+
CODE_SIGN_STYLE = Manual;
542526
CURRENT_PROJECT_VERSION = 66;
543527
DEVELOPMENT_TEAM = "";
544528
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U;
@@ -559,7 +543,7 @@
559543
PRODUCT_NAME = sentryreactnativesample;
560544
PROVISIONING_PROFILE_SPECIFIER = "";
561545
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample";
562-
RCT_NEW_ARCH_ENABLED = 1;
546+
RCT_NEW_ARCH_ENABLED = 1;
563547
SWIFT_VERSION = 5.0;
564548
VERSIONING_SYSTEM = "apple-generic";
565549
};
@@ -640,7 +624,10 @@
640624
"-DFOLLY_CFG_NO_COROUTINES=1",
641625
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
642626
);
643-
OTHER_LDFLAGS = "$(inherited) ";
627+
OTHER_LDFLAGS = (
628+
"$(inherited)",
629+
" ",
630+
);
644631
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
645632
SDKROOT = iphoneos;
646633
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -713,7 +700,10 @@
713700
"-DFOLLY_CFG_NO_COROUTINES=1",
714701
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
715702
);
716-
OTHER_LDFLAGS = "$(inherited) ";
703+
OTHER_LDFLAGS = (
704+
"$(inherited)",
705+
" ",
706+
);
717707
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
718708
SDKROOT = iphoneos;
719709
USE_HERMES = true;

samples/react-native/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const BottomTabNavigator = createBottomTabNavigator();
4747
Sentry.init({
4848
// Replace the example DSN below with your own DSN:
4949
dsn: getDsn(),
50+
onNativeLog: ({ level, component, message }) => {
51+
console.log(`ALWX [Sentry Native] [${level}] [${component}] ${message}`);
52+
},
5053
debug: true,
5154
environment: 'dev',
5255
beforeSend: (event: Sentry.ErrorEvent) => {

0 commit comments

Comments
 (0)