Skip to content

ANR based on ApplicationExitInfo #166

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 72 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
5a3d7cd
Shared preferences demo
Feb 10, 2025
0a42a52
ANR demo
Feb 11, 2025
3d1e630
Merge branch 'master' into anr-app-exit
Feb 12, 2025
50624f3
ANR state and shared preferences
Feb 13, 2025
cf6edc3
Extract information about app exit info
Feb 15, 2025
da9220d
Improve processing of AppExitInfo - use only ANRs
Feb 17, 2025
68fa0fe
Make default ANR handler more generic
Feb 18, 2025
d2223fb
Refactor ANR settings
Feb 19, 2025
5fa59ca
Use annotations instead of string msg
Feb 21, 2025
856320a
Fix ANR msg
Mar 3, 2025
1e36b99
Resolve TODOs
Mar 4, 2025
0b38137
Refactor
Mar 4, 2025
a84bb3d
Add unit-tests
Mar 4, 2025
bbf4e12
Add integration tests
Mar 5, 2025
5668812
Android ANR integration tests
Mar 6, 2025
c0b2111
Unit-tests not working
Mar 11, 2025
ae05d99
Refactor - add extra abstraction layer to be able to mock sdk for int…
Mar 12, 2025
6e975c7
Merge branch 'master' into anr-app-exit
Mar 18, 2025
c14a094
Remove unused code
Mar 18, 2025
4700510
Refactor
Mar 18, 2025
8f6c895
Remove old test
Mar 23, 2025
c47c19c
Remove unused code
Mar 23, 2025
8d5d629
Reformat code
Mar 23, 2025
a0978ee
Revert changes in MainActivity
Mar 23, 2025
b5c3bfc
Refactor code
Mar 23, 2025
8d71e91
Use only SharedPreferences apply instead of apply and commit
Mar 23, 2025
40bba34
Reformat file
Mar 23, 2025
46929b6
Fix unit-tests
Mar 23, 2025
337722f
Revert bump versions - applied in separate PR
Mar 23, 2025
0e98e14
Remove unused package
Mar 23, 2025
294e876
Revert changes
Mar 23, 2025
ccddb2f
Remove TODO
Mar 23, 2025
17a2047
Increase time
Mar 24, 2025
7fe8282
Run tests only on sdk > android R
Mar 25, 2025
2bf1189
Test ANR on old devices < android R
Mar 25, 2025
75ac457
Tmp increase time
Mar 25, 2025
c3cf579
Reformat code
Mar 25, 2025
62af9e8
Rename anr type
Mar 25, 2025
4f94897
Simplify code
Mar 25, 2025
43801c0
Tmp test improvement
Mar 25, 2025
aeec6a5
Fix unit-test
Mar 25, 2025
ef23879
Fix thread name
Mar 28, 2025
b24ff1f
Fix thread name
Apr 1, 2025
9f37687
ANR App exit parse stack
Apr 8, 2025
f3eebe7
Parse stack-trace
Apr 9, 2025
50f4820
Refactor ANR extract stacktrace
Apr 9, 2025
34483c0
Refactor
Apr 9, 2025
c4721c6
Parse Native Frame
Apr 16, 2025
9f2f6ef
Refactor NativeStackTrace parsing
Apr 16, 2025
106b3d2
Refactor parsing stack-trace
Apr 16, 2025
181958a
Refactor stack trace parser
Apr 16, 2025
dea99df
Refactor
Apr 17, 2025
ff55403
Improve native stack trace line
Apr 21, 2025
6452ad5
Add asserts for ANR native stacktrace
Apr 21, 2025
34aabf8
Revert time
Apr 21, 2025
5d13ffc
Unit tests for ExitInfoStackTraceParserTest
Apr 21, 2025
20046ab
Add unit-tests
Apr 23, 2025
1cbf352
Fix thread regex
Apr 23, 2025
135d2b2
Simplify code
Apr 23, 2025
6eeab8d
Add ANR unit tests
Apr 23, 2025
a1ce340
Refactor and fix unit-tests
Apr 24, 2025
a9d3b27
Merge branch 'master' into anr-app-exit
BartoszLitwiniuk Apr 24, 2025
4dec205
Revert MainActivity.java changes
Apr 24, 2025
46de78b
Improve parsing stack-trace
Apr 24, 2025
6b2e74c
Improve unit-tests and fix parsing stacktrace refactor
Apr 26, 2025
09d935e
Fix unit tests
Apr 26, 2025
e5295e1
Fix timestamp unit test
May 4, 2025
4c14ca1
Merge branch 'master' into anr-app-exit
May 5, 2025
af36328
Uncomment code
May 17, 2025
3d0c74d
Commit instead of apply
May 17, 2025
d24346f
Fix - interrupt anr thread
May 17, 2025
a137d5c
Merge branch 'master' into anr-app-exit
BartoszLitwiniuk Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package backtraceio.library;

import java.io.InputStream;

public class TestUtils {

public static InputStream readFileAsStream(Object obj, String fileName) {
ClassLoader classLoader = obj.getClass().getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(fileName);

if (inputStream != null) {
return inputStream;
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package backtraceio.library.anr;

import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.app.ApplicationExitInfo;
import android.content.Context;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;

import net.jodah.concurrentunit.Waiter;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import backtraceio.library.BacktraceClient;
import backtraceio.library.BacktraceCredentials;
import backtraceio.library.TestUtils;
import backtraceio.library.events.RequestHandler;
import backtraceio.library.models.BacktraceApiResult;
import backtraceio.library.models.BacktraceData;
import backtraceio.library.models.BacktraceResult;

@RunWith(AndroidJUnit4.class)
public class BacktraceAppExitInfoSenderHandlerTest {
@Mock
private Context mockContext;

private final String PACKAGE_NAME = "backtrace.io.tests";

private final String ANR_APPEXIT_STACKTRACE_FILE = "anrAppExitInfoStacktrace.txt";

private final BacktraceCredentials credentials = new BacktraceCredentials("https://example-endpoint.com/", "");
private BacktraceClient backtraceClient;

@Before
public void setUp() throws Exception {
this.mockContext = InstrumentationRegistry.getInstrumentation().getContext();
this.backtraceClient = new BacktraceClient(this.mockContext, credentials);
}

private ExitInfo mockApplicationExitInfo(String description, Long timestamp, int reason,
int pid, int importance, long pss, long rss, InputStream stacktrace) throws IOException {
ExitInfo mockExitInfo = mock(ExitInfo.class);
when(mockExitInfo.getDescription()).thenReturn(description);
when(mockExitInfo.getTimestamp()).thenReturn(timestamp);
when(mockExitInfo.getReason()).thenReturn(reason);
when(mockExitInfo.getPid()).thenReturn(pid);
when(mockExitInfo.getImportance()).thenReturn(importance);
when(mockExitInfo.getPss()).thenReturn(pss);
when(mockExitInfo.getRss()).thenReturn(rss);
when(mockExitInfo.getTraceInputStream()).thenReturn(stacktrace);
return mockExitInfo;
}

private ExitInfo mockApplicationExitInfo(String description, Long timestamp, int reason, InputStream stacktrace) throws IOException {
return mockApplicationExitInfo(description, timestamp, reason, 0, 0, 0L, 0L, stacktrace);
}

private ProcessExitInfoProvider mockActivityManagerExitInfoProvider() throws IOException {
ActivityManagerExitInfoProvider mock = mock(ActivityManagerExitInfoProvider.class);
final List<ExitInfo> exitInfoList = new ArrayList<>();
exitInfoList.add(mockApplicationExitInfo("random-text", System.currentTimeMillis(), ApplicationExitInfo.REASON_CRASH_NATIVE, null));
exitInfoList.add(mockApplicationExitInfo("anr", System.currentTimeMillis(), ApplicationExitInfo.REASON_ANR, TestUtils.readFileAsStream(this, ANR_APPEXIT_STACKTRACE_FILE)));
exitInfoList.add(mockApplicationExitInfo("anr without stacktrace", System.currentTimeMillis(), ApplicationExitInfo.REASON_ANR, null));
exitInfoList.add(mockApplicationExitInfo("random-description", System.currentTimeMillis(), ApplicationExitInfo.REASON_LOW_MEMORY, null));

when(mock.getHistoricalExitInfo(PACKAGE_NAME, 0, 0)).thenReturn(exitInfoList);
when(mock.getSupportedTypesOfExitInfo()).thenReturn(Collections.singletonList(ApplicationExitInfo.REASON_ANR));
return mock;
}

private AnrExitInfoState mockAnrExitInfoState() {
AnrExitInfoState mock = mock(AnrExitInfoState.class);
doNothing().when(mock).saveTimestamp(anyLong());
when(mock.getLastTimestamp()).thenReturn(0L);
return mock;
}

@Test
@SdkSuppress(minSdkVersion = android.os.Build.VERSION_CODES.R)
public void checkIfANRIsSentFromAppExitInfo() throws IOException {
// GIVEN
final ProcessExitInfoProvider mockProcessExitInfoProvider = mockActivityManagerExitInfoProvider();
final AnrExitInfoState anrExitInfoState = mockAnrExitInfoState();
final Waiter waiter = new Waiter();
backtraceClient.setOnRequestHandler(new RequestHandler() {
@Override
public BacktraceResult onRequest(BacktraceData data) {

Map<String, String> attributes = data.getAttributes();
Map<String, Object> annotations = data.getAnnotations();
Map<String, Object> anrAnnotations = (Map<String, Object>) annotations.get("ANR annotations");

waiter.assertEquals(data.getReport().getException().getStackTrace().length, 33);

waiter.assertNotNull(anrAnnotations);
waiter.assertNotNull(attributes);
waiter.assertEquals("anr", anrAnnotations.get("description"));
waiter.assertEquals(ApplicationExitInfo.REASON_ANR, anrAnnotations.get("reason-code"));
waiter.assertEquals("anr", anrAnnotations.get("reason"));
waiter.assertTrue(((Map<String, Object>)annotations.get("ANR parsed stacktrace")).size() > 0);
waiter.assertEquals("backtraceio.library.anr.BacktraceANRExitInfoException", attributes.get("classifier"));
waiter.assertEquals("Hang", attributes.get("error.type"));
waiter.assertTrue(attributes.get("ANR stacktrace").length() > 0);
waiter.resume();

return new BacktraceResult(new BacktraceApiResult("_", "ok"));
}
});
// WHEN
new BacktraceAppExitInfoSenderHandler(this.backtraceClient, PACKAGE_NAME, anrExitInfoState, mockProcessExitInfoProvider);

// THEN
try {
waiter.await(5, TimeUnit.SECONDS); // Check if anr is detected and event was emitted
} catch (Exception ex) {
fail(ex.getMessage());
}
}

@Test
@SdkSuppress(maxSdkVersion = android.os.Build.VERSION_CODES.Q)
public void checkIfANRIsNotSentOnOldSDK() throws IOException {
// GIVEN
final int THREAD_SLEEP_TIME_MS = 3000;
final ProcessExitInfoProvider mockProcessExitInfoProvider = mockActivityManagerExitInfoProvider();
final AnrExitInfoState anrExitInfoState = mockAnrExitInfoState();
final Waiter waiter = new Waiter();
backtraceClient.setOnRequestHandler(new RequestHandler() {
@Override
public BacktraceResult onRequest(BacktraceData data) {
waiter.fail();
return new BacktraceResult(new BacktraceApiResult("_", "ok"));
}
});
// WHEN
new BacktraceAppExitInfoSenderHandler(this.backtraceClient, PACKAGE_NAME, anrExitInfoState, mockProcessExitInfoProvider);

// THEN
try {
Thread.sleep(THREAD_SLEEP_TIME_MS);
} catch (Exception ex) {
fail(ex.getMessage());
}
System.out.println("wat");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package backtraceio.library.common;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.SharedPreferences;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class SharedPreferencesManagerTest {

private SharedPreferencesManager sharedPreferencesManager;

@Mock
private Context mockContext;

@Mock
private SharedPreferences mockSharedPreferences;

@Mock
private SharedPreferences.Editor mockEditor;

@Before
public void setUp() {
sharedPreferencesManager = new SharedPreferencesManager(mockContext);
when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences);
when(mockSharedPreferences.edit()).thenReturn(mockEditor);
when(mockEditor.putLong(anyString(), anyLong())).thenReturn(mockEditor);
}

@Test
public void testSaveLongToSharedPreferences() {
// GIVEN
String prefName = "test_prefs";
String key = "test_key";
long value = 12345L;

// WHEN
sharedPreferencesManager.saveLongToSharedPreferences(prefName, key, value);

// THEN
verify(mockContext).getSharedPreferences(prefName, Context.MODE_PRIVATE);
verify(mockSharedPreferences).edit();
verify(mockEditor).putLong(key, value);
verify(mockEditor).apply();
}

@Test
public void testReadLongFromSharedPreferences() {
// GIVEN
String prefName = "test_prefs";
String key = "test_key";
long defaultValue = 0L;
Long expectedValue = 12345L;

// WHEN
when(mockSharedPreferences.getLong(key, defaultValue)).thenReturn(expectedValue);

// THEN
Long result = sharedPreferencesManager.readLongFromSharedPreferences(prefName, key, defaultValue);

verify(mockContext).getSharedPreferences(prefName, Context.MODE_PRIVATE);
verify(mockSharedPreferences).getLong(key, defaultValue);
assertEquals(expectedValue, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void setUp() {
public void checkIfANRIsDetectedCorrectly() {
// GIVEN
final Waiter waiter = new Waiter();
BacktraceANRWatchdog watchdog = new BacktraceANRWatchdog(this.backtraceClient, 500);
BacktraceANRHandlerWatchdog watchdog = new BacktraceANRHandlerWatchdog(this.backtraceClient, 500);
watchdog.setOnApplicationNotRespondingEvent(new OnApplicationNotRespondingEvent() {
@Override
public void onEvent(BacktraceWatchdogTimeoutException exception) {
Expand Down Expand Up @@ -96,7 +96,7 @@ public void checkIfANRIsNotDetected() {
// GIVEN
final int numberOfIterations = 5;
final Waiter waiter = new Waiter();
BacktraceANRWatchdog watchdog = new BacktraceANRWatchdog(this.backtraceClient, 5000);
BacktraceANRHandlerWatchdog watchdog = new BacktraceANRHandlerWatchdog(this.backtraceClient, 5000);
watchdog.setOnApplicationNotRespondingEvent(new OnApplicationNotRespondingEvent() {
@Override
public void onEvent(BacktraceWatchdogTimeoutException exception) {
Expand Down Expand Up @@ -145,4 +145,4 @@ public void onEvent(BacktraceWatchdogTimeoutException exception) {
fail(e.getMessage());
}
}
}
}
Loading
Loading