-
Notifications
You must be signed in to change notification settings - Fork 12
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
Add support to log JavaScript exceptions to Sentry #278
Conversation
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
/// - Parameters: | ||
/// - exception: The exception object | ||
/// - callback: Callback triggered upon completion | ||
func logJavaScriptException(_ jsException: JSException, callback: @escaping () -> Void) { |
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.
The callback
argument feels redundant to me. I have asked about removing it in WordPress/gutenberg#59221 (review).
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.
I shared a comment about this here. In React Native, the communication between JavaScript and native is asynchronous so it's common to use callbacks.
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.
I'm wondering about the callback too. It doesn't seem to add much value when passed this far down the call stack. As you suggest in this comment, it's only needed to inform about a malformed rawException
. Even there I'm not sure how useful it will be.
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.
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.
@crazytonyli Regarding this comment, I think we need to have the callback here in Tracks because we need to guarantee that it's triggered after flushing all queued events:
Automattic-Tracks-iOS/Sources/Remote Logging/Crash Logging/CrashLogging.swift
Lines 159 to 162 in 07a7012
DispatchQueue.global().async { | |
SentrySDK.flush(timeout: self.flushTimeout) | |
callback() | |
} |
This implementation is based on other functions we have to ensure that events are sent, e.g.:
Automattic-Tracks-iOS/Sources/Remote Logging/Crash Logging/CrashLogging.swift
Lines 247 to 274 in 07a7012
private func logErrorsImmediately( | |
_ errors: [Error], | |
userInfo: [String: Any]? = nil, | |
level: SentryLevel = .error, | |
andWait wait: Bool, | |
callback: @escaping () -> Void | |
) { | |
errors.forEach { error in | |
// Amending the global scope on a per-event basis seems like the best way to add the | |
// caller-provided `userInfo` and `level`. | |
SentrySDK.capture(error: error) { scope in | |
// Under the hood, `setExtras` uses `NSMutableDictionary` `addEntriesFromDictionary` | |
// meaning this won't replace the whole extras dictionary. | |
scope.setExtras(userInfo) | |
scope.setLevel(level) | |
} | |
} | |
let flushEventThenCallCallback: () -> Void = { | |
SentrySDK.flush(timeout: self.flushTimeout) | |
callback() | |
} | |
if wait { | |
flushEventThenCallCallback() | |
} else { | |
DispatchQueue.global().async { flushEventThenCallCallback() } | |
} | |
} |
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
public let context: [String: Any] | ||
public let tags: [String: String] | ||
public let isHandled: Bool | ||
public let handledBy: String |
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.
Nitpick: If handledBy
is declared as String?
, I think the isHandled
property can be implied as var isHandled: Bool { handledBy != nil }
?
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.
Although both seem connected, I'm hesitant to infer that having handledBy
defined implies that isHandled
should be false
. Let me explain a couple of cases to clarify it.
isHandled
determines if the exception is handled by the editor, currently, this happens for exceptions captured by the error boundary components (reference). When an error is encountered, for instance, at block level we display an error message but let the user continue editing a post. In this case, handledBy
will tell the mechanism used to handle it and isHandled
will be true
.
On the other hand, other exceptions can be captured via the global error handler (reference). These are exceptions produced outside the error boundaries. Here, we use handledBy
to tell that the exception was captured by the global error handler but we mark them as unhandled (i.e. isHandled
as false
) because the editor will crash.
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.
Thanks for the explanation!
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
Sources/Remote Logging/Crash Logging/SentryEventJSException.swift
Outdated
Show resolved
Hide resolved
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.
LGTM!
I'm not familiar with this codebase and not sure how feasible/easy it is to add unit tests, but it'd be nice to add some unit tests to ensure the event payload scheme is correct, i.e. no threads
and debug_meta
.
Yep, I haven't added unit tests yet to this repository either. I briefly checked the unit tests of Thank you so much @crazytonyli for your suggestions and for reviewing the PR 🙇 ! |
I noticed that the job |
Related PRs:
This PR expands the Crash logging service to support logging JavaScript exceptions to Sentry. This will allow hybrid sources like React Native (used in Gutenberg Mobile) to log exceptions with detailed information and symbolicated stack traces, which will greatly simplify the crash debugging process.
The approach is based on previous attempts to log JavaScript exceptions (p9ugOq-249-p2). However, in this case, we are following an SDK-less implementation where the exception is bubbled up to the host app and sent as a Sentry event using the Sentry iOS SDK. As can be seen in the implementation, the Sentry event is adjusted to ensure that Sentry processes it as a JavaScript exception and hence, symbolicate the stack trace with previously uploaded source maps.
The process to log a JavaScript exception has the following steps:
Regarding the source maps, they are uploaded as part of the build process of the host app (reference).
More information about this approach is detailed in p9ugOq-4p6-p2.
To test
Since it involves testing exceptions, we need to modify the code to force them and generate an installable build. A test PR has been created for this purpose, follow the testing instructions from wordpress-mobile/gutenberg-mobile#6654.
CHANGELOG.md
if necessary.