diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaa44fff..93e41b0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## XX.XX.XX +* Added a new server configuration to manage uncaught crash reporting. * Mitigated an issue where visibility could have been wrongly assigned if a view was closed while going to background. (Experimental!) ## 24.7.5 diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java index f0ae4d761..b3a70f55f 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java @@ -77,6 +77,10 @@ public void setUp() { @Override public boolean getTrackingEnabled() { return true; } + + @Override public boolean getCrashReportingEnabled() { + return true; + } }; Countly.sharedInstance().setLoggingEnabled(true); diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java index 4b74e725e..f74a61434 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -2,6 +2,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; import org.json.JSONException; import org.json.JSONObject; import org.junit.After; @@ -10,20 +11,21 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.mockito.Mockito.mock; - @RunWith(AndroidJUnit4.class) public class ModuleConfigurationTests { CountlyStore countlyStore; @Before public void setUp() { - countlyStore = new CountlyStore(TestUtils.getContext(), mock(ModuleLog.class)); + countlyStore = TestUtils.getCountyStore(); countlyStore.clear(); + Countly.sharedInstance().halt(); } @After public void tearDown() { + TestUtils.getCountyStore().clear(); + Countly.sharedInstance().halt(); } /** @@ -64,7 +66,7 @@ public void init_enabled_storageEmpty() { */ @Test public void init_enabled_storageAllowing() throws JSONException { - countlyStore.setServerConfig(getStorageString(true, true)); + countlyStore.setServerConfig(getStorageString(true, true, true)); CountlyConfig config = TestUtils.createConfigurationConfig(true, null); Countly countly = (new Countly()).init(config); @@ -82,7 +84,7 @@ public void init_enabled_storageAllowing() throws JSONException { */ @Test public void init_enabled_storageForbidding() throws JSONException { - countlyStore.setServerConfig(getStorageString(false, false)); + countlyStore.setServerConfig(getStorageString(false, false, false)); CountlyConfig config = TestUtils.createConfigurationConfig(true, null); Countly countly = (new Countly()).init(config); @@ -90,6 +92,7 @@ public void init_enabled_storageForbidding() throws JSONException { Assert.assertNotNull(countlyStore.getServerConfig()); Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled()); } /** @@ -101,7 +104,7 @@ public void init_enabled_storageForbidding() throws JSONException { */ @Test public void init_disabled_storageAllowing() throws JSONException { - countlyStore.setServerConfig(getStorageString(true, true)); + countlyStore.setServerConfig(getStorageString(true, true, true)); CountlyConfig config = TestUtils.createConfigurationConfig(false, null); Countly countly = Countly.sharedInstance().init(config); @@ -119,7 +122,7 @@ public void init_disabled_storageAllowing() throws JSONException { */ @Test public void init_disabled_storageForbidding() throws JSONException { - countlyStore.setServerConfig(getStorageString(false, false)); + countlyStore.setServerConfig(getStorageString(false, false, false)); CountlyConfig config = TestUtils.createConfigurationConfig(false, null); Countly countly = (new Countly()).init(config); @@ -165,13 +168,14 @@ public void validatingTrackingConfig() throws JSONException { Assert.assertEquals("", countlyStore.getRequestQueueRaw()); Assert.assertEquals(0, countlyStore.getEvents().length); - countlyStore.setServerConfig(getStorageString(false, false)); + countlyStore.setServerConfig(getStorageString(false, false, false)); CountlyConfig config = TestUtils.createConfigurationConfig(true, null); Countly countly = (new Countly()).init(config); Assert.assertFalse(countly.moduleConfiguration.getNetworkingEnabled()); Assert.assertFalse(countly.moduleConfiguration.getTrackingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled()); //try events countly.events().recordEvent("d"); @@ -189,6 +193,64 @@ public void validatingTrackingConfig() throws JSONException { Assert.assertEquals(0, countlyStore.getEvents().length); } + /** + * Only disable crashes to try out unhandled crash reporting + * Make sure that call is called but no request is added to the RQ + * Call count to the unhandled crash reporting call should be 1 because countly SDK won't call and override the default handler + * And validate that no crash request is generated + */ + @Test + public void validatingCrashReportingConfig() throws JSONException { + AtomicInteger callCount = new AtomicInteger(0); + RuntimeException unhandledException = new RuntimeException("Simulated unhandled exception"); + // Create a new thread to simulate unhandled exception + Thread threadThrows = new Thread(() -> { + // This will throw an unhandled exception in this thread + throw unhandledException; + }); + + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + Assert.assertEquals(unhandledException, throwable); + Assert.assertEquals(threadThrows, thread); + callCount.incrementAndGet(); + }); + + TestUtils.getCountyStore().setServerConfig(getStorageString(true, true, false)); + CountlyConfig config = TestUtils.createBaseConfig(); + config.enableServerConfiguration().setEventQueueSizeToSend(2); + config.crashes.enableCrashReporting(); // this call will enable unhandled crash reporting + Countly countly = new Countly().init(config); + + Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled()); + Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled()); + Assert.assertFalse(countly.moduleConfiguration.getCrashReportingEnabled()); + + // Start the thread and wait for it to terminate + threadThrows.start(); + try { + threadThrows.join(); // Wait for thread to finish + } catch (InterruptedException ignored) { + } + + //try events + countly.events().recordEvent("d"); + countly.events().recordEvent("1"); + Assert.assertEquals(1, callCount.get()); + + //try a non event recording + countly.crashes().recordHandledException(new Exception()); + + //try a direct request + countly.requestQueue().addDirectRequest(new HashMap<>()); + + countly.requestQueue().attemptToSendStoredRequests(); + + // There are two requests in total, but they are not containing unhandled exception + Assert.assertEquals(2, TestUtils.getCurrentRQ("Simulated unhandled exception").length); + Assert.assertNull(TestUtils.getCurrentRQ("Simulated unhandled exception")[0]); + Assert.assertNull(TestUtils.getCurrentRQ("Simulated unhandled exception")[1]); + } + /** * Making sure that bad config responses are rejected */ @@ -239,6 +301,7 @@ Countly initAndValidateConfigParsingResult(String targetResponse, boolean respon void assertConfigDefault(Countly countly) { Assert.assertTrue(countly.moduleConfiguration.getNetworkingEnabled()); Assert.assertTrue(countly.moduleConfiguration.getTrackingEnabled()); + Assert.assertTrue(countly.moduleConfiguration.getCrashReportingEnabled()); } ImmediateRequestGenerator createIRGForSpecificResponse(final String targetResponse) { @@ -267,12 +330,13 @@ ImmediateRequestGenerator createIRGForSpecificResponse(final String targetRespon } //creates the stringified storage object with all the required properties - String getStorageString(boolean tracking, boolean networking) throws JSONException { + String getStorageString(boolean tracking, boolean networking, boolean crashes) throws JSONException { JSONObject jsonObject = new JSONObject(); JSONObject jsonObjectConfig = new JSONObject(); jsonObjectConfig.put("tracking", tracking); jsonObjectConfig.put("networking", networking); + jsonObjectConfig.put("crashes", crashes); jsonObject.put("v", 1); jsonObject.put("t", 1_681_808_287_464L); diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java index b0134fec9..f6d44882d 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java @@ -4,4 +4,6 @@ interface ConfigurationProvider { boolean getNetworkingEnabled(); boolean getTrackingEnabled(); + + boolean getCrashReportingEnabled(); } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java index 331d7d51c..9623eb00f 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java @@ -15,6 +15,7 @@ class ModuleConfiguration extends ModuleBase implements ConfigurationProvider { //config keys final static String keyTracking = "tracking"; final static String keyNetworking = "networking"; + final static String keyCrashReporting = "crashes"; //request keys final static String keyRTimestamp = "t"; @@ -23,9 +24,11 @@ class ModuleConfiguration extends ModuleBase implements ConfigurationProvider { final static boolean defaultVTracking = true; final static boolean defaultVNetworking = true; + final static boolean defaultVCrashReporting = true; boolean currentVTracking = true; boolean currentVNetworking = true; + boolean currentVCrashReporting = true; boolean configurationFetched = false; ModuleConfiguration(@NonNull Countly cly, @NonNull CountlyConfig config) { @@ -92,6 +95,7 @@ void updateConfigVariables() { //set all to defaults currentVNetworking = defaultVNetworking; currentVTracking = defaultVTracking; + currentVCrashReporting = defaultVCrashReporting; if (latestRetrievedConfiguration == null) { //no config, don't continue @@ -103,7 +107,7 @@ void updateConfigVariables() { try { currentVNetworking = latestRetrievedConfiguration.getBoolean(keyNetworking); } catch (JSONException e) { - L.w("[ModuleConfiguration] updateConfigs, failed to load 'networking', " + e); + L.w("[ModuleConfiguration] updateConfigVariables, failed to load 'networking', " + e); } } @@ -112,7 +116,16 @@ void updateConfigVariables() { try { currentVTracking = latestRetrievedConfiguration.getBoolean(keyTracking); } catch (JSONException e) { - L.w("[ModuleConfiguration] updateConfigs, failed to load 'tracking', " + e); + L.w("[ModuleConfiguration] updateConfigVariables, failed to load 'tracking', " + e); + } + } + + //tracking + if (latestRetrievedConfiguration.has(keyCrashReporting)) { + try { + currentVCrashReporting = latestRetrievedConfiguration.getBoolean(keyCrashReporting); + } catch (JSONException e) { + L.w("[ModuleConfiguration] updateConfigVariables, failed to load 'crash_reporting', " + e); } } } @@ -231,4 +244,12 @@ public boolean getTrackingEnabled() { } return currentVTracking; } + + @Override + public boolean getCrashReportingEnabled() { + if (!serverConfigEnabled) { + return defaultVCrashReporting; + } + return currentVCrashReporting; + } } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java b/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java index 8985e33fa..8d7d62240 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleCrash.java @@ -338,8 +338,10 @@ Countly addBreadcrumbInternal(@Nullable String breadcrumb) { @Override void initFinished(@NonNull CountlyConfig config) { //enable unhandled crash reporting - if (config.crashes.enableUnhandledCrashReporting) { + if (config.crashes.enableUnhandledCrashReporting && (config.configProvider == null || config.configProvider.getCrashReportingEnabled())) { enableCrashReporting(); + } else if (config.crashes.enableUnhandledCrashReporting) { + L.w("[ModuleCrash] initFinished, Crash reporting is enabled in the configuration, but it is not enabled in the config provider"); } //check for previous native crash dumps