Created
September 30, 2021 11:07
-
-
Save maciejwalkowiak/faaaad64ae59c4f79127a91022fcc3e6 to your computer and use it in GitHub Desktop.
Sentry suppressed exceptions workaround
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.jetbrains.annotations.Nullable; | |
import org.jetbrains.annotations.TestOnly; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.List; | |
import io.sentry.protocol.SentryStackFrame; | |
/** class responsible for converting Java StackTraceElements to SentryStackFrames */ | |
final class SentryStackTraceFactory { | |
/** list of inApp excludes */ | |
private final @Nullable List<String> inAppExcludes; | |
/** list of inApp includes */ | |
private final @Nullable List<String> inAppIncludes; | |
public SentryStackTraceFactory( | |
@Nullable final List<String> inAppExcludes, @Nullable List<String> inAppIncludes) { | |
this.inAppExcludes = inAppExcludes; | |
this.inAppIncludes = inAppIncludes; | |
} | |
/** | |
* convert an Array of Java StackTraceElements to a list of SentryStackFrames | |
* | |
* @param elements Array of Java StackTraceElements | |
* @return list of SentryStackFrames or null if none | |
*/ | |
@Nullable | |
List<SentryStackFrame> getStackFrames(@Nullable final StackTraceElement[] elements) { | |
List<SentryStackFrame> sentryStackFrames = null; | |
if (elements != null && elements.length > 0) { | |
sentryStackFrames = new ArrayList<>(); | |
for (StackTraceElement item : elements) { | |
if (item != null) { | |
// we don't want to add our own frames | |
final String className = item.getClassName(); | |
if (className.startsWith("io.sentry.") | |
&& !className.startsWith("io.sentry.samples.") | |
&& !className.startsWith("io.sentry.mobile.")) { | |
continue; | |
} | |
final SentryStackFrame sentryStackFrame = new SentryStackFrame(); | |
// https://docs.sentry.io/development/sdk-dev/features/#in-app-frames | |
sentryStackFrame.setInApp(isInApp(className)); | |
sentryStackFrame.setModule(className); | |
sentryStackFrame.setFunction(item.getMethodName()); | |
sentryStackFrame.setFilename(item.getFileName()); | |
// Protocol doesn't accept negative line numbers. | |
// The runtime seem to use -2 as a way to signal a native method | |
if (item.getLineNumber() >= 0) { | |
sentryStackFrame.setLineno(item.getLineNumber()); | |
} | |
sentryStackFrame.setNative(item.isNativeMethod()); | |
sentryStackFrames.add(sentryStackFrame); | |
} | |
} | |
Collections.reverse(sentryStackFrames); | |
} | |
return sentryStackFrames; | |
} | |
/** | |
* Returns if the className is InApp or not. | |
* | |
* @param className the className | |
* @return true if it is or false otherwise | |
*/ | |
@TestOnly | |
boolean isInApp(final @Nullable String className) { | |
if (className == null || className.isEmpty()) { | |
return true; | |
} | |
if (inAppIncludes != null) { | |
for (String include : inAppIncludes) { | |
if (className.startsWith(include)) { | |
return true; | |
} | |
} | |
} | |
if (inAppExcludes != null) { | |
for (String exclude : inAppExcludes) { | |
if (className.startsWith(exclude)) { | |
return false; | |
} | |
} | |
} | |
return false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.jetbrains.annotations.NotNull; | |
import org.jetbrains.annotations.Nullable; | |
import org.springframework.stereotype.Component; | |
import io.sentry.EventProcessor; | |
import io.sentry.SentryEvent; | |
@Component | |
public class SuppressedAwareEventProcessor implements EventProcessor { | |
private final SuppressedAwareSentryExceptionFactory exceptionFactory; | |
public SuppressedAwareEventProcessor(SuppressedAwareSentryExceptionFactory exceptionFactory) { | |
this.exceptionFactory = exceptionFactory; | |
} | |
@Override | |
public @Nullable SentryEvent process(@NotNull SentryEvent event, @Nullable Object hint) { | |
if (event.getThrowable() != null) { | |
event.setExceptions(exceptionFactory.getSentryExceptions(event.getThrowable())); | |
} | |
return event; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.jetbrains.annotations.NotNull; | |
import org.jetbrains.annotations.Nullable; | |
import org.jetbrains.annotations.TestOnly; | |
import org.springframework.stereotype.Component; | |
import java.util.ArrayDeque; | |
import java.util.ArrayList; | |
import java.util.Deque; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Set; | |
import io.sentry.SentryOptions; | |
import io.sentry.exception.ExceptionMechanismException; | |
import io.sentry.protocol.Mechanism; | |
import io.sentry.protocol.SentryException; | |
import io.sentry.protocol.SentryStackFrame; | |
import io.sentry.protocol.SentryStackTrace; | |
/** class responsible for converting Java Throwable to SentryExceptions */ | |
@Component | |
final class SuppressedAwareSentryExceptionFactory { | |
/** the SentryStackTraceFactory */ | |
private final @NotNull SentryStackTraceFactory sentryStackTraceFactory; | |
public SuppressedAwareSentryExceptionFactory(final @NotNull SentryOptions options) { | |
this.sentryStackTraceFactory = new SentryStackTraceFactory(options.getInAppExcludes(), options.getInAppIncludes()); | |
} | |
/** | |
* Creates a new instance from the given {@code throwable}. | |
* | |
* @param throwable the {@link Throwable} to build this instance from | |
*/ | |
@NotNull | |
List<SentryException> getSentryExceptions(final @NotNull Throwable throwable) { | |
return getSentryExceptions(extractExceptionQueue(throwable)); | |
} | |
/** | |
* Creates a new instance from the given {@code exceptions}. | |
* | |
* @param exceptions a {@link Deque} of {@link SentryException} to build this instance from | |
*/ | |
private @NotNull List<SentryException> getSentryExceptions( | |
final @NotNull Deque<SentryException> exceptions) { | |
return new ArrayList<>(exceptions); | |
} | |
/** | |
* Creates a Sentry exception based on a Java Throwable. | |
* | |
* <p>The {@code childExceptionStackTrace} parameter is used to define the common frames with the | |
* child exception (Exception caused by {@code throwable}). | |
* | |
* @param throwable Java exception to send to Sentry. | |
* @param exceptionMechanism The optional {@link Mechanism} of the {@code throwable}. Or null if | |
* none exist. | |
* @param thread The optional {@link Thread} which the exception originated. Or null if not known. | |
* @param snapshot if the captured {@link Thread}'s stacktrace is a snapshot, See {@link | |
* SentryStackTrace#getSnapshot()} | |
*/ | |
private @NotNull SentryException getSentryException( | |
@NotNull final Throwable throwable, | |
@Nullable final Mechanism exceptionMechanism, | |
@Nullable final Thread thread, | |
final boolean snapshot) { | |
final Package exceptionPackage = throwable.getClass().getPackage(); | |
final String fullClassName = throwable.getClass().getName(); | |
final SentryException exception = new SentryException(); | |
final String exceptionMessage = throwable.getMessage(); | |
final String exceptionClassName = | |
exceptionPackage != null | |
? fullClassName.replace(exceptionPackage.getName() + ".", "") | |
: fullClassName; | |
final String exceptionPackageName = | |
exceptionPackage != null ? exceptionPackage.getName() : null; | |
final List<SentryStackFrame> frames = | |
sentryStackTraceFactory.getStackFrames(throwable.getStackTrace()); | |
if (frames != null && !frames.isEmpty()) { | |
final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); | |
if (snapshot) { | |
sentryStackTrace.setSnapshot(true); | |
} | |
exception.setStacktrace(sentryStackTrace); | |
} | |
if (thread != null) { | |
exception.setThreadId(thread.getId()); | |
} | |
exception.setType(exceptionClassName); | |
exception.setMechanism(exceptionMechanism); | |
exception.setModule(exceptionPackageName); | |
exception.setValue(exceptionMessage); | |
return exception; | |
} | |
/** | |
* Transforms a {@link Throwable} into a Queue of {@link SentryException}. | |
* | |
* <p>Multiple values represent chained exceptions and should be sorted oldest to newest. | |
* | |
* @param throwable throwable to transform in a queue of exceptions. | |
* @return a queue of exception with StackTrace. | |
*/ | |
@TestOnly | |
@NotNull | |
Deque<SentryException> extractExceptionQueue(final @NotNull Throwable throwable) { | |
final Deque<SentryException> exceptions = new ArrayDeque<>(); | |
final Set<Throwable> circularityDetector = new HashSet<>(); | |
Mechanism exceptionMechanism; | |
Thread thread; | |
Throwable currentThrowable = throwable; | |
// Stack the exceptions to send them in the reverse order | |
while (currentThrowable != null && circularityDetector.add(currentThrowable)) { | |
boolean snapshot = false; | |
if (currentThrowable instanceof ExceptionMechanismException) { | |
// this is for ANR I believe | |
ExceptionMechanismException exceptionMechanismThrowable = | |
(ExceptionMechanismException) currentThrowable; | |
exceptionMechanism = exceptionMechanismThrowable.getExceptionMechanism(); | |
currentThrowable = exceptionMechanismThrowable.getThrowable(); | |
thread = exceptionMechanismThrowable.getThread(); | |
snapshot = exceptionMechanismThrowable.isSnapshot(); | |
} else { | |
exceptionMechanism = null; | |
thread = Thread.currentThread(); | |
} | |
SentryException exception = | |
getSentryException(currentThrowable, exceptionMechanism, thread, snapshot); | |
exceptions.addFirst(exception); | |
currentThrowable = currentThrowable.getCause(); | |
} | |
if (throwable.getSuppressed() != null) { | |
Mechanism suppressedMechanism = new Mechanism(); | |
suppressedMechanism.setType("suppressed"); | |
suppressedMechanism.setHandled(false); | |
for (Throwable suppressed : throwable.getSuppressed()) { | |
exceptions.addFirst(getSentryException(suppressed, suppressedMechanism, null, false)); | |
} | |
} | |
return exceptions; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment