Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -49,7 +49,6 @@
import io.sentry.SentryReplayOptions;
import io.sentry.SentryReplayOptions.SentryReplayQuality;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.android.core.AndroidLogger;
import io.sentry.android.core.AndroidProfiler;
import io.sentry.android.core.AnrIntegration;
import io.sentry.android.core.BuildConfig;
Expand Down Expand Up @@ -107,7 +106,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
Expand Down Expand Up @@ -207,8 +207,16 @@ 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);

SentryAndroid.init(
getApplicationContext(), options -> getSentryAndroidOptions(options, rnOptions, logger));
getApplicationContext(),
options -> {
// Use our custom logger that forwards to JS
options.setLogger(rnLogger);
getSentryAndroidOptions(options, rnOptions, logger);
});

promise.resolve(true);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

#import "RNSentryDependencyContainer.h"
#import "RNSentryEvents.h"
#import "RNSentryNativeLogsForwarder.h"

#if SENTRY_TARGET_REPLAY_SUPPORTED
# import "RNSentryReplay.h"
Expand Down Expand Up @@ -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<NSString *> *)supportedEvents
{
return @[ RNSentryNewFrameEvent ];
return @[ RNSentryNewFrameEvent, RNSentryNativeLogEvent ];
}

RCT_EXPORT_METHOD(
Expand Down
1 change: 1 addition & 0 deletions packages/core/ios/RNSentryEvents.h
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;
1 change: 1 addition & 0 deletions packages/core/ios/RNSentryEvents.m
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";
2 changes: 1 addition & 1 deletion packages/core/ios/RNSentryExperimentalOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ + (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled
if (sentryOptions == nil) {
return;
}
sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled;
// sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled;
}

+ (void)configureProfilingWithOptions:(NSDictionary *)profilingOptions
Expand Down
20 changes: 20 additions & 0 deletions packages/core/ios/RNSentryNativeLogsForwarder.h
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
144 changes: 144 additions & 0 deletions packages/core/ios/RNSentryNativeLogsForwarder.m
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

View workflow job for this annotation

GitHub Actions / Build legacy ios production dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy macos production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios dev dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build new ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios dev dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios production dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy macos dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 30 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build new ios production no-frameworks

no known class method for selector 'setOutput:'
// 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

View workflow job for this annotation

GitHub Actions / Build legacy ios production dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy macos production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios dev dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build new ios dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios dev dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build ios production dynamic-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy ios production no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build legacy macos dev no-frameworks

no known class method for selector 'setOutput:'

Check failure on line 52 in packages/core/ios/RNSentryNativeLogsForwarder.m

View workflow job for this annotation

GitHub Actions / Build new ios production no-frameworks

no known class method for selector 'setOutput:'
}

- (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
Loading
Loading