-
-
Notifications
You must be signed in to change notification settings - Fork 359
[iOS only] 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
Open
alwx
wants to merge
5
commits into
main
Choose a base branch
from
alwx/experiment/native-logs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
[iOS only] Add the ability to intercept errors from native side and forward them to JS console #5622
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| #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) { | ||
| // 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); }]; | ||
| } | ||
|
|
||
| - (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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { debug } from '@sentry/core'; | ||
| import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; | ||
| import type { NativeLogEntry } from './options'; | ||
|
|
||
| const NATIVE_LOG_EVENT_NAME = 'SentryNativeLog'; | ||
|
|
||
| let nativeLogListener: ReturnType<NativeEventEmitter['addListener']> | 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. | ||
| * | ||
| * @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; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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()) { | ||
|
Collaborator
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. It would be nice if |
||
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This will work on the future once we have support for Android and iOS. For the time being, this PR is only adding support for iOS