Skip to content

[Feat][SDK-347] ANR report #323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0161c77
feat: Update android min sdk from 16 to 21 and target/compile sdk fro…
christianbuon Dec 8, 2024
69df96d
feat: Add ANR detectors
christianbuon Jan 13, 2025
6ae33b7
feat: send non main threads as extra information for api >= 30
christianbuon Jan 26, 2025
bc3f27a
refactor: remove unnecessary code
christianbuon Jan 26, 2025
b8cb8f0
refactor: don't launch thread if listener is null
christianbuon Jan 26, 2025
957a0d8
refactor: remove unnecessary attribute for Line
christianbuon Jan 26, 2025
91ab622
refactor: do not create watchdog or run it if anr listener is null
christianbuon Feb 2, 2025
a0082ff
test: add Tests for Watchdog
christianbuon Feb 2, 2025
c69563e
test: remove unused imports
christianbuon Feb 2, 2025
c3ed238
test: set parameters as private
christianbuon Feb 10, 2025
dcac7ab
test: Add tests for HistoricalAnrDetector
christianbuon Feb 10, 2025
61dfa33
refactor: Initialize ThreadParser as expected
christianbuon Feb 10, 2025
6c0848e
feat: Add AndroidConfiguration to tun on/off ANR detectors implementa…
christianbuon Feb 16, 2025
64c5f4e
Merge branch 'master' into feat/SDK-347/anr-report
christianbuon May 18, 2025
da109a7
refactor: use RollbarThread to send ANR information
christianbuon May 26, 2025
c1a5df0
feat(HistoricalAnrDetector): save last anr timestamp
christianbuon May 28, 2025
a349bf3
build: downgrade compile and target sdk to 30, to test CI
christianbuon May 29, 2025
c8e8c77
test: delete tests to validate if CI completes
christianbuon May 29, 2025
1c23f8b
fix: lint
christianbuon May 29, 2025
5c94db7
fix(RollbarThrowableWrapper): lint
christianbuon May 29, 2025
978c831
fix(RollbarThrowableWrapper): lint
christianbuon May 29, 2025
7000260
chore: add new method information in revapi file
christianbuon May 29, 2025
073a470
build(android-example): migrate old libs to AndroidX
christianbuon May 29, 2025
257f738
build: update compile and target sdk to 32
christianbuon Jun 1, 2025
d044fa3
build: downgrade compile and target sdk to 31
christianbuon Jun 1, 2025
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
6 changes: 3 additions & 3 deletions examples/rollbar-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ buildscript {
apply plugin: 'com.android.application'

android {
compileSdkVersion 27
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.rollbar.example.android"
minSdkVersion 16
minSdkVersion 21
// FIXME: Pending further discussion
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 27
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Expand Down
3 changes: 2 additions & 1 deletion examples/rollbar-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
Expand Down
6 changes: 3 additions & 3 deletions rollbar-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ apply from: "$rootDir/gradle/release.gradle"
apply from: "$rootDir/gradle/android.quality.gradle"

android {
compileSdkVersion 27
compileSdkVersion 33
buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+

defaultConfig {
minSdkVersion 16
minSdkVersion 21
// FIXME: Pending further discussion
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 27
targetSdkVersion 33
consumerProguardFiles 'proguard-rules.pro'
manifestPlaceholders = [notifierVersion: VERSION_NAME]
}
Expand Down
13 changes: 13 additions & 0 deletions rollbar-android/src/main/java/com/rollbar/android/Rollbar.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import android.os.Bundle;

import android.util.Log;

import com.rollbar.android.anr.AnrDetector;
import com.rollbar.android.anr.AnrDetectorFactory;
import com.rollbar.android.anr.AnrException;
import com.rollbar.android.notifier.sender.ConnectionAwareSenderFailureStrategy;
import com.rollbar.android.provider.ClientProvider;
import com.rollbar.api.payload.data.TelemetryType;
Expand All @@ -30,6 +34,7 @@
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -164,6 +169,8 @@ public static Rollbar init(Context context, String accessToken, String environme
includeLogcat, provider, DEFAULT_CAPTURE_IP, DEFAULT_MAX_LOGCAT_SIZE,
suspendWhenNetworkIsUnavailable);
}
AnrDetector anrDetector = AnrDetectorFactory.create(context, error -> reportANR(error));
anrDetector.init();
return notifier;
}

Expand Down Expand Up @@ -1086,4 +1093,10 @@ private static void ensureInit(Runnable runnable) {
}
}

private static void reportANR(AnrException error){
Map<String, Object> map = new HashMap<>();
map.put("TYPE", "ANR");
notifier.log(error, map);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.rollbar.android.anr;

public interface AnrDetector {
void init();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.rollbar.android.anr;

import android.content.Context;
import android.os.Build;

import com.rollbar.android.anr.historical.HistoricalAnrDetector;
import com.rollbar.android.anr.watchdog.WatchdogAnrDetector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AnrDetectorFactory {
private final static Logger LOGGER = LoggerFactory.getLogger(AnrDetectorFactory.class);

public static AnrDetector create(
Context context,
AnrListener anrListener
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
LOGGER.debug("Creating HistoricalAnrDetector");
return new HistoricalAnrDetector(context, anrListener);
} else {
LOGGER.debug("Creating WatchdogAnrDetector");
return new WatchdogAnrDetector(context, anrListener);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.rollbar.android.anr;

public final class AnrException extends RuntimeException {

public AnrException(String message, Thread thread) {
super(message);
setStackTrace(thread.getStackTrace());
}

public AnrException(StackTraceElement[] stackTraceElements) {
super("Application Not Responding");
setStackTrace(stackTraceElements);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.rollbar.android.anr;

public interface AnrListener {
/**
* Called when an ANR is detected.
*
* @param error The error describing the ANR.
*/
void onAppNotResponding(AnrException error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.rollbar.android.anr.historical;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.content.Context;

import com.rollbar.android.anr.AnrDetector;
import com.rollbar.android.anr.AnrException;
import com.rollbar.android.anr.AnrListener;
import com.rollbar.android.anr.historical.stacktrace.Lines;
import com.rollbar.android.anr.historical.stacktrace.RollbarThread;
import com.rollbar.android.anr.historical.stacktrace.ThreadDumpParser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Objects;

@SuppressLint("NewApi") // Validated in the Factory
public class HistoricalAnrDetector implements AnrDetector {
private final static Logger LOGGER = LoggerFactory.getLogger(HistoricalAnrDetector.class);

private final Context context;
private final AnrListener anrListener;
ThreadDumpParser threadDumpParser = new ThreadDumpParser(true);//todo remove isBackground

public HistoricalAnrDetector(
Context context,
AnrListener anrListener
) {
this.context = context;
this.anrListener = anrListener;
}

@Override
public void init() {
Thread thread = new Thread("HistoricalAnrDetectorThread") {
@Override
public void run() {
super.run();
evaluateLastExitReasons();
}
};
thread.setDaemon(true);
thread.start();
}


private void evaluateLastExitReasons() {
if (anrListener == null) {
LOGGER.error("AnrListener is null");
return;
}

List<ApplicationExitInfo> applicationExitInfoList = getApplicationExitInformation();

if (applicationExitInfoList.isEmpty()) {
LOGGER.debug("Empty ApplicationExitInfo List");
return;
}

for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList) {
if (isNotAnr(applicationExitInfo)) {
continue;
}

try {
List<RollbarThread> threads = getThreads(applicationExitInfo);

if (threads.isEmpty()) {
LOGGER.warn("Error parsing ANR");
continue;//Todo: Do something ?
}

anrListener.onAppNotResponding(createAnrException(threads));
} catch (Throwable e) {
LOGGER.error("Can't parse ANR", e);
}
}
}

private boolean isNotAnr(ApplicationExitInfo applicationExitInfo) {
return applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR;
}

private AnrException createAnrException(List<RollbarThread> threads) {
return new AnrException(threads.get(0).toStackTraceElement());
}

private List<ApplicationExitInfo> getApplicationExitInformation() {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
return activityManager.getHistoricalProcessExitReasons(null, 0, 0);
}

private List<RollbarThread> getThreads(ApplicationExitInfo applicationExitInfo) throws IOException {
Lines lines = getLines(applicationExitInfo);
return threadDumpParser.parse(lines);
}

private Lines getLines(ApplicationExitInfo applicationExitInfo) throws IOException {
byte[] dump = getDumpBytes(Objects.requireNonNull(applicationExitInfo.getTraceInputStream()));
return getLines(dump);
}

private Lines getLines(byte[] dump) throws IOException {
return Lines.readLines(toBufferReader(dump));
}

private BufferedReader toBufferReader(byte[] dump) {
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)));
}

private byte[] getDumpBytes(final InputStream trace) throws IOException {
try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {

int nRead;
byte[] data = new byte[1024];

while ((nRead = trace.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}

return buffer.toByteArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rollbar.android.anr.historical.stacktrace;

public final class Line {
public int lineno;
Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds like something that should be read-only and hidden?

public String text;

public Line(final int lineno, final String text) {
this.lineno = lineno;
this.text = text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.rollbar.android.anr.historical.stacktrace;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

public final class Lines {
private final ArrayList<? extends Line> mList;
private final int mMin;
private final int mMax;

/** The read position inside the list. */
public int pos;

/** Read the whole file into a Lines object. */
public static Lines readLines(final File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return Lines.readLines(reader);
}
}

/** Read the whole file into a Lines object. */
public static Lines readLines(final BufferedReader in) throws IOException {
final ArrayList<Line> list = new ArrayList<>();

int lineno = 0;
String text;
while ((text = in.readLine()) != null) {
lineno++;
list.add(new Line(lineno, text));
}

return new Lines(list);
}

/** Construct with a list of lines. */
public Lines(final ArrayList<Line> list) {
this.mList = list;
mMin = 0;
mMax = mList.size();
}

/** If there are more lines to read within the current range. */
public boolean hasNext() {
return pos < mMax;
}

/**
* Return the next line, or null if there are no more lines to read. Also returns null in the
* error condition where pos is before the beginning.
*/
public Line next() {
if (pos >= mMin && pos < mMax) {
return this.mList.get(pos++);
} else {
return null;
}
}

/** Move the read position back by one line. */
public void rewind() {
pos--;
}
}
Loading
Loading