From ca61a7badb0a832c0dd1157517e5d93c09341283 Mon Sep 17 00:00:00 2001 From: Zahid Zafar Date: Wed, 20 Apr 2022 15:39:13 +0500 Subject: [PATCH] [SDK 712] Backend mode feature added. (#15) * Basic structure * Removed previous code * - sessions, event, views, crash and user request added into q - request q process * - Sample App added * - record user properties method updated - Sample app updated * Backend mode events unit tests added. * - views - sessions (end, update, begin) - crash - user properties Unit tests added. * BackendMode import in Countly * Stable Code point * - Config unit test added - Unit tests init changed * SDK Core feature will not work if backend mode is enabled. * - Example app updated - Changelog added * Sample App updated Config updated * - session calls moved into switch - interal calss added to recrod data - ConfigTests2.java removed - Change log updated * - Getter Setter removed * logs call updated to spec not java sdk. * - Logs and message updated - Unit tests updated (common event and request time validate method) - Map ---> Map - Sample App updated * - metrics param added in session begin - unit test for session begin metrics - Unit tests comments added and method renamed - Exception on other unit tests fixed * - 'requestQueueMaxSize' field added in configuration - Process request qu logic remvoed form 'BackendModeModule' and moved to SDKCore - Views Invalid data unit tests Added - Events invalid data unit tests added - Sessions (begin, update, end) invalid data unit tests added. * - record exception invalid data unit tests added - recrod user properties invalid data unit tests added - Segmentation data validatio added (Views, Events, user detial, exceptions) * segmentation invalid data type unit tests added for Events, Veiws, user, exceptoin. * segmentation validation for user properties. * - userProperteis restructure logic added - unit tests added. * - Sample app updated - process request signal added - null pointer bug fixed * - record direct request - unit tests added. * - sdk_name, sdk_version, checksum protected - drop reqeust logic added * Sample updated userdetail internal call logic updated userdetail unit tests added MaxRequestQSize lower limit logic added PR Changes * smaple app updated * - Example app updated - logs updated * - example updated - method documentation added - timestamp datatype updated * - getQueueSize moved to BackendMode class - code formating * getQueueSize = request queue + event queue assert added in unit tests * version updated to 20.11.2-RC1 * formating fixed * Tweaking releasing things * Sum bug fixed! * record event sum data type updated * performance unit test * request queue size = 1000000 * perfomance unit test reverted performance test sample app added. * stop on performing test * Example updated * time and count info added * Log updated * redundant removed from user detail unit tests. * Performance test updated Backend mode example updated Error print if reqeust queue size is lessthan 1 * - Crash detail added while recording crash - Crash unit tests - Backend mode sample app updated - Backend Mode performance app updated * Default folder path added * Mehods documenation added. * Event duration data type udpatd from 'double' to 'Double' unit tests updated example app updated * time calculation move outside switch backend mode enable check added. * moved related unit test in single unit test extra unit tests removed * sesseion begin location added * session Location added session location unit tests added * Version updated to '20.11.2' BackendMode 'disableMode'check added Unit test function name updated Co-authored-by: Zahid Zafar <> Co-authored-by: ArtursK --- .idea/runConfigurations.xml | 12 - .idea/uiDesigner.xml | 124 +++ CHANGELOG.md | 3 + .../count/java/demo/BackendModeExample.java | 203 ++++ .../demo/BackendModePerformanceTests.java | 213 ++++ gradle.properties | 7 +- sdk-java/build.gradle | 2 +- sdk-java/gradle.properties | 8 - .../main/java/ly/count/sdk/java/Config.java | 45 +- .../main/java/ly/count/sdk/java/Countly.java | 18 + .../ly/count/sdk/java/CountlyLifecycle.java | 9 + .../count/sdk/java/internal/CoreFeature.java | 1 + .../sdk/java/internal/DefaultNetworking.java | 8 +- .../count/sdk/java/internal/DeviceCore.java | 4 + .../ly/count/sdk/java/internal/EventImpl.java | 10 + .../internal/IStorageForRequestQueue.java | 6 + .../sdk/java/internal/ModuleBackendMode.java | 648 ++++++++++++ .../count/sdk/java/internal/ModuleCrash.java | 6 + .../sdk/java/internal/ModuleRatingCore.java | 1 - .../sdk/java/internal/ModuleSessions.java | 2 +- .../count/sdk/java/internal/Networking.java | 2 +- .../ly/count/sdk/java/internal/SDKCore.java | 78 +- .../count/sdk/java/internal/SDKModules.java | 2 + .../count/sdk/java/internal/SessionImpl.java | 43 +- .../sdk/java/internal/UserEditorImpl.java | 5 + .../ly/count/sdk/java/internal/ViewImpl.java | 10 + .../sdk/java/internal/BackendModeTests.java | 932 ++++++++++++++++++ .../count/sdk/java/internal/ConfigTests.java | 97 +- .../count/sdk/java/internal/ConfigTests2.java | 182 ---- .../sdk/java/internal/SessionImplTests.java | 1 + 30 files changed, 2436 insertions(+), 246 deletions(-) delete mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 app-java/src/main/java/ly/count/java/demo/BackendModeExample.java create mode 100644 app-java/src/main/java/ly/count/java/demo/BackendModePerformanceTests.java create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/IStorageForRequestQueue.java create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleBackendMode.java create mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java delete mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests2.java diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460d8..000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 000000000..e96534fb2 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f658c5fa7..c9e95d9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +20.11.2 +* Added backend mode feature and a new configuration field to enable it. + 20.11.1 * Fixed a bug related to server response handling. * Fixed a potential issue with parameters tampering protection while adding checksum. diff --git a/app-java/src/main/java/ly/count/java/demo/BackendModeExample.java b/app-java/src/main/java/ly/count/java/demo/BackendModeExample.java new file mode 100644 index 000000000..b3b79afcf --- /dev/null +++ b/app-java/src/main/java/ly/count/java/demo/BackendModeExample.java @@ -0,0 +1,203 @@ +package ly.count.java.demo; + +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class BackendModeExample { + final static String DEVICE_ID = "device-id"; + final static String COUNTLY_APP_KEY = "YOUR_APP_KEY"; + final static String COUNTLY_SERVER_URL = "https://try.count.ly/"; + + static void recordDataWithLegacyCalls() { + // Record Event + Countly.api().event("Event With Sum And Count") + .setSum(23) + .setCount(2).record(); + + // Record view + Countly.api().view("Start view"); + + // Record Location + Countly.api().addLocation(31.5204, 74.3587); + + // Record user properties + Countly.api().user().edit() + .setName("Full name") + .setUsername("nickname") + .setEmail("test@test.com") + .setOrg("Tester") + .setPhone("+123456789") + .commit(); + + // Record crash + try { + int a = 10 / 0; + } catch (Exception e) { + Countly.api().addCrashReport(e, false, "Divided by zero", null, "sample app"); + } + + } + + public static void main(String[] args) throws Exception { + + Scanner scanner = new Scanner(System.in); + + Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY) + .setLoggingLevel(Config.LoggingLevel.DEBUG) + .enableBackendMode() + .setRequestQueueMaxSize(10000) + .setDeviceIdStrategy(Config.DeviceIdStrategy.UUID) + .setRequiresConsent(false) + .setEventsBufferSize(1000); + + // Countly needs persistent storage for requests, configuration storage, user profiles and other temporary data, + // therefore requires a separate data folder to run + + File targetFolder = new File("d:\\__COUNTLY\\java_test\\"); + + // Main initialization call, SDK can be used after this one is done + Countly.init(targetFolder, config); + boolean running = true; + while (running) { + + System.out.println("Choose your option: "); + + System.out.println("1) Record an event with key, count, sum, duration and segmentation"); + System.out.println("2) Record a view"); + System.out.println("3) Record user properties"); + System.out.println("4) Record an exception with throwable and segmentation"); + System.out.println("5) Record an exception with message, stacktrace and segmentation"); + System.out.println("6) Start session"); + System.out.println("7) Update session"); + System.out.println("8) End session"); + System.out.println("9) Record a direct request"); + System.out.println("99) Record data with legacy calls"); + System.out.println("0) Exit "); + + int input = scanner.nextInt(); + switch (input) { + case 0: + running = false; + break; + case 1: { // Record an event with key, count, sum, duration and segmentation + Map segment = new HashMap() {{ + put("Time Spent", 60); + put("Retry Attempts", 60); + }}; + + Countly.backendMode().recordEvent(DEVICE_ID, "Event Key", 1, 0.1, 5.0, segment, null); + } + break; + case 2: { // Record a view + Map segmentation = new HashMap() {{ + put("visit", "1"); + put("segment", "Windows"); + put("start", "1"); + }}; + + Countly.backendMode().recordView(DEVICE_ID, "SampleView", segmentation, 1646640780130L); + } + break; + case 3: { // record user detail and properties + Map userDetail = new HashMap<>(); + userDetail.put("name", "Full Name"); + userDetail.put("username", "username1"); + userDetail.put("email", "user@gmail.com"); + userDetail.put("organization", "Countly"); + userDetail.put("phone", "000-111-000"); + userDetail.put("gender", "M"); + userDetail.put("byear", "1991"); + //custom detail + userDetail.put("hair", "black"); + userDetail.put("height", 5.9); + userDetail.put("fav-colors", "{$push: black}"); + userDetail.put("marks", "{$inc: 1}"); + + Countly.backendMode().recordUserProperties(DEVICE_ID, userDetail, null); + } + break; + case 4: { // record an exception with throwable and segmentation + Map segmentation = new HashMap() {{ + put("login page", "authenticate request"); + }}; + Map crashDetails = new HashMap() {{ + put("_os", "Windows 11"); + put("_os_version", "11.202"); + put("_logs", "main page"); + }}; + try { + int a = 10 / 0; + } catch (Exception e) { + Countly.backendMode().recordException(DEVICE_ID, e, segmentation, crashDetails, null); + } + } + break; + case 5: { // record an exception with message, stacktrace and segmentation + Map segmentation = new HashMap() {{ + put("login page", "authenticate request"); + }}; + + Map crashDetails = new HashMap() {{ + put("_os", "Windows 11"); + put("_os_version", "11.202"); + put("_logs", "main page"); + }}; + try { + int a = 10 / 0; + } catch (Exception e) { + Countly.backendMode().recordException(DEVICE_ID, "Divided By Zero", "stack traces", segmentation, crashDetails, null); + } + } + break; + case 6: { // start a session + Map metrics = new HashMap() {{ + put("_os", "Android"); + put("_os_version", "10"); + put("_app_version", "1.2"); + }}; + + Map location = new HashMap() {{ + put("ip_address", "192.168.1.1"); + put("city", "Lahore"); + put("country_code", "PK"); + put("location", "31.5204,74.3587"); + }}; + + Countly.backendMode().sessionBegin(DEVICE_ID, metrics, location, null); + break; + } + case 7: // update session + Countly.backendMode().sessionUpdate(DEVICE_ID, 10, null); + break; + case 8: // end session + Countly.backendMode().sessionEnd(DEVICE_ID, 20, null); + break; + case 9: { // record a direct request + Map requestData = new HashMap<>(); + requestData.put("device_id", "id"); + requestData.put("timestamp", "1646640780130"); + requestData.put("end_session", "1"); + requestData.put("session_duration", "20.5"); + Countly.backendMode().recordDirectRequest(DEVICE_ID, requestData, null); + break; + } + case 99: // record data with legacy call + recordDataWithLegacyCalls(); + break; + default: + break; + } + + } + + // Gracefully stop SDK to stop all SDK threads and allow this app to exit + // Just in case, usually you don't want to clear data to reuse device id for next app runs + // and to send any requests which might not be sent + Countly.stop(false); + } +} diff --git a/app-java/src/main/java/ly/count/java/demo/BackendModePerformanceTests.java b/app-java/src/main/java/ly/count/java/demo/BackendModePerformanceTests.java new file mode 100644 index 000000000..56d34af57 --- /dev/null +++ b/app-java/src/main/java/ly/count/java/demo/BackendModePerformanceTests.java @@ -0,0 +1,213 @@ +package ly.count.java.demo; + +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import ly.count.sdk.java.internal.DeviceCore; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class BackendModePerformanceTests { + final static String DEVICE_ID = "device-id"; + final static String COUNTLY_APP_KEY = "YOUR_APP_KEY"; + final static String COUNTLY_SERVER_URL = "https://try.count.ly/"; + + + private static void initSDK(int eventQueueSize, int requestQueueSize) { + Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY) + .setLoggingLevel(Config.LoggingLevel.OFF) + .enableBackendMode() + .setRequestQueueMaxSize(requestQueueSize) + .setDeviceIdStrategy(Config.DeviceIdStrategy.UUID) + .setRequiresConsent(false) + .setEventsBufferSize(1); + + File targetFolder = new File("d:\\__COUNTLY\\java_test\\"); + + // Main initialization call, SDK can be used after this one is done + Countly.init(targetFolder, config); + } + + static void performLargeRequestQueueSizeTest() { + System.out.println("===== Test Started: 'Large request queue size' ====="); + int requestQSize = 1000000; + System.out.printf("Before SDK Initialization: Total Memory = %dMb, Available RAM = %dMb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + initSDK(1, requestQSize); + System.out.printf("After SDK Initialization: Total Memory = %d Mb, Available RAM= %d Mb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + + int batchSize = requestQSize / 25; + + System.out.printf("Adding %d requests(events) into request Queue%n", batchSize); + for (int i = 1; i < batchSize; ++i) { + + Map segment = new HashMap() {{ + put("Time Spent", 60); + put("Retry Attempts", 60); + }}; + + Countly.backendMode().recordEvent(DEVICE_ID, "Event Key " + i, 1, 0.1, 5.0, segment, null); + } + + System.out.printf("Adding %d requests(crash) into request Queue%n", batchSize); + for (int i = 1; i < batchSize; ++i) { + + Map segmentation = new HashMap() {{ + put("login page", "authenticate request"); + }}; + + Map crashDetails = new HashMap() {{ + put("_os", "Windows 11"); + put("_os_version", "11.202"); + put("_logs", "main page"); + }}; + + Countly.backendMode().recordException(DEVICE_ID, "Message: " + i, "stack traces " + 1, segmentation, crashDetails, + null); + } + + System.out.printf("Adding %d requests(user properties) into request Queue%n", batchSize); + for (int i = 1; i < batchSize; ++i) { + + // User detail + Map userDetail = new HashMap<>(); + userDetail.put("name", "Full Name"); + userDetail.put("username", "username1"); + userDetail.put("email", "user@gmail.com"); + userDetail.put("organization", "Countly"); + userDetail.put("phone", i); + userDetail.put("gender", "M"); + userDetail.put("byear", "1991"); + //custom detail + userDetail.put("hair", "black"); + userDetail.put("height", 5.9); + userDetail.put("fav-colors", "{$push: black}"); + userDetail.put("marks", "{$inc: 1}"); + + Countly.backendMode().recordUserProperties(DEVICE_ID, userDetail, null); + } + + System.out.printf("Adding %d requests(sessions) into request Queue%n", batchSize); + for (int i = 1; i < batchSize; ++i) { + Map metrics = new HashMap() {{ + put("_os", "Android"); + put("_os_version", "10"); + put("_app_version", "1.2"); + }}; + + Map location = new HashMap() {{ + put("ip_address", "192.168.1.1"); + put("city", "Lahore"); + put("country_code", "PK"); + put("location", "31.5204,74.3587"); + }}; + + Countly.backendMode().sessionBegin(DEVICE_ID, metrics, location, null); + } + + System.out.printf("After adding %d request: Total Memory = %d Mb, Available RAM= %d Mb %n", requestQSize, DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + + Countly.stop(false); + System.out.println("=====SDK Stop====="); + } + + static void performLargeEventQueueTest() { + int noOfEvents = 100000; + System.out.println("===== Test Start: 'Large Event queues against multiple devices ids' ====="); + System.out.printf("Before SDK Initialization: Total Memory = %dMb, Available RAM = %dMb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + initSDK(noOfEvents, 1000); + System.out.printf("After SDK Initialization: Total Memory = %d Mb, Available RAM= %d Mb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + + int noOfDevices = 10; + for (int d = 0; d <= noOfDevices; ++d) { + System.out.printf("Adding %d events into event Queue against deviceID = %s%n", 100000, "device-id-" + d); + for (int i = 1; i <= noOfEvents; ++i) { + + Map segment = new HashMap() {{ + put("Time Spent", 60); + put("Retry Attempts", 60); + }}; + + Countly.backendMode().recordEvent("device-id-" + d, "Event Key " + i, 1, 0.1, 5.0, segment, null); + } + } + System.out.printf("After adding %d events into event queue: Total Memory = %d Mb, Available RAM= %d Mb %n", noOfEvents * noOfDevices, DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + + Countly.stop(false); + System.out.println("=====SDK Stop====="); + } + + static void recordBulkDataAndSendToServer() throws InterruptedException { + + System.out.println("===== Test Start: 'Record bulk data to server' ====="); + System.out.printf("Before SDK Initialization: Total Memory = %dMb, Available RAM = %dMb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + initSDK(100, 1000); + System.out.printf("After SDK Initialization: Total Memory = %d Mb, Available RAM= %d Mb %n", DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + int countOfRequest = 10; + int remaining = countOfRequest; + int secondsToSleep = 5; + do { + if (Countly.backendMode().getQueueSize() >= 100) { + Thread.sleep(secondsToSleep * 1000); + } else { + if (remaining > 0) { + Map segment = new HashMap() {{ + put("Time Spent", 60); + put("Retry Attempts", 60); + }}; + + Countly.backendMode().recordEvent("device-id", "Event Key " + remaining, 1, 0.1, 5.0, segment, null); + --remaining; + } + + } + + } while (remaining != 0 || Countly.backendMode().getQueueSize() != 0); + + + System.out.printf("After successfully sending %d requests to server: Total Memory = %d Mb, Available RAM= %d Mb %n", countOfRequest, DeviceCore.dev.getRAMTotal(), DeviceCore.dev.getRAMAvailable()); + + Countly.stop(false); + System.out.println("=====SDK Stop====="); + } + + public static void main(String[] args) throws Exception { + boolean running = true; + long startTime = 0; + Scanner scanner = new Scanner(System.in); + while (running) { + + System.out.println("Choose your option: "); + + System.out.println("1) Perform Large Request Queue Size Test"); + System.out.println("2) Perform Large Event queues test"); + System.out.println("3) Record bulk data to server"); + + int input = scanner.nextInt(); + startTime = System.currentTimeMillis(); + switch (input) { + case 1: + performLargeRequestQueueSizeTest(); + running = false; + System.out.printf("Time spent: %dms%n", (System.currentTimeMillis() - startTime)); + break; + case 2: + performLargeEventQueueTest(); + running = false; + System.out.printf("Time spent: %dms%n", (System.currentTimeMillis() - startTime)); + break; + case 3: + startTime = System.currentTimeMillis(); + recordBulkDataAndSendToServer(); + running = false; + System.out.printf("Time spent: %dms%n", (System.currentTimeMillis() - startTime)); + break; + default: + break; + } + } + + System.out.println("Exit"); + } +} diff --git a/gradle.properties b/gradle.properties index 6dccbccb4..c9db11b04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ # org.gradle.parallel=true # RELEASE FIELD SECTION -VERSION_NAME=20.11.1 +VERSION_NAME=20.11.2 GROUP=ly.count.sdk POM_URL=https://github.com/Countly/countly-sdk-java @@ -30,9 +30,10 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_NAME=Countly +#SIGNING SECTION signing.keyId=xx signing.password=xx signing.secretKeyRingFile=xx -mavenCentralRepositoryUsername=xx -mavenCentralRepositoryPassword=xx +mavenCentralUsername=xx +mavenCentralPassword=xx diff --git a/sdk-java/build.gradle b/sdk-java/build.gradle index 096d05e5b..018d887f1 100644 --- a/sdk-java/build.gradle +++ b/sdk-java/build.gradle @@ -8,7 +8,7 @@ buildscript { //mavenLocal() } dependencies { - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.14.2' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0' //for publishing } } diff --git a/sdk-java/gradle.properties b/sdk-java/gradle.properties index 8f01b28a8..e2da0c37a 100644 --- a/sdk-java/gradle.properties +++ b/sdk-java/gradle.properties @@ -4,11 +4,3 @@ POM_ARTIFACT_ID=java POM_NAME=Countly Java SDK POM_DESCRIPTION=Java SDK for Countly mobile analytics POM_INCEPTION_YEAR=2019 - -#SIGNING SECTION -signing.keyId=XXXX -signing.password=XXXX -signing.secretKeyRingFile=XXXX - -mavenCentralRepositoryUsername=XXXX -mavenCentralRepositoryPassword=XXXX \ No newline at end of file diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Config.java b/sdk-java/src/main/java/ly/count/sdk/java/Config.java index 2c5c559d7..a5ee12d51 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Config.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Config.java @@ -265,7 +265,7 @@ public boolean restore(byte[] data) { /** * Countly SDK version to be sent in HTTP requests */ - protected String sdkVersion = "20.11.1"; + protected String sdkVersion = "20.11.2"; /** * Countly SDK name to be sent in HTTP requests @@ -282,6 +282,11 @@ public boolean restore(byte[] data) { */ protected boolean usePOST = false; + /** + * This would be a special state where the majority of the SDK calls don't work anymore and only a few special calls work. + */ + protected boolean enableBackendMode = false; + /** * Salt string for parameter tampering protection */ @@ -478,6 +483,11 @@ public boolean restore(byte[] data) { */ protected Long remoteConfigUpdateRequestTimeout = null; + /** + * Maximum in memory request queue size. + */ + protected int requestQueueMaxSize = 1000; + //endregion @@ -656,10 +666,33 @@ public Config setUsePOST(boolean usePOST) { return this; } + /** + * Enable SDK's backend mode. + * @return {@code this} instance for method chaining + */ + public Config enableBackendMode() { + this.enableBackendMode = true; + return this; + } + + public int getRequestQueueMaxSize() { + return requestQueueMaxSize; + } + + /** + * In backend mode set the in memory request queue size. + * @param requestQueueMaxSize int to set request queue maximum size for backend mode + * @return {@code this} instance for method chaining + */ + public Config setRequestQueueMaxSize(int requestQueueMaxSize) { + this.requestQueueMaxSize = requestQueueMaxSize; + return this; + } + /** * Enable parameter tampering protection * - * @param salt String to add to each request bebfore calculating checksum + * @param salt String to add to each request before calculating checksum * @return {@code this} instance for method chaining */ public Config enableParameterTamperingProtection(String salt) { @@ -1204,6 +1237,14 @@ public boolean isUsePOST() { return usePOST; } + /** + * Getter for {@link #enableBackendMode} + * @return {@link #enableBackendMode} value + */ + public boolean isBackendModeEnabled() { + return enableBackendMode; + } + /** * Getter for {@link #salt} * @return {@link #salt} value diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Countly.java b/sdk-java/src/main/java/ly/count/sdk/java/Countly.java index 141d52011..d8fe5e33a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Countly.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Countly.java @@ -4,6 +4,7 @@ import ly.count.sdk.java.internal.CtxImpl; import ly.count.sdk.java.internal.Device; +import ly.count.sdk.java.internal.ModuleBackendMode; import ly.count.sdk.java.internal.SDK; /** @@ -60,6 +61,23 @@ public static Session session(){ return Cly.session(cly.ctx); } + public static ModuleBackendMode.BackendMode backendMode(){ + if (!isInitialized()) { + L.wtf("Countly SDK is not initialized yet."); + return null; + } else { + ModuleBackendMode mbm = cly.sdk.module(ModuleBackendMode.class); + if (cly.ctx.getConfig().enableBackendMode && mbm != null) { + return mbm.new BackendMode(); + } + //if it is null, feature was not enabled, return mock + L.w("BackendMode was not enabled, returning dummy module"); + ModuleBackendMode emptyMbm = new ModuleBackendMode(); + emptyMbm.disableModule(); + return emptyMbm.new BackendMode(); + } + } + /** * Returns active {@link Session} if any or {@code null} otherwise. * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/CountlyLifecycle.java b/sdk-java/src/main/java/ly/count/sdk/java/CountlyLifecycle.java index 201dc9f94..200e03896 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/CountlyLifecycle.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/CountlyLifecycle.java @@ -42,6 +42,15 @@ public static void init (final File directory, final Config config) { stop(false); } + if(config.enableBackendMode) { + config.sdkName = "java-native-backend"; + } + + if(config.requestQueueMaxSize < 1) { + L.e("init: Request queue max size can not be less than 1."); + config.requestQueueMaxSize = 1; + } + SDK sdk = new SDK(); sdk.init(new CtxImpl(sdk, new InternalConfig(config), directory)); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java index 539479a02..e0769d0b9 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java @@ -17,6 +17,7 @@ public enum CoreFeature { PerformanceMonitoring(1 << 14); */ + BackendMode(1 << 12), RemoteConfig(1 << 13), TestDummy(1 << 19),//used during testing DeviceId(1 << 20), diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/DefaultNetworking.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/DefaultNetworking.java index a4460793b..e0f3d02be 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/DefaultNetworking.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/DefaultNetworking.java @@ -6,13 +6,15 @@ public class DefaultNetworking implements Networking { private Transport transport; private Tasks tasks; private boolean shutdown; + IStorageForRequestQueue storageForRequestQueue; @Override - public void init(CtxCore ctx) { + public void init(CtxCore ctx, IStorageForRequestQueue storageForRequestQueue) { shutdown = false; transport = new Transport(); transport.init(ctx.getConfig()); tasks = new Tasks("network"); + this.storageForRequestQueue = storageForRequestQueue; } @Override @@ -33,7 +35,7 @@ protected Tasks.Task submit(final CtxCore ctx) { return new Tasks.Task(Tasks.ID_STRICT) { @Override public Boolean call() throws Exception { - final Request request = Storage.readOne(ctx, new Request(0L), true); + final Request request = storageForRequestQueue.getNextRequest(); if (request == null) { return false; } else { @@ -52,7 +54,7 @@ public Boolean call() throws Exception { public void call(Boolean result) throws Exception { L.d("Request " + request.storageId() + " sent?: " + result); if (result) { - Storage.remove(ctx, request); + storageForRequestQueue.removeRequest(request); check(ctx); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/DeviceCore.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/DeviceCore.java index c9171cc87..dd56b2619 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/DeviceCore.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/DeviceCore.java @@ -212,6 +212,10 @@ public int currentHour() { return Calendar.getInstance().get(Calendar.HOUR_OF_DAY); } + public int getHourFromCalendar(Calendar calendar) { + return calendar.get(Calendar.HOUR_OF_DAY); + } + /** * Convert time in nanoseconds to milliseconds * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/EventImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/EventImpl.java index 80b37e2e3..3c6a014c1 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/EventImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/EventImpl.java @@ -58,6 +58,11 @@ public interface EventRecorder { @Override public void record() { + if(SDKCore.instance != null && SDKCore.instance.config.isBackendModeEnabled()) { + L.w("record: Skipping event, backend mode is enabled!"); + return; + } + if (recorder != null && !invalid) { invalid = true; recorder.recordEvent(this); @@ -68,6 +73,11 @@ public void record() { @Override public void endAndRecord() { + if(SDKCore.instance != null && SDKCore.instance.config.isBackendModeEnabled()) { + L.w("endAndRecord: Skipping event, backend mode is enabled!"); + return; + } + setDuration((DeviceCore.dev.uniqueTimestamp() - timestamp) / 1000); record(); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/IStorageForRequestQueue.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/IStorageForRequestQueue.java new file mode 100644 index 000000000..40a7db763 --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/IStorageForRequestQueue.java @@ -0,0 +1,6 @@ +package ly.count.sdk.java.internal; + +public interface IStorageForRequestQueue { + Request getNextRequest(); + Boolean removeRequest(Request request); +} diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleBackendMode.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleBackendMode.java new file mode 100644 index 000000000..71796138b --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleBackendMode.java @@ -0,0 +1,648 @@ +package ly.count.sdk.java.internal; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ModuleBackendMode extends ModuleBase { + + protected static final Log.Module L = Log.module("BackendMode"); + protected InternalConfig internalConfig = null; + protected CtxCore ctx = null; + + //disabled is set when a empty module is created + //in instances when the rating feature was not enabled + //when a module is disabled, developer facing functions do nothing + protected boolean disabledModule = false; + + protected int eventQSize = 0; + protected final Map eventQueues = new HashMap<>(); + + private ScheduledExecutorService executor = null; + + String[] userPredefinedKeys = {"name", "username", "email", "organization", "phone", "gender", "byear"}; + + @Override + public void init(InternalConfig config) { + internalConfig = config; + L.d("init: config = " + config); + } + + @Override + public void onContextAcquired(CtxCore ctx) { + this.ctx = ctx; + L.d("onContextAcquired: " + ctx.toString()); + + if (ctx.getConfig().isBackendModeEnabled() && ctx.getConfig().getSendUpdateEachSeconds() > 0 && executor == null) { + executor = Executors.newScheduledThreadPool(1); + executor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + addEventsToRequestQ(); + } + }, ctx.getConfig().getSendUpdateEachSeconds(), ctx.getConfig().getSendUpdateEachSeconds(), TimeUnit.SECONDS); + } + } + + @Override + public Integer getFeature() { + return CoreFeature.BackendMode.getIndex(); + } + + @Override + public Boolean onRequest(Request request) { + return true; + } + + @Override + public void onRequestCompleted(Request request, String response, int responseCode) { + + } + + @Override + public void stop(CtxCore ctx, boolean clear) { + super.stop(ctx, clear); + executor.shutdownNow(); + } + + public void disableModule() { + disabledModule = true; + } + + + private void recordEventInternal(String deviceID, String key, int count, Double sum, Double dur, Map segmentation, Long timestamp) { + L.d(String.format("recordEventInternal: deviceID = %s, key = %s,, count = %d, sum = %f, dur = %f, segmentation = %s, timestamp = %d", deviceID, key, count, sum, dur, segmentation, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + removeInvalidDataFromSegments(segmentation); + + JSONObject jsonObject = buildEventJSONObject(key, count, sum, dur, segmentation, timestamp); + + if (!eventQueues.containsKey(deviceID)) { + eventQueues.put(deviceID, new JSONArray()); + } + eventQueues.get(deviceID).put(jsonObject); + ++eventQSize; + + if (eventQSize >= internalConfig.getEventsBufferSize()) { + addEventsToRequestQ(); + } + } + + private void sessionBeginInternal(String deviceID, Map metrics, Map location, Long timestamp) { + L.d(String.format("sessionBeginInternal: deviceID = %s, timestamp = %d", deviceID, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + Request request = new Request(); + request.params.add("device_id", deviceID); + request.params.add("begin_session", 1); + + JSONObject metricsJson = new JSONObject(metrics); + request.params.add("metrics", metricsJson); + + if (location != null) { + for (Map.Entry entry : location.entrySet()) { + request.params.add(entry.getKey(), entry.getValue()); + } + } + + addTimeInfoIntoRequest(request, timestamp); + + addRequestToRequestQ(request); + } + + private void sessionUpdateInternal(String deviceID, Double duration, Long timestamp) { + L.d(String.format("sessionUpdateInternal: deviceID = %s, duration = %f, timestamp = %d", deviceID, duration, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + Request request = new Request(); + request.params.add("device_id", deviceID); + request.params.add("session_duration", duration); + + addTimeInfoIntoRequest(request, timestamp); + addRequestToRequestQ(request); + } + + private void sessionEndInternal(String deviceID, double duration, Long timestamp) { + L.d(String.format("sessionEndInternal: deviceID = %s, duration = %f, timestamp = %d", deviceID, duration, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + //Add events against device ID to request Q + addEventsAgainstDeviceIdToRequestQ(deviceID); + eventQueues.remove(deviceID); + + Request request = new Request(); + request.params.add("device_id", deviceID); + request.params.add("end_session", 1); + request.params.add("session_duration", duration); + + addTimeInfoIntoRequest(request, timestamp); + addRequestToRequestQ(request); + } + + public void recordExceptionInternal(String deviceID, String message, String stacktrace, Map segmentation, Map crashDetails, Long timestamp) { + L.d(String.format("recordExceptionInternal: deviceID = %s, message = %s, stacktrace = %s, segmentation = %s, timestamp = %d", deviceID, message, stacktrace, segmentation, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + removeInvalidDataFromSegments(segmentation); + + JSONObject crash = new JSONObject(); + crash.put("_error", stacktrace); + crash.put("_custom", segmentation); + crash.put("_name", message); + + if (crashDetails != null && !crashDetails.isEmpty()) { + removeInvalidDataFromSegments(segmentation); + for (Map.Entry entry : crashDetails.entrySet()) { + crash.put(entry.getKey(), entry.getValue()); + } + } + + Request request = new Request(); + request.params.add("device_id", deviceID); + request.params.add("crash", crash); + + addTimeInfoIntoRequest(request, timestamp); + + addRequestToRequestQ(request); + } + + private void recordUserPropertiesInternal(String deviceID, Map userProperties, Long timestamp) { + L.d(String.format("recordUserPropertiesInternal: deviceID = %s, userProperties = %s, timestamp = %d", deviceID, userProperties, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + removeInvalidDataFromSegments(userProperties); + + Map userDetail = new HashMap<>(); + Map customDetail = new HashMap<>(); + for (Map.Entry item : userProperties.entrySet()) { + if (Arrays.stream(userPredefinedKeys).anyMatch(item.getKey()::equalsIgnoreCase)) { + userDetail.put(item.getKey(), item.getValue()); + } else { + Object v = item.getValue(); + if (v instanceof String) { + String value = (String) v; + if (!value.isEmpty() && value.charAt(0) == '{') { + try { + v = new JSONObject(value); + } catch (Exception ignored) { + } + } + } + customDetail.put(item.getKey(), v); + } + } + + userDetail.put("custom", customDetail); + + Request request = new Request(); + JSONObject properties = new JSONObject(userDetail); + + request.params.add("device_id", deviceID); + request.params.add("user_details", properties); + + addTimeInfoIntoRequest(request, timestamp); + addRequestToRequestQ(request); + } + + void recordDirectRequestInternal(String deviceID, Map requestData, Long timestamp) { + L.d(String.format("recordDirectRequestInternal: deviceID = %s, requestJson = %s, timestamp = %d", deviceID, requestData, timestamp)); + + if (timestamp == null || timestamp < 1) { + timestamp = DeviceCore.dev.uniqueTimestamp(); + } + + Request request = new Request(); + request.params.add("device_id", deviceID); + addTimeInfoIntoRequest(request, timestamp); + + //remove checksum, will add before sending request to server + requestData.remove("checksum"); + requestData.remove("checksum256"); + requestData.remove("sdk_name"); + requestData.remove("sdk_version"); + + for (Map.Entry item : requestData.entrySet()) { + request.params.add(item.getKey(), item.getValue()); + } + + addRequestToRequestQ(request); + } + + private JSONObject buildEventJSONObject(String key, int count, Double sum, Double dur, Map segmentation, Long timestamp) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + final int hour = calendar.get(Calendar.HOUR_OF_DAY); + final int dow = calendar.get(Calendar.DAY_OF_WEEK) - 1; + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("key", key); + + jsonObject.put("sum", sum); + + if (count > 0) { + jsonObject.put("count", count); + } + if (dur != null) { + jsonObject.put("dur", dur); + } + + jsonObject.put("segmentation", segmentation); + jsonObject.put("dow", dow); + jsonObject.put("hour", hour); + jsonObject.put("timestamp", timestamp); + + L.d(String.format("buildEventJSONObject: jsonObject = %s", jsonObject)); + + return jsonObject; + } + + private void addTimeInfoIntoRequest(Request request, Long timestamp) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + + final int hour = calendar.get(Calendar.HOUR_OF_DAY); + final int dow = calendar.get(Calendar.DAY_OF_WEEK) - 1; + + request.params.add("dow", dow); + request.params.add("hour", hour); + request.params.add("timestamp", timestamp); + request.params.add("tz", DeviceCore.dev.getTimezoneOffset()); + } + + private void addEventsAgainstDeviceIdToRequestQ(String deviceID) { + JSONArray events = eventQueues.get(deviceID); + if (events == null || events.isEmpty()) { + return; + } + + eventQSize -= events.length(); + + Request request = new Request(); + request.params.add("device_id", deviceID); + request.params.add("events", events); + addTimeInfoIntoRequest(request, System.currentTimeMillis()); + request.own(ModuleBackendMode.class); + addRequestToRequestQ(request); + } + + private void addEventsToRequestQ() { + L.d("addEventsToRequestQ"); + + for (String s : eventQueues.keySet()) { + addEventsAgainstDeviceIdToRequestQ(s); + } + + eventQSize = 0; + eventQueues.clear(); + + } + + private void addRequestToRequestQ(Request request) { + L.d("addRequestToRequestQ"); + if (internalConfig.getRequestQueueMaxSize() == SDKCore.instance.requestQueueMemory.size()) { + L.d("addRequestToRequestQ: In Memory request queue is full, dropping oldest request: " + request.params.toString()); + SDKCore.instance.requestQueueMemory.remove(); + } + + SDKCore.instance.requestQueueMemory.add(request); + SDKCore.instance.networking.check(ctx); + } + + protected Map removeInvalidDataFromSegments(Map segments) { + + if (segments == null || segments.isEmpty()) { + return segments; + } + + int i = 0; + List toRemove = new ArrayList<>(); + for (Map.Entry item : segments.entrySet()) { + Object type = item.getValue(); + + boolean isValidDataType = (type instanceof Boolean + || type instanceof Integer + || type instanceof Long + || type instanceof String + || type instanceof Double + || type instanceof Float + ); + + if (!isValidDataType) { + toRemove.add(item.getKey()); + L.w("RemoveSegmentInvalidDataTypes: In segmentation Data type '" + type + "' of item '" + item.getValue() + "' isn't valid."); + } + } + + for (String k : toRemove) { + segments.remove(k); + } + + return segments; + } + + public class BackendMode { + /** + * Record a view. + * + * @param deviceID device id, cannot be null or empty + * @param name String representing name of this View, cannot be null or empty + * @param segmentation additional view segmentation you want to set, leave null if you don't want to add anything + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordView(String deviceID, String name, Map segmentation, Long timestamp) { + L.i(String.format(":recordView: deviceID = %s, key = %s, segmentation = %s, timestamp = %d", deviceID, name, segmentation, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordView: DeviceID can not be null or empty."); + return; + } + + if (name == null || name.isEmpty()) { + L.e("recordView: Name can not be null or empty."); + return; + } + + if (segmentation == null) { + segmentation = new HashMap<>(); + } + + segmentation.put("name", name); + + recordEventInternal(deviceID, "[CLY]_view", 1, null, null, segmentation, timestamp); + } + + /** + * Record an event. + * + * @param deviceID device id, cannot be null or empty + * @param key key for this event, cannot be null or empty + * @param count how many of these events have occurred, default value is "1" + * @param sum set sum if needed, leave null if you don't have it. + * @param dur set duration if needed, default value is "0" + * @param segmentation additional view segmentation you want to set, leave null if you don't want to add anything + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordEvent(String deviceID, String key, int count, Double sum, Double dur, Map segmentation, Long timestamp) { + L.i(String.format("recordEvent: deviceID = %s, key = %s, count = %d, sum = %f, dur = %f, segmentation = %s, timestamp = %d", deviceID, key, count, sum, dur, segmentation, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordEvent: DeviceID can not be null or empty."); + return; + } + + if (key == null || key.isEmpty()) { + L.e("recordEvent: Event key can not be null or empty."); + return; + } + + if (count < 1) { + count = 1; + } + + recordEventInternal(deviceID, key, count, sum, dur, segmentation, timestamp); + } + + /** + * Start the session. + * + * @param deviceID device id, cannot be null or empty + * @param metrics additional information you want to set, leave null if you don't want to add anything + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void sessionBegin(String deviceID, Map metrics, Map location, Long timestamp) { + L.i(String.format("sessionBegin: deviceID = %s, timestamp = %d", deviceID, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("sessionBegin: DeviceID can not be null or empty."); + return; + } + + sessionBeginInternal(deviceID, metrics, location, timestamp); + } + + /** + * Send update request to the server saying that user is still using the app. + * + * @param deviceID device id, cannot be null or empty + * @param duration app usage duration + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void sessionUpdate(String deviceID, double duration, Long timestamp) { + L.i(String.format("sessionUpdate: deviceID = %s, duration = %f, timestamp = %d", deviceID, duration, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("sessionUpdate: DeviceID can not be null or empty."); + return; + } + + if (duration < 0) { + duration = 0; + } + + sessionUpdateInternal(deviceID, duration, timestamp); + } + + /** + * End this session, add corresponding request to queue + * + * @param deviceID device id, cannot be null or empty + * @param duration app usage duration + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void sessionEnd(String deviceID, double duration, Long timestamp) { + L.i(String.format("sessionEnd: deviceID = %s, duration = %f, timestamp = %d", deviceID, duration, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("sessionEnd: DeviceID can not be null or empty."); + return; + } + + if (duration < 0) { + duration = 0; + } + + sessionEndInternal(deviceID, duration, timestamp); + } + + /** + * Record a crash. + * + * @param deviceID device id, cannot be null or empty + * @param throwable {@link Throwable} to log + * @param segmentation (optional, can be {@code null}) additional crash segments map + * @param crashDetails (optional, can be {@code null}) a map contains crash detail + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordException(String deviceID, Throwable throwable, Map segmentation, Map crashDetails, Long timestamp) { + L.i(String.format("recordException: deviceID = %s, throwable = %s, segmentation = %s, timestamp = %d", deviceID, throwable, segmentation, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordException: DeviceID can not be null or empty."); + return; + } + + if (throwable == null) { + L.e("recordException: throwable can not be null."); + return; + } + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + + recordExceptionInternal(deviceID, throwable.getMessage(), sw.toString(), segmentation, crashDetails, timestamp); + } + + /** + * Record a crash. + * + * @param deviceID device id, cannot be null or empty + * @param message a string that contain detailed description of the exception + * @param stacktrace a string that describes the contents of the callstack. + * @param segmentation (optional, can be {@code null}) additional crash information + * @param crashDetails (optional, can be {@code null}) a map contains crash detail + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordException(String deviceID, String message, String stacktrace, Map segmentation, Map crashDetails, Long timestamp) { + L.i(String.format("recordException: deviceID = %s, message = %s, stacktrace = %s, segmentation = %s, timestamp = %d", deviceID, message, stacktrace, segmentation, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordException: DeviceID can not be null or empty."); + return; + } + + if (message == null || message.isEmpty()) { + L.e("recordException: message can not be null or empty."); + return; + } + + if (stacktrace == null || stacktrace.isEmpty()) { + L.e("recordException: stacktrace can not be null."); + return; + } + + recordExceptionInternal(deviceID, message, stacktrace, segmentation, crashDetails, timestamp); + } + + /** + * Record user detail and user custom detail. + * + * @param deviceID device id, cannot be null or empty + * @param userProperties a map contains user detail, it can not be null or empty + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordUserProperties(String deviceID, Map userProperties, Long timestamp) { + L.i(String.format("recordUserProperties: deviceID = %s, userProperties = %s, timestamp = %d", deviceID, userProperties, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordUserProperties: DeviceID can not be null or empty."); + return; + } + if (userProperties == null || userProperties.isEmpty()) { + L.e("recordUserProperties: userProperties can not be null or empty."); + return; + } + + recordUserPropertiesInternal(deviceID, userProperties, timestamp); + } + + /** + * Record a direct request. + * + * @param deviceID device id, cannot be null or empty + * @param requestData a map contains request data, it can not be null or empty + * @param timestamp record time in milliseconds, leave null if you don't have it + */ + public void recordDirectRequest(String deviceID, Map requestData, Long timestamp) { + L.i(String.format("recordDirectRequest: deviceID = %s, requestData = %s, timestamp = %d", deviceID, requestData, timestamp)); + + if (disabledModule) { + return; + } + + if (deviceID == null || deviceID.isEmpty()) { + L.e("recordDirectRequest: DeviceID can not be null or empty."); + return; + } + if (requestData == null || requestData.isEmpty()) { + L.e("recordDirectRequest: requestData can not be null or empty."); + return; + } + recordDirectRequestInternal(deviceID, requestData, timestamp); + } + + /** + * Return queue size + * + * @return sum of request queue size and event queue size + */ + public int getQueueSize() { + int queueSize = 0; + int eSize = eventQueues.size(); + int rSize = SDKCore.instance.requestQueueMemory.size(); + + return rSize + eSize; + } + + protected ModuleBase getModule() { + return ModuleBackendMode.this; + } + } +} diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleCrash.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleCrash.java index 681125614..cc92529b5 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleCrash.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleCrash.java @@ -78,6 +78,12 @@ public Integer getFeature() { } public CrashImplCore onCrash(CtxCore ctx, Throwable t, boolean fatal, String name, Map segments, String... logs) { + + if(ctx.getConfig().isBackendModeEnabled()) { + L.w("onCrash: Skipping crash, backend mode is enabled!"); + return null; + } + if (t == null) { L.e("Throwable cannot be null"); return null; diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleRatingCore.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleRatingCore.java index 1e430e44e..49a8445bb 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleRatingCore.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleRatingCore.java @@ -15,7 +15,6 @@ public class ModuleRatingCore extends ModuleBase { //in instances when the rating feature was not enabled //when a module is disabled, developer facing functions do nothing protected boolean disabledModule = false; - public final static Long storableStorageId = 123L; public final static String storableStoragePrefix = "rating"; diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleSessions.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleSessions.java index 9b98b7579..9111f5be2 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleSessions.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleSessions.java @@ -58,7 +58,7 @@ public void onContextAcquired(CtxCore ctx) { executor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { - if (isActive() && getSession() != null) { + if (!ctx.getConfig().isBackendModeEnabled() && isActive() && getSession() != null) { L.i("updating session"); getSession().update(); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/Networking.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/Networking.java index 8945b5835..01a2e8ecd 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/Networking.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/Networking.java @@ -1,7 +1,7 @@ package ly.count.sdk.java.internal; public interface Networking { - void init(CtxCore ctx); + void init(CtxCore ctx, IStorageForRequestQueue storageForRequestQueue); boolean isSending(); boolean check(CtxCore ctx); void stop(CtxCore ctx); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java index 0db3e5b8b..818ec5bb4 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java @@ -1,16 +1,11 @@ package ly.count.sdk.java.internal; +import ly.count.sdk.java.Config; import org.json.JSONObject; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.Future; -import ly.count.sdk.java.Config; - public abstract class SDKCore extends SDKModules { private static final Log.Module L = Log.module("SDKCore"); @@ -19,6 +14,7 @@ public abstract class SDKCore extends SDKModules { private UserImpl user; public InternalConfig config; protected Networking networking; + protected Queue requestQueueMemory = null; public enum Signal { DID(1), @@ -28,14 +24,19 @@ public enum Signal { private final int index; - Signal(int index){ this.index = index; } + Signal(int index) { + this.index = index; + } - public int getIndex(){ return index; } + public int getIndex() { + return index; + } } protected SDKCore() { this.modules = new TreeMap<>(); instance = this; + } protected InternalConfig prepareConfig(CtxCore ctx) { @@ -62,6 +63,7 @@ public void init(final CtxCore ctx) { super.init(ctx); + requestQueueMemory = new ArrayDeque<>(config.getRequestQueueMaxSize()); // ModuleSessions is always enabled, even without consent int consents = ctx.getConfig().getFeatures1() | CoreFeature.Sessions.getIndex(); // build modules @@ -91,7 +93,40 @@ public void run(int feature, Module module) { if (config.isDefaultNetworking()) { networking = new DefaultNetworking(); - networking.init(ctx); + + if (config.isBackendModeEnabled()) { + //Backend mode is enabled, we will use memory only request queue. + networking.init(ctx, new IStorageForRequestQueue() { + @Override + public Request getNextRequest() { + if(requestQueueMemory.isEmpty()) { + return null; + } + + return requestQueueMemory.element(); + } + + @Override + public Boolean removeRequest(Request request) { + return requestQueueMemory.remove(request); + } + }); + } else { + // Backend mode isn't enabled, we use persistent file storage. + networking.init(ctx, new IStorageForRequestQueue() { + @Override + public Request getNextRequest() { + return Storage.readOne(ctx, new Request(0L), true); + } + + @Override + public Boolean removeRequest(Request request) { + return Storage.remove(ctx, request); + } + }); + } + + networking.check(ctx); } @@ -157,7 +192,7 @@ public UserImpl user() { } TimedEvents timedEvents() { - return ((ModuleSessions)module(CoreFeature.Sessions.getIndex())).timedEvents(); + return ((ModuleSessions) module(CoreFeature.Sessions.getIndex())).timedEvents(); } @Override @@ -230,23 +265,23 @@ public void onDeviceId(CtxCore ctx, Config.DID id, Config.DID old) { public Future acquireId(final CtxCore ctx, final Config.DID holder, final boolean fallbackAllowed, final Tasks.Callback callback) { - return ((ModuleDeviceIdCore)module(CoreFeature.DeviceId.getIndex())).acquireId(ctx, holder, fallbackAllowed, callback); + return ((ModuleDeviceIdCore) module(CoreFeature.DeviceId.getIndex())).acquireId(ctx, holder, fallbackAllowed, callback); } public void login(CtxCore ctx, String id) { - ((ModuleDeviceIdCore)module(CoreFeature.DeviceId.getIndex())).login(ctx, id); + ((ModuleDeviceIdCore) module(CoreFeature.DeviceId.getIndex())).login(ctx, id); } public void logout(CtxCore ctx) { - ((ModuleDeviceIdCore)module(CoreFeature.DeviceId.getIndex())).logout(ctx); + ((ModuleDeviceIdCore) module(CoreFeature.DeviceId.getIndex())).logout(ctx); } public void changeDeviceIdWithoutMerge(CtxCore ctx, String id) { - ((ModuleDeviceIdCore)module(CoreFeature.DeviceId.getIndex())).changeDeviceId(ctx, id, false); + ((ModuleDeviceIdCore) module(CoreFeature.DeviceId.getIndex())).changeDeviceId(ctx, id, false); } public void changeDeviceIdWithMerge(CtxCore ctx, String id) { - ((ModuleDeviceIdCore)module(CoreFeature.DeviceId.getIndex())).changeDeviceId(ctx, id, true); + ((ModuleDeviceIdCore) module(CoreFeature.DeviceId.getIndex())).changeDeviceId(ctx, id, true); } public static boolean enabled(int feature) { @@ -258,8 +293,8 @@ public static boolean enabled(CoreFeature feature) { return enabled(feature.getIndex()); } - public boolean hasConsentForFeature(CoreFeature feature){ - if(!instance.config.requiresConsent()){ + public boolean hasConsentForFeature(CoreFeature feature) { + if (!instance.config.requiresConsent()) { //if no consent required, return true return true; } @@ -272,7 +307,7 @@ public Boolean isRequestReady(Request request) { if (cls == null) { return true; } else { - ModuleBase module = (ModuleBase)module(cls); + ModuleBase module = (ModuleBase) module(cls); request.params.remove(Request.MODULE); if (module == null) { return true; @@ -286,9 +321,10 @@ public Boolean isRequestReady(Request request) { * After a network request has been finished * propagate that response to the module * that owns the request + * * @param request the request that was sent, used to identify the request */ - public void onRequestCompleted(Request request, String response, int responseCode, Class requestOwner){ + public void onRequestCompleted(Request request, String response, int responseCode, Class requestOwner) { if (requestOwner != null) { Module module = module(requestOwner); @@ -298,7 +334,7 @@ public void onRequestCompleted(Request request, String response, int responseCod } } - protected void recover (CtxCore ctx) { + protected void recover(CtxCore ctx) { List crashes = Storage.list(ctx, CrashImplCore.getStoragePrefix()); for (Long id : crashes) { diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKModules.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKModules.java index 275d56943..4d0de9c6c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKModules.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SDKModules.java @@ -30,6 +30,7 @@ protected static void registerDefaultModuleMapping(int feature, Class begin(Long now) { @Override public Session update() { + if(ctx.getConfig().isBackendModeEnabled()) { + L.w("update: Skipping session update, backend mode is enabled!"); + return this; + } + L.d("update"); update(null); return this; @@ -159,6 +169,11 @@ Future update(Long now) { @Override public void end() { + if(ctx.getConfig().isBackendModeEnabled()) { + L.w("end: Skipping session end, backend mode is enabled!"); + return; + } + L.d("end"); end(null, null, null); } @@ -274,7 +289,7 @@ public Event timedEvent(String key) { public void recordEvent(Event event) { L.d("recordEvent: " + event.toString()); if (!SDKCore.enabled(CoreFeature.Events)) { - L.i("Skipping event - feature is not enabled"); + L.i("recordEvent: Skipping event - feature is not enabled"); return; } if (began == null) { @@ -311,8 +326,13 @@ public Session addCrashReport(Throwable t, boolean fatal) { @Override public Session addCrashReport(Throwable t, boolean fatal, String name, Map segments, String... logs) { + if(ctx.getConfig().isBackendModeEnabled()) { + L.w("addCrashReport: Skipping crash, backend mode is enabled!"); + return this; + } + if (!SDKCore.enabled(CoreFeature.CrashReporting)) { - L.i("Skipping event - feature is not enabled"); + L.i("addCrashReport: Skipping event - feature is not enabled"); return this; } SDKCore.instance.onCrash(ctx, t, fatal, name, segments, logs); @@ -321,9 +341,14 @@ public Session addCrashReport(Throwable t, boolean fatal, String name, Map cohortsAdded = new HashSet<>(); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewImpl.java index 33a8dc255..4e7bd6db1 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewImpl.java @@ -36,6 +36,11 @@ class ViewImpl implements View { @Override public void start(boolean firstView) { + if(SDKCore.instance != null && SDKCore.instance.config.isBackendModeEnabled()) { + L.w("start: Skipping view, backend mode is enabled!"); + return; + } + L.d("start: firstView = " + firstView); if (started) { return; @@ -56,6 +61,11 @@ public void start(boolean firstView) { @Override public void stop(boolean lastView) { + if(SDKCore.instance != null && SDKCore.instance.config.isBackendModeEnabled()) { + L.w("stop: Skipping view, backend mode is enabled!"); + return; + } + L.d("stop: lastView = " + lastView); if (ended) { diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java new file mode 100644 index 000000000..774590874 --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java @@ -0,0 +1,932 @@ +package ly.count.sdk.java.internal; + +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.*; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +@RunWith(JUnit4.class) +public class BackendModeTests { + private ModuleBackendMode moduleBackendMode; + + @BeforeClass + public static void init() { + Config cc = new Config("https://try.count.ly", "COUNTLY_APP_KEY"); + cc.setEventsBufferSize(4).enableBackendMode(); + + File targetFolder = new File("d:\\__COUNTLY\\java_test\\"); + Countly.init(targetFolder, cc); + } + + @AfterClass + public static void stop() throws Exception { + Countly.stop(false); + } + + @Before + public void start() { + moduleBackendMode = (ModuleBackendMode) Countly.backendMode().getModule(); + } + + @After + public void end() { + moduleBackendMode.eventQSize = 0; + SDKCore.instance.requestQueueMemory.clear(); + moduleBackendMode.eventQueues.clear(); + } + + /** + * It validates the SDK name and 'enableBackendMode' in configuration. + */ + @Test + public void testConfigurationValues() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Assert.assertTrue(moduleBackendMode.internalConfig.isBackendModeEnabled()); + Assert.assertEquals(4, moduleBackendMode.internalConfig.getEventsBufferSize()); + Assert.assertEquals("java-native-backend", moduleBackendMode.internalConfig.getSdkName()); + Assert.assertEquals(1000, moduleBackendMode.internalConfig.getRequestQueueMaxSize()); + } + + /** + * It validates the functionality of 'recordView' method. + */ + @Test + public void testMethodRecordView() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap() {{ + put("name", "SampleView"); + put("visit", "1"); + put("segment", "Windows"); + put("start", "1"); + }}; + + + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + backendMode.recordView("device-id-1", "SampleView", segmentation, 1646640780130L); + + JSONArray events = moduleBackendMode.eventQueues.get("device-id-1"); + Assert.assertEquals(1L, events.length()); + Assert.assertEquals(1L, moduleBackendMode.eventQSize); + Assert.assertEquals(1, backendMode.getQueueSize()); + + JSONObject event = events.getJSONObject(0); + validateEventFields("[CLY]_view", 1, null, null, 1, 13, 1646640780130L, event); + + JSONObject segments = event.getJSONObject("segmentation"); + Assert.assertEquals("SampleView", segments.get("name")); + Assert.assertEquals("1", segments.get("visit")); + Assert.assertEquals("Windows", segments.get("segment")); + Assert.assertEquals("1", segments.get("start")); + + backendMode.recordView("device-id-2", "SampleView2", null, 1646640780130L); + + events = moduleBackendMode.eventQueues.get("device-id-2"); + Assert.assertEquals(1L, events.length()); + Assert.assertEquals(2L, moduleBackendMode.eventQSize); + Assert.assertEquals(2, backendMode.getQueueSize()); + + event = events.getJSONObject(0); + validateEventFields("[CLY]_view", 1, null, null, 1, 13, 1646640780130L, event); + + segments = event.getJSONObject("segmentation"); + Assert.assertEquals("SampleView2", segments.get("name")); + } + + /** + * It validates the functionality of 'recordView' method against invalid data. + */ + @Test + public void testMethodRecordViewWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap() {{ + put("name", "SampleView"); + put("visit", "1"); + put("segment", "Windows"); + put("start", "1"); + }}; + + /* Invalid Device ID */ + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + backendMode.recordView("", "SampleView1", segmentation, 1646640780130L); + backendMode.recordView(null, "SampleView1", segmentation, 1646640780130L); + + Assert.assertTrue(moduleBackendMode.eventQueues.isEmpty()); + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + + /* Invalid view name */ + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + backendMode.recordView("device-id-1", "", segmentation, 1646640780130L); + backendMode.recordView("device-id-2", null, segmentation, 1646640780130L); + + Assert.assertTrue(moduleBackendMode.eventQueues.isEmpty()); + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + Assert.assertEquals(0, backendMode.getQueueSize()); + } + + /** + * It validates the functionality of 'recordEvent' method and event queue size by using single device ID. + */ + @Test + public void testMethodRecordEventWithSingleDeviceID() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + + JSONArray events = moduleBackendMode.eventQueues.get("device-id-1"); + Assert.assertEquals(1, events.length()); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(1, backendMode.getQueueSize()); + + JSONObject event = events.getJSONObject(0); + validateEventFields("key-1", 1, 0.1, 10.0, 1, 13, 1646640780130L, event); + + JSONObject segments = event.getJSONObject("segmentation"); + Assert.assertEquals("value1", segments.get("key1")); + Assert.assertEquals("value2", segments.get("key2")); + } + + /** + * It validates the functionality of 'recordEvent' method against invalid data. + */ + @Test + public void testMethodRecordEventWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + + /* Invalid Device ID */ + backendMode.recordEvent("", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + backendMode.recordEvent(null, "key-2", 1, 0.1, 10.0, segmentation, 1646640780130L); + + Assert.assertTrue(moduleBackendMode.eventQueues.isEmpty()); + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + + /* Invalid view name */ + backendMode.recordEvent("device-id-1", "", 1, 0.1, 10.0, segmentation, 1646640780130L); + backendMode.recordEvent("device-id-1", null, 1, 0.1, 10.0, segmentation, 1646640780130L); + + Assert.assertTrue(moduleBackendMode.eventQueues.isEmpty()); + Assert.assertEquals(0L, moduleBackendMode.eventQSize); + Assert.assertEquals(0, backendMode.getQueueSize()); + + //TODO: validate segmentation data type. + } + + /** + * It validates the functionality of 'recordEvent' method and event queue size by using multiple device IDs. + */ + @Test + public void testMethodRecordEventWithMultipleDeviceID() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Map segmentation1 = new HashMap<>(); + segmentation1.put("key3", "value3"); + segmentation1.put("key4", "value4"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(1, backendMode.getQueueSize()); + backendMode.recordEvent("device-id-2", "key-2", 1, 0.1, 10.0, segmentation, 1646640780130L); + backendMode.recordEvent("device-id-2", "key-3", 2, 0.2, 20.0, segmentation1, 1646644457826L); + + Assert.assertEquals(3, moduleBackendMode.eventQSize); + Assert.assertEquals(2, backendMode.getQueueSize()); + + //Events with Device ID = 'device-id-1' + JSONArray events = moduleBackendMode.eventQueues.get("device-id-2"); + Assert.assertEquals(2, events.length()); + + JSONObject event = events.getJSONObject(0); + validateEventFields("key-2", 1, 0.1, 10.0, 1, 13, 1646640780130L, event); + + + JSONObject segments = event.getJSONObject("segmentation"); + Assert.assertEquals("value1", segments.get("key1")); + Assert.assertEquals("value2", segments.get("key2")); + + //Events with Device ID = 'device-id-2' + events = moduleBackendMode.eventQueues.get("device-id-2"); + Assert.assertEquals(2, events.length()); + + event = events.getJSONObject(0); + Assert.assertEquals("key-2", event.get("key")); + validateEventFields("key-2", 1, 0.1, 10.0, 1, 13, 1646640780130L, event); + + + segments = event.getJSONObject("segmentation"); + Assert.assertEquals("value1", segments.get("key1")); + Assert.assertEquals("value2", segments.get("key2")); + + event = events.getJSONObject(1); + Assert.assertEquals("key-3", event.get("key")); + validateEventFields("key-3", 2, 0.2, 20.0, 1, 14, 1646644457826L, event); + + + segments = event.getJSONObject("segmentation"); + Assert.assertEquals("value3", segments.get("key3")); + Assert.assertEquals("value4", segments.get("key4")); + } + + /** + * It validates the event thresh hold against single and multiple device IDs. + */ + @Test + public void TestEventThreshHoldWithSingleAndMultiple() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Map segmentation1 = new HashMap<>(); + segmentation1.put("key3", "value3"); + segmentation1.put("key4", "value4"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(1, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-1", "key-2", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(2, moduleBackendMode.eventQSize); + Assert.assertEquals(2, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(1, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-1", "key-3", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(3, moduleBackendMode.eventQSize); + Assert.assertEquals(1, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-1", "key-3", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertNull(moduleBackendMode.eventQueues.get("device-id-1")); + Assert.assertEquals(1, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(2, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-2", "key-2", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(2, moduleBackendMode.eventQSize); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-2").length()); + Assert.assertEquals(3, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-2", "key-3", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(3, moduleBackendMode.eventQSize); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(2, moduleBackendMode.eventQueues.get("device-id-2").length()); + Assert.assertEquals(3, backendMode.getQueueSize()); + + backendMode.recordEvent("device-id-2", "key-4", 2, 0.2, 20.0, segmentation1, 1646644457826L); + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertEquals(3, backendMode.getQueueSize()); + Assert.assertNull(moduleBackendMode.eventQueues.get("device-id-1")); + Assert.assertNull(moduleBackendMode.eventQueues.get("device-id-2")); + } + + /** + * It validates the functionality of adding events into request queue on session update. + */ + @Test + public void testFunctionalityAddEventsIntoRequestQueueOnSessionUpdate() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Map segmentation1 = new HashMap<>(); + segmentation1.put("key3", "value3"); + segmentation1.put("key4", "value4"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + + backendMode.recordEvent("device-id-2", "key-3", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(2, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-2").length()); + + backendMode.recordEvent("device-id-2", "key-4", 2, 0.2, 20.0, segmentation1, 1646644457826L); + Assert.assertEquals(3, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(2, moduleBackendMode.eventQueues.get("device-id-2").length()); + + } + + /** + * It validates the functionality of adding events into request queue on session end. + */ + @Test + public void testFunctionalityAddEventsIntoRequestQueueOnSessionEnd() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", "value1"); + segmentation.put("key2", "value2"); + + Map segmentation1 = new HashMap<>(); + segmentation1.put("key3", "value3"); + segmentation1.put("key4", "value4"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + + backendMode.recordEvent("device-id-2", "key-3", 1, 0.1, 10.0, segmentation, 1646640780130L); + Assert.assertEquals(2, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-2").length()); + + backendMode.recordEvent("device-id-2", "key-4", 2, 0.2, 20.0, segmentation1, 1646644457826L); + Assert.assertEquals(3, moduleBackendMode.eventQSize); + Assert.assertEquals(0, SDKCore.instance.requestQueueMemory.size()); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + Assert.assertEquals(2, moduleBackendMode.eventQueues.get("device-id-2").length()); + + backendMode.sessionEnd("device-id-2", 60, 1646644457826L); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + Assert.assertEquals(2, SDKCore.instance.requestQueueMemory.size()); + Assert.assertNull(moduleBackendMode.eventQueues.get("device-id-2")); + Assert.assertEquals(1, moduleBackendMode.eventQueues.get("device-id-1").length()); + } + + /** + * It validates the request and functionality of 'sessionBegin' method. + */ + @Test + public void testMethodSessionBegin() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Map metrics = new HashMap<>(); + metrics.put("os", "windows"); + metrics.put("app-version", "0.1"); + + Map location = new HashMap() {{ + put("ip_address", "192.168.1.1"); + put("city", "Lahore"); + put("country_code", "PK"); + put("location", "31.5204,74.3587"); + }}; + + backendMode.sessionBegin("device-id-1", metrics, location, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + + String session = request.params.get("metrics"); + JSONObject sessionJson = new JSONObject(session); + + Assert.assertEquals("Lahore", request.params.get("city")); + Assert.assertEquals("PK", request.params.get("country_code")); + Assert.assertEquals("192.168.1.1", request.params.get("ip_address")); + Assert.assertEquals("31.5204,74.3587", request.params.get("location")); + + Assert.assertEquals("windows", sessionJson.get("os")); + Assert.assertEquals("0.1", sessionJson.get("app-version")); + Assert.assertEquals("1", request.params.get("begin_session")); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + } + + /** + * It validates functionality of 'sessionBegin' method against invalid data. + */ + @Test + public void testMethodSessionBeginWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Map metrics = new HashMap<>(); + metrics.put("os", "windows"); + metrics.put("app-version", "0.1"); + + backendMode.sessionBegin("", metrics, null, 1646640780130L); + backendMode.sessionBegin(null, metrics, null, 1646640780130L); + + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + } + + /** + * It validates the request and functionality of 'sessionUpdate' method. + */ + @Test + public void testMethodSessionUpdate() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + backendMode.sessionUpdate("device-id-1", 10.5, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + + Assert.assertEquals("10.5", request.params.get("session_duration")); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + } + + /** + * It validates functionality of 'sessionUpdate' method against invalid data. + */ + @Test + public void testMethodSessionUpdateWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + backendMode.sessionUpdate("", 10.5, 1646640780130L); + backendMode.sessionUpdate(null, 10.5, 1646640780130L); + + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + } + + /** + * It validates the request and functionality of 'sessionEnd' method. + */ + @Test + public void testSessionEnd() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + backendMode.sessionEnd("device-id-1", 10.5, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + + Assert.assertEquals("1", request.params.get("end_session")); + Assert.assertEquals("10.5", request.params.get("session_duration")); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + } + + /** + * It validates functionality of 'sessionEnd' method against invalid data. + */ + @Test + public void testMethodSessionEndWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + backendMode.sessionEnd("", 10.5, 1646640780130L); + backendMode.sessionEnd(null, 20.5, 1646640780130L); + + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + } + + /** + * It validates the request and functionality of 'recordException' method. + */ + @Test + public void testMethodRecordException() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Map segmentation = new HashMap() {{ + put("key1", "value1"); + }}; + + Map crashDetails = new HashMap() {{ + put("_error", "Custom Error"); + put("_logs", "Logs"); + put("_os", "Operating System"); + }}; + + try { + int a = 10 / 0; + } catch (Exception e) { + backendMode.recordException("device-id-1", e, segmentation, crashDetails, 1646640780130L); + backendMode.recordException("device-id-2", "Divided By Zero", "stack traces", null, null, 1646640780130L); + + Assert.assertEquals(2, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + + String crash = request.params.get("crash"); + JSONObject crashJson = new JSONObject(crash); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + + Assert.assertEquals(e.getMessage(), crashJson.get("_name")); + Assert.assertEquals("Custom Error", crashJson.get("_error")); + Assert.assertEquals("Logs", crashJson.get("_logs")); + Assert.assertEquals("Operating System", crashJson.get("_os")); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + + + JSONObject segments = crashJson.getJSONObject("_custom"); + Assert.assertEquals("value1", segments.get("key1")); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + request = SDKCore.instance.requestQueueMemory.remove(); + + crash = request.params.get("crash"); + crashJson = new JSONObject(crash); + + Assert.assertEquals("Divided By Zero", crashJson.get("_name")); + Assert.assertEquals("stack traces", crashJson.get("_error")); + + validateRequestTimeFields("device-id-2", 1646640780130L, request); + + segments = crashJson.getJSONObject("_custom"); + Assert.assertTrue(segments.isEmpty()); + } + } + + /** + * It validates functionality of 'recordException' method against invalid data. + */ + @Test + public void testMethodRecordExceptionWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Map segmentation = new HashMap() {{ + put("key1", "value1"); + }}; + + backendMode.recordException("", null, segmentation, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException(null, null, segmentation, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException("device-id-1", null, segmentation, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException("device-id-2", "", "stack traces", null, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException("device-id-2", "device-id", "", null, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException("device-id-2", null, "stack traces", null, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordException("device-id-2", "device-id", null, null, null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + } + + /** + * It validates the user detail, user custom detail and operations on custom properties. + */ + @Test + public void testUserDetailCustomDetailAndOperations() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + // User detail + Map userDetail = populateUserProperties(true, true, false); + + backendMode.recordUserProperties("device-id-1", userDetail, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + + String userDetails = request.params.get("user_details"); + validateUserProperties(userDetails, true, true, false); + } + + /** + * It validates the structure of user detail , custom user detail and operations. + * Case 1: When custom detail and are provided, along with user detail. + */ + @Test + public void testUserDetailStructureAllDataAtSameLevel() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + // User detail + Map userDetail = populateUserProperties(true, true, true); + backendMode.recordUserProperties("device-id-1", userDetail, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + + String userDetails = request.params.get("user_details"); + validateUserProperties(userDetails, true, true, true); + + } + + /** + * It validates the structure of user detail , custom user detail and operations. + * Case 2: When only user custom detail is provided. + */ + @Test + public void testUserDetailStructureWithOnlyCustomDetail() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map userDetail = populateUserProperties(false, true, false); + + backendMode.recordUserProperties("device-id-1", userDetail, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + + String userDetails = request.params.get("user_details"); + validateUserProperties(userDetails, false, true, false); + } + + /** + * It validates the structure of user detail , custom user detail and operations. + * Case 3: When only operation data is provided. + */ + @Test + public void testUserDetailStructureWithOnlyOperationData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map userDetail = populateUserProperties(false, false, true); + + backendMode.recordUserProperties("device-id-1", userDetail, 1646640780130L); + + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + Request request = SDKCore.instance.requestQueueMemory.remove(); + validateRequestTimeFields("device-id-1", 1646640780130L, request); + + String userDetails = request.params.get("user_details"); + validateUserProperties(userDetails, false, false, true); + } + + /** + * It validates functionality of 'recordUserProperties' method against invalid data. + */ + @Test + public void testMethodRecordUserPropertiesWithInvalidData() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + Map userDetail = populateUserProperties(true, false, false); + + backendMode.recordUserProperties("", userDetail, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordUserProperties(null, userDetail, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + backendMode.recordUserProperties("device-id", null, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + + userDetail.clear(); + backendMode.recordUserProperties("device-id", userDetail, 1646640780130L); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + } + + /* + * It validates the data type of Event's segment items. + */ + @Test + public void testEventSegmentDataType() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", null); //invalid + segmentation.put("key2", "value"); + segmentation.put("key3", 1); + segmentation.put("key4", 20.5); + segmentation.put("key5", true); + segmentation.put("key6", backendMode); //invalid + segmentation.put("key7", 10L); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + backendMode.recordEvent("device-id-1", "key-1", 1, 0.1, 10.0, segmentation, 1646640780130L); + + JSONArray events = moduleBackendMode.eventQueues.get("device-id-1"); + Assert.assertEquals(1, events.length()); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + + JSONObject event = events.getJSONObject(0); + validateEventFields("key-1", 1, 0.1, 10.0, 1, 13, 1646640780130L, event); + + JSONObject segments = event.getJSONObject("segmentation"); + Assert.assertEquals(5, segments.length()); + Assert.assertEquals("value", segments.get("key2")); + Assert.assertEquals(1, segments.get("key3")); + Assert.assertEquals(20.5, segments.get("key4")); + Assert.assertEquals(10L, segments.get("key7")); + Assert.assertEquals(true, segments.get("key5")); + + Assert.assertFalse(segments.has("key1")); + Assert.assertFalse(segments.has("key6")); + } + + /* + * It validates the data type of View's segment items. + */ + @Test + public void testViewSegmentDataType() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", null); //invalid + segmentation.put("key2", "value"); + segmentation.put("key3", 1); + segmentation.put("key4", 20.5); + segmentation.put("key5", true); + segmentation.put("key6", backendMode); //invalid + segmentation.put("key7", 10L); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + backendMode.recordView("device-id-1", "view-1", segmentation, 1646640780130L); + + JSONArray events = moduleBackendMode.eventQueues.get("device-id-1"); + Assert.assertEquals(1, events.length()); + Assert.assertEquals(1, moduleBackendMode.eventQSize); + + JSONObject event = events.getJSONObject(0); + JSONObject segments = event.getJSONObject("segmentation"); + Assert.assertEquals(6, segments.length()); + Assert.assertEquals("value", segments.get("key2")); + Assert.assertEquals(1, segments.get("key3")); + Assert.assertEquals(20.5, segments.get("key4")); + Assert.assertEquals(10L, segments.get("key7")); + Assert.assertEquals(true, segments.get("key5")); + Assert.assertEquals("view-1", segments.get("name")); + + Assert.assertFalse(segments.has("key1")); + Assert.assertFalse(segments.has("key6")); + } + + /* + * It validates the data type of Crash's segment items. + */ + @Test + public void testCrashSegmentDataType() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + Map segmentation = new HashMap<>(); + segmentation.put("key1", null); //invalid + segmentation.put("key2", "value"); + segmentation.put("key3", 1); + segmentation.put("key4", 20.5); + segmentation.put("key5", true); + segmentation.put("key6", backendMode); //invalid + segmentation.put("key7", 10L); + + Map crashDetails = new HashMap<>(); + crashDetails.put("K1", null); //invalid + crashDetails.put("K2", "V2"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + backendMode.recordException("device-id-1", "key-1", "stacktrace", segmentation, crashDetails, 1646640780130L); + + Request request = SDKCore.instance.requestQueueMemory.remove(); + String crash = request.params.get("crash"); + + JSONObject crashJson = new JSONObject(crash); + JSONObject segments = crashJson.getJSONObject("_custom"); + Assert.assertEquals(5, segments.length()); + Assert.assertEquals("value", segments.get("key2")); + Assert.assertEquals(1, segments.get("key3")); + Assert.assertEquals(20.5, segments.get("key4")); + Assert.assertEquals(true, segments.get("key5")); + Assert.assertEquals(10, segments.get("key7")); + Assert.assertEquals(10, segments.get("key7")); + + Assert.assertFalse(crashJson.has("K1")); + Assert.assertEquals("V2", crashJson.get("K2")); + + Assert.assertFalse(segments.has("key1")); + Assert.assertFalse(segments.has("key6")); + } + + /** + * It validates the request and functionality of 'recordDirectRequest' method. + */ + @Test + public void testRecordDirectRequest() { + ModuleBackendMode.BackendMode backendMode = moduleBackendMode.new BackendMode(); + + // Direct request with timestamp and device id + Map requestData = new HashMap<>(); + requestData.put("data1", "value1"); + requestData.put("device_id", "device-id-1"); + requestData.put("timestamp", "1647938191782"); + requestData.put("tz", "100"); + requestData.put("dow", "0"); + requestData.put("hour", "9"); + requestData.put("data3", "value3"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + backendMode.recordDirectRequest("device-id-2", requestData, 1647938191782L); + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + + Request request = SDKCore.instance.requestQueueMemory.remove(); + Assert.assertEquals("value1", request.params.get("data1")); + Assert.assertEquals("value3", request.params.get("data3")); + validateRequestTimeFields("device-id-2", 1647938191782L, request); + + // Direct request without timestamp and device id + requestData = new HashMap<>(); + requestData.put("data2", "value2"); + requestData.put("data4", "value4"); + + Assert.assertEquals(0, moduleBackendMode.eventQSize); + Assert.assertTrue(SDKCore.instance.requestQueueMemory.isEmpty()); + backendMode.recordDirectRequest("device-id-2", requestData, 987654321L); + Assert.assertEquals(1, SDKCore.instance.requestQueueMemory.size()); + + request = SDKCore.instance.requestQueueMemory.remove(); + Assert.assertEquals("value2", request.params.get("data2")); + Assert.assertEquals("value4", request.params.get("data4")); + validateRequestTimeFields("device-id-2", 987654321L, request); + } + + private Map populateUserProperties(boolean addUserDetail, boolean addCustomDetail, boolean addOperation) { + Map userDetail = new HashMap<>(); + if (addUserDetail) { + userDetail.put("name", "Full Name"); + userDetail.put("username", "username1"); + userDetail.put("email", "user@gmail.com"); + userDetail.put("organization", "Countly"); + userDetail.put("phone", "000-111-000"); + userDetail.put("gender", "M"); + userDetail.put("byear", "1991"); + } + + if (addCustomDetail) { + userDetail.put("hair", "black"); + userDetail.put("height", 5.9); + } + + if (addOperation) { + userDetail.put("weight", "{\"$inc\": 1}"); + } + + return userDetail; + } + + private void validateEventFields(String key, int count, Double sum, Double dur, int dow, int hour, long timestamp, JSONObject event) { + Assert.assertEquals(key, event.get("key")); + Assert.assertEquals(sum, event.opt("sum")); + Assert.assertEquals(count, event.get("count")); + Assert.assertEquals(dur, event.opt("dur")); + + Assert.assertEquals(dow, event.get("dow")); + Assert.assertEquals(hour, event.get("hour")); + Assert.assertEquals(timestamp, event.get("timestamp")); + } + + private void validateRequestTimeFields(String deviceID, long timestamp, Request request) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + + final int hour = calendar.get(Calendar.HOUR_OF_DAY); + final int dow = calendar.get(Calendar.DAY_OF_WEEK) - 1; + + Assert.assertEquals(DeviceCore.dev.getTimezoneOffset() + "", request.params.get("tz")); + Assert.assertEquals(dow + "", request.params.get("dow")); + Assert.assertEquals(hour + "", request.params.get("hour")); + Assert.assertEquals(deviceID, request.params.get("device_id")); + Assert.assertEquals(timestamp + "", request.params.get("timestamp")); + } + + private void validateUserProperties(String userDetails, boolean validateUserDetail, boolean validateCustomDetail, boolean validateOperation) { + JSONObject userDetailsJson = new JSONObject(userDetails); + + if (validateUserDetail) { + Assert.assertEquals("Full Name", userDetailsJson.get("name")); + Assert.assertEquals("username1", userDetailsJson.get("username")); + Assert.assertEquals("user@gmail.com", userDetailsJson.get("email")); + Assert.assertEquals("Countly", userDetailsJson.get("organization")); + Assert.assertEquals("000-111-000", userDetailsJson.get("phone")); + Assert.assertEquals("M", userDetailsJson.get("gender")); + Assert.assertEquals("1991", userDetailsJson.get("byear")); + } + + if (validateCustomDetail) { + //Custom properties + JSONObject customPropertiesJson = userDetailsJson.getJSONObject("custom"); + Assert.assertEquals("black", customPropertiesJson.get("hair")); + Assert.assertEquals(5.9, customPropertiesJson.get("height")); + } + + if (validateOperation) { + JSONObject customPropertiesJson = userDetailsJson.getJSONObject("custom"); + JSONObject operationsJson = customPropertiesJson.getJSONObject("weight"); + Assert.assertEquals(1, operationsJson.get("$inc")); + } + } +} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests.java index 6bab87c50..7b5a29b3a 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests.java @@ -1,14 +1,14 @@ package ly.count.sdk.java.internal; -import junit.framework.Assert; +import ly.count.sdk.java.Config; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import ly.count.sdk.java.internal.BaseTestsCore; -import ly.count.sdk.java.internal.InternalConfig; +import java.net.URL; @RunWith(JUnit4.class) public class ConfigTests extends BaseTestsCore { @@ -18,16 +18,99 @@ public class ConfigTests extends BaseTestsCore { @Before public void setUp() throws Exception { - internalConfig = (InternalConfig)defaultConfigWithLogsForConfigTests(); + internalConfig = (InternalConfig) defaultConfigWithLogsForConfigTests(); + } + + + @Test + public void testServerUrlAndAppKey() throws Exception { + URL url = new URL(serverUrl); + Assert.assertEquals(serverAppKey, internalConfig.getServerAppKey()); + Assert.assertEquals(url, internalConfig.getServerURL()); + } + + @Test + public void testRequestMethod() { + Assert.assertFalse(internalConfig.isUsePOST()); + + internalConfig.enableUsePOST(); + Assert.assertTrue(internalConfig.isUsePOST()); + + internalConfig.setUsePOST(false); + Assert.assertFalse(internalConfig.isUsePOST()); + + internalConfig.setUsePOST(true); + Assert.assertTrue(internalConfig.isUsePOST()); + } + + @Test + public void testLoggingTag() { + Assert.assertEquals("Countly", internalConfig.getLoggingTag()); + + internalConfig.setLoggingTag(""); + Assert.assertEquals("Countly", internalConfig.getLoggingTag()); + + internalConfig.setLoggingTag(null); + Assert.assertEquals("Countly", internalConfig.getLoggingTag()); + + internalConfig.setLoggingTag("New Tag"); + Assert.assertEquals("New Tag", internalConfig.getLoggingTag()); + } + + @Test + public void testLoggingLevel() { + Assert.assertEquals(Config.LoggingLevel.DEBUG, internalConfig.getLoggingLevel()); + + internalConfig.setLoggingLevel(Config.LoggingLevel.INFO); + Assert.assertEquals(Config.LoggingLevel.INFO, internalConfig.getLoggingLevel()); } @Test - public void sdkName_default(){ + public void testSDKName() { + Assert.assertEquals("java-native", internalConfig.getSdkName()); + + internalConfig.setSdkName(null); + Assert.assertEquals("java-native", internalConfig.getSdkName()); + + internalConfig.setSdkName(""); Assert.assertEquals("java-native", internalConfig.getSdkName()); + + internalConfig.setSdkName("new-name"); + Assert.assertEquals("new-name", internalConfig.getSdkName()); } @Test - public void sdkVersion_default(){ - Assert.assertEquals("20.11.1", internalConfig.getSdkVersion()); + public void testSDKVersion() { + String versionName = "20.11.2"; + Assert.assertEquals(versionName, internalConfig.getSdkVersion()); + + internalConfig.setSdkVersion(null); + Assert.assertEquals(versionName, internalConfig.getSdkVersion()); + + internalConfig.setSdkVersion(""); + Assert.assertEquals(versionName, internalConfig.getSdkVersion()); + + internalConfig.setSdkVersion("new-version"); + Assert.assertEquals("new-version", internalConfig.getSdkVersion()); + } + + + @Test + public void testSendUpdateEachSeconds() { + Assert.assertEquals(30, internalConfig.getSendUpdateEachSeconds()); + + internalConfig.disableUpdateRequests(); + Assert.assertEquals(0, internalConfig.getSendUpdateEachSeconds()); + + internalConfig.setSendUpdateEachSeconds(123); + Assert.assertEquals(123, internalConfig.getSendUpdateEachSeconds()); + } + + @Test + public void testEventBufferSize() { + Assert.assertEquals(10, internalConfig.getEventsBufferSize()); + + internalConfig.setEventsBufferSize(60); + Assert.assertEquals(60, internalConfig.getEventsBufferSize()); } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests2.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests2.java deleted file mode 100644 index 7cff7e74a..000000000 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ConfigTests2.java +++ /dev/null @@ -1,182 +0,0 @@ -package ly.count.sdk.java.internal; - -import junit.framework.Assert; - -import ly.count.sdk.java.Config; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.net.URL; - -import ly.count.sdk.java.internal.BaseTestsCore; -import ly.count.sdk.java.internal.InternalConfig; - -@RunWith(JUnit4.class) -public class ConfigTests2 extends BaseTestsCore { - private InternalConfig internalConfig; - private String serverUrl = "http://www.serverurl.com"; - private String serverAppKey = "1234"; - - @Before - public void setUp() throws Exception { - internalConfig = (InternalConfig)defaultConfigWithLogsForConfigTests(); - } - - @Test - public void setup_urlAndKey() throws Exception{ - URL url = new URL(serverUrl); - Assert.assertEquals(serverAppKey, internalConfig.getServerAppKey()); - Assert.assertEquals(url, internalConfig.getServerURL()); - } - - @Test - public void setUsePost_setAndDeset(){ - Assert.assertFalse(internalConfig.isUsePOST()); - internalConfig.enableUsePOST(); - Assert.assertTrue(internalConfig.isUsePOST()); - internalConfig.setUsePOST(false); - Assert.assertFalse(internalConfig.isUsePOST()); - internalConfig.setUsePOST(true); - Assert.assertTrue(internalConfig.isUsePOST()); - } - - @Test - public void setLoggingTag_default(){ - Assert.assertEquals("Countly", internalConfig.getLoggingTag()); - } - - @Test - public void setLoggingTag_null(){ - Config.LoggingLevel level = internalConfig.getLoggingLevel(); - internalConfig.setLoggingTag(null); - Assert.assertEquals(level, internalConfig.getLoggingLevel()); - } - - @Test - public void setLoggingTag_empty(){ - String tag = internalConfig.getLoggingTag(); - internalConfig.setLoggingTag(""); - Assert.assertEquals(tag, internalConfig.getLoggingTag()); - - } - - @Test - public void setLoggingTag_simple(){ - String tagName = "simpleName"; - internalConfig.setLoggingTag(tagName); - Assert.assertEquals(tagName, internalConfig.getLoggingTag()); - } - - @Test - public void setLoggingLevel_null(){ - Config.LoggingLevel level = internalConfig.getLoggingLevel(); - internalConfig.setLoggingTag(null); - Assert.assertEquals(level, internalConfig.getLoggingLevel()); - } - - @Test - public void sdkName_null(){ - String prvName = internalConfig.getSdkName(); - internalConfig.setSdkName(null); - Assert.assertEquals(prvName, internalConfig.getSdkName()); - } - - @Test - public void sdkName_empty(){ - String prvName = internalConfig.getSdkName(); - internalConfig.setSdkName(""); - Assert.assertEquals(prvName, internalConfig.getSdkName()); - } - - @Test - public void sdkName_setting(){ - String newSdkName = "new-some-name"; - internalConfig.setSdkName(newSdkName); - Assert.assertEquals(newSdkName, internalConfig.getSdkName()); - - newSdkName = "another-name"; - internalConfig.setSdkName(newSdkName); - Assert.assertEquals(newSdkName, internalConfig.getSdkName()); - } - - @Test - public void sdkVersion_null(){ - String prv = internalConfig.getSdkVersion(); - internalConfig.setSdkVersion(null); - Assert.assertEquals(prv, internalConfig.getSdkVersion()); - } - - @Test - public void sdkVersion_empty(){ - String prv = internalConfig.getSdkVersion(); - internalConfig.setSdkVersion(""); - Assert.assertEquals(prv, internalConfig.getSdkVersion()); - } - - @Test - public void sdkVersion_setting(){ - String versionName = "123"; - internalConfig.setSdkVersion(versionName); - Assert.assertEquals(versionName, internalConfig.getSdkVersion()); - - versionName = "asd"; - internalConfig.setSdkVersion(versionName); - Assert.assertEquals(versionName, internalConfig.getSdkVersion()); - } - - @Test - public void programmaticSessionsControl_default(){ - Assert.assertTrue(internalConfig.isAutoSessionsTrackingEnabled()); - } - - @Test - public void programmaticSessionsControl_enableAndDisable(){ - Assert.assertTrue(internalConfig.isAutoSessionsTrackingEnabled()); - internalConfig.setAutoSessionsTracking(false); - Assert.assertFalse(internalConfig.isAutoSessionsTrackingEnabled()); - } - - @Test - public void sendUpdateEachSeconds_default(){ - Assert.assertEquals(30, internalConfig.getSendUpdateEachSeconds()); - } - - @Test - public void sendUpdateEachSeconds_disable(){ - internalConfig.disableUpdateRequests(); - Assert.assertEquals(0, internalConfig.getSendUpdateEachSeconds()); - } - - @Test - public void sendUpdateEachSeconds_set(){ - int secondsAmount = 123; - internalConfig.setSendUpdateEachSeconds(secondsAmount); - Assert.assertEquals(secondsAmount, internalConfig.getSendUpdateEachSeconds()); - } - - @Test - public void sendUpdateEachEvents_default(){ - Assert.assertEquals(10, internalConfig.getEventsBufferSize()); - } - - @Test - public void sendUpdateEachEvents_disable(){ - internalConfig.disableUpdateRequests(); - Assert.assertEquals(0, internalConfig.getSendUpdateEachSeconds()); - } - - @Test - public void sendUpdateEachEvents_set(){ - int eventsAmount = 123; - internalConfig.setEventsBufferSize(eventsAmount); - Assert.assertEquals(eventsAmount, internalConfig.getEventsBufferSize()); - } - - @Test - public void sdkVersion_default(){ - Assert.assertEquals("20.11.1", internalConfig.getSdkVersion()); - } - -} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java index 8e34e612b..4242f3dc1 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java @@ -37,6 +37,7 @@ public void constructor_deserialize(){ @Test public void addParams() { SessionImpl session = new SessionImpl(ctx); + session.setPushOnChange(false); Assert.assertNotNull(session.params); StringBuilder sbParams = new StringBuilder();