diff --git a/CHANGELOG.md b/CHANGELOG.md index c1128f95..2b582cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ _None._ ### New Features -_None._ +- Add function to log JavaScript exceptions in `CrashLogging` [#278] ### Bug Fixes diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index d67e78ae..59962441 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -146,6 +146,22 @@ public class CrashLogging { // MARK: - Manual Error Logging public extension CrashLogging { + /// Writes a JavaScript exception to the Crash Logging system, including its stack trace. + /// Note that this function is provided mainly for hybrid sources like React Native. + /// + /// - Parameters: + /// - exception: The exception object + /// - callback: Callback triggered upon completion + func logJavaScriptException(_ jsException: any JSException, callback: @escaping () -> Void) { + + SentrySDK.capture(event: SentryEventJSException.init(jsException: jsException)) + + DispatchQueue.global().async { + SentrySDK.flush(timeout: self.flushTimeout) + callback() + } + } + /// Writes the error to the Crash Logging system, and includes a stack trace. This API supports for rich events. /// By setting a Tag/Value pair, you'll be able to filter these events, directly, with the `has:` operator (Sentry Web Interface). /// diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift new file mode 100644 index 00000000..5fabc2f2 --- /dev/null +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -0,0 +1,80 @@ +import Foundation +import Sentry + +public protocol JSException { + associatedtype StacktraceLine: JSStacktraceLine + var type: String { get } + var message: String { get } + var stacktrace: [StacktraceLine] { get } + var context: [String: Any] { get } + var tags: [String: String] { get } + var isHandled: Bool { get } + var handledBy: String { get } +} + +public protocol JSStacktraceLine { + var filename: String? { get } + var function: String { get } + var lineno: NSNumber? { get } + var colno: NSNumber? { get } +} + +public class SentryEventJSException: Event { + required init() { + // All JavaScript exceptions should be trated as fatal errors + super.init(level: .fatal) + // Setting the event's platform to JavaScript is required by Sentry to be processed + // as a JavaScript exception. Otherwise, Sentry won't symbolicate the stack trace. + self.platform = "javascript" + } + + public convenience init(jsException: any JSException) { + self.init() + + // Generate exception based on JavaScript exception parameters + let sentryException = Exception(value: jsException.message, type: jsException.type) + + // Generate the stacktrace frames + let frames = jsException.stacktrace.map { + let frame = Frame() + frame.fileName = $0.filename + frame.function = $0.function + frame.inApp = true + frame.lineNumber = $0.lineno + frame.columnNumber = $0.colno + return frame + } + sentryException.stacktrace = SentryStacktrace(frames: frames, registers: [:]) + + // Add exception mechanism + let mechanism = Mechanism(type: jsException.handledBy) + mechanism.handled = jsException.isHandled ? 1 : 0 + sentryException.mechanism = mechanism + + // Attach JavaScript exception to Sentry event + self.exceptions = [sentryException] + + // Set event context + var context = self.context ?? [:] + context["react_native_context"] = jsException.context; + self.context = context + + // Set event tags + let tags = self.tags ?? [:] + self.tags = tags.merging(jsException.tags) { $1 } + } + + override public func serialize() -> [String : Any] { + var serializedData = super.serialize() + + // By default, events generated in Sentry iOS SDK are tagged to "cocoa" platform. + // Hence, we use the original platform set. + serializedData["platform"] = self.platform + + // Removing metadata associated with native exception, as it's not needed for JavaScript exceptions. + serializedData["debug_meta"] = nil + serializedData["threads"] = nil + + return serializedData + } +}