Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.util.SampleRateUtils;
import java.util.function.Supplier;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -261,6 +262,15 @@ public interface BeforeCaptureCallback {

private @Nullable Double anrProfilingSampleRate;

/**
* Optional provider for the stack trace used during ANR profiling. When set, the integration
* calls this supplier instead of {@link Thread#getStackTrace()} on the main thread. This lets
* hybrid SDKs (e.g. Flutter, React Native) supply a combined or native-enriched stack trace.
*
* <p>Defaults to {@code null}, which falls back to {@code mainThread.getStackTrace()}.
*/
private @Nullable Supplier<StackTraceElement[]> anrStackTraceProvider;

private boolean enableAnrFingerprinting = true;

public SentryAndroidOptions() {
Expand Down Expand Up @@ -732,6 +742,28 @@ public boolean isAnrProfilingEnabled() {
return anrProfilingSampleRate != null && anrProfilingSampleRate > 0;
}

/**
* Returns the custom stack trace provider used during ANR profiling, or {@code null} if the
* default {@link Thread#getStackTrace()} behaviour should be used.
*/
public @Nullable Supplier<StackTraceElement[]> getAnrStackTraceProvider() {
return anrStackTraceProvider;
}

/**
* Sets a custom stack trace provider used during ANR profiling. When non-null the integration
* calls this supplier instead of {@link Thread#getStackTrace()} on the main thread. Hybrid SDKs
* can use this to expose Dart / JS / native frames alongside JVM frames.
*
* <p>Pass {@code null} (the default) to restore the built-in behaviour.
*
* @param anrStackTraceProvider supplier that returns the current stack trace, or {@code null}
*/
public void setAnrStackTraceProvider(
final @Nullable Supplier<StackTraceElement[]> anrStackTraceProvider) {
this.anrStackTraceProvider = anrStackTraceProvider;
}

/**
* Returns whether ANR fingerprinting is enabled. When enabled, the SDK assigns static
* fingerprints to ANR events that would otherwise produce noisy grouping. Currently, this applies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -235,8 +236,13 @@ protected void checkMainThread(final @NotNull Thread mainThread) throws IOExcept
|| mainThreadState == MainThreadState.ANR_DETECTED)) {
if (numCollectedStacks.get() < MAX_NUM_STACKS) {
final long start = SystemClock.uptimeMillis();
final @Nullable SentryAndroidOptions opts = options;
final @Nullable Supplier<StackTraceElement[]> provider =
opts != null ? opts.getAnrStackTraceProvider() : null;
final @NotNull StackTraceElement[] stackTrace =
provider != null ? provider.get() : mainThread.getStackTrace();
final @NotNull AnrStackTrace trace =
new AnrStackTrace(System.currentTimeMillis(), mainThread.getStackTrace());
new AnrStackTrace(System.currentTimeMillis(), stackTrace);
final long duration = SystemClock.uptimeMillis() - start;
if (logger.isEnabled(SentryLevel.DEBUG)) {
logger.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package io.sentry.android.core
import io.sentry.ITransactionProfiler
import io.sentry.NoOpTransactionProfiler
import io.sentry.protocol.DebugImage
import java.util.function.Supplier
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
import org.mockito.kotlin.mock

Expand Down Expand Up @@ -233,6 +235,28 @@ class SentryAndroidOptionsTest {
sentryOptions.anrProfilingSampleRate = 2.0
}

@Test
fun `anrStackTraceProvider is null by default`() {
val sentryOptions = SentryAndroidOptions()
assertNull(sentryOptions.anrStackTraceProvider)
}

@Test
fun `anrStackTraceProvider can be set and retrieved`() {
val sentryOptions = SentryAndroidOptions()
val provider = Supplier<Array<StackTraceElement>> { emptyArray() }
sentryOptions.anrStackTraceProvider = provider
assertSame(provider, sentryOptions.anrStackTraceProvider)
}

@Test
fun `anrStackTraceProvider can be cleared to null`() {
val sentryOptions = SentryAndroidOptions()
sentryOptions.anrStackTraceProvider = Supplier<Array<StackTraceElement>> { emptyArray() }
sentryOptions.anrStackTraceProvider = null
assertNull(sentryOptions.anrStackTraceProvider)
}

private class CustomDebugImagesLoader : IDebugImagesLoader {
override fun loadDebugImages(): List<DebugImage>? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.sentry.SentryOptions
import io.sentry.android.core.AppState
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.test.getProperty
import java.util.function.Supplier
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -310,4 +311,76 @@ class AnrProfilingIntegrationTest {

integration.close()
}

@Test
fun `custom anrStackTraceProvider is used when set`() {
val mainThread = Thread.currentThread()
SystemClock.setCurrentTimeMillis(1_000)

val customFrames =
arrayOf(
StackTraceElement("com.example.Dart", "dartMain", "main.dart", 42),
StackTraceElement("com.example.Flutter", "runApp", "app.dart", 10),
)
val providerCallCount = java.util.concurrent.atomic.AtomicInteger(0)
val customProvider =
Supplier<Array<StackTraceElement>> {
providerCallCount.incrementAndGet()
customFrames
}

val androidOptions =
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
anrProfilingSampleRate = 1.0
anrStackTraceProvider = customProvider
}

val integration = AnrProfilingIntegration()
integration.register(mockScopes, androidOptions)

// Advance time into the suspicious window and trigger a stack capture
SystemClock.setCurrentTimeMillis(3_000)
integration.checkMainThread(mainThread)

// One stack should have been collected using the custom provider
assertEquals(1, integration.numCollectedStacks.get())
assertTrue(providerCallCount.get() > 0, "Custom provider should have been called")

val stacks = integration.profileManager.load().stacks
assertEquals(1, stacks.size)
val capturedFrames = stacks[0].stack
assertEquals("com.example.Dart", capturedFrames[0].className)
assertEquals("dartMain", capturedFrames[0].methodName)
}

@Test
fun `null anrStackTraceProvider falls back to mainThread getStackTrace`() {
val mainThread = Thread.currentThread()
SystemClock.setCurrentTimeMillis(1_000)

val androidOptions =
SentryAndroidOptions().apply {
cacheDirPath = tmpDir.root.absolutePath
setLogger(mockLogger)
anrProfilingSampleRate = 1.0
// anrStackTraceProvider left as null (default)
}

assertEquals(null, androidOptions.anrStackTraceProvider)

val integration = AnrProfilingIntegration()
integration.register(mockScopes, androidOptions)

SystemClock.setCurrentTimeMillis(3_000)
integration.checkMainThread(mainThread)

// Should still have collected one stack via the default path
assertEquals(1, integration.numCollectedStacks.get())
val stacks = integration.profileManager.load().stacks
assertEquals(1, stacks.size)
// Frames should be real JVM frames (non-empty)
assertTrue(stacks[0].stack.isNotEmpty())
}
}