Skip to content

Instantly share code, notes, and snippets.

@maciejwalkowiak
Created September 30, 2021 11:07
Show Gist options
  • Save maciejwalkowiak/faaaad64ae59c4f79127a91022fcc3e6 to your computer and use it in GitHub Desktop.
Save maciejwalkowiak/faaaad64ae59c4f79127a91022fcc3e6 to your computer and use it in GitHub Desktop.
Sentry suppressed exceptions workaround
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;
}
}
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;
}
}
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