From 4fdb86b44df03d796cc7b71cc9348086b79e54c7 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 20 Apr 2026 18:29:10 -0300 Subject: [PATCH 1/9] feat(android): capture logcat output as telemetry events --- .../rollbar/android/AndroidConfiguration.java | 41 ++++ .../android/LogcatTelemetryCapture.java | 198 ++++++++++++++++++ .../java/com/rollbar/android/Rollbar.java | 36 ++++ .../android/LogcatTelemetryCaptureTest.java | 147 +++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java create mode 100644 rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.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 index 8bf625f7..aed9087f 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java +++ b/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java @@ -1,14 +1,19 @@ package com.rollbar.android; import com.rollbar.android.anr.AnrConfiguration; +import com.rollbar.api.payload.data.Level; public class AndroidConfiguration { private final AnrConfiguration anrConfiguration; private final boolean mustCaptureNavigationEvents; + private final boolean mustCaptureLogsAsTelemetry; + private final Level minimumLogCaptureLevel; AndroidConfiguration(Builder builder) { anrConfiguration = builder.anrConfiguration; mustCaptureNavigationEvents = builder.mustCaptureNavigationEvents; + mustCaptureLogsAsTelemetry = builder.mustCaptureLogsAsTelemetry; + minimumLogCaptureLevel = builder.minimumLogCaptureLevel; } public AnrConfiguration getAnrConfiguration() { @@ -19,10 +24,20 @@ public boolean mustCaptureNavigationEvents() { return mustCaptureNavigationEvents; } + public boolean mustCaptureLogsAsTelemetry() { + return mustCaptureLogsAsTelemetry; + } + + public Level getMinimumLogCaptureLevel() { + return minimumLogCaptureLevel; + } + public static final class Builder { private AnrConfiguration anrConfiguration; private boolean mustCaptureNavigationEvents = true; + private boolean mustCaptureLogsAsTelemetry = false; + private Level minimumLogCaptureLevel = Level.WARNING; public Builder() { anrConfiguration = new AnrConfiguration.Builder().build(); @@ -49,6 +64,32 @@ public Builder captureNewActivityTelemetryEvents(boolean mustCaptureNavigationEv return this; } + /** + * Enable or disable automatic capture of Android log output as telemetry events. + * When enabled, logs emitted via {@code android.util.Log} (and any other source written to + * logcat from this app's UID, including third-party libraries) at or above the configured + * minimum level are recorded as manual telemetry events with + * {@link com.rollbar.api.payload.data.Source#CLIENT}. + * Default is disabled. + * @param mustCaptureLogsAsTelemetry if automatic capture must be enabled or disabled. + * @return the builder instance + */ + public Builder captureLogsAsTelemetry(boolean mustCaptureLogsAsTelemetry) { + this.mustCaptureLogsAsTelemetry = mustCaptureLogsAsTelemetry; + return this; + } + + /** + * Minimum log level to capture as telemetry when {@link #captureLogsAsTelemetry(boolean)} + * is enabled. Default is {@link Level#WARNING}. + * @param minimumLogCaptureLevel the minimum level (inclusive) to capture. + * @return the builder instance + */ + public Builder minimumLogCaptureLevel(Level minimumLogCaptureLevel) { + this.minimumLogCaptureLevel = minimumLogCaptureLevel; + return this; + } + public AndroidConfiguration build() { return new AndroidConfiguration(this); } diff --git a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java new file mode 100644 index 00000000..9b6dde05 --- /dev/null +++ b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java @@ -0,0 +1,198 @@ +package com.rollbar.android; + +import android.util.Log; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LogcatTelemetryCapture { + + // threadtime format: "MM-dd HH:mm:ss.SSS PID TID L Tag: message" + private static final Pattern LOGCAT_LINE_PATTERN = Pattern.compile( + "^\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+\\d+\\s+\\d+\\s+([VDIWEF])\\s+(.+?):\\s(.*)$" + ); + + private final TelemetryEventTracker tracker; + private final Level minimumLevel; + private final String selfTag; + private final ProcessFactory processFactory; + + private Thread thread; + private Process process; + private volatile boolean running; + + LogcatTelemetryCapture( + TelemetryEventTracker tracker, + Level minimumLevel, + String selfTag + ) { + this(tracker, minimumLevel, selfTag, defaultProcessFactory()); + } + + LogcatTelemetryCapture( + TelemetryEventTracker tracker, + Level minimumLevel, + String selfTag, + ProcessFactory processFactory + ) { + this.tracker = tracker; + this.minimumLevel = minimumLevel != null ? minimumLevel : Level.WARNING; + this.selfTag = selfTag; + this.processFactory = processFactory; + } + + synchronized void start() { + if (running) { + return; + } + try { + this.process = processFactory.start(logcatPriorityFor(this.minimumLevel)); + } catch (IOException e) { + Log.w(Rollbar.TAG, "Failed to start logcat telemetry capture", e); + return; + } + running = true; + thread = new Thread(new Runnable() { + @Override + public void run() { + readLoop(); + } + }, "rollbar-logcat-telemetry"); + thread.setDaemon(true); + thread.start(); + } + + synchronized void stop() { + if (!running) { + return; + } + running = false; + if (process != null) { + process.destroy(); + process = null; + } + if (thread != null) { + thread.interrupt(); + thread = null; + } + } + + private void readLoop() { + Process currentProcess = this.process; + if (currentProcess == null) { + return; + } + BufferedReader reader = new BufferedReader( + new InputStreamReader(currentProcess.getInputStream(), Charset.forName("UTF-8"))); + try { + String line; + while (running && (line = reader.readLine()) != null) { + processLine(line); + } + } catch (IOException e) { + // Process died or was destroyed — expected on stop(). + } finally { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + + void processLine(String line) { + if (line == null) { + return; + } + Matcher matcher = LOGCAT_LINE_PATTERN.matcher(line); + if (!matcher.matches()) { + return; + } + + String priority = matcher.group(1); + String tag = matcher.group(2).trim(); + String message = matcher.group(3); + + if (selfTag != null && selfTag.equals(tag)) { + return; + } + + Level level = mapPriorityToLevel(priority); + if (level == null) { + return; + } + if (level.level() < minimumLevel.level()) { + return; + } + + try { + tracker.recordManualEventFor(level, Source.CLIENT, message); + } catch (Exception e) { + // Never let a broken tracker kill the reader thread. + } + } + + static Level mapPriorityToLevel(String priority) { + if (priority == null || priority.isEmpty()) { + return null; + } + switch (priority.charAt(0)) { + case 'V': + case 'D': + return Level.DEBUG; + case 'I': + return Level.INFO; + case 'W': + return Level.WARNING; + case 'E': + return Level.ERROR; + case 'F': + return Level.CRITICAL; + default: + return null; + } + } + + static String logcatPriorityFor(Level level) { + if (level == null) { + return "W"; + } + switch (level) { + case DEBUG: + return "D"; + case INFO: + return "I"; + case WARNING: + return "W"; + case ERROR: + return "E"; + case CRITICAL: + return "F"; + default: + return "W"; + } + } + + interface ProcessFactory { + Process start(String priorityFilter) throws IOException; + } + + private static ProcessFactory defaultProcessFactory() { + return new ProcessFactory() { + @Override + public Process start(String priorityFilter) throws IOException { + return new ProcessBuilder( + "logcat", "-v", "threadtime", "*:" + priorityFilter) + .redirectErrorStream(true) + .start(); + } + }; + } +} 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 02811fe1..2200fbdf 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -67,6 +67,7 @@ public class Rollbar implements Closeable { private final ConnectionAwareSenderFailureStrategy senderFailureStrategy; private com.rollbar.notifier.Rollbar rollbar; + private LogcatTelemetryCapture logcatTelemetryCapture; private static Rollbar notifier; private final int versionCode; @@ -236,6 +237,7 @@ public static Rollbar init( if (androidConfiguration != null) { initAnrDetector(context, androidConfiguration); initAutomaticCaptureOfNavigationTelemetryEvents(context, androidConfiguration); + initAutomaticCaptureOfLogTelemetryEvents(androidConfiguration); } } @@ -277,12 +279,21 @@ public static Rollbar init(Context context, ConfigProvider provider) { AndroidConfiguration androidConfiguration = makeDefaultAndroidConfiguration(); initAnrDetector(context, androidConfiguration); initAutomaticCaptureOfNavigationTelemetryEvents(context, androidConfiguration); + initAutomaticCaptureOfLogTelemetryEvents(androidConfiguration); } return notifier; } @Override public void close() throws IOException { + if (logcatTelemetryCapture != null) { + try { + logcatTelemetryCapture.stop(); + } catch (Exception e) { + Log.w(TAG, "Error stopping logcat telemetry capture", e); + } + logcatTelemetryCapture = null; + } if (rollbar != null) { try { rollbar.close(false); @@ -1202,6 +1213,31 @@ private static void initAutomaticCaptureOfNavigationTelemetryEvents( } } + private static void initAutomaticCaptureOfLogTelemetryEvents( + AndroidConfiguration androidConfiguration + ) { + if (!androidConfiguration.mustCaptureLogsAsTelemetry()) { + return; + } + + com.rollbar.notifier.Rollbar rollbarNotifier = notifier.rollbar; + if (rollbarNotifier == null) { + return; + } + + TelemetryEventTracker telemetryEventTracker = rollbarNotifier.getTelemetryEventTracker(); + if (telemetryEventTracker == null) { + return; + } + + LogcatTelemetryCapture logcatTelemetryCapture = new LogcatTelemetryCapture( + telemetryEventTracker, + androidConfiguration.getMinimumLogCaptureLevel(), + TAG); + logcatTelemetryCapture.start(); + notifier.logcatTelemetryCapture = logcatTelemetryCapture; + } + private String loadAccessTokenFromManifest(Context context) throws NameNotFoundException { Context appContext = context.getApplicationContext(); ApplicationInfo ai = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA); diff --git a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java new file mode 100644 index 00000000..2281a40b --- /dev/null +++ b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java @@ -0,0 +1,147 @@ +package com.rollbar.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.rollbar.api.payload.data.Level; +import com.rollbar.api.payload.data.Source; +import com.rollbar.notifier.telemetry.TelemetryEventTracker; + +import org.junit.Before; +import org.junit.Test; + +public class LogcatTelemetryCaptureTest { + + private TelemetryEventTracker tracker; + + @Before + public void setUp() { + tracker = mock(TelemetryEventTracker.class); + } + + @Test + public void mapPriorityToLevel_knownPriorities() { + assertEquals(Level.DEBUG, LogcatTelemetryCapture.mapPriorityToLevel("V")); + assertEquals(Level.DEBUG, LogcatTelemetryCapture.mapPriorityToLevel("D")); + assertEquals(Level.INFO, LogcatTelemetryCapture.mapPriorityToLevel("I")); + assertEquals(Level.WARNING, LogcatTelemetryCapture.mapPriorityToLevel("W")); + assertEquals(Level.ERROR, LogcatTelemetryCapture.mapPriorityToLevel("E")); + assertEquals(Level.CRITICAL, LogcatTelemetryCapture.mapPriorityToLevel("F")); + } + + @Test + public void mapPriorityToLevel_unknownReturnsNull() { + assertNull(LogcatTelemetryCapture.mapPriorityToLevel("X")); + assertNull(LogcatTelemetryCapture.mapPriorityToLevel("")); + assertNull(LogcatTelemetryCapture.mapPriorityToLevel(null)); + } + + @Test + public void logcatPriorityFor_levels() { + assertEquals("D", LogcatTelemetryCapture.logcatPriorityFor(Level.DEBUG)); + assertEquals("I", LogcatTelemetryCapture.logcatPriorityFor(Level.INFO)); + assertEquals("W", LogcatTelemetryCapture.logcatPriorityFor(Level.WARNING)); + assertEquals("E", LogcatTelemetryCapture.logcatPriorityFor(Level.ERROR)); + assertEquals("F", LogcatTelemetryCapture.logcatPriorityFor(Level.CRITICAL)); + assertEquals("W", LogcatTelemetryCapture.logcatPriorityFor(null)); + } + + @Test + public void processLine_recordsWarningAtThreshold() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: warn message"); + + verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn message")); + } + + @Test + public void processLine_recordsErrorAboveThreshold() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 E MyTag: boom"); + + verify(tracker).recordManualEventFor(eq(Level.ERROR), eq(Source.CLIENT), eq("boom")); + } + + @Test + public void processLine_skipsBelowThreshold() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 I MyTag: info message"); + capture.processLine("04-20 12:34:56.789 1234 5678 D MyTag: debug message"); + + verifyNoInteractions(tracker); + } + + @Test + public void processLine_skipsSelfTag() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 W Rollbar: recursion risk"); + + verifyNoInteractions(tracker); + } + + @Test + public void processLine_skipsUnparseable() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("--------- beginning of main"); + capture.processLine(""); + capture.processLine(null); + + verifyNoInteractions(tracker); + } + + @Test + public void processLine_trimsTag() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag : message"); + + verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); + } + + @Test + public void processLine_trackerThrow_doesNotPropagate() { + doThrow(new RuntimeException("tracker boom")) + .when(tracker).recordManualEventFor(any(), any(), any()); + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: message"); + + verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); + } + + @Test + public void processLine_messageWithColon_preserved() { + LogcatTelemetryCapture capture = newCapture(Level.WARNING); + + capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: key: value"); + + verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("key: value")); + } + + @Test + public void processLine_defaultsToWarningWhenMinLevelIsNull() { + LogcatTelemetryCapture capture = newCapture(null); + + capture.processLine("04-20 12:34:56.789 1234 5678 I MyTag: info"); + verify(tracker, never()).recordManualEventFor(any(), any(), any()); + + capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: warn"); + verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn")); + } + + private LogcatTelemetryCapture newCapture(Level minLevel) { + return new LogcatTelemetryCapture(tracker, minLevel, "Rollbar"); + } +} From b5f0407c6f472b01f0194e81791663bbae26373d Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 27 Apr 2026 02:44:29 -0300 Subject: [PATCH 2/9] fix: include verbosity logs for debug level --- .../main/java/com/rollbar/android/LogcatTelemetryCapture.java | 2 +- .../java/com/rollbar/android/LogcatTelemetryCaptureTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java index 9b6dde05..79627401 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java +++ b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java @@ -166,7 +166,7 @@ static String logcatPriorityFor(Level level) { } switch (level) { case DEBUG: - return "D"; + return "V"; case INFO: return "I"; case WARNING: diff --git a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java index 2281a40b..91398a79 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java @@ -45,7 +45,7 @@ public void mapPriorityToLevel_unknownReturnsNull() { @Test public void logcatPriorityFor_levels() { - assertEquals("D", LogcatTelemetryCapture.logcatPriorityFor(Level.DEBUG)); + assertEquals("V", LogcatTelemetryCapture.logcatPriorityFor(Level.DEBUG)); assertEquals("I", LogcatTelemetryCapture.logcatPriorityFor(Level.INFO)); assertEquals("W", LogcatTelemetryCapture.logcatPriorityFor(Level.WARNING)); assertEquals("E", LogcatTelemetryCapture.logcatPriorityFor(Level.ERROR)); From 858a713730808ad9bf5f94e0b9f0f16253b36ca2 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Thu, 30 Apr 2026 15:43:51 -0300 Subject: [PATCH 3/9] fix(android): reset logcat capture state on unexpected process death --- .../android/LogcatTelemetryCapture.java | 4 +++ .../android/LogcatTelemetryCaptureTest.java | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java index 79627401..8bdb185b 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java +++ b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java @@ -104,6 +104,10 @@ private void readLoop() { reader.close(); } catch (IOException ignored) { } + if (running) { + Log.w(Rollbar.TAG, "logcat process exited unexpectedly; resetting capture state"); + stop(); + } } } diff --git a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java index 91398a79..e23035d9 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; import com.rollbar.api.payload.data.Level; import com.rollbar.api.payload.data.Source; @@ -17,6 +18,9 @@ import org.junit.Before; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.util.concurrent.atomic.AtomicInteger; + public class LogcatTelemetryCaptureTest { private TelemetryEventTracker tracker; @@ -141,6 +145,28 @@ public void processLine_defaultsToWarningWhenMinLevelIsNull() { verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn")); } + @Test + public void start_afterUnexpectedProcessDeath_allowsRestartWithNewProcess() throws Exception { + Process dyingProcess = mock(Process.class); + when(dyingProcess.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[0])); + + AtomicInteger factoryCallCount = new AtomicInteger(0); + LogcatTelemetryCapture.ProcessFactory factory = priority -> { + factoryCallCount.incrementAndGet(); + return dyingProcess; + }; + + LogcatTelemetryCapture capture = new LogcatTelemetryCapture(tracker, Level.WARNING, "Rollbar", factory); + capture.start(); + + // Allow the reader thread to detect EOF and reset running=false via stop() + Thread.sleep(200); + + capture.start(); + + assertEquals(2, factoryCallCount.get()); + } + private LogcatTelemetryCapture newCapture(Level minLevel) { return new LogcatTelemetryCapture(tracker, minLevel, "Rollbar"); } From 0f13b1a95b4f3009efd7175769072db9f3e2cc74 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Thu, 30 Apr 2026 15:45:40 -0300 Subject: [PATCH 4/9] fix(android): skip logcat ring buffer replay on startup --- .../main/java/com/rollbar/android/LogcatTelemetryCapture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java index 8bdb185b..86845af5 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java +++ b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java @@ -193,7 +193,7 @@ private static ProcessFactory defaultProcessFactory() { @Override public Process start(String priorityFilter) throws IOException { return new ProcessBuilder( - "logcat", "-v", "threadtime", "*:" + priorityFilter) + "logcat", "-v", "threadtime", "-T", "1", "*:" + priorityFilter) .redirectErrorStream(true) .start(); } From e63b0aff905e398ada764adc372f67f7cd20c782 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Thu, 30 Apr 2026 15:47:57 -0300 Subject: [PATCH 5/9] fix(android): classify captured logcat entries as log telemetry type --- .../rollbar/android/LogcatTelemetryCapture.java | 2 +- .../android/LogcatTelemetryCaptureTest.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java index 86845af5..8fc635ad 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java +++ b/rollbar-android/src/main/java/com/rollbar/android/LogcatTelemetryCapture.java @@ -137,7 +137,7 @@ void processLine(String line) { } try { - tracker.recordManualEventFor(level, Source.CLIENT, message); + tracker.recordLogEventFor(level, Source.CLIENT, message); } catch (Exception e) { // Never let a broken tracker kill the reader thread. } diff --git a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java index e23035d9..3c4594b9 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java @@ -63,7 +63,7 @@ public void processLine_recordsWarningAtThreshold() { capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: warn message"); - verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn message")); + verify(tracker).recordLogEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn message")); } @Test @@ -72,7 +72,7 @@ public void processLine_recordsErrorAboveThreshold() { capture.processLine("04-20 12:34:56.789 1234 5678 E MyTag: boom"); - verify(tracker).recordManualEventFor(eq(Level.ERROR), eq(Source.CLIENT), eq("boom")); + verify(tracker).recordLogEventFor(eq(Level.ERROR), eq(Source.CLIENT), eq("boom")); } @Test @@ -111,18 +111,18 @@ public void processLine_trimsTag() { capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag : message"); - verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); + verify(tracker).recordLogEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); } @Test public void processLine_trackerThrow_doesNotPropagate() { doThrow(new RuntimeException("tracker boom")) - .when(tracker).recordManualEventFor(any(), any(), any()); + .when(tracker).recordLogEventFor(any(), any(), any()); LogcatTelemetryCapture capture = newCapture(Level.WARNING); capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: message"); - verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); + verify(tracker).recordLogEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("message")); } @Test @@ -131,7 +131,7 @@ public void processLine_messageWithColon_preserved() { capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: key: value"); - verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("key: value")); + verify(tracker).recordLogEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("key: value")); } @Test @@ -139,10 +139,10 @@ public void processLine_defaultsToWarningWhenMinLevelIsNull() { LogcatTelemetryCapture capture = newCapture(null); capture.processLine("04-20 12:34:56.789 1234 5678 I MyTag: info"); - verify(tracker, never()).recordManualEventFor(any(), any(), any()); + verify(tracker, never()).recordLogEventFor(any(), any(), any()); capture.processLine("04-20 12:34:56.789 1234 5678 W MyTag: warn"); - verify(tracker).recordManualEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn")); + verify(tracker).recordLogEventFor(eq(Level.WARNING), eq(Source.CLIENT), eq("warn")); } @Test From b47bc625217cec039f946ed47f8b23aaaa93919b Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Thu, 30 Apr 2026 17:01:42 -0300 Subject: [PATCH 6/9] fix(android): resolve test failure caused by missing Android stub defaults --- rollbar-android/build.gradle.kts | 4 ++++ .../rollbar/android/LogcatTelemetryCaptureTest.java | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/rollbar-android/build.gradle.kts b/rollbar-android/build.gradle.kts index e4d0e634..a3f1049e 100644 --- a/rollbar-android/build.gradle.kts +++ b/rollbar-android/build.gradle.kts @@ -25,6 +25,10 @@ android { } } + testOptions { + unitTests.isReturnDefaultValues = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java index 3c4594b9..0594ad58 100644 --- a/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java +++ b/rollbar-android/src/test/java/com/rollbar/android/LogcatTelemetryCaptureTest.java @@ -159,10 +159,12 @@ public void start_afterUnexpectedProcessDeath_allowsRestartWithNewProcess() thro LogcatTelemetryCapture capture = new LogcatTelemetryCapture(tracker, Level.WARNING, "Rollbar", factory); capture.start(); - // Allow the reader thread to detect EOF and reset running=false via stop() - Thread.sleep(200); - - capture.start(); + // Poll start() until the reader thread detects EOF and resets running=false + long deadline = System.currentTimeMillis() + 2000; + while (factoryCallCount.get() < 2 && System.currentTimeMillis() < deadline) { + Thread.sleep(10); + capture.start(); + } assertEquals(2, factoryCallCount.get()); } From 41da663906f3c8209b1778721b7fb120547a40ef Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 14:20:39 -0300 Subject: [PATCH 7/9] fix(android): use Rollbar.TAG in ConnectivityDetector to suppress SDK logs from telemetry --- .../rollbar/android/notifier/sender/ConnectivityDetector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/notifier/sender/ConnectivityDetector.java b/rollbar-android/src/main/java/com/rollbar/android/notifier/sender/ConnectivityDetector.java index 46cf2c92..01298ade 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/notifier/sender/ConnectivityDetector.java +++ b/rollbar-android/src/main/java/com/rollbar/android/notifier/sender/ConnectivityDetector.java @@ -13,6 +13,7 @@ import android.net.NetworkInfo; import android.os.Bundle; import android.util.Log; +import com.rollbar.android.Rollbar; import com.rollbar.notifier.util.ObjectsUtils; import java.io.Closeable; @@ -46,7 +47,7 @@ public void updateContext(Context androidContext) { String message = "This application is missing the " + "android.permission.ACCESS_NETWORK_STATE permission. The Rollbar notifier " + "will *not* be able to detect when the network is unavailable."; - Log.w(ConnectivityDetector.class.getCanonicalName(), message); + Log.w(Rollbar.TAG, message); } } From ea3a83c7f95372ea1ab956f6f4d7218ebb6fb902 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 14:21:48 -0300 Subject: [PATCH 8/9] docs(android): correct captureLogsAsTelemetry javadoc to reference log telemetry type --- .../src/main/java/com/rollbar/android/AndroidConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java b/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java index aed9087f..bcb0e310 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java +++ b/rollbar-android/src/main/java/com/rollbar/android/AndroidConfiguration.java @@ -68,7 +68,7 @@ public Builder captureNewActivityTelemetryEvents(boolean mustCaptureNavigationEv * Enable or disable automatic capture of Android log output as telemetry events. * When enabled, logs emitted via {@code android.util.Log} (and any other source written to * logcat from this app's UID, including third-party libraries) at or above the configured - * minimum level are recorded as manual telemetry events with + * minimum level are recorded as log telemetry events with * {@link com.rollbar.api.payload.data.Source#CLIENT}. * Default is disabled. * @param mustCaptureLogsAsTelemetry if automatic capture must be enabled or disabled. From e8355f27e764918644d4064f023a194a782052d5 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 14:25:02 -0300 Subject: [PATCH 9/9] docs: fix stale dump() references in telemetry javadoc --- .../notifier/telemetry/RollbarTelemetryEventTracker.java | 2 +- .../com/rollbar/notifier/telemetry/TelemetryEventTracker.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java index ba1750d2..9d71fd57 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/RollbarTelemetryEventTracker.java @@ -20,7 +20,7 @@ * buffer. When the configured maximum capacity is reached, the oldest events * are discarded to make room for new ones. * - *

Recorded events are returned and cleared when {@link #dump()} is called. + *

Recorded events are returned when {@link #getAll()} is called. */ public class RollbarTelemetryEventTracker implements TelemetryEventTracker { public static final int MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS = 100; diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java index 846bc636..d1f2eb04 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/telemetry/TelemetryEventTracker.java @@ -10,8 +10,8 @@ /** * Collects and manages telemetry events that provide additional runtime * context for error and message payloads. - * Telemetry events are typically buffered and cleared after they are - * {@link #dump() dumped}. + * Telemetry events are typically buffered and retrieved via + * {@link #getAll()}. */ public interface TelemetryEventTracker {