diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 7d4728089..25df214a3 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -7,11 +7,11 @@ import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import io.sentry.core.ILogger +import io.sentry.core.MainEventProcessor import io.sentry.core.SentryOptions import java.io.File import kotlin.test.BeforeTest import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.runner.RunWith @@ -36,7 +36,7 @@ class AndroidOptionsInitializerTest { val loggerField = logger.get(sentryOptions) val innerLogger = loggerField.javaClass.declaredFields.first { it.name == "logger" } innerLogger.isAccessible = true - assertEquals(AndroidLogger::class, innerLogger.get(loggerField)::class) + assertTrue(innerLogger.get(loggerField) is AndroidLogger) } @Test @@ -46,7 +46,18 @@ class AndroidOptionsInitializerTest { val mockLogger = mock() AndroidOptionsInitializer.init(sentryOptions, mockContext, mockLogger) - val actual = sentryOptions.eventProcessors.firstOrNull { it::class == DefaultAndroidEventProcessor::class } + val actual = sentryOptions.eventProcessors.any { it is DefaultAndroidEventProcessor } + assertNotNull(actual) + } + + @Test + fun `MainEventProcessor added to processors list and its the 1st`() { + val sentryOptions = SentryOptions() + val mockContext = createMockContext() + val mockLogger = mock() + + AndroidOptionsInitializer.init(sentryOptions, mockContext, mockLogger) + val actual = sentryOptions.eventProcessors.firstOrNull { it is MainEventProcessor } assertNotNull(actual) } diff --git a/sentry-core/src/main/java/io/sentry/core/MainEventProcessor.java b/sentry-core/src/main/java/io/sentry/core/MainEventProcessor.java new file mode 100644 index 000000000..6764a7704 --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/MainEventProcessor.java @@ -0,0 +1,37 @@ +package io.sentry.core; + +import io.sentry.core.util.Objects; + +public class MainEventProcessor implements EventProcessor { + + private final SentryOptions options; + private final SentryThreadFactory sentryThreadFactory = new SentryThreadFactory(); + private final SentryStackTraceFactory sentryStackTraceFactory = new SentryStackTraceFactory(); + private final SentryExceptionFactory sentryExceptionFactory = + new SentryExceptionFactory(sentryStackTraceFactory); + + MainEventProcessor(SentryOptions options) { + this.options = Objects.requireNonNull(options, "The SentryOptions is required."); + } + + @Override + public SentryEvent process(SentryEvent event) { + if (event.getThreads() == null) { + event.setThreads(sentryThreadFactory.getCurrentThreads()); + } + + if (event.getRelease() == null) { + event.setRelease(options.getRelease()); + } + if (event.getEnvironment() == null) { + event.setEnvironment(options.getEnvironment()); + } + + Throwable throwable = event.getThrowable(); + if (throwable != null) { + event.setException(sentryExceptionFactory.getSentryExceptions(throwable)); + } + + return event; + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/SentryClient.java b/sentry-core/src/main/java/io/sentry/core/SentryClient.java index 3ab5b2aea..5b99de755 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryClient.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryClient.java @@ -35,19 +35,6 @@ public SentryClient(SentryOptions options, @Nullable AsyncConnection connection) public SentryId captureEvent(SentryEvent event, @Nullable Scope scope) { log(options.getLogger(), SentryLevel.DEBUG, "Capturing event: %s", event.getEventId()); - // TODO: This will go into the MainEventProcessor - if (event.getThreads() == null) { - SentryThreadFactory sentryThreadFactory = new SentryThreadFactory(); - event.setThreads(sentryThreadFactory.getCurrentThreads()); - } - // TODO: To be done in the main processor - if (event.getRelease() == null) { - event.setRelease(options.getRelease()); - } - if (event.getEnvironment() == null) { - event.setEnvironment(options.getEnvironment()); - } - for (EventProcessor processor : options.getEventProcessors()) { processor.process(event); } diff --git a/sentry-core/src/main/java/io/sentry/core/SentryEvent.java b/sentry-core/src/main/java/io/sentry/core/SentryEvent.java index 78a8bf436..5d729f1f2 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryEvent.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryEvent.java @@ -19,7 +19,7 @@ public class SentryEvent implements IUnknownPropertiesConsumer { private String dist; private String logger; private SentryValues threads; - private SentryValues exceptions; + private SentryValues exception; private SentryLevel level; private String transaction; private String environment; @@ -119,12 +119,12 @@ public void setThreads(List threads) { this.threads = new SentryValues<>(threads); } - public List getExceptions() { - return exceptions.getValues(); + public List getException() { + return exception.getValues(); } - public void setExceptions(List exceptions) { - this.exceptions = new SentryValues<>(exceptions); + public void setException(List exception) { + this.exception = new SentryValues<>(exception); } public void setEventId(SentryId eventId) { @@ -139,14 +139,6 @@ public void setThrowable(Throwable throwable) { this.throwable = throwable; } - public void setThreads(SentryValues threads) { - this.threads = threads; - } - - public void setExceptions(SentryValues exceptions) { - this.exceptions = exceptions; - } - public SentryLevel getLevel() { return level; } diff --git a/sentry-core/src/main/java/io/sentry/core/SentryExceptionFactory.java b/sentry-core/src/main/java/io/sentry/core/SentryExceptionFactory.java new file mode 100644 index 000000000..158e04f21 --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/SentryExceptionFactory.java @@ -0,0 +1,118 @@ +package io.sentry.core; + +import io.sentry.core.exception.ExceptionMechanismThrowable; +import io.sentry.core.protocol.Mechanism; +import io.sentry.core.protocol.SentryException; +import io.sentry.core.protocol.SentryStackTrace; +import io.sentry.core.util.Objects; +import io.sentry.core.util.VisibleForTesting; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** class responsible for converting Java Throwable to SentryExceptions */ +class SentryExceptionFactory { + + private final SentryStackTraceFactory sentryStackTraceFactory; + + public SentryExceptionFactory(SentryStackTraceFactory sentryStackTraceFactory) { + this.sentryStackTraceFactory = + Objects.requireNonNull(sentryStackTraceFactory, "The SentryStackTraceFactory is required."); + } + + /** + * Creates a new instance from the given {@code throwable}. + * + * @param throwable the {@link Throwable} to build this instance from + */ + List getSentryExceptions(final 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 List getSentryExceptions(final Deque exceptions) { + return new ArrayList<>(exceptions); + } + + /** + * Creates a Sentry exception based on a Java Throwable. + * + *

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. + */ + private SentryException getSentryException( + final Throwable throwable, final Mechanism exceptionMechanism) { + + Package exceptionPackage = throwable.getClass().getPackage(); + String fullClassName = throwable.getClass().getName(); + + SentryException exception = new SentryException(); + + String exceptionMessage = throwable.getMessage(); + + String exceptionClassName = + exceptionPackage != null + ? fullClassName.replace(exceptionPackage.getName() + ".", "") + : fullClassName; + + String exceptionPackageName = exceptionPackage != null ? exceptionPackage.getName() : null; + + SentryStackTrace sentryStackTrace = new SentryStackTrace(); + sentryStackTrace.setFrames(sentryStackTraceFactory.getStackFrames(throwable.getStackTrace())); + + exception.setStacktrace(sentryStackTrace); + exception.setType(exceptionClassName); + exception.setMechanism(exceptionMechanism); + exception.setModule(exceptionPackageName); + exception.setValue(exceptionMessage); + + return exception; + } + + /** + * Transforms a {@link Throwable} into a Queue of {@link SentryException}. + * + *

Exceptions are stored in the queue from the most recent one to the oldest one. + * + * @param throwable throwable to transform in a queue of exceptions. + * @return a queue of exception with StackTrace. + */ + @VisibleForTesting + Deque extractExceptionQueue(final Throwable throwable) { + Deque exceptions = new ArrayDeque<>(); + Set circularityDetector = new HashSet<>(); + Mechanism exceptionMechanism; + + Throwable currentThrowable = throwable; + + // Stack the exceptions to send them in the reverse order + while (currentThrowable != null && circularityDetector.add(currentThrowable)) { + if (currentThrowable instanceof ExceptionMechanismThrowable) { + // this is for ANR I believe + ExceptionMechanismThrowable exceptionMechanismThrowable = + (ExceptionMechanismThrowable) currentThrowable; + exceptionMechanism = exceptionMechanismThrowable.getExceptionMechanism(); + currentThrowable = exceptionMechanismThrowable.getThrowable(); + } else { + exceptionMechanism = null; + } + + SentryException exception = getSentryException(currentThrowable, exceptionMechanism); + exceptions.add(exception); + currentThrowable = currentThrowable.getCause(); + } + + return exceptions; + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java index f50732c5e..2e2b9742e 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java @@ -171,6 +171,7 @@ public interface BeforeBreadcrumbCallback { } public SentryOptions() { + eventProcessors.add(new MainEventProcessor(this)); integrations.add(new UncaughtExceptionHandlerIntegration()); } } diff --git a/sentry-core/src/main/java/io/sentry/core/SentryStackTraceFactory.java b/sentry-core/src/main/java/io/sentry/core/SentryStackTraceFactory.java new file mode 100644 index 000000000..dec01304f --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/SentryStackTraceFactory.java @@ -0,0 +1,35 @@ +package io.sentry.core; + +import io.sentry.core.protocol.SentryStackFrame; +import io.sentry.core.util.Nullable; +import java.util.ArrayList; +import java.util.List; + +/** class responsible for converting Java StackTraceElements to SentryStackFrames */ +class SentryStackTraceFactory { + + /** + * convert an Array of Java StackTraceElements to a list of SentryStackFrames + * + * @param elements Array of Java StackTraceElements + * @return list of SentryStackFrames + */ + List getStackFrames(@Nullable final StackTraceElement[] elements) { + List sentryStackFrames = new ArrayList<>(); + + if (elements != null) { + for (StackTraceElement item : elements) { + SentryStackFrame sentryStackFrame = new SentryStackFrame(); + sentryStackFrame.setModule(item.getClassName()); + sentryStackFrame.setFunction(item.getMethodName()); + sentryStackFrame.setFilename(item.getFileName()); + sentryStackFrame.setLineno(item.getLineNumber()); + sentryStackFrame.setNative(item.isNativeMethod()); + + sentryStackFrames.add(sentryStackFrame); + } + } + + return sentryStackFrames; + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/SentryThreadFactory.java b/sentry-core/src/main/java/io/sentry/core/SentryThreadFactory.java index 5c79cc001..92bc95e91 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryThreadFactory.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryThreadFactory.java @@ -20,7 +20,7 @@ List getCurrentThreads() { return getCurrentThreads(null); } - private List getCurrentThreads(@Nullable Thread crashedThread) { + private List getCurrentThreads(@Nullable final Thread crashedThread) { Map threads = Thread.getAllStackTraces(); List result = new ArrayList<>(); @@ -33,10 +33,10 @@ private List getCurrentThreads(@Nullable Thread crashedThread) { } private SentryThread getSentryThread( - @Nullable Thread crashedThread, - Thread currentThread, - StackTraceElement[] stackFramesElements, - Thread thread) { + @Nullable final Thread crashedThread, + final Thread currentThread, + final StackTraceElement[] stackFramesElements, + final Thread thread) { SentryThread sentryThread = new SentryThread(); sentryThread.setName(thread.getName()); @@ -49,10 +49,8 @@ private SentryThread getSentryThread( } sentryThread.setCurrent(thread == currentThread); - List frames = new ArrayList<>(); - for (StackTraceElement element : stackFramesElements) { - frames.add(getSentryStackFrame(element)); - } + SentryStackTraceFactory sentryStackTraceFactory = new SentryStackTraceFactory(); + List frames = sentryStackTraceFactory.getStackFrames(stackFramesElements); if (frames.size() > 0) { sentryThread.setStacktrace(new SentryStackTrace(frames)); @@ -60,16 +58,4 @@ private SentryThread getSentryThread( return sentryThread; } - - private SentryStackFrame getSentryStackFrame(StackTraceElement element) { - SentryStackFrame sentryStackFrame = new SentryStackFrame(); - sentryStackFrame.setModule(element.getClassName()); - sentryStackFrame.setFilename(element.getFileName()); - if (element.getLineNumber() >= 0) { - sentryStackFrame.setLineno(element.getLineNumber()); - } - sentryStackFrame.setFunction(element.getMethodName()); - sentryStackFrame.setNative(element.isNativeMethod()); - return sentryStackFrame; - } } diff --git a/sentry-core/src/main/java/io/sentry/core/exception/ExceptionMechanismThrowable.java b/sentry-core/src/main/java/io/sentry/core/exception/ExceptionMechanismThrowable.java new file mode 100644 index 000000000..381edeb64 --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/exception/ExceptionMechanismThrowable.java @@ -0,0 +1,33 @@ +package io.sentry.core.exception; + +import io.sentry.core.protocol.Mechanism; + +/** + * A throwable decorator that holds an {@link io.sentry.core.protocol.Mechanism} related to the + * decorated {@link Throwable}. + */ +@SuppressWarnings("serial") +public final class ExceptionMechanismThrowable extends Throwable { + + private final Mechanism exceptionMechanism; + private final Throwable throwable; + + /** + * A {@link Throwable} that decorates another with a Sentry {@link Mechanism}. + * + * @param mechanism The {@link Mechanism}. + * @param throwable The {@link java.lang.Throwable}. + */ + public ExceptionMechanismThrowable(Mechanism mechanism, Throwable throwable) { + this.exceptionMechanism = mechanism; + this.throwable = throwable; + } + + public Mechanism getExceptionMechanism() { + return exceptionMechanism; + } + + public Throwable getThrowable() { + return throwable; + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackFrame.java b/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackFrame.java index 6417a8bed..dc48c5cc7 100644 --- a/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackFrame.java +++ b/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackFrame.java @@ -15,15 +15,15 @@ public class SentryStackFrame implements IUnknownPropertiesConsumer { private String module; private Integer lineno; private Integer colno; - private String absolutePath; + private String absPath; private String contextLine; private Boolean inApp; - private Boolean isNative; - private String _package; + private String _package; // TODO: _package as its a reserverd word + private Boolean _native; // TODO: _native as its a reserverd word private String platform; - private Long imageAddress; - private Long SymbolAddress; - private Long instructionOffset; + private Long imageAddr; + private Long symbolAddr; + private Long instructionAddr; private Map unknown; public List getPreContext() { @@ -98,12 +98,12 @@ public void setColno(Integer colno) { this.colno = colno; } - public String getAbsolutePath() { - return absolutePath; + public String getAbsPath() { + return absPath; } - public void setAbsolutePath(String absolutePath) { - this.absolutePath = absolutePath; + public void setAbsPath(String absPath) { + this.absPath = absPath; } public String getContextLine() { @@ -138,36 +138,36 @@ public void setPlatform(String platform) { this.platform = platform; } - public Long getImageAddress() { - return imageAddress; + public Long getImageAddr() { + return imageAddr; } - public void setImageAddress(Long imageAddress) { - this.imageAddress = imageAddress; + public void setImageAddr(Long imageAddr) { + this.imageAddr = imageAddr; } - public Long getSymbolAddress() { - return SymbolAddress; + public Long getSymbolAddr() { + return symbolAddr; } - public void setSymbolAddress(Long symbolAddress) { - SymbolAddress = symbolAddress; + public void setSymbolAddr(Long symbolAddr) { + this.symbolAddr = symbolAddr; } - public Long getInstructionOffset() { - return instructionOffset; + public Long getInstructionAddr() { + return instructionAddr; } - public void setInstructionOffset(Long instructionOffset) { - this.instructionOffset = instructionOffset; + public void setInstructionAddr(Long instructionAddr) { + this.instructionAddr = instructionAddr; } - public Boolean getNative() { - return isNative; + public Boolean isNative() { + return _native; } - public void setNative(Boolean isNative) { - this.isNative = isNative; + public void setNative(Boolean _native) { + this._native = _native; } @Override diff --git a/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackTrace.java b/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackTrace.java index 740330bcb..88f6b4b02 100644 --- a/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackTrace.java +++ b/sentry-core/src/main/java/io/sentry/core/protocol/SentryStackTrace.java @@ -7,6 +7,7 @@ /** The Sentry stacktrace. */ public class SentryStackTrace implements IUnknownPropertiesConsumer { private List frames; + // TODO registers is missing? private Map unknown; public SentryStackTrace() {} diff --git a/sentry-core/src/test/java/io/sentry/core/SentryExceptionFactoryTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryExceptionFactoryTest.kt new file mode 100644 index 000000000..c4a5c4b14 --- /dev/null +++ b/sentry-core/src/test/java/io/sentry/core/SentryExceptionFactoryTest.kt @@ -0,0 +1,65 @@ +package io.sentry.core + +import io.sentry.core.exception.ExceptionMechanismThrowable +import io.sentry.core.protocol.Mechanism +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SentryExceptionFactoryTest { + private val sut = SentryExceptionFactory(SentryStackTraceFactory()) + + @Test + fun `when getSentryExceptions is called passing an Exception, not empty result`() { + val exception = Exception("Exception") + assertTrue(sut.getSentryExceptions(exception).size > 0) + } + + @Test + fun `when getSentryExceptions is called passing null, empty result`() { + assertEquals(0, sut.getSentryExceptions(null).size) + } + + @Test + fun `when getSentryExceptions is called passing an Exception, it should set its fields`() { + val exception = Exception("Exception") + val sentryExceptions = sut.getSentryExceptions(exception) + assertEquals("Exception", sentryExceptions[0].type) + assertEquals("Exception", sentryExceptions[0].value) + assertEquals("java.lang", sentryExceptions[0].module) + assertTrue(sentryExceptions[0].stacktrace.frames.size > 0) + } + + @Test + fun `when getSentryExceptions is called passing a ExceptionMechanism, it should set its fields`() { + val mechanism = Mechanism() + mechanism.type = "anr" + mechanism.handled = false + + val error = Exception("Exception") + + val throwable = ExceptionMechanismThrowable(mechanism, error) + + val sentryExceptions = sut.getSentryExceptions(throwable) + assertEquals("anr", sentryExceptions[0].mechanism.type) + assertEquals(false, sentryExceptions[0].mechanism.handled) + } + + @Test + fun `when exception has a cause, ensure conversion queue keeps order`() { + val exception = Exception("message", Exception("cause")) + val queue = sut.extractExceptionQueue(exception) + + assertEquals("message", queue.first.value) + assertEquals("cause", queue.last.value) + } + + @Test + fun `when getSentryExceptions is called passing an Inner exception, not empty result`() { + val exception = InnerClassThrowable(InnerClassThrowable()) + val queue = sut.extractExceptionQueue(exception) + assertEquals("SentryExceptionFactoryTest\$InnerClassThrowable", queue.first.type) + } + + private inner class InnerClassThrowable constructor(cause: Throwable? = null) : Throwable(cause) +} diff --git a/sentry-core/src/test/java/io/sentry/core/SentryStackTraceFactoryTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryStackTraceFactoryTest.kt new file mode 100644 index 000000000..c6e0e5f98 --- /dev/null +++ b/sentry-core/src/test/java/io/sentry/core/SentryStackTraceFactoryTest.kt @@ -0,0 +1,32 @@ +package io.sentry.core + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SentryStackTraceFactoryTest { + private val sut = SentryStackTraceFactory() + + @Test + fun `when getStackFrames is called passing a valid Array, not empty result`() { + val stacktraces = Thread.currentThread().stackTrace + val count = stacktraces.size + assertEquals(count, sut.getStackFrames(stacktraces).count()) + } + + @Test + fun `when getStackFrames is called passing null, empty result`() { + assertEquals(0, sut.getStackFrames(null).count()) + } + + @Test + fun `when getStackFrames is called passing a valid array, fields should be set`() { + val element = StackTraceElement("class", "method", "fileName", -2) + val stacktraces = Array(1) { element } + val stackFrames = sut.getStackFrames(stacktraces) + assertEquals("class", stackFrames[0].module) + assertEquals("method", stackFrames[0].function) + assertEquals("fileName", stackFrames[0].filename) + assertEquals(-2, stackFrames[0].lineno) + assertEquals(true, stackFrames[0].isNative) + } +}