-
-
Notifications
You must be signed in to change notification settings - Fork 237
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
Improve ergonomics for reporting nested exceptions #1045
Comments
I am not entirely sure if I get it, what you need is something like this?
|
Yeah, but those should also be passed though the event processors, so that for example the dio eventprocessor can add the request information to the event. I'll try to add an example, which should demonstrate the problem much better. |
Image the following scenario:
The Simplified, it looks like this: // from graphql
class NetworkException extends LinkException {
Object? originalException;
StackTrace? originalStackTrace;
String? message;
Uri? uri;
}
// from dio
class DioError implements Exception {
RequestOptions requestOptions;
Response? response;
dynamic error;
StackTrace? stackTrace;
} Now I still want to get the http request information added to the Sentry event. In order to do that, I have to write a custom event processor for the This is just one example, but it also see it happening with our application code and other libraries :( Does that help making the problem more understandable? |
I understand the problem but both exceptions come from different libs, so I don't see how this would be solved automatically by the Sentry SDK. |
I would like to have an API which is like |
I've played around a bit and came up with this proof-of-concept: import '../sentry.dart';
import 'utils/isolate_utils.dart';
class NestedExceptionUtil {
Future<SentryEvent?> eventFromException({
required SentryOptions options,
required Object exception,
StackTrace? stackTrace,
}) async {
SentryEvent event = _attachStackTrace(
options: options,
event: SentryEvent(throwable: exception),
stackTrace: stackTrace,
);
for (final processor in options.eventProcessors) {
SentryEvent? processedEvent = event;
try {
processedEvent = await processor.apply(event);
} catch (exception, stackTrace) {
options.logger(
SentryLevel.error,
'An exception occurred while processing event by a processor',
exception: exception,
stackTrace: stackTrace,
);
}
if (processedEvent == null) {
return null;
}
event = processedEvent;
}
return event;
}
SentryEvent _attachStackTrace({
required SentryOptions options,
required SentryEvent event,
StackTrace? stackTrace,
}) {
final isolateName = getIsolateName();
// Isolates have no id, so the hashCode of the name will be used as id
final isolateId = isolateName?.hashCode;
if (event.throwableMechanism != null) {
var sentryException = options.exceptionFactory.getSentryException(
event.throwableMechanism,
stackTrace: stackTrace,
);
if (options.platformChecker.isWeb) {
return event.copyWith(
exceptions: [
...?event.exceptions,
sentryException,
],
);
}
SentryThread? thread;
if (isolateName != null && options.attachThreads) {
sentryException = sentryException.copyWith(threadId: isolateId);
thread = SentryThread(
id: isolateId,
name: isolateName,
crashed: true,
current: true,
);
}
return event.copyWith(
exceptions: [...?event.exceptions, sentryException],
threads: [
...?event.threads,
if (thread != null) thread,
],
);
}
// The stacktrace is not part of an exception,
// therefore add it to the threads.
// https://develop.sentry.dev/sdk/event-payloads/stacktrace/
// Don't automatically add stacktraces to inner exceptions
if (stackTrace != null) {
final frames = options.stackTraceFactory.getStackFrames(stackTrace);
if (frames.isNotEmpty) {
event = event.copyWith(threads: [
...?event.threads,
SentryThread(
name: isolateName,
id: isolateId,
crashed: false,
current: true,
stacktrace: SentryStackTrace(frames: frames),
),
]);
}
}
return event;
}
SentryEvent merge(SentryEvent main, SentryEvent second) {
return main.copyWith(
user: main.user ?? second.user,
exceptions: [
...?main.exceptions,
...?second.exceptions,
],
threads: [
...?main.threads,
...?second.threads,
],
extra: {
...?main.extra,
...?second.extra,
},
request: main.request ?? second.request,
serverName: main.serverName ?? second.serverName,
);
}
} Then you could do something like this in an event processor: // Given this exception from graphql
class NetworkException extends LinkException {
Object? originalException;
StackTrace? originalStackTrace;
String? message;
Uri? uri;
}
class ExampleEventProcessor implements EventProcessor {
@override
FutureOr<SentryEvent?> apply(SentryEvent event, {dynamic hint}) {
final exception = event.throwable;
if(exception is NetworkException) {
final secondaryEvent = NestedExceptionUtil.eventFromException(options, exception.originalException!, exception.originalStackTrace!);
return NestedExceptionUtil.merge(event,secondaryEvent);
}
return event;
}
} That way nested exception are recursively added to an event when possible. |
Give me some time to think about this approach. |
@ueman what if we offer a method like:
|
How would that help for automatic reported nested exceptions? At that point, you don't know whether it is a nested exception. For manually reported exceptions that would help though. |
That would be the first step, yes.
You have the instance of the event, the event now gets a list of Example: class ExampleEventProcessor implements EventProcessor {
@override
FutureOr<SentryEvent?> apply(SentryEvent event, {dynamic hint}) {
final exception = event.throwable;
if(exception is NetworkException) {
event.throwables.add(exception.originalException).
}
return event;
}
} |
Quick note, maybe this list should also get a |
Ah, I see, that makes sense. But currently the event processors run after the SentryException and SentryStackTrace are created right? |
yes, that's a good point, and it should run after because the users should be able to mutate the final version of the event, this is something that should be considered when working on this, not sure how this would look like, maybe having a |
Its a good candidate for v7, I will discuss this, thanks for the input. |
Maybe there could be hook between Basically something like (probably not the best naming, but I hope you get the idea) class ExceptionExctractor<T> {
// list to handle cases where there might be list of exceptions
List<Tuple<Object, StackTrace?>> extract(T exception);
}
class NetworkExceptionExtractor implements ExceptionExctractor<NetworkException> {
List<Tuple<Object, StackTrace?>> extract(NetworkException exception) {
return [Tuple(exception.originalException, exception.originalStackTrace)];
}
}
options.registerExtractor(NetworkException, NetworkExceptionExtractor()); And the results of Event Processors then can just handle the case, where there's additional data in an exception to put it on the event. For dio the event processor would then basically just be class DioEventProcessor implements EventProcessor {
@override
FutureOr<SentryEvent?> apply(SentryEvent event, {dynamic hint}) {
final dioError = event.throwables.firstWhereOrNull((e) => e is DioError);
if (dioError == null) {
return event;
}
Contexts contexts = event.contexts;
if (event.contexts.response == null) {
contexts = contexts.copyWith(response: _responseFrom(dioError));
}
// Don't override just parts of the original request.
// Keep the original one or if there's none create one.
event = event.copyWith(
request: event.request ?? _requestFrom(dioError),
contexts: contexts,
);
return event;
}
} That would solve the problem of the order of processing, seems like it's mostly backwards compatible and easy to use. |
Then |
This likely requires a breaking change so adding the v7 milestone in it. |
Some languages have a |
I was starting to prototype this #1045 (comment) proposal, but I didn't yet make it work. So far, that's the solution I like the most from the solutions from this thread. Maybe there are even better ways? |
I'd like to request a way to make it easier to handle nested exceptions.
Since there is no built-in way in Dart to nest exceptions, a lot of libraries have their own way of nesting exceptions.
This can already be seen in the current Sentry code base as there are already a couple of places in the current code base where nested exceptions are handled. Those include the PlatformException event processor for Android, the Dio EventProcessor and possibly more.
Now the problem is, that they all handle just their own nested exceptions, but there's no way of recursively resolve interdependencies, like if a nested dio exception is included in a hypothetical other nested exception.
Solution 1:
Therefore, I would like to have an API where I can input an exception and get a sentry exception (or even a sentry event) out of it, without it being send. That api should also apply all the event processors. That way, it's way easier to create better reports for nested exceptions.
Solution 2:
Introduce an interface for nested exceptions which libraries/applications can use and which Sentry can resolve on its own. This probably doesn't really solve the problem for other libraries but maybe for applications.
The text was updated successfully, but these errors were encountered: