From 0161c7757bae76930653740e085fbab0470a8229 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 7 Dec 2024 23:52:06 -0300 Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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()); } From da109a7351888e2858644553a25b83dcf4fc5324 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 26 May 2025 02:39:23 -0300 Subject: [PATCH 14/32] refactor: use RollbarThread to send ANR information --- .../java/com/rollbar/android/Rollbar.java | 44 ++++- .../com/rollbar/android/anr/AnrException.java | 8 +- .../anr/historical/HistoricalAnrDetector.java | 30 +--- .../historical/stacktrace/RollbarThread.java | 123 ------------- .../historical/stacktrace/ThreadParser.java | 166 ++++-------------- .../api/payload/data/body/RollbarThread.java | 11 +- .../java/com/rollbar/notifier/Rollbar.java | 18 ++ .../rollbar/notifier/util/BodyFactory.java | 22 +++ .../wrapper/RollbarThrowableWrapper.java | 115 ++++++++---- .../notifier/wrapper/ThrowableWrapper.java | 8 + 10 files changed, 221 insertions(+), 324 deletions(-) delete mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.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 c5ecc827..349135a0 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -17,6 +17,7 @@ import com.rollbar.android.notifier.sender.ConnectionAwareSenderFailureStrategy; import com.rollbar.android.provider.ClientProvider; import com.rollbar.api.payload.data.TelemetryType; +import com.rollbar.api.payload.data.body.RollbarThread; import com.rollbar.notifier.config.ConfigProvider; import com.rollbar.notifier.uncaughtexception.RollbarUncaughtExceptionHandler; import com.rollbar.android.provider.NotifierProvider; @@ -28,6 +29,8 @@ import com.rollbar.notifier.sender.SyncSender; import com.rollbar.notifier.sender.queue.DiskQueue; import com.rollbar.notifier.util.ObjectsUtils; +import com.rollbar.notifier.wrapper.RollbarThrowableWrapper; +import com.rollbar.notifier.wrapper.ThrowableWrapper; import org.slf4j.LoggerFactory; @@ -37,6 +40,7 @@ import java.lang.Thread.UncaughtExceptionHandler; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -227,10 +231,9 @@ public static Rollbar init( notifier = new Rollbar(context, accessToken, environment, registerExceptionHandler, includeLogcat, provider, DEFAULT_CAPTURE_IP, DEFAULT_MAX_LOGCAT_SIZE, suspendWhenNetworkIsUnavailable); - } - - if (androidConfiguration != null && !isInit()) { - initAnrDetector(context, androidConfiguration); + if (androidConfiguration != null) { + initAnrDetector(context, androidConfiguration); + } } return notifier; @@ -919,6 +922,24 @@ public void log(Throwable error, Map custom, Level level) { log(error, custom, null, level); } + /** + * Record an error or message with extra data at the level specified. At least one of `error` or + * `description` must be non-null. If error is null, `description` will be sent as a message. If + * error is non-null, description will be sent as the description of the error. Custom data will + * be attached to message if the error is null. Custom data will extend whatever {@link + * Config#custom} returns. + * + * @param error the error (if any). + * @param custom the custom data (if any). + * @param description the description of the error, or the message to send. + * @param level the level to send it at. + * @param isUncaught whether this data comes from an uncaught exception. + */ + public void log(ThrowableWrapper error, Map custom, String description, + Level level, boolean isUncaught) { + rollbar.log(error, custom, description, level, isUncaught); + } + /** * Log an error at level specified. * @@ -1170,10 +1191,17 @@ 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); + List rollbarThreads = error.getThreads(); + + ThrowableWrapper throwableWrapper; + + if (rollbarThreads == null) { + throwableWrapper = new RollbarThrowableWrapper(error); + } else { + throwableWrapper = new RollbarThrowableWrapper(error, rollbarThreads); + } + + notifier.log(throwableWrapper, new HashMap<>(), "ANR", Level.CRITICAL, false); } private static AndroidConfiguration makeDefaultAndroidConfiguration() { 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 7b717c1a..ec47578e 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,22 +1,20 @@ package com.rollbar.android.anr; -import com.rollbar.android.anr.historical.stacktrace.RollbarThread; +import com.rollbar.api.payload.data.body.RollbarThread; -import java.util.ArrayList; import java.util.List; public final class AnrException extends RuntimeException { - private List threads = new ArrayList<>(); + private List threads; public AnrException(String message, Thread thread) { super(message); setStackTrace(thread.getStackTrace()); } - public AnrException(StackTraceElement[] mainStackTraceElements, List threads) { + public AnrException(List threads) { super("Application Not Responding"); - setStackTrace(mainStackTraceElements); this.threads = 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 2daf751e..768b82c8 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 @@ -9,8 +9,8 @@ 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.ThreadParser; +import com.rollbar.api.payload.data.body.RollbarThread; import org.slf4j.Logger; @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -79,11 +78,10 @@ private void evaluateLastExitReasons() { continue; } - AnrException anrException = createAnrException(threads); - if (anrException == null) { - logger.error("Main thread not found, skipping ANR"); + if (containsMainThread(threads)) { + anrListener.onAppNotResponding(new AnrException(threads)); } else { - anrListener.onAppNotResponding(anrException); + logger.error("Main thread not found, skipping ANR"); } } catch (Throwable e) { logger.error("Can't parse ANR", e); @@ -95,21 +93,13 @@ private boolean isNotAnr(ApplicationExitInfo applicationExitInfo) { return applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR; } - private AnrException createAnrException(List threads) { - List rollbarThreads = new ArrayList<>(); - RollbarThread mainThread = null; + private boolean containsMainThread(List threads) { for (RollbarThread thread: threads) { if (thread.isMain()) { - mainThread = thread; - } else { - rollbarThreads.add(thread); + return true; } } - - if (mainThread == null) { - return null; - } - return new AnrException(mainThread.toStackTraceElement(), rollbarThreads); + return false; } private List getApplicationExitInformation() { @@ -120,14 +110,10 @@ private List getApplicationExitInformation() { private List getThreads(ApplicationExitInfo applicationExitInfo) throws IOException { Lines lines = getLines(applicationExitInfo); - ThreadParser threadParser = new ThreadParser(isBackground(applicationExitInfo)); + ThreadParser threadParser = new ThreadParser(); return threadParser.parse(lines); } - private boolean isBackground(ApplicationExitInfo applicationExitInfo) { - return applicationExitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; - } - private Lines getLines(ApplicationExitInfo applicationExitInfo) throws IOException { byte[] dump = getDumpBytes(Objects.requireNonNull(applicationExitInfo.getTraceInputStream())); return getLines(dump); 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 deleted file mode 100644 index bbc3fabc..00000000 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/RollbarThread.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.rollbar.android.anr.historical.stacktrace; - -import com.rollbar.api.json.JsonSerializable; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public class RollbarThread implements JsonSerializable { - private Long id; - private String name; - private String state; - private Boolean crashed; - private Boolean current; - private Boolean main; - private StackTrace stacktrace; - - private Map heldLocks; - - public StackTraceElement[] toStackTraceElement() { - return stacktrace.getStackTraceElements(); - } - - public void setId(final Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public void setCrashed(final Boolean crashed) { - this.crashed = crashed; - } - - public void setCurrent(final Boolean current) { - this.current = current; - } - - public void setStacktrace(final StackTrace stacktrace) { - this.stacktrace = stacktrace; - } - - public Boolean isMain() { - return main; - } - - public void setMain(final Boolean main) { - this.main = main; - } - - 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 (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 (main != null) { - values.put(JsonKeys.MAIN, main); - } - if (stacktrace != null) { - values.put(JsonKeys.STACKTRACE, stacktrace); - } - if (heldLocks != null) { - values.put(JsonKeys.HELD_LOCKS, heldLocks); - } - return values; - } - - @Override - public String toString() { - return "RollbarThread{" + - "id=" + id + - ", name='" + name + '\'' + - ", state='" + state + '\'' + - ", crashed=" + crashed + - ", current=" + current + - ", main=" + main + - ", stacktrace=" + Arrays.toString(toStackTraceElement()) + - ", heldLocks=" + heldLocks + - '}'; - } - - public static final class JsonKeys { - public static final String ID = "id"; - 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 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/ThreadParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java index c97687ec..63875c5f 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java @@ -1,13 +1,16 @@ package com.rollbar.android.anr.historical.stacktrace; +import com.rollbar.api.payload.data.body.RollbarThread; +import com.rollbar.notifier.util.BodyFactory; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.Deque; import java.util.List; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,17 +50,8 @@ public class ThreadParser { Pattern.compile(" *- waiting to lock an unknown object"); private static final Pattern BLANK_RE = Pattern.compile("\\s+"); - - private final boolean isBackground; - - public ThreadParser(final boolean isBackground) { - this.isBackground = isBackground; - } - - public List parse(final Lines lines) { - final List rollbarThreads = new ArrayList<>(); - + Deque rollbarThreads = new ArrayDeque<>(); final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); @@ -65,24 +59,30 @@ public List parse(final Lines lines) { Line line = lines.next(); if (line == null) { LOGGER.warn("No line: Internal error while parsing thread dump"); - return rollbarThreads; + return new ArrayList<>(rollbarThreads); } final String text = line.getText(); if (matches(beginManagedThreadRe, text) || matches(beginUnmanagedNativeThreadRe, text)) { lines.rewind(); - final RollbarThread rollbarThread = parseThread(lines); + RollbarThread rollbarThread = parseThread(lines); if (rollbarThread != null) { - rollbarThreads.add(rollbarThread); + if (rollbarThread.isMain()) { + rollbarThreads.addFirst(rollbarThread); + } else { + rollbarThreads.addLast(rollbarThread); + } } } } - return rollbarThreads; + return new ArrayList<>(rollbarThreads); } private RollbarThread parseThread(final Lines lines) { - final RollbarThread RollbarThread = new RollbarThread(); + String id = ""; + String name = ""; + String state = ""; final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); @@ -101,62 +101,47 @@ private RollbarThread parseThread(final Lines lines) { 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); + id = threadId.toString(); + name = beginManagedThreadRe.group(1); + state = beginManagedThreadRe.group(5); - if (state != null) { - if (state.contains(" ")) { - RollbarThread.setState(state.substring(0, state.indexOf(' '))); - } else { - RollbarThread.setState(state); - } + if (state != null && state.contains(" ")) { + state = state.substring(0, state.indexOf(' ')); } } 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"); + LOGGER.debug("No system 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); - RollbarThread.setCrashed(isMain); - RollbarThread.setCurrent(isMain && !isBackground); + id = systemThreadId.toString(); + name = beginUnmanagedNativeThreadRe.group(1); } - final StackTrace stackTrace = parseStacktrace(lines, RollbarThread); - RollbarThread.setStacktrace(stackTrace); - return RollbarThread; + StackTrace stackTrace = parseStacktrace(lines); + return new RollbarThread( + name, + id, + "", + state, + new BodyFactory().from(stackTrace.getStackTraceElements()) + ); } - private StackTrace parseStacktrace( - final Lines lines, final RollbarThread rollbarThread) { + private StackTrace parseStacktrace(Lines lines) { 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"); + LOGGER.warn("Internal error while parsing thread dump, no line"); break; } final String text = line.getText(); @@ -166,13 +151,11 @@ private StackTrace parseStacktrace( 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); @@ -183,7 +166,6 @@ private StackTrace parseStacktrace( frame.setFilename(javaRe.group(4)); frame.setLineno(getUInteger(javaRe, 5, null)); frames.add(frame); - lastJavaFrame = frame; } else if (matches(jniRe, text)) { final StackFrame frame = new StackFrame(); final String packageName = jniRe.group(1); @@ -192,71 +174,12 @@ private StackTrace parseStacktrace( frame.setModule(module); frame.setFunction(jniRe.group(3)); 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)) { + } else if (text.isEmpty() || matches(blankRe, text)) { break; } } - Collections.reverse(frames);//Todo review later + Collections.reverse(frames); final StackTrace stackTrace = new StackTrace(frames); stackTrace.setSnapshot(true); return stackTrace; @@ -267,21 +190,6 @@ private boolean matches(final Matcher matcher, final String 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) { - 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); diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java index efff8f00..77ee2217 100644 --- a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java @@ -32,7 +32,7 @@ public RollbarThread(Thread thread, Group group) { this.group = group; } - private RollbarThread( + public RollbarThread( String name, String id, String priority, @@ -92,6 +92,15 @@ public String getName() { return name; } + /** + * Getter. + * + * @return true if this represents the main thread. + */ + public boolean isMain() { + return isMain; + } + @Override public Object asJson() { Map values = new HashMap<>(); diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java b/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java index e59b95e3..b9265745 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java @@ -10,6 +10,7 @@ import com.rollbar.notifier.util.ObjectsUtils; import com.rollbar.notifier.wrapper.ThrowableWrapper; import java.lang.Thread.UncaughtExceptionHandler; +import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -611,6 +612,23 @@ public void log( this.log(wrapThrowable(error, thread), custom, description, level, isUncaught); } + /** + * Record an error or message with extra data at the level specified. At least one of `error` or + * `description` must be non-null. If error is null, `description` will be sent as a message. If + * error is non-null, description will be sent as the description of the error. Custom data will + * be attached to message if the error is null. Custom data will extend whatever {@link + * Config#custom} returns. + * + * @param error the error (if any). + */ + public void log(ThrowableWrapper error) { + try { + process(error, new HashMap<>(), null, Level.CRITICAL, false); + } catch (Exception e) { + LOGGER.error("Error while processing payload to send to Rollbar: {}", e); + } + } + /** * Record an error or message with extra data at the level specified. At least one of `error` or * `description` must be non-null. If error is null, `description` will be sent as a message. If diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java b/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java index 103d3ed0..a72098ac 100755 --- a/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/util/BodyFactory.java @@ -87,6 +87,22 @@ public RollbarThread from( return new RollbarThread(thread, new Group(traceChain)); } + /** + * Builds a Group from an Array of StackTraceElement. + * + * @param stackTraceElements the stack trace elements. + * @return the Group. + */ + public Group from( + StackTraceElement[] stackTraceElements + ) { + if (stackTraceElements == null) { + return null; + } + TraceChain traceChain = traceChain(stackTraceElements); + return new Group(traceChain); + } + private Body from( ThrowableWrapper throwableWrapper, String description, @@ -105,6 +121,12 @@ private List makeRollbarThreads( if (throwableWrapper == null) { return null; } + + List wrapperRollbarThreads = throwableWrapper.getRollbarThreads(); + if (wrapperRollbarThreads != null && !wrapperRollbarThreads.isEmpty()) { + return wrapperRollbarThreads; + } + Map allStackTraces = throwableWrapper.getAllStackTraces(); if (allStackTraces == null) { return null; diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java index 7eee3ba2..718da90b 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -26,6 +27,8 @@ public class RollbarThrowableWrapper implements ThrowableWrapper { private final Map allStackTraces; + private final List rollbarThreads; + /** * Constructor. * @@ -39,7 +42,8 @@ public RollbarThrowableWrapper(Throwable throwable) { getInnerThrowableWrapper(throwable), throwable, Thread.currentThread(), - captureAllStackTraces(Thread.currentThread()) + captureAllStackTraces(Thread.currentThread()), + null ); } @@ -56,7 +60,29 @@ public RollbarThrowableWrapper(Throwable throwable, Thread thread) { getInnerThrowableWrapper(throwable), throwable, thread, - captureAllStackTraces(thread) + captureAllStackTraces(thread), + null + ); + } + + /** + * Constructor. + * + * @param throwable the throwable. + */ + public RollbarThrowableWrapper( + Throwable throwable, + List rollbarThreads + ) { + this( + throwable.getClass().getName(), + throwable.getMessage(), + throwable.getStackTrace(), + getInnerThrowableWrapper(throwable), + throwable, + null, + null, + rollbarThreads ); } @@ -75,42 +101,11 @@ private RollbarThrowableWrapper(Throwable throwable, boolean unusedParameter) { getInnerThrowableWrapper(throwable), throwable, null, + null, null ); } - private static ThrowableWrapper getInnerThrowableWrapper(Throwable throwable) { - if (throwable.getCause() == null) { - return null; - } - return new RollbarThrowableWrapper(throwable.getCause(), false); - } - - private static Map captureAllStackTraces(Thread thread) { - if (thread == null) { - return null; - } - - return filter(thread, Thread.getAllStackTraces()); - } - - private static Map filter( - Thread thread, Map allStackTraces - ) { - HashMap filteredStackTraces = new HashMap<>(); - - for (Map.Entry entry : allStackTraces.entrySet()) { - Thread entryThread = entry.getKey(); - - if (!thread.equals(entryThread)) { - filteredStackTraces.put(entryThread, entry.getValue()); - } - } - - return filteredStackTraces; - } - /** * Constructor. * @@ -125,7 +120,16 @@ public RollbarThrowableWrapper( StackTraceElement[] stackTraceElements, ThrowableWrapper cause ) { - this(className, message, stackTraceElements, cause, null, null, null); + this( + className, + message, + stackTraceElements, + cause, + null, + null, + null, + null + ); } private RollbarThrowableWrapper( @@ -135,7 +139,8 @@ private RollbarThrowableWrapper( ThrowableWrapper cause, Throwable throwable, Thread thread, - Map allStackTraces + Map allStackTraces, + List rollbarThreads ) { this.className = className; this.message = message; @@ -144,6 +149,39 @@ private RollbarThrowableWrapper( this.throwable = throwable; this.rollbarThread = new BodyFactory().from(thread); this.allStackTraces = allStackTraces; + this.rollbarThreads = rollbarThreads; + } + + private static ThrowableWrapper getInnerThrowableWrapper(Throwable throwable) { + if (throwable.getCause() == null) { + return null; + } + return new RollbarThrowableWrapper(throwable.getCause(), false); + } + + private static Map captureAllStackTraces(Thread thread) { + if (thread == null) { + return null; + } + + return filter(thread, Thread.getAllStackTraces()); + } + + private static Map filter( + Thread thread, Map allStackTraces + ) { + HashMap filteredStackTraces = new HashMap<>(); + + for (Map.Entry entry : allStackTraces.entrySet()) { + Thread entryThread = entry.getKey(); + + if (!thread.equals(entryThread)) { + filteredStackTraces.put(entryThread, entry.getValue()); + } + } + + return filteredStackTraces; } @Override @@ -181,6 +219,11 @@ public Map getAllStackTraces() { return allStackTraces; } + @Override + public List getRollbarThreads() { + return rollbarThreads; + } + @Override public String toString() { return "RollbarThrowableWrapper{" diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/ThrowableWrapper.java b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/ThrowableWrapper.java index 480de92e..bfc9434f 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/ThrowableWrapper.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/ThrowableWrapper.java @@ -2,6 +2,7 @@ import com.rollbar.api.payload.data.body.RollbarThread; +import java.util.List; import java.util.Map; /** @@ -57,4 +58,11 @@ public interface ThrowableWrapper { * @return the map. */ Map getAllStackTraces(); + + /** + * Get a list of the RollbarThreads for this error. + * + * @return the RollbarThreads. + */ + List getRollbarThreads(); } From c1a5df04c0f73d4d6b17be4c7479a5d2d3469afa Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 28 May 2025 13:32:36 -0300 Subject: [PATCH 15/32] feat(HistoricalAnrDetector): save last anr timestamp --- .../anr/historical/HistoricalAnrDetector.java | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 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 768b82c8..a9ac7f7b 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 @@ -17,14 +17,21 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; import java.util.Objects; @SuppressLint("NewApi") // Validated in the Factory public class HistoricalAnrDetector implements AnrDetector { + private static final String TIMESTAMP_FILE = "rollbar-anr-timestamp"; private final Logger logger; private final Context context; private final AnrListener anrListener; @@ -45,20 +52,80 @@ public void init() { logger.error("AnrListener is null"); return; } + + Long lastAnrTimestamp = getLastAnrTimestamp(); + if (lastAnrTimestamp == null) { + return; + } + Thread thread = new Thread("HistoricalAnrDetectorThread") { @Override public void run() { super.run(); - evaluateLastExitReasons(); + evaluateLastExitReasons(lastAnrTimestamp); } }; thread.setDaemon(true); thread.start(); } + private Long getLastAnrTimestamp() { + File file = new File(context.getCacheDir().getAbsolutePath(), TIMESTAMP_FILE); + if (isNotValid(file)) { + logger.error("Can't retrieve last ANR timestamp"); + return null; + } + + try { + return getLastAnrTimestamp(file); + } catch (IOException e) { + logger.error("Error reading last ANR timestamp"); + return null; + } + } + + private boolean isNotValid(File file) { + + if (file != null && !file.exists()) { + createFile(file); + } + + return file == null || !file.exists() || !file.isFile() || !file.canRead(); + } + + private void createFile(File file) { + try { + file.createNewFile(); + } catch (IOException e) { + logger.error("can't create file"); + } + } - private void evaluateLastExitReasons() { + private Long getLastAnrTimestamp(File file) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + if ((line = br.readLine()) != null) { + stringBuilder.append(line); + } + while ((line = br.readLine()) != null) { + stringBuilder.append("\n").append(line); + } + } catch (FileNotFoundException ignored) { + return null; + } + String content = stringBuilder.toString(); + + try { + return (content.equals("null") || content.isBlank()) ? 0L : Long.parseLong(content.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private void evaluateLastExitReasons(Long lastAnrReportedTimestamp) { List applicationExitInfoList = getApplicationExitInformation(); + Long newestAnrTimestamp = lastAnrReportedTimestamp; if (applicationExitInfoList.isEmpty()) { logger.debug("Empty ApplicationExitInfo List"); @@ -70,6 +137,11 @@ private void evaluateLastExitReasons() { continue; } + long anrTimestamp = applicationExitInfo.getTimestamp(); + if (anrTimestamp <= lastAnrReportedTimestamp) { + logger.warn("ANR already sent"); + continue; + } try { List threads = getThreads(applicationExitInfo); @@ -80,6 +152,10 @@ private void evaluateLastExitReasons() { if (containsMainThread(threads)) { anrListener.onAppNotResponding(new AnrException(threads)); + if (anrTimestamp > newestAnrTimestamp) { + newestAnrTimestamp = anrTimestamp; + saveAnrTimestamp(anrTimestamp); + } } else { logger.error("Main thread not found, skipping ANR"); } @@ -89,6 +165,21 @@ private void evaluateLastExitReasons() { } } + private void saveAnrTimestamp(long timestamp) { + File file = new File(context.getCacheDir(), TIMESTAMP_FILE); + if (isNotValid(file)) { + logger.error("Can't save last ANR timestamp"); + return; + } + + try (final OutputStream outputStream = Files.newOutputStream(file.toPath())) { + outputStream.write(String.valueOf(timestamp).getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); + } catch (Throwable e) { + logger.error("Error writing the ANR marker to the disk", e); + } + } + private boolean isNotAnr(ApplicationExitInfo applicationExitInfo) { return applicationExitInfo.getReason() != ApplicationExitInfo.REASON_ANR; } From a349bf3b98e66c2c41157588b65910b4b87deaa0 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:04:09 -0300 Subject: [PATCH 16/32] build: downgrade compile and target sdk to 30, to test CI --- examples/rollbar-android/build.gradle | 4 ++-- rollbar-android/build.gradle | 4 ++-- .../rollbar/android/anr/historical/HistoricalAnrDetector.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index 6271b056..7d323c62 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 33 + compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.rollbar.example.android" minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 33 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/rollbar-android/build.gradle b/rollbar-android/build.gradle index b81741da..bfb777fc 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 33 + compileSdkVersion 30 buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+ defaultConfig { minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 33 + targetSdkVersion 30 consumerProguardFiles 'proguard-rules.pro' manifestPlaceholders = [notifierVersion: VERSION_NAME] } 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 a9ac7f7b..ec009fb8 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 @@ -117,7 +117,7 @@ private Long getLastAnrTimestamp(File file) throws IOException { String content = stringBuilder.toString(); try { - return (content.equals("null") || content.isBlank()) ? 0L : Long.parseLong(content.trim()); + return (content.equals("null") || content.trim().isEmpty()) ? 0L : Long.parseLong(content.trim()); } catch (NumberFormatException ignored) { return null; } From c8e8c778047b994a9902257e8bf552692688e99f Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:18:33 -0300 Subject: [PATCH 17/32] test: delete tests to validate if CI completes --- .../historical/HistoricalAnrDetectorTest.java | 27 ------------------- 1 file changed, 27 deletions(-) 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 index f35c73fc..0f40c8cd 100644 --- 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 @@ -60,34 +60,7 @@ public void shouldNotDetectAnrWhenAnrListenerIsNull() throws InterruptedExceptio 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(); From 1c23f8bc929b07e5969696444087f53207bc1a04 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:30:11 -0300 Subject: [PATCH 18/32] fix: lint --- .../api/payload/data/body/RollbarThread.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java index 77ee2217..7cc33eca 100644 --- a/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/data/body/RollbarThread.java @@ -32,6 +32,14 @@ public RollbarThread(Thread thread, Group group) { this.group = group; } + /** + * Constructor. + * @param name the name of the thread. + * @param id the id of the thread. + * @param priority the priority of the thread. + * @param state the state of the thread. + * @param group the Group of trace chains. + */ public RollbarThread( String name, String id, @@ -101,6 +109,10 @@ public boolean isMain() { return isMain; } + private boolean isMain(String string) { + return "main".equals(string); + } + @Override public Object asJson() { Map values = new HashMap<>(); @@ -156,10 +168,6 @@ public int hashCode() { return Objects.hash(name, id, priority, state, isMain, group); } - private boolean isMain(String string) { - return "main".equals(string); - } - /** * Builder class for {@link RollbarThread RollbarThread}. */ From 5c94db718670e900dac99527b615e1b2db8900bc Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:36:27 -0300 Subject: [PATCH 19/32] fix(RollbarThrowableWrapper): lint --- .../wrapper/RollbarThrowableWrapper.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java index 718da90b..1cf188f5 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java @@ -71,18 +71,18 @@ public RollbarThrowableWrapper(Throwable throwable, Thread thread) { * @param throwable the throwable. */ public RollbarThrowableWrapper( - Throwable throwable, - List rollbarThreads - ) { + Throwable throwable, + List rollbarThreads + ) { this( - throwable.getClass().getName(), - throwable.getMessage(), - throwable.getStackTrace(), - getInnerThrowableWrapper(throwable), - throwable, - null, - null, - rollbarThreads + throwable.getClass().getName(), + throwable.getMessage(), + throwable.getStackTrace(), + getInnerThrowableWrapper(throwable), + throwable, + null, + null, + rollbarThreads ); } From 978c8313a267dd0440592daae099391a1397bc32 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:40:47 -0300 Subject: [PATCH 20/32] fix(RollbarThrowableWrapper): lint --- .../com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java index 1cf188f5..87acf700 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/wrapper/RollbarThrowableWrapper.java @@ -71,8 +71,8 @@ public RollbarThrowableWrapper(Throwable throwable, Thread thread) { * @param throwable the throwable. */ public RollbarThrowableWrapper( - Throwable throwable, - List rollbarThreads + Throwable throwable, + List rollbarThreads ) { this( throwable.getClass().getName(), From 70002605f77bc3d4d485a031935936c2e4159a67 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 02:45:48 -0300 Subject: [PATCH 21/32] chore: add new method information in revapi file --- .palantir/revapi.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml index 24ca338e..f29e24ec 100644 --- a/.palantir/revapi.yml +++ b/.palantir/revapi.yml @@ -45,3 +45,8 @@ acceptedBreaks: justification: "This is a binary compatible change, which could only break custom\ \ implementations of our config interfaces, but those interfaces are not meant\ \ to be implemented by users" + - code: "java.method.addedToInterface" + new: "method java.util.List com.rollbar.notifier.wrapper.ThrowableWrapper::getRollbarThreads()" + justification: "This is a binary compatible change, which could only break custom\ + \ implementations of our config interfaces, but those interfaces are not meant\ + \ to be implemented by users" From 073a470c1de97994beaca31d0f4a2989d453816b Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 29 May 2025 03:12:11 -0300 Subject: [PATCH 22/32] build(android-example): migrate old libs to AndroidX --- examples/rollbar-android/build.gradle | 4 +- .../rollbar/example/android/MainActivity.java | 10 ++-- .../src/main/res/layout/activity_main.xml | 50 +++++++++---------- gradle.properties | 2 + 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index 7d323c62..d947cc8b 100644 --- a/examples/rollbar-android/build.gradle +++ b/examples/rollbar-android/build.gradle @@ -38,9 +38,9 @@ dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation 'com.android.support:design:27.1.1' + implementation 'com.google.android.material:material:1.0.0' implementation 'org.slf4j:slf4j-android:1.7.25' testImplementation group: 'junit', name: 'junit', version: '4.12' } diff --git a/examples/rollbar-android/src/main/java/com/rollbar/example/android/MainActivity.java b/examples/rollbar-android/src/main/java/com/rollbar/example/android/MainActivity.java index 7a5bf067..f9e6c0b3 100644 --- a/examples/rollbar-android/src/main/java/com/rollbar/example/android/MainActivity.java +++ b/examples/rollbar-android/src/main/java/com/rollbar/example/android/MainActivity.java @@ -1,14 +1,16 @@ package com.rollbar.example.android; import android.os.Bundle; -import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.Menu; import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; import com.rollbar.android.Rollbar; public class MainActivity extends AppCompatActivity { diff --git a/examples/rollbar-android/src/main/res/layout/activity_main.xml b/examples/rollbar-android/src/main/res/layout/activity_main.xml index e840ffb3..e97823c0 100644 --- a/examples/rollbar-android/src/main/res/layout/activity_main.xml +++ b/examples/rollbar-android/src/main/res/layout/activity_main.xml @@ -1,34 +1,34 @@ - + + + android:theme="@style/AppTheme.AppBarOverlay"> - - - + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + app:popupTheme="@style/AppTheme.PopupOverlay"/> - + - + - + diff --git a/gradle.properties b/gradle.properties index b8b04e9e..22599278 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,8 @@ VERSION_NAME=2.0.0 GROUP=com.rollbar +android.useAndroidX=true + POM_DESCRIPTION=For connecting your applications built on the JVM to Rollbar for Error Reporting POM_URL=https://github.com/rollbar/rollbar-java POM_SCM_URL=https://github.com/rollbar/rollbar-java From 257f73803943fcef7417cb6ae4a6dc9214ebbb62 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 31 May 2025 23:51:58 -0300 Subject: [PATCH 23/32] build: update compile and target sdk to 32 --- examples/rollbar-android/build.gradle | 4 ++-- rollbar-android/build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index d947cc8b..2eb11678 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 30 + compileSdkVersion 32 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.rollbar.example.android" minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 30 + targetSdkVersion 32 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/rollbar-android/build.gradle b/rollbar-android/build.gradle index bfb777fc..24996094 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 30 + compileSdkVersion 32 buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+ defaultConfig { minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 30 + targetSdkVersion 32 consumerProguardFiles 'proguard-rules.pro' manifestPlaceholders = [notifierVersion: VERSION_NAME] } From d044fa3533b428341da64522280bc4167be9b3ad Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 31 May 2025 23:56:32 -0300 Subject: [PATCH 24/32] build: downgrade compile and target sdk to 31 --- examples/rollbar-android/build.gradle | 4 ++-- rollbar-android/build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index 2eb11678..60e76d55 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 32 + compileSdkVersion 31 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.rollbar.example.android" minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 32 + targetSdkVersion 31 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/rollbar-android/build.gradle b/rollbar-android/build.gradle index 24996094..e453144b 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 32 + compileSdkVersion 31 buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+ defaultConfig { minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 32 + targetSdkVersion 31 consumerProguardFiles 'proguard-rules.pro' manifestPlaceholders = [notifierVersion: VERSION_NAME] } From ceac2ac5c0fce89d7225ce14c658302a825c5fd4 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 31 May 2025 23:58:40 -0300 Subject: [PATCH 25/32] Revert "test: delete tests to validate if CI completes" This reverts commit c8e8c778047b994a9902257e8bf552692688e99f. --- .../historical/HistoricalAnrDetectorTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 index 0f40c8cd..f35c73fc 100644 --- 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 @@ -60,7 +60,34 @@ public void shouldNotDetectAnrWhenAnrListenerIsNull() throws InterruptedExceptio 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(); From 6feb4fc4173e04f355fc8dc3f5d2ea6f9cc52c57 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 12 Jun 2025 02:07:54 -0300 Subject: [PATCH 26/32] refactor: set HistoricalAnrDetector File for Anr timestamps by constructor to improve testing --- .../android/anr/AnrDetectorFactory.java | 13 ++++- .../anr/historical/HistoricalAnrDetector.java | 23 ++++---- .../historical/HistoricalAnrDetectorTest.java | 56 ++++++++++++++++++- 3 files changed, 77 insertions(+), 15 deletions(-) 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 0bee60d9..a4930e22 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 @@ -9,6 +9,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; + public class AnrDetectorFactory { public static AnrDetector create( @@ -33,7 +35,12 @@ public static AnrDetector create( } logger.debug("Creating HistoricalAnrDetector"); - return new HistoricalAnrDetector(context, anrListener, createHistoricalAnrDetectorLogger()); + return new HistoricalAnrDetector( + context, + anrListener, + createAnrTimeStampFile(context), + createHistoricalAnrDetectorLogger() + ); } else { if (anrConfiguration.watchdogConfiguration == null) { logger.warn("No Watchdog configuration"); @@ -49,6 +56,10 @@ public static AnrDetector create( } } + private static File createAnrTimeStampFile(Context context) { + return new File(context.getCacheDir(), "rollbar-anr-timestamp"); + } + 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 ec009fb8..5bae2267 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 @@ -31,18 +31,20 @@ @SuppressLint("NewApi") // Validated in the Factory public class HistoricalAnrDetector implements AnrDetector { - private static final String TIMESTAMP_FILE = "rollbar-anr-timestamp"; private final Logger logger; private final Context context; private final AnrListener anrListener; + private final File file; public HistoricalAnrDetector( Context context, AnrListener anrListener, + File file, Logger logger ) { this.context = context; this.anrListener = anrListener; + this.file = file; this.logger = logger; } @@ -53,7 +55,7 @@ public void init() { return; } - Long lastAnrTimestamp = getLastAnrTimestamp(); + Long lastAnrTimestamp = getLastAnrTimestamp(file); if (lastAnrTimestamp == null) { return; } @@ -62,22 +64,21 @@ public void init() { @Override public void run() { super.run(); - evaluateLastExitReasons(lastAnrTimestamp); + evaluateLastExitReasons(file, lastAnrTimestamp); } }; thread.setDaemon(true); thread.start(); } - private Long getLastAnrTimestamp() { - File file = new File(context.getCacheDir().getAbsolutePath(), TIMESTAMP_FILE); + private Long getLastAnrTimestamp(File file) { if (isNotValid(file)) { logger.error("Can't retrieve last ANR timestamp"); return null; } try { - return getLastAnrTimestamp(file); + return readLastAnrTimestamp(file); } catch (IOException e) { logger.error("Error reading last ANR timestamp"); return null; @@ -85,7 +86,6 @@ private Long getLastAnrTimestamp() { } private boolean isNotValid(File file) { - if (file != null && !file.exists()) { createFile(file); } @@ -101,7 +101,7 @@ private void createFile(File file) { } } - private Long getLastAnrTimestamp(File file) throws IOException { + private Long readLastAnrTimestamp(File file) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (BufferedReader br = new BufferedReader(new FileReader(file))) { String line; @@ -123,7 +123,7 @@ private Long getLastAnrTimestamp(File file) throws IOException { } } - private void evaluateLastExitReasons(Long lastAnrReportedTimestamp) { + private void evaluateLastExitReasons(File file, Long lastAnrReportedTimestamp) { List applicationExitInfoList = getApplicationExitInformation(); Long newestAnrTimestamp = lastAnrReportedTimestamp; @@ -154,7 +154,7 @@ private void evaluateLastExitReasons(Long lastAnrReportedTimestamp) { anrListener.onAppNotResponding(new AnrException(threads)); if (anrTimestamp > newestAnrTimestamp) { newestAnrTimestamp = anrTimestamp; - saveAnrTimestamp(anrTimestamp); + saveAnrTimestamp(file, anrTimestamp); } } else { logger.error("Main thread not found, skipping ANR"); @@ -165,8 +165,7 @@ private void evaluateLastExitReasons(Long lastAnrReportedTimestamp) { } } - private void saveAnrTimestamp(long timestamp) { - File file = new File(context.getCacheDir(), TIMESTAMP_FILE); + private void saveAnrTimestamp(File file, long timestamp) { if (isNotValid(file)) { logger.error("Can't save last ANR timestamp"); return; 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 index f35c73fc..d95f6f23 100644 --- 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 @@ -20,7 +20,10 @@ import org.slf4j.Logger; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -32,6 +35,8 @@ public class HistoricalAnrDetectorTest { @Mock private Context context; + private File file; + @Mock private AnrListener anrListener; @@ -43,16 +48,19 @@ public class HistoricalAnrDetectorTest { private HistoricalAnrDetector historicalAnrDetector; + private final static long ANR_TIMESTAMP = 10L; + @Before public void setup() { MockitoAnnotations.initMocks(this); - historicalAnrDetector = new HistoricalAnrDetector(context, anrListener, logger); + createTemporalFile(); + historicalAnrDetector = new HistoricalAnrDetector(context, anrListener, file, logger); } @Test public void shouldNotDetectAnrWhenAnrListenerIsNull() throws InterruptedException { givenAnActivityManagerWithoutExitInfo(); - historicalAnrDetector = new HistoricalAnrDetector(context, null, logger); + historicalAnrDetector = new HistoricalAnrDetector(context, null, file, logger); whenDetectorIsExecuted(); @@ -80,6 +88,27 @@ public void shouldNotDetectAnrWhenMainThreadIsNotParsed() throws InterruptedExce thenErrorLogMustSay("Main thread not found, skipping ANR"); } + @Test + public void shouldDoNothingIfFileForAnrTimeStampsIsNull() throws InterruptedException, IOException { + givenAnActivityManagerWithAnAnr(anrWithoutMainThread()); + historicalAnrDetector = new HistoricalAnrDetector(context, anrListener, null, logger); + + whenDetectorIsExecuted(); + + thenTheListenerIsNeverCalled(); + thenErrorLogMustSay("Can't retrieve last ANR timestamp"); + } + + @Test + public void shouldNotSendAnrIfItHasAlreadyBeenSent() throws InterruptedException, IOException { + givenAnActivityManagerWithAnAnr(anr()); + givenAnAlreadySentAnr(); + + whenDetectorIsExecuted(); + + thenWarningLogMustSay("ANR already sent"); + } + @Test public void shouldDetectAnr() throws InterruptedException, IOException { givenAnActivityManagerWithAnAnr(anr()); @@ -113,10 +142,19 @@ private void setActivityManagerService() { } private void setAnr(ByteArrayInputStream anr) throws IOException { + givenAnAnrNotSent(); when(applicationExitInfo.getReason()).thenReturn(ApplicationExitInfo.REASON_ANR); when(applicationExitInfo.getTraceInputStream()).thenReturn(anr); } + private void givenAnAnrNotSent() { + when(applicationExitInfo.getTimestamp()).thenReturn(ANR_TIMESTAMP + 1); + } + + private void givenAnAlreadySentAnr() { + when(applicationExitInfo.getTimestamp()).thenReturn(ANR_TIMESTAMP); + } + private void setExitReason(List applicationExitInfos) { when(activityManager.getHistoricalProcessExitReasons(eq(null), eq(0), eq(0))).thenReturn(applicationExitInfos); } @@ -182,6 +220,16 @@ private ByteArrayInputStream anrWithoutMainThread() { return new ByteArrayInputStream(string.getBytes()); } + private void createTemporalFile() { + try { + Path tempPath = Files.createTempFile("rollbar-anr-timestamp", ".txt"); + Files.write(tempPath, ("" + ANR_TIMESTAMP).getBytes()); + file = tempPath.toFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void waitForDetectorToRun() throws InterruptedException { for(int i = 0; i<3 ; i++) { Thread.sleep(50); @@ -200,6 +248,10 @@ private void thenDebugLogMustSay(String logMessage) { verify(logger, times(1)).debug(logMessage); } + private void thenWarningLogMustSay(String logMessage) { + verify(logger, times(1)).warn(logMessage); + } + private void thenErrorLogMustSay(String logMessage) { verify(logger, times(1)).error(logMessage); } From 20e087605b80df229e60ed177c25966fccbd2584 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 12 Jun 2025 02:08:40 -0300 Subject: [PATCH 27/32] build: downgrade compile and target sdk to 30 --- examples/rollbar-android/build.gradle | 4 ++-- rollbar-android/build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/rollbar-android/build.gradle b/examples/rollbar-android/build.gradle index 60e76d55..d947cc8b 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 31 + compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.rollbar.example.android" minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 31 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/rollbar-android/build.gradle b/rollbar-android/build.gradle index e453144b..bfb777fc 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 31 + compileSdkVersion 30 buildToolsVersion '30.0.3' // Going above here requires bumping the AGP to version 4+ defaultConfig { minSdkVersion 21 // FIXME: Pending further discussion //noinspection ExpiredTargetSdkVersion - targetSdkVersion 31 + targetSdkVersion 30 consumerProguardFiles 'proguard-rules.pro' manifestPlaceholders = [notifierVersion: VERSION_NAME] } From 9edd8b8a91f1d9a65f3e54c1244eaa5eb0374a96 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 16 Jun 2025 01:27:58 -0300 Subject: [PATCH 28/32] refactor: remove LockReason --- .../anr/historical/stacktrace/LockReason.java | 112 ------------------ .../anr/historical/stacktrace/StackFrame.java | 10 -- 2 files changed, 122 deletions(-) delete mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java 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 deleted file mode 100644 index db6929ae..00000000 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/LockReason.java +++ /dev/null @@ -1,112 +0,0 @@ -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; - - 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 void setPackageName(final String packageName) { - this.packageName = packageName; - } - - public void setClassName(final String className) { - this.className = className; - } - - 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/StackFrame.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/StackFrame.java index 66108dc9..a675798c 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 @@ -17,8 +17,6 @@ public StackTraceElement toStackTraceElement() { return new StackTraceElement(module, function, filename, lineno); } - private LockReason lock; - public void setFilename(final String filename) { this.filename = filename; } @@ -43,10 +41,6 @@ public void setPackage(final String _package) { this._package = _package; } - public void setLock(final LockReason lock) { - this.lock = lock; - } - @Override public Object asJson() { Map values = new HashMap<>(); @@ -65,9 +59,6 @@ public Object asJson() { if (_package != null) { values.put(JsonKeys.PACKAGE, _package); } - if (lock != null) { - values.put(JsonKeys.LOCK, lock); - } return values; } @@ -77,6 +68,5 @@ public static final class JsonKeys { public static final String MODULE = "module"; public static final String LINENO = "lineno"; public static final String PACKAGE = "package"; - public static final String LOCK = "lock"; } } From caf072faddd74a03471ab17300a658f6d0fb3d31 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 16 Jun 2025 01:49:03 -0300 Subject: [PATCH 29/32] refactor: simplify getStackTraceElements method --- .../android/anr/historical/stacktrace/StackTrace.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 3e8c101d..f34d6d21 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 @@ -17,10 +17,8 @@ public StackTrace(final List frames) { public StackTraceElement[] getStackTraceElements() { StackTraceElement[] stackTraceElements = new StackTraceElement[frames.size()]; - int element = 0; - for (StackFrame frame : frames) { - stackTraceElements[element] = frame.toStackTraceElement(); - element++; + for (int i = 0; i < frames.size(); i++) { + stackTraceElements[i] = frames.get(i).toStackTraceElement(); } return stackTraceElements; } From afdbac4f6218970a842cbcfaef02334fd0b1862e Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 16 Jun 2025 02:16:19 -0300 Subject: [PATCH 30/32] refactor: remove Line and Lines, use a List instead --- .../anr/historical/HistoricalAnrDetector.java | 17 ++++-- .../anr/historical/stacktrace/Line.java | 13 ----- .../anr/historical/stacktrace/Lines.java | 55 ------------------- .../historical/stacktrace/ThreadParser.java | 33 +++++------ 4 files changed, 29 insertions(+), 89 deletions(-) delete mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java delete mode 100644 rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java 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 5bae2267..ff7f5d53 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 @@ -8,7 +8,6 @@ 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.ThreadParser; import com.rollbar.api.payload.data.body.RollbarThread; @@ -26,6 +25,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -199,18 +199,25 @@ private List getApplicationExitInformation() { } private List getThreads(ApplicationExitInfo applicationExitInfo) throws IOException { - Lines lines = getLines(applicationExitInfo); + List lines = getLines(applicationExitInfo); ThreadParser threadParser = new ThreadParser(); return threadParser.parse(lines); } - private Lines getLines(ApplicationExitInfo applicationExitInfo) throws IOException { + private List 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 List getLines(byte[] dump) throws IOException { + List list = new ArrayList<>(); + try (BufferedReader bufferedReader = toBufferReader(dump)) { + String text; + while ((text = bufferedReader.readLine()) != null) { + list.add(text); + } + } + return list; } private BufferedReader toBufferReader(byte[] dump) { 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 deleted file mode 100644 index 9764d032..00000000 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Line.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.rollbar.android.anr.historical.stacktrace; - -public final class Line { - private String text; - - 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 deleted file mode 100644 index 15f0a02c..00000000 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/Lines.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.rollbar.android.anr.historical.stacktrace; - -import java.io.BufferedReader; -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 BufferedReader in) throws IOException { - final ArrayList list = new ArrayList<>(); - - String text; - while ((text = in.readLine()) != null) { - list.add(new Line(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/ThreadParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java index 63875c5f..9e4eea8f 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.Deque; import java.util.List; +import java.util.ListIterator; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,23 +51,24 @@ public class ThreadParser { Pattern.compile(" *- waiting to lock an unknown object"); private static final Pattern BLANK_RE = Pattern.compile("\\s+"); - public List parse(final Lines lines) { + public List parse(List lines) { + ListIterator iterator = lines.listIterator(); + Deque rollbarThreads = new ArrayDeque<>(); final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); - while (lines.hasNext()) { - Line line = lines.next(); + while (iterator.hasNext()) { + String line = iterator.next(); if (line == null) { LOGGER.warn("No line: Internal error while parsing thread dump"); return new ArrayList<>(rollbarThreads); } - final String text = line.getText(); - if (matches(beginManagedThreadRe, text) || matches(beginUnmanagedNativeThreadRe, text)) { - lines.rewind(); + if (matches(beginManagedThreadRe, line) || matches(beginUnmanagedNativeThreadRe, line)) { + iterator.previous(); - RollbarThread rollbarThread = parseThread(lines); + RollbarThread rollbarThread = parseThread(iterator); if (rollbarThread != null) { if (rollbarThread.isMain()) { rollbarThreads.addFirst(rollbarThread); @@ -79,7 +81,7 @@ public List parse(final Lines lines) { return new ArrayList<>(rollbarThreads); } - private RollbarThread parseThread(final Lines lines) { + private RollbarThread parseThread(ListIterator lines) { String id = ""; String name = ""; String state = ""; @@ -90,12 +92,12 @@ private RollbarThread parseThread(final Lines lines) { if (!lines.hasNext()) { return null; } - final Line line = lines.next(); - if (line == null) { + String text = lines.next(); + if (text == null) { LOGGER.warn("Internal error while parsing thread dump"); return null; } - if (matches(beginManagedThreadRe, line.getText())) { + if (matches(beginManagedThreadRe, text)) { Long threadId = getLong(beginManagedThreadRe, 4, null); if (threadId == null) { LOGGER.debug("No thread id in the dump, skipping thread"); @@ -108,7 +110,7 @@ private RollbarThread parseThread(final Lines lines) { if (state != null && state.contains(" ")) { state = state.substring(0, state.indexOf(' ')); } - } else if (matches(beginUnmanagedNativeThreadRe, line.getText())) { + } else if (matches(beginUnmanagedNativeThreadRe, text)) { Long systemThreadId = getLong(beginUnmanagedNativeThreadRe, 3, null); if (systemThreadId == null) { LOGGER.debug("No system thread id in the dump, skipping thread"); @@ -129,7 +131,7 @@ private RollbarThread parseThread(final Lines lines) { } - private StackTrace parseStacktrace(Lines lines) { + private StackTrace parseStacktrace(ListIterator lines) { final List frames = new ArrayList<>(); final Matcher nativeRe = NATIVE_RE.matcher(""); @@ -139,12 +141,11 @@ private StackTrace parseStacktrace(Lines lines) { final Matcher blankRe = BLANK_RE.matcher(""); while (lines.hasNext()) { - final Line line = lines.next(); - if (line == null) { + String text = lines.next(); + if (text == null) { LOGGER.warn("Internal error while parsing thread dump, no line"); break; } - final String text = line.getText(); if (matches(nativeRe, text)) { final StackFrame frame = new StackFrame(); frame.setPackage(nativeRe.group(1)); From a4b1d3cdcd04fe03798f64483752356a8aea5149 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 16 Jun 2025 02:21:40 -0300 Subject: [PATCH 31/32] refactor: remove LockReason regex patterns --- .../anr/historical/stacktrace/ThreadParser.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java index 9e4eea8f..b0649eef 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java +++ b/rollbar-android/src/main/java/com/rollbar/android/anr/historical/stacktrace/ThreadParser.java @@ -34,21 +34,6 @@ public class ThreadParser { 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+"); public List parse(List lines) { From 54c855001b598d01a6ed6811ea65b3efd34c1d8e Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 16 Jun 2025 03:14:21 -0300 Subject: [PATCH 32/32] refactor: improve test coverage --- .../anr/historical/stacktrace/StackFrame.java | 23 +++--- .../anr/historical/stacktrace/StackTrace.java | 14 ++-- .../historical/HistoricalAnrDetectorTest.java | 3 + .../historical/stacktrace/StackFrameTest.java | 71 +++++++++++++++++++ 4 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 rollbar-android/src/test/java/com/rollbar/android/anr/historical/stacktrace/StackFrameTest.java 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 a675798c..6212b8df 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 @@ -6,6 +6,11 @@ import java.util.Map; public class StackFrame implements JsonSerializable { + private static final String MODULE_KEY = "module"; + private static final String PACKAGE_KEY = "package"; + private static final String FILENAME_KEY = "filename"; + private static final String FUNCTION_KEY = "function"; + private static final String LINE_NUMBER_KEY = "lineno"; private String filename = ""; private String function = ""; @@ -45,28 +50,20 @@ public void setPackage(final String _package) { public Object asJson() { Map values = new HashMap<>(); if (filename != null) { - values.put(JsonKeys.FILENAME, filename); + values.put(FILENAME_KEY, filename); } if (function != null) { - values.put(JsonKeys.FUNCTION, function); + values.put(FUNCTION_KEY, function); } if (module != null) { - values.put(JsonKeys.MODULE, module); + values.put(MODULE_KEY, module); } if (lineno != null) { - values.put(JsonKeys.LINENO, lineno); + values.put(LINE_NUMBER_KEY, lineno); } if (_package != null) { - values.put(JsonKeys.PACKAGE, _package); + values.put(PACKAGE_KEY, _package); } 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 PACKAGE = "package"; - } } 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 f34d6d21..2a89d680 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 @@ -7,11 +7,13 @@ import java.util.Map; public final class StackTrace implements JsonSerializable { + private static final String FRAMES_KEY = "frames"; + private static final String SNAPSHOT_KEY = "snapshot"; private final List frames; private Boolean snapshot; - public StackTrace(final List frames) { + public StackTrace(final List frames) { this.frames = frames; } @@ -23,7 +25,7 @@ public StackTraceElement[] getStackTraceElements() { return stackTraceElements; } - public void setSnapshot(final Boolean snapshot) { + public void setSnapshot(final Boolean snapshot) { this.snapshot = snapshot; } @@ -32,16 +34,12 @@ public Object asJson() { Map values = new HashMap<>(); if (frames != null) { - values.put(JsonKeys.FRAMES, frames); + values.put(FRAMES_KEY, frames); } if (snapshot != null) { - values.put(JsonKeys.SNAPSHOT, snapshot); + values.put(SNAPSHOT_KEY, snapshot); } return values; } - public static final class JsonKeys { - public static final String FRAMES = "frames"; - public static final String SNAPSHOT = "snapshot"; - } } 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 index d95f6f23..77c70d20 100644 --- 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 @@ -211,6 +211,9 @@ private ByteArrayInputStream anrWithoutMainThread() { "| stack=0x76fd7fe000-0x76fd800000 stackSize=1039KB\n" + "| held mutexes=\n" + "at java.lang.Object.wait(Native method)\n" + + "native: #5 com.example.library 7fa123456789 libsomething (libfoo.so+1234) (funcName+56)\n"+ + "#9 my.module.name 7f1234abcde0 libbar.so (doStuff)\n"+ + " \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" + diff --git a/rollbar-android/src/test/java/com/rollbar/android/anr/historical/stacktrace/StackFrameTest.java b/rollbar-android/src/test/java/com/rollbar/android/anr/historical/stacktrace/StackFrameTest.java new file mode 100644 index 00000000..2cbe77ff --- /dev/null +++ b/rollbar-android/src/test/java/com/rollbar/android/anr/historical/stacktrace/StackFrameTest.java @@ -0,0 +1,71 @@ +package com.rollbar.android.anr.historical.stacktrace; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.util.HashMap; + +public class StackFrameTest { + private static final String MODULE = "any module"; + private static final String PACKAGE = "any package"; + private static final String FILENAME = "any filename"; + private static final String FUNCTION = "any function"; + private static final int LINE_NO = 20; + + @Test + public void jsonRepresentationShouldBeTheExpected() { + StackFrame stackFrame = givenAStackFrame(); + + HashMap json = getJson(stackFrame); + + assertEquals(expectedJson(), json); + } + + @Test + public void jsonRepresentationShouldBeAnEmptyMapIfAllPropertiesAreNull() { + StackFrame stackFrame = givenAStackFrameWithNullProperties(); + + HashMap json = getJson(stackFrame); + + assertEquals(new HashMap<>(), json); + } + + @SuppressWarnings("unchecked") + private HashMap getJson(StackFrame stackFrame) { + return (HashMap) stackFrame.asJson(); + } + + private StackFrame givenAStackFrame() { + StackFrame stackFrame = new StackFrame(); + stackFrame.setModule(MODULE); + stackFrame.setPackage(PACKAGE); + stackFrame.setFilename(FILENAME); + stackFrame.setFunction(FUNCTION); + stackFrame.setLineno(LINE_NO); + return stackFrame; + } + + private HashMap expectedJson() { + HashMap json = new HashMap<>(); + + json.put("module", MODULE); + json.put("package", PACKAGE); + json.put("filename", FILENAME); + json.put("function", FUNCTION); + json.put("lineno", LINE_NO); + + return json; + } + + private StackFrame givenAStackFrameWithNullProperties() { + StackFrame stackFrame = new StackFrame(); + stackFrame.setModule(null); + stackFrame.setPackage(null); + stackFrame.setFilename(null); + stackFrame.setFunction(null); + stackFrame.setLineno(null); + return stackFrame; + } + +}