Skip to content
Draft
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
Binary file modified packages/core/android/libs/replay-stubs.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.sentry.Breadcrumb;
import io.sentry.ILogger;
import io.sentry.IScope;
import io.sentry.ISentryClient;
import io.sentry.ISentryExecutorService;
import io.sentry.ISerializer;
import io.sentry.ScopesAdapter;
Expand All @@ -55,6 +56,10 @@
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.SentryLogEvent;
import io.sentry.SentryLogEventAttributeValue;
import io.sentry.SentryLogLevel;
import io.sentry.SpanId;
import io.sentry.util.DebugMetaPropertiesApplier;
import io.sentry.util.FileUtils;
import io.sentry.util.JsonSerializationUtils;
Expand Down Expand Up @@ -350,6 +355,136 @@ public void captureEnvelope(String rawBytes, ReadableMap options, Promise promis
promise.resolve(true);
}

public void captureLog(ReadableMap log) {
if (log == null) {
return;
}

try {
String levelStr = log.hasKey("level") ? log.getString("level") : "info";
String body = log.hasKey("body") ? log.getString("body") : null;

if (body == null) {
return;
}

// Parse log level
SentryLogLevel level;
switch (levelStr != null ? levelStr : "info") {
case "trace":
level = SentryLogLevel.TRACE;
break;
case "debug":
level = SentryLogLevel.DEBUG;
break;
case "warn":
level = SentryLogLevel.WARN;
break;
case "error":
level = SentryLogLevel.ERROR;
break;
case "fatal":
level = SentryLogLevel.FATAL;
break;
case "info":
default:
level = SentryLogLevel.INFO;
break;
}

// Parse traceId
String traceIdStr = log.hasKey("traceId") ? log.getString("traceId") : null;
SentryId traceId =
traceIdStr != null && !traceIdStr.isEmpty()
? new SentryId(traceIdStr)
: SentryId.EMPTY_ID;

// Parse timestamp (in seconds)
Double timestamp =
log.hasKey("timestamp") ? log.getDouble("timestamp") : System.currentTimeMillis() / 1000.0;

// Create the log event
SentryLogEvent logEvent = new SentryLogEvent(traceId, timestamp, body, level);

// Set spanId if provided
if (log.hasKey("spanId")) {
String spanIdStr = log.getString("spanId");
if (spanIdStr != null && !spanIdStr.isEmpty()) {
logEvent.setSpanId(new SpanId(spanIdStr));
}
}

// Set severity number if provided
if (log.hasKey("severityNumber")) {
logEvent.setSeverityNumber((int) log.getDouble("severityNumber"));
}

// Parse and set attributes
if (log.hasKey("attributes")) {
ReadableMap jsAttributes = log.getMap("attributes");
if (jsAttributes != null) {
Map<String, SentryLogEventAttributeValue> attributes = new HashMap<>();

ReadableMapKeySetIterator iterator = jsAttributes.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableMap attrValue = jsAttributes.getMap(key);
if (attrValue != null && attrValue.hasKey("value")) {
String type = attrValue.hasKey("type") ? attrValue.getString("type") : "string";
SentryLogEventAttributeValue attributeValue = null;

switch (type != null ? type : "string") {
case "boolean":
attributeValue =
new SentryLogEventAttributeValue("boolean", attrValue.getBoolean("value"));
break;
case "integer":
attributeValue =
new SentryLogEventAttributeValue("integer", (int) attrValue.getDouble("value"));
break;
case "double":
attributeValue =
new SentryLogEventAttributeValue("double", attrValue.getDouble("value"));
break;
case "string":
default:
attributeValue =
new SentryLogEventAttributeValue("string", attrValue.getString("value"));
break;
}

if (attributeValue != null) {
attributes.put(key, attributeValue);
}
}
}

// Add origin attribute to indicate this log came from JavaScript
attributes.put(
"sentry.origin", new SentryLogEventAttributeValue("string", "react-native.js"));

logEvent.setAttributes(attributes);
}
} else {
// Still add the origin attribute even if no other attributes
Map<String, SentryLogEventAttributeValue> attributes = new HashMap<>();
attributes.put(
"sentry.origin", new SentryLogEventAttributeValue("string", "react-native.js"));
logEvent.setAttributes(attributes);
}

// Capture the log through the client
Sentry.configureScope(
scope -> {
final ISentryClient client = scope.getClient();
client.captureLog(logEvent, scope);
});

} catch (Throwable e) { // NOPMD - We don't want to crash in any case
logger.log(SentryLevel.ERROR, "Error while capturing log", e);
}
}

public void captureScreenshot(Promise promise) {

final Activity activity = getCurrentActivity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@Override
public void captureLog(ReadableMap log) {
this.impl.captureLog(log);
}

@Override
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@ReactMethod
public void captureLog(ReadableMap log) {
this.impl.captureLog(log);
}

@ReactMethod
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
Expand Down
93 changes: 93 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
#import <Sentry/SentryGeo.h>
#import <Sentry/SentryUser.h>

#if __has_include(<Sentry/Sentry-Swift.h>)
# import <Sentry/Sentry-Swift.h>
#endif

// This guard prevents importing Hermes in JSC apps
#if SENTRY_PROFILING_ENABLED
# import <hermes/hermes.h>
Expand Down Expand Up @@ -978,4 +982,93 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
resolve(base64String);
}

RCT_EXPORT_METHOD(captureLog : (NSDictionary *)log)
{
#if __has_include(<Sentry/Sentry-Swift.h>)
NSString *levelStr = log[@"level"];
NSString *body = log[@"body"];

if (body == nil) {
return;
}

SentryLogLevel level = SentryLogLevelInfo;
if ([levelStr isEqualToString:@"trace"]) {
level = SentryLogLevelTrace;
} else if ([levelStr isEqualToString:@"debug"]) {
level = SentryLogLevelDebug;
} else if ([levelStr isEqualToString:@"info"]) {
level = SentryLogLevelInfo;
} else if ([levelStr isEqualToString:@"warn"]) {
level = SentryLogLevelWarning;
} else if ([levelStr isEqualToString:@"error"]) {
level = SentryLogLevelError;
} else if ([levelStr isEqualToString:@"fatal"]) {
level = SentryLogLevelFatal;
}

// Convert attributes from JS format to SentryAttribute dictionary
NSMutableDictionary<NSString *, SentryAttribute *> *attributes = [NSMutableDictionary new];
NSDictionary *jsAttributes = log[@"attributes"];
if ([jsAttributes isKindOfClass:[NSDictionary class]]) {
for (NSString *key in jsAttributes) {
NSDictionary *attrValue = jsAttributes[key];
if ([attrValue isKindOfClass:[NSDictionary class]]) {
id value = attrValue[@"value"];
if (value != nil) {
if ([value isKindOfClass:[NSString class]]) {
attributes[key] = [[SentryAttribute alloc] initWithValue:value];
} else if ([value isKindOfClass:[NSNumber class]]) {
// Check if it's a boolean
if (strcmp([value objCType], @encode(BOOL)) == 0) {
attributes[key] = [[SentryAttribute alloc] initWithValue:value];
} else {
attributes[key] = [[SentryAttribute alloc] initWithValue:value];
}
}
}
}
}
}

// Add origin attribute to indicate this log came from JavaScript
attributes[@"sentry.origin"] = [[SentryAttribute alloc] initWithValue:@"react-native.js"];

// Create SentryLog with level, body, and attributes
SentryLog *sentryLog = [[SentryLog alloc] initWithLevel:level body:body attributes:attributes];

// Set timestamp if provided (convert from seconds to Date)
NSNumber *timestamp = log[@"timestamp"];
if ([timestamp isKindOfClass:[NSNumber class]]) {
sentryLog.timestamp = [NSDate dateWithTimeIntervalSince1970:[timestamp doubleValue]];
}

// Set traceId if provided
NSString *traceId = log[@"traceId"];
if ([traceId isKindOfClass:[NSString class]] && traceId.length > 0) {
sentryLog.traceId = [[SentryId alloc] initWithUUIDString:traceId];
}

// Set spanId if provided
NSString *spanId = log[@"spanId"];
if ([spanId isKindOfClass:[NSString class]] && spanId.length > 0) {
sentryLog.spanId = [[SpanId alloc] initWithValue:spanId];
}

// Set severityNumber if provided
NSNumber *severityNumber = log[@"severityNumber"];
if ([severityNumber isKindOfClass:[NSNumber class]]) {
sentryLog.severityNumber = severityNumber;
}

// Capture the log through the SDK
[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
SentryClient *client = [PrivateSentrySDKOnly getCurrentHub].client;
if (client != nil) {
[client captureLog:sentryLog withScope:scope];
}
}];
#endif
}

@end
22 changes: 22 additions & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface Spec extends TurboModule {
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
setActiveSpanId(spanId: string): boolean;
encodeToBase64(data: number[]): Promise<string | undefined | null>;
captureLog(log: UnsafeObject): void;
}

export type NativeStackFrame = {
Expand Down Expand Up @@ -159,5 +160,26 @@ export type NativeScreenshot = {
filename: string;
};

/**
* Log event structure for forwarding JS logs to native SDK.
* The native SDK will batch and handle lifecycle events (background/termination).
*/
export type NativeLogEvent = {
/** Timestamp in seconds since epoch */
timestamp: number;
/** Log level: trace, debug, info, warn, error, fatal */
level: string;
/** The log message body */
body: string;
/** Trace ID to associate this log with distributed tracing */
traceId: string;
/** Span ID of the active span when the log was captured */
spanId?: string;
/** Numeric severity level */
severityNumber?: number;
/** Structured attributes attached to the log */
attributes?: Record<string, { type: string; value: unknown }>;
};

// The export must be here to pass codegen even if not used
export default TurboModuleRegistry.getEnforcing<Spec>('RNSentry');
Loading
Loading