Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
58 changes: 58 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isCollectAdditionalContext ()Z
public fun isEnableActivityLifecycleBreadcrumbs ()Z
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
public fun isEnableAnrProfiling ()Z
public fun isEnableAppComponentBreadcrumbs ()Z
public fun isEnableAppLifecycleBreadcrumbs ()Z
public fun isEnableAutoActivityLifecycleTracing ()Z
Expand All @@ -351,6 +352,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
public fun setEnableAnrProfiling (Z)V
public fun setEnableAppComponentBreadcrumbs (Z)V
public fun setEnableAppLifecycleBreadcrumbs (Z)V
public fun setEnableAutoActivityLifecycleTracing (Z)V
Expand Down Expand Up @@ -480,6 +482,62 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
}

public class io/sentry/android/core/anr/AggregatedStackTrace {
public fun <init> ([Ljava/lang/StackTraceElement;IIJI)V
public fun add (J)V
public fun getStack ()[Ljava/lang/StackTraceElement;
}

public class io/sentry/android/core/anr/AnrCulpritIdentifier {
public fun <init> ()V
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
}

public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
}

public class io/sentry/android/core/anr/AnrProfile {
public final field endtimeMs J
public final field stacks Ljava/util/List;
public final field startTimeMs J
public fun <init> (Ljava/util/List;)V
}

public class io/sentry/android/core/anr/AnrProfileManager {
public fun <init> (Lio/sentry/SentryOptions;)V
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
public fun clear ()V
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
}

public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
public static final field POLLING_INTERVAL_MS J
public static final field THRESHOLD_ANR_MS J
public fun <init> ()V
public fun close ()V
public fun onBackground ()V
public fun onForeground ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
public fun run ()V
}

public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
public final field stack [Ljava/lang/StackTraceElement;
public final field timestampMs J
public fun <init> (J[Ljava/lang/StackTraceElement;)V
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
public synthetic fun compareTo (Ljava/lang/Object;)I
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
public fun serialize (Ljava/io/DataOutputStream;)V
}

public final class io/sentry/android/core/anr/StackTraceConverter {
public fun <init> ()V
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
}

public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
public static final field LAST_ANR_REPORT Ljava/lang/String;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.SentryOpenTelemetryMode;
import io.sentry.android.core.anr.AnrProfilingIntegration;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
Expand Down Expand Up @@ -391,6 +392,10 @@ static void installDefaultIntegrations(
// it to set the replayId in case of an ANR
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));

if (options.isEnableAnrProfiling()) {
options.addIntegration(new AnrProfilingIntegration());
}

// registerActivityLifecycleCallbacks is only available if Context is an AppContext
if (context instanceof Application) {
options.addIntegration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@
import io.sentry.ILogger;
import io.sentry.IScopes;
import io.sentry.Integration;
import io.sentry.ProfileChunk;
import io.sentry.ProfileContext;
import io.sentry.SentryEvent;
import io.sentry.SentryExceptionFactory;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SentryStackTraceFactory;
import io.sentry.android.core.anr.AggregatedStackTrace;
import io.sentry.android.core.anr.AnrCulpritIdentifier;
import io.sentry.android.core.anr.AnrException;
import io.sentry.android.core.anr.AnrProfile;
import io.sentry.android.core.anr.AnrProfileManager;
import io.sentry.android.core.anr.StackTraceConverter;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.threaddump.Lines;
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
Expand All @@ -28,6 +38,7 @@
import io.sentry.protocol.Message;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryThread;
import io.sentry.protocol.profiling.SentryProfile;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.ICurrentDateProvider;
import io.sentry.util.HintUtils;
Expand All @@ -41,6 +52,7 @@
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
Expand Down Expand Up @@ -284,6 +296,8 @@ private void reportAsSentryEvent(
}
}

applyAnrProfile(isBackground, anrTimestamp, event);

final @NotNull SentryId sentryId = scopes.captureEvent(event, hint);
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
if (!isEventDropped) {
Expand All @@ -299,6 +313,67 @@ private void reportAsSentryEvent(
}
}

private void applyAnrProfile(
final boolean isBackground, final long anrTimestamp, final @NotNull SentryEvent event) {

// as of now AnrProfilingIntegration only generates profiles in foreground
if (isBackground) {
return;
}

@Nullable AnrProfile anrProfile = null;
try {
final AnrProfileManager provider = new AnrProfileManager(options);
anrProfile = provider.load();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: AnrProfilingIntegration and AnrV2Integration concurrently access the same QueueFile without synchronization.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The AnrProfilingIntegration (writer) and AnrV2Integration (reader) concurrently access the same underlying QueueFile at options.getCacheDirPath() + "anr_profile" without any synchronization. This violates the explicit contract of QueueFile which states, "Only one instance should access a given file at a time." Concurrent unsynchronized access can lead to data corruption, lost stack traces, or file format inconsistencies due to conflicting writes and reads to the shared file header and data structures.

💡 Suggested Fix

Implement a synchronization mechanism (e.g., file-level locking) to ensure exclusive access to the QueueFile, or refactor to use a single QueueFile instance managed by a central component.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java#L326-L327

Potential issue: The `AnrProfilingIntegration` (writer) and `AnrV2Integration` (reader)
concurrently access the same underlying `QueueFile` at `options.getCacheDirPath() +
"anr_profile"` without any synchronization. This violates the explicit contract of
`QueueFile` which states, "Only one instance should access a given file at a time."
Concurrent unsynchronized access can lead to data corruption, lost stack traces, or file
format inconsistencies due to conflicting writes and reads to the shared file header and
data structures.

Did we get this right? 👍 / 👎 to inform future reviews.

Reference_id: 2623394

} catch (Throwable t) {
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Concurrent File Access Causes Data Corruption

Creating a new AnrProfileManager instance in applyAnrProfile while AnrProfilingIntegration may still be running creates a race condition. Both instances open file handles to the same anr_profile file, potentially corrupting data as AnrProfilingIntegration writes while AnrV2Integration reads and closes the queue file, leading to concurrent access issues with the underlying QueueFile.

Fix in Cursor Fix in Web


if (anrProfile != null) {
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
// TODO maybe be less strict around the end timestamp
if (anrTimestamp >= anrProfile.startTimeMs && anrTimestamp <= anrProfile.endtimeMs) {
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
final ProfileChunk chunk =
new ProfileChunk(
new SentryId(),
new SentryId(),
null,
new HashMap<>(0),
anrTimestamp / 1000.0d,
ProfileChunk.PLATFORM_JAVA,
options);
chunk.setSentryProfile(profile);

options.getLogger().log(SentryLevel.DEBUG, "");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Silence the Empty Debug Logs

Empty debug log statement with no message at options.getLogger().log(SentryLevel.DEBUG, "") appears to be accidentally committed debugging code that provides no diagnostic value and should be removed or given a meaningful message.

Fix in Cursor Fix in Web

scopes.captureProfileChunk(chunk);

final @Nullable AggregatedStackTrace culprit =
AnrCulpritIdentifier.identify(anrProfile.stacks);
if (culprit != null) {
// TODO if quality is low (e.g. when culprit is pollNative())
// consider throwing the ANR using a static fingerprint to reduce noise
final @NotNull StackTraceElement[] stack = culprit.getStack();
if (stack.length > 0) {
final StackTraceElement stackTraceElement = culprit.getStack()[0];
final String message =
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
final AnrException exception = new AnrException(message);
exception.setStackTrace(stack);

// TODO should this be re-used from somewhere else?
final SentryExceptionFactory factory =
new SentryExceptionFactory(new SentryStackTraceFactory(options));
event.setExceptions(factory.getSentryExceptions(exception));
event.getContexts().setProfile(new ProfileContext(chunk.getProfilerId()));
}
}
} else {
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
}
}
}

private @NotNull ParseResult parseThreadDump(
final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
final byte[] dump;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ final class ManifestMetadataReader {

static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";

static final String ENABLE_ANR_PROFILING = "io.sentry.anr.enable-profiling";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -522,6 +524,9 @@ static void applyMetadata(
metadata, logger, FEEDBACK_USE_SENTRY_USER, feedbackOptions.isUseSentryUser()));
feedbackOptions.setShowBranding(
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));

options.setEnableAnrProfiling(
readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
}
options
.getLogger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ public interface BeforeCaptureCallback {

private @Nullable SentryFrameMetricsCollector frameMetricsCollector;

private boolean enableAnrProfiling = false;

public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down Expand Up @@ -626,6 +628,14 @@ public void setEnableSystemEventBreadcrumbsExtras(
this.enableSystemEventBreadcrumbsExtras = enableSystemEventBreadcrumbsExtras;
}

public boolean isEnableAnrProfiling() {
return enableAnrProfiling;
}

public void setEnableAnrProfiling(final boolean enableAnrProfiling) {
this.enableAnrProfiling = enableAnrProfiling;
}

static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
@Override
public void showDialog(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.sentry.android.core.anr;

import java.util.Arrays;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class AggregatedStackTrace {
// the number of frames of the stacktrace
final int depth;

// the quality of the stack trace, higher means better
final int quality;

private final StackTraceElement[] stack;

// 0 is the most detailed frame in the stacktrace
private final int stackStartIdx;
private final int stackEndIdx;

// the total number of times this exact stacktrace was captured
int count;

// first time the stacktrace occured
private long startTimeMs;

// last time the stacktrace occured
private long endTimeMs;

public AggregatedStackTrace(
final StackTraceElement[] stack,
final int stackStartIdx,
final int stackEndIdx,
final long timestampMs,
final int quality) {
this.stack = stack;
this.stackStartIdx = stackStartIdx;
this.stackEndIdx = stackEndIdx;
this.depth = stackEndIdx - stackStartIdx;
this.startTimeMs = timestampMs;
this.endTimeMs = timestampMs;
this.count = 1;
this.quality = quality;
}

public void add(long timestampMs) {
this.startTimeMs = Math.min(startTimeMs, timestampMs);
this.endTimeMs = Math.max(endTimeMs, timestampMs);
this.count++;
}

public StackTraceElement[] getStack() {
return Arrays.copyOfRange(stack, stackStartIdx, stackEndIdx + 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.sentry.android.core.anr;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public class AnrCulpritIdentifier {

// common Java and Android packages who are less relevant for being the actual culprit
private static final List<String> lowQualityPackages = new ArrayList<>(9);

{
lowQualityPackages.add("java.lang");
lowQualityPackages.add("java.util");
lowQualityPackages.add("android.app");
lowQualityPackages.add("android.os.Handler");
lowQualityPackages.add("android.os.Looper");
lowQualityPackages.add("android.view");
lowQualityPackages.add("android.widget");
lowQualityPackages.add("com.android.internal");
lowQualityPackages.add("com.google.android");
}

/**
* @param dumps
* @return
*/
@Nullable
public static AggregatedStackTrace identify(final @NotNull List<AnrStackTrace> dumps) {
if (dumps.isEmpty()) {
return null;
}

// fold all stacktraces and count their occurrences
final Map<Integer, AggregatedStackTrace> stackTraceMap = new HashMap<>();
for (final AnrStackTrace dump : dumps) {

// entry 0 is the most detailed element in the stacktrace
// so create sub-stacks (1..n, 2..n, ...) to capture the most common root cause of an ANR
for (int i = 0; i < dump.stack.length - 1; i++) {
final int key = subArrayHashCode(dump.stack, i, dump.stack.length - 1);
int quality = 10;
final String clazz = dump.stack[i].getClassName();
for (String ignoredPackage : lowQualityPackages) {
if (clazz.startsWith(ignoredPackage)) {
quality = 1;
break;
}
}

@Nullable AggregatedStackTrace aggregatedStackTrace = stackTraceMap.get(key);
if (aggregatedStackTrace == null) {
aggregatedStackTrace =
new AggregatedStackTrace(
dump.stack, i, dump.stack.length - 1, dump.timestampMs, quality);
stackTraceMap.put(key, aggregatedStackTrace);
} else {
aggregatedStackTrace.add(dump.timestampMs);
}
}
}

// the deepest stacktrace with most count wins
return Collections.max(
stackTraceMap.values(),
(c1, c2) -> {
final int countComparison = Integer.compare(c1.count * c1.quality, c2.count * c2.quality);
if (countComparison == 0) {
return Integer.compare(c1.depth, c2.depth);
}
return countComparison;
});
}

private static int subArrayHashCode(
final @NotNull Object[] arr, final int stackStartIdx, final int stackEndIdx) {
int result = 1;
for (int i = stackStartIdx; i <= stackEndIdx; i++) {
final Object item = arr[i];
result = 31 * result + item.hashCode();
}
return result;
}
}
Loading
Loading