-
-
Notifications
You must be signed in to change notification settings - Fork 359
[iOS + Android] Add the ability to intercept errors from native side and forward them to JS console #5622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[iOS + Android] Add the ability to intercept errors from native side and forward them to JS console #5622
Changes from all commits
6f6171f
5437acb
61dec2f
25d3c35
c6368f6
33b9ea5
457aec4
61ea8c0
f22f040
4a1e0cb
fcba8c8
85d0939
e152992
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package io.sentry.react; | ||
|
|
||
| import com.facebook.react.bridge.Arguments; | ||
| import com.facebook.react.bridge.ReactApplicationContext; | ||
| import com.facebook.react.bridge.WritableMap; | ||
| import com.facebook.react.modules.core.DeviceEventManagerModule; | ||
| import io.sentry.ILogger; | ||
| import io.sentry.SentryLevel; | ||
| import io.sentry.android.core.AndroidLogger; | ||
| import java.lang.ref.WeakReference; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** | ||
| * Custom ILogger implementation that wraps AndroidLogger and forwards log messages to React Native. | ||
| * This allows native SDK logs to appear in the Metro console when debug mode is enabled. | ||
| */ | ||
| public class RNSentryLogger implements ILogger { | ||
| private static final String TAG = "Sentry"; | ||
| private static final String EVENT_NAME = "SentryNativeLog"; | ||
|
|
||
| private final AndroidLogger androidLogger; | ||
| private WeakReference<ReactApplicationContext> reactContextRef; | ||
|
|
||
| public RNSentryLogger() { | ||
| this.androidLogger = new AndroidLogger(TAG); | ||
| } | ||
|
|
||
| public void setReactContext(@Nullable ReactApplicationContext context) { | ||
| this.reactContextRef = context != null ? new WeakReference<>(context) : null; | ||
| } | ||
|
|
||
| @Override | ||
| public void log(@NotNull SentryLevel level, @NotNull String message, @Nullable Object... args) { | ||
| // Always log to Logcat (default behavior) | ||
| androidLogger.log(level, message, args); | ||
|
|
||
| // Forward to JS | ||
| String formattedMessage = | ||
| (args == null || args.length == 0) ? message : String.format(message, args); | ||
| forwardToJS(level, formattedMessage); | ||
| } | ||
|
|
||
| @Override | ||
| public void log( | ||
| @NotNull SentryLevel level, @NotNull String message, @Nullable Throwable throwable) { | ||
| androidLogger.log(level, message, throwable); | ||
|
|
||
| String fullMessage = throwable != null ? message + ": " + throwable.getMessage() : message; | ||
| forwardToJS(level, fullMessage); | ||
| } | ||
|
|
||
| @Override | ||
| public void log( | ||
| @NotNull SentryLevel level, | ||
| @Nullable Throwable throwable, | ||
| @NotNull String message, | ||
| @Nullable Object... args) { | ||
| androidLogger.log(level, throwable, message, args); | ||
|
|
||
| String formattedMessage = | ||
| (args == null || args.length == 0) ? message : String.format(message, args); | ||
| if (throwable != null) { | ||
| formattedMessage += ": " + throwable.getMessage(); | ||
| } | ||
| forwardToJS(level, formattedMessage); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isEnabled(@Nullable SentryLevel level) { | ||
| return androidLogger.isEnabled(level); | ||
| } | ||
|
|
||
| private void forwardToJS(@NotNull SentryLevel level, @NotNull String message) { | ||
| ReactApplicationContext context = reactContextRef != null ? reactContextRef.get() : null; | ||
| if (context == null || !context.hasActiveReactInstance()) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| WritableMap params = Arguments.createMap(); | ||
| params.putString("level", level.name().toLowerCase()); | ||
| params.putString("component", "Sentry"); | ||
| params.putString("message", message); | ||
|
|
||
| context | ||
| .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) | ||
| .emit(EVENT_NAME, params); | ||
| } catch (Exception e) { | ||
| // Silently ignore - don't cause issues if JS bridge isn't ready | ||
| // We intentionally swallow this exception to avoid disrupting the app | ||
| // when the React Native bridge is not yet initialized or has been torn down | ||
| androidLogger.log(SentryLevel.DEBUG, "Failed to forward log to JS: " + e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,6 @@ | |
| import io.sentry.SentryExecutorService; | ||
| import io.sentry.SentryLevel; | ||
| import io.sentry.SentryOptions; | ||
| import io.sentry.android.core.AndroidLogger; | ||
| import io.sentry.android.core.AndroidProfiler; | ||
| import io.sentry.android.core.BuildInfoProvider; | ||
| import io.sentry.android.core.InternalSentrySdk; | ||
|
|
@@ -87,7 +86,8 @@ public class RNSentryModuleImpl { | |
|
|
||
| public static final String NAME = "RNSentry"; | ||
|
|
||
| private static final ILogger logger = new AndroidLogger(NAME); | ||
| private static final RNSentryLogger rnLogger = new RNSentryLogger(); | ||
| private static final ILogger logger = rnLogger; | ||
| private static final BuildInfoProvider buildInfo = new BuildInfoProvider(logger); | ||
| private static final String modulesPath = "modules.json"; | ||
| private static final Charset UTF_8 = Charset.forName("UTF-8"); // NOPMD - Allow using UTF-8 | ||
|
|
@@ -170,8 +170,17 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { | |
| } | ||
|
|
||
| public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { | ||
| // Set the React context for the logger so it can forward logs to JS | ||
| rnLogger.setReactContext(this.reactApplicationContext); | ||
|
|
||
| RNSentryStart.startWithOptions( | ||
| getApplicationContext(), rnOptions, getCurrentActivity(), logger); | ||
| getApplicationContext(), | ||
| rnOptions, | ||
| options -> { | ||
| // Use our custom logger that forwards to JS | ||
| options.setLogger(rnLogger); | ||
| }, | ||
| logger); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current activity no longer set during SDK initializationHigh Severity The call to |
||
|
|
||
| promise.resolve(true); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| extern NSString *const RNSentryNewFrameEvent; | ||
| extern NSString *const RNSentryNativeLogEvent; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| #import "RNSentryEvents.h" | ||
|
|
||
| NSString *const RNSentryNewFrameEvent = @"rn_sentry_new_frame"; | ||
| NSString *const RNSentryNativeLogEvent = @"SentryNativeLog"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,7 @@ + (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled | |
| if (sentryOptions == nil) { | ||
| return; | ||
| } | ||
| sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled; | ||
| // sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Session replay setting silently disabled by commenting outHigh Severity The |
||
| } | ||
|
|
||
| + (void)configureProfilingWithOptions:(NSDictionary *)profilingOptions | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #import <Foundation/Foundation.h> | ||
| #import <React/RCTEventEmitter.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /** | ||
| * Singleton class that forwards native Sentry SDK logs to JavaScript via React Native events. | ||
| * This allows React Native developers to see native SDK logs in the Metro console. | ||
| */ | ||
| @interface RNSentryNativeLogsForwarder : NSObject | ||
|
|
||
| + (instancetype)shared; | ||
|
|
||
| - (void)configureWithEventEmitter:(RCTEventEmitter *)emitter; | ||
|
|
||
| - (void)stopForwarding; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| #import "RNSentryNativeLogsForwarder.h" | ||
|
|
||
| @import Sentry; | ||
|
|
||
| static NSString *const RNSentryNativeLogEventName = @"SentryNativeLog"; | ||
|
|
||
| @interface RNSentryNativeLogsForwarder () | ||
|
|
||
| @property (nonatomic, weak) RCTEventEmitter *eventEmitter; | ||
|
|
||
| @end | ||
|
|
||
| @implementation RNSentryNativeLogsForwarder | ||
|
|
||
| + (instancetype)shared | ||
| { | ||
| static RNSentryNativeLogsForwarder *instance = nil; | ||
| static dispatch_once_t onceToken; | ||
| dispatch_once(&onceToken, ^{ instance = [[RNSentryNativeLogsForwarder alloc] init]; }); | ||
| return instance; | ||
| } | ||
|
|
||
| - (void)configureWithEventEmitter:(RCTEventEmitter *)emitter | ||
| { | ||
| self.eventEmitter = emitter; | ||
|
|
||
| __weak RNSentryNativeLogsForwarder *weakSelf = self; | ||
|
|
||
| // Set up the Sentry SDK log output to forward logs to JS | ||
| [SentrySDKLog setOutput:^(NSString *_Nonnull message) { | ||
|
Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m
|
||
| // Always print to console (default behavior) | ||
| NSLog(@"%@", message); | ||
|
|
||
| // Forward to JS if we have an emitter | ||
| RNSentryNativeLogsForwarder *strongSelf = weakSelf; | ||
| if (strongSelf) { | ||
| [strongSelf forwardLogMessage:message]; | ||
| } | ||
| }]; | ||
|
|
||
| // Send a test log to verify the forwarding works | ||
| [self forwardLogMessage: | ||
| @"[Sentry] [info] [0] [RNSentryNativeLogsForwarder] Native log forwarding " | ||
| @"configured successfully"]; | ||
| } | ||
|
|
||
| - (void)stopForwarding | ||
| { | ||
| self.eventEmitter = nil; | ||
|
|
||
| // Reset to default print behavior | ||
| [SentrySDKLog setOutput:^(NSString *_Nonnull message) { NSLog(@"%@", message); }]; | ||
|
Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m
|
||
| } | ||
|
|
||
| - (void)forwardLogMessage:(NSString *)message | ||
| { | ||
| RCTEventEmitter *emitter = self.eventEmitter; | ||
| if (emitter == nil) { | ||
| return; | ||
| } | ||
|
|
||
| // Only forward messages that look like Sentry SDK logs | ||
| if (![message hasPrefix:@"[Sentry]"]) { | ||
| return; | ||
| } | ||
|
|
||
| // Parse the log message to extract level and component | ||
| // Format: "[Sentry] [level] [timestamp] [Component:line] message" | ||
| // or: "[Sentry] [level] [timestamp] message" | ||
| NSString *level = [self extractLevelFromMessage:message]; | ||
| NSString *component = [self extractComponentFromMessage:message]; | ||
| NSString *cleanMessage = [self extractCleanMessageFromMessage:message]; | ||
|
|
||
| NSDictionary *body = @{ | ||
| @"level" : level, | ||
| @"component" : component, | ||
| @"message" : cleanMessage, | ||
| }; | ||
|
|
||
| // Dispatch async to avoid blocking the calling thread and potential deadlocks | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| RCTEventEmitter *currentEmitter = self.eventEmitter; | ||
| if (currentEmitter != nil) { | ||
| [currentEmitter sendEventWithName:RNSentryNativeLogEventName body:body]; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| - (NSString *)extractLevelFromMessage:(NSString *)message | ||
| { | ||
| // Look for patterns like [debug], [info], [warning], [error], [fatal] | ||
| NSRegularExpression *regex = | ||
| [NSRegularExpression regularExpressionWithPattern:@"\\[(debug|info|warning|error|fatal)\\]" | ||
| options:NSRegularExpressionCaseInsensitive | ||
| error:nil]; | ||
|
|
||
| NSTextCheckingResult *match = [regex firstMatchInString:message | ||
| options:0 | ||
| range:NSMakeRange(0, message.length)]; | ||
|
|
||
| if (match && match.numberOfRanges > 1) { | ||
| return [[message substringWithRange:[match rangeAtIndex:1]] lowercaseString]; | ||
| } | ||
|
|
||
| return @"info"; | ||
| } | ||
|
|
||
| - (NSString *)extractComponentFromMessage:(NSString *)message | ||
| { | ||
| // Look for pattern like [ComponentName:123] | ||
| NSRegularExpression *regex = | ||
| [NSRegularExpression regularExpressionWithPattern:@"\\[([A-Za-z]+):\\d+\\]" | ||
| options:0 | ||
| error:nil]; | ||
|
|
||
| NSTextCheckingResult *match = [regex firstMatchInString:message | ||
| options:0 | ||
| range:NSMakeRange(0, message.length)]; | ||
|
|
||
| if (match && match.numberOfRanges > 1) { | ||
| return [message substringWithRange:[match rangeAtIndex:1]]; | ||
| } | ||
|
|
||
| return @"Sentry"; | ||
| } | ||
|
|
||
| - (NSString *)extractCleanMessageFromMessage:(NSString *)message | ||
| { | ||
| // Remove the prefix parts: [Sentry] [level] [timestamp] [Component:line] | ||
| // and return just the actual message content | ||
| NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: | ||
| @"^\\[Sentry\\]\\s*\\[[^\\]]+\\]\\s*\\[[^\\]]+\\]\\s*(?:\\[[^\\]]+\\]\\s*)?" | ||
| options:0 | ||
| error:nil]; | ||
|
|
||
| NSString *cleanMessage = [regex stringByReplacingMatchesInString:message | ||
| options:0 | ||
| range:NSMakeRange(0, message.length) | ||
| withTemplate:@""]; | ||
|
|
||
| return [cleanMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; | ||
| } | ||
|
|
||
| @end | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Android Logcat tag silently changed from "RNSentry" to "Sentry"
Low Severity
RNSentryLoggerusesTAG = "Sentry"for its internalAndroidLogger, but the previous logger inRNSentryModuleImplusedNAME = "RNSentry"as the Logcat tag. This silently changes all Logcat output tags for the React Native Sentry module, which could break developers' Logcat filtering workflows that rely on the"RNSentry"tag.Additional Locations (1)
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java#L88-L90