From cf6accc5e21ee023b552afae6e4675f30dd0d9f8 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 20 Feb 2024 18:09:31 +0100 Subject: [PATCH 01/10] Add support to send JavaScript exceptions to Sentry --- .../Crash Logging/CrashLogging.swift | 21 ++++++++ .../SentryEventJSException.swift | 52 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 Sources/Remote Logging/Crash Logging/SentryEventJSException.swift diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index d67e78ae..f7bf89b2 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -146,6 +146,27 @@ public class CrashLogging { // MARK: - Manual Error Logging public extension CrashLogging { + func logJavaScriptException(_ exception: [AnyHashable: Any]) { + let jsException = SentryEventJSException.initWithException(exception) + + if jsException.context == nil { + jsException.context = [:] + } + + var reactNativeContext:[String: Any] = exception["context"] as! [String: Any] ?? [:] + jsException.context?["react_native_context"] = reactNativeContext; + + if jsException.tags == nil { + jsException.tags = [:] + } + + if exception["tags"] != nil { + jsException.tags = jsException.tags?.merging(exception["tags"] as! [String: String]) { $1 } + } + + SentrySDK.capture(event: jsException) + } + /// 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..ab24b1c3 --- /dev/null +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -0,0 +1,52 @@ +import Foundation +import Sentry + +public class SentryEventJSException: Event { + override 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 static func initWithException(_ rawException: [AnyHashable: Any]) -> SentryEventJSException { + let sentryEvent = self.init() + + // Generate exception based on JavaScript exception parameters. + let sentryException = Exception(value: rawException["value"] as! String, type: rawException["type"] as! String) + + // Generate the stacktrace frames. + var frames:[Frame] = [] + let stacktrace = rawException["stacktrace"] as! [[AnyHashable: Any]] + for entry in stacktrace { + let frame = Frame() + frame.fileName = entry["filename"] as! String + frame.function = entry["function"] as! String + frame.inApp = true + frame.lineNumber = entry["lineno"] as? NSNumber ?? 0 + frame.columnNumber = entry["colno"] as? NSNumber ?? 0 + frames.append(frame) + } + sentryException.stacktrace = SentryStacktrace(frames: frames, registers: [:]) + + // Attach JavaScript exception to Sentry event. + sentryEvent.exceptions = [sentryException] + + return sentryEvent + } + + 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 + } +} From f7920171e01f7b42cca94cefe090940b7e64c8d0 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 23 Feb 2024 19:04:47 +0100 Subject: [PATCH 02/10] Trigger callback upon sending JS exception --- .../Remote Logging/Crash Logging/CrashLogging.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index f7bf89b2..f120450f 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -146,7 +146,13 @@ public class CrashLogging { // MARK: - Manual Error Logging public extension CrashLogging { - func logJavaScriptException(_ exception: [AnyHashable: Any]) { + /// 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(_ exception: [AnyHashable: Any], callback: @escaping () -> Void) { let jsException = SentryEventJSException.initWithException(exception) if jsException.context == nil { @@ -165,6 +171,11 @@ public extension CrashLogging { } SentrySDK.capture(event: 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. From f3b3ac710682b0c7662f37202fa7090b6d1d3a51 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 27 Feb 2024 15:21:04 +0100 Subject: [PATCH 03/10] Add typing for JavaScript exception object --- .../SentryEventJSException.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index ab24b1c3..c81f5096 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -1,6 +1,40 @@ import Foundation import Sentry +public struct JSException { + public let type: String + public let value: String + public let stacktrace: [StacktraceLine] + public let context: [String: Any] + public let tags: [String: String] + public let isHandled: Bool + public let handledBy: String + + public init(type: String, value: String, stacktrace: [StacktraceLine], context: [String : Any], tags: [String : String], isHandled: Bool, handledBy: String) { + self.type = type + self.value = value + self.stacktrace = stacktrace + self.context = context + self.tags = tags + self.isHandled = isHandled + self.handledBy = handledBy + } + + public struct StacktraceLine { + public let filename: String? + public let function: String? + public let lineno: NSNumber? + public let colno: NSNumber? + + public init(filename: String?, function: String?, lineno: NSNumber?, colno: NSNumber?) { + self.filename = filename + self.function = function + self.lineno = lineno + self.colno = colno + } + } +} + public class SentryEventJSException: Event { override required init() { // All JavaScript exceptions should be trated as fatal errors. From decdafc1c8b08ff83935cccc8880b0809ca9e5c6 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 27 Feb 2024 15:21:44 +0100 Subject: [PATCH 04/10] Log exception using JavaScript exception type --- .../Crash Logging/CrashLogging.swift | 20 +-------- .../SentryEventJSException.swift | 44 ++++++++++++------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index f120450f..e2415374 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -152,25 +152,9 @@ public extension CrashLogging { /// - Parameters: /// - exception: The exception object /// - callback: Callback triggered upon completion - func logJavaScriptException(_ exception: [AnyHashable: Any], callback: @escaping () -> Void) { - let jsException = SentryEventJSException.initWithException(exception) + func logJavaScriptException(_ jsException: JSException, callback: @escaping () -> Void) { - if jsException.context == nil { - jsException.context = [:] - } - - var reactNativeContext:[String: Any] = exception["context"] as! [String: Any] ?? [:] - jsException.context?["react_native_context"] = reactNativeContext; - - if jsException.tags == nil { - jsException.tags = [:] - } - - if exception["tags"] != nil { - jsException.tags = jsException.tags?.merging(exception["tags"] as! [String: String]) { $1 } - } - - SentrySDK.capture(event: jsException) + SentrySDK.capture(event: SentryEventJSException.initWithException(jsException)) DispatchQueue.global().async { SentrySDK.flush(timeout: self.flushTimeout) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index c81f5096..f37ed107 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -37,36 +37,48 @@ public struct JSException { public class SentryEventJSException: Event { override required init() { - // All JavaScript exceptions should be trated as fatal errors. + // 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. + // 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 static func initWithException(_ rawException: [AnyHashable: Any]) -> SentryEventJSException { + public static func initWithException(_ jsException: JSException) -> SentryEventJSException { let sentryEvent = self.init() - // Generate exception based on JavaScript exception parameters. - let sentryException = Exception(value: rawException["value"] as! String, type: rawException["type"] as! String) + // Generate exception based on JavaScript exception parameters + let sentryException = Exception(value: jsException.value, type: jsException.type) - // Generate the stacktrace frames. - var frames:[Frame] = [] - let stacktrace = rawException["stacktrace"] as! [[AnyHashable: Any]] - for entry in stacktrace { + // Generate the stacktrace frames + let frames = jsException.stacktrace.map { let frame = Frame() - frame.fileName = entry["filename"] as! String - frame.function = entry["function"] as! String + frame.fileName = $0.filename + frame.function = $0.function frame.inApp = true - frame.lineNumber = entry["lineno"] as? NSNumber ?? 0 - frame.columnNumber = entry["colno"] as? NSNumber ?? 0 - frames.append(frame) + frame.lineNumber = $0.lineno + frame.columnNumber = $0.colno + return frame } sentryException.stacktrace = SentryStacktrace(frames: frames, registers: [:]) - // Attach JavaScript exception to Sentry event. + // Add exception mechanism + let mechanism = Mechanism(type: jsException.handledBy) + mechanism.handled = jsException.isHandled ? 1 : 0 + sentryException.mechanism = mechanism + + // Attach JavaScript exception to Sentry event sentryEvent.exceptions = [sentryException] + // Set event context + var context = sentryEvent.context ?? [:] + context["react_native_context"] = jsException.context; + sentryEvent.context = context + + // Set event tags + var tags = sentryEvent.tags ?? [:] + sentryEvent.tags = tags.merging(jsException.tags) { $1 } + return sentryEvent } From 27d5147e6d17120c845a03729b05ac112debccee Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 27 Feb 2024 18:59:39 +0100 Subject: [PATCH 05/10] Fix warnings shared by CI jobs --- .../Remote Logging/Crash Logging/SentryEventJSException.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index f37ed107..d2c7a989 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -36,7 +36,7 @@ public struct JSException { } public class SentryEventJSException: Event { - override required init() { + 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 @@ -76,7 +76,7 @@ public class SentryEventJSException: Event { sentryEvent.context = context // Set event tags - var tags = sentryEvent.tags ?? [:] + let tags = sentryEvent.tags ?? [:] sentryEvent.tags = tags.merging(jsException.tags) { $1 } return sentryEvent From 18adbd8bac085f7dda820fa244b8add3a118d4cd Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 27 Feb 2024 19:05:55 +0100 Subject: [PATCH 06/10] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ba6686962376f334881a18641515db92f0bfb4b0 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 29 Feb 2024 13:19:22 +0100 Subject: [PATCH 07/10] Use convenience initializer for `SentryJSException` --- .../Crash Logging/CrashLogging.swift | 2 +- .../Crash Logging/SentryEventJSException.swift | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index e2415374..6eb36021 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -154,7 +154,7 @@ public extension CrashLogging { /// - callback: Callback triggered upon completion func logJavaScriptException(_ jsException: JSException, callback: @escaping () -> Void) { - SentrySDK.capture(event: SentryEventJSException.initWithException(jsException)) + SentrySDK.capture(event: SentryEventJSException.init(jsException: jsException)) DispatchQueue.global().async { SentrySDK.flush(timeout: self.flushTimeout) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index d2c7a989..68a2adb4 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -44,8 +44,8 @@ public class SentryEventJSException: Event { self.platform = "javascript" } - public static func initWithException(_ jsException: JSException) -> SentryEventJSException { - let sentryEvent = self.init() + public convenience init(jsException: JSException) { + self.init() // Generate exception based on JavaScript exception parameters let sentryException = Exception(value: jsException.value, type: jsException.type) @@ -68,18 +68,16 @@ public class SentryEventJSException: Event { sentryException.mechanism = mechanism // Attach JavaScript exception to Sentry event - sentryEvent.exceptions = [sentryException] + self.exceptions = [sentryException] // Set event context - var context = sentryEvent.context ?? [:] + var context = self.context ?? [:] context["react_native_context"] = jsException.context; - sentryEvent.context = context + self.context = context // Set event tags - let tags = sentryEvent.tags ?? [:] - sentryEvent.tags = tags.merging(jsException.tags) { $1 } - - return sentryEvent + let tags = self.tags ?? [:] + self.tags = tags.merging(jsException.tags) { $1 } } override public func serialize() -> [String : Any] { From 07a7012a5af7a5e8c47bc5959e5d1331a8cffe6f Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 29 Feb 2024 13:26:01 +0100 Subject: [PATCH 08/10] Rename `JSException` param to `message` --- .../Crash Logging/SentryEventJSException.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index 68a2adb4..c9a4d36a 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -3,16 +3,16 @@ import Sentry public struct JSException { public let type: String - public let value: String + public let message: String public let stacktrace: [StacktraceLine] public let context: [String: Any] public let tags: [String: String] public let isHandled: Bool public let handledBy: String - public init(type: String, value: String, stacktrace: [StacktraceLine], context: [String : Any], tags: [String : String], isHandled: Bool, handledBy: String) { + public init(type: String, message: String, stacktrace: [StacktraceLine], context: [String : Any], tags: [String : String], isHandled: Bool, handledBy: String) { self.type = type - self.value = value + self.message = message self.stacktrace = stacktrace self.context = context self.tags = tags @@ -48,7 +48,7 @@ public class SentryEventJSException: Event { self.init() // Generate exception based on JavaScript exception parameters - let sentryException = Exception(value: jsException.value, type: jsException.type) + let sentryException = Exception(value: jsException.message, type: jsException.type) // Generate the stacktrace frames let frames = jsException.stacktrace.map { From 270a150509e28eeb8e8142a0d4304ff7101a9e17 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 6 Mar 2024 18:20:08 +0100 Subject: [PATCH 09/10] Use protocols to define JavaScript exceptions --- .../SentryEventJSException.swift | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index c9a4d36a..5688a3c0 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -1,38 +1,21 @@ import Foundation import Sentry -public struct JSException { - public let type: String - public let message: String - public let stacktrace: [StacktraceLine] - public let context: [String: Any] - public let tags: [String: String] - public let isHandled: Bool - public let handledBy: String +public protocol JSException { + var type: String { get } + var message: String { get } + var jsStacktrace: [StacktraceLine] { get } + var context: [String: Any] { get } + var tags: [String: String] { get } + var isHandled: Bool { get } + var handledBy: String { get } +} - public init(type: String, message: String, stacktrace: [StacktraceLine], context: [String : Any], tags: [String : String], isHandled: Bool, handledBy: String) { - self.type = type - self.message = message - self.stacktrace = stacktrace - self.context = context - self.tags = tags - self.isHandled = isHandled - self.handledBy = handledBy - } - - public struct StacktraceLine { - public let filename: String? - public let function: String? - public let lineno: NSNumber? - public let colno: NSNumber? - - public init(filename: String?, function: String?, lineno: NSNumber?, colno: NSNumber?) { - self.filename = filename - self.function = function - self.lineno = lineno - self.colno = colno - } - } +public protocol StacktraceLine { + var filename: String? { get } + var function: String { get } + var lineno: NSNumber? { get } + var colno: NSNumber? { get } } public class SentryEventJSException: Event { @@ -51,7 +34,7 @@ public class SentryEventJSException: Event { let sentryException = Exception(value: jsException.message, type: jsException.type) // Generate the stacktrace frames - let frames = jsException.stacktrace.map { + let frames = jsException.jsStacktrace.map { let frame = Frame() frame.fileName = $0.filename frame.function = $0.function From d3694e8a352f8cac3f3c26f73125614408c01abc Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 7 Mar 2024 10:53:24 +0100 Subject: [PATCH 10/10] Use associated type in `JSException` protocol --- Sources/Remote Logging/Crash Logging/CrashLogging.swift | 2 +- .../Crash Logging/SentryEventJSException.swift | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/Remote Logging/Crash Logging/CrashLogging.swift b/Sources/Remote Logging/Crash Logging/CrashLogging.swift index 6eb36021..59962441 100644 --- a/Sources/Remote Logging/Crash Logging/CrashLogging.swift +++ b/Sources/Remote Logging/Crash Logging/CrashLogging.swift @@ -152,7 +152,7 @@ public extension CrashLogging { /// - Parameters: /// - exception: The exception object /// - callback: Callback triggered upon completion - func logJavaScriptException(_ jsException: JSException, callback: @escaping () -> Void) { + func logJavaScriptException(_ jsException: any JSException, callback: @escaping () -> Void) { SentrySDK.capture(event: SentryEventJSException.init(jsException: jsException)) diff --git a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift index 5688a3c0..5fabc2f2 100644 --- a/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift +++ b/Sources/Remote Logging/Crash Logging/SentryEventJSException.swift @@ -2,16 +2,17 @@ import Foundation import Sentry public protocol JSException { + associatedtype StacktraceLine: JSStacktraceLine var type: String { get } var message: String { get } - var jsStacktrace: [StacktraceLine] { 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 StacktraceLine { +public protocol JSStacktraceLine { var filename: String? { get } var function: String { get } var lineno: NSNumber? { get } @@ -27,14 +28,14 @@ public class SentryEventJSException: Event { self.platform = "javascript" } - public convenience init(jsException: JSException) { + 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.jsStacktrace.map { + let frames = jsException.stacktrace.map { let frame = Frame() frame.fileName = $0.filename frame.function = $0.function