Skip to content

feat: android ANR detection#13

Merged
bobbyg603 merged 15 commits intomainfrom
feat/anr-detection
Apr 16, 2026
Merged

feat: android ANR detection#13
bobbyg603 merged 15 commits intomainfrom
feat/anr-detection

Conversation

@bobbyg603
Copy link
Copy Markdown
Member

@bobbyg603 bobbyg603 commented Apr 15, 2026

Summary

  • ANR detection via ApplicationExitInfo API on Android 11+ (API 30+). On each SDK init, checks for unreported ANR exit reasons from previous sessions, reads the system thread dump, and uploads it to BugSplat. Thread dumps include full Java + native stacks with BuildIds for symbolication against .sym files.
  • 3-part S3 upload (ReportUploader) replacing direct multipart POSTs: getCrashUploadUrl → PUT to presigned S3 URL → commitS3CrashUpload. Used by both AnrReporter (crashTypeId=37) and FeedbackClient (crashTypeId=36).
  • Unit tests — 21 tests covering ReportUploader (zip creation, MD5, HTTP flow with MockWebServer) and FeedbackClient (report formatting, field handling, crash type).
  • CI workflow — GitHub Actions runs unit tests on PRs to main.

Test plan

  • Verify ANR detection on Android 11+ device: trigger ANR, relaunch app, confirm upload
  • Verify feedback upload still works via example app
  • Verify CI workflow runs tests on this PR
  • Confirm ./gradlew :app:testDebugUnitTest passes locally

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 15, 2026 01:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Android ANR detection/reporting (API 30+) and migrates crash/feedback uploads to a 3-step presigned S3 flow, along with new unit tests and a CI workflow to run them on PRs.

Changes:

  • Introduce AnrReporter to detect historical ANR exits and upload the OS-provided trace dump on next SDK init.
  • Add ReportUploader implementing the 3-part S3 upload flow and refactor FeedbackClient to use it.
  • Add MockWebServer-based unit tests and a GitHub Actions workflow to run :app:testDebugUnitTest.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Adds MockWebServer version for unit test HTTP flow.
app/build.gradle Enables unit test defaults and adds test dependencies (mockwebserver, org.json).
app/src/main/java/com/bugsplat/android/ReportUploader.java Implements 3-step presigned S3 upload + supporting utilities.
app/src/main/java/com/bugsplat/android/FeedbackClient.java Switches feedback posting to use ReportUploader and sets crash type metadata.
app/src/main/java/com/bugsplat/android/AnrReporter.java Adds ANR detection via ApplicationExitInfo and uploads trace dumps.
app/src/main/java/com/bugsplat/android/BugSplatBridge.java Triggers ANR check/report during SDK initialization.
app/src/test/java/com/bugsplat/android/ReportUploaderTest.java Adds unit/integration-style tests for zipping, md5, and 3-step HTTP flow.
app/src/test/java/com/bugsplat/android/FeedbackClientTest.java Adds tests for feedback report formatting and crash type metadata.
example/src/main/res/layout/activity_main.xml Adds an “ANR” trigger button to the example UI.
example/src/main/java/com/bugsplat/example/MainActivity.java Adds click handler to intentionally block main thread to trigger ANR.
.github/workflows/tests.yml Adds PR CI job to run Android unit tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java Outdated
Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java
Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java
Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java
Comment thread app/src/main/java/com/bugsplat/android/FeedbackClient.java
Comment thread app/src/main/java/com/bugsplat/android/AnrReporter.java
Comment thread example/src/main/res/layout/activity_main.xml Outdated
Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java Outdated
Comment thread app/src/main/java/com/bugsplat/android/FeedbackClient.java Outdated
Comment thread app/src/test/java/com/bugsplat/android/ReportUploaderTest.java Outdated
bobbyg603 and others added 10 commits April 14, 2026 21:28
Adds BugSplat.hang() backed by a native infinite loop (jniHang) so the
ANR thread dump includes a symbolicated C++ frame, enabling end-to-end
testing of the symbolication pipeline against .sym files.

Updates the example app's Trigger ANR button to call BugSplat.hang()
instead of a Java busy-loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the new ANR detection feature (Android 11+), including how it
works, configuration, testing with BugSplat.hang(), and the Android 11+
requirement. Also updates the example app feature list to mention the
ANR trigger button and custom attribute dialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Loads bugsplat.database, bugsplat.clientId, and bugsplat.clientSecret
from the gitignored local.properties instead of hardcoding them in
example/build.gradle. Fails the build with a clear error if
bugsplat.database is not configured.

Also switches bugsplatAppName and bugsplatAppVersion to be derived from
android.defaultConfig.applicationId and versionName respectively, so
there's a single source of truth for these values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The per-ABI uploadBugSplatSymbols{BuildType}{Abi} tasks didn't declare
a dependency on the merge${BuildType}NativeLibs task that produces
merged_native_libs/. If the upload task ran before packageDebug (e.g.
when passed alongside installDebug on the command line), the native
libs directory didn't exist yet and the upload was silently skipped.

Adding dependsOn merge${BuildType}NativeLibs ensures the native libs
are always in place before the upload runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AGP 8.6 changed the intermediates layout to include the producing task
name in the path:
  old: merged_native_libs/{buildType}/out/lib/{abi}/
  new: merged_native_libs/{buildType}/merge{BuildType}NativeLibs/out/lib/{abi}/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the shared uploadBugSplatSymbols task (which used
System.setProperty for buildType/abi, making it racy and Gradle-dedupe
unsafe) with a proper per-ABI approach:

- Extract upload logic into uploadSymbolsForAbi(buildType, abi) and
  resolveSymbolUploadExecutable() closures.
- Each per-ABI task has its own doLast body that calls the helper
  with explicit parameters — no more System.setProperty.
- AllAbis chains the per-ABI tasks with mustRunAfter so they run
  serially. The symbol-upload binary uses a shared temp dir, so
  parallel invocations aren't safe.
- Delete the legacy uploadBugSplatSymbols task entirely.

Fixes: AllAbis previously only uploaded one ABI due to Gradle
deduplicating the shared task dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the stale Gradle example that showed hardcoded credentials in
ext{} with the local.properties approach. Adds notes on:

- AGP 8.6+ intermediate path layout
- Serial vs parallel execution (symbol-upload's shared temp dir)
- ABI-filter-aware skipping for single-ABI builds (e.g. AS running on
  a single-ABI emulator)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI doesn't have a local.properties, so the top-level bugsplat.database
check was failing the build. Adds resolution order:
  local.properties → -Pbugsplat.database=... → BUGSPLAT_DATABASE env var

Both CI workflows (Tests, CodeQL) now pass BUGSPLAT_DATABASE=fred.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AnrReporter:
- Only advance last_reported_anr_timestamp when upload succeeds so
  transient failures don't permanently skip the affected ANRs.
- Process ANRs oldest-first (reverse the newest-first list from
  getHistoricalProcessExitReasons) and break on failure so newer
  ANRs are retried next launch without masking an older failure.
- Shut down the one-shot executor after submitting the check so the
  worker thread doesn't linger.

ReportUploader:
- Fix FileInputStream leak in upload(File, ...) with try-with-resources.
- URL-encode query string params in getCrashUploadUrl.
- Use UTF-8 explicitly in readBody() and all multipart byte writes.
- Accept any 2xx from S3 PUT (not just 200).
- Add upload(Map<String, byte[]>, crashType, id) overload for
  multi-entry zips, plus matching createZip(Map) helper.

FeedbackClient:
- Restore attachment upload — attachments are now packed as real zip
  entries alongside feedback.txt, not just logged as filenames in the
  text report. This fixes a behavioral regression from the previous
  multipart feedback POST.

Example app:
- Move the "Trigger ANR" button label into strings.xml for i18n.

Tests:
- Clean up dead code in TestableReportUploader trailing-slash handling.
- Update feedback attachment test to verify the new behavior (zip entry)
  rather than the old (name logged in feedback.txt).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bobbyg603 bobbyg603 changed the title feat: ANR detection and 3-part report upload feat: android ANR detection Apr 16, 2026
@bobbyg603 bobbyg603 requested a review from Copilot April 16, 2026 00:05
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

whoops what happened here?!?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right — that was from the feedback.txt era. Now resolved: feedback is emitted as feedback.json (verified against https://docs.bugsplat.com/introduction/development/web-services/user-feedback), with crashType=User.Feedback, crashTypeId=36, and the optional user / email / description / appKey / attributes fields carried on the commit request instead of baked into the body.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/src/main/java/com/bugsplat/android/FeedbackClient.java Outdated
Comment thread app/src/main/java/com/bugsplat/android/AnrReporter.java Outdated
Comment thread app/src/main/java/com/bugsplat/android/AnrReporter.java
Comment thread app/src/main/java/com/bugsplat/android/ReportUploader.java
bobbyg603 and others added 2 commits April 15, 2026 20:17
Conforms FeedbackClient and AnrReporter to the documented BugSplat
crash/feedback upload API:
https://docs.bugsplat.com/introduction/development/web-services/crash
https://docs.bugsplat.com/introduction/development/web-services/user-feedback

Changes:
- FeedbackClient now emits a JSON feedback.json body with just
  title and optional description (not a human-readable text report).
- crashType changed from "UserFeedback" / "Android ANR" to the dotted
  canonical forms "User.Feedback" / "Android.ANR".
- Optional commit fields (user, email, description, appKey) now live
  on the commitS3CrashUpload multipart form, not baked into the body.
- New `attributes` field support — pass a Map<String, String>, which
  gets JSON-encoded as the "attributes" commit field.
- Added BugSplat.postFeedback / postFeedbackBlocking overloads that
  accept attributes.
- Fixed commit field name casing: s3key → s3Key (matches docs).
- ReportUploader.upload gains an (entries, crashType, crashTypeId,
  extraCommitFields) overload so callers can pass the optional fields.

Tests updated:
- feedbackBodyIsJson_withTitleAndDescription parses the JSON body
- feedbackBody_omitsDescriptionWhenNullOrEmpty
- feedbackBody_handlesNullTitleAsEmptyString
- commitRequest_usesUserDotFeedbackCrashType
- commitRequest_includesOptionalUserEmailAppKey
- commitRequest_includesAttributesAsJsonString
- commitRequest_omitsOptionalFieldsWhenNullOrEmpty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a new 'Loading config from local.properties' section under Usage,
and updates all init/postFeedback examples to reference
BuildConfig.BUGSPLAT_DATABASE / APP_NAME / APP_VERSION instead of
hardcoding 'fred' / 'my-app' / '1.0.0'.

The same bugsplat.database value is reused by the symbol upload task,
so there's now a single source of truth across runtime and build.

Also adds a 'Custom Attributes' subsection under User Feedback showing
how to pass the new attributes Map parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the opaque Map<String, String> extra-fields with a typed
CommitOptions class that mirrors the documented commitS3CrashUpload
multipart body 1-to-1 (crashType, crashTypeId, fullDumpFlag, appKey,
description, user, email, internalIP, notes, processor, crashTime,
attributes, crashHash). CommitOptions uses fluent setters on a
package-private POJO — a real Builder would be more idiomatic if we
ever expose it publicly, but for internal use the lighter form is fine.

Simplifies ReportUploader:
- Single entry point: upload(byte[] zipped, CommitOptions). Callers
  produce their own zips.
- Small zip(name, bytes) helper for the common single-entry case
  (used by AnrReporter).
- No more Map<String, ZipEntrySource> plumbing — FeedbackClient builds
  its zip inline with ZipOutputStream, streaming File attachments
  directly from disk so peak heap is ~zip size rather than
  attachment + zip size. Important on mobile where 100 MB max
  attachments could otherwise approach heap limits.
- Drops the uniqueEntryName helper — callers are responsible for
  attachment filename collisions (ZipOutputStream throws if violated).

AnrReporter:
- Uses ReportUploader.zip(...) for its single-entry zip.
- Replaces remaining "UTF-8" string literals with StandardCharsets.UTF_8
  to avoid the checked-exception path.

Tests updated to match the new API and expanded to cover all
CommitOptions fields on the commit request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bobbyg603 bobbyg603 merged commit 8a1d008 into main Apr 16, 2026
4 checks passed
@bobbyg603 bobbyg603 mentioned this pull request Apr 16, 2026
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.

3 participants