Skip to content

Commit

Permalink
Report suppressed exceptions as exception group (#3396)
Browse files Browse the repository at this point in the history
* replace hub with scopes

* Add Scopes

* Introduce `IScopes` interface.

* Replace `IHub` with `IScopes` in core

* Replace `IHub` with `IScopes` in android core

* Replace `IHub` with `IScopes` in android integrations

* Replace `IHub` with `IScopes` in apollo integrations

* Replace `IHub` with `IScopes` in okhttp integration

* Replace `IHub` with `IScopes` in graphql integration

* Replace `IHub` with `IScopes` in logging integrations

* Replace `IHub` with `IScopes` in more integrations

* Replace `IHub` with `IScopes` in OTel integration

* Replace `IHub` with `IScopes` in Spring 5 / Spring Boot 2 integrations

* Replace `IHub` with `IScopes` in Spring 6 / Spring Boot 3 integrations

* Replace `IHub` with `IScopes` in samples

* gitscopes -> github

* Replace ThreadLocal with ScopesStorage

* Move client and throwable to span map to scope

* Add global scope

* use global scope in Scopes

* Implement pushScope popScope and withScope for Scopes

* Add pushIsolationScope; add fork methods to ISCope

* Use separate scopes for current, isolation and global scope; rename mainScopes to rootScopes

* Allow controlling which scope configureScope uses

* Combine scopes

* Use new API for CRONS integrations

* Add lifecycle helper

* Change spring integrations to use new API

* Use new API in servlet integrations

* Use new API for kotlin coroutines and wrapers for Supplier/Callable

* Discussion TODOs

* Fix breadcrumb ordering

* Mark TODOS with [HSM]

* Add getGlobalScope and forkedRootScopes to IScopes

* Fix EventProcessor ordering on scopes

* Reuse code in Scopes

* No longer replace global scope

* Replace hub occurrences in comments, var names etc.

* Implement ScopesTest

* Implement CombinedScopeViewTest

* Fix combined contexts

* Use combined scopes for cross platform

* Changes according to reviews of previous PRs

* more

* even more

* isEnabled checks client instead of having a property on Scopes

* Use SentryOptions.empty

* Remove Hub

* Report suppressed exceptions as exception group

* api dump

* add tests for suppressed exceptions

* Format code

* add additinoal test

* Format code

* apply suggestion

* add changelog

* fix changelog

---------

Co-authored-by: Lukas Bloder <lukas.bloder@gmail.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
3 people authored May 13, 2024
1 parent e7007dd commit dc56a6a
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add support for Spring Rest Client ([#3199](https://github.com/getsentry/sentry-java/pull/3199))
- Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396)

## 7.6.0

Expand Down
9 changes: 9 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -4472,18 +4472,24 @@ public final class io/sentry/protocol/Mechanism : io/sentry/JsonSerializable, io
public fun <init> (Ljava/lang/Thread;)V
public fun getData ()Ljava/util/Map;
public fun getDescription ()Ljava/lang/String;
public fun getExceptionId ()Ljava/lang/Integer;
public fun getHelpLink ()Ljava/lang/String;
public fun getMeta ()Ljava/util/Map;
public fun getParentId ()Ljava/lang/Integer;
public fun getSynthetic ()Ljava/lang/Boolean;
public fun getType ()Ljava/lang/String;
public fun getUnknown ()Ljava/util/Map;
public fun isExceptionGroup ()Ljava/lang/Boolean;
public fun isHandled ()Ljava/lang/Boolean;
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
public fun setData (Ljava/util/Map;)V
public fun setDescription (Ljava/lang/String;)V
public fun setExceptionGroup (Ljava/lang/Boolean;)V
public fun setExceptionId (Ljava/lang/Integer;)V
public fun setHandled (Ljava/lang/Boolean;)V
public fun setHelpLink (Ljava/lang/String;)V
public fun setMeta (Ljava/util/Map;)V
public fun setParentId (Ljava/lang/Integer;)V
public fun setSynthetic (Ljava/lang/Boolean;)V
public fun setType (Ljava/lang/String;)V
public fun setUnknown (Ljava/util/Map;)V
Expand All @@ -4498,9 +4504,12 @@ public final class io/sentry/protocol/Mechanism$Deserializer : io/sentry/JsonDes
public final class io/sentry/protocol/Mechanism$JsonKeys {
public static final field DATA Ljava/lang/String;
public static final field DESCRIPTION Ljava/lang/String;
public static final field EXCEPTION_ID Ljava/lang/String;
public static final field HANDLED Ljava/lang/String;
public static final field HELP_LINK Ljava/lang/String;
public static final field IS_EXCEPTION_GROUP Ljava/lang/String;
public static final field META Ljava/lang/String;
public static final field PARENT_ID Ljava/lang/String;
public static final field SYNTHETIC Ljava/lang/String;
public static final field TYPE Ljava/lang/String;
public fun <init> ()V
Expand Down
40 changes: 34 additions & 6 deletions sentry/src/main/java/io/sentry/SentryExceptionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -136,12 +136,20 @@ public List<SentryException> getSentryExceptions(final @NotNull Throwable throwa
@TestOnly
@NotNull
Deque<SentryException> extractExceptionQueue(final @NotNull Throwable throwable) {
final Deque<SentryException> exceptions = new ArrayDeque<>();
final Set<Throwable> circularityDetector = new HashSet<>();
return extractExceptionQueueInternal(
throwable, new AtomicInteger(-1), new HashSet<>(), new ArrayDeque<>());
}

Deque<SentryException> extractExceptionQueueInternal(
final @NotNull Throwable throwable,
final @NotNull AtomicInteger exceptionId,
final @NotNull HashSet<Throwable> circularityDetector,
final @NotNull Deque<SentryException> exceptions) {
Mechanism exceptionMechanism;
Thread thread;

Throwable currentThrowable = throwable;
int parentId = exceptionId.get();

// Stack the exceptions to send them in the reverse order
while (currentThrowable != null && circularityDetector.add(currentThrowable)) {
Expand All @@ -155,20 +163,40 @@ Deque<SentryException> extractExceptionQueue(final @NotNull Throwable throwable)
thread = exceptionMechanismThrowable.getThread();
snapshot = exceptionMechanismThrowable.isSnapshot();
} else {
exceptionMechanism = null;
exceptionMechanism = new Mechanism();
thread = Thread.currentThread();
}

final boolean includeSentryFrames =
exceptionMechanism != null && Boolean.FALSE.equals(exceptionMechanism.isHandled());
final boolean includeSentryFrames = Boolean.FALSE.equals(exceptionMechanism.isHandled());
final List<SentryStackFrame> frames =
sentryStackTraceFactory.getStackFrames(
currentThrowable.getStackTrace(), includeSentryFrames);
SentryException exception =
getSentryException(
currentThrowable, exceptionMechanism, thread.getId(), frames, snapshot);
exceptions.addFirst(exception);

if (exceptionMechanism.getType() == null) {
exceptionMechanism.setType("chained");
}

if (exceptionId.get() >= 0) {
exceptionMechanism.setParentId(parentId);
}

final int currentExceptionId = exceptionId.incrementAndGet();
exceptionMechanism.setExceptionId(currentExceptionId);

Throwable[] suppressed = currentThrowable.getSuppressed();
if (suppressed != null && suppressed.length > 0) {
exceptionMechanism.setExceptionGroup(true);
for (Throwable suppressedThrowable : suppressed) {
extractExceptionQueueInternal(
suppressedThrowable, exceptionId, circularityDetector, exceptions);
}
}
currentThrowable = currentThrowable.getCause();
parentId = currentExceptionId;
}

return exceptions;
Expand Down
57 changes: 57 additions & 0 deletions sentry/src/main/java/io/sentry/protocol/Mechanism.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ public final class Mechanism implements JsonUnknown, JsonSerializable {
* for grouping or display purposes.
*/
private @Nullable Boolean synthetic;
/**
* Exception ID. Used. e.g. for exception groups to build a hierarchy. This is referenced as
* parent by child exceptions which for Java SDK means Throwable.getSuppressed().
*/
private @Nullable Integer exceptionId;
/** Parent exception ID. Used e.g. for exception groups to build a hierarchy. */
private @Nullable Integer parentId;
/**
* Whether this is a group of exceptions. For Java SDK this means there were suppressed
* exceptions.
*/
private @Nullable Boolean exceptionGroup;

@SuppressWarnings("unused")
private @Nullable Map<String, Object> unknown;
Expand Down Expand Up @@ -140,6 +152,30 @@ public void setSynthetic(final @Nullable Boolean synthetic) {
this.synthetic = synthetic;
}

public @Nullable Integer getExceptionId() {
return exceptionId;
}

public void setExceptionId(final @Nullable Integer exceptionId) {
this.exceptionId = exceptionId;
}

public @Nullable Integer getParentId() {
return parentId;
}

public void setParentId(final @Nullable Integer parentId) {
this.parentId = parentId;
}

public @Nullable Boolean isExceptionGroup() {
return exceptionGroup;
}

public void setExceptionGroup(final @Nullable Boolean exceptionGroup) {
this.exceptionGroup = exceptionGroup;
}

// JsonKeys

public static final class JsonKeys {
Expand All @@ -150,6 +186,9 @@ public static final class JsonKeys {
public static final String META = "meta";
public static final String DATA = "data";
public static final String SYNTHETIC = "synthetic";
public static final String EXCEPTION_ID = "exception_id";
public static final String PARENT_ID = "parent_id";
public static final String IS_EXCEPTION_GROUP = "is_exception_group";
}

// JsonUnknown
Expand Down Expand Up @@ -191,6 +230,15 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
if (synthetic != null) {
writer.name(JsonKeys.SYNTHETIC).value(synthetic);
}
if (exceptionId != null) {
writer.name(JsonKeys.EXCEPTION_ID).value(logger, exceptionId);
}
if (parentId != null) {
writer.name(JsonKeys.PARENT_ID).value(logger, parentId);
}
if (exceptionGroup != null) {
writer.name(JsonKeys.IS_EXCEPTION_GROUP).value(exceptionGroup);
}
if (unknown != null) {
for (String key : unknown.keySet()) {
Object value = unknown.get(key);
Expand Down Expand Up @@ -238,6 +286,15 @@ public static final class Deserializer implements JsonDeserializer<Mechanism> {
case JsonKeys.SYNTHETIC:
mechanism.synthetic = reader.nextBooleanOrNull();
break;
case JsonKeys.EXCEPTION_ID:
mechanism.exceptionId = reader.nextIntegerOrNull();
break;
case JsonKeys.PARENT_ID:
mechanism.parentId = reader.nextIntegerOrNull();
break;
case JsonKeys.IS_EXCEPTION_GROUP:
mechanism.exceptionGroup = reader.nextBooleanOrNull();
break;
default:
if (unknown == null) {
unknown = new HashMap<>();
Expand Down
Loading

0 comments on commit dc56a6a

Please sign in to comment.