Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# vNext

* Fix: Mark stacktrace as snapshot if captured at arbitrary moment #1231
* Enchancement: Improve EventProcessor nullability annotations (#1229).

# 4.1.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,6 @@ public interface ANRListener {
*
* @param error The error describing the ANR.
*/
void onAppNotResponding(ApplicationNotResponding error);
void onAppNotResponding(@NotNull ApplicationNotResponding error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ void reportANR(
final @NotNull ApplicationNotResponding error) {
logger.log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage());

Mechanism mechanism = new Mechanism();
final Mechanism mechanism = new Mechanism();
mechanism.setType("ANR");
ExceptionMechanismException throwable =
new ExceptionMechanismException(mechanism, error, error.getThread());
final ExceptionMechanismException throwable =
new ExceptionMechanismException(mechanism, error, error.getThread(), true);

hub.captureException(throwable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.sentry.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Error thrown by ANRWatchDog when an ANR is detected. Contains the stack trace of the frozen UI
Expand All @@ -16,15 +17,15 @@
final class ApplicationNotResponding extends RuntimeException {
private static final long serialVersionUID = 252541144579117016L;

private final Thread thread;
private final @NotNull Thread thread;

ApplicationNotResponding(@NotNull String message, @NotNull Thread thread) {
ApplicationNotResponding(final @Nullable String message, final @NotNull Thread thread) {
super(message);
this.thread = Objects.requireNonNull(thread, "Thread must be provided.");
setStackTrace(this.thread.getStackTrace());
}

public Thread getThread() {
public @NotNull Thread getThread() {
return thread;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.sentry.android.core;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

@TestOnly
interface IHandler {
void post(Runnable runnable);
void post(@NotNull Runnable runnable);

@NotNull
Thread getThread();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@

import android.os.Handler;
import android.os.Looper;
import org.jetbrains.annotations.NotNull;

final class MainLooperHandler implements IHandler {
private final Handler handler;
private final @NotNull Handler handler;

MainLooperHandler() {
handler = new Handler(Looper.getMainLooper());
this(Looper.getMainLooper());
}

MainLooperHandler(final @NotNull Looper looper) {
handler = new Handler(looper);
}

@Override
public void post(Runnable runnable) {
public void post(final @NotNull Runnable runnable) {
handler.post(runnable);
}

@Override
public Thread getThread() {
public @NotNull Thread getThread() {
return handler.getLooper().getThread();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.sentry.android.core

import android.content.Context
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.check
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import io.sentry.IHub
import io.sentry.exception.ExceptionMechanismException
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull
Expand All @@ -12,49 +15,80 @@ import kotlin.test.assertTrue

class AnrIntegrationTest {

private val integration = AnrIntegration(mock())
private class Fixture {
val context = mock<Context>()
val hub = mock<IHub>()
val options = SentryAndroidOptions()

fun getSut(): AnrIntegration {
return AnrIntegration(context)
}
}

private val fixture = Fixture()

@BeforeTest
fun `before each test`() {
val sut = fixture.getSut()
// watch dog is static and has shared state
integration.close()
sut.close()
}

@Test
fun `When ANR is enabled, ANR watch dog should be started`() {
val options = SentryAndroidOptions()
val hub = mock<IHub>()
integration.register(hub, options)
assertNotNull(integration.anrWatchDog)
assertTrue((integration.anrWatchDog as ANRWatchDog).isAlive)
val sut = fixture.getSut()

sut.register(fixture.hub, fixture.options)

assertNotNull(sut.anrWatchDog)
assertTrue((sut.anrWatchDog as ANRWatchDog).isAlive)
}

@Test
fun `When ANR is disabled, ANR should not be started`() {
val options = SentryAndroidOptions()
options.isAnrEnabled = false
val hub = mock<IHub>()
val integration = AnrIntegration(mock())
integration.register(hub, options)
assertNull(integration.anrWatchDog)
val sut = fixture.getSut()
fixture.options.isAnrEnabled = false

sut.register(fixture.hub, fixture.options)

assertNull(sut.anrWatchDog)
}

@Test
fun `When ANR watch dog is triggered, it should capture exception`() {
val hub = mock<IHub>()
val integration = AnrIntegration(mock())
integration.reportANR(hub, mock(), mock())
verify(hub).captureException(any())
val sut = fixture.getSut()

sut.reportANR(fixture.hub, mock(), getApplicationNotResponding())

verify(fixture.hub).captureException(any())
}

@Test
fun `When ANR integration is closed, watch dog should stop`() {
val options = SentryAndroidOptions()
val hub = mock<IHub>()
val integration = AnrIntegration(mock())
integration.register(hub, options)
assertNotNull(integration.anrWatchDog)
integration.close()
assertNull(integration.anrWatchDog)
val sut = fixture.getSut()

sut.register(fixture.hub, fixture.options)

assertNotNull(sut.anrWatchDog)

sut.close()

assertNull(sut.anrWatchDog)
}

@Test
fun `When ANR watch dog is triggered, snapshot flag should be true`() {
val sut = fixture.getSut()

sut.reportANR(fixture.hub, mock(), getApplicationNotResponding())

verify(fixture.hub).captureException(check {
val ex = it as ExceptionMechanismException
assertTrue(ex.isSnapshot)
})
}

private fun getApplicationNotResponding(): ApplicationNotResponding {
return ApplicationNotResponding("ApplicationNotResponding", Thread.currentThread())
}
}
6 changes: 5 additions & 1 deletion sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public SentryHandler() {
this(new SentryOptions(), true);
}

/** Creates an instance of SentryHandler. */
/**
* Creates an instance of SentryHandler.
*
* @param options the SentryOptions
*/
public SentryHandler(final @NotNull SentryOptions options) {
this(options, true);
}
Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1145,9 +1145,11 @@ public final class io/sentry/config/PropertiesProviderFactory {

public final class io/sentry/exception/ExceptionMechanismException : java/lang/RuntimeException {
public fun <init> (Lio/sentry/protocol/Mechanism;Ljava/lang/Throwable;Ljava/lang/Thread;)V
public fun <init> (Lio/sentry/protocol/Mechanism;Ljava/lang/Throwable;Ljava/lang/Thread;Z)V
public fun getExceptionMechanism ()Lio/sentry/protocol/Mechanism;
public fun getThread ()Ljava/lang/Thread;
public fun getThrowable ()Ljava/lang/Throwable;
public fun isSnapshot ()Z
}

public final class io/sentry/exception/InvalidDsnException : java/lang/RuntimeException {
Expand Down Expand Up @@ -1597,8 +1599,10 @@ public final class io/sentry/protocol/SentryStackTrace : io/sentry/IUnknownPrope
public fun acceptUnknownProperties (Ljava/util/Map;)V
public fun getFrames ()Ljava/util/List;
public fun getRegisters ()Ljava/util/Map;
public fun getSnapshot ()Ljava/lang/Boolean;
public fun setFrames (Ljava/util/List;)V
public fun setRegisters (Ljava/util/Map;)V
public fun setSnapshot (Ljava/lang/Boolean;)V
}

public final class io/sentry/protocol/SentryThread : io/sentry/IUnknownPropertiesConsumer {
Expand Down
15 changes: 12 additions & 3 deletions sentry/src/main/java/io/sentry/SentryExceptionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ List<SentryException> getSentryExceptions(final @NotNull Throwable throwable) {
* @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 java.lang.Thread}'s stacktrace is a snapshot.
*/
private @NotNull SentryException getSentryException(
@NotNull final Throwable throwable,
@Nullable final Mechanism exceptionMechanism,
@Nullable final Thread thread) {
@Nullable final Thread thread,
final boolean snapshot) {

final Package exceptionPackage = throwable.getClass().getPackage();
final String fullClassName = throwable.getClass().getName();
Expand All @@ -86,7 +88,11 @@ List<SentryException> getSentryExceptions(final @NotNull Throwable throwable) {
final List<SentryStackFrame> frames =
sentryStackTraceFactory.getStackFrames(throwable.getStackTrace());
if (frames != null && !frames.isEmpty()) {
exception.setStacktrace(new SentryStackTrace(frames));
final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames);
if (snapshot) {
sentryStackTrace.setSnapshot(true);
}
exception.setStacktrace(sentryStackTrace);
}

if (thread != null) {
Expand Down Expand Up @@ -120,19 +126,22 @@ Deque<SentryException> extractExceptionQueue(final @NotNull Throwable 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);
SentryException exception =
getSentryException(currentThrowable, exceptionMechanism, thread, snapshot);
exceptions.addFirst(exception);
currentThrowable = currentThrowable.getCause();
}
Expand Down
6 changes: 5 additions & 1 deletion sentry/src/main/java/io/sentry/SentryThreadFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ List<SentryThread> getCurrentThreads(
sentryStackTraceFactory.getStackFrames(stackFramesElements);

if (options.isAttachStacktrace() && frames != null && !frames.isEmpty()) {
sentryThread.setStacktrace(new SentryStackTrace(frames));
final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames);
// threads are always gotten either via Thread.currentThread() or Thread.getAllStackTraces()
sentryStackTrace.setSnapshot(true);

sentryThread.setStacktrace(sentryStackTrace);
}

return sentryThread;
Expand Down
Loading