Skip to content

feat(anr): Profile main thread when ANR and report ANR profiles to Sentry#4899

Open
markushi wants to merge 39 commits intomainfrom
markushi/feat/anr-profiling
Open

feat(anr): Profile main thread when ANR and report ANR profiles to Sentry#4899
markushi wants to merge 39 commits intomainfrom
markushi/feat/anr-profiling

Conversation

@markushi
Copy link
Member

@markushi markushi commented Nov 12, 2025

📜 Description

Adds ANR (Application Not Responding) profiling integration that profiles the main thread when an ANR is detected and reports the captured profiles to Sentry.

Key Changes:

  • New AnrProfilingIntegration to capture profiles during ANR events
  • AnrV2Integration now takes care of matching and capturing the profile on the next start
  • If the captured ANR event only contains system frames, a static fingerprint will get set, effectively changing the grouping behavior to group all noisy ANRs into a single issue

💡 Motivation and Context

This feature enables better ANR diagnostics by capturing profiling data at the time of ANR detection, allowing developers to identify performance bottlenecks and problematic code paths causing application hangs.

Example event: https://sentry-sdks.sentry.io/issues/7229210096/events/4598ff6fcc0f402d8ecca615005e7f64/

💚 How did you test it?

  • Added tests

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

@github-actions
Copy link
Contributor

github-actions bot commented Nov 12, 2025

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 8a49e18

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Nov 12, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 310.80 ms 367.02 ms 56.22 ms
Size 1.58 MiB 2.29 MiB 726.95 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
b6cfb57 372.92 ms 507.77 ms 134.85 ms
ce0a49e 532.00 ms 609.96 ms 77.96 ms
e59e22a 374.68 ms 442.14 ms 67.46 ms
fc5ccaf 256.80 ms 322.36 ms 65.56 ms
d15471f 294.13 ms 399.49 ms 105.36 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
abfcc92 337.38 ms 427.39 ms 90.00 ms
ee35ac3 346.83 ms 435.48 ms 88.65 ms
d15471f 322.58 ms 396.08 ms 73.50 ms
f634d01 359.58 ms 433.88 ms 74.30 ms

App size

Revision Plain With Sentry Diff
b6cfb57 1.58 MiB 2.28 MiB 718.80 KiB
ce0a49e 1.58 MiB 2.10 MiB 532.94 KiB
e59e22a 1.58 MiB 2.20 MiB 635.34 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
abfcc92 1.58 MiB 2.13 MiB 557.31 KiB
ee35ac3 1.58 MiB 2.13 MiB 558.77 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
f634d01 1.58 MiB 2.10 MiB 533.40 KiB

Previous results on branch: markushi/feat/anr-profiling

Startup times

Revision Plain With Sentry Diff
fca8df8 326.79 ms 379.69 ms 52.90 ms
00299fd 359.87 ms 424.85 ms 64.98 ms
2cee1ab 318.29 ms 361.00 ms 42.71 ms
eb02e45 362.67 ms 431.71 ms 69.04 ms
c10e603 367.92 ms 393.50 ms 25.58 ms
4c0ffee 314.94 ms 377.79 ms 62.86 ms
fa76e86 274.32 ms 349.63 ms 75.31 ms
8c4e7d6 309.15 ms 359.27 ms 50.12 ms
eb7143a 347.66 ms 408.54 ms 60.88 ms
ddbbe91 289.51 ms 359.74 ms 70.23 ms

App size

Revision Plain With Sentry Diff
fca8df8 1.58 MiB 2.29 MiB 723.68 KiB
00299fd 1.58 MiB 2.29 MiB 723.50 KiB
2cee1ab 1.58 MiB 2.29 MiB 723.68 KiB
eb02e45 1.58 MiB 2.29 MiB 725.26 KiB
c10e603 1.58 MiB 2.29 MiB 723.72 KiB
4c0ffee 1.58 MiB 2.29 MiB 723.67 KiB
fa76e86 1.58 MiB 2.29 MiB 724.06 KiB
8c4e7d6 1.58 MiB 2.29 MiB 727.03 KiB
eb7143a 1.58 MiB 2.29 MiB 724.12 KiB
ddbbe91 1.58 MiB 2.29 MiB 724.15 KiB

@markushi markushi marked this pull request as draft December 3, 2025 07:19
@markushi
Copy link
Member Author

@sentry review

@markushi markushi marked this pull request as ready for review December 17, 2025 09:51
Copy link
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

Great work already! I believe there are some things to address but I can check once more after that

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

value.serialize(dos);
dos.flush();
}
sink.flush();
Copy link

Choose a reason for hiding this comment

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

toStream closes caller-owned stream then flushes it

Low Severity

The toStream converter wraps sink in a DataOutputStream try-with-resources, which closes both dos and the underlying sink when the block exits. The sink.flush() call afterward operates on an already-closed stream. This works only because the actual sink from FileObjectQueue is a ByteArrayOutputStream where close() is a no-op, but the pattern violates stream ownership conventions and the post-close flush is dead code.

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.

}
}
return true;
}
Copy link

Choose a reason for hiding this comment

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

hasOnlySystemFrames returns true with no frames present

Medium Severity

hasOnlySystemFrames returns true when exceptions have no stacktrace or empty frames, not just when all frames are system frames. When the main thread is absent from the ANR thread dump, setAnrExceptions creates a dummy exception with a null stacktrace. The loop in hasOnlySystemFrames skips exceptions with null stacktraces and never finds any non-system frame, so it returns true. This causes setDefaultAnrFingerprint to apply the "system-frames-only-anr" fingerprint, grouping these no-information ANRs with ANRs that genuinely contain only system frames — two different categories that likely warrant separate investigation.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.

@markushi markushi requested a review from romtsn February 26, 2026 20:02
Comment on lines +874 to +877
final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
final @NotNull StackTraceElement[] stack = culprit.getStack();

if (stack.length > 0) {
Copy link

Choose a reason for hiding this comment

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

Bug: The captureAnrProfile method returns the locally generated SentryId instead of the one returned from Sentry.getCurrentScopes().captureProfileChunk(), which could be different.
Severity: MEDIUM

Suggested Fix

The captureAnrProfile method should return the SentryId that is returned by the Sentry.getCurrentScopes().captureProfileChunk(chunk) call. If the capture fails and returns Sentry.EMPTY_ID, the method should return null as it currently does.

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/ApplicationExitInfoEventProcessor.java#L874-L877

Potential issue: The `captureAnrProfile` method creates a `ProfileChunk` with a new,
locally generated `SentryId`. It then calls
`Sentry.getCurrentScopes().captureProfileChunk(chunk)`, which captures the profile and
returns a `SentryId`. However, the method ignores this returned ID and instead returns
the original, locally generated ID from `chunk.getProfilerId()`. This can lead to a
mismatch between the `profiler_id` attached to the ANR event and the actual ID of the
profile chunk stored by Sentry, making it impossible to link them.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

return Collections.max(
stackTraceMap.values(),
(c1, c2) ->
Float.compare(c1.count * c1.quality * c1.depth, c2.count * c2.quality * c2.depth));
Copy link

Choose a reason for hiding this comment

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

Scoring formula collapses to zero for system-only stacks

Medium Severity

The scoring formula count * quality * depth evaluates to zero for every candidate when all stack frames are system frames (quality = 0). This causes Collections.max to return an arbitrary sub-stack from the HashMap values — potentially a shallow 1-frame sub-stack rather than the full, most informative stack trace. The comment says "the deepest stacktrace with most count wins," but with quality = 0, count and depth are effectively ignored.

Fix in Cursor Fix in Web

}
}
return true;
}
Copy link

Choose a reason for hiding this comment

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

hasOnlySystemFrames returns true when frames are absent

Low Severity

hasOnlySystemFrames returns true when exceptions exist but have null stacktraces or empty frame lists, since the loop completes without finding any non-system frame. This causes the "system-frames-only-anr" fingerprint to be applied to events that have no stack information at all — a semantically different case from events whose stacks contain only system frames.

Fix in Cursor Fix in Web

Copy link
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

some bugbot comments still left, but to me this looks great, hats off!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants