From 0161c7757bae76930653740e085fbab0470a8229 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 7 Dec 2024 23:52:06 -0300 Subject: [PATCH 01/13] feat: Update android min sdk from 16 to 21 and target/compile sdk from 27 to 33 --- examples/rollbar-android/build.gradle | 6 +++--- examples/rollbar-android/src/main/AndroidManifest.xml | 3 ++- rollbar-android/build.gradle | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index 41e42406..6271b056 100644 --- a/examples/rollbar-android/build.gradle +++ b/examples/rollbar-android/build.gradle @@ -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" diff --git a/examples/rollbar-android/src/main/AndroidManifest.xml b/examples/rollbar-android/src/main/AndroidManifest.xml index 3e3d8183..7a40e790 100644 --- a/examples/rollbar-android/src/main/AndroidManifest.xml +++ b/examples/rollbar-android/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ android:theme="@style/AppTheme"> diff --git a/rollbar-android/build.gradle b/rollbar-android/build.gradle index 7a155de4..b81741da 100644 --- a/rollbar-android/build.gradle +++ b/rollbar-android/build.gradle @@ -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] } From 69df96d1008802bd382d9bcf3a02557c7eea64d0 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 12 Jan 2025 22:40:03 -0300 Subject: [PATCH 02/13] feat: Add ANR detectors --- .../java/com/rollbar/android/Rollbar.java | 13 + .../com/rollbar/android/anr/AnrDetector.java | 5 + .../android/anr/AnrDetectorFactory.java | 27 ++ .../com/rollbar/android/anr/AnrException.java | 15 + .../com/rollbar/android/anr/AnrListener.java | 10 + .../anr/historical/HistoricalAnrDetector.java | 135 ++++++++ .../anr/historical/stacktrace/Line.java | 11 + .../anr/historical/stacktrace/Lines.java | 66 ++++ .../anr/historical/stacktrace/LockReason.java | 129 +++++++ .../historical/stacktrace/RollbarThread.java | 181 ++++++++++ .../anr/historical/stacktrace/StackFrame.java | 315 +++++++++++++++++ .../anr/historical/stacktrace/StackTrace.java | 85 +++++ .../stacktrace/StackTraceFactory.java | 130 +++++++ .../stacktrace/ThreadDumpParser.java | 327 ++++++++++++++++++ .../android/anr/watchdog/LooperHandler.java | 19 + .../android/anr/watchdog/WatchDog.java | 110 ++++++ .../anr/watchdog/WatchdogAnrDetector.java | 70 ++++ 17 files changed, 1648 insertions(+) create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetector.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/AnrListener.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/LooperHandler.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java diff --git a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java index 5fe245c4..8d9c1374 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -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; @@ -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; @@ -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; } @@ -1086,4 +1093,10 @@ private static void ensureInit(Runnable runnable) { } } + private static void reportANR(AnrException error){ + Map map = new HashMap<>(); + map.put("TYPE", "ANR"); + notifier.log(error, map); + } + } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetector.java new file mode 100644 index 00000000..2054b7ea --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetector.java @@ -0,0 +1,5 @@ +package com.rollbar.android.anr; + +public interface AnrDetector { + void init(); +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java new file mode 100644 index 00000000..d29cafb3 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java @@ -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); + } + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java new file mode 100644 index 00000000..35841bfa --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java @@ -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); + } + +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrListener.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrListener.java new file mode 100644 index 00000000..1224d8e1 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrListener.java @@ -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); +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java new file mode 100644 index 00000000..d9fbe37a --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java @@ -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 applicationExitInfoList = getApplicationExitInformation(); + + if (applicationExitInfoList.isEmpty()) { + LOGGER.debug("Empty ApplicationExitInfo List"); + return; + } + + for (ApplicationExitInfo applicationExitInfo : applicationExitInfoList) { + if (isNotAnr(applicationExitInfo)) { + continue; + } + + try { + List 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 threads) { + return new AnrException(threads.get(0).toStackTraceElement()); + } + + private List getApplicationExitInformation() { + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + return activityManager.getHistoricalProcessExitReasons(null, 0, 0); + } + + private List 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(); + } + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java new file mode 100644 index 00000000..c9290930 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java @@ -0,0 +1,11 @@ +package com.rollbar.android.anr.historical.stacktrace; + +public final class Line { + public int lineno; + public String text; + + public Line(final int lineno, final String text) { + this.lineno = lineno; + this.text = text; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java new file mode 100644 index 00000000..ebc42d6a --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java @@ -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 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 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 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--; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java new file mode 100644 index 00000000..8882a36c --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java @@ -0,0 +1,129 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import com.rollbar.api.json.JsonSerializable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class LockReason implements JsonSerializable { + + public static final int LOCKED = 1; + public static final int WAITING = 2; + public static final int SLEEPING = 4; + public static final int BLOCKED = 8; + + public static final int ANY = LOCKED | WAITING | SLEEPING | BLOCKED; + + private int type; + private String address; + private String packageName; + private String className; + private Long threadId; + private Map unknown; + + public LockReason() {} + + public LockReason(final LockReason other) { + this.type = other.type; + this.address = other.address; + this.packageName = other.packageName; + this.className = other.className; + this.threadId = other.threadId; + if (other.unknown != null) { + this.unknown = new ConcurrentHashMap<>(other.unknown); + } + } + + @SuppressWarnings("unused") + public int getType() { + return type; + } + + public void setType(final int type) { + this.type = type; + } + + + public String getAddress() { + return address; + } + + public void setAddress(final String address) { + this.address = address; + } + + + public String getPackageName() { + return packageName; + } + + public void setPackageName(final String packageName) { + this.packageName = packageName; + } + + + public String getClassName() { + return className; + } + + public void setClassName(final String className) { + this.className = className; + } + + + public Long getThreadId() { + return threadId; + } + + public void setThreadId(final Long threadId) { + this.threadId = threadId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LockReason that = (LockReason) o; + return Objects.equals(address, that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + @Override + public Object asJson() { + Map values = new HashMap<>(); + values.put(JsonKeys.TYPE, type); + if (address != null) { + values.put(JsonKeys.ADDRESS, address); + } + if (packageName != null) { + values.put(JsonKeys.PACKAGE_NAME, packageName); + } + if (className != null) { + values.put(JsonKeys.CLASS_NAME, className); + } + if (threadId != null) { + values.put(JsonKeys.THREAD_ID, threadId); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + values.put(key, value); + } + } + return values; + } + + public static final class JsonKeys { + public static final String TYPE = "type"; + public static final String ADDRESS = "address"; + public static final String PACKAGE_NAME = "package_name"; + public static final String CLASS_NAME = "class_name"; + public static final String THREAD_ID = "thread_id"; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java new file mode 100644 index 00000000..02376847 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java @@ -0,0 +1,181 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import com.rollbar.api.json.JsonSerializable; + +import java.util.HashMap; +import java.util.Map; + +public class RollbarThread implements JsonSerializable { + private Long id; + private Integer priority; + private String name; + private String state; + private Boolean crashed; + private Boolean current; + private Boolean daemon; + private Boolean main; + private StackTrace stacktrace; + + private Map heldLocks; + + public StackTraceElement[] toStackTraceElement() { + return stacktrace.getStackTraceElements(); + } + + @SuppressWarnings("unused") + private Map unknown; + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public Boolean isCrashed() { + return crashed; + } + + public void setCrashed(final Boolean crashed) { + this.crashed = crashed; + } + + public Boolean isCurrent() { + return current; + } + + public void setCurrent(final Boolean current) { + this.current = current; + } + + public StackTrace getStacktrace() { + return stacktrace; + } + + public void setStacktrace(final StackTrace stacktrace) { + this.stacktrace = stacktrace; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(final Integer priority) { + this.priority = priority; + } + + public Boolean isDaemon() { + return daemon; + } + + public void setDaemon(final Boolean daemon) { + this.daemon = daemon; + } + + public Boolean isMain() { + return main; + } + + public void setMain(final Boolean main) { + this.main = main; + } + + public String getState() { + return state; + } + + public void setState(final String state) { + this.state = state; + } + + public Map getHeldLocks() { + return heldLocks; + } + + public void setHeldLocks(final Map heldLocks) { + this.heldLocks = heldLocks; + } + + @Override + public Object asJson() { + Map values = new HashMap<>(); + + if (id != null) { + values.put(JsonKeys.ID, id); + } + if (priority != null) { + values.put(JsonKeys.PRIORITY, priority); + } + if (name != null) { + values.put(JsonKeys.NAME, name); + } + if (state != null) { + values.put(JsonKeys.STATE, state); + } + if (crashed != null) { + values.put(JsonKeys.CRASHED, crashed); + } + if (current != null) { + values.put(JsonKeys.CURRENT, current); + } + if (daemon != null) { + values.put(JsonKeys.DAEMON, daemon); + } + if (main != null) { + values.put(JsonKeys.MAIN, main); + } + if (stacktrace != null) { + values.put(JsonKeys.STACKTRACE, stacktrace); + } + if (heldLocks != null) { + values.put(JsonKeys.HELD_LOCKS, heldLocks); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + values.put(key, value); + } + } + return values; + } + + @Override + public String toString() { + return "RollbarThread{" + + "id=" + id + + ", priority=" + priority + + ", name='" + name + '\'' + + ", state='" + state + '\'' + + ", crashed=" + crashed + + ", current=" + current + + ", daemon=" + daemon + + ", main=" + main + + ", stacktrace=" + stacktrace + + ", heldLocks=" + heldLocks + + ", unknown=" + unknown + + '}'; + } + + + public static final class JsonKeys { + public static final String ID = "id"; + public static final String PRIORITY = "priority"; + public static final String NAME = "name"; + public static final String STATE = "state"; + public static final String CRASHED = "crashed"; + public static final String CURRENT = "current"; + public static final String DAEMON = "daemon"; + public static final String MAIN = "main"; + public static final String STACKTRACE = "stacktrace"; + public static final String HELD_LOCKS = "held_locks"; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java new file mode 100644 index 00000000..369ebcbc --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java @@ -0,0 +1,315 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import com.rollbar.api.json.JsonSerializable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StackFrame implements JsonSerializable { + + private List preContext; + + private List postContext; + + private Map vars; + + private List framesOmitted; + + private String filename = ""; + + private String function = ""; + + private String module = ""; + + private Integer lineno = 0; + + private Integer colno; + + private String absPath; + + private String contextLine; + + private Boolean inApp; + + private String _package; + + private Boolean _native; + + private String platform; + + private String imageAddr; + + private String symbolAddr; + + private String instructionAddr; + + private String symbol; + + @SuppressWarnings("unused") + private Map unknown; + + public StackTraceElement toStackTraceElement() { + return new StackTraceElement(module, function, filename, lineno); + } + + private String rawFunction; + + private LockReason lock; + + public List getPreContext() { + return preContext; + } + + public void setPreContext(final List preContext) { + this.preContext = preContext; + } + + public List getPostContext() { + return postContext; + } + + public void setPostContext(final List postContext) { + this.postContext = postContext; + } + + public Map getVars() { + return vars; + } + + public void setVars(final Map vars) { + this.vars = vars; + } + + public List getFramesOmitted() { + return framesOmitted; + } + + public void setFramesOmitted(final List framesOmitted) { + this.framesOmitted = framesOmitted; + } + + public String getFilename() { + return filename; + } + + public void setFilename(final String filename) { + this.filename = filename; + } + + public String getFunction() { + return function; + } + + public void setFunction(final String function) { + this.function = function; + } + + public String getModule() { + return module; + } + + public void setModule(final String module) { + this.module = module; + } + + public Integer getLineno() { + return lineno; + } + + public void setLineno(final Integer lineno) { + this.lineno = lineno; + } + + public Integer getColno() { + return colno; + } + + public void setColno(final Integer colno) { + this.colno = colno; + } + + public String getAbsPath() { + return absPath; + } + + public void setAbsPath(final String absPath) { + this.absPath = absPath; + } + + public String getContextLine() { + return contextLine; + } + + public void setContextLine(final String contextLine) { + this.contextLine = contextLine; + } + + public Boolean isInApp() { + return inApp; + } + + public void setInApp(final Boolean inApp) { + this.inApp = inApp; + } + + public String getPackage() { + return _package; + } + + public void setPackage(final String _package) { + this._package = _package; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(final String platform) { + this.platform = platform; + } + + public String getImageAddr() { + return imageAddr; + } + + public void setImageAddr(final String imageAddr) { + this.imageAddr = imageAddr; + } + + public String getSymbolAddr() { + return symbolAddr; + } + + public void setSymbolAddr(final String symbolAddr) { + this.symbolAddr = symbolAddr; + } + + public String getInstructionAddr() { + return instructionAddr; + } + + public void setInstructionAddr(final String instructionAddr) { + this.instructionAddr = instructionAddr; + } + + public Boolean isNative() { + return _native; + } + + public void setNative(final Boolean _native) { + this._native = _native; + } + + public String getRawFunction() { + return rawFunction; + } + + public void setRawFunction(final String rawFunction) { + this.rawFunction = rawFunction; + } + + + public String getSymbol() { + return symbol; + } + + public void setSymbol(final String symbol) { + this.symbol = symbol; + } + + + public LockReason getLock() { + return lock; + } + + public void setLock(final LockReason lock) { + this.lock = lock; + } + + // region json + + @Override + public Object asJson() { + Map values = new HashMap<>(); + if (filename != null) { + values.put(JsonKeys.FILENAME, filename); + } + if (function != null) { + values.put(JsonKeys.FUNCTION, function); + } + if (module != null) { + values.put(JsonKeys.MODULE, module); + } + if (lineno != null) { + values.put(JsonKeys.LINENO, lineno); + } + if (colno != null) { + values.put(JsonKeys.COLNO, colno); + } + if (absPath != null) { + values.put(JsonKeys.ABS_PATH, absPath); + } + if (contextLine != null) { + values.put(JsonKeys.CONTEXT_LINE, contextLine); + } + if (inApp != null) { + values.put(JsonKeys.IN_APP, inApp); + } + if (_package != null) { + values.put(JsonKeys.PACKAGE, _package); + } + if (_native != null) { + values.put(JsonKeys.NATIVE, _native); + } + if (platform != null) { + values.put(JsonKeys.PLATFORM, platform); + } + if (imageAddr != null) { + values.put(JsonKeys.IMAGE_ADDR, imageAddr); + } + if (symbolAddr != null) { + values.put(JsonKeys.SYMBOL_ADDR, symbolAddr); + } + if (instructionAddr != null) { + values.put(JsonKeys.INSTRUCTION_ADDR, instructionAddr); + } + if (rawFunction != null) { + values.put(JsonKeys.RAW_FUNCTION, rawFunction); + } + if (symbol != null) { + values.put(JsonKeys.SYMBOL, symbol); + } + if (lock != null) { + values.put(JsonKeys.LOCK, lock); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + values.put(key, value); + + } + } + return values; + } + + public static final class JsonKeys { + public static final String FILENAME = "filename"; + public static final String FUNCTION = "function"; + public static final String MODULE = "module"; + public static final String LINENO = "lineno"; + public static final String COLNO = "colno"; + public static final String ABS_PATH = "abs_path"; + public static final String CONTEXT_LINE = "context_line"; + public static final String IN_APP = "in_app"; + public static final String PACKAGE = "package"; + public static final String NATIVE = "native"; + public static final String PLATFORM = "platform"; + public static final String IMAGE_ADDR = "image_addr"; + public static final String SYMBOL_ADDR = "symbol_addr"; + public static final String INSTRUCTION_ADDR = "instruction_addr"; + public static final String RAW_FUNCTION = "raw_function"; + public static final String SYMBOL = "symbol"; + public static final String LOCK = "lock"; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java new file mode 100644 index 00000000..f71ed47c --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java @@ -0,0 +1,85 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import com.rollbar.api.json.JsonSerializable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class StackTrace implements JsonSerializable { + + private List frames; + private Map registers; + private Boolean snapshot; + + @SuppressWarnings("unused") + private Map unknown; + + public StackTrace() {} + + public StackTrace(final List frames) { + this.frames = frames; + } + + public StackTraceElement[] getStackTraceElements() { + StackTraceElement[] stackTraceElements = new StackTraceElement[frames.size()]; + int element = 0; + for (StackFrame frame : frames) { + stackTraceElements[element] = frame.toStackTraceElement(); + element++; + } + return stackTraceElements; + } + + public List getFrames() { + return frames; + } + + public void setFrames(final List frames) { + this.frames = frames; + } + + public Map getRegisters() { + return registers; + } + + public void setRegisters(final Map registers) { + this.registers = registers; + } + + public Boolean getSnapshot() { + return snapshot; + } + + public void setSnapshot(final Boolean snapshot) { + this.snapshot = snapshot; + } + + @Override + public Object asJson() { + Map values = new HashMap<>(); + + if (frames != null) { + values.put(JsonKeys.FRAMES, frames); + } + if (registers != null) { + values.put(JsonKeys.REGISTERS, registers); + } + if (snapshot != null) { + values.put(JsonKeys.SNAPSHOT, snapshot); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + values.put(key, value); + } + } + return values; + } + + public static final class JsonKeys { + public static final String FRAMES = "frames"; + public static final String REGISTERS = "registers"; + public static final String SNAPSHOT = "snapshot"; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java new file mode 100644 index 00000000..aded2957 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java @@ -0,0 +1,130 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class StackTraceFactory { + + private static final int STACKTRACE_FRAME_LIMIT = 100; + + public StackTraceFactory(/*todo pass options?*/) { + } + + public List getStackFrames( + final StackTraceElement[] elements, final boolean includeFrames) { + List StackFrames = null; + + if (elements != null && elements.length > 0) { + StackFrames = new ArrayList<>(); + for (StackTraceElement item : elements) { + if (item != null) { + + final String className = item.getClassName(); + if (!includeFrames && (className.startsWith("com.rollbar."))) { + continue; + } + + final StackFrame StackFrame = new StackFrame(); + StackFrame.setInApp(isInApp(className)); + StackFrame.setModule(className); + StackFrame.setFunction(item.getMethodName()); + StackFrame.setFilename(item.getFileName()); + if (item.getLineNumber() >= 0) { + StackFrame.setLineno(item.getLineNumber()); + } + StackFrame.setNative(item.isNativeMethod()); + StackFrames.add(StackFrame); + + if (StackFrames.size() >= STACKTRACE_FRAME_LIMIT) { + break; + } + } + } + Collections.reverse(StackFrames); + } + + return StackFrames; + } + + /** + * Returns if the className is InApp or not. + * + * @param className the className + * @return true if it is or false otherwise + */ + + public Boolean isInApp(final String className) { + if (className == null || className.isEmpty()) { + return true; + } +/* + final List inAppIncludes = options.getInAppIncludes(); + for (String include : inAppIncludes) { + if (className.startsWith(include)) { + return true; + } + } + + */ +/* + final List inAppExcludes = options.getInAppExcludes(); + for (String exclude : inAppExcludes) { + if (className.startsWith(exclude)) { + return false; + } + } + + */ + + return null; + } + + /** + * Returns the call stack leading to the exception, including in-app frames and excluding rollbar + * and system frames. + * + * @param exception an exception to get the call stack to + * @return a list of rollbar stack frames leading to the exception + */ + + List getInAppCallStack(final Throwable exception) { + final StackTraceElement[] stacktrace = exception.getStackTrace(); + final List frames = getStackFrames(stacktrace, false); + if (frames == null) { + return Collections.emptyList(); + } +/* + final List inAppFrames = + CollectionUtils.filterListEntries(frames, (frame) -> Boolean.TRUE.equals(frame.isInApp())); + + if (!inAppFrames.isEmpty()) { + return inAppFrames; + } + + // if inAppFrames is empty, most likely we're operating over an obfuscated app, just trying to + // fallback to all the frames that are not system frames + return CollectionUtils.filterListEntries( + frames, + (frame) -> { + final String module = frame.getModule(); + boolean isSystemFrame = false; + if (module != null) { + isSystemFrame = + module.startsWith("sun.") + || module.startsWith("java.") + || module.startsWith("android.") + || module.startsWith("com.android."); + } + return !isSystemFrame; + }); + todo crb + */ + return Collections.emptyList(); + } + + public List getInAppCallStack() { + return getInAppCallStack(new Exception()); + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java new file mode 100644 index 00000000..41836583 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java @@ -0,0 +1,327 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ThreadDumpParser { + private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + + private static final Pattern BEGIN_MANAGED_THREAD_RE = + Pattern.compile("\"(.*)\" (.*) ?prio=(\\d+)\\s+tid=(\\d+)\\s*(.*)"); + + private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE = + Pattern.compile("\"(.*)\" (.*) ?sysTid=(\\d+)"); + + private static final Pattern NATIVE_RE = + Pattern.compile( + " *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*?)\\s+\\((.*)\\+(\\d+)\\)(?: \\(.*\\))?"); + private static final Pattern NATIVE_NO_LOC_RE = + Pattern.compile( + " *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?(?: \\(.*\\))?"); + private static final Pattern JAVA_RE = + Pattern.compile(" *at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)"); + private static final Pattern JNI_RE = + Pattern.compile(" *at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\(Native method\\)"); + private static final Pattern LOCKED_RE = + Pattern.compile(" *- locked \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern SLEEPING_ON_RE = + Pattern.compile(" *- sleeping on \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_ON_RE = + Pattern.compile(" *- waiting on \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_TO_LOCK_RE = + Pattern.compile( + " *- waiting to lock \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_TO_LOCK_HELD_RE = + Pattern.compile( + " *- waiting to lock \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)" + + "(?: held by thread (\\d+))"); + private static final Pattern WAITING_TO_LOCK_UNKNOWN_RE = + Pattern.compile(" *- waiting to lock an unknown object"); + private static final Pattern BLANK_RE = Pattern.compile("\\s+"); + + + private final boolean isBackground; + + private final StackTraceFactory stackTraceFactory; + + public ThreadDumpParser(final boolean isBackground) { + this.isBackground = isBackground; + this.stackTraceFactory = new StackTraceFactory(); + } + + + public List parse(final Lines lines) { + final List rollbarThreads = new ArrayList<>(); + + final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); + final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); + + while (lines.hasNext()) { + Line line = lines.next(); + if (line == null) { + LOGGER.warn("No line: Internal error while parsing thread dump"); + return rollbarThreads; + } + final String text = line.text; + // we only handle managed threads, as unmanaged/not attached do not have the thread id and + // our protocol does not support this case + if (matches(beginManagedThreadRe, text) || matches(beginUnmanagedNativeThreadRe, text)) { + lines.rewind(); + + final RollbarThread rollbarThread = parseThread(lines); + if (rollbarThread != null) { + rollbarThreads.add(rollbarThread); + } + } + } + return rollbarThreads; + } + + private RollbarThread parseThread(final Lines lines) { + final RollbarThread RollbarThread = new RollbarThread(); + + final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); + final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); + + // thread attributes + if (!lines.hasNext()) { + return null; + } + final Line line = lines.next(); + if (line == null) { + LOGGER.warn("Internal error while parsing thread dump"); + return null; + } + if (matches(beginManagedThreadRe, line.text)) { + Long threadId = getLong(beginManagedThreadRe, 4, null); + if (threadId == null) { + LOGGER.debug("No thread id in the dump, skipping thread"); + return null; + } + RollbarThread.setId(threadId); + RollbarThread.setName(beginManagedThreadRe.group(1)); + final String state = beginManagedThreadRe.group(5); + // sanitizing thread that have more details after their actual state, e.g. + // "Native (still starting up)" <- we just need "Native" here + if (state != null) { + if (state.contains(" ")) { + RollbarThread.setState(state.substring(0, state.indexOf(' '))); + } else { + RollbarThread.setState(state); + } + } + } else if (matches(beginUnmanagedNativeThreadRe, line.text)) { + Long systemThreadId = getLong(beginUnmanagedNativeThreadRe, 3, null); + if (systemThreadId == null) { + LOGGER.debug("No thread id in the dump, skipping thread"); + return null; + } + RollbarThread.setId(systemThreadId); + RollbarThread.setName(beginUnmanagedNativeThreadRe.group(1)); + } + + final String threadName = RollbarThread.getName(); + if (threadName != null) { + boolean isMain = threadName.equals("main"); + RollbarThread.setMain(isMain); + // since it's an ANR, the crashed thread will always be main + RollbarThread.setCrashed(isMain); + RollbarThread.setCurrent(isMain && !isBackground); + } + + // thread stacktrace + final StackTrace stackTrace = parseStacktrace(lines, RollbarThread); + RollbarThread.setStacktrace(stackTrace); + return RollbarThread; + } + + + private StackTrace parseStacktrace( + final Lines lines, final RollbarThread rollbarThread) { + final List frames = new ArrayList<>(); + StackFrame lastJavaFrame = null; + + final Matcher nativeRe = NATIVE_RE.matcher(""); + final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE.matcher(""); + final Matcher javaRe = JAVA_RE.matcher(""); + final Matcher jniRe = JNI_RE.matcher(""); + final Matcher lockedRe = LOCKED_RE.matcher(""); + final Matcher waitingOnRe = WAITING_ON_RE.matcher(""); + final Matcher sleepingOnRe = SLEEPING_ON_RE.matcher(""); + final Matcher waitingToLockHeldRe = WAITING_TO_LOCK_HELD_RE.matcher(""); + final Matcher waitingToLockRe = WAITING_TO_LOCK_RE.matcher(""); + final Matcher waitingToLockUnknownRe = WAITING_TO_LOCK_UNKNOWN_RE.matcher(""); + final Matcher blankRe = BLANK_RE.matcher(""); + + while (lines.hasNext()) { + final Line line = lines.next(); + if (line == null) { + LOGGER.warn("Internal error while parsing thread dump"); + break; + } + final String text = line.text; + if (matches(nativeRe, text)) { + final StackFrame frame = new StackFrame(); + frame.setPackage(nativeRe.group(1)); + frame.setFunction(nativeRe.group(2)); + frame.setLineno(getInteger(nativeRe, 3, null)); + frames.add(frame); + lastJavaFrame = null; + } else if (matches(nativeNoLocRe, text)) { + final StackFrame frame = new StackFrame(); + frame.setPackage(nativeNoLocRe.group(1)); + frame.setFunction(nativeNoLocRe.group(2)); + frames.add(frame); + lastJavaFrame = null; + } else if (matches(javaRe, text)) { + final StackFrame frame = new StackFrame(); + final String packageName = javaRe.group(1); + final String className = javaRe.group(2); + final String module = String.format("%s.%s", packageName, className); + frame.setModule(module); + frame.setFunction(javaRe.group(3)); + frame.setFilename(javaRe.group(4)); + frame.setLineno(getUInteger(javaRe, 5, null)); + frame.setInApp(stackTraceFactory.isInApp(module)); + frames.add(frame); + lastJavaFrame = frame; + } else if (matches(jniRe, text)) { + final StackFrame frame = new StackFrame(); + final String packageName = jniRe.group(1); + final String className = jniRe.group(2); + final String module = String.format("%s.%s", packageName, className); + frame.setModule(module); + frame.setFunction(jniRe.group(3)); + frame.setInApp(stackTraceFactory.isInApp(module)); + frames.add(frame); + lastJavaFrame = frame; + } else if (matches(lockedRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.LOCKED); + lock.setAddress(lockedRe.group(1)); + lock.setPackageName(lockedRe.group(2)); + lock.setClassName(lockedRe.group(3)); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (matches(waitingOnRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.WAITING); + lock.setAddress(waitingOnRe.group(1)); + lock.setPackageName(waitingOnRe.group(2)); + lock.setClassName(waitingOnRe.group(3)); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (matches(sleepingOnRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.SLEEPING); + lock.setAddress(sleepingOnRe.group(1)); + lock.setPackageName(sleepingOnRe.group(2)); + lock.setClassName(sleepingOnRe.group(3)); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (matches(waitingToLockHeldRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.BLOCKED); + lock.setAddress(waitingToLockHeldRe.group(1)); + lock.setPackageName(waitingToLockHeldRe.group(2)); + lock.setClassName(waitingToLockHeldRe.group(3)); + lock.setThreadId(getLong(waitingToLockHeldRe, 4, null)); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (matches(waitingToLockRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.BLOCKED); + lock.setAddress(waitingToLockRe.group(1)); + lock.setPackageName(waitingToLockRe.group(2)); + lock.setClassName(waitingToLockRe.group(3)); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (matches(waitingToLockUnknownRe, text)) { + if (lastJavaFrame != null) { + final LockReason lock = new LockReason(); + lock.setType(LockReason.BLOCKED); + lastJavaFrame.setLock(lock); + combineThreadLocks(rollbarThread, lock); + } + } else if (text.length() == 0 || matches(blankRe, text)) { + break; + } + } + + Collections.reverse(frames);//Todo review later + final StackTrace stackTrace = new StackTrace(frames); + // it's a thread dump + stackTrace.setSnapshot(true); + return stackTrace; + } + + private boolean matches(final Matcher matcher, final String text) { + matcher.reset(text); + return matcher.matches(); + } + + private void combineThreadLocks( + final RollbarThread rollbarThread, final LockReason lockReason) { + Map heldLocks = rollbarThread.getHeldLocks(); + if (heldLocks == null) { + heldLocks = new HashMap<>(); + } + final LockReason prev = heldLocks.get(lockReason.getAddress()); + if (prev != null) { + // higher type prevails as we are tagging thread with the most severe lock reason + prev.setType(Math.max(prev.getType(), lockReason.getType())); + } else { + heldLocks.put(lockReason.getAddress(), new LockReason(lockReason)); + } + rollbarThread.setHeldLocks(heldLocks); + } + + private Long getLong( + final Matcher matcher, final int group, final Long defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + return Long.parseLong(str); + } + } + + private Integer getInteger( + final Matcher matcher, final int group, final Integer defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + return Integer.parseInt(str); + } + } + + private Integer getUInteger( + final Matcher matcher, final int group, final Integer defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + final Integer parsed = Integer.parseInt(str); + return parsed >= 0 ? parsed : defaultValue; + } + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/LooperHandler.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/LooperHandler.java new file mode 100644 index 00000000..c5782201 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/LooperHandler.java @@ -0,0 +1,19 @@ +package com.rollbar.android.anr.watchdog; + +import android.os.Handler; +import android.os.Looper; + +public class LooperHandler { + private final Handler handler; + LooperHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } + + public void post(Runnable runnable) { + handler.post(runnable); + } + + public Thread getThread() { + return handler.getLooper().getThread(); + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java new file mode 100644 index 00000000..1ee86d04 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java @@ -0,0 +1,110 @@ +package com.rollbar.android.anr.watchdog; + +import static android.app.ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING; + +import android.app.ActivityManager; +import android.content.Context; + +import com.rollbar.android.anr.AnrException; +import com.rollbar.android.anr.AnrListener; +import com.rollbar.notifier.provider.Provider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class WatchDog extends Thread { + private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final long POLLING_INTERVAL_MILLIS = 500; + private static final long TIMEOUT_MILLIS = 5000; + private static final String MESSAGE = "Application Not Responding for at least " + TIMEOUT_MILLIS + " ms."; + + private final LooperHandler uiHandler; + private final Provider timeProvider; + private volatile long lastKnownActiveUiTimestampMs = 0; + private final AtomicBoolean reported = new AtomicBoolean(false); + private final Runnable ticker; + private final Context context; + private final AnrListener anrListener; + + public WatchDog( + Context context, + AnrListener anrListener, + Provider timeProvider + ) { + uiHandler = new LooperHandler(); + this.anrListener = anrListener; + this.context = context; + this.timeProvider = timeProvider; + this.ticker = + () -> { + lastKnownActiveUiTimestampMs = timeProvider.provide(); + reported.set(false); + }; + } + + @Override + public void run() { + ticker.run(); + + while (!isInterrupted()) { + uiHandler.post(ticker); + + try { + Thread.sleep(POLLING_INTERVAL_MILLIS); + } catch (InterruptedException e) { + try { + Thread.currentThread().interrupt(); + } catch (SecurityException ignored) { + LOGGER.warn("Failed to interrupt due to SecurityException: {}", e.getMessage()); + return; + } + LOGGER.warn("Interrupted: {}", e.getMessage()); + return; + } + + if (isMainThreadNotHandlerTicker()) { + if (isProcessNotResponding() && reported.compareAndSet(false, true)) { + if (anrListener != null) { + anrListener.onAppNotResponding(makeException()); + } + } + } + } + } + + private AnrException makeException() { + return new AnrException(MESSAGE, uiHandler.getThread()); + } + + private boolean isMainThreadNotHandlerTicker() { + long unresponsiveDurationMs = timeProvider.provide() - lastKnownActiveUiTimestampMs; + return unresponsiveDurationMs > TIMEOUT_MILLIS; + } + + private boolean isProcessNotResponding() { + final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) return true; + + List processesInErrorState = null; + try { + processesInErrorState = activityManager.getProcessesInErrorState(); + } catch (Exception e) { + LOGGER.error("Error getting ActivityManager#getProcessesInErrorState: {}", e.getMessage()); + } + + if (processesInErrorState == null) { + return false; + } + + for (ActivityManager.ProcessErrorStateInfo item : processesInErrorState) { + if (item.condition == NOT_RESPONDING) { + return true; + } + } + + return false; + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java new file mode 100644 index 00000000..611f9937 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java @@ -0,0 +1,70 @@ +package com.rollbar.android.anr.watchdog; + +import android.annotation.SuppressLint; +import android.content.Context; + +import com.rollbar.android.anr.AnrDetector; +import com.rollbar.android.anr.AnrListener; +import com.rollbar.notifier.provider.timestamp.TimestampProvider; + +import java.io.Closeable; +import java.io.IOException; + +public class WatchdogAnrDetector implements AnrDetector, Closeable { + private boolean isClosed = false; + private final Object startLock = new Object(); + + @SuppressLint("StaticFieldLeak") + private static WatchDog watchDog; + private static final Object watchDogLock = new Object(); + + public WatchdogAnrDetector( + Context context, + AnrListener anrListener + ) { + interruptWatchdog(); + createWatchdog(context, anrListener); + } + + @Override + public void init() { + Thread thread = new Thread("WatchdogAnrDetectorThread") { + @Override + public void run() { + super.run(); + synchronized (startLock) { + if (!isClosed) { + watchDog.start(); + } + } + } + }; + thread.setDaemon(true); + thread.start(); + } + + @Override + public void close() throws IOException { + synchronized (startLock) { + isClosed = true; + } + interruptWatchdog(); + } + + private void createWatchdog( + Context context, + AnrListener anrListener + ) { + watchDog = new WatchDog(context, anrListener, new TimestampProvider()); + } + + private void interruptWatchdog() { + synchronized (watchDogLock) { + if (watchDog != null) { + watchDog.interrupt(); + watchDog = null; + } + } + } + +} From 6ae33b7e692e2e4356a3ce83de988692accb5755 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 26 Jan 2025 14:47:59 -0300 Subject: [PATCH 03/13] feat: send non main threads as extra information for api >= 30 --- .../java/com/rollbar/android/Rollbar.java | 1 + .../com/rollbar/android/anr/AnrException.java | 16 +++++++++++-- .../anr/historical/HistoricalAnrDetector.java | 23 +++++++++++++++++-- .../historical/stacktrace/RollbarThread.java | 3 ++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java index 8d9c1374..8fb52d13 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -1096,6 +1096,7 @@ private static void ensureInit(Runnable runnable) { private static void reportANR(AnrException error){ Map map = new HashMap<>(); map.put("TYPE", "ANR"); + map.put("Threads", error.getThreads()); notifier.log(error, map); } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java index 35841bfa..7b717c1a 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrException.java @@ -1,15 +1,27 @@ package com.rollbar.android.anr; +import com.rollbar.android.anr.historical.stacktrace.RollbarThread; + +import java.util.ArrayList; +import java.util.List; + public final class AnrException extends RuntimeException { + private List threads = new ArrayList<>(); + public AnrException(String message, Thread thread) { super(message); setStackTrace(thread.getStackTrace()); } - public AnrException(StackTraceElement[] stackTraceElements) { + public AnrException(StackTraceElement[] mainStackTraceElements, List threads) { super("Application Not Responding"); - setStackTrace(stackTraceElements); + setStackTrace(mainStackTraceElements); + this.threads = threads; + } + + public List getThreads() { + return threads; } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java index d9fbe37a..b20dcb0f 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -80,7 +81,12 @@ private void evaluateLastExitReasons() { continue;//Todo: Do something ? } - anrListener.onAppNotResponding(createAnrException(threads)); + AnrException anrException = createAnrException(threads); + if (anrException == null) { + LOGGER.error("Main thread not found, skipping ANR"); + } else { + anrListener.onAppNotResponding(anrException); + } } catch (Throwable e) { LOGGER.error("Can't parse ANR", e); } @@ -92,7 +98,20 @@ private boolean isNotAnr(ApplicationExitInfo applicationExitInfo) { } private AnrException createAnrException(List threads) { - return new AnrException(threads.get(0).toStackTraceElement()); + List rollbarThreads = new ArrayList<>(); + RollbarThread mainThread = null; + for (RollbarThread thread: threads) { + if (thread.isMain()) { + mainThread = thread; + } else { + rollbarThreads.add(thread); + } + } + + if (mainThread == null) { + return null; + } + return new AnrException(mainThread.toStackTraceElement(), rollbarThreads); } private List getApplicationExitInformation() { diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java index 02376847..3b66aa43 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java @@ -2,6 +2,7 @@ import com.rollbar.api.json.JsonSerializable; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -159,7 +160,7 @@ public String toString() { ", current=" + current + ", daemon=" + daemon + ", main=" + main + - ", stacktrace=" + stacktrace + + ", stacktrace=" + Arrays.toString(toStackTraceElement()) + ", heldLocks=" + heldLocks + ", unknown=" + unknown + '}'; From bc3f27adb4fc0f60dbf302816c854c1103fb0d7a Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 26 Jan 2025 17:55:26 -0300 Subject: [PATCH 04/13] refactor: remove unnecessary code --- .../anr/historical/stacktrace/Lines.java | 9 - .../anr/historical/stacktrace/LockReason.java | 17 -- .../historical/stacktrace/RollbarThread.java | 59 ----- .../anr/historical/stacktrace/StackFrame.java | 233 ------------------ .../anr/historical/stacktrace/StackTrace.java | 38 +-- .../stacktrace/StackTraceFactory.java | 130 ---------- .../stacktrace/ThreadDumpParser.java | 24 +- 7 files changed, 7 insertions(+), 503 deletions(-) delete mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java index ebc42d6a..c0c95b35 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java @@ -1,8 +1,6 @@ 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; @@ -14,13 +12,6 @@ public final class Lines { /** 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 list = new ArrayList<>(); diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java index 8882a36c..db6929ae 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java @@ -14,8 +14,6 @@ public class LockReason implements JsonSerializable { public static final int SLEEPING = 4; public static final int BLOCKED = 8; - public static final int ANY = LOCKED | WAITING | SLEEPING | BLOCKED; - private int type; private String address; private String packageName; @@ -54,29 +52,14 @@ public void setAddress(final String address) { this.address = address; } - - public String getPackageName() { - return packageName; - } - public void setPackageName(final String packageName) { this.packageName = packageName; } - - public String getClassName() { - return className; - } - public void setClassName(final String className) { this.className = className; } - - public Long getThreadId() { - return threadId; - } - public void setThreadId(final Long threadId) { this.threadId = threadId; } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java index 3b66aa43..bbc3fabc 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java @@ -8,12 +8,10 @@ public class RollbarThread implements JsonSerializable { private Long id; - private Integer priority; private String name; private String state; private Boolean crashed; private Boolean current; - private Boolean daemon; private Boolean main; private StackTrace stacktrace; @@ -23,13 +21,6 @@ public StackTraceElement[] toStackTraceElement() { return stacktrace.getStackTraceElements(); } - @SuppressWarnings("unused") - private Map unknown; - - public Long getId() { - return id; - } - public void setId(final Long id) { this.id = id; } @@ -42,45 +33,17 @@ public void setName(final String name) { this.name = name; } - public Boolean isCrashed() { - return crashed; - } - public void setCrashed(final Boolean crashed) { this.crashed = crashed; } - public Boolean isCurrent() { - return current; - } - public void setCurrent(final Boolean current) { this.current = current; } - public StackTrace getStacktrace() { - return stacktrace; - } - public void setStacktrace(final StackTrace stacktrace) { this.stacktrace = stacktrace; } - - public Integer getPriority() { - return priority; - } - - public void setPriority(final Integer priority) { - this.priority = priority; - } - - public Boolean isDaemon() { - return daemon; - } - - public void setDaemon(final Boolean daemon) { - this.daemon = daemon; - } public Boolean isMain() { return main; @@ -90,10 +53,6 @@ public void setMain(final Boolean main) { this.main = main; } - public String getState() { - return state; - } - public void setState(final String state) { this.state = state; } @@ -113,9 +72,6 @@ public Object asJson() { if (id != null) { values.put(JsonKeys.ID, id); } - if (priority != null) { - values.put(JsonKeys.PRIORITY, priority); - } if (name != null) { values.put(JsonKeys.NAME, name); } @@ -128,9 +84,6 @@ public Object asJson() { if (current != null) { values.put(JsonKeys.CURRENT, current); } - if (daemon != null) { - values.put(JsonKeys.DAEMON, daemon); - } if (main != null) { values.put(JsonKeys.MAIN, main); } @@ -140,12 +93,6 @@ public Object asJson() { if (heldLocks != null) { values.put(JsonKeys.HELD_LOCKS, heldLocks); } - if (unknown != null) { - for (String key : unknown.keySet()) { - Object value = unknown.get(key); - values.put(key, value); - } - } return values; } @@ -153,28 +100,22 @@ public Object asJson() { public String toString() { return "RollbarThread{" + "id=" + id + - ", priority=" + priority + ", name='" + name + '\'' + ", state='" + state + '\'' + ", crashed=" + crashed + ", current=" + current + - ", daemon=" + daemon + ", main=" + main + ", stacktrace=" + Arrays.toString(toStackTraceElement()) + ", heldLocks=" + heldLocks + - ", unknown=" + unknown + '}'; } - public static final class JsonKeys { public static final String ID = "id"; - public static final String PRIORITY = "priority"; public static final String NAME = "name"; public static final String STATE = "state"; public static final String CRASHED = "crashed"; public static final String CURRENT = "current"; - public static final String DAEMON = "daemon"; public static final String MAIN = "main"; public static final String STACKTRACE = "stacktrace"; public static final String HELD_LOCKS = "held_locks"; diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java index 369ebcbc..66108dc9 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java @@ -3,156 +3,38 @@ import com.rollbar.api.json.JsonSerializable; import java.util.HashMap; -import java.util.List; import java.util.Map; public class StackFrame implements JsonSerializable { - private List preContext; - - private List postContext; - - private Map vars; - - private List framesOmitted; - private String filename = ""; - private String function = ""; - private String module = ""; - private Integer lineno = 0; - - private Integer colno; - - private String absPath; - - private String contextLine; - - private Boolean inApp; - private String _package; - private Boolean _native; - - private String platform; - - private String imageAddr; - - private String symbolAddr; - - private String instructionAddr; - - private String symbol; - - @SuppressWarnings("unused") - private Map unknown; - public StackTraceElement toStackTraceElement() { return new StackTraceElement(module, function, filename, lineno); } - private String rawFunction; - private LockReason lock; - public List getPreContext() { - return preContext; - } - - public void setPreContext(final List preContext) { - this.preContext = preContext; - } - - public List getPostContext() { - return postContext; - } - - public void setPostContext(final List postContext) { - this.postContext = postContext; - } - - public Map getVars() { - return vars; - } - - public void setVars(final Map vars) { - this.vars = vars; - } - - public List getFramesOmitted() { - return framesOmitted; - } - - public void setFramesOmitted(final List framesOmitted) { - this.framesOmitted = framesOmitted; - } - - public String getFilename() { - return filename; - } - public void setFilename(final String filename) { this.filename = filename; } - public String getFunction() { - return function; - } - public void setFunction(final String function) { this.function = function; } - public String getModule() { - return module; - } - public void setModule(final String module) { this.module = module; } - public Integer getLineno() { - return lineno; - } - public void setLineno(final Integer lineno) { this.lineno = lineno; } - public Integer getColno() { - return colno; - } - - public void setColno(final Integer colno) { - this.colno = colno; - } - - public String getAbsPath() { - return absPath; - } - - public void setAbsPath(final String absPath) { - this.absPath = absPath; - } - - public String getContextLine() { - return contextLine; - } - - public void setContextLine(final String contextLine) { - this.contextLine = contextLine; - } - - public Boolean isInApp() { - return inApp; - } - - public void setInApp(final Boolean inApp) { - this.inApp = inApp; - } - public String getPackage() { return _package; } @@ -161,74 +43,10 @@ public void setPackage(final String _package) { this._package = _package; } - public String getPlatform() { - return platform; - } - - public void setPlatform(final String platform) { - this.platform = platform; - } - - public String getImageAddr() { - return imageAddr; - } - - public void setImageAddr(final String imageAddr) { - this.imageAddr = imageAddr; - } - - public String getSymbolAddr() { - return symbolAddr; - } - - public void setSymbolAddr(final String symbolAddr) { - this.symbolAddr = symbolAddr; - } - - public String getInstructionAddr() { - return instructionAddr; - } - - public void setInstructionAddr(final String instructionAddr) { - this.instructionAddr = instructionAddr; - } - - public Boolean isNative() { - return _native; - } - - public void setNative(final Boolean _native) { - this._native = _native; - } - - public String getRawFunction() { - return rawFunction; - } - - public void setRawFunction(final String rawFunction) { - this.rawFunction = rawFunction; - } - - - public String getSymbol() { - return symbol; - } - - public void setSymbol(final String symbol) { - this.symbol = symbol; - } - - - public LockReason getLock() { - return lock; - } - public void setLock(final LockReason lock) { this.lock = lock; } - // region json - @Override public Object asJson() { Map values = new HashMap<>(); @@ -244,52 +62,12 @@ public Object asJson() { if (lineno != null) { values.put(JsonKeys.LINENO, lineno); } - if (colno != null) { - values.put(JsonKeys.COLNO, colno); - } - if (absPath != null) { - values.put(JsonKeys.ABS_PATH, absPath); - } - if (contextLine != null) { - values.put(JsonKeys.CONTEXT_LINE, contextLine); - } - if (inApp != null) { - values.put(JsonKeys.IN_APP, inApp); - } if (_package != null) { values.put(JsonKeys.PACKAGE, _package); } - if (_native != null) { - values.put(JsonKeys.NATIVE, _native); - } - if (platform != null) { - values.put(JsonKeys.PLATFORM, platform); - } - if (imageAddr != null) { - values.put(JsonKeys.IMAGE_ADDR, imageAddr); - } - if (symbolAddr != null) { - values.put(JsonKeys.SYMBOL_ADDR, symbolAddr); - } - if (instructionAddr != null) { - values.put(JsonKeys.INSTRUCTION_ADDR, instructionAddr); - } - if (rawFunction != null) { - values.put(JsonKeys.RAW_FUNCTION, rawFunction); - } - if (symbol != null) { - values.put(JsonKeys.SYMBOL, symbol); - } if (lock != null) { values.put(JsonKeys.LOCK, lock); } - if (unknown != null) { - for (String key : unknown.keySet()) { - Object value = unknown.get(key); - values.put(key, value); - - } - } return values; } @@ -298,18 +76,7 @@ public static final class JsonKeys { public static final String FUNCTION = "function"; public static final String MODULE = "module"; public static final String LINENO = "lineno"; - public static final String COLNO = "colno"; - public static final String ABS_PATH = "abs_path"; - public static final String CONTEXT_LINE = "context_line"; - public static final String IN_APP = "in_app"; public static final String PACKAGE = "package"; - public static final String NATIVE = "native"; - public static final String PLATFORM = "platform"; - public static final String IMAGE_ADDR = "image_addr"; - public static final String SYMBOL_ADDR = "symbol_addr"; - public static final String INSTRUCTION_ADDR = "instruction_addr"; - public static final String RAW_FUNCTION = "raw_function"; - public static final String SYMBOL = "symbol"; public static final String LOCK = "lock"; } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java index f71ed47c..3e8c101d 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTrace.java @@ -8,15 +8,9 @@ public final class StackTrace implements JsonSerializable { - private List frames; - private Map registers; + private final List frames; private Boolean snapshot; - @SuppressWarnings("unused") - private Map unknown; - - public StackTrace() {} - public StackTrace(final List frames) { this.frames = frames; } @@ -31,26 +25,6 @@ public StackTraceElement[] getStackTraceElements() { return stackTraceElements; } - public List getFrames() { - return frames; - } - - public void setFrames(final List frames) { - this.frames = frames; - } - - public Map getRegisters() { - return registers; - } - - public void setRegisters(final Map registers) { - this.registers = registers; - } - - public Boolean getSnapshot() { - return snapshot; - } - public void setSnapshot(final Boolean snapshot) { this.snapshot = snapshot; } @@ -62,24 +36,14 @@ public Object asJson() { if (frames != null) { values.put(JsonKeys.FRAMES, frames); } - if (registers != null) { - values.put(JsonKeys.REGISTERS, registers); - } if (snapshot != null) { values.put(JsonKeys.SNAPSHOT, snapshot); } - if (unknown != null) { - for (String key : unknown.keySet()) { - Object value = unknown.get(key); - values.put(key, value); - } - } return values; } public static final class JsonKeys { public static final String FRAMES = "frames"; - public static final String REGISTERS = "registers"; public static final String SNAPSHOT = "snapshot"; } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java deleted file mode 100644 index aded2957..00000000 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackTraceFactory.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.rollbar.android.anr.historical.stacktrace; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - - -public class StackTraceFactory { - - private static final int STACKTRACE_FRAME_LIMIT = 100; - - public StackTraceFactory(/*todo pass options?*/) { - } - - public List getStackFrames( - final StackTraceElement[] elements, final boolean includeFrames) { - List StackFrames = null; - - if (elements != null && elements.length > 0) { - StackFrames = new ArrayList<>(); - for (StackTraceElement item : elements) { - if (item != null) { - - final String className = item.getClassName(); - if (!includeFrames && (className.startsWith("com.rollbar."))) { - continue; - } - - final StackFrame StackFrame = new StackFrame(); - StackFrame.setInApp(isInApp(className)); - StackFrame.setModule(className); - StackFrame.setFunction(item.getMethodName()); - StackFrame.setFilename(item.getFileName()); - if (item.getLineNumber() >= 0) { - StackFrame.setLineno(item.getLineNumber()); - } - StackFrame.setNative(item.isNativeMethod()); - StackFrames.add(StackFrame); - - if (StackFrames.size() >= STACKTRACE_FRAME_LIMIT) { - break; - } - } - } - Collections.reverse(StackFrames); - } - - return StackFrames; - } - - /** - * Returns if the className is InApp or not. - * - * @param className the className - * @return true if it is or false otherwise - */ - - public Boolean isInApp(final String className) { - if (className == null || className.isEmpty()) { - return true; - } -/* - final List inAppIncludes = options.getInAppIncludes(); - for (String include : inAppIncludes) { - if (className.startsWith(include)) { - return true; - } - } - - */ -/* - final List inAppExcludes = options.getInAppExcludes(); - for (String exclude : inAppExcludes) { - if (className.startsWith(exclude)) { - return false; - } - } - - */ - - return null; - } - - /** - * Returns the call stack leading to the exception, including in-app frames and excluding rollbar - * and system frames. - * - * @param exception an exception to get the call stack to - * @return a list of rollbar stack frames leading to the exception - */ - - List getInAppCallStack(final Throwable exception) { - final StackTraceElement[] stacktrace = exception.getStackTrace(); - final List frames = getStackFrames(stacktrace, false); - if (frames == null) { - return Collections.emptyList(); - } -/* - final List inAppFrames = - CollectionUtils.filterListEntries(frames, (frame) -> Boolean.TRUE.equals(frame.isInApp())); - - if (!inAppFrames.isEmpty()) { - return inAppFrames; - } - - // if inAppFrames is empty, most likely we're operating over an obfuscated app, just trying to - // fallback to all the frames that are not system frames - return CollectionUtils.filterListEntries( - frames, - (frame) -> { - final String module = frame.getModule(); - boolean isSystemFrame = false; - if (module != null) { - isSystemFrame = - module.startsWith("sun.") - || module.startsWith("java.") - || module.startsWith("android.") - || module.startsWith("com.android."); - } - return !isSystemFrame; - }); - todo crb - */ - return Collections.emptyList(); - } - - public List getInAppCallStack() { - return getInAppCallStack(new Exception()); - } -} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java index 41836583..d8b2ec29 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java @@ -50,14 +50,11 @@ public class ThreadDumpParser { private final boolean isBackground; - private final StackTraceFactory stackTraceFactory; - public ThreadDumpParser(final boolean isBackground) { this.isBackground = isBackground; - this.stackTraceFactory = new StackTraceFactory(); } - + public List parse(final Lines lines) { final List rollbarThreads = new ArrayList<>(); @@ -71,8 +68,7 @@ public List parse(final Lines lines) { return rollbarThreads; } final String text = line.text; - // we only handle managed threads, as unmanaged/not attached do not have the thread id and - // our protocol does not support this case + if (matches(beginManagedThreadRe, text) || matches(beginUnmanagedNativeThreadRe, text)) { lines.rewind(); @@ -91,7 +87,6 @@ private RollbarThread parseThread(final Lines lines) { final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); - // thread attributes if (!lines.hasNext()) { return null; } @@ -109,8 +104,7 @@ private RollbarThread parseThread(final Lines lines) { RollbarThread.setId(threadId); RollbarThread.setName(beginManagedThreadRe.group(1)); final String state = beginManagedThreadRe.group(5); - // sanitizing thread that have more details after their actual state, e.g. - // "Native (still starting up)" <- we just need "Native" here + if (state != null) { if (state.contains(" ")) { RollbarThread.setState(state.substring(0, state.indexOf(' '))); @@ -132,18 +126,16 @@ private RollbarThread parseThread(final Lines lines) { if (threadName != null) { boolean isMain = threadName.equals("main"); RollbarThread.setMain(isMain); - // since it's an ANR, the crashed thread will always be main RollbarThread.setCrashed(isMain); RollbarThread.setCurrent(isMain && !isBackground); } - // thread stacktrace final StackTrace stackTrace = parseStacktrace(lines, RollbarThread); RollbarThread.setStacktrace(stackTrace); return RollbarThread; } - + private StackTrace parseStacktrace( final Lines lines, final RollbarThread rollbarThread) { final List frames = new ArrayList<>(); @@ -190,7 +182,6 @@ private StackTrace parseStacktrace( frame.setFunction(javaRe.group(3)); frame.setFilename(javaRe.group(4)); frame.setLineno(getUInteger(javaRe, 5, null)); - frame.setInApp(stackTraceFactory.isInApp(module)); frames.add(frame); lastJavaFrame = frame; } else if (matches(jniRe, text)) { @@ -200,7 +191,6 @@ private StackTrace parseStacktrace( final String module = String.format("%s.%s", packageName, className); frame.setModule(module); frame.setFunction(jniRe.group(3)); - frame.setInApp(stackTraceFactory.isInApp(module)); frames.add(frame); lastJavaFrame = frame; } else if (matches(lockedRe, text)) { @@ -268,7 +258,6 @@ private StackTrace parseStacktrace( Collections.reverse(frames);//Todo review later final StackTrace stackTrace = new StackTrace(frames); - // it's a thread dump stackTrace.setSnapshot(true); return stackTrace; } @@ -286,7 +275,6 @@ private void combineThreadLocks( } final LockReason prev = heldLocks.get(lockReason.getAddress()); if (prev != null) { - // higher type prevails as we are tagging thread with the most severe lock reason prev.setType(Math.max(prev.getType(), lockReason.getType())); } else { heldLocks.put(lockReason.getAddress(), new LockReason(lockReason)); @@ -307,7 +295,7 @@ private Long getLong( private Integer getInteger( final Matcher matcher, final int group, final Integer defaultValue) { final String str = matcher.group(group); - if (str == null || str.length() == 0) { + if (str == null || str.isEmpty()) { return defaultValue; } else { return Integer.parseInt(str); @@ -317,7 +305,7 @@ private Integer getInteger( private Integer getUInteger( final Matcher matcher, final int group, final Integer defaultValue) { final String str = matcher.group(group); - if (str == null || str.length() == 0) { + if (str == null || str.isEmpty()) { return defaultValue; } else { final Integer parsed = Integer.parseInt(str); From b8cb8f0acc677df09090df17741bef4f399fbb5c Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 26 Jan 2025 17:55:49 -0300 Subject: [PATCH 05/13] refactor: don't launch thread if listener is null --- .../android/anr/historical/HistoricalAnrDetector.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java index b20dcb0f..c86f1752 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java @@ -43,6 +43,10 @@ public HistoricalAnrDetector( @Override public void init() { + if (anrListener == null) { + LOGGER.error("AnrListener is null"); + return; + } Thread thread = new Thread("HistoricalAnrDetectorThread") { @Override public void run() { @@ -56,11 +60,6 @@ public void run() { private void evaluateLastExitReasons() { - if (anrListener == null) { - LOGGER.error("AnrListener is null"); - return; - } - List applicationExitInfoList = getApplicationExitInformation(); if (applicationExitInfoList.isEmpty()) { From 957a0d8f7f40ecb8e483aa209278c1fcc94b705e Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 26 Jan 2025 18:04:13 -0300 Subject: [PATCH 06/13] refactor: remove unnecessary attribute for Line --- .../android/anr/historical/stacktrace/Line.java | 10 ++++++---- .../android/anr/historical/stacktrace/Lines.java | 4 +--- .../anr/historical/stacktrace/ThreadDumpParser.java | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java index c9290930..9764d032 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java @@ -1,11 +1,13 @@ package com.rollbar.android.anr.historical.stacktrace; public final class Line { - public int lineno; - public String text; + private String text; - public Line(final int lineno, final String text) { - this.lineno = lineno; + public Line(final String text) { this.text = text; } + + public String getText() { + return text; + } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java index c0c95b35..15f0a02c 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java @@ -16,11 +16,9 @@ public final class Lines { public static Lines readLines(final BufferedReader in) throws IOException { final ArrayList list = new ArrayList<>(); - int lineno = 0; String text; while ((text = in.readLine()) != null) { - lineno++; - list.add(new Line(lineno, text)); + list.add(new Line(text)); } return new Lines(list); diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java index d8b2ec29..d95054ab 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java @@ -67,7 +67,7 @@ public List parse(final Lines lines) { LOGGER.warn("No line: Internal error while parsing thread dump"); return rollbarThreads; } - final String text = line.text; + final String text = line.getText(); if (matches(beginManagedThreadRe, text) || matches(beginUnmanagedNativeThreadRe, text)) { lines.rewind(); @@ -95,7 +95,7 @@ private RollbarThread parseThread(final Lines lines) { LOGGER.warn("Internal error while parsing thread dump"); return null; } - if (matches(beginManagedThreadRe, line.text)) { + if (matches(beginManagedThreadRe, line.getText())) { Long threadId = getLong(beginManagedThreadRe, 4, null); if (threadId == null) { LOGGER.debug("No thread id in the dump, skipping thread"); @@ -112,7 +112,7 @@ private RollbarThread parseThread(final Lines lines) { RollbarThread.setState(state); } } - } else if (matches(beginUnmanagedNativeThreadRe, line.text)) { + } else if (matches(beginUnmanagedNativeThreadRe, line.getText())) { Long systemThreadId = getLong(beginUnmanagedNativeThreadRe, 3, null); if (systemThreadId == null) { LOGGER.debug("No thread id in the dump, skipping thread"); @@ -159,7 +159,7 @@ private StackTrace parseStacktrace( LOGGER.warn("Internal error while parsing thread dump"); break; } - final String text = line.text; + final String text = line.getText(); if (matches(nativeRe, text)) { final StackFrame frame = new StackFrame(); frame.setPackage(nativeRe.group(1)); From 91ab62271cc1c4208d8577c832032344a521874d Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 2 Feb 2025 15:49:13 -0300 Subject: [PATCH 07/13] refactor: do not create watchdog or run it if anr listener is null --- .../java/com/rollbar/android/anr/watchdog/WatchDog.java | 4 +--- .../rollbar/android/anr/watchdog/WatchdogAnrDetector.java | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java index 1ee86d04..669afd72 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java @@ -67,9 +67,7 @@ public void run() { if (isMainThreadNotHandlerTicker()) { if (isProcessNotResponding() && reported.compareAndSet(false, true)) { - if (anrListener != null) { - anrListener.onAppNotResponding(makeException()); - } + anrListener.onAppNotResponding(makeException()); } } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java index 611f9937..044cad16 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java @@ -28,6 +28,8 @@ public WatchdogAnrDetector( @Override public void init() { + if (watchDog == null) return; + Thread thread = new Thread("WatchdogAnrDetectorThread") { @Override public void run() { @@ -55,7 +57,10 @@ private void createWatchdog( Context context, AnrListener anrListener ) { - watchDog = new WatchDog(context, anrListener, new TimestampProvider()); + if (context == null) return; + if (anrListener == null) return; + + watchDog = new WatchDog(context, anrListener, new LooperHandler(), new TimestampProvider()); } private void interruptWatchdog() { From a0082ffd68a9bd099f9a80c11ea09eb0ec3df83a Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 2 Feb 2025 15:49:33 -0300 Subject: [PATCH 08/13] test: add Tests for Watchdog --- .../android/anr/watchdog/WatchDog.java | 3 +- .../android/anr/watchdog/WatchDogTest.java | 164 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java index 669afd72..50ccbe0d 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java @@ -32,9 +32,10 @@ public final class WatchDog extends Thread { public WatchDog( Context context, AnrListener anrListener, + LooperHandler looperHandler, Provider timeProvider ) { - uiHandler = new LooperHandler(); + uiHandler = looperHandler; this.anrListener = anrListener; this.context = context; this.timeProvider = timeProvider; diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java new file mode 100644 index 00000000..cca1ca3f --- /dev/null +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java @@ -0,0 +1,164 @@ +package com.rollbar.android.anr.watchdog; + +import static android.app.ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.content.Context; + +import com.rollbar.android.anr.AnrException; +import com.rollbar.android.anr.AnrListener; +import com.rollbar.notifier.provider.Provider; +import com.rollbar.notifier.provider.timestamp.TimestampProvider; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +public class WatchDogTest { + private static final long ANR_TIMEOUT_MILLIS = 5000; + + private long currentTimeMs = 0L; + private AnrException anrException; + private final AnrListener anrListener = new AnrListenerFake(); + private final Thread.State blockedState = Thread.State.BLOCKED; + private final StackTraceElement stacktrace = new StackTraceElement("declaringClass", + "methodName", "fileName", 7); + + @Mock + Thread thread; + + @Mock + ActivityManager activityManager; + + @Mock + Context context; + + @Mock + LooperHandler looperHandler; + + private final Provider timeProvider = new TimestampProviderFake(); + + private WatchDog watchDog; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + currentTimeMs = 0; + watchDog = new WatchDog(context, anrListener, looperHandler, timeProvider); + } + + @After + public void tearDown() { + watchDog.interrupt(); + } + + @Test + public void shouldNotDetectAnrWhenTimeOutIsNotSurpassed() throws InterruptedException { + whenWatchdogStart(); + whenAnrTimeOutIsNotSurpassed(); + + thenAnrIsNotDetected(); + } + + @Test + public void shouldDetectAnrWhenMainThreadIsBlockedAndActivityManagerNotAvailable() throws InterruptedException { + givenABlockedThread(); + + whenWatchdogStart(); + whenAnrTimeOutIsSurpassed(); + + thenAnrExceptionIsTheExpected(); + } + + @Test + public void shouldDetectAnrWhenMainThreadIsBlockedAndActivityManagerHasAnr() throws InterruptedException { + givenAnActivityManagerWithAnr(); + givenABlockedThread(); + + whenWatchdogStart(); + whenAnrTimeOutIsSurpassed(); + + thenAnrExceptionIsTheExpected(); + } + + private void thenAnrIsNotDetected() { + assertNull(anrException); + } + + private void thenAnrExceptionIsTheExpected() { + assertNotNull(anrException); + assertEquals(stacktrace.getClassName(), anrException.getStackTrace()[0].getClassName()); + } + + private void whenWatchdogStart() { + watchDog.start(); + } + + private void whenAnrTimeOutIsNotSurpassed() throws InterruptedException { + int iterations = 0; + int maxIterations = 10; //just to prevent infinite execution + + while (iterations < maxIterations) { + iterations++; + currentTimeMs += 1; + defaultSleep(); + } + } + + private void whenAnrTimeOutIsSurpassed() throws InterruptedException { + int iterations = 0; + int maxIterations = 30; //just to prevent infinite execution + + while (anrException == null && iterations < maxIterations) { + iterations++; + currentTimeMs += ANR_TIMEOUT_MILLIS + 1; + defaultSleep(); + } + } + + private void givenAnActivityManagerWithAnr() { + ActivityManager.ProcessErrorStateInfo stateInfo = new ActivityManager.ProcessErrorStateInfo(); + stateInfo.condition = NOT_RESPONDING; + List anrs = new ArrayList<>(); + anrs.add(stateInfo); + + when(context.getSystemService(eq(Context.ACTIVITY_SERVICE))).thenReturn(activityManager); + when(activityManager.getProcessesInErrorState()).thenReturn(anrs); + } + + private void givenABlockedThread() { + StackTraceElement[] stackTraceElements = {stacktrace}; + + when(thread.getState()).thenReturn(blockedState); + when(thread.getStackTrace()).thenReturn(stackTraceElements); + when(looperHandler.getThread()).thenReturn(thread); + } + + private void defaultSleep() throws InterruptedException { + Thread.sleep(20); + } + + private class TimestampProviderFake extends TimestampProvider { + @Override + public Long provide() { + return currentTimeMs; + } + } + + private class AnrListenerFake implements AnrListener { + @Override + public void onAppNotResponding(AnrException error) { + anrException = error; + } + } +} From c69563eef818498f8bc8b4ccc6e1f09fcc2316e1 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 2 Feb 2025 17:09:34 -0300 Subject: [PATCH 09/13] test: remove unused imports --- .../test/java/com/rollbar/android/RollbarTest.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/rollbar-android/src/test/java/com/rollbar/android/RollbarTest.java b/rollbar-android/src/test/java/com/rollbar/android/RollbarTest.java index 0ec880ab..98c65068 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/RollbarTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/RollbarTest.java @@ -3,21 +3,17 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageInfo; import android.os.Bundle; -import android.test.mock.MockPackageManager; import com.rollbar.android.provider.NotifierProvider; import com.rollbar.api.payload.Payload; import com.rollbar.api.payload.data.Data; import com.rollbar.api.payload.data.Level; -import com.rollbar.api.payload.data.Notifier; import com.rollbar.notifier.config.Config; import com.rollbar.notifier.config.ConfigBuilder; import com.rollbar.notifier.config.ConfigProvider; import com.rollbar.notifier.filter.Filter; -import com.rollbar.notifier.provider.Provider; import com.rollbar.notifier.sender.BufferedSender; import com.rollbar.notifier.sender.Sender; import com.rollbar.notifier.sender.SyncSender; @@ -32,21 +28,13 @@ import static org.junit.Assert.*; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.mockito.stubbing.Answer; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; From c3ed23860b86a1ea1130b433769aa3e852f9e3b4 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 10 Feb 2025 00:51:04 -0300 Subject: [PATCH 10/13] test: set parameters as private --- .../com/rollbar/android/anr/watchdog/WatchDogTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java index cca1ca3f..67c67687 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java @@ -35,16 +35,16 @@ public class WatchDogTest { "methodName", "fileName", 7); @Mock - Thread thread; + private Thread thread; @Mock - ActivityManager activityManager; + private ActivityManager activityManager; @Mock - Context context; + private Context context; @Mock - LooperHandler looperHandler; + private LooperHandler looperHandler; private final Provider timeProvider = new TimestampProviderFake(); From dcac7ab618cadbf3fed6ea9b6bff303f93c2ec60 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 10 Feb 2025 01:04:35 -0300 Subject: [PATCH 11/13] test: Add tests for HistoricalAnrDetector --- .../android/anr/AnrDetectorFactory.java | 6 +- .../anr/historical/HistoricalAnrDetector.java | 20 +- .../historical/HistoricalAnrDetectorTest.java | 206 ++++++++++++++++++ 3 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 rollbar-android/src/test/java/com/rollbar/android/anr/historical/HistoricalAnrDetectorTest.java diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java index d29cafb3..93583298 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java @@ -18,10 +18,14 @@ public static AnrDetector create( ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { LOGGER.debug("Creating HistoricalAnrDetector"); - return new HistoricalAnrDetector(context, anrListener); + return new HistoricalAnrDetector(context, anrListener, createHistoricalAnrDetectorLogger()); } else { LOGGER.debug("Creating WatchdogAnrDetector"); return new WatchdogAnrDetector(context, anrListener); } } + + private static Logger createHistoricalAnrDetectorLogger() { + return LoggerFactory.getLogger(HistoricalAnrDetector.class); + } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java index c86f1752..ea86904d 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java @@ -13,7 +13,6 @@ import com.rollbar.android.anr.historical.stacktrace.ThreadDumpParser; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -27,24 +26,25 @@ @SuppressLint("NewApi") // Validated in the Factory public class HistoricalAnrDetector implements AnrDetector { - private final static Logger LOGGER = LoggerFactory.getLogger(HistoricalAnrDetector.class); - + private final Logger logger; private final Context context; private final AnrListener anrListener; ThreadDumpParser threadDumpParser = new ThreadDumpParser(true);//todo remove isBackground public HistoricalAnrDetector( Context context, - AnrListener anrListener + AnrListener anrListener, + Logger logger ) { this.context = context; this.anrListener = anrListener; + this.logger = logger; } @Override public void init() { if (anrListener == null) { - LOGGER.error("AnrListener is null"); + logger.error("AnrListener is null"); return; } Thread thread = new Thread("HistoricalAnrDetectorThread") { @@ -63,7 +63,7 @@ private void evaluateLastExitReasons() { List applicationExitInfoList = getApplicationExitInformation(); if (applicationExitInfoList.isEmpty()) { - LOGGER.debug("Empty ApplicationExitInfo List"); + logger.debug("Empty ApplicationExitInfo List"); return; } @@ -76,18 +76,18 @@ private void evaluateLastExitReasons() { List threads = getThreads(applicationExitInfo); if (threads.isEmpty()) { - LOGGER.warn("Error parsing ANR"); - continue;//Todo: Do something ? + logger.error("Error parsing ANR"); + continue; } AnrException anrException = createAnrException(threads); if (anrException == null) { - LOGGER.error("Main thread not found, skipping ANR"); + logger.error("Main thread not found, skipping ANR"); } else { anrListener.onAppNotResponding(anrException); } } catch (Throwable e) { - LOGGER.error("Can't parse ANR", e); + logger.error("Can't parse ANR", e); } } } diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/historical/HistoricalAnrDetectorTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/historical/HistoricalAnrDetectorTest.java new file mode 100644 index 00000000..f35c73fc --- /dev/null +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/historical/HistoricalAnrDetectorTest.java @@ -0,0 +1,206 @@ +package com.rollbar.android.anr.historical; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.content.Context; + +import com.rollbar.android.anr.AnrListener; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class HistoricalAnrDetectorTest { + + @Mock + private ApplicationExitInfo applicationExitInfo; + + @Mock + private Context context; + + @Mock + private AnrListener anrListener; + + @Mock + private Logger logger; + + @Mock + private ActivityManager activityManager; + + private HistoricalAnrDetector historicalAnrDetector; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + historicalAnrDetector = new HistoricalAnrDetector(context, anrListener, logger); + } + + @Test + public void shouldNotDetectAnrWhenAnrListenerIsNull() throws InterruptedException { + givenAnActivityManagerWithoutExitInfo(); + historicalAnrDetector = new HistoricalAnrDetector(context, null, logger); + + whenDetectorIsExecuted(); + + thenTheListenerIsNeverCalled(); + thenErrorLogMustSay("AnrListener is null"); + } + + @Test + public void shouldNotDetectAnrWhenApplicationExitInfoIsEmpty() throws InterruptedException { + givenAnActivityManagerWithoutExitInfo(); + + whenDetectorIsExecuted(); + + thenTheListenerIsNeverCalled(); + thenDebugLogMustSay("Empty ApplicationExitInfo List"); + } + + @Test + public void shouldNotDetectAnrWhenMainThreadIsNotParsed() throws InterruptedException, IOException { + givenAnActivityManagerWithAnAnr(anrWithoutMainThread()); + + whenDetectorIsExecuted(); + + thenTheListenerIsNeverCalled(); + thenErrorLogMustSay("Main thread not found, skipping ANR"); + } + + @Test + public void shouldDetectAnr() throws InterruptedException, IOException { + givenAnActivityManagerWithAnAnr(anr()); + + whenDetectorIsExecuted(); + + thenTheListenerIsCalled(); + } + + private void whenDetectorIsExecuted() throws InterruptedException { + historicalAnrDetector.init(); + waitForDetectorToRun(); + } + + private void givenAnActivityManagerWithAnAnr(ByteArrayInputStream anr) throws IOException { + setAnr(anr); + setActivityManagerService(); + + List list = new ArrayList<>(); + list.add(applicationExitInfo); + setExitReason(list); + } + + private void givenAnActivityManagerWithoutExitInfo() { + setActivityManagerService(); + setExitReason(new ArrayList<>()); + } + + private void setActivityManagerService() { + when(context.getSystemService(eq(Context.ACTIVITY_SERVICE))).thenReturn(activityManager); + } + + private void setAnr(ByteArrayInputStream anr) throws IOException { + when(applicationExitInfo.getReason()).thenReturn(ApplicationExitInfo.REASON_ANR); + when(applicationExitInfo.getTraceInputStream()).thenReturn(anr); + } + + private void setExitReason(List applicationExitInfos) { + when(activityManager.getHistoricalProcessExitReasons(eq(null), eq(0), eq(0))).thenReturn(applicationExitInfos); + } + + private ByteArrayInputStream anr() { + String string = "\"main\" prio=5 tid=1 Sleeping\n" + + "| group=\"main\" sCount=1 ucsCount=0 flags=1 obj=0x72273478 self=0xb4000077e811ff50\n" + + "| sysTid=20408 nice=-10 cgrp=top-app sched=0/0 handle=0x79e97864f8\n" + + "| state=S schedstat=( 856373236 2319887008 1428 ) utm=74 stm=10 core=0 HZ=100\n" + + "| stack=0x7fd2fc2000-0x7fd2fc4000 stackSize=8188KB\n" + + "| held mutexes=" + + "at java.lang.Thread.sleep(Native method)" + + "- sleeping on <0x0c0f663b> (a java.lang.Object)\n" + + "at java.lang.Thread.sleep(Thread.java:450)\n" + + "- locked <0x0c0f663b> (a java.lang.Object)\n" + + "at java.lang.Thread.sleep(Thread.java:355)\n" + + "at com.rollbar.example.android.MainActivity.clickAction(MainActivity.java:77)\n" + + "at com.rollbar.example.android.MainActivity.access$000(MainActivity.java:14)\n" + + "at com.rollbar.example.android.MainActivity$1$1.onClick(MainActivity.java:34)\n" + + "at android.support.design.widget.Snackbar$1.onClick(Snackbar.java:255)\n" + + "at android.view.View.performClick(View.java:7659)\n" + + "at android.view.View.performClickInternal(View.java:7636)\n" + + "at android.view.View.-$$Nest$mperformClickInternal(unavailable:0)\n" + + "at android.view.View$PerformClick.run(View.java:30156)\n" + + "at android.os.Handler.handleCallback(Handler.java:958)\n" + + "at android.os.Handler.dispatchMessage(Handler.java:99)\n" + + "at android.os.Looper.loopOnce(Looper.java:205)\n" + + "at android.os.Looper.loop(Looper.java:294)\n" + + "at android.app.ActivityThread.main(ActivityThread.java:8177)\n" + + "at java.lang.reflect.Method.invoke(Native method)\n" + + "at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)\n" + + "at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)\n" + + "\"OkHttp ConnectionPool\" daemon prio=5 tid=3 TimedWaiting\n" + + "| group=\"main\" sCount=1 ucsCount=0 flags=1 obj=0x12c4ddc0 self=0xb4000077e8175220\n" + + "| sysTid=20482 nice=0 cgrp=top-app sched=0/0 handle=0x76fd901cb0\n" + + "| state=S schedstat=( 598626 7237000 4 ) utm=0 stm=0 core=0 HZ=100\n" + + "| stack=0x76fd7fe000-0x76fd800000 stackSize=1039KB\n" + + "| held mutexes=\n" + + "at java.lang.Object.wait(Native method)\n" + + "- waiting on <0x06842b17> (a com.android.okhttp.ConnectionPool)\n" + + "at com.android.okhttp.ConnectionPool$1.run(ConnectionPool.java:106)\n" + + "- locked <0x06842b17> (a com.android.okhttp.ConnectionPool)\n" + + "at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)\n" + + "at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)\n" + + "at java.lang.Thread.run(Thread.java:1012)\n"; + return new ByteArrayInputStream(string.getBytes()); + } + + private ByteArrayInputStream anrWithoutMainThread() { + String string = "\"OkHttp ConnectionPool\" daemon prio=5 tid=3 TimedWaiting\n" + + "| group=\"main\" sCount=1 ucsCount=0 flags=1 obj=0x12c4ddc0 self=0xb4000077e8175220\n" + + "| sysTid=20482 nice=0 cgrp=top-app sched=0/0 handle=0x76fd901cb0\n" + + "| state=S schedstat=( 598626 7237000 4 ) utm=0 stm=0 core=0 HZ=100\n" + + "| stack=0x76fd7fe000-0x76fd800000 stackSize=1039KB\n" + + "| held mutexes=\n" + + "at java.lang.Object.wait(Native method)\n" + + "- waiting on <0x06842b17> (a com.android.okhttp.ConnectionPool)\n" + + "at com.android.okhttp.ConnectionPool$1.run(ConnectionPool.java:106)\n" + + "- locked <0x06842b17> (a com.android.okhttp.ConnectionPool)\n" + + "at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)\n" + + "at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)\n" + + "at java.lang.Thread.run(Thread.java:1012)\n"; + return new ByteArrayInputStream(string.getBytes()); + } + + private void waitForDetectorToRun() throws InterruptedException { + for(int i = 0; i<3 ; i++) { + Thread.sleep(50); + } + } + + private void thenTheListenerIsCalled() { + verify(anrListener).onAppNotResponding(any()); + } + + private void thenTheListenerIsNeverCalled() { + verify(anrListener, never()).onAppNotResponding(any()); + } + + private void thenDebugLogMustSay(String logMessage) { + verify(logger, times(1)).debug(logMessage); + } + + private void thenErrorLogMustSay(String logMessage) { + verify(logger, times(1)).error(logMessage); + } +} From 61dfa3343726e0ee1ffbe4f1f2c1e65389cbe429 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 10 Feb 2025 01:17:48 -0300 Subject: [PATCH 12/13] refactor: Initialize ThreadParser as expected --- .../android/anr/historical/HistoricalAnrDetector.java | 10 +++++++--- .../{ThreadDumpParser.java => ThreadParser.java} | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) rename rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/{ThreadDumpParser.java => ThreadParser.java} (99%) diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java index ea86904d..2daf751e 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/HistoricalAnrDetector.java @@ -10,7 +10,7 @@ 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 com.rollbar.android.anr.historical.stacktrace.ThreadParser; import org.slf4j.Logger; @@ -29,7 +29,6 @@ public class HistoricalAnrDetector implements AnrDetector { private final Logger logger; private final Context context; private final AnrListener anrListener; - ThreadDumpParser threadDumpParser = new ThreadDumpParser(true);//todo remove isBackground public HistoricalAnrDetector( Context context, @@ -121,7 +120,12 @@ private List getApplicationExitInformation() { private List getThreads(ApplicationExitInfo applicationExitInfo) throws IOException { Lines lines = getLines(applicationExitInfo); - return threadDumpParser.parse(lines); + ThreadParser threadParser = new ThreadParser(isBackground(applicationExitInfo)); + return threadParser.parse(lines); + } + + private boolean isBackground(ApplicationExitInfo applicationExitInfo) { + return applicationExitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; } private Lines getLines(ApplicationExitInfo applicationExitInfo) throws IOException { diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java similarity index 99% rename from rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java rename to rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java index d95054ab..c97687ec 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadDumpParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java @@ -11,7 +11,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class ThreadDumpParser { +public class ThreadParser { private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); private static final Pattern BEGIN_MANAGED_THREAD_RE = @@ -50,7 +50,7 @@ public class ThreadDumpParser { private final boolean isBackground; - public ThreadDumpParser(final boolean isBackground) { + public ThreadParser(final boolean isBackground) { this.isBackground = isBackground; } From 6c0848e632baa8f824cf116258117820eb4576f4 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 16 Feb 2025 17:54:06 -0300 Subject: [PATCH 13/13] feat: Add AndroidConfiguration to tun on/off ANR detectors implementations --- .../rollbar/android/AndroidConfiguration.java | 38 ++++++++ .../java/com/rollbar/android/Rollbar.java | 88 ++++++++++++++++++- .../rollbar/android/anr/AnrConfiguration.java | 44 ++++++++++ .../android/anr/AnrDetectorFactory.java | 32 ++++++- .../android/anr/watchdog/WatchDog.java | 17 ++-- .../anr/watchdog/WatchdogAnrDetector.java | 12 ++- .../anr/watchdog/WatchdogConfiguration.java | 64 ++++++++++++++ .../android/anr/AnrDetectorFactoryTest.java | 49 +++++++++++ .../android/anr/watchdog/WatchDogTest.java | 9 +- 9 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/AnrConfiguration.java create mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogConfiguration.java create mode 100644 rollbar-android/src/test/java/com/rollbar/android/anr/AnrDetectorFactoryTest.java diff --git a/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java b/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java new file mode 100644 index 00000000..0474da47 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java @@ -0,0 +1,38 @@ +package com.rollbar.android; + +import com.rollbar.android.anr.AnrConfiguration; + +public class AndroidConfiguration { + private final AnrConfiguration anrConfiguration; + + AndroidConfiguration(Builder builder) { + anrConfiguration = builder.anrConfiguration; + } + + public AnrConfiguration getAnrConfiguration() { + return anrConfiguration; + } + + + public static final class Builder { + private AnrConfiguration anrConfiguration; + + Builder() { + anrConfiguration = new AnrConfiguration.Builder().build(); + } + + /** + * The ANR configuration, if this field is null, no ANR would be captured + * @param anrConfiguration the ANR configuration + * @return the builder instance + */ + public Builder setAnrConfiguration(AnrConfiguration anrConfiguration) { + this.anrConfiguration = anrConfiguration; + return this; + } + + public AndroidConfiguration build() { + return new AndroidConfiguration(this); + } + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java index 8fb52d13..c5ecc827 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -29,6 +29,8 @@ import com.rollbar.notifier.sender.queue.DiskQueue; import com.rollbar.notifier.util.ObjectsUtils; +import org.slf4j.LoggerFactory; + import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -76,6 +78,28 @@ public static Rollbar init(Context context) { return init(context, null, null); } + /** + * Initialize the singleton instance of Rollbar. + * Defaults to reading the access token from the manifest, handling uncaught exceptions, and setting + * the environment to production. + * + * @param context Android context to use. + * @param androidConfiguration configuration for Android features. + * @return the managed instance of Rollbar. + */ + public static Rollbar init(Context context, AndroidConfiguration androidConfiguration) { + return init( + context, + null, + DEFAULT_ENVIRONMENT, + DEFAULT_REGISTER_EXCEPTION_HANDLER, + DEFAULT_INCLUDE_LOGCAT, + DEFAULT_CONFIG_PROVIDER, + DEFAULT_SUSPEND_WHEN_NETWORK_IS_UNAVAILABLE, + androidConfiguration + ); + } + /** * Initialize the singleton instance of Rollbar. * @@ -160,20 +184,72 @@ public static Rollbar init(Context context, String accessToken, String environme public static Rollbar init(Context context, String accessToken, String environment, boolean registerExceptionHandler, boolean includeLogcat, ConfigProvider provider, boolean suspendWhenNetworkIsUnavailable) { + return init( + context, + accessToken, + environment, + registerExceptionHandler, + includeLogcat, + provider, + suspendWhenNetworkIsUnavailable, + makeDefaultAndroidConfiguration() + ); + } + + /** + * Initialize the singleton instance of Rollbar. + * + * @param context Android context to use. + * @param accessToken a Rollbar access token with at least post_client_item scope + * @param environment the environment to set for items + * @param registerExceptionHandler whether or not to handle uncaught exceptions. + * @param includeLogcat whether or not to include logcat output with items + * @param provider a configuration provider that can be used to customize the configuration further. + * @param suspendWhenNetworkIsUnavailable if true, sending occurrences will be suspended while the network is unavailable + * @param androidConfiguration configuration for Android features + * @return the managed instance of Rollbar. + */ + public static Rollbar init( + Context context, + String accessToken, + String environment, + boolean registerExceptionHandler, + boolean includeLogcat, + ConfigProvider provider, + boolean suspendWhenNetworkIsUnavailable, + AndroidConfiguration androidConfiguration + ) { if (isInit()) { Log.w(TAG, "Rollbar.init() called when it was already initialized."); // This is likely an activity that was destroyed and recreated, so we need to update it notifier.updateContext(context); } else { notifier = new Rollbar(context, accessToken, environment, registerExceptionHandler, - includeLogcat, provider, DEFAULT_CAPTURE_IP, DEFAULT_MAX_LOGCAT_SIZE, - suspendWhenNetworkIsUnavailable); + includeLogcat, provider, DEFAULT_CAPTURE_IP, DEFAULT_MAX_LOGCAT_SIZE, + suspendWhenNetworkIsUnavailable); } - AnrDetector anrDetector = AnrDetectorFactory.create(context, error -> reportANR(error)); - anrDetector.init(); + + if (androidConfiguration != null && !isInit()) { + initAnrDetector(context, androidConfiguration); + } + return notifier; } + private static void initAnrDetector( + Context context, + AndroidConfiguration androidConfiguration + ) { + AnrDetector anrDetector = AnrDetectorFactory.create( + context, + LoggerFactory.getLogger(AnrDetectorFactory.class), + androidConfiguration.getAnrConfiguration(), + error -> reportANR(error)); + if (anrDetector != null) { + anrDetector.init(); + } + } + private void updateContext(Context context) { if (this.senderFailureStrategy != null) { this.senderFailureStrategy.updateContext(context); @@ -1100,4 +1176,8 @@ private static void reportANR(AnrException error){ notifier.log(error, map); } + private static AndroidConfiguration makeDefaultAndroidConfiguration() { + return new AndroidConfiguration.Builder().build(); + } + } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrConfiguration.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrConfiguration.java new file mode 100644 index 00000000..f9af3916 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrConfiguration.java @@ -0,0 +1,44 @@ +package com.rollbar.android.anr; + +import com.rollbar.android.anr.watchdog.WatchdogConfiguration; + +public class AnrConfiguration { + WatchdogConfiguration watchdogConfiguration; + boolean captureHistoricalAnr; + + public AnrConfiguration(Builder builder) { + this.watchdogConfiguration = builder.watchdogConfiguration; + this.captureHistoricalAnr = builder.captureHistoricalAnr; + } + + public static final class Builder { + private boolean captureHistoricalAnr = true; + private WatchdogConfiguration watchdogConfiguration = new WatchdogConfiguration.Builder().build(); + + /** + * The WatchdogConfiguration configuration, if this field is null, no ANR would be captured. + * By default this feature is on, in build versions < 30. + * @param watchdogConfiguration the Watchdog configuration + * @return the builder instance + */ + public Builder setWatchdogConfiguration(WatchdogConfiguration watchdogConfiguration) { + this.watchdogConfiguration = watchdogConfiguration; + return this; + } + + /** + * A flag to turn on or off the HistoricalAnr detector implementation. + * This implementation is used if the build version is >= 30 + * @param captureHistoricalAnr HistoricalAnrDetector flag + * @return the builder instance + */ + public Builder setCaptureHistoricalAnr(boolean captureHistoricalAnr) { + this.captureHistoricalAnr = captureHistoricalAnr; + return this; + } + + public AnrConfiguration build() { + return new AnrConfiguration(this); + } + } +} diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java index 93583298..0bee60d9 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/AnrDetectorFactory.java @@ -10,18 +10,42 @@ import org.slf4j.LoggerFactory; public class AnrDetectorFactory { - private final static Logger LOGGER = LoggerFactory.getLogger(AnrDetectorFactory.class); public static AnrDetector create( Context context, + Logger logger, + AnrConfiguration anrConfiguration, AnrListener anrListener ) { + if (anrConfiguration == null) { + logger.warn("No ANR configuration"); + return null; + } + if (context == null) { + logger.warn("No context"); + return null; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - LOGGER.debug("Creating HistoricalAnrDetector"); + if (!anrConfiguration.captureHistoricalAnr) { + logger.warn("Historical ANR capture is off"); + return null; + } + + logger.debug("Creating HistoricalAnrDetector"); return new HistoricalAnrDetector(context, anrListener, createHistoricalAnrDetectorLogger()); } else { - LOGGER.debug("Creating WatchdogAnrDetector"); - return new WatchdogAnrDetector(context, anrListener); + if (anrConfiguration.watchdogConfiguration == null) { + logger.warn("No Watchdog configuration"); + return null; + } + + logger.debug("Creating WatchdogAnrDetector"); + return new WatchdogAnrDetector( + context, + anrConfiguration.watchdogConfiguration, + anrListener + ); } } diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java index 50ccbe0d..3847eb91 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchDog.java @@ -17,9 +17,7 @@ public final class WatchDog extends Thread { private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - private static final long POLLING_INTERVAL_MILLIS = 500; - private static final long TIMEOUT_MILLIS = 5000; - private static final String MESSAGE = "Application Not Responding for at least " + TIMEOUT_MILLIS + " ms."; + private static final String MESSAGE = "Application Not Responding for at least %s ms."; private final LooperHandler uiHandler; private final Provider timeProvider; @@ -28,17 +26,20 @@ public final class WatchDog extends Thread { private final Runnable ticker; private final Context context; private final AnrListener anrListener; + private final WatchdogConfiguration watchdogConfiguration; public WatchDog( Context context, AnrListener anrListener, LooperHandler looperHandler, + WatchdogConfiguration watchdogConfiguration, Provider timeProvider ) { uiHandler = looperHandler; this.anrListener = anrListener; this.context = context; this.timeProvider = timeProvider; + this.watchdogConfiguration = watchdogConfiguration; this.ticker = () -> { lastKnownActiveUiTimestampMs = timeProvider.provide(); @@ -54,7 +55,7 @@ public void run() { uiHandler.post(ticker); try { - Thread.sleep(POLLING_INTERVAL_MILLIS); + Thread.sleep(watchdogConfiguration.getPollingIntervalMillis()); } catch (InterruptedException e) { try { Thread.currentThread().interrupt(); @@ -75,12 +76,16 @@ public void run() { } private AnrException makeException() { - return new AnrException(MESSAGE, uiHandler.getThread()); + return new AnrException(createAnrMessage(), uiHandler.getThread()); + } + + private String createAnrMessage() { + return String.format(MESSAGE, watchdogConfiguration.getTimeOutMillis()); } private boolean isMainThreadNotHandlerTicker() { long unresponsiveDurationMs = timeProvider.provide() - lastKnownActiveUiTimestampMs; - return unresponsiveDurationMs > TIMEOUT_MILLIS; + return unresponsiveDurationMs > watchdogConfiguration.getTimeOutMillis(); } private boolean isProcessNotResponding() { diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java index 044cad16..9d8593fd 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogAnrDetector.java @@ -20,10 +20,11 @@ public class WatchdogAnrDetector implements AnrDetector, Closeable { public WatchdogAnrDetector( Context context, + WatchdogConfiguration watchdogConfiguration, AnrListener anrListener ) { interruptWatchdog(); - createWatchdog(context, anrListener); + createWatchdog(context, watchdogConfiguration, anrListener); } @Override @@ -55,12 +56,19 @@ public void close() throws IOException { private void createWatchdog( Context context, + WatchdogConfiguration watchdogConfiguration, AnrListener anrListener ) { if (context == null) return; if (anrListener == null) return; - watchDog = new WatchDog(context, anrListener, new LooperHandler(), new TimestampProvider()); + watchDog = new WatchDog( + context, + anrListener, + new LooperHandler(), + watchdogConfiguration, + new TimestampProvider() + ); } private void interruptWatchdog() { diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogConfiguration.java b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogConfiguration.java new file mode 100644 index 00000000..b16d894f --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/watchdog/WatchdogConfiguration.java @@ -0,0 +1,64 @@ +package com.rollbar.android.anr.watchdog; + +public class WatchdogConfiguration { + private long pollingIntervalMillis; + private long timeOutMillis; + + WatchdogConfiguration(Builder builder) { + pollingIntervalMillis = builder.pollingIntervalMillis; + timeOutMillis = builder.timeOutMillis; + } + + /** + * Returns the Watchdog pooling interval in millis. + * Default is 500 + * + * @return the pooling interval in millis + */ + public long getPollingIntervalMillis() { + return pollingIntervalMillis; + } + + /** + * Returns the ANR timeout in millis. + * Default is 5000, 5 seconds + * + * @return the timeout in millis + */ + public long getTimeOutMillis() { + return timeOutMillis; + } + + public static final class Builder { + private long pollingIntervalMillis = 500; + private long timeOutMillis = 5000; + + /** + * Set the Watchdog pooling interval in millis. + * Default is 500 + * + * @param pollingIntervalMillis timeout in millis + * @return the builder instance + */ + public Builder setPollingIntervalMillis(long pollingIntervalMillis) { + this.pollingIntervalMillis = pollingIntervalMillis; + return this; + } + + /** + * Set the ANR timeout in millis. + * Default is 5000, 5 seconds + * + * @param timeOutMillis timeout in millis + * @return the builder instance + */ + public Builder setTimeOutMillis(long timeOutMillis) { + this.timeOutMillis = timeOutMillis; + return this; + } + + public WatchdogConfiguration build() { + return new WatchdogConfiguration(this); + } + } +} diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/AnrDetectorFactoryTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/AnrDetectorFactoryTest.java new file mode 100644 index 00000000..151100dc --- /dev/null +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/AnrDetectorFactoryTest.java @@ -0,0 +1,49 @@ +package com.rollbar.android.anr; + +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.slf4j.Logger; + +public class AnrDetectorFactoryTest { + @Mock + private Context context; + + @Mock + private AnrListener anrListener; + + @Mock + private Logger logger; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void createShouldReturnNullWhenNoAnrConfigurationIsProvided() { + AnrDetector anrDetector = AnrDetectorFactory.create(context, logger, null, anrListener); + + assertNull(anrDetector); + thenWarningLogMustSay("No ANR configuration"); + } + + @Test + public void createShouldReturnNullWhenNoContextIsProvided() { + AnrDetector anrDetector = AnrDetectorFactory.create(null, logger, new AnrConfiguration.Builder().build(), anrListener); + + assertNull(anrDetector); + thenWarningLogMustSay("No context"); + } + + private void thenWarningLogMustSay(String logMessage) { + verify(logger, times(1)).warn(logMessage); + } +} diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java index 67c67687..667b3d55 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/watchdog/WatchDogTest.java @@ -54,7 +54,13 @@ public class WatchDogTest { public void setup() { MockitoAnnotations.initMocks(this); currentTimeMs = 0; - watchDog = new WatchDog(context, anrListener, looperHandler, timeProvider); + watchDog = new WatchDog( + context, + anrListener, + looperHandler, + new WatchdogConfiguration.Builder().build(), + timeProvider + ); } @After @@ -97,6 +103,7 @@ private void thenAnrIsNotDetected() { private void thenAnrExceptionIsTheExpected() { assertNotNull(anrException); + assertEquals(anrException.getMessage(), "Application Not Responding for at least 5000 ms."); assertEquals(stacktrace.getClassName(), anrException.getStackTrace()[0].getClassName()); }