Skip to content

Commit

Permalink
Merge 663b337 into 06db228
Browse files Browse the repository at this point in the history
  • Loading branch information
adinauer authored Apr 30, 2024
2 parents 06db228 + 663b337 commit a23c145
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 6 deletions.
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;
Integer 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
187 changes: 187 additions & 0 deletions sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,193 @@ class SentryExceptionFactoryTest {
assertEquals(777, frame.lineno)
}

@Test
fun `when exception with mechanism suppressed exceptions, add them and show as group`() {
val exception = Exception("message")
val suppressedException = Exception("suppressed")
exception.addSuppressed(suppressedException)

val mechanism = Mechanism()
mechanism.type = "ANR"
val thread = Thread()
val throwable = ExceptionMechanismException(mechanism, exception, thread)

val queue = fixture.getSut().extractExceptionQueue(throwable)

val suppressedInQueue = queue.pop()
val mainInQueue = queue.pop()

assertEquals("suppressed", suppressedInQueue.value)
assertEquals(1, suppressedInQueue.mechanism?.exceptionId)
assertEquals(0, suppressedInQueue.mechanism?.parentId)

assertEquals("message", mainInQueue.value)
assertEquals(0, mainInQueue.mechanism?.exceptionId)
assertEquals(true, mainInQueue.mechanism?.isExceptionGroup)
}

@Test
fun `nested exception that contains suppressed exceptions is marked as group`() {
val exception = Exception("inner")
val suppressedException = Exception("suppressed")
exception.addSuppressed(suppressedException)

val outerException = Exception("outer", exception)

val queue = fixture.getSut().extractExceptionQueue(outerException)

val suppressedInQueue = queue.pop()
val mainInQueue = queue.pop()
val outerInQueue = queue.pop()

assertEquals("suppressed", suppressedInQueue.value)
assertEquals(2, suppressedInQueue.mechanism?.exceptionId)
assertEquals(1, suppressedInQueue.mechanism?.parentId)

assertEquals("inner", mainInQueue.value)
assertEquals(1, mainInQueue.mechanism?.exceptionId)
assertEquals(0, mainInQueue.mechanism?.parentId)
assertEquals(true, mainInQueue.mechanism?.isExceptionGroup)

assertEquals("outer", outerInQueue.value)
assertEquals(0, outerInQueue.mechanism?.exceptionId)
assertNull(outerInQueue.mechanism?.parentId)
assertNull(outerInQueue.mechanism?.isExceptionGroup)
}

@Test
fun `nested exception within Mechanism that contains suppressed exceptions is marked as group`() {
val exception = Exception("inner")
val suppressedException = Exception("suppressed")
exception.addSuppressed(suppressedException)

val mechanism = Mechanism()
mechanism.type = "ANR"
val thread = Thread()

val outerException = ExceptionMechanismException(mechanism, Exception("outer", exception), thread)

val queue = fixture.getSut().extractExceptionQueue(outerException)

val suppressedInQueue = queue.pop()
val mainInQueue = queue.pop()
val outerInQueue = queue.pop()

assertEquals("suppressed", suppressedInQueue.value)
assertEquals(2, suppressedInQueue.mechanism?.exceptionId)
assertEquals(1, suppressedInQueue.mechanism?.parentId)

assertEquals("inner", mainInQueue.value)
assertEquals(1, mainInQueue.mechanism?.exceptionId)
assertEquals(0, mainInQueue.mechanism?.parentId)
assertEquals(true, mainInQueue.mechanism?.isExceptionGroup)

assertEquals("outer", outerInQueue.value)
assertEquals(0, outerInQueue.mechanism?.exceptionId)
assertNull(outerInQueue.mechanism?.parentId)
assertNull(outerInQueue.mechanism?.isExceptionGroup)
}

@Test
fun `nested exception with nested exception that contain suppressed exceptions are marked as group`() {
val innerMostException = Exception("innermost")
val innerMostSuppressed = Exception("innermostSuppressed")
innerMostException.addSuppressed(innerMostSuppressed)

val innerException = Exception("inner", innerMostException)
val innerSuppressed = Exception("suppressed")
innerException.addSuppressed(innerSuppressed)

val outerException = Exception("outer", innerException)

val queue = fixture.getSut().extractExceptionQueue(outerException)

val innerMostSuppressedInQueue = queue.pop()
val innerMostExceptionInQueue = queue.pop()
val innerSuppressedInQueue = queue.pop()
val innerExceptionInQueue = queue.pop()
val outerInQueue = queue.pop()

assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value)
assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId)
assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId)
assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup)

assertEquals("innermost", innerMostExceptionInQueue.value)
assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId)
assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId)
assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup)

assertEquals("suppressed", innerSuppressedInQueue.value)
assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId)
assertEquals(1, innerSuppressedInQueue.mechanism?.parentId)
assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup)

assertEquals("inner", innerExceptionInQueue.value)
assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId)
assertEquals(0, innerExceptionInQueue.mechanism?.parentId)
assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup)

assertEquals("outer", outerInQueue.value)
assertEquals(0, outerInQueue.mechanism?.exceptionId)
assertNull(outerInQueue.mechanism?.parentId)
assertNull(outerInQueue.mechanism?.isExceptionGroup)
}

@Test
fun `nested exception with nested exception that contain suppressed exceptions with a nested exception are marked as group`() {
val innerMostException = Exception("innermost")

val innerMostSuppressedNestedException = Exception("innermostSuppressedNested");
val innerMostSuppressed = Exception("innermostSuppressed", innerMostSuppressedNestedException)
innerMostException.addSuppressed(innerMostSuppressed)

val innerException = Exception("inner", innerMostException)
val innerSuppressed = Exception("suppressed")
innerException.addSuppressed(innerSuppressed)

val outerException = Exception("outer", innerException)

val queue = fixture.getSut().extractExceptionQueue(outerException)

val innerMostSuppressedNestedExceptionInQueue = queue.pop()
val innerMostSuppressedInQueue = queue.pop()
val innerMostExceptionInQueue = queue.pop()
val innerSuppressedInQueue = queue.pop()
val innerExceptionInQueue = queue.pop()
val outerInQueue = queue.pop()

assertEquals("innermostSuppressedNested", innerMostSuppressedNestedExceptionInQueue.value)
assertEquals(5, innerMostSuppressedNestedExceptionInQueue.mechanism?.exceptionId)
assertEquals(4, innerMostSuppressedNestedExceptionInQueue.mechanism?.parentId)
assertNull(innerMostSuppressedNestedExceptionInQueue.mechanism?.isExceptionGroup)

assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value)
assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId)
assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId)
assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup)

assertEquals("innermost", innerMostExceptionInQueue.value)
assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId)
assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId)
assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup)

assertEquals("suppressed", innerSuppressedInQueue.value)
assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId)
assertEquals(1, innerSuppressedInQueue.mechanism?.parentId)
assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup)

assertEquals("inner", innerExceptionInQueue.value)
assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId)
assertEquals(0, innerExceptionInQueue.mechanism?.parentId)
assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup)

assertEquals("outer", outerInQueue.value)
assertEquals(0, outerInQueue.mechanism?.exceptionId)
assertNull(outerInQueue.mechanism?.parentId)
assertNull(outerInQueue.mechanism?.isExceptionGroup)
}

internal class InnerClassThrowable constructor(cause: Throwable? = null) : Throwable(cause)

private val anonymousException = object : Exception() {
Expand Down

0 comments on commit a23c145

Please sign in to comment.