From 3cc180bd1b51fe2a2da657516d780a879936b884 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 4 Dec 2023 15:24:49 +0300 Subject: [PATCH 01/38] feat: init view module --- .../count/sdk/java/internal/ModuleViews.java | 635 +++++++++++++++++- .../ly/count/sdk/java/internal/TimeUtils.java | 10 + 2 files changed, 642 insertions(+), 3 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 8f45c0913..a9ae13562 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -1,9 +1,638 @@ package ly.count.sdk.java.internal; -/** - * Views support - */ +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import ly.count.sdk.java.Countly; public class ModuleViews extends ModuleBase { + private String currentViewID = null; + private String previousViewID = null; + private boolean firstView = true; + + boolean autoViewTracker = false; + boolean automaticTrackingShouldUseShortName = false; + + //track orientation changes + boolean trackOrientationChanges; + + int currentOrientation = -1; + final static String ORIENTATION_EVENT_KEY = "[CLY]_orientation"; + + final static String VIEW_EVENT_KEY = "[CLY]_view"; + + Class[] autoTrackingActivityExceptions = null;//excluded activities from automatic view tracking + + Map automaticViewSegmentation = new HashMap<>();//automatic view segmentation + + Map viewDataMap = new HashMap<>(); // map viewIDs to its viewData + + String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; + + public @Nonnull String getCurrentViewId() { + return currentViewID == null ? "" : currentViewID; + } + + public @Nonnull String getPreviousViewId() { + return previousViewID == null ? "" : previousViewID; + } + + static class ViewData { + String viewID; + long viewStartTimeSeconds; // if this is 0 then the view is not started yet or was paused + String viewName; + boolean isAutoStoppedView = false;//views started with "startAutoStoppedView" would have this as "true". If set to "true" views should be automatically closed when another one is started. + boolean isAutoPaused = false;//this marks that this view automatically paused when going to the background + } + + //interface for SDK users + Views viewsInterface; + + ModuleViews() { + } + + @Override + public void init(InternalConfig config) { + super.init(config); + L.v("[ModuleViews] Initializing"); + + if (config.enableAutomaticViewTracking) { + L.d("[ModuleViews] Enabling automatic view tracking"); + autoViewTracker = config.enableAutomaticViewTracking; + } + + if (config.autoTrackingUseShortName) { + L.d("[ModuleViews] Enabling automatic view tracking short names"); + automaticTrackingShouldUseShortName = config.autoTrackingUseShortName; + } + + setGlobalViewSegmentationInternal(config.globalViewSegmentation); + trackOrientationChanges = config.trackOrientationChange; + + viewsInterface = new Views(); + } + + private void removeReservedKeysAndUnsupportedTypesFromViewSegmentation(Map segmentation) { + if (segmentation == null) { + return; + } + + List gonnaDeleteKeys = new ArrayList<>(); + + for (String key : reservedSegmentationKeysViews) { + if (segmentation.containsKey(key)) { + segmentation.remove(key); + L.w("[ModuleViews] removeReservedKeysAndUnsupportedTypesFromViewSegmentation, You cannot use the key:[" + key + "] in your segmentation since it's reserved by the SDK"); + } + } + + for (String key : segmentation.keySet()) { + if (!Utils.isValidDataType(segmentation.get(key))) { + gonnaDeleteKeys.add(key); + L.w("[ModuleViews] removeReservedKeysAndUnsupportedTypesFromViewSegmentation, You have provided an unsupported data type in your View Segmentation. Removing the unsupported values."); + } + } + + gonnaDeleteKeys.forEach(segmentation::remove); + } + + /** + * Checks the provided Segmentation by the user. Sanitizes it + * and transfers the data into an internal Segmentation Object. + */ + void setGlobalViewSegmentationInternal(@Nullable Map segmentation) { + L.d("[ModuleViews] Calling setGlobalViewSegmentationInternal with[" + (segmentation == null ? "null" : segmentation.size()) + "] entries"); + + automaticViewSegmentation.clear(); + + if (segmentation != null) { + removeReservedKeysAndUnsupportedTypesFromViewSegmentation(segmentation); + automaticViewSegmentation.putAll(segmentation); + } + } + + public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { + removeReservedKeysAndUnsupportedTypesFromViewSegmentation(segmentation); + automaticViewSegmentation.putAll(segmentation); + } + + /** + * This should be called in case a new session starts so that we could identify the new "first view" + */ + public void resetFirstView() { + firstView = true; + } + + Map CreateViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { + Map viewSegmentation = new HashMap<>(); + if (customViewSegmentation != null) { + viewSegmentation.putAll(customViewSegmentation); + } + + viewSegmentation.put("name", vd.viewName); + if (visit) { + viewSegmentation.put("visit", "1"); + } + if (firstView) { + viewSegmentation.put("start", "1"); + } + viewSegmentation.put("segment", "Android"); + + return viewSegmentation; + } + + void autoCloseRequiredViews(boolean closeAllViews, Map customViewSegmentation) { + L.d("[ModuleViews] autoCloseRequiredViews"); + List viewsToRemove = new ArrayList<>(1); + + for (Map.Entry entry : viewDataMap.entrySet()) { + ViewData vd = entry.getValue(); + if (closeAllViews || vd.isAutoStoppedView) { + viewsToRemove.add(vd.viewID); + } + } + + if (!viewsToRemove.isEmpty()) { + L.d("[ModuleViews] autoCloseRequiredViews, about to close [" + viewsToRemove.size() + "] views"); + } + + removeReservedKeysAndUnsupportedTypesFromViewSegmentation(customViewSegmentation); + viewsToRemove.forEach(s -> stopViewWithIDInternal(s, customViewSegmentation)); + } + + /** + * Record a view manually, without automatic tracking + * or tracks a view that is not automatically tracked + * like a fragment, Message box or a transparent Activity + * with segmentation if provided. (This is the internal function) + * + * @param viewName String - name of the view + * @param customViewSegmentation Map - segmentation that will be added to the view, set 'null' if none should be added + * @return Returns link to Countly for call chaining + */ + @Nullable String startViewInternal(@Nullable String viewName, @Nullable Map customViewSegmentation, boolean viewShouldBeAutomaticallyStopped) { + + if (viewName == null || viewName.isEmpty()) { + L.e("[ModuleViews] startViewInternal, Trying to record view with null or empty view name, ignoring request"); + return null; + } + + removeReservedKeysAndUnsupportedTypesFromViewSegmentation(customViewSegmentation); + + int segmCount = 0; + if (customViewSegmentation != null) { + segmCount = customViewSegmentation.size(); + } + L.d("[ModuleViews] Recording view with name: [" + viewName + "], previous view ID:[" + currentViewID + "] custom view segment count:[" + segmCount + "], first:[" + firstView + "], autoStop:[" + viewShouldBeAutomaticallyStopped + "]"); + + //stop views that should be automatically stopped + //no segmentation should be used in this case + autoCloseRequiredViews(false, null); + + ViewData currentViewData = new ViewData(); + currentViewData.viewID = safeRandomVal(); + currentViewData.viewName = viewName; + currentViewData.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); + currentViewData.isAutoStoppedView = viewShouldBeAutomaticallyStopped; + + viewDataMap.put(currentViewData.viewID, currentViewData); + previousViewID = currentViewID; + currentViewID = currentViewData.viewID; + + Map accumulatedEventSegm = new HashMap(automaticViewSegmentation); + if (customViewSegmentation != null) { + accumulatedEventSegm.putAll(customViewSegmentation); + } + + Map viewSegmentation = CreateViewEventSegmentation(currentViewData, firstView, true, accumulatedEventSegm); + + if (firstView) { + L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); + firstView = false; + } + + viewSegmentation.put("_idv", currentViewData.viewID); + Countly.instance().events().recordEvent(VIEW_EVENT_KEY, viewSegmentation, 1, 0.0, 0.0); + + return currentViewData.viewID; + } + + void stopViewWithNameInternal(@Nullable String viewName, @Nullable Map customViewSegmentation) { + if (viewName == null || viewName.isEmpty()) { + L.e("[ModuleViews] stopViewWithNameInternal, Trying to record view with null or empty view name, ignoring request"); + return; + } + + String viewID = null; + + for (Map.Entry entry : viewDataMap.entrySet()) { + ViewData vd = entry.getValue(); + if (vd != null && viewName.equals(vd.viewName)) { + viewID = entry.getKey(); + } + } + + if (viewID == null) { + L.e("[ModuleViews] stopViewWithNameInternal, No view entry found with the provided name :[" + viewName + "]"); + return; + } + + stopViewWithIDInternal(viewID, customViewSegmentation); + } + + void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map customViewSegmentation) { + if (viewID == null || viewID.isEmpty()) { + L.e("[ModuleViews] stopViewWithNameInternal, Trying to record view with null or empty view ID, ignoring request"); + return; + } + //todo extract common checks + if (!viewDataMap.containsKey(viewID)) { + L.w("[ModuleViews] stopViewWithIDInternal, there is no view with the provided view id to close"); + return; + } + + ViewData vd = viewDataMap.get(viewID); + if (vd == null) { + L.e("[ModuleViews] stopViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); + return; + } + + L.d("[ModuleViews] View [" + vd.viewName + "], id:[" + vd.viewID + "] is getting closed, reporting duration: [" + (TimeUtils.uniqueTimestampS() - vd.viewStartTimeSeconds) + "] s, current timestamp: [" + TimeUtils.uniqueTimestampMs() + "]"); + recordViewEndEvent(vd, customViewSegmentation, "stopViewWithIDInternal"); + + viewDataMap.remove(vd.viewID); + } + + void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { + long lastElapsedDurationSeconds = 0; + //we sanity check the time component and print error in case of problem + if (vd.viewStartTimeSeconds < 0) { + L.e("[ModuleViews] " + viewRecordingSource + ", view start time value is not normal: [" + vd.viewStartTimeSeconds + "], ignoring that duration"); + } else if (vd.viewStartTimeSeconds == 0) { + L.i("[ModuleViews] " + viewRecordingSource + ", view is either paused or didn't run, ignoring start timestamp"); + } else { + lastElapsedDurationSeconds = TimeUtils.uniqueTimestampS() - vd.viewStartTimeSeconds; + } + + //only record view if the view name is not null + if (vd.viewName == null) { + L.e("[ModuleViews] stopViewWithIDInternal, view has no internal name, ignoring it"); + return; + } + + Map accumulatedEventSegm = new HashMap(automaticViewSegmentation); + if (filteredCustomViewSegmentation != null) { + accumulatedEventSegm.putAll(filteredCustomViewSegmentation); + } + + long viewDurationSeconds = lastElapsedDurationSeconds; + Map segments = CreateViewEventSegmentation(vd, false, false, accumulatedEventSegm); + segments.put("_idv", vd.viewID); + Countly.instance().events().recordEvent(VIEW_EVENT_KEY, segments, 1, 0.0, new Long(viewDurationSeconds).doubleValue()); + } + + void pauseViewWithIDInternal(String viewID, boolean pausedAutomatically) { + if (viewID == null || viewID.isEmpty()) { + L.e("[ModuleViews] pauseViewWithIDInternal, Trying to record view with null or empty view ID, ignoring request"); + return; + } + + if (!viewDataMap.containsKey(viewID)) { + L.w("[ModuleViews] pauseViewWithIDInternal, there is no view with the provided view id to close"); + return; + } + + ViewData vd = viewDataMap.get(viewID); + if (vd == null) { + L.e("[ModuleViews] pauseViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening, auto paused:[" + pausedAutomatically + "]"); + return; + } + + L.d("[ModuleViews] pauseViewWithIDInternal, pausing view for ID:[" + viewID + "], name:[" + vd.viewName + "]"); + + if (vd.viewStartTimeSeconds == 0) { + L.w("[ModuleViews] pauseViewWithIDInternal, pausing a view that is already paused. ID:[" + viewID + "], name:[" + vd.viewName + "]"); + return; + } + + vd.isAutoPaused = pausedAutomatically; + + recordViewEndEvent(vd, null, "pauseViewWithIDInternal"); + + vd.viewStartTimeSeconds = 0; + } + + void resumeViewWithIDInternal(String viewID) { + if (viewID == null || viewID.isEmpty()) { + L.e("[ModuleViews] resumeViewWithIDInternal, Trying to record view with null or empty view ID, ignoring request"); + return; + } + + if (!viewDataMap.containsKey(viewID)) { + L.w("[ModuleViews] resumeViewWithIDInternal, there is no view with the provided view id to close"); + return; + } + + ViewData vd = viewDataMap.get(viewID); + if (vd == null) { + L.e("[ModuleViews] resumeViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); + return; + } + + L.d("[ModuleViews] resumeViewWithIDInternal, resuming view for ID:[" + viewID + "], name:[" + vd.viewName + "]"); + + if (vd.viewStartTimeSeconds > 0) { + L.w("[ModuleViews] resumeViewWithIDInternal, resuming a view that is already running. ID:[" + viewID + "], name:[" + vd.viewName + "]"); + return; + } + + vd.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); + vd.isAutoPaused = false; + } + + void stopAllViewsInternal(Map viewSegmentation) { + L.d("[ModuleViews] stopAllViewsInternal"); + + autoCloseRequiredViews(true, viewSegmentation); + } + + void updateOrientation(int newOrientation) { + L.d("[ModuleViews] Calling [updateOrientation], new orientation:[" + newOrientation + "]"); + + if (!internalConfig.sdk.hasConsentForFeature(CoreFeature.UserProfiles)) { + //we don't have consent, just leave + return; + } + + if (currentOrientation != newOrientation) { + currentOrientation = newOrientation; + + Map segm = new HashMap<>(); + + if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + segm.put("mode", "portrait"); + } else { + segm.put("mode", "landscape"); + } + + Countly.instance().events().recordEvent(ORIENTATION_EVENT_KEY, segm); + } + } + + void pauseRunningViewsAndSend() { + L.d("[ModuleViews] pauseRunningViewsAndSend, going to the background and pausing"); + for (Map.Entry entry : viewDataMap.entrySet()) { + ViewData vd = entry.getValue(); + + if (vd.viewStartTimeSeconds > 0) { + //if the view is running + pauseViewWithIDInternal(vd.viewID, true); + } + } + } + + void resumeAutoPausedViews() { + L.d("[ModuleViews] resumeAutoPausedViews, going to the foreground and resuming"); + for (Map.Entry entry : viewDataMap.entrySet()) { + ViewData vd = entry.getValue(); + + if (vd.isAutoPaused) { + //if the view was automatically paused, resume it + resumeViewWithIDInternal(vd.viewID); + } + } + } + + @Override + public void stop(InternalConfig config, boolean clear) { + if (automaticViewSegmentation != null) { + automaticViewSegmentation.clear(); + automaticViewSegmentation = null; + } + autoTrackingActivityExceptions = null; + viewsInterface = null; + } + + /** + * Creates a crypto-safe SHA-256 hashed random value + * + * @return returns a random string value + */ + public static String safeRandomVal() { + long timestamp = System.currentTimeMillis(); + SecureRandom random = new SecureRandom(); + byte[] value = new byte[6]; + random.nextBytes(value); + String b64Value = Utils.Base64.encode(value); + return b64Value + timestamp; + } + + public class Views { + + /** + * Record a view manually, without automatic tracking + * or tracks a view that is not automatically tracked + * like a fragment, Message box or a transparent Activity + * + * @param viewName String - name of the view + * @return Returns View ID + */ + public String startAutoStoppedView(@Nullable String viewName) { + synchronized (Countly.instance()) { + // call the general function that has two parameters + return startAutoStoppedView(viewName, null); + } + } + + /** + * Record a view manually, without automatic tracking + * or tracks a view that is not automatically tracked + * like a fragment, Message box or a transparent Activity + * with segmentation. (This is the main function that is used) + * + * @param viewName String - name of the view + * @param viewSegmentation Map - segmentation that will be added to the view, set 'null' if none should be added + * @return String - view ID + */ + public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startAutoStoppedView [" + viewName + "]"); + + if (autoViewTracker) { + L.e("[Views] startAutoStoppedView, manual view call will be ignored since automatic tracking is enabled."); + return null; + } + + return startViewInternal(viewName, viewSegmentation, true); + } + } + + /** + * Starts a view which would not close automatically (For multi view tracking) + * + * @param viewName - String + * @return String - View ID + */ + public @Nullable String startView(@Nullable String viewName) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startView vn[" + viewName + "]"); + + if (autoViewTracker) { + L.e("[Views] startView, manual view call will be ignored since automatic tracking is enabled."); + return null; + } + + return startViewInternal(viewName, null, false); + } + } + + /** + * Starts a view which would not close automatically (For multi view tracking) + * + * @param viewName String - name of the view + * @param viewSegmentation Map - segmentation that will be added to the view, set 'null' if none should be added + * @return String - View ID + */ + public @Nullable String startView(@Nullable String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startView vn[" + viewName + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + + if (autoViewTracker) { + L.e("[Views] startView, manual view call will be ignored since automatic tracking is enabled."); + return null; + } + + return startViewInternal(viewName, viewSegmentation, false); + } + } + + /** + * Stops a view with the given name if it was open + * + * @param viewName String - view name + */ + public void stopViewWithName(@Nullable String viewName) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithName vn[" + viewName + "]"); + + stopViewWithNameInternal(viewName, null); + } + } + + /** + * Stops a view with the given name if it was open + * + * @param viewName String - view name + * @param viewSegmentation Map - view segmentation + */ + public void stopViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + + stopViewWithNameInternal(viewName, viewSegmentation); + } + } + + /** + * Stops a view with the given ID if it was open + * + * @param viewID String - view ID + */ + public void stopViewWithID(@Nullable String viewID) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithID vi[" + viewID + "]"); + + stopViewWithIDInternal(viewID, null); + } + } + + /** + * Stops a view with the given ID if it was open + * + * @param viewID String - view ID + * @param viewSegmentation Map - view segmentation + */ + public void stopViewWithID(@Nullable String viewID, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + + stopViewWithIDInternal(viewID, viewSegmentation); + } + } + + /** + * Pauses a view with the given ID + * + * @param viewID String - view ID + */ + public void pauseViewWithID(@Nullable String viewID) { + synchronized (Countly.instance()) { + L.i("[Views] Calling pauseViewWithID vi[" + viewID + "]"); + + pauseViewWithIDInternal(viewID, false); + } + } + + /** + * Resumes a view with the given ID + * + * @param viewID String - view ID + */ + public void resumeViewWithID(@Nullable String viewID) { + synchronized (Countly.instance()) { + L.i("[Views] Calling resumeViewWithID vi[" + viewID + "]"); + + resumeViewWithIDInternal(viewID); + } + } + + /** + * Set a segmentation to be recorded with all views + * + * @param segmentation Map - global view segmentation + */ + public void setGlobalViewSegmentation(@Nullable Map segmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling setGlobalViewSegmentation sg[" + (segmentation == null ? segmentation : segmentation.size()) + "]"); + + setGlobalViewSegmentationInternal(segmentation); + } + } + + /** + * Updates the global segmentation for views + * + * @param segmentation Map - global view segmentation + */ + public void updateGlobalViewSegmentation(@Nullable Map segmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling updateGlobalViewSegmentation sg[" + (segmentation == null ? segmentation : segmentation.size()) + "]"); + + if (segmentation == null) { + L.w("[View] When updating segmentation values, they can't be 'null'."); + return; + } + + updateGlobalViewSegmentationInternal(segmentation); + } + } + + /** + * Stops all views and records a segmentation if set + * + * @param viewSegmentation Map - view segmentation + */ + public void stopAllViews(@Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopAllViews sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + + stopAllViewsInternal(viewSegmentation); + } + } + } } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java index b0f105c4d..ead958de7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java @@ -50,6 +50,16 @@ public static synchronized long uniqueTimestampMs() { return uniqueTimer.timestamp(); } + /** + * Wraps {@link System#currentTimeMillis()} to always return different value, even within + * same millisecond and even when time changes. Works in a limited window of 10 timestamps for now. + * + * @return unique time in seconds + */ + public static synchronized int uniqueTimestampS() { + return new Double(uniqueTimestampMs() / MS_IN_SECOND).intValue(); + } + /** * Convert time in nanoseconds to milliseconds * From 2f1f368a57a32874bf55c17b034378a7734a650a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 5 Dec 2023 13:33:15 +0300 Subject: [PATCH 02/38] feat: module views --- .../main/java/ly/count/sdk/java/Config.java | 33 +++++++ .../sdk/java/internal/InternalConfig.java | 12 +++ .../count/sdk/java/internal/ModuleViews.java | 90 +++---------------- 3 files changed, 56 insertions(+), 79 deletions(-) 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 7f114defc..afb4a18cc 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 @@ -437,6 +437,10 @@ public String toString() { // // } + protected boolean enableAutomaticViewTracking = false; + protected boolean autoTrackingUseShortName = false; + protected Map globalViewSegmentation = null; + // TODO: storage limits & configuration // protected int maxRequestsStored = 0; // protected int storageDirectory = ""; @@ -1479,4 +1483,33 @@ public Config remoteConfigRegisterGlobalCallback(RCDownloadCallback callback) { remoteConfigGlobalCallbacks.add(callback); return this; } + + /** + * Enable automatic view tracking + * + * @return {@code this} instance for method chaining + */ + public Config enableAutomaticViewTracking() { + this.enableAutomaticViewTracking = true; + return this; + } + + /** + * Enable short names for automatic view tracking + * + * @return {@code this} instance for method chaining + */ + public Config enableAutomaticViewShortNames() { + this.autoTrackingUseShortName = true; + return this; + } + + /** + * @param segmentation segmentation values that will be added for all recorded views (manual and automatic) + * @return {@code this} instance for method chaining + */ + public Config setGlobalViewSegmentation(Map segmentation) { + globalViewSegmentation = segmentation; + return this; + } } \ No newline at end of file diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index d5edf160d..172c01627 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -193,4 +193,16 @@ File getSdkStorageRootDirectory() { Log getLogger() { return configLog; } + + public boolean isAutomaticViewTrackingEnabled() { + return enableAutomaticViewTracking; + } + + public boolean isAutoTrackingUseShortNameEnabled() { + return autoTrackingUseShortName; + } + + public Map getGlobalViewSegmentation() { + return globalViewSegmentation; + } } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index a9ae13562..10dcc1708 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -12,26 +12,12 @@ public class ModuleViews extends ModuleBase { private String currentViewID = null; private String previousViewID = null; - private boolean firstView = true; - boolean autoViewTracker = false; boolean automaticTrackingShouldUseShortName = false; - - //track orientation changes - boolean trackOrientationChanges; - - int currentOrientation = -1; - final static String ORIENTATION_EVENT_KEY = "[CLY]_orientation"; - final static String VIEW_EVENT_KEY = "[CLY]_view"; - - Class[] autoTrackingActivityExceptions = null;//excluded activities from automatic view tracking - Map automaticViewSegmentation = new HashMap<>();//automatic view segmentation - Map viewDataMap = new HashMap<>(); // map viewIDs to its viewData - String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; public @Nonnull String getCurrentViewId() { @@ -47,7 +33,6 @@ static class ViewData { long viewStartTimeSeconds; // if this is 0 then the view is not started yet or was paused String viewName; boolean isAutoStoppedView = false;//views started with "startAutoStoppedView" would have this as "true". If set to "true" views should be automatically closed when another one is started. - boolean isAutoPaused = false;//this marks that this view automatically paused when going to the background } //interface for SDK users @@ -61,19 +46,17 @@ public void init(InternalConfig config) { super.init(config); L.v("[ModuleViews] Initializing"); - if (config.enableAutomaticViewTracking) { + if (config.isAutomaticViewTrackingEnabled()) { L.d("[ModuleViews] Enabling automatic view tracking"); - autoViewTracker = config.enableAutomaticViewTracking; + autoViewTracker = config.isAutomaticViewTrackingEnabled(); } - if (config.autoTrackingUseShortName) { + if (config.isAutoTrackingUseShortNameEnabled()) { L.d("[ModuleViews] Enabling automatic view tracking short names"); - automaticTrackingShouldUseShortName = config.autoTrackingUseShortName; + automaticTrackingShouldUseShortName = config.isAutoTrackingUseShortNameEnabled(); } - setGlobalViewSegmentationInternal(config.globalViewSegmentation); - trackOrientationChanges = config.trackOrientationChange; - + setGlobalViewSegmentationInternal(config.getGlobalViewSegmentation()); viewsInterface = new Views(); } @@ -128,7 +111,7 @@ public void resetFirstView() { firstView = true; } - Map CreateViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { + Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new HashMap<>(); if (customViewSegmentation != null) { viewSegmentation.putAll(customViewSegmentation); @@ -209,7 +192,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie accumulatedEventSegm.putAll(customViewSegmentation); } - Map viewSegmentation = CreateViewEventSegmentation(currentViewData, firstView, true, accumulatedEventSegm); + Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, accumulatedEventSegm); if (firstView) { L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); @@ -291,12 +274,12 @@ void recordViewEndEvent(ViewData vd, @Nullable Map filteredCusto } long viewDurationSeconds = lastElapsedDurationSeconds; - Map segments = CreateViewEventSegmentation(vd, false, false, accumulatedEventSegm); + Map segments = createViewEventSegmentation(vd, false, false, accumulatedEventSegm); segments.put("_idv", vd.viewID); Countly.instance().events().recordEvent(VIEW_EVENT_KEY, segments, 1, 0.0, new Long(viewDurationSeconds).doubleValue()); } - void pauseViewWithIDInternal(String viewID, boolean pausedAutomatically) { + void pauseViewWithIDInternal(String viewID) { if (viewID == null || viewID.isEmpty()) { L.e("[ModuleViews] pauseViewWithIDInternal, Trying to record view with null or empty view ID, ignoring request"); return; @@ -309,7 +292,7 @@ void pauseViewWithIDInternal(String viewID, boolean pausedAutomatically) { ViewData vd = viewDataMap.get(viewID); if (vd == null) { - L.e("[ModuleViews] pauseViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening, auto paused:[" + pausedAutomatically + "]"); + L.e("[ModuleViews] pauseViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); return; } @@ -320,8 +303,6 @@ void pauseViewWithIDInternal(String viewID, boolean pausedAutomatically) { return; } - vd.isAutoPaused = pausedAutomatically; - recordViewEndEvent(vd, null, "pauseViewWithIDInternal"); vd.viewStartTimeSeconds = 0; @@ -352,7 +333,6 @@ void resumeViewWithIDInternal(String viewID) { } vd.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); - vd.isAutoPaused = false; } void stopAllViewsInternal(Map viewSegmentation) { @@ -361,60 +341,12 @@ void stopAllViewsInternal(Map viewSegmentation) { autoCloseRequiredViews(true, viewSegmentation); } - void updateOrientation(int newOrientation) { - L.d("[ModuleViews] Calling [updateOrientation], new orientation:[" + newOrientation + "]"); - - if (!internalConfig.sdk.hasConsentForFeature(CoreFeature.UserProfiles)) { - //we don't have consent, just leave - return; - } - - if (currentOrientation != newOrientation) { - currentOrientation = newOrientation; - - Map segm = new HashMap<>(); - - if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { - segm.put("mode", "portrait"); - } else { - segm.put("mode", "landscape"); - } - - Countly.instance().events().recordEvent(ORIENTATION_EVENT_KEY, segm); - } - } - - void pauseRunningViewsAndSend() { - L.d("[ModuleViews] pauseRunningViewsAndSend, going to the background and pausing"); - for (Map.Entry entry : viewDataMap.entrySet()) { - ViewData vd = entry.getValue(); - - if (vd.viewStartTimeSeconds > 0) { - //if the view is running - pauseViewWithIDInternal(vd.viewID, true); - } - } - } - - void resumeAutoPausedViews() { - L.d("[ModuleViews] resumeAutoPausedViews, going to the foreground and resuming"); - for (Map.Entry entry : viewDataMap.entrySet()) { - ViewData vd = entry.getValue(); - - if (vd.isAutoPaused) { - //if the view was automatically paused, resume it - resumeViewWithIDInternal(vd.viewID); - } - } - } - @Override public void stop(InternalConfig config, boolean clear) { if (automaticViewSegmentation != null) { automaticViewSegmentation.clear(); automaticViewSegmentation = null; } - autoTrackingActivityExceptions = null; viewsInterface = null; } @@ -574,7 +506,7 @@ public void pauseViewWithID(@Nullable String viewID) { synchronized (Countly.instance()) { L.i("[Views] Calling pauseViewWithID vi[" + viewID + "]"); - pauseViewWithIDInternal(viewID, false); + pauseViewWithIDInternal(viewID); } } From 7bdd3877603bd53dbe7415ce7103ef036bac6906 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 5 Dec 2023 13:51:11 +0300 Subject: [PATCH 03/38] refactor: delete unused things --- .../count/sdk/java/internal/ModuleViews.java | 107 ++++++++---------- 1 file changed, 45 insertions(+), 62 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 10dcc1708..febc5bff7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -5,13 +5,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; import ly.count.sdk.java.Countly; public class ModuleViews extends ModuleBase { - private String currentViewID = null; - private String previousViewID = null; + String currentViewID = null; + String previousViewID = null; private boolean firstView = true; boolean autoViewTracker = false; boolean automaticTrackingShouldUseShortName = false; @@ -19,14 +20,8 @@ public class ModuleViews extends ModuleBase { Map automaticViewSegmentation = new HashMap<>();//automatic view segmentation Map viewDataMap = new HashMap<>(); // map viewIDs to its viewData String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; - - public @Nonnull String getCurrentViewId() { - return currentViewID == null ? "" : currentViewID; - } - - public @Nonnull String getPreviousViewId() { - return previousViewID == null ? "" : previousViewID; - } + //interface for SDK users + Views viewsInterface; static class ViewData { String viewID; @@ -35,9 +30,6 @@ static class ViewData { boolean isAutoStoppedView = false;//views started with "startAutoStoppedView" would have this as "true". If set to "true" views should be automatically closed when another one is started. } - //interface for SDK users - Views viewsInterface; - ModuleViews() { } @@ -112,7 +104,7 @@ public void resetFirstView() { } Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { - Map viewSegmentation = new HashMap<>(); + Map viewSegmentation = new ConcurrentHashMap<>(); if (customViewSegmentation != null) { viewSegmentation.putAll(customViewSegmentation); } @@ -124,8 +116,15 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi if (firstView) { viewSegmentation.put("start", "1"); } - viewSegmentation.put("segment", "Android"); + viewSegmentation.put("segment", internalConfig.getSdkPlatform()); + viewSegmentation.put("_idv", vd.viewID); + if (previousViewID != null) { + viewSegmentation.put("pvid", previousViewID); + } + if (currentViewID != null) { + viewSegmentation.put("cvid", currentViewID); + } return viewSegmentation; } @@ -187,7 +186,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie previousViewID = currentViewID; currentViewID = currentViewData.viewID; - Map accumulatedEventSegm = new HashMap(automaticViewSegmentation); + Map accumulatedEventSegm = new ConcurrentHashMap<>(automaticViewSegmentation); if (customViewSegmentation != null) { accumulatedEventSegm.putAll(customViewSegmentation); } @@ -199,7 +198,6 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie firstView = false; } - viewSegmentation.put("_idv", currentViewData.viewID); Countly.instance().events().recordEvent(VIEW_EVENT_KEY, viewSegmentation, 1, 0.0, 0.0); return currentViewData.viewID; @@ -229,19 +227,8 @@ void stopViewWithNameInternal(@Nullable String viewName, @Nullable Map customViewSegmentation) { - if (viewID == null || viewID.isEmpty()) { - L.e("[ModuleViews] stopViewWithNameInternal, Trying to record view with null or empty view ID, ignoring request"); - return; - } - //todo extract common checks - if (!viewDataMap.containsKey(viewID)) { - L.w("[ModuleViews] stopViewWithIDInternal, there is no view with the provided view id to close"); - return; - } - - ViewData vd = viewDataMap.get(viewID); + ViewData vd = validateViewID(viewID, "stopViewWithIDInternal"); if (vd == null) { - L.e("[ModuleViews] stopViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); return; } @@ -253,7 +240,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { long lastElapsedDurationSeconds = 0; - //we sanity check the time component and print error in case of problem + //we do sanity check the time component and print error in case of problem if (vd.viewStartTimeSeconds < 0) { L.e("[ModuleViews] " + viewRecordingSource + ", view start time value is not normal: [" + vd.viewStartTimeSeconds + "], ignoring that duration"); } else if (vd.viewStartTimeSeconds == 0) { @@ -268,31 +255,19 @@ void recordViewEndEvent(ViewData vd, @Nullable Map filteredCusto return; } - Map accumulatedEventSegm = new HashMap(automaticViewSegmentation); + Map accumulatedEventSegm = new ConcurrentHashMap<>(automaticViewSegmentation); if (filteredCustomViewSegmentation != null) { accumulatedEventSegm.putAll(filteredCustomViewSegmentation); } long viewDurationSeconds = lastElapsedDurationSeconds; Map segments = createViewEventSegmentation(vd, false, false, accumulatedEventSegm); - segments.put("_idv", vd.viewID); Countly.instance().events().recordEvent(VIEW_EVENT_KEY, segments, 1, 0.0, new Long(viewDurationSeconds).doubleValue()); } void pauseViewWithIDInternal(String viewID) { - if (viewID == null || viewID.isEmpty()) { - L.e("[ModuleViews] pauseViewWithIDInternal, Trying to record view with null or empty view ID, ignoring request"); - return; - } - - if (!viewDataMap.containsKey(viewID)) { - L.w("[ModuleViews] pauseViewWithIDInternal, there is no view with the provided view id to close"); - return; - } - - ViewData vd = viewDataMap.get(viewID); + ViewData vd = validateViewID(viewID, "pauseViewWithIDInternal"); if (vd == null) { - L.e("[ModuleViews] pauseViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); return; } @@ -309,19 +284,8 @@ void pauseViewWithIDInternal(String viewID) { } void resumeViewWithIDInternal(String viewID) { - if (viewID == null || viewID.isEmpty()) { - L.e("[ModuleViews] resumeViewWithIDInternal, Trying to record view with null or empty view ID, ignoring request"); - return; - } - - if (!viewDataMap.containsKey(viewID)) { - L.w("[ModuleViews] resumeViewWithIDInternal, there is no view with the provided view id to close"); - return; - } - - ViewData vd = viewDataMap.get(viewID); + ViewData vd = validateViewID(viewID, "resumeViewWithIDInternal"); if (vd == null) { - L.e("[ModuleViews] resumeViewWithIDInternal, view id:[" + viewID + "] has a 'null' value. This should not be happening"); return; } @@ -341,6 +305,25 @@ void stopAllViewsInternal(Map viewSegmentation) { autoCloseRequiredViews(true, viewSegmentation); } + private ViewData validateViewID(String viewID, String function) { + if (viewID == null || viewID.isEmpty()) { + L.e("[ModuleViews] validateViewID, " + function + ", Trying to process view with null or empty view ID, ignoring request"); + return null; + } + + if (!viewDataMap.containsKey(viewID)) { + L.w("[ModuleViews] validateViewID, " + function + ", there is no view with the provided view id"); + return null; + } + + ViewData vd = viewDataMap.get(viewID); + if (vd == null) { + L.e("[ModuleViews] validateViewID, " + function + ", view id:[" + viewID + "] has a 'null' value. This should not be happening"); + } + + return vd; + } + @Override public void stop(InternalConfig config, boolean clear) { if (automaticViewSegmentation != null) { @@ -432,7 +415,7 @@ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling startView vn[" + viewName + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + L.i("[Views] Calling startView vn[" + viewName + "] sg[" + viewSegmentation + "]"); if (autoViewTracker) { L.e("[Views] startView, manual view call will be ignored since automatic tracking is enabled."); @@ -464,7 +447,7 @@ public void stopViewWithName(@Nullable String viewName) { */ public void stopViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); stopViewWithNameInternal(viewName, viewSegmentation); } @@ -491,7 +474,7 @@ public void stopViewWithID(@Nullable String viewID) { */ public void stopViewWithID(@Nullable String viewID, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + viewSegmentation + "]"); stopViewWithIDInternal(viewID, viewSegmentation); } @@ -530,7 +513,7 @@ public void resumeViewWithID(@Nullable String viewID) { */ public void setGlobalViewSegmentation(@Nullable Map segmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling setGlobalViewSegmentation sg[" + (segmentation == null ? segmentation : segmentation.size()) + "]"); + L.i("[Views] Calling setGlobalViewSegmentation sg[" + segmentation + "]"); setGlobalViewSegmentationInternal(segmentation); } @@ -543,7 +526,7 @@ public void setGlobalViewSegmentation(@Nullable Map segmentation */ public void updateGlobalViewSegmentation(@Nullable Map segmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling updateGlobalViewSegmentation sg[" + (segmentation == null ? segmentation : segmentation.size()) + "]"); + L.i("[Views] Calling updateGlobalViewSegmentation sg[" + segmentation + "]"); if (segmentation == null) { L.w("[View] When updating segmentation values, they can't be 'null'."); @@ -561,7 +544,7 @@ public void updateGlobalViewSegmentation(@Nullable Map segmentat */ public void stopAllViews(@Nullable Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling stopAllViews sg[" + (viewSegmentation == null ? viewSegmentation : viewSegmentation.size()) + "]"); + L.i("[Views] Calling stopAllViews sg[" + viewSegmentation + "]"); stopAllViewsInternal(viewSegmentation); } From 8a00406b0d28e4a4560a1f8f1811f472973e02bb Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 5 Dec 2023 15:36:18 +0300 Subject: [PATCH 04/38] refactor: delete unnecessary things for views java --- .../main/java/ly/count/sdk/java/Config.java | 10 --- .../count/sdk/java/internal/ModuleViews.java | 77 +------------------ 2 files changed, 2 insertions(+), 85 deletions(-) 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 afb4a18cc..6457e76e2 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 @@ -439,7 +439,6 @@ public String toString() { protected boolean enableAutomaticViewTracking = false; protected boolean autoTrackingUseShortName = false; - protected Map globalViewSegmentation = null; // TODO: storage limits & configuration // protected int maxRequestsStored = 0; @@ -1503,13 +1502,4 @@ public Config enableAutomaticViewShortNames() { this.autoTrackingUseShortName = true; return this; } - - /** - * @param segmentation segmentation values that will be added for all recorded views (manual and automatic) - * @return {@code this} instance for method chaining - */ - public Config setGlobalViewSegmentation(Map segmentation) { - globalViewSegmentation = segmentation; - return this; - } } \ No newline at end of file diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index febc5bff7..14971a50c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -17,7 +17,6 @@ public class ModuleViews extends ModuleBase { boolean autoViewTracker = false; boolean automaticTrackingShouldUseShortName = false; final static String VIEW_EVENT_KEY = "[CLY]_view"; - Map automaticViewSegmentation = new HashMap<>();//automatic view segmentation Map viewDataMap = new HashMap<>(); // map viewIDs to its viewData String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; //interface for SDK users @@ -48,7 +47,6 @@ public void init(InternalConfig config) { automaticTrackingShouldUseShortName = config.isAutoTrackingUseShortNameEnabled(); } - setGlobalViewSegmentationInternal(config.getGlobalViewSegmentation()); viewsInterface = new Views(); } @@ -76,26 +74,6 @@ private void removeReservedKeysAndUnsupportedTypesFromViewSegmentation(Map segmentation) { - L.d("[ModuleViews] Calling setGlobalViewSegmentationInternal with[" + (segmentation == null ? "null" : segmentation.size()) + "] entries"); - - automaticViewSegmentation.clear(); - - if (segmentation != null) { - removeReservedKeysAndUnsupportedTypesFromViewSegmentation(segmentation); - automaticViewSegmentation.putAll(segmentation); - } - } - - public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { - removeReservedKeysAndUnsupportedTypesFromViewSegmentation(segmentation); - automaticViewSegmentation.putAll(segmentation); - } - /** * This should be called in case a new session starts so that we could identify the new "first view" */ @@ -119,12 +97,6 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi viewSegmentation.put("segment", internalConfig.getSdkPlatform()); viewSegmentation.put("_idv", vd.viewID); - if (previousViewID != null) { - viewSegmentation.put("pvid", previousViewID); - } - if (currentViewID != null) { - viewSegmentation.put("cvid", currentViewID); - } return viewSegmentation; } @@ -186,12 +158,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie previousViewID = currentViewID; currentViewID = currentViewData.viewID; - Map accumulatedEventSegm = new ConcurrentHashMap<>(automaticViewSegmentation); - if (customViewSegmentation != null) { - accumulatedEventSegm.putAll(customViewSegmentation); - } - - Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, accumulatedEventSegm); + Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, customViewSegmentation); if (firstView) { L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); @@ -255,13 +222,8 @@ void recordViewEndEvent(ViewData vd, @Nullable Map filteredCusto return; } - Map accumulatedEventSegm = new ConcurrentHashMap<>(automaticViewSegmentation); - if (filteredCustomViewSegmentation != null) { - accumulatedEventSegm.putAll(filteredCustomViewSegmentation); - } - long viewDurationSeconds = lastElapsedDurationSeconds; - Map segments = createViewEventSegmentation(vd, false, false, accumulatedEventSegm); + Map segments = createViewEventSegmentation(vd, false, false, filteredCustomViewSegmentation); Countly.instance().events().recordEvent(VIEW_EVENT_KEY, segments, 1, 0.0, new Long(viewDurationSeconds).doubleValue()); } @@ -326,10 +288,6 @@ private ViewData validateViewID(String viewID, String function) { @Override public void stop(InternalConfig config, boolean clear) { - if (automaticViewSegmentation != null) { - automaticViewSegmentation.clear(); - automaticViewSegmentation = null; - } viewsInterface = null; } @@ -506,37 +464,6 @@ public void resumeViewWithID(@Nullable String viewID) { } } - /** - * Set a segmentation to be recorded with all views - * - * @param segmentation Map - global view segmentation - */ - public void setGlobalViewSegmentation(@Nullable Map segmentation) { - synchronized (Countly.instance()) { - L.i("[Views] Calling setGlobalViewSegmentation sg[" + segmentation + "]"); - - setGlobalViewSegmentationInternal(segmentation); - } - } - - /** - * Updates the global segmentation for views - * - * @param segmentation Map - global view segmentation - */ - public void updateGlobalViewSegmentation(@Nullable Map segmentation) { - synchronized (Countly.instance()) { - L.i("[Views] Calling updateGlobalViewSegmentation sg[" + segmentation + "]"); - - if (segmentation == null) { - L.w("[View] When updating segmentation values, they can't be 'null'."); - return; - } - - updateGlobalViewSegmentationInternal(segmentation); - } - } - /** * Stops all views and records a segmentation if set * From f3c73539d16eee8696c0c8322eb005019eef4608 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 5 Dec 2023 15:39:10 +0300 Subject: [PATCH 05/38] refactor: delete unnecessary things for views java --- .../main/java/ly/count/sdk/java/internal/InternalConfig.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index 172c01627..7f9f0b72d 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -201,8 +201,4 @@ public boolean isAutomaticViewTrackingEnabled() { public boolean isAutoTrackingUseShortNameEnabled() { return autoTrackingUseShortName; } - - public Map getGlobalViewSegmentation() { - return globalViewSegmentation; - } } From c9d5de17ddb56ee64b24546e9896c2bfda464192 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 5 Dec 2023 15:43:21 +0300 Subject: [PATCH 06/38] feat: views to the flow --- .../src/main/java/ly/count/sdk/java/Countly.java | 16 ++++++++++++++++ .../java/ly/count/sdk/java/internal/SDKCore.java | 9 +++++++++ 2 files changed, 25 insertions(+) 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 dd93a9bd8..c51d3b8fb 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 @@ -13,6 +13,7 @@ import ly.count.sdk.java.internal.ModuleFeedback; import ly.count.sdk.java.internal.ModuleRemoteConfig; import ly.count.sdk.java.internal.ModuleUserProfile; +import ly.count.sdk.java.internal.ModuleViews; import ly.count.sdk.java.internal.SDKCore; /** @@ -467,6 +468,21 @@ public ModuleCrashes.Crashes crashes() { return sdk.crashes(); } + /** + * Views interface to use views feature. + * + * @return {@link ModuleViews.Views} instance. + */ + public ModuleViews.Views views() { + if (!isInitialized()) { + if (L != null) { + L.e("[Countly] SDK is not initialized yet."); + } + return null; + } + return sdk.views(); + } + /** * Get existing or create new timed event object, don't record it. * 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 cf18590d3..28697d589 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 @@ -397,6 +397,15 @@ public ModuleCrashes.Crashes crashes() { return module(ModuleCrashes.class).crashInterface; } + public ModuleViews.Views views() { + if (!hasConsentForFeature(CoreFeature.Views)) { + L.v("[SDKCore] views, Views feature has no consent, returning null"); + return null; + } + + return module(ModuleViews.class).viewsInterface; + } + public ModuleDeviceIdCore.DeviceId deviceId() { return module(ModuleDeviceIdCore.class).deviceIdInterface; } From 7423108cab312224de18aa19c8416c91d5d531dd Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 7 Dec 2023 10:15:41 +0300 Subject: [PATCH 07/38] fix: missing thingies --- sdk-java/src/main/java/ly/count/sdk/java/Config.java | 7 ++++--- .../java/ly/count/sdk/java/internal/InternalConfig.java | 1 + .../main/java/ly/count/sdk/java/internal/TimeUtils.java | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) 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 27d081d52..92e7a9bee 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 @@ -1496,9 +1496,10 @@ public Config remoteConfigRegisterGlobalCallback(RCDownloadCallback callback) { */ public Config enableAutomaticViewTracking() { this.enableAutomaticViewTracking = true; + return this; } - - /** + + /** * Set global location parameters * * @param countryCode ISO Country code @@ -1525,7 +1526,7 @@ public Config enableAutomaticViewShortNames() { this.autoTrackingUseShortName = true; return this; } - + /** * Disable location tracking * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index a3ca9c744..a63104239 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -200,6 +200,7 @@ public boolean isAutomaticViewTrackingEnabled() { public boolean isAutoTrackingUseShortNameEnabled() { return autoTrackingUseShortName; + } String[] getLocationParams() { return new String[] { country, city, location, ip }; diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java index 508265019..be2004d2c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/TimeUtils.java @@ -70,7 +70,8 @@ public static synchronized long uniqueTimestampMs() { */ public static synchronized int uniqueTimestampS() { return new Double(uniqueTimestampMs() / MS_IN_SECOND).intValue(); - + } + /** * Wraps {@link System#currentTimeMillis()} and returns it * From 3c961e37372e4c229f2e41394f5ae7b7ed71fc2b Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 18 Dec 2023 14:32:12 +0300 Subject: [PATCH 08/38] fix: missing curly bracket --- .../main/java/ly/count/sdk/java/internal/InternalConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index ab14742e4..b7e1ad36a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -194,13 +194,13 @@ Log getLogger() { return configLog; } - public boolean isAutomaticViewTrackingEnabled() { return enableAutomaticViewTracking; } public boolean isAutoTrackingUseShortNameEnabled() { return autoTrackingUseShortName; + } boolean isUnhandledCrashReportingEnabled() { return unhandledCrashReportingEnabled; From c7b30eb96e44510efb56ece5694a41bbe3d67a46 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 18 Dec 2023 14:38:07 +0300 Subject: [PATCH 09/38] fix: remove _idv --- .../src/main/java/ly/count/sdk/java/internal/ModuleViews.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 14971a50c..54b4ff172 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -96,7 +96,6 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi } viewSegmentation.put("segment", internalConfig.getSdkPlatform()); - viewSegmentation.put("_idv", vd.viewID); return viewSegmentation; } From 3ced2c6e9aa71d658fda4dae3108883f6e82931d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 18 Dec 2023 15:09:57 +0300 Subject: [PATCH 10/38] feat: new functions --- .../count/sdk/java/internal/ModuleViews.java | 97 ++++++++++++++----- 1 file changed, 75 insertions(+), 22 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 54b4ff172..113d07dd7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -27,6 +27,7 @@ static class ViewData { long viewStartTimeSeconds; // if this is 0 then the view is not started yet or was paused String viewName; boolean isAutoStoppedView = false;//views started with "startAutoStoppedView" would have this as "true". If set to "true" views should be automatically closed when another one is started. + Map viewSegmentation = new ConcurrentHashMap<>(); } ModuleViews() { @@ -87,6 +88,8 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi viewSegmentation.putAll(customViewSegmentation); } + viewSegmentation.putAll(vd.viewSegmentation); + viewSegmentation.put("name", vd.viewName); if (visit) { viewSegmentation.put("visit", "1"); @@ -158,6 +161,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie currentViewID = currentViewData.viewID; Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, customViewSegmentation); + currentViewData.viewSegmentation = viewSegmentation; if (firstView) { L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); @@ -170,22 +174,8 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie } void stopViewWithNameInternal(@Nullable String viewName, @Nullable Map customViewSegmentation) { - if (viewName == null || viewName.isEmpty()) { - L.e("[ModuleViews] stopViewWithNameInternal, Trying to record view with null or empty view name, ignoring request"); - return; - } - - String viewID = null; - - for (Map.Entry entry : viewDataMap.entrySet()) { - ViewData vd = entry.getValue(); - if (vd != null && viewName.equals(vd.viewName)) { - viewID = entry.getKey(); - } - } - + String viewID = validateViewWithName(viewName, "stopViewWithNameInternal"); if (viewID == null) { - L.e("[ModuleViews] stopViewWithNameInternal, No view entry found with the provided name :[" + viewName + "]"); return; } @@ -285,6 +275,50 @@ private ViewData validateViewID(String viewID, String function) { return vd; } + private String validateViewWithName(String viewName, String function) { + if (viewName == null || viewName.isEmpty()) { + L.e("[ModuleViews] " + function + ", Trying to process the view with null or empty view name, ignoring request"); + return null; + } + + String viewID = null; + + for (Map.Entry entry : viewDataMap.entrySet()) { + ViewData vd = entry.getValue(); + if (vd != null && viewName.equals(vd.viewName)) { + viewID = entry.getKey(); + } + } + + if (viewID == null) { + L.e("[ModuleViews] " + function + ", No view entry found with the provided name :[" + viewName + "]"); + } + + return viewID; + } + + void addSegmentationToViewWithNameInternal(@Nullable String viewName, @Nullable Map viewSegmentation) { + String viewID = validateViewWithName(viewName, "addSegmentationToViewWithNameInternal"); + if (viewID == null) { + return; + } + addSegmentationToViewWithIDInternal(viewID, viewSegmentation); + } + + private void addSegmentationToViewWithIDInternal(String viewID, Map viewSegmentation) { + ViewData vd = validateViewID(viewID, "addSegmentationToViewWithIdInternal"); + if (vd == null) { + return; + } + + if (viewSegmentation == null || viewSegmentation.isEmpty()) { + L.e("[ModuleViews] addSegmentationToViewWithIdInternal, Trying to record view with null or empty view segmentation, ignoring request"); + return; + } + removeReservedKeysAndUnsupportedTypesFromViewSegmentation(viewSegmentation); + vd.viewSegmentation.putAll(viewSegmentation); + } + @Override public void stop(InternalConfig config, boolean clear) { viewsInterface = null; @@ -391,7 +425,6 @@ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); - stopViewWithNameInternal(viewName, viewSegmentation); } } @@ -418,7 +450,6 @@ public void stopViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + viewSegmentation + "]"); - stopViewWithIDInternal(viewID, viewSegmentation); } } @@ -445,7 +475,6 @@ public void stopViewWithID(@Nullable String viewID, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling stopAllViews sg[" + viewSegmentation + "]"); - stopAllViewsInternal(viewSegmentation); } } + + /** + * Adds segmentation to a view with the given name + * + * @param viewName String + * @param viewSegmentation Map + */ + public void addSegmentationToViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling addSegmentationToViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); + addSegmentationToViewWithNameInternal(viewName, viewSegmentation); + } + } + + /** + * Adds segmentation to a view with the given ID + * + * @param viewId String + * @param viewSegmentation Map + */ + public void addSegmentationToViewWithID(@Nullable String viewId, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling addSegmentationToViewWithID vi[" + viewId + "] sg[" + viewSegmentation + "]"); + addSegmentationToViewWithIDInternal(viewId, viewSegmentation); + } + } } } From ffa09328a5a6c522620eb336ee015d46b6cd8e14 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 19 Dec 2023 10:18:13 +0300 Subject: [PATCH 11/38] fix: view segmentation --- .../src/main/java/ly/count/sdk/java/internal/ModuleViews.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 113d07dd7..660d1bb7b 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -161,7 +161,6 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie currentViewID = currentViewData.viewID; Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, customViewSegmentation); - currentViewData.viewSegmentation = viewSegmentation; if (firstView) { L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); From 7da3d55d012e902194a5427c21ee6bbf3924b81d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 19 Dec 2023 10:27:56 +0300 Subject: [PATCH 12/38] fix: convert to linekd hash map --- .../src/main/java/ly/count/sdk/java/internal/ModuleViews.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 660d1bb7b..d65eaae5a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -2,7 +2,7 @@ import java.security.SecureRandom; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -17,7 +17,7 @@ public class ModuleViews extends ModuleBase { boolean autoViewTracker = false; boolean automaticTrackingShouldUseShortName = false; final static String VIEW_EVENT_KEY = "[CLY]_view"; - Map viewDataMap = new HashMap<>(); // map viewIDs to its viewData + Map viewDataMap = new LinkedHashMap<>(); // map viewIDs to its viewData String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; //interface for SDK users Views viewsInterface; From 3efa3bf5118e7423a274cb1ba1dc2aad7b6c17aa Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 19 Dec 2023 10:34:45 +0300 Subject: [PATCH 13/38] feat: move convenient functions to utils --- .../java/internal/ModuleLocationsTests.java | 18 +-- .../java/internal/ModuleUserProfileTests.java | 18 +-- .../java/internal/ScenarioLocationTests.java | 22 +-- .../ly/count/sdk/java/internal/TestUtils.java | 53 ++++++++ .../sdk/java/internal/UserEditorTests.java | 125 +++++------------- 5 files changed, 118 insertions(+), 118 deletions(-) diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java index 41a3fd2cd..6784b085d 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleLocationsTests.java @@ -32,7 +32,7 @@ public void disableLocation() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); Countly.session().begin(); Countly.instance().location().disableLocation(); - validateLocationRequestInRQ(UserEditorTests.map("location", ""), 0); + validateLocationRequestInRQ(TestUtils.map("location", ""), 0); } /** @@ -57,7 +57,7 @@ public void setLocation() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); Countly.session().begin(); Countly.instance().location().setLocation("US", "New York", "1,2", "1.1.1.1"); - validateLocationRequestInRQ(UserEditorTests.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); + validateLocationRequestInRQ(TestUtils.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); } /** @@ -70,7 +70,7 @@ public void setLocation_cityOnly() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); Countly.session().begin(); Countly.instance().location().setLocation(null, "New York", "1,2", "1.1.1.1"); - validateLocationRequestInRQ(UserEditorTests.map("city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); + validateLocationRequestInRQ(TestUtils.map("city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); } /** @@ -82,9 +82,9 @@ public void setLocation_cityOnly() { public void setLocation_notBeganSession() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions)); Countly.instance().location().setLocation("US", "New York", "1,2", "1.1.1.1"); - validateLocationRequestInRQ(UserEditorTests.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); + validateLocationRequestInRQ(TestUtils.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); Countly.session().begin(); - validateLocationRequestInRQ(UserEditorTests.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1", "begin_session", "1"), 1); + validateLocationRequestInRQ(TestUtils.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1", "begin_session", "1"), 1); } /** @@ -97,10 +97,10 @@ public void setLocation_notBeganSession_withConfig() throws InterruptedException Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions) .setLocation("US", "New York", "1,2", "1.1.1.1")); Thread.sleep(100); - validateLocationRequestInRQ(UserEditorTests.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); + validateLocationRequestInRQ(TestUtils.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1"), 0); Countly.session().begin(); Thread.sleep(100); - validateLocationRequestInRQ(UserEditorTests.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1", "begin_session", "1"), 1); + validateLocationRequestInRQ(TestUtils.map("country_code", "US", "city", "New York", "location", "1,2", "ip", "1.1.1.1", "begin_session", "1"), 1); } /** @@ -113,9 +113,9 @@ public void disableLocation_notBeganSession_withConfig() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions) .setLocation("US", "New York", "1,2", "1.1.1.1") .disableLocation()); - validateLocationRequestInRQ(UserEditorTests.map("location", ""), 0); + validateLocationRequestInRQ(TestUtils.map("location", ""), 0); Countly.session().begin(); - validateLocationRequestInRQ(UserEditorTests.map("location", "", "begin_session", "1"), 1); + validateLocationRequestInRQ(TestUtils.map("location", "", "begin_session", "1"), 1); } /** diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java index 829497209..c9713e3b5 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleUserProfileTests.java @@ -45,7 +45,7 @@ public void setUserBasics() { Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.json( + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json( "name", "Test", "username", "TestUsername", "email", "test@test.test", @@ -77,10 +77,10 @@ public void clear() { Countly.instance().userProfile().clear(); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.json( + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json( "name", "Test", "username", "TestUsername", - "custom", UserEditorTests.map("level", 56) + "custom", TestUtils.map("level", 56) ))); } @@ -95,7 +95,7 @@ public void setProperties_empty_null() { Countly.instance().userProfile().setProperties(null); Countly.instance().userProfile().setProperties(new ConcurrentHashMap<>()); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map()); + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -109,7 +109,7 @@ public void increment() { Countly.instance().userProfile().increment("test"); Countly.instance().userProfile().incrementBy("test", 2); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", UserEditorTests.c(UserEditorTests.opJson("test", "$inc", 3)) )); } @@ -128,7 +128,7 @@ public void saveMax_Min() { Countly.instance().userProfile().saveMin(TestUtils.eKeys[1], 2); Countly.instance().userProfile().saveMin(TestUtils.eKeys[1], 0.002); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", UserEditorTests.c( UserEditorTests.opJson(TestUtils.eKeys[1], "$min", 0.002), UserEditorTests.opJson(TestUtils.eKeys[0], "$max", 9.62) ))); @@ -144,7 +144,7 @@ public void multiply() { Countly.instance().init(TestUtils.getBaseConfig()); Countly.instance().userProfile().multiply("test", 2); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", UserEditorTests.c(UserEditorTests.opJson("test", "$mul", 2)) )); } @@ -193,7 +193,7 @@ public void pullPush_base(String op, BiConsumer opFunction) { Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", UserEditorTests.c( UserEditorTests.opJson(TestUtils.eKeys[3], op, TestUtils.eKeys[2]), UserEditorTests.opJson(TestUtils.eKeys[0], op, TestUtils.eKeys[1], TestUtils.eKeys[2], 89, TestUtils.eKeys[2], "") ) @@ -211,7 +211,7 @@ public void setOnce() { Countly.instance().userProfile().setOnce(TestUtils.eKeys[0], 56); Countly.instance().userProfile().setOnce(TestUtils.eKeys[0], TestUtils.eKeys[1]); Countly.instance().userProfile().save(); - UserEditorTests.validateUserDetailsRequestInRQ(UserEditorTests.map("user_details", UserEditorTests.c( + UserEditorTests.validateUserDetailsRequestInRQ(TestUtils.map("user_details", UserEditorTests.c( UserEditorTests.opJson(TestUtils.eKeys[0], "$setOnce", TestUtils.eKeys[1])))); } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioLocationTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioLocationTests.java index c306c2800..6e5e6c1fe 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioLocationTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioLocationTests.java @@ -43,7 +43,7 @@ public void setLocation_noInitTimeConfigForLocation() throws InterruptedExceptio Countly.instance().location().setLocation(null, null, "1,2", null); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("location", "1,2"), 1); + validateLocationRequestInRQ(TestUtils.map("location", "1,2"), 1); endAndValidateEndSessionRequest(2); beginAndValidateSessionRequest(3, "begin_session", "1", "location", "1,2"); @@ -70,13 +70,13 @@ public void setLocationOnInitAndAfterInit() throws InterruptedException { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions) .setLocation("TR", "Izmir", null, null)); Thread.sleep(200); // wait for first init location req to be written - validateLocationRequestInRQ(UserEditorTests.map("city", "Izmir", "country_code", "TR"), 0); + validateLocationRequestInRQ(TestUtils.map("city", "Izmir", "country_code", "TR"), 0); beginAndValidateSessionRequest(1, "begin_session", "1", "city", "Izmir", "country_code", "TR"); Countly.instance().location().setLocation(null, null, "1,2", null); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("location", "1,2"), 2); + validateLocationRequestInRQ(TestUtils.map("location", "1,2"), 2); endAndValidateEndSessionRequest(3); beginAndValidateSessionRequest(4, "begin_session", "1", "location", "1,2"); @@ -104,20 +104,20 @@ public void setLocationOnInitAndAfterBeginSession() throws InterruptedException Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions) .setLocation("TR", "Izmir", null, null)); Thread.sleep(200); // wait for first init location req to be written - validateLocationRequestInRQ(UserEditorTests.map("city", "Izmir", "country_code", "TR"), 0); + validateLocationRequestInRQ(TestUtils.map("city", "Izmir", "country_code", "TR"), 0); beginAndValidateSessionRequest(1, "begin_session", "1", "city", "Izmir", "country_code", "TR"); Countly.instance().location().setLocation(null, null, "1,2", null); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("location", "1,2"), 2); + validateLocationRequestInRQ(TestUtils.map("location", "1,2"), 2); endAndValidateEndSessionRequest(3); beginAndValidateSessionRequest(4, "begin_session", "1", "location", "1,2"); Countly.instance().location().setLocation(null, null, null, "1.1.1.1"); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("ip", "1.1.1.1"), 5); + validateLocationRequestInRQ(TestUtils.map("ip", "1.1.1.1"), 5); endAndValidateEndSessionRequest(6); beginAndValidateSessionRequest(7, "begin_session", "1", "ip", "1.1.1.1"); @@ -141,17 +141,17 @@ public void setLocationOnInitAndBeforeBeginSession() throws InterruptedException Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location, Config.Feature.Sessions) .setLocation("TR", "Izmir", null, null)); Thread.sleep(200); // wait for first location req to be written - validateLocationRequestInRQ(UserEditorTests.map("country_code", "TR", "city", "Izmir"), 0); + validateLocationRequestInRQ(TestUtils.map("country_code", "TR", "city", "Izmir"), 0); Countly.instance().location().setLocation(null, null, "1,2", "1.1.1.1"); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("ip", "1.1.1.1", "location", "1,2"), 1); + validateLocationRequestInRQ(TestUtils.map("ip", "1.1.1.1", "location", "1,2"), 1); beginAndValidateSessionRequest(2, "begin_session", "1", "ip", "1.1.1.1", "location", "1,2"); Countly.instance().location().setLocation("TR", "Izmir", "3,4", null); Thread.sleep(200); // wait for location req to be written - validateLocationRequestInRQ(UserEditorTests.map("country_code", "TR", "location", "3,4", "city", "Izmir"), 3); + validateLocationRequestInRQ(TestUtils.map("country_code", "TR", "location", "3,4", "city", "Izmir"), 3); endAndValidateEndSessionRequest(4); beginAndValidateSessionRequest(5, "begin_session", "1", "country_code", "TR", "location", "3,4", "city", "Izmir"); @@ -161,13 +161,13 @@ public void setLocationOnInitAndBeforeBeginSession() throws InterruptedException private void beginAndValidateSessionRequest(int rqIdx, Object... params) throws InterruptedException { Countly.session().begin(); Thread.sleep(200); // wait for begin_session req to be written - validateLocationRequestInRQ(UserEditorTests.map(params), rqIdx); + validateLocationRequestInRQ(TestUtils.map(params), rqIdx); } private void endAndValidateEndSessionRequest(int rqIdx) throws InterruptedException { Countly.session().end(); Thread.sleep(200); // wait for end_session req to be written - validateLocationRequestInRQ(UserEditorTests.map("end_session", "1"), rqIdx); + validateLocationRequestInRQ(TestUtils.map("end_session", "1"), rqIdx); } /** diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java index fda18add6..567a215b9 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import ly.count.sdk.java.Config; import ly.count.sdk.java.Countly; @@ -518,4 +519,56 @@ public String toString() { return value; } } + + /** + * Converts a map to json string + * + * @param entries map to convert + * @return json string + */ + protected static String json(Map entries) { + return jsonObj(entries).toString(); + } + + /** + * Converts a map to json object + * + * @param entries map to convert + * @return json string + */ + protected static JSONObject jsonObj(Map entries) { + JSONObject json = new JSONObject(); + entries.forEach(json::put); + return json; + } + + /** + * Converts array of objects to json string + * Returns empty json if array is null or empty + * + * @param args array of objects + * @return json string + */ + protected static String json(Object... args) { + if (args == null || args.length == 0) { + return "{}"; + } + return json(map(args)); + } + + /** + * Converts array of objects to a 'String, Object' map + * + * @param args array of objects + * @return map + */ + protected static Map map(Object... args) { + Map map = new ConcurrentHashMap<>(); + if (args.length % 2 == 0) { + for (int i = 0; i < args.length; i += 2) { + map.put(args[i].toString(), args[i + 1]); + } + } + return map; + } } \ No newline at end of file diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java index a4dab7afa..ff3529037 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UserEditorTests.java @@ -3,7 +3,6 @@ import java.io.File; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Supplier; import ly.count.sdk.java.Config; @@ -48,7 +47,7 @@ public void setPicturePath_webUrl() { //set profile picture url and commit it sessionHandler(() -> Countly.instance().user().edit().setPicturePath(imgFileWebUrl).commit()); validatePictureAndPath(imgFileWebUrl, null); - validateUserDetailsRequestInRQ(map("user_details", "{\"picture\":\"" + imgFileWebUrl + "\"}")); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", "{\"picture\":\"" + imgFileWebUrl + "\"}")); } /** @@ -64,7 +63,7 @@ public void setPicturePath_localPath() { //set profile picture url and commit it sessionHandler(() -> Countly.instance().user().edit().setPicturePath(imgFile.getAbsolutePath()).commit()); validatePictureAndPath(imgFile.getAbsolutePath(), null); - validateUserDetailsRequestInRQ(map("user_details", "{}", "picturePath", imgFile.getAbsolutePath())); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", "{}", "picturePath", imgFile.getAbsolutePath())); } /** @@ -79,7 +78,7 @@ public void setPicturePath_null() { //set profile picture url and commit it sessionHandler(() -> Countly.instance().user().edit().setPicturePath(null).commit()); validatePictureAndPath(null, null); - validateUserDetailsRequestInRQ(map("user_details", "{\"picture\":null}")); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", "{\"picture\":null}")); } /** @@ -94,7 +93,7 @@ public void setPicturePath_garbage() { //set profile picture url and commit it sessionHandler(() -> Countly.instance().user().edit().setPicturePath("garbage_thing/.txt").commit()); validatePictureAndPath(null, null); - validateUserDetailsRequestInRQ(map()); + validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -112,7 +111,7 @@ public void setPicture_binaryData() { sessionHandler(() -> Countly.instance().user().edit().setPicture(imgData).commit()); validatePictureAndPath(null, imgData); Countly.session().end(); - validateUserDetailsRequestInRQ(map("user_details", "{}", "picturePath", ModuleUserProfile.PICTURE_IN_USER_PROFILE)); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", "{}", "picturePath", ModuleUserProfile.PICTURE_IN_USER_PROFILE)); } /** @@ -127,7 +126,7 @@ public void setPicture_null() { //set profile picture url and commit it sessionHandler(() -> Countly.instance().user().edit().setPicture(null).commit()); validatePictureAndPath(null, null); - validateUserDetailsRequestInRQ(map("user_details", "{\"picture\":null}")); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", "{\"picture\":null}")); } /** @@ -142,7 +141,7 @@ public void setOnce() { .setOnce(TestUtils.eKeys[0], 56) .setOnce(TestUtils.eKeys[0], TestUtils.eKeys[1]) .commit()); - validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", TestUtils.eKeys[1])))); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", TestUtils.eKeys[1])))); } /** @@ -154,7 +153,7 @@ public void setOnce() { public void setOnce_null() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit().setOnce(TestUtils.eKeys[0], null).commit()); - validateUserDetailsRequestInRQ(map()); + validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -166,7 +165,7 @@ public void setOnce_null() { public void setOnce_empty() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit().setOnce(TestUtils.eKeys[0], "").commit()); - validateUserDetailsRequestInRQ(map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", "")))); + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c(opJson(TestUtils.eKeys[0], "$setOnce", "")))); } /** @@ -184,7 +183,7 @@ public void setLocationBasics() { .setLocation(40.7128, -74.0060) .commit()); - validateUserDetailsRequestInRQ(map( + validateUserDetailsRequestInRQ(TestUtils.map( "country_code", "US", "city", "New York", "location", "40.7128,-74.006")); @@ -205,7 +204,7 @@ public void setLocationBasics_null() { .setLocation(null) .commit()); - validateUserDetailsRequestInRQ(map( + validateUserDetailsRequestInRQ(TestUtils.map( "country_code", JSONObject.NULL, "city", JSONObject.NULL, "location", JSONObject.NULL)); @@ -226,7 +225,7 @@ public void setLocationBasics_noConsent() { .setLocation(38.4237, 27.1428) .commit()); - validateUserDetailsRequestInRQ(map("locale", "tr")); + validateUserDetailsRequestInRQ(TestUtils.map("locale", "tr")); } /** @@ -273,7 +272,7 @@ private void pullPush_base(String op, BiFunction opF return opFunction.apply(TestUtils.eKeys[0], "").commit(); }); - validateUserDetailsRequestInRQ(map("user_details", c( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c( opJson(TestUtils.eKeys[3], op, TestUtils.eKeys[2]), opJson(TestUtils.eKeys[0], op, TestUtils.eKeys[1], TestUtils.eKeys[2], 89, TestUtils.eKeys[2], "") ) @@ -300,7 +299,7 @@ public void setCustom() { .setCustom("tags", new Object[] { "tag1", "tag2", 34, 67.8, null, "" }) .commit()); - validateUserDetailsRequestInRQ(map("user_details", c(map( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c(TestUtils.map( TestUtils.eKeys[0], TestUtils.eKeys[1], TestUtils.eKeys[5], "", TestUtils.eKeys[6], 128.987, @@ -325,7 +324,7 @@ public void max() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", c( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c( opJson(TestUtils.eKeys[2], "$max", 0), opJson(TestUtils.eKeys[1], "$max", -1), opJson(TestUtils.eKeys[0], "$max", 128))) @@ -349,7 +348,7 @@ public void min() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", c( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c( opJson(TestUtils.eKeys[2], "$min", 0), opJson(TestUtils.eKeys[1], "$min", -155.9), opJson(TestUtils.eKeys[0], "$min", 122))) @@ -373,7 +372,7 @@ public void inc() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", c( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c( opJson(TestUtils.eKeys[2], "$inc", 0), opJson(TestUtils.eKeys[1], "$inc", -155), opJson(TestUtils.eKeys[0], "$inc", 0))) @@ -400,7 +399,7 @@ public void mul() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", c( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", c( opJson(TestUtils.eKeys[3], "$mul", -90), opJson(TestUtils.eKeys[2], "$mul", 0), opJson(TestUtils.eKeys[1], "$mul", -5.28), @@ -427,7 +426,7 @@ public void setUserBasics() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", json( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json( "name", "Test", "username", "TestUsername", "email", "test@test.test", @@ -457,7 +456,7 @@ public void setUserBasics_null() { .commit() ); - validateUserDetailsRequestInRQ(map("user_details", json( + validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json( "name", JSONObject.NULL, "username", JSONObject.NULL, "email", JSONObject.NULL, @@ -475,7 +474,7 @@ public void setUserBasics_null() { */ @Test public void setBirthYear_invalidParam() { - setBirthYear_base(TestUtils.eKeys[0], map()); + setBirthYear_base(TestUtils.eKeys[0], TestUtils.map()); } /** @@ -485,7 +484,7 @@ public void setBirthYear_invalidParam() { */ @Test public void setBirthYear_stringInteger() { - setBirthYear_base("1999", map("user_details", json("byear", 1999))); + setBirthYear_base("1999", TestUtils.map("user_details", TestUtils.json("byear", 1999))); } /** @@ -495,7 +494,7 @@ public void setBirthYear_stringInteger() { */ @Test public void setBirthYear_stringNotInteger() { - setBirthYear_base("1999.0", map()); + setBirthYear_base("1999.0", TestUtils.map()); } private void setBirthYear_base(String value, Map expectedValues) { @@ -511,7 +510,7 @@ private void setBirthYear_base(String value, Map expectedValues) */ @Test public void setGender_invalid() { - setGender_base("Non-Binary", map()); + setGender_base("Non-Binary", TestUtils.map()); } /** @@ -521,7 +520,7 @@ public void setGender_invalid() { */ @Test public void setGender_number() { - setGender_base(1, map()); + setGender_base(1, TestUtils.map()); } /** @@ -531,7 +530,7 @@ public void setGender_number() { */ @Test public void setGender_string() { - setGender_base("M", map("user_details", json("gender", "M"))); + setGender_base("M", TestUtils.map("user_details", TestUtils.json("gender", "M"))); } private void setGender_base(Object gender, Map expectedValues) { @@ -549,7 +548,7 @@ private void setGender_base(Object gender, Map expectedValues) { public void setLocation_fromString() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation("-40.7128, 74.0060").commit()); - validateUserDetailsRequestInRQ(map("location", "-40.7128,74.006")); + validateUserDetailsRequestInRQ(TestUtils.map("location", "-40.7128,74.006")); } /** @@ -561,7 +560,7 @@ public void setLocation_fromString() { public void setLocation_fromString_noConsent() { Countly.instance().init(TestUtils.getBaseConfig()); sessionHandler(() -> Countly.instance().user().edit().setLocation("32.78, 28.01").commit()); - validateUserDetailsRequestInRQ(map()); + validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -573,7 +572,7 @@ public void setLocation_fromString_noConsent() { public void setLocation_fromString_invalid() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation(",28.34").commit()); - validateUserDetailsRequestInRQ(map()); + validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -585,7 +584,7 @@ public void setLocation_fromString_invalid() { public void setLocation_fromString_onePair() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation("61.32,").commit()); - validateUserDetailsRequestInRQ(map()); + validateUserDetailsRequestInRQ(TestUtils.map()); } /** @@ -597,7 +596,7 @@ public void setLocation_fromString_onePair() { public void setLocation_fromString_null() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().setLocation(null).commit()); - validateUserDetailsRequestInRQ(map("location", JSONObject.NULL)); + validateUserDetailsRequestInRQ(TestUtils.map("location", JSONObject.NULL)); } /** @@ -609,7 +608,7 @@ public void setLocation_fromString_null() { public void optOutFromLocationServices() { Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Location)); sessionHandler(() -> Countly.instance().user().edit().optOutFromLocationServices().commit()); - validateUserDetailsRequestInRQ(map("location", JSONObject.NULL, "country_code", JSONObject.NULL, "city", JSONObject.NULL)); + validateUserDetailsRequestInRQ(TestUtils.map("location", JSONObject.NULL, "country_code", JSONObject.NULL, "city", JSONObject.NULL)); } /** @@ -634,7 +633,7 @@ public void set_notAString() { //.set(ModuleUserProfile.COUNTRY_KEY, new TestUtils.AtomicString("Not a country")) //.set(ModuleUserProfile.LOCALE_KEY, new TestUtils.AtomicString("Not a locale")) .commit()); - validateUserDetailsRequestInRQ(map("user_details", json("name", "Magical", + validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json("name", "Magical", "username", "TestUsername", "email", "test@test.ly", "organization", "Magical Org", @@ -663,12 +662,12 @@ public void set_multipleCalls_sessionsEnabled() { .setBirthyear("3000") .setPicturePath("https://someurl.com") .commit()); - validateUserDetailsRequestInRQ(map("user_details", json("name", "SomeName", + validateUserDetailsRequestInRQ(TestUtils.map("user_details", TestUtils.json("name", "SomeName", "byear", 3000, "gender", "F", "picture", "https://someurl.com", "email", "SomeEmail", - "custom", jsonObj(map(TestUtils.eKeys[0], map("$push", new Object[] { 56, "TW" }), "some_custom", 56))), + "custom", TestUtils.jsonObj(TestUtils.map(TestUtils.eKeys[0], TestUtils.map("$push", new Object[] { 56, "TW" }), "some_custom", 56))), "country_code", "US", "city", "New York", "location", "40.7128,-74.006"), 1, 3); @@ -745,7 +744,7 @@ protected static String c(String... entries) { * @return wrapped json */ private String c(Map entries) { - return "{\"custom\":" + json(entries) + "}"; + return "{\"custom\":" + TestUtils.json(entries) + "}"; } /** @@ -778,56 +777,4 @@ private void sessionHandler(Supplier process) { Assert.assertNotNull(process.get()); Countly.session().end(); } - - /** - * Converts a map to json string - * - * @param entries map to convert - * @return json string - */ - protected static String json(Map entries) { - return jsonObj(entries).toString(); - } - - /** - * Converts a map to json object - * - * @param entries map to convert - * @return json string - */ - protected static JSONObject jsonObj(Map entries) { - JSONObject json = new JSONObject(); - entries.forEach(json::put); - return json; - } - - /** - * Converts array of objects to json string - * Returns empty json if array is null or empty - * - * @param args array of objects - * @return json string - */ - protected static String json(Object... args) { - if (args == null || args.length == 0) { - return "{}"; - } - return json(map(args)); - } - - /** - * Converts array of objects to a 'String, Object' map - * - * @param args array of objects - * @return map - */ - protected static Map map(Object... args) { - Map map = new ConcurrentHashMap<>(); - if (args.length % 2 == 0) { - for (int i = 0; i < args.length; i += 2) { - map.put(args[i].toString(), args[i + 1]); - } - } - return map; - } } From f120c022f77167efe4426143f7b60d2acaec42cb Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 19 Dec 2023 10:53:55 +0300 Subject: [PATCH 14/38] fix: remove unnecesarry check --- .../count/sdk/java/internal/ModuleViews.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index d65eaae5a..2baa88631 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -51,28 +51,17 @@ public void init(InternalConfig config) { viewsInterface = new Views(); } - private void removeReservedKeysAndUnsupportedTypesFromViewSegmentation(Map segmentation) { + private void removeReservedKeysFromViewSegmentation(Map segmentation) { if (segmentation == null) { return; } - List gonnaDeleteKeys = new ArrayList<>(); - for (String key : reservedSegmentationKeysViews) { if (segmentation.containsKey(key)) { segmentation.remove(key); L.w("[ModuleViews] removeReservedKeysAndUnsupportedTypesFromViewSegmentation, You cannot use the key:[" + key + "] in your segmentation since it's reserved by the SDK"); } } - - for (String key : segmentation.keySet()) { - if (!Utils.isValidDataType(segmentation.get(key))) { - gonnaDeleteKeys.add(key); - L.w("[ModuleViews] removeReservedKeysAndUnsupportedTypesFromViewSegmentation, You have provided an unsupported data type in your View Segmentation. Removing the unsupported values."); - } - } - - gonnaDeleteKeys.forEach(segmentation::remove); } /** @@ -117,7 +106,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie L.d("[ModuleViews] autoCloseRequiredViews, about to close [" + viewsToRemove.size() + "] views"); } - removeReservedKeysAndUnsupportedTypesFromViewSegmentation(customViewSegmentation); + removeReservedKeysFromViewSegmentation(customViewSegmentation); viewsToRemove.forEach(s -> stopViewWithIDInternal(s, customViewSegmentation)); } @@ -138,7 +127,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie return null; } - removeReservedKeysAndUnsupportedTypesFromViewSegmentation(customViewSegmentation); + removeReservedKeysFromViewSegmentation(customViewSegmentation); int segmCount = 0; if (customViewSegmentation != null) { @@ -314,7 +303,7 @@ private void addSegmentationToViewWithIDInternal(String viewID, Map Date: Tue, 19 Dec 2023 11:06:47 +0300 Subject: [PATCH 15/38] feat: session changes --- .../count/sdk/java/internal/ModuleViews.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 2baa88631..587ab3fb4 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -9,6 +9,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import ly.count.sdk.java.Countly; +import ly.count.sdk.java.Session; public class ModuleViews extends ModuleBase { String currentViewID = null; @@ -51,6 +52,18 @@ public void init(InternalConfig config) { viewsInterface = new Views(); } + @Override + public void onSessionBegan(Session session, InternalConfig config) { + super.onSessionBegan(session, config); + resetFirstView(); + } + + @Override + public void stop(InternalConfig config, boolean clear) { + viewsInterface = null; + viewDataMap.clear(); + } + private void removeReservedKeysFromViewSegmentation(Map segmentation) { if (segmentation == null) { return; @@ -73,11 +86,6 @@ public void resetFirstView() { Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new ConcurrentHashMap<>(); - if (customViewSegmentation != null) { - viewSegmentation.putAll(customViewSegmentation); - } - - viewSegmentation.putAll(vd.viewSegmentation); viewSegmentation.put("name", vd.viewName); if (visit) { @@ -87,6 +95,10 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi viewSegmentation.put("start", "1"); } viewSegmentation.put("segment", internalConfig.getSdkPlatform()); + if (customViewSegmentation != null) { + viewSegmentation.putAll(customViewSegmentation); + } + viewSegmentation.putAll(vd.viewSegmentation); return viewSegmentation; } @@ -307,11 +319,6 @@ private void addSegmentationToViewWithIDInternal(String viewID, Map Date: Tue, 19 Dec 2023 16:28:44 +0300 Subject: [PATCH 16/38] fix: delete unncessary --- .../main/java/ly/count/sdk/java/Config.java | 26 +-------------- .../sdk/java/internal/InternalConfig.java | 8 ----- .../count/sdk/java/internal/ModuleViews.java | 32 +------------------ 3 files changed, 2 insertions(+), 64 deletions(-) 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 ddd1ccd3b..a20a0cfcd 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 @@ -379,17 +379,13 @@ public Config disableUnhandledCrashReporting() { this.unhandledCrashReportingEnabled = false; return this; } - + protected String location = null; protected String ip = null; protected String city = null; protected String country = null; protected boolean locationEnabled = true; - - protected boolean enableAutomaticViewTracking = false; - protected boolean autoTrackingUseShortName = false; - // TODO: storage limits & configuration // protected int maxRequestsStored = 0; // protected int storageDirectory = ""; @@ -1433,16 +1429,6 @@ public Config remoteConfigRegisterGlobalCallback(RCDownloadCallback callback) { return this; } - /** - * Enable automatic view tracking - * - * @return {@code this} instance for method chaining - */ - public Config enableAutomaticViewTracking() { - this.enableAutomaticViewTracking = true; - return this; - } - /** * Set global location parameters * @@ -1461,16 +1447,6 @@ public Config setLocation(String countryCode, String cityName, String gpsCoordin return this; } - /** - * Enable short names for automatic view tracking - * - * @return {@code this} instance for method chaining - */ - public Config enableAutomaticViewShortNames() { - this.autoTrackingUseShortName = true; - return this; - } - /** * Disable location tracking * diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index b7e1ad36a..976f7b7c1 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -194,14 +194,6 @@ Log getLogger() { return configLog; } - public boolean isAutomaticViewTrackingEnabled() { - return enableAutomaticViewTracking; - } - - public boolean isAutoTrackingUseShortNameEnabled() { - return autoTrackingUseShortName; - } - boolean isUnhandledCrashReportingEnabled() { return unhandledCrashReportingEnabled; } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 587ab3fb4..35a5f0f1a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -15,8 +15,6 @@ public class ModuleViews extends ModuleBase { String currentViewID = null; String previousViewID = null; private boolean firstView = true; - boolean autoViewTracker = false; - boolean automaticTrackingShouldUseShortName = false; final static String VIEW_EVENT_KEY = "[CLY]_view"; Map viewDataMap = new LinkedHashMap<>(); // map viewIDs to its viewData String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; @@ -38,17 +36,6 @@ static class ViewData { public void init(InternalConfig config) { super.init(config); L.v("[ModuleViews] Initializing"); - - if (config.isAutomaticViewTrackingEnabled()) { - L.d("[ModuleViews] Enabling automatic view tracking"); - autoViewTracker = config.isAutomaticViewTrackingEnabled(); - } - - if (config.isAutoTrackingUseShortNameEnabled()) { - L.d("[ModuleViews] Enabling automatic view tracking short names"); - automaticTrackingShouldUseShortName = config.isAutoTrackingUseShortNameEnabled(); - } - viewsInterface = new Views(); } @@ -187,6 +174,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling startAutoStoppedView [" + viewName + "]"); - - if (autoViewTracker) { - L.e("[Views] startAutoStoppedView, manual view call will be ignored since automatic tracking is enabled."); - return null; - } - return startViewInternal(viewName, viewSegmentation, true); } } @@ -382,12 +364,6 @@ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling startView vn[" + viewName + "] sg[" + viewSegmentation + "]"); - - if (autoViewTracker) { - L.e("[Views] startView, manual view call will be ignored since automatic tracking is enabled."); - return null; - } - return startViewInternal(viewName, viewSegmentation, false); } } From 41427ab7d96429371566ab57b5482497c8d7394c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 19 Dec 2023 16:32:05 +0300 Subject: [PATCH 17/38] fix: whitespace --- sdk-java/src/main/java/ly/count/sdk/java/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a20a0cfcd..63739bd16 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 @@ -379,7 +379,7 @@ public Config disableUnhandledCrashReporting() { this.unhandledCrashReportingEnabled = false; return this; } - + protected String location = null; protected String ip = null; protected String city = null; From 45964c6ed563c9aeba305fa6063d906b30623989 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 20 Dec 2023 13:40:59 +0300 Subject: [PATCH 18/38] fix: some of orders --- .../count/sdk/java/internal/ModuleViews.java | 36 +++-- .../ly/count/sdk/java/internal/SDKCore.java | 144 +++++++++--------- 2 files changed, 101 insertions(+), 79 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 35a5f0f1a..914b1ea4f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -15,7 +15,13 @@ public class ModuleViews extends ModuleBase { String currentViewID = null; String previousViewID = null; private boolean firstView = true; - final static String VIEW_EVENT_KEY = "[CLY]_view"; + static final String KEY_VIEW_EVENT = "[CLY]_view"; + static final String KEY_NAME = "name"; + static final String KEY_VISIT = "visit"; + static final String KEY_VISIT_VALUE = "1"; + static final String KEY_SEGMENT = "segment"; + static final String KEY_START = "start"; + static final String KEY_START_VALUE = "1"; Map viewDataMap = new LinkedHashMap<>(); // map viewIDs to its viewData String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; //interface for SDK users @@ -40,8 +46,9 @@ public void init(InternalConfig config) { } @Override - public void onSessionBegan(Session session, InternalConfig config) { - super.onSessionBegan(session, config); + public void onSessionEnded(Session session, InternalConfig config) { + super.onSessionEnded(session, config); + stopAllViewsInternal(null); resetFirstView(); } @@ -74,14 +81,14 @@ public void resetFirstView() { Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new ConcurrentHashMap<>(); - viewSegmentation.put("name", vd.viewName); + viewSegmentation.put(KEY_NAME, vd.viewName); if (visit) { - viewSegmentation.put("visit", "1"); + viewSegmentation.put(KEY_VISIT, KEY_VISIT_VALUE); } if (firstView) { - viewSegmentation.put("start", "1"); + viewSegmentation.put(KEY_START, KEY_START_VALUE); } - viewSegmentation.put("segment", internalConfig.getSdkPlatform()); + viewSegmentation.put(KEY_SEGMENT, internalConfig.getSdkPlatform()); if (customViewSegmentation != null) { viewSegmentation.putAll(customViewSegmentation); } @@ -155,8 +162,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie firstView = false; } - Countly.instance().events().recordEvent(VIEW_EVENT_KEY, viewSegmentation, 1, 0.0, 0.0); - + recordView(0.0, viewSegmentation); return currentViewData.viewID; } @@ -182,6 +188,16 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map segmentation) { + ModuleEvents.Events events = Countly.instance().events(); + if (events == null) { + L.e("[ModuleViews] recordView, events module is not initialized"); + return; + } + + events.recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); + } + void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { long lastElapsedDurationSeconds = 0; //we do sanity check the time component and print error in case of problem @@ -201,7 +217,7 @@ void recordViewEndEvent(ViewData vd, @Nullable Map filteredCusto long viewDurationSeconds = lastElapsedDurationSeconds; Map segments = createViewEventSegmentation(vd, false, false, filteredCustomViewSegmentation); - Countly.instance().events().recordEvent(VIEW_EVENT_KEY, segments, 1, 0.0, new Long(viewDurationSeconds).doubleValue()); + recordView(new Long(viewDurationSeconds).doubleValue(), segments); } void pauseViewWithIDInternal(String viewID) { 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 a0a7a80af..bd8041f35 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 @@ -379,63 +379,6 @@ public SessionImpl getSession() { return null; } - public ModuleFeedback.Feedback feedback() { - - if (!hasConsentForFeature(CoreFeature.Feedback)) { - L.v("[SDKCore] feedback, Feedback feature has no consent, returning null"); - return null; - } - - return module(ModuleFeedback.class).feedbackInterface; - } - - public ModuleCrashes.Crashes crashes() { - if (!hasConsentForFeature(CoreFeature.CrashReporting)) { - L.v("[SDKCore] crash, Crash Reporting feature has no consent, returning null"); - return null; - } - - return module(ModuleCrashes.class).crashInterface; - } - - public ModuleViews.Views views() { - if (!hasConsentForFeature(CoreFeature.Views)) { - L.v("[SDKCore] views, Views feature has no consent, returning null"); - return null; - } - - return module(ModuleViews.class).viewsInterface; - } - - public ModuleDeviceIdCore.DeviceId deviceId() { - return module(ModuleDeviceIdCore.class).deviceIdInterface; - } - - public ModuleRemoteConfig.RemoteConfig remoteConfig() { - if (!hasConsentForFeature(CoreFeature.RemoteConfig)) { - L.v("[SDKCore] remoteConfig, RemoteConfig feature has no consent, returning null"); - return null; - } - - return module(ModuleRemoteConfig.class).remoteConfigInterface; - } - - public ModuleUserProfile.UserProfile userProfile() { - return module(ModuleUserProfile.class).userProfileInterface; - } - - public ModuleLocation.Location location() { - if (!hasConsentForFeature(CoreFeature.Location)) { - L.v("[SDKCore] location, Location feature has no consent, returning null"); - return null; - } - ModuleLocation module = module(ModuleLocation.class); - if (module == null) { - return null; - } - return module.locationInterface; - } - /** * Get current {@link SessionImpl} or create new one if current is {@code null}. * @@ -590,18 +533,6 @@ public UserImpl user() { return user; } - /** - * @return timedEvents interface - * @deprecated use {@link ModuleEvents.Events#startEvent(String)} instead via instance().events() call - */ - TimedEvents timedEvents() { - return ((ModuleSessions) module(CoreFeature.Sessions.getIndex())).timedEvents(); - } - - public ModuleEvents.Events events() { - return ((ModuleEvents) module(CoreFeature.Events.getIndex())).eventsInterface; - } - public InternalConfig config() { return config; } @@ -766,4 +697,79 @@ private boolean processCrash(InternalConfig config, Long id) { public void onRequest(InternalConfig config, Request request) { onSignal(config, SDKCore.Signal.Ping.getIndex(), null); } + + //Module functions + + /** + * @return timedEvents interface + * @deprecated use {@link ModuleEvents.Events#startEvent(String)} instead via instance().events() call + */ + TimedEvents timedEvents() { + return ((ModuleSessions) module(CoreFeature.Sessions.getIndex())).timedEvents(); + } + + public ModuleEvents.Events events() { + if (!hasConsentForFeature(CoreFeature.Events)) { + L.v("[SDKCore] events, Events feature has no consent, returning null"); + return null; + } + return module(ModuleEvents.class).eventsInterface; + } + + public ModuleFeedback.Feedback feedback() { + + if (!hasConsentForFeature(CoreFeature.Feedback)) { + L.v("[SDKCore] feedback, Feedback feature has no consent, returning null"); + return null; + } + + return module(ModuleFeedback.class).feedbackInterface; + } + + public ModuleCrashes.Crashes crashes() { + if (!hasConsentForFeature(CoreFeature.CrashReporting)) { + L.v("[SDKCore] crash, Crash Reporting feature has no consent, returning null"); + return null; + } + + return module(ModuleCrashes.class).crashInterface; + } + + public ModuleViews.Views views() { + if (!hasConsentForFeature(CoreFeature.Views)) { + L.v("[SDKCore] views, Views feature has no consent, returning null"); + return null; + } + + return module(ModuleViews.class).viewsInterface; + } + + public ModuleDeviceIdCore.DeviceId deviceId() { + return module(ModuleDeviceIdCore.class).deviceIdInterface; + } + + public ModuleRemoteConfig.RemoteConfig remoteConfig() { + if (!hasConsentForFeature(CoreFeature.RemoteConfig)) { + L.v("[SDKCore] remoteConfig, RemoteConfig feature has no consent, returning null"); + return null; + } + + return module(ModuleRemoteConfig.class).remoteConfigInterface; + } + + public ModuleUserProfile.UserProfile userProfile() { + return module(ModuleUserProfile.class).userProfileInterface; + } + + public ModuleLocation.Location location() { + if (!hasConsentForFeature(CoreFeature.Location)) { + L.v("[SDKCore] location, Location feature has no consent, returning null"); + return null; + } + ModuleLocation module = module(ModuleLocation.class); + if (module == null) { + return null; + } + return module.locationInterface; + } } From 4ca30d4cec4787c6e968d4585f2c7807af2b170c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 20 Dec 2023 13:59:35 +0300 Subject: [PATCH 19/38] feat: random val --- .../count/sdk/java/internal/ModuleViews.java | 17 +------------- .../ly/count/sdk/java/internal/Utils.java | 15 ++++++++++++ .../count/sdk/java/internal/UtilsTests.java | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 914b1ea4f..9ecd6192c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -1,6 +1,5 @@ package ly.count.sdk.java.internal; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -146,7 +145,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie autoCloseRequiredViews(false, null); ViewData currentViewData = new ViewData(); - currentViewData.viewID = safeRandomVal(); + currentViewData.viewID = Utils.safeRandomVal(); currentViewData.viewName = viewName; currentViewData.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); currentViewData.isAutoStoppedView = viewShouldBeAutomaticallyStopped; @@ -323,20 +322,6 @@ private void addSegmentationToViewWithIDInternal(String viewID, Map 0); + } else { + Assert.fail("No match for " + val); + } + } } From 216766ed59896e8cfd7570f378422993f849b1ee Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:04:15 +0300 Subject: [PATCH 20/38] Update ModuleViews.java --- .../main/java/ly/count/sdk/java/internal/ModuleViews.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 9ecd6192c..5cb83597f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -188,13 +188,12 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map segmentation) { - ModuleEvents.Events events = Countly.instance().events(); - if (events == null) { - L.e("[ModuleViews] recordView, events module is not initialized"); + if (!internalConfig.sdk.hasConsentForFeature(CoreFeature.Events)) { + L.e("[ModuleViews] recordView, not consent for events"); return; } - events.recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); + Countly.instance().events().recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); } void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { From 1913a1d307542baf1767e096f64bc040c3aa1658 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:05:56 +0300 Subject: [PATCH 21/38] Update ModuleViews.java --- .../main/java/ly/count/sdk/java/internal/ModuleViews.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 5cb83597f..9ecd6192c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -188,12 +188,13 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map segmentation) { - if (!internalConfig.sdk.hasConsentForFeature(CoreFeature.Events)) { - L.e("[ModuleViews] recordView, not consent for events"); + ModuleEvents.Events events = Countly.instance().events(); + if (events == null) { + L.e("[ModuleViews] recordView, events module is not initialized"); return; } - Countly.instance().events().recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); + events.recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); } void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { From 10904677bf20783944c8d7510f44f58ef58ad814 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:09:12 +0300 Subject: [PATCH 22/38] Update SDKCore.java --- .../src/main/java/ly/count/sdk/java/internal/SDKCore.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 bd8041f35..d3316a789 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 @@ -713,7 +713,13 @@ public ModuleEvents.Events events() { L.v("[SDKCore] events, Events feature has no consent, returning null"); return null; } - return module(ModuleEvents.class).eventsInterface; + + ModuleEvents module = module(ModuleEvents.class); + if (module == null) { + return null; + } + + return module.eventsInterface; } public ModuleFeedback.Feedback feedback() { From a767a523a38f6c5aee187a55e33ecaea5b28e0db Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:15:08 +0300 Subject: [PATCH 23/38] Update UtilsTests.java --- .../src/test/java/ly/count/sdk/java/internal/UtilsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java index 2ff90a645..e0489f1a3 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java @@ -572,7 +572,7 @@ public void readStream() { @Test public void safeRandomVal() throws NumberFormatException { String val = Utils.safeRandomVal(); - Pattern pattern = Pattern.compile("^([a-zA-Z0-9]{8})(\\d+)$"); + Pattern pattern = Pattern.compile("^(.{8})(\\d+)$"); Matcher matcher = pattern.matcher(val); if (matcher.matches()) { From 4de8422d02f0b65dfd29cadf0a95783354583351 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Jan 2024 14:45:40 +0300 Subject: [PATCH 24/38] fix: revert test --- .../count/sdk/java/internal/UtilsTests.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java index e0489f1a3..a3c83858e 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java @@ -13,8 +13,6 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import ly.count.sdk.java.Config; import org.junit.Assert; import org.junit.Before; @@ -561,25 +559,4 @@ public void readStream() { Assert.assertNull(Utils.readStream(null, logger)); Assert.assertEquals(value, new String(Utils.readStream(new ByteArrayInputStream(value.getBytes()), logger))); } - - /** - * "safeRandomVal" - * An random value is generated and validated by the regex, and consist of 2 parts - * Should return the string value generated by the algorithm and matches with the regex - * - * @throws NumberFormatException for parsing part 2 - */ - @Test - public void safeRandomVal() throws NumberFormatException { - String val = Utils.safeRandomVal(); - Pattern pattern = Pattern.compile("^(.{8})(\\d+)$"); - - Matcher matcher = pattern.matcher(val); - if (matcher.matches()) { - Assert.assertFalse(matcher.group(1).isEmpty()); - Assert.assertTrue(Long.parseLong(matcher.group(2)) > 0); - } else { - Assert.fail("No match for " + val); - } - } } From 2ba6412b4cc94457b30930d1379831b3606c50e7 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Jan 2024 14:50:14 +0300 Subject: [PATCH 25/38] fix: changes from child pr --- .../src/main/java/ly/count/sdk/java/internal/ModuleViews.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 9ecd6192c..fad7b154c 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -334,7 +334,6 @@ public class Views { */ public String startAutoStoppedView(@Nullable String viewName) { synchronized (Countly.instance()) { - // call the general function that has two parameters return startAutoStoppedView(viewName, null); } } @@ -351,7 +350,7 @@ public String startAutoStoppedView(@Nullable String viewName) { */ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling startAutoStoppedView [" + viewName + "]"); + L.i("[Views] Calling startAutoStoppedView [" + viewName + "] sg[" + viewSegmentation + "]"); return startViewInternal(viewName, viewSegmentation, true); } } From d45cea3b45cad27650156f4e9e1f9da5133f3bd3 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 4 Jan 2024 09:47:45 +0300 Subject: [PATCH 26/38] feat: migrate test branch logic to main --- CHANGELOG.md | 8 ++- .../main/java/ly/count/sdk/java/Countly.java | 10 ++++ .../main/java/ly/count/sdk/java/Usage.java | 11 ++-- .../src/main/java/ly/count/sdk/java/View.java | 14 +++++ .../count/sdk/java/internal/ModuleEvents.java | 9 +--- .../count/sdk/java/internal/SessionImpl.java | 21 ++++++-- .../ly/count/sdk/java/internal/ViewImpl.java | 53 ++++--------------- 7 files changed, 67 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9baeb66b8..e5323e213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ * "setLocale(String)" * Added the user profiles feature interface, and it is accessible through "Countly::instance()::userProfile()" call. - * Added the location feature interface, and it is accessible through "Countly::instance()::location()" call. * Added init time configuration for the location parameters: * "setLocation(String countryCode, String city, String location, String ipAddress)" @@ -12,6 +11,7 @@ * Crash Reporting interface added and accessible through "Countly::instance()::crash()" call. * Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. * Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. +* Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. * Fixed a bug where setting custom user properties would not work. * Fixed a bug where setting organization of the user would not work. @@ -46,6 +46,12 @@ * "setCustom(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call * "set(String, Object)" instead use "Countly::userProfile::setProperty" via "instance()" call * "picture(byte[])" instead use "Countly::userProfile::setProperty" via "instance()" call +* Deprecated "View::start(bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "View::stop(bool)" call, use "Countly::views::stopViewWithName" or "Countly::views::stopViewWithID" instead via "instance()" call. +* Deprecated "Usage::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Usage::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String)" call, use "Countly::views::startView" instead via "instance()" call. +* Deprecated "Countly::view(String, bool)" call, use "Countly::views::startView" instead via "instance()" call. ## 23.10.1 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 22cae574d..70cf205b9 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 @@ -563,12 +563,22 @@ public Usage addLocation(double latitude, double longitude) { return ((Session) sdk.session(null)).addLocation(latitude, longitude); } + /** + * {@inheritDoc} + * + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} instance() call + */ @Override public View view(String name, boolean start) { L.d("[Countly] view: name = " + name + " start = " + start); return ((Session) sdk.session(null)).view(name, start); } + /** + * {@inheritDoc} + * + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} instance() call + */ @Override public View view(String name) { L.d("[Countly] view: name = " + name); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Usage.java b/sdk-java/src/main/java/ly/count/sdk/java/Usage.java index e2e237f15..d33f555c0 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Usage.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Usage.java @@ -3,6 +3,7 @@ import java.util.Map; import ly.count.sdk.java.internal.ModuleDeviceIdCore; import ly.count.sdk.java.internal.ModuleEvents; +import ly.count.sdk.java.internal.ModuleViews; /** * This interface represents session concept, that is one indivisible usage occasion of your application. @@ -17,7 +18,7 @@ public interface Usage { /** * Create event object, don't record it yet. Creates begin request if this session - * hasn't yet been began. + * hasn't yet been begun. * * @param key key for this event, cannot be null or empty * @return Event instance. @@ -28,7 +29,7 @@ public interface Usage { /** * Get existing or create new timed event object, don't record it. Creates begin request if this session - * hasn't yet been began. + * hasn't yet been begun. * * @param key key for this event, cannot be null or empty * @return timed Event instance. @@ -97,21 +98,23 @@ public interface Usage { * Start new view. * In case previous view in this session is not ended yet, it will be ended automatically. * In case session ends and last view haven't been ended yet, it will be ended automatically. - * Creates begin request if this session hasn't yet been began. + * Creates begin request if this session hasn't yet been begun. * * @param name String representing name of this View * @param start whether this view is first in current application launch * @return new but already started {@link View}, you're responsible for its ending by calling {@link View#stop(boolean)} + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} */ View view(String name, boolean start); /** * Identical to {@link #view(String, boolean)}, but without {@code start} parameter which * is determined automatically based on whether this view is first in this session. - * Creates begin request if this session hasn't yet been began. + * Creates begin request if this session hasn't yet been begun. * * @param name String representing name of this View * @return new but already started {@link View}, you're responsible for its ending by calling {@link View#stop(boolean)} + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} */ View view(String name); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/View.java b/sdk-java/src/main/java/ly/count/sdk/java/View.java index 8e57b1502..2c970a7b0 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/View.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/View.java @@ -1,11 +1,25 @@ package ly.count.sdk.java; +import ly.count.sdk.java.internal.ModuleViews; /** * Contract interface for Views functionality */ public interface View { + + /** + * Start view + * + * @param firstView true if this is the first view in the session + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} + */ void start(boolean firstView); + /** + * Stop view + * + * @param lastView true if this is the last view in the session + * @deprecated use {@link ModuleViews.Views#stopViewWithName(String)} or {@link ModuleViews.Views#stopViewWithID(String)} instead via {@link Countly#views()} + */ void stop(boolean lastView); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index 6ca2564a4..c4b5df889 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -6,7 +6,6 @@ import java.util.stream.Collectors; import ly.count.sdk.java.Countly; import ly.count.sdk.java.Session; -import ly.count.sdk.java.View; public class ModuleEvents extends ModuleBase { protected EventQueue eventQueue = null; @@ -45,12 +44,8 @@ public void deviceIdChanged(String oldDeviceId, boolean withMerge) { // this part is to end and record the current view if exists Session session = Countly.session(); if ((session != null && session.isActive())) { - View currentView = ((SessionImpl) session).currentView; - if (currentView != null) { - currentView.stop(true); - } else { - Storage.pushAsync(internalConfig, ((SessionImpl) Countly.session())); - } + Countly.instance().views().stopAllViews(null); + Storage.pushAsync(internalConfig, ((SessionImpl) Countly.session())); } addEventsToRequestQ(oldDeviceId); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java index 715f9c434..bcfc502f7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java @@ -193,11 +193,7 @@ Future end(Long now, final Tasks.Callback callback, String did this.consents = SDKCore.instance.consents; - if (currentView != null) { - currentView.stop(true); - } else { - Storage.pushAsync(config, this); - } + Storage.pushAsync(config, this); Long duration = updateDuration(now); @@ -368,6 +364,14 @@ public Session addLocation(double latitude, double longitude) { return this; } + /** + * Start view + * + * @param name String representing name of this View + * @param start whether this view is first in current application launch + * @return View instance + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} + */ public View view(String name, boolean start) { L.d("[SessionImpl] view: name = " + name + " start = " + start); if (!SDKCore.enabled(CoreFeature.Views)) { @@ -385,6 +389,13 @@ public View view(String name, boolean start) { return currentView; } + /** + * Start view + * + * @param name String representing name of this View + * @return View instance + * @deprecated use {@link ModuleViews.Views#startView(String)} instead via {@link Countly#views()} + */ public View view(String name) { return view(name, startView); } 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 7b70fbebd..6c2c2c118 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 @@ -1,5 +1,6 @@ package ly.count.sdk.java.internal; +import ly.count.sdk.java.Countly; import ly.count.sdk.java.Session; import ly.count.sdk.java.View; @@ -9,18 +10,10 @@ class ViewImpl implements View { private Log L = null; - static final String EVENT = "[CLY]_view"; - static final String NAME = "name"; - static final String VISIT = "visit"; - static final String VISIT_VALUE = "1"; - static final String SEGMENT = "segment"; - static final String START = "start"; - static final String START_VALUE = "1"; - final String name; final Session session; - EventImpl start; - boolean started, ended; + boolean start = false; + boolean stop = false; ViewImpl(Session session, String name, Log logger) { this.L = logger; @@ -35,19 +28,13 @@ public void start(boolean firstView) { return; } - L.d("[ViewImpl] start: firstView = " + firstView); - if (started) { + if (start) { + L.w("[ViewImpl] start: View already started!"); return; } - this.started = true; - - start = (EventImpl) session.event(EVENT).addSegments(NAME, this.name, VISIT, VISIT_VALUE, SEGMENT, SDKCore.instance.config.getSdkPlatform()); - - if (firstView) { - start.addSegment(START, START_VALUE); - } - start.record(); + start = true; + Countly.instance().views().startView(name); } @Override @@ -57,27 +44,12 @@ public void stop(boolean lastView) { return; } - if (start == null) { - L.e("[ViewImpl] stop: We are trying to end a view that has not been started."); - return; - } - - L.d("[ViewImpl] stop: lastView = " + lastView); - - if (ended) { + if (stop) { + L.w("[ViewImpl] stop: View already stopped!"); return; } - ended = true; - EventImpl event = (EventImpl) session.event(EVENT).addSegments(NAME, this.name, SEGMENT, SDKCore.instance.config.getSdkPlatform()); - - long startTs = start.getTimestamp(); - long endTs = TimeUtils.timestampMs(); - - long viewDurationSeconds = (endTs - startTs) / 1000; - - event.setDuration(viewDurationSeconds); - - event.record(); + stop = true; + Countly.instance().views().stopViewWithName(name); } @Override @@ -85,9 +57,6 @@ public String toString() { return "ViewImpl{" + "name='" + name + '\'' + ", session=" + session + - ", start=" + start + - ", started=" + started + - ", ended=" + ended + '}'; } } From a8f684cb1d664e7705b8def5b118c73585cb1936 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Jan 2024 11:20:32 +0300 Subject: [PATCH 27/38] feat: ids --- .../ly/count/sdk/java/internal/EventImpl.java | 46 ++++++++++++++- .../count/sdk/java/internal/IdGenerator.java | 7 +++ .../sdk/java/internal/InternalConfig.java | 3 + .../count/sdk/java/internal/ModuleEvents.java | 59 +++++++++++++++---- .../count/sdk/java/internal/ModuleViews.java | 16 ++++- .../ly/count/sdk/java/internal/SDKCore.java | 22 +++++++ .../count/sdk/java/internal/SessionImpl.java | 2 +- .../sdk/java/internal/ViewIdProvider.java | 9 +++ .../sdk/java/internal/EventQueueTests.java | 2 +- 9 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/IdGenerator.java create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/ViewIdProvider.java 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 239c52b84..527a69826 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 @@ -26,6 +26,11 @@ class EventImpl implements Event, JSONable { protected int hour; protected int dow; + protected String id; + protected String pvid; + protected String cvid; + protected String peid; + final Log L; public interface EventRecorder { @@ -38,7 +43,7 @@ public interface EventRecorder { */ private boolean invalid = false; - EventImpl(@Nonnull String key, int count, Double sum, Double duration, @Nonnull Map segmentation, @Nonnull Log givenL) { + EventImpl(@Nonnull String key, int count, Double sum, Double duration, @Nonnull Map segmentation, @Nonnull Log givenL, String id, String pvid, String cvid, String peid) { L = givenL; this.recorder = null; @@ -51,6 +56,10 @@ public interface EventRecorder { this.timestamp = instant.timestamp; this.hour = instant.hour; this.dow = instant.dow; + this.id = id; + this.pvid = pvid; + this.cvid = cvid; + this.peid = peid; } EventImpl(@Nonnull EventRecorder recorder, @Nonnull String key, @Nonnull Log givenL) { @@ -239,6 +248,10 @@ public boolean equals(Object obj) { protected static final String TIMESTAMP_KEY = "timestamp"; protected static final String DAY_OF_WEEK = "dow"; protected static final String HOUR = "hour"; + protected static final String ID_KEY = "id"; + protected static final String PV_ID_KEY = "pvid"; + protected static final String CV_ID_KEY = "cvid"; + protected static final String PE_ID_KEY = "peid"; /** * Serialize to JSON format according to Countly server requirements @@ -266,6 +279,23 @@ public String toJSON(@Nonnull Log log) { if (duration != null) { json.put(DUR_KEY, duration); } + + //set the ID's only if they are not 'null' + if (id != null) { + json.put(ID_KEY, id); + } + + if (pvid != null) { + json.put(PV_ID_KEY, pvid); + } + + if (cvid != null) { + json.put(CV_ID_KEY, cvid); + } + + if (peid != null) { + json.put(PE_ID_KEY, peid); + } } catch (JSONException e) { log.e("[EventImpl] Cannot serialize event to JSON " + e); } @@ -304,6 +334,20 @@ public void recordEvent(Event event) { event.hour = json.optInt(HOUR); event.dow = json.optInt(DAY_OF_WEEK); + // the parsed ID's might not be set, or it might be set as null + if (!json.isNull(ID_KEY)) { + event.id = json.getString(ID_KEY); + } + if (!json.isNull(PV_ID_KEY)) { + event.pvid = json.getString(PV_ID_KEY); + } + if (!json.isNull(CV_ID_KEY)) { + event.cvid = json.getString(CV_ID_KEY); + } + if (!json.isNull(PE_ID_KEY)) { + event.peid = json.getString(PE_ID_KEY); + } + if (!json.isNull(SEGMENTATION_KEY)) { final JSONObject segm = json.getJSONObject(SEGMENTATION_KEY); final HashMap segmentation = new HashMap<>(segm.length()); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/IdGenerator.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/IdGenerator.java new file mode 100644 index 000000000..05b9fa0e6 --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/IdGenerator.java @@ -0,0 +1,7 @@ +package ly.count.sdk.java.internal; + +import javax.annotation.Nonnull; + +public interface IdGenerator { + @Nonnull String generateId(); +} diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index 976f7b7c1..2b7ea81f9 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -29,6 +29,9 @@ public class InternalConfig extends Config { ImmediateRequestGenerator immediateRequestGenerator = null; public SDKCore sdk; public StorageProvider storageProvider; + protected IdGenerator viewIdGenerator; + protected IdGenerator eventIdGenerator; + protected ViewIdProvider viewIdProvider; /** * Shouldn't be used! diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index c4b5df889..c9c989309 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -11,6 +11,9 @@ public class ModuleEvents extends ModuleBase { protected EventQueue eventQueue = null; final Map timedEvents = new ConcurrentHashMap<>(); protected Events eventsInterface = null; + ViewIdProvider viewIdProvider = null; + IdGenerator idGenerator = null; + String previousEventId = null; @Override public void init(InternalConfig config) { @@ -19,6 +22,14 @@ public void init(InternalConfig config) { eventQueue = new EventQueue(L, config.getEventsBufferSize()); eventQueue.restoreFromDisk(); eventsInterface = new Events(); + + idGenerator = config.eventIdGenerator; + } + + @Override + public void initFinished(InternalConfig config) { + super.initFinished(config); + viewIdProvider = config.viewIdProvider; } @Override @@ -97,26 +108,48 @@ protected void removeInvalidDataFromSegments(Map segments) { }); } - protected void recordEventInternal(String key, int count, Double sum, Double dur, Map segmentation) { + protected void recordEventInternal(String key, int count, Double sum, Double dur, Map segmentation, String eventIdOverride) { if (count <= 0) { - L.w("[ModuleEvents] recordEventInternal: Count can't be less than 1, ignoring this event."); + L.w("[ModuleEvents] recordEventInternal, Count can't be less than 1, ignoring this event."); return; } if (key == null || key.isEmpty()) { - L.w("[ModuleEvents] recordEventInternal: Key can't be null or empty, ignoring this event."); + L.w("[ModuleEvents] recordEventInternal, Key can't be null or empty, ignoring this event."); return; } + L.d("[ModuleEvents] recordEventInternal, Recording event with key: [" + key + "] and provided event ID of:[" + eventIdOverride + "] and segmentation with:[" + (segmentation == null ? "null" : segmentation.size()) + "] keys"); + removeInvalidDataFromSegments(segmentation); - EventImpl event = new EventImpl(key, count, sum, dur, segmentation, L); - addEventToQueue(event); - } - private void addEventToQueue(EventImpl event) { - L.d("[ModuleEvents] addEventToQueue"); - eventQueue.addEvent(event); - checkEventQueueToSend(false); + String eventId, pvid = null, cvid = null; + if (Utils.isEmptyOrNull(eventIdOverride)) { + L.d("[ModuleEvents] recordEventInternal, Generating new event id because it was null or empty"); + eventId = idGenerator.generateId(); + } else { + eventId = eventIdOverride; + } + + if (key.equals(ModuleViews.KEY_VIEW_EVENT)) { + pvid = viewIdProvider.getPreviousViewId(); + } else { + cvid = viewIdProvider.getCurrentViewId(); + } + + boolean forceSend = false; + String previousEventIdToSend = this.previousEventId; + if (key.equals(FeedbackWidgetType.nps.eventKey) || key.equals(FeedbackWidgetType.survey.eventKey)) { + forceSend = true; + previousEventIdToSend = null; + } else if (key.equals(ModuleViews.KEY_VIEW_EVENT) || key.equals(FeedbackWidgetType.rating.eventKey)) { + previousEventIdToSend = null; + } else { + this.previousEventId = eventId; + } + + eventQueue.addEvent(new EventImpl(key, count, sum, dur, segmentation, L, eventId, pvid, cvid, previousEventIdToSend)); + checkEventQueueToSend(forceSend); } private void checkEventQueueToSend(boolean forceSend) { @@ -143,7 +176,7 @@ boolean startEventInternal(final String key) { L.w("startEventInternal, eventRecorder, No timed event with the name [" + key + "] is started, nothing to end. Will ignore call."); return; } - recordEventInternal(eventImpl.key, eventImpl.count, eventImpl.sum, eventImpl.duration, eventImpl.segmentation); + recordEventInternal(eventImpl.key, eventImpl.count, eventImpl.sum, eventImpl.duration, eventImpl.segmentation, eventImpl.id); }, key, L)); return true; @@ -174,7 +207,7 @@ boolean endEventInternal(final String key, final Map segmentatio long currentTimestamp = TimeUtils.timestampMs(); double duration = (currentTimestamp - event.timestamp) / 1000.0; - recordEventInternal(key, count, sum, duration, segmentation); + recordEventInternal(key, count, sum, duration, segmentation, null); return true; } @@ -202,7 +235,7 @@ public class Events { */ public void recordEvent(String key, Map segmentation, int count, Double sum, Double dur) { L.i("[Events] recordEvent: key = " + key + ", count = " + count + ", sum = " + sum + ", segmentation = " + segmentation + ", dur = " + dur); - recordEventInternal(key, count, sum, dur, segmentation); + recordEventInternal(key, count, sum, dur, segmentation, null); } /** diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index fad7b154c..dea31b571 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -10,7 +10,7 @@ import ly.count.sdk.java.Countly; import ly.count.sdk.java.Session; -public class ModuleViews extends ModuleBase { +public class ModuleViews extends ModuleBase implements ViewIdProvider { String currentViewID = null; String previousViewID = null; private boolean firstView = true; @@ -25,6 +25,7 @@ public class ModuleViews extends ModuleBase { String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; //interface for SDK users Views viewsInterface; + IdGenerator idGenerator; static class ViewData { String viewID; @@ -42,6 +43,9 @@ public void init(InternalConfig config) { super.init(config); L.v("[ModuleViews] Initializing"); viewsInterface = new Views(); + + idGenerator = config.viewIdGenerator; + config.viewIdProvider = this; } @Override @@ -145,7 +149,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie autoCloseRequiredViews(false, null); ViewData currentViewData = new ViewData(); - currentViewData.viewID = Utils.safeRandomVal(); + currentViewData.viewID = idGenerator.generateId(); currentViewData.viewName = viewName; currentViewData.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); currentViewData.isAutoStoppedView = viewShouldBeAutomaticallyStopped; @@ -322,6 +326,14 @@ private void addSegmentationToViewWithIDInternal(String viewID, Map(config.getRequestQueueMaxSize()); + + if (config.viewIdGenerator == null) { + config.viewIdGenerator = Utils::safeRandomVal; + } + + if (config.eventIdGenerator == null) { + config.eventIdGenerator = Utils::safeRandomVal; + } + + if (config.viewIdProvider == null) { + config.viewIdProvider = new ViewIdProvider() { + @Nonnull public String getCurrentViewId() { + return null; + } + + @Nonnull public String getPreviousViewId() { + return null; + } + }; + } + // ModuleSessions is always enabled, even without consent int consents = config.getFeatures1() | CoreFeature.Sessions.getIndex(); // build modules diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java index bcfc502f7..2b5dbe3d1 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java @@ -307,7 +307,7 @@ public void recordEvent(Event event) { ModuleEvents eventsModule = (ModuleEvents) SDKCore.instance.module(CoreFeature.Events.getIndex()); EventImpl eventImpl = (EventImpl) event; - eventsModule.recordEventInternal(eventImpl.key, eventImpl.count, eventImpl.sum, eventImpl.duration, eventImpl.segmentation); + eventsModule.recordEventInternal(eventImpl.key, eventImpl.count, eventImpl.sum, eventImpl.duration, eventImpl.segmentation, eventImpl.id); } @Override diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewIdProvider.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewIdProvider.java new file mode 100644 index 000000000..a37fdd1fd --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewIdProvider.java @@ -0,0 +1,9 @@ +package ly.count.sdk.java.internal; + +import javax.annotation.Nonnull; + +public interface ViewIdProvider { + @Nonnull String getCurrentViewId(); + + @Nonnull String getPreviousViewId(); +} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java index cad1f0a68..c6a925677 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java @@ -247,7 +247,7 @@ static void writeToEventQueue(String fileContent, boolean delete) throws IOExcep } private EventImpl createEvent(String key, Map segmentation, int count, Double sum, Double dur) { - return new EventImpl(key, count, sum, dur, segmentation, L); + return new EventImpl(key, count, sum, dur, segmentation, L, null, null, null, null); } public static void validateEventInQueue(String key, Map segmentation, From 1e9179c7e52a6da5655cb7a703276e27dae40acf Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Jan 2024 11:45:07 +0300 Subject: [PATCH 28/38] feat: usage of ids --- .../java/ly/count/sdk/java/internal/ModuleViews.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index dea31b571..756a2a8aa 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -165,7 +165,7 @@ void autoCloseRequiredViews(boolean closeAllViews, Map customVie firstView = false; } - recordView(0.0, viewSegmentation); + recordView(currentViewID, 0.0, viewSegmentation); return currentViewData.viewID; } @@ -191,14 +191,14 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map segmentation) { - ModuleEvents.Events events = Countly.instance().events(); + void recordView(String id, Double duration, Map segmentation) { + ModuleEvents events = internalConfig.sdk.module(ModuleEvents.class); if (events == null) { L.e("[ModuleViews] recordView, events module is not initialized"); return; } - events.recordEvent(KEY_VIEW_EVENT, segmentation, 1, 0.0, duration); + events.recordEventInternal(KEY_VIEW_EVENT, 1, 0.0, duration, segmentation, id); } void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { @@ -220,7 +220,7 @@ void recordViewEndEvent(ViewData vd, @Nullable Map filteredCusto long viewDurationSeconds = lastElapsedDurationSeconds; Map segments = createViewEventSegmentation(vd, false, false, filteredCustomViewSegmentation); - recordView(new Long(viewDurationSeconds).doubleValue(), segments); + recordView(vd.viewID, new Long(viewDurationSeconds).doubleValue(), segments); } void pauseViewWithIDInternal(String viewID) { From e204b9ffdcc7d7092972adc2d3ba5059bfa854ce Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Jan 2024 12:00:21 +0300 Subject: [PATCH 29/38] feat: revert force send --- .../ly/count/sdk/java/internal/ModuleEvents.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index c9c989309..d082c5dc8 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -137,19 +137,20 @@ protected void recordEventInternal(String key, int count, Double sum, Double dur cvid = viewIdProvider.getCurrentViewId(); } - boolean forceSend = false; String previousEventIdToSend = this.previousEventId; - if (key.equals(FeedbackWidgetType.nps.eventKey) || key.equals(FeedbackWidgetType.survey.eventKey)) { - forceSend = true; - previousEventIdToSend = null; - } else if (key.equals(ModuleViews.KEY_VIEW_EVENT) || key.equals(FeedbackWidgetType.rating.eventKey)) { + if (key.equals(FeedbackWidgetType.nps.eventKey) || key.equals(FeedbackWidgetType.survey.eventKey) || key.equals(ModuleViews.KEY_VIEW_EVENT) || key.equals(FeedbackWidgetType.rating.eventKey)) { previousEventIdToSend = null; } else { this.previousEventId = eventId; } - eventQueue.addEvent(new EventImpl(key, count, sum, dur, segmentation, L, eventId, pvid, cvid, previousEventIdToSend)); - checkEventQueueToSend(forceSend); + addEventToQueue(new EventImpl(key, count, sum, dur, segmentation, L, eventId, pvid, cvid, previousEventIdToSend)); + } + + private void addEventToQueue(EventImpl event) { + L.d("[ModuleEvents] addEventToQueue"); + eventQueue.addEvent(event); + checkEventQueueToSend(false); } private void checkEventQueueToSend(boolean forceSend) { From 7f65a1e6bae683d3502b8b10d9f756f910a3626a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 9 Jan 2024 16:01:24 +0300 Subject: [PATCH 30/38] fix: nonull --- .../count/sdk/java/internal/ModuleViews.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 756a2a8aa..440633b37 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -344,7 +344,7 @@ public class Views { * @param viewName String - name of the view * @return Returns View ID */ - public String startAutoStoppedView(@Nullable String viewName) { + public String startAutoStoppedView(@Nonnull String viewName) { synchronized (Countly.instance()) { return startAutoStoppedView(viewName, null); } @@ -360,7 +360,7 @@ public String startAutoStoppedView(@Nullable String viewName) { * @param viewSegmentation Map - segmentation that will be added to the view, set 'null' if none should be added * @return String - view ID */ - public String startAutoStoppedView(@Nullable String viewName, @Nullable Map viewSegmentation) { + public String startAutoStoppedView(@Nonnull String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling startAutoStoppedView [" + viewName + "] sg[" + viewSegmentation + "]"); return startViewInternal(viewName, viewSegmentation, true); @@ -373,7 +373,7 @@ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map - segmentation that will be added to the view, set 'null' if none should be added * @return String - View ID */ - public @Nullable String startView(@Nullable String viewName, @Nullable Map viewSegmentation) { + public @Nullable String startView(@Nonnull String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling startView vn[" + viewName + "] sg[" + viewSegmentation + "]"); return startViewInternal(viewName, viewSegmentation, false); @@ -399,7 +399,7 @@ public String startAutoStoppedView(@Nullable String viewName, @Nullable Map - view segmentation */ - public void stopViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { + public void stopViewWithName(@Nonnull String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); stopViewWithNameInternal(viewName, viewSegmentation); @@ -424,7 +424,7 @@ public void stopViewWithName(@Nullable String viewName, @Nullable Map - view segmentation */ - public void stopViewWithID(@Nullable String viewID, @Nullable Map viewSegmentation) { + public void stopViewWithID(@Nonnull String viewID, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + viewSegmentation + "]"); stopViewWithIDInternal(viewID, viewSegmentation); @@ -449,7 +449,7 @@ public void stopViewWithID(@Nullable String viewID, @Nullable Map viewSegmentation) { * @param viewName String * @param viewSegmentation Map */ - public void addSegmentationToViewWithName(@Nullable String viewName, @Nullable Map viewSegmentation) { + public void addSegmentationToViewWithName(@Nonnull String viewName, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling addSegmentationToViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); addSegmentationToViewWithNameInternal(viewName, viewSegmentation); @@ -499,7 +499,7 @@ public void addSegmentationToViewWithName(@Nullable String viewName, @Nullable M * @param viewId String * @param viewSegmentation Map */ - public void addSegmentationToViewWithID(@Nullable String viewId, @Nullable Map viewSegmentation) { + public void addSegmentationToViewWithID(@Nonnull String viewId, @Nullable Map viewSegmentation) { synchronized (Countly.instance()) { L.i("[Views] Calling addSegmentationToViewWithID vi[" + viewId + "] sg[" + viewSegmentation + "]"); addSegmentationToViewWithIDInternal(viewId, viewSegmentation); From 06acbc070f22b60da610f7568600aa182d12c9d9 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 9 Jan 2024 17:12:47 +0300 Subject: [PATCH 31/38] feat: migrate from test branch --- .../main/java/ly/count/sdk/java/internal/SDKCore.java | 9 +++++---- .../main/java/ly/count/sdk/java/internal/ViewImpl.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 a0369da51..83b2a4c1a 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 @@ -447,11 +447,11 @@ public void init(final InternalConfig givenConfig) { if (config.viewIdProvider == null) { config.viewIdProvider = new ViewIdProvider() { @Nonnull public String getCurrentViewId() { - return null; + return ""; } @Nonnull public String getPreviousViewId() { - return null; + return ""; } }; } @@ -755,12 +755,13 @@ public ModuleCrashes.Crashes crashes() { } public ModuleViews.Views views() { - if (!hasConsentForFeature(CoreFeature.Views)) { + ModuleViews module = module(ModuleViews.class); + if (module == null) { L.v("[SDKCore] views, Views feature has no consent, returning null"); return null; } - return module(ModuleViews.class).viewsInterface; + return module.viewsInterface; } public ModuleDeviceIdCore.DeviceId deviceId() { 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 6c2c2c118..9914ef354 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 @@ -34,7 +34,7 @@ public void start(boolean firstView) { } start = true; - Countly.instance().views().startView(name); + Countly.instance().views().startAutoStoppedView(name); } @Override From 3cdb82e55a60742e6ad90d3c38bcc9fec757bd1c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jan 2024 11:14:02 +0300 Subject: [PATCH 32/38] feat: global segmns --- CHANGELOG.md | 2 + .../main/java/ly/count/sdk/java/Config.java | 3 + .../count/sdk/java/internal/ConfigViews.java | 23 +++++++ .../count/sdk/java/internal/ModuleEvents.java | 2 +- .../count/sdk/java/internal/ModuleViews.java | 62 +++++++++++++++++++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 sdk-java/src/main/java/ly/count/sdk/java/internal/ConfigViews.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e5323e213..e1c65319a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * Added "disableUnhandledCrashReporting" function to the "Config" class to disable automatic uncaught crash reporting. * Added "setMaxBreadcrumbCount(int)" function to the "Config" class to change allowed max breadcrumb count. * Added the views feature interface, and it is accessible through "Countly::instance()::views()" call. +* Added a configuration function to set global view segmentation to the "Config" class: + * "views.setGlobalViewSegmentation(Map)" * Fixed a bug where setting custom user properties would not work. * Fixed a bug where setting organization of the user would not work. 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 93836f3f2..321c42d79 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 @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import ly.count.sdk.java.internal.ConfigViews; import ly.count.sdk.java.internal.CoreFeature; import ly.count.sdk.java.internal.Log; import ly.count.sdk.java.internal.LogCallback; @@ -1456,4 +1457,6 @@ public Config disableLocation() { locationEnabled = false; return this; } + + public ConfigViews views = new ConfigViews(this); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ConfigViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ConfigViews.java new file mode 100644 index 000000000..6886181b0 --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ConfigViews.java @@ -0,0 +1,23 @@ +package ly.count.sdk.java.internal; + +import java.util.Map; +import ly.count.sdk.java.Config; + +public class ConfigViews { + private final Config config; + + public ConfigViews(Config config) { + this.config = config; + } + + protected Map globalViewSegmentation = null; + + /** + * @param segmentation segmentation values that will be added for all recorded views (manual and automatic) + * @return Returns the same config object for convenient linking + */ + public Config setGlobalViewSegmentation(Map segmentation) { + globalViewSegmentation = segmentation; + return config; + } +} diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index 1cf8ae8aa..ae016282a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -95,7 +95,7 @@ private synchronized void addEventsToRequestQ(String deviceId) { ModuleRequests.pushAsync(internalConfig, request); } - protected void removeInvalidDataFromSegments(Map segments) { + protected static void removeInvalidDataFromSegments(Map segments) { if (segments == null || segments.isEmpty()) { return; diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 440633b37..0595bc401 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -26,6 +26,7 @@ public class ModuleViews extends ModuleBase implements ViewIdProvider { //interface for SDK users Views viewsInterface; IdGenerator idGenerator; + Map globalViewSegmentation = new ConcurrentHashMap<>();//automatic view segmentation static class ViewData { String viewID; @@ -44,6 +45,8 @@ public void init(InternalConfig config) { L.v("[ModuleViews] Initializing"); viewsInterface = new Views(); + setGlobalViewSegmentationInternal(config.views.globalViewSegmentation); + idGenerator = config.viewIdGenerator; config.viewIdProvider = this; } @@ -59,6 +62,10 @@ public void onSessionEnded(Session session, InternalConfig config) { public void stop(InternalConfig config, boolean clear) { viewsInterface = null; viewDataMap.clear(); + if (globalViewSegmentation != null) { + globalViewSegmentation.clear(); + globalViewSegmentation = null; + } } private void removeReservedKeysFromViewSegmentation(Map segmentation) { @@ -74,6 +81,29 @@ private void removeReservedKeysFromViewSegmentation(Map segmenta } } + /** + * Checks the provided Segmentation by the user. Sanitizes it + * and transfers the data into an internal Segmentation Object. + */ + void setGlobalViewSegmentationInternal(@Nullable Map segmentation) { + L.d("[ModuleViews] setGlobalViewSegmentationInternal, with[" + (segmentation == null ? "null" : segmentation.size()) + "] entries"); + + globalViewSegmentation.clear(); + + if (segmentation != null) { + removeReservedKeysFromViewSegmentation(segmentation); + ModuleEvents.removeInvalidDataFromSegments(segmentation); + globalViewSegmentation.putAll(segmentation); + } + } + + public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { + removeReservedKeysFromViewSegmentation(segmentation); + ModuleEvents.removeInvalidDataFromSegments(segmentation); + + globalViewSegmentation.putAll(segmentation); + } + /** * This should be called in case a new session starts so that we could identify the new "first view" */ @@ -96,6 +126,7 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi viewSegmentation.putAll(customViewSegmentation); } viewSegmentation.putAll(vd.viewSegmentation); + viewSegmentation.putAll(globalViewSegmentation); return viewSegmentation; } @@ -505,5 +536,36 @@ public void addSegmentationToViewWithID(@Nonnull String viewId, @Nullable Map - global view segmentation + */ + public void setGlobalViewSegmentation(@Nullable Map segmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling setGlobalViewSegmentation sg[" + (segmentation == null ? null : segmentation.size()) + "]"); + + setGlobalViewSegmentationInternal(segmentation); + } + } + + /** + * Updates the global segmentation for views + * + * @param segmentation Map - global view segmentation + */ + public void updateGlobalViewSegmentation(@Nullable Map segmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling updateGlobalViewSegmentation sg[" + (segmentation == null ? null : segmentation.size()) + "]"); + + if (segmentation == null) { + L.w("[View] When updating segmentation values, they can't be 'null'."); + return; + } + + updateGlobalViewSegmentationInternal(segmentation); + } + } } } From c2d2f4631b57528244da665f8bf1bcd0cfd860a1 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jan 2024 11:18:21 +0300 Subject: [PATCH 33/38] feat: add log --- .../main/java/ly/count/sdk/java/internal/ModuleEvents.java | 5 +++-- .../main/java/ly/count/sdk/java/internal/ModuleViews.java | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index ae016282a..8f61f974d 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import ly.count.sdk.java.Countly; import ly.count.sdk.java.Session; +import ly.count.sdk.java.View; public class ModuleEvents extends ModuleBase { protected EventQueue eventQueue = null; @@ -95,7 +96,7 @@ private synchronized void addEventsToRequestQ(String deviceId) { ModuleRequests.pushAsync(internalConfig, request); } - protected static void removeInvalidDataFromSegments(Map segments) { + protected static void removeInvalidDataFromSegments(Map segments, Log L) { if (segments == null || segments.isEmpty()) { return; @@ -125,7 +126,7 @@ protected void recordEventInternal(String key, int count, Double sum, Double dur L.d("[ModuleEvents] recordEventInternal, Recording event with key: [" + key + "] and provided event ID of:[" + eventIdOverride + "] and segmentation with:[" + (segmentation == null ? "null" : segmentation.size()) + "] keys"); - removeInvalidDataFromSegments(segmentation); + removeInvalidDataFromSegments(segmentation, L); String eventId, pvid = null, cvid = null; if (Utils.isEmptyOrNull(eventIdOverride)) { diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 0595bc401..778638d9a 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -92,14 +92,14 @@ void setGlobalViewSegmentationInternal(@Nullable Map segmentatio if (segmentation != null) { removeReservedKeysFromViewSegmentation(segmentation); - ModuleEvents.removeInvalidDataFromSegments(segmentation); + ModuleEvents.removeInvalidDataFromSegments(segmentation, L); globalViewSegmentation.putAll(segmentation); } } public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { removeReservedKeysFromViewSegmentation(segmentation); - ModuleEvents.removeInvalidDataFromSegments(segmentation); + ModuleEvents.removeInvalidDataFromSegments(segmentation, L); globalViewSegmentation.putAll(segmentation); } From 62ca2151a733bc9c167856de24fe5632de6fc158 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jan 2024 11:46:24 +0300 Subject: [PATCH 34/38] feat: add is empty checl --- .../src/main/java/ly/count/sdk/java/internal/ModuleViews.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 778638d9a..9a8b59b6f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -90,7 +90,7 @@ void setGlobalViewSegmentationInternal(@Nullable Map segmentatio globalViewSegmentation.clear(); - if (segmentation != null) { + if (segmentation != null && !segmentation.isEmpty()) { removeReservedKeysFromViewSegmentation(segmentation); ModuleEvents.removeInvalidDataFromSegments(segmentation, L); globalViewSegmentation.putAll(segmentation); From 44d4295724271287a8b0e47ddad087e114d020f2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 30 Jan 2024 17:18:01 +0300 Subject: [PATCH 35/38] fix: pr things --- .../count/sdk/java/internal/ModuleEvents.java | 21 +--------- .../count/sdk/java/internal/ModuleViews.java | 40 +++++++++++-------- .../ly/count/sdk/java/internal/Utils.java | 24 +++++++++++ 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index 8f61f974d..4cae3bb5e 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -1,9 +1,7 @@ package ly.count.sdk.java.internal; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import ly.count.sdk.java.Countly; import ly.count.sdk.java.Session; import ly.count.sdk.java.View; @@ -96,23 +94,6 @@ private synchronized void addEventsToRequestQ(String deviceId) { ModuleRequests.pushAsync(internalConfig, request); } - protected static void removeInvalidDataFromSegments(Map segments, Log L) { - - if (segments == null || segments.isEmpty()) { - return; - } - - List toRemove = segments.entrySet().stream() - .filter(entry -> !Utils.isValidDataType(entry.getValue())) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - toRemove.forEach(key -> { - L.w("[ModuleEvents] RemoveSegmentInvalidDataTypes: In segmentation Data type '" + segments.get(key) + "' of item '" + key + "' isn't valid."); - segments.remove(key); - }); - } - protected void recordEventInternal(String key, int count, Double sum, Double dur, Map segmentation, String eventIdOverride) { if (count <= 0) { L.w("[ModuleEvents] recordEventInternal, Count can't be less than 1, ignoring this event."); @@ -126,7 +107,7 @@ protected void recordEventInternal(String key, int count, Double sum, Double dur L.d("[ModuleEvents] recordEventInternal, Recording event with key: [" + key + "] and provided event ID of:[" + eventIdOverride + "] and segmentation with:[" + (segmentation == null ? "null" : segmentation.size()) + "] keys"); - removeInvalidDataFromSegments(segmentation, L); + Utils.removeInvalidDataFromSegments(segmentation, L); String eventId, pvid = null, cvid = null; if (Utils.isEmptyOrNull(eventIdOverride)) { diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index 9a8b59b6f..a98a32926 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -22,11 +22,11 @@ public class ModuleViews extends ModuleBase implements ViewIdProvider { static final String KEY_START = "start"; static final String KEY_START_VALUE = "1"; Map viewDataMap = new LinkedHashMap<>(); // map viewIDs to its viewData - String[] reservedSegmentationKeysViews = new String[] { "name", "visit", "start", "segment" }; + String[] reservedSegmentationKeysViews = new String[] { KEY_NAME, KEY_VISIT, KEY_START, KEY_SEGMENT }; //interface for SDK users Views viewsInterface; IdGenerator idGenerator; - Map globalViewSegmentation = new ConcurrentHashMap<>();//automatic view segmentation + Map globalViewSegmentation = new ConcurrentHashMap<>(); static class ViewData { String viewID; @@ -58,6 +58,15 @@ public void onSessionEnded(Session session, InternalConfig config) { resetFirstView(); } + @Override + public void deviceIdChanged(String oldDeviceId, boolean withMerge) { + super.deviceIdChanged(oldDeviceId, withMerge); + L.d("[ModuleViews] deviceIdChanged: oldDeviceId = " + oldDeviceId + ", withMerge = " + withMerge); + if (!withMerge) { + stopAllViewsInternal(null); + } + } + @Override public void stop(InternalConfig config, boolean clear) { viewsInterface = null; @@ -92,14 +101,14 @@ void setGlobalViewSegmentationInternal(@Nullable Map segmentatio if (segmentation != null && !segmentation.isEmpty()) { removeReservedKeysFromViewSegmentation(segmentation); - ModuleEvents.removeInvalidDataFromSegments(segmentation, L); + Utils.removeInvalidDataFromSegments(segmentation, L); globalViewSegmentation.putAll(segmentation); } } public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { removeReservedKeysFromViewSegmentation(segmentation); - ModuleEvents.removeInvalidDataFromSegments(segmentation, L); + Utils.removeInvalidDataFromSegments(segmentation, L); globalViewSegmentation.putAll(segmentation); } @@ -111,7 +120,7 @@ public void resetFirstView() { firstView = true; } - Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { + private Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new ConcurrentHashMap<>(); viewSegmentation.put(KEY_NAME, vd.viewName); @@ -131,9 +140,9 @@ Map createViewEventSegmentation(@Nonnull ViewData vd, boolean fi return viewSegmentation; } - void autoCloseRequiredViews(boolean closeAllViews, Map customViewSegmentation) { + private void autoCloseRequiredViews(boolean closeAllViews, Map customViewSegmentation) { L.d("[ModuleViews] autoCloseRequiredViews"); - List viewsToRemove = new ArrayList<>(1); + List viewsToRemove = new ArrayList<>(); for (Map.Entry entry : viewDataMap.entrySet()) { ViewData vd = entry.getValue(); @@ -222,7 +231,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map segmentation) { + private void recordView(String id, Double duration, Map segmentation) { ModuleEvents events = internalConfig.sdk.module(ModuleEvents.class); if (events == null) { L.e("[ModuleViews] recordView, events module is not initialized"); @@ -232,26 +241,25 @@ void recordView(String id, Double duration, Map segmentation) { events.recordEventInternal(KEY_VIEW_EVENT, 1, 0.0, duration, segmentation, id); } - void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { - long lastElapsedDurationSeconds = 0; + private void recordViewEndEvent(ViewData vd, @Nullable Map filteredCustomViewSegmentation, String viewRecordingSource) { + double lastElapsedDurationSeconds = 0.0; //we do sanity check the time component and print error in case of problem if (vd.viewStartTimeSeconds < 0) { L.e("[ModuleViews] " + viewRecordingSource + ", view start time value is not normal: [" + vd.viewStartTimeSeconds + "], ignoring that duration"); } else if (vd.viewStartTimeSeconds == 0) { L.i("[ModuleViews] " + viewRecordingSource + ", view is either paused or didn't run, ignoring start timestamp"); } else { - lastElapsedDurationSeconds = TimeUtils.uniqueTimestampS() - vd.viewStartTimeSeconds; + lastElapsedDurationSeconds = (double) (TimeUtils.uniqueTimestampS() - vd.viewStartTimeSeconds); } //only record view if the view name is not null if (vd.viewName == null) { - L.e("[ModuleViews] stopViewWithIDInternal, view has no internal name, ignoring it"); + L.e("[ModuleViews] " + viewRecordingSource + " , view has no internal name, ignoring it"); return; } - long viewDurationSeconds = lastElapsedDurationSeconds; Map segments = createViewEventSegmentation(vd, false, false, filteredCustomViewSegmentation); - recordView(vd.viewID, new Long(viewDurationSeconds).doubleValue(), segments); + recordView(vd.viewID, lastElapsedDurationSeconds, segments); } void pauseViewWithIDInternal(String viewID) { @@ -350,7 +358,7 @@ private void addSegmentationToViewWithIDInternal(String viewID, Map viewSegmentation) { synchronized (Countly.instance()) { - L.i("[Views] Calling stopViewWithName vi[" + viewID + "] sg[" + viewSegmentation + "]"); + L.i("[Views] Calling stopViewWithID vi[" + viewID + "] sg[" + viewSegmentation + "]"); stopViewWithIDInternal(viewID, viewSegmentation); } } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java index e8c11b6b4..20e784a6f 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; /** * Utility class @@ -402,4 +403,27 @@ public static String safeRandomVal() { String b64Value = Utils.Base64.encode(value); return b64Value + timestamp; } + + /** + * Removes invalid data types from segments + * + * @param segments to check + * @param L logger + */ + public static void removeInvalidDataFromSegments(Map segments, Log L) { + + if (segments == null || segments.isEmpty()) { + return; + } + + List toRemove = segments.entrySet().stream() + .filter(entry -> !Utils.isValidDataType(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + toRemove.forEach(key -> { + L.w("[Utils] removeInvalidDataFromSegments, In segmentation Data type '" + segments.get(key) + "' of item '" + key + "' isn't valid."); + segments.remove(key); + }); + } } From d2fb6daa02e0ca90d70825dd10f6ff4589d033f4 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 31 Jan 2024 10:12:45 +0300 Subject: [PATCH 36/38] feat: add old way call --- .../ly/count/sdk/java/internal/ModuleViews.java | 15 --------------- .../ly/count/sdk/java/internal/SessionImpl.java | 6 +++++- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index a98a32926..cf43e3737 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -8,7 +8,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import ly.count.sdk.java.Countly; -import ly.count.sdk.java.Session; public class ModuleViews extends ModuleBase implements ViewIdProvider { String currentViewID = null; @@ -51,13 +50,6 @@ public void init(InternalConfig config) { config.viewIdProvider = this; } - @Override - public void onSessionEnded(Session session, InternalConfig config) { - super.onSessionEnded(session, config); - stopAllViewsInternal(null); - resetFirstView(); - } - @Override public void deviceIdChanged(String oldDeviceId, boolean withMerge) { super.deviceIdChanged(oldDeviceId, withMerge); @@ -113,13 +105,6 @@ public void updateGlobalViewSegmentationInternal(@Nonnull Map se globalViewSegmentation.putAll(segmentation); } - /** - * This should be called in case a new session starts so that we could identify the new "first view" - */ - public void resetFirstView() { - firstView = true; - } - private Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new ConcurrentHashMap<>(); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java index a3921db24..a59dd93be 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java @@ -193,7 +193,11 @@ Future end(Long now, final Tasks.Callback callback, String did this.consents = SDKCore.instance.consents; - Storage.pushAsync(config, this); + if (currentView != null) { + currentView.stop(true); + } else { + Storage.pushAsync(config, this); + } Long duration = updateDuration(now); From 171052f12ff0e5a06d7f031b53e6535f5285bb6d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 31 Jan 2024 10:39:28 +0300 Subject: [PATCH 37/38] feat: test changes --- .../ly/count/sdk/java/internal/ModuleViews.java | 16 ++++++++++------ .../ly/count/sdk/java/internal/ViewImpl.java | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java index cf43e3737..dd176c3ee 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleViews.java @@ -107,6 +107,12 @@ public void updateGlobalViewSegmentationInternal(@Nonnull Map se private Map createViewEventSegmentation(@Nonnull ViewData vd, boolean firstView, boolean visit, Map customViewSegmentation) { Map viewSegmentation = new ConcurrentHashMap<>(); + viewSegmentation.putAll(globalViewSegmentation); + viewSegmentation.putAll(vd.viewSegmentation); + + if (customViewSegmentation != null) { + viewSegmentation.putAll(customViewSegmentation); + } viewSegmentation.put(KEY_NAME, vd.viewName); if (visit) { @@ -116,12 +122,6 @@ private Map createViewEventSegmentation(@Nonnull ViewData vd, bo viewSegmentation.put(KEY_START, KEY_START_VALUE); } viewSegmentation.put(KEY_SEGMENT, internalConfig.getSdkPlatform()); - if (customViewSegmentation != null) { - viewSegmentation.putAll(customViewSegmentation); - } - viewSegmentation.putAll(vd.viewSegmentation); - viewSegmentation.putAll(globalViewSegmentation); - return viewSegmentation; } @@ -194,6 +194,10 @@ private void autoCloseRequiredViews(boolean closeAllViews, Map c return currentViewData.viewID; } + protected void setFirstViewInternal(boolean firstView) { + this.firstView = firstView; + } + void stopViewWithNameInternal(@Nullable String viewName, @Nullable Map customViewSegmentation) { String viewID = validateViewWithName(viewName, "stopViewWithNameInternal"); if (viewID == null) { 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 9914ef354..be165d3cd 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 @@ -50,6 +50,10 @@ public void stop(boolean lastView) { } stop = true; Countly.instance().views().stopViewWithName(name); + ModuleViews viewsModule = (ModuleViews) SDKCore.instance.module(CoreFeature.Views.getIndex()); + if (viewsModule != null) { + viewsModule.setFirstViewInternal(lastView); + } } @Override From 7cce4bcc569255fba0bbc1f76153d341e5470d89 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:31:22 +0000 Subject: [PATCH 38/38] [Java] Implement the new module for view tracking - tests (#236) * feat: view impl tests * feat: on session began callback * feat: add things to the stop * feat: all bad values and non existing ones * feat: scenario simple flow * fix: simple flow * fix: map usage * fix: rename * feat: segm testing1 * feat: mixed test flow 1 * feat: auto stopped ones * feat: validate segmentation 1 * feat: validate segmentation 2 * feat: validate internal keys * fix: int val check * fix: remove unused var * fix: delete unncessary things * chore: remove whitespcae * chore: whitespace * feat: module views test * feat: integrate views to flow * fix: undo * fix: add missing log * fix: regex pattern * fix: add changelog and deprecate * fix: remove unused import * fix: test thing * fix: device id tests * feat: test id generator * fix: view impl tests * fix: all view ids * feat: additional tests * fix: idx * feat: missing test logic * feat: new way of testing scneario * doc: update comment of scenario id * Update ScenarioManuelViewTests.java * feat: test prefixes * refactor: checkings * Update InternalConfig.java * Update InternalConfig.java * chore: naming sc * refactor: fix PR changes * feat: basic test about global segm * refactor: order * feat: view case proposal * feat: add missing test cases * feat: views module changes * fix: undo * fix: conflict * fix: conflict --- .../sdk/java/internal/EventQueueTests.java | 6 +- .../java/internal/ModuleDeviceIdTests.java | 12 +- .../sdk/java/internal/ModuleEventsTests.java | 30 +- .../java/internal/ModuleFeedbackTests.java | 4 +- .../sdk/java/internal/ModuleViewsTests.java | 858 ++++++++++++++++++ .../sdk/java/internal/SessionImplTests.java | 27 +- .../ly/count/sdk/java/internal/TestUtils.java | 62 +- .../sdk/java/internal/TimedEventsTests.java | 2 +- .../sdk/java/internal/ViewImplTests.java | 215 +++++ .../java/internal/sc_MV_ManualViewTests.java | 535 +++++++++++ ...oUtilsTests.java => sc_UA_UtilsTests.java} | 18 +- 11 files changed, 1715 insertions(+), 54 deletions(-) create mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleViewsTests.java create mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/ViewImplTests.java create mode 100644 sdk-java/src/test/java/ly/count/sdk/java/internal/sc_MV_ManualViewTests.java rename sdk-java/src/test/java/ly/count/sdk/java/internal/{ScenarioUtilsTests.java => sc_UA_UtilsTests.java} (61%) diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java index c6a925677..dd33ba117 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java @@ -177,12 +177,12 @@ public void restoreFromDisk() throws IOException { init(TestUtils.getConfigEvents(2)); TestUtils.validateEQSize(0, eventQueue); - writeToEventQueue("{\"hour\":10,\"count\":1,\"dow\":4,\"key\":\"test-joinEvents-1\",\"timestamp\":1695887006647}:::{\"hour\":10,\"count\":1,\"dow\":4,\"key\":\"test-joinEvents-2\",\"timestamp\":1695887006657}", false); + writeToEventQueue("{\"id\":\"id\",\"cvid\":\"cvid\",\"pvid\":\"pvid\",\"peid\":\"peid\",\"hour\":10,\"count\":1,\"dow\":4,\"key\":\"test-joinEvents-1\",\"timestamp\":1695887006647}:::{\"hour\":10,\"count\":1,\"dow\":4,\"key\":\"test-joinEvents-2\",\"timestamp\":1695887006657}", false); eventQueue.restoreFromDisk(); TestUtils.validateEQSize(2, eventQueue); - validateEvent(eventQueue.eventQueueMemoryCache.get(0), "test-joinEvents-1", null, 1, null, null); - validateEvent(eventQueue.eventQueueMemoryCache.get(1), "test-joinEvents-2", null, 1, null, null); + validateEvent(eventQueue.eventQueueMemoryCache.get(0), "test-joinEvents-1", null, 1, null, null, "id", "pvid", "cvid", "peid"); + validateEvent(eventQueue.eventQueueMemoryCache.get(1), "test-joinEvents-2", null, 1, null, null, null, null, null, null); } /** diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleDeviceIdTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleDeviceIdTests.java index e8313eaeb..a0882287d 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleDeviceIdTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleDeviceIdTests.java @@ -458,10 +458,10 @@ private void validateDeviceIdWithoutMergeChange(final int rqSize, String oldDevi viewSegmentation.put("name", TestUtils.keysValues[1]); viewSegmentation.put("start", "1"); viewSegmentation.put("visit", "1"); - TestUtils.validateEvent(existingEvents.get(1), "[CLY]_view", viewSegmentation, 1, null, null); // view start event + TestUtils.validateEvent(existingEvents.get(1), "[CLY]_view", viewSegmentation, 1, 0.0, null, "_CLY_", "", null, null); // view start event viewSegmentation.remove("start"); viewSegmentation.remove("visit"); - TestUtils.validateEvent(existingEvents.get(3), "[CLY]_view", viewSegmentation, 1, null, 1.0); // view stop event + TestUtils.validateEvent(existingEvents.get(3), "[CLY]_view", viewSegmentation, 1, 0.0, 1.0, "_CLY_", "", null, null); // view stop event remainingRequestIndex++; } } catch (NullPointerException ignored) { @@ -591,8 +591,12 @@ private void setupEvent() { private List validateEvents(int requestIndex, String deviceId, int timedEventIdx) { List existingEvents = TestUtils.readEventsFromRequest(requestIndex, deviceId); if (!existingEvents.isEmpty()) { - TestUtils.validateEvent(existingEvents.get(0), TestUtils.keysValues[2], null, 1, null, null); // casual event - TestUtils.validateEvent(existingEvents.get(timedEventIdx), TestUtils.keysValues[0], null, 1, null, 1.0); // timed event + String expectedCVId = ""; + if (existingEvents.size() > 2) { + expectedCVId = existingEvents.get(3).id; + } + TestUtils.validateEvent(existingEvents.get(0), TestUtils.keysValues[2], null, 1, null, null, "_CLY_", null, "", null); // casual event + TestUtils.validateEvent(existingEvents.get(timedEventIdx), TestUtils.keysValues[0], null, 1, null, 1.0, "_CLY_", null, expectedCVId, existingEvents.get(0).id); // timed event } return existingEvents; diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java index 688ee6c7a..17e556a87 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java @@ -51,7 +51,7 @@ public void recordEvent() { Countly.instance().events().recordEvent(eKeys[0], segmentation, 1, 45.9, 32.0); //check if event was recorded correctly and size of event queue is equal to size of events in queue - TestUtils.validateEventInEQ(eKeys[0], segmentation, 1, 45.9, 32.0, 0, 1); + TestUtils.validateEventInEQ(eKeys[0], segmentation, 1, 45.9, 32.0, 0, 1, "_CLY_", null, "", null); } /** @@ -75,8 +75,11 @@ public void recordEvent_queueSizeOver() { Assert.assertEquals(1, TestUtils.getCurrentRQ().length); List eventsInRequest = TestUtils.readEventsFromRequest(); - validateEvent(eventsInRequest.get(0), eKeys[0], null, 1, 45.9, 32.0); - validateEvent(eventsInRequest.get(1), eKeys[1], null, 1, 45.9, 32.0); + + // check first event has no peid and has an SDK generated id by giving "_CLY_" flag to the function + validateEvent(eventsInRequest.get(0), eKeys[0], null, 1, 45.9, 32.0, "_CLY_", null, "", null); + // check second event has peid of first event and has an SDK generated id by giving "_CLY_" flag to the function + validateEvent(eventsInRequest.get(1), eKeys[1], null, 1, 45.9, 32.0, "_CLY_", null, "", eventsInRequest.get(0).id); } /** @@ -98,7 +101,7 @@ public void recordEvent_queueSizeOverMemory() throws IOException { List eventsInRequest = TestUtils.readEventsFromRequest(); validateEvent(eventsInRequest.get(0), "test-joinEvents-1", null, 5, null, null); validateEvent(eventsInRequest.get(1), "test-joinEvents-2", null, 1, null, null); - validateEvent(eventsInRequest.get(2), eKeys[0], null, 1, 45.9, 32.0); + validateEvent(eventsInRequest.get(2), eKeys[0], null, 1, 45.9, 32.0, "_CLY_", null, "", null); } /** @@ -168,7 +171,7 @@ public void recordEvent_invalidSegment() { //record event with key segmentation Countly.instance().events().recordEvent(eKeys[0], segmentation); - TestUtils.validateEventInEQ(eKeys[0], expectedSegmentation, 1, null, null, 0, 1); + TestUtils.validateEventInEQ(eKeys[0], expectedSegmentation, 1, null, null, 0, 1, "_CLY_", null, "", null); } /** @@ -191,7 +194,7 @@ public void startEvent() { endEvent(eKeys[0], null, 1, null); Assert.assertEquals(0, moduleEvents.timedEvents.size()); - TestUtils.validateEventInEQ(eKeys[0], null, 1, null, 0.0, 0, 1); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, 0.0, 0, 1, "_CLY_", null, "", null); } /** @@ -248,7 +251,7 @@ public void startEvent_alreadyStarted() { endEvent(eKeys[0], null, 1, null); Assert.assertEquals(0, moduleEvents.timedEvents.size()); - TestUtils.validateEventInEQ(eKeys[0], null, 1, null, 0.0, 0, 1); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, 0.0, 0, 1, "_CLY_", null, "", null); } /** @@ -313,12 +316,13 @@ public void endEvent_withSegmentation() { Map segmentation = new ConcurrentHashMap<>(); segmentation.put("hair_color", "red"); segmentation.put("hair_length", "short"); - segmentation.put("chauffeur", "g3chauffeur"); // + segmentation.put("chauffeur", "g3chauffeur"); endEvent(eKeys[0], segmentation, 1, 5.0); Assert.assertEquals(0, moduleEvents.timedEvents.size()); - TestUtils.validateEventInEQ(eKeys[0], segmentation, 1, 5.0, 0.0, 0, 1); + // check event has desired segmentation, no peid and a sdk generated id by giving "_CLY_" flag to the function + TestUtils.validateEventInEQ(eKeys[0], segmentation, 1, 5.0, 0.0, 0, 1, "_CLY_", null, "", null); } /** @@ -414,7 +418,9 @@ public void cancelEvent() { @Test public void timedEventFlow() throws InterruptedException { - init(TestUtils.getConfigEvents(4)); + InternalConfig config = new InternalConfig(TestUtils.getConfigEvents(4)); + config.eventIdGenerator = TestUtils.idGenerator(); + init(config); validateTimedEventSize(0, 0); startEvent(eKeys[0]); // start event to end it @@ -428,12 +434,12 @@ public void timedEventFlow() throws InterruptedException { endEvent(eKeys[1], null, 3, 15.0); Assert.assertEquals(1, moduleEvents.timedEvents.size()); - TestUtils.validateEventInEQ(eKeys[1], null, 3, 15.0, 1.0, 0, 1); + TestUtils.validateEventInEQ(eKeys[1], null, 3, 15.0, 1.0, 0, 1, TestUtils.keysValues[0], null, "", null); endEvent(eKeys[0], null, 2, 4.0); Assert.assertEquals(0, moduleEvents.timedEvents.size()); - TestUtils.validateEventInEQ(eKeys[0], null, 2, 4.0, 2.0, 1, 2); + TestUtils.validateEventInEQ(eKeys[0], null, 2, 4.0, 2.0, 1, 2, TestUtils.keysValues[1], null, "", TestUtils.keysValues[0]); } private void validateTimedEventSize(int expectedQueueSize, int expectedTimedEventSize) { diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleFeedbackTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleFeedbackTests.java index 21110ac29..815dbbdd3 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleFeedbackTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleFeedbackTests.java @@ -591,10 +591,10 @@ private Map requiredWidgetSegmentation(String widgetId, Map feedbackWidgetResult, int eqIndex) { - validateEvent(TestUtils.getCurrentEQ().get(eqIndex), eventKey, requiredWidgetSegmentation(widgetID, feedbackWidgetResult), 1, null, null); + validateEvent(TestUtils.getCurrentEQ().get(eqIndex), eventKey, requiredWidgetSegmentation(widgetID, feedbackWidgetResult), 1, null, null, "_CLY_", null, "", null); } void feedbackValidateManualResultRQ(String eventKey, String widgetID, Map feedbackWidgetResult, int eqIndex) { - validateEvent(TestUtils.readEventsFromRequest().get(eqIndex), eventKey, requiredWidgetSegmentation(widgetID, feedbackWidgetResult), 1, null, null); + validateEvent(TestUtils.readEventsFromRequest().get(eqIndex), eventKey, requiredWidgetSegmentation(widgetID, feedbackWidgetResult), 1, null, null, "_CLY_", null, "", null); } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleViewsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleViewsTests.java new file mode 100644 index 000000000..3575f17ef --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleViewsTests.java @@ -0,0 +1,858 @@ +package ly.count.sdk.java.internal; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ModuleViewsTests { + + @Before + public void beforeTest() { + TestUtils.createCleanTestState(); + } + + @After + public void afterTest() { + Countly.instance().halt(); + } + + // Non-existing views + + /** + * "stopViewWithName" with non-existing view + * Validating that "stopViewWithName" with non-existing view doesn't generate any event + * No event should be generated + */ + @Test + public void stopViewWithName_nonExisting() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + TestUtils.validateEQSize(0); + Countly.instance().views().stopViewWithName(TestUtils.keysValues[0]); + TestUtils.validateEQSize(0); + } + + /** + * "stopViewWithID" with non-existing view + * Validating that "stopViewWithID" with non-existing view doesn't generate any event + * No event should be generated + */ + @Test + public void stopViewWithID_nonExisting() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + TestUtils.validateEQSize(0); + Countly.instance().views().stopViewWithID(TestUtils.keysValues[0]); + TestUtils.validateEQSize(0); + } + + /** + * "pauseViewWithID" with non-existing view + * Validating that "pauseViewWithID" with non-existing view doesn't generate any event + * No event should be generated + */ + @Test + public void pauseViewWithID_nonExisting() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + TestUtils.validateEQSize(0); + Countly.instance().views().pauseViewWithID(TestUtils.keysValues[0]); + TestUtils.validateEQSize(0); + } + + /** + * "resumeViewWithID" with non-existing view + * Validating that "resumeViewWithID" with non-existing view doesn't generate any event + * No event should be generated + */ + @Test + public void resumeViewWithID_nonExisting() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + TestUtils.validateEQSize(0); + Countly.instance().views().resumeViewWithID(TestUtils.keysValues[0]); + TestUtils.validateEQSize(0); + } + + /** + * "addSegmentationToView" with non-existing view + * Validating that "addSegmentationToView" with non-existing view doesn't generate any event + * No event should be generated + */ + @Test + public void addSegmentationToView_nonExisting() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + TestUtils.validateEQSize(0); + Countly.instance().views().addSegmentationToViewWithName(TestUtils.keysValues[0], TestUtils.map("a", 1, "b", 2)); + TestUtils.validateEQSize(0); + Countly.instance().views().addSegmentationToViewWithID(TestUtils.keysValues[0], TestUtils.map("a", 1, "b", 2)); + TestUtils.validateEQSize(0); + } + + // Providing bad values + + /** + * "startView" with null and empty names + * Both "startView" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void startView_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::startView, Countly.instance().views()::startView); + } + + /** + * "startAutoStoppedView" with null and empty names + * Both "startAutoStoppedView" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void startAutoStoppedView_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::startAutoStoppedView, Countly.instance().views()::startAutoStoppedView); + } + + /** + * "stopViewWithName" with null and empty names + * Both "stopViewWithName" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void stopViewWithName_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::stopViewWithName, Countly.instance().views()::stopViewWithName); + } + + /** + * "stopViewWithID" with null and empty names + * Both "stopViewWithName" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void stopViewWithID_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::stopViewWithID, Countly.instance().views()::stopViewWithID); + } + + /** + * "pauseViewWithID" with null and empty names + * Both "pauseViewWithID" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void pauseViewWithID_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::pauseViewWithID, null); + } + + /** + * "resumeViewWithID" with null and empty names + * Both "resumeViewWithID" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void resumeViewWithID_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(Countly.instance().views()::resumeViewWithID, null); + } + + /** + * "addSegmentationToViewWithID" with null and empty names + * Both "addSegmentationToViewWithID" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void addSegmentationToViewWithID_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(null, Countly.instance().views()::addSegmentationToViewWithID); + } + + /** + * "addSegmentationToViewWithName" with null and empty names + * Both "addSegmentationToViewWithName" calls should be ignored and no event should be generated + * No event should be existed in the EQ + */ + @Test + public void addSegmentationToViewWithName_nullAndEmpty() { + Countly.instance().init(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + badValueTrier(null, Countly.instance().views()::addSegmentationToViewWithName); + } + + private void badValueTrier(Consumer idNameViewFunction, BiConsumer> idNameSegmentViewFunction) { + TestUtils.validateEQSize(0); + if (idNameViewFunction != null) { + idNameViewFunction.accept(null); + TestUtils.validateEQSize(0); + idNameViewFunction.accept(""); + TestUtils.validateEQSize(0); + } + if (idNameSegmentViewFunction != null) { + idNameSegmentViewFunction.accept(null, null); + TestUtils.validateEQSize(0); + idNameSegmentViewFunction.accept("", null); + TestUtils.validateEQSize(0); + } + } + + // A simple flow + + /** + *
+     * Make sure all the basic functions are working correctly and we are keeping time correctly
+     *
+     * 1- start view A
+     * 2- start view B
+     * 3- wait a moment
+     * 4- pause view A
+     * 5- wait a moment
+     * 6- resume view A
+     * 7- stop view with stopViewWithName/stopViewWithID/stopAllViews
+     * 8- Stop view B if needed
+     * 9- make sure the summary time is correct and two events are recorded
+     * 
+ */ + @Test + public void simpleFlow() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map customSegmentationA = TestUtils.map("count", 56, "start", "1", "visit", "1", "name", TestUtils.keysValues[0], "segment", TestUtils.keysValues[1]); + String viewA = Countly.instance().views().startView("A", customSegmentationA); + TestUtils.validateEQSize(1); + Map customSegmentationB = TestUtils.map("gone", true, "lev", 78.91, "map", TestUtils.map("a", 1, "b", 2)); + Countly.instance().views().startView("B", customSegmentationB); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID(viewA); + Thread.sleep(1000); + Countly.instance().views().resumeViewWithID(viewA); + Countly.instance().views().stopAllViews(null); + TestUtils.validateEQSize(5); + + validateView("A", 0.0, 0, 5, true, true, TestUtils.map("count", 56), "idv1", ""); + validateView("B", 0.0, 1, 5, false, true, TestUtils.map("gone", true, "lev", BigDecimal.valueOf(78.91)), "idv2", "idv1"); + validateView("A", 1.0, 2, 5, false, false, null, "idv1", "idv1"); + validateView("A", 0.0, 3, 5, false, false, null, "idv1", "idv1"); + validateView("B", 2.0, 4, 5, false, false, null, "idv2", "idv1"); + } + + /** + *
+     * Validate the interaction of "startView" and "startAutoStoppedView". "startAutoStoppedView" should be automatically stopped when calling "startView", but not the other way around
+     *
+     * startView A
+     * startAutoStoppedView
+     * startView B
+     * make sure that at this point there are 4 events, 3 starting and 1 closing.
+     *
+     * stopViewWithName A
+     * stopViewWithID B
+     * 
+ */ + @Test + public void mixedTestFlow1() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map customSegmentationA = TestUtils.map("money", 238746798234739L, "start", "1", "visit", "1", "name", TestUtils.keysValues[0], "segment", TestUtils.keysValues[1]); + Map customSegmentationB = TestUtils.map("gone_to", "Wall Sina", "map", TestUtils.map("titan", true, "level", 65)); + + Countly.instance().views().startView("A", customSegmentationA); + Countly.instance().views().startAutoStoppedView("AutoStopped", customSegmentationB); + Thread.sleep(1000); + String viewB = Countly.instance().views().startView("B"); + + TestUtils.validateEQSize(4); + + validateView("A", 0.0, 0, 4, true, true, TestUtils.map("money", 238746798234739L), "idv1", ""); // starting + validateView("AutoStopped", 0.0, 1, 4, false, true, TestUtils.map("gone_to", "Wall Sina"), "idv2", "idv1"); // starting + validateView("AutoStopped", 1.0, 2, 4, false, false, null, "idv2", "idv1"); // closing + validateView("B", 0.0, 3, 4, false, true, null, "idv3", "idv2"); // starting + + Countly.instance().views().stopViewWithName("A"); + Countly.instance().views().stopViewWithID(viewB); + TestUtils.validateEQSize(6); + + validateView("A", 1.0, 4, 6, false, false, null, "idv1", "idv2"); // closing + validateView("B", 0.0, 5, 6, false, false, null, "idv3", "idv2"); // closing + } + + /** + *
+     * Make sure we can use view actions with the auto stopped ones
+     *
+     * startAutoStoppedView A
+     * wait a moment
+     * pause view
+     * wait a moment
+     * resume view
+     * stop view with stopViewWithName/stopViewWithID
+     * 
+ */ + @Test + public void useWithAutoStoppedOnes() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map customSegmentationA = TestUtils.map("power_percent", 56.7f, "start", "1", "visit", "1", "name", TestUtils.keysValues[0], "segment", TestUtils.keysValues[1]); + + String viewA = Countly.instance().views().startAutoStoppedView("A", customSegmentationA); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID(viewA); + Thread.sleep(1000); + Countly.instance().views().resumeViewWithID(viewA); + Countly.instance().views().stopViewWithName("A", null); + + TestUtils.validateEQSize(3); + + validateView("A", 0.0, 0, 3, true, true, TestUtils.map("power_percent", BigDecimal.valueOf(56.7)), "idv1", ""); // starting + validateView("A", 1.0, 1, 3, false, false, null, "idv1", ""); // starting + validateView("A", 0.0, 2, 3, false, false, null, "idv1", ""); // closing + } + + /** + *
+     * Validate segmentation
+     * Just make sure the values are used
+     *
+     * startView A with segmentation
+     * make sure the correct things are added
+     *
+     * startView B with segmentation
+     * make sure the correct things are added
+     *
+     * Stop A with no segmentation
+     *
+     * Stop B with segmentation
+     *
+     * again make sure that segmentation is correctly applied
+     * 
+ */ + @Test + public void validateSegmentation1() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map customSegmentationA = TestUtils.map("FigmaId", "YXNkOThhZnM=", "start", "1", "visit", "1", "name", TestUtils.keysValues[0], "segment", TestUtils.keysValues[1]); + Map customSegmentationB = TestUtils.map("FigmaId", "OWE4cZdkOWFz", "start", "1", "end", "1", "name", TestUtils.keysValues[2], "segment", TestUtils.keysValues[3]); + + String viewA = Countly.instance().views().startView("A", customSegmentationA); + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("FigmaId", "YXNkOThhZnM="), "idv1", ""); // starting + + Countly.instance().views().startView("B", customSegmentationB); + validateView("B", 0.0, 1, 2, false, true, TestUtils.map("FigmaId", "OWE4cZdkOWFz", "end", "1"), "idv2", "idv1"); // starting + + Countly.instance().views().stopViewWithID(viewA, null); + validateView("A", 0.0, 2, 3, false, false, null, "idv1", "idv1"); // closing + + Countly.instance().views().stopViewWithName("B", TestUtils.map("ClickCount", 45)); + validateView("B", 0.0, 3, 4, false, false, TestUtils.map("ClickCount", 45), "idv2", "idv1"); // closing + } + + /** + *
+     * Validate segmentation 2
+     *
+     * - startView A
+     * - startView B
+     * - stopAllViews with segmentation
+     *
+     * make sure that the stop segmentation was added to all views
+     * 
+ */ + @Test + public void validateSegmentation2() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A"); + Countly.instance().views().startView("B"); + validateView("A", 0.0, 0, 2, true, true, null, "idv1", ""); + validateView("B", 0.0, 1, 2, false, true, null, "idv2", "idv1"); + + Map allSegmentation = TestUtils.map("Copyright", "Countly", "AppExit", true, "DestroyToken", false, "ExitedAt", 1702975890000L); + Countly.instance().views().stopAllViews(allSegmentation); + + validateView("A", 0.0, 2, 4, false, false, allSegmentation, "idv1", "idv1"); + validateView("B", 0.0, 3, 4, false, false, allSegmentation, "idv2", "idv1"); + } + + /** + *

Validate segmentation does not override internal keys

+ *
+     * Internal keys: "name", "start", "visit", "segment"
+     *
+     * - Start view and provide segmentation with internal keys
+     * - Stop view and provide segmentation with internal keys
+     * make sure that internal keys are not overridden at any point
+     * 
+ */ + @Test + public void validateSegmentation_internalKeys() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map internalKeysSegmentation = TestUtils.map("start", "YES", "name", TestUtils.keysValues[0], "visit", "YES", "segment", TestUtils.keysValues[1]); + + Countly.instance().views().startView("A", TestUtils.map(internalKeysSegmentation, "ultimate", "YES")); + Countly.instance().views().stopViewWithName("A", TestUtils.map(internalKeysSegmentation, "end", "Unfortunately", "time", 1234567890L)); + validateView("A", 0.0, 0, 2, true, true, TestUtils.map("ultimate", "YES"), "idv1", ""); + validateView("A", 0.0, 1, 2, false, false, TestUtils.map("end", "Unfortunately", "time", 1234567890), "idv1", ""); + } + + /** + *
+     * Try add segmentation to view functions with internal keys
+     *
+     * - start view A with segmentation - validate that event is created
+     * - Add segmentation to view with name A - with internal keys + valid param
+     * - pause view A - validate that segmentation is not empty and only valid segmentation is added and internal keys are not overridden
+     * - Add segmentation to view A with ID - with internal keys + valid param
+     * - stop view with ID A - validate that segmentation is not empty and only valid segmentation is added and internal keys are not overridden
+     *
+     * 
+ */ + @Test + public void addSegmentationToView_internalKeys() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map internalKeysSegmentation = TestUtils.map("start", "YES", "name", TestUtils.keysValues[0], "visit", "YES", "segment", TestUtils.keysValues[1]); + + String viewIDA = Countly.instance().views().startView("A"); + validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + Countly.instance().views().addSegmentationToViewWithName("A", TestUtils.map(internalKeysSegmentation, "aniki", "HAVE")); + Countly.instance().views().pauseViewWithID(viewIDA); + validateView("A", 0.0, 1, 2, false, false, TestUtils.map("aniki", "HAVE"), "idv1", ""); + + Countly.instance().views().addSegmentationToViewWithID(viewIDA, TestUtils.map(internalKeysSegmentation, "oni-chan", "HAVE")); + Countly.instance().views().stopViewWithID(viewIDA); + validateView("A", 0.0, 2, 3, false, false, TestUtils.map("aniki", "HAVE", "oni-chan", "HAVE"), "idv1", ""); + } + + /** + *
+     * Try add segmentation to view functions with null and empty values
+     *
+     * - start view A with segmentation + some internal keys - validate event is created and only valid segmentation is added
+     * - Add segmentation to view with name A - with a param
+     * - Add segmentation to view with name A - null
+     * - pause view A - validate that segmentation is not empty and first call added the segmentation
+     * - Add segmentation to view with ID A - with a param
+     * - Add segmentation to view with ID A - empty
+     * - stop view A - validate that segmentation is not empty and added segmentations are exists and not overridden by null and empty values
+     *
+     * 
+ */ + @Test + public void addSegmentationToView_nullEmpty() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Map viewSegmentation = TestUtils.map("name", "A", "segment", TestUtils.getOS(), "arr", new ArrayList<>(), "done", true); + + String viewIDA = Countly.instance().views().startView("A", viewSegmentation); + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("done", true), "idv1", ""); + Countly.instance().views().addSegmentationToViewWithName("A", TestUtils.map("a", 1)); + Countly.instance().views().addSegmentationToViewWithName("A", null); + Countly.instance().views().pauseViewWithID(viewIDA); + validateView("A", 0.0, 1, 2, false, false, TestUtils.map("a", 1), "idv1", ""); + + Countly.instance().views().addSegmentationToViewWithID(viewIDA, TestUtils.map("b", 2)); + Countly.instance().views().addSegmentationToViewWithID(viewIDA, TestUtils.map()); + Countly.instance().views().stopViewWithID(viewIDA); + validateView("A", 0.0, 2, 3, false, false, TestUtils.map("a", 1, "b", 2), "idv1", ""); + } + + /** + *
+     * Add segmentation to view with init given global segmentation
+     *
+     * - start view A with none segmentation
+     * - Add segmentation to view A
+     * - pause view A - validate that segmentation is added
+     * - stop view A - validate that segmentation is also added to stop view event
+     *
+     * 
+ */ + @Test + public void addSegmentationToView() { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("glob", "al"))); + TestUtils.validateEQSize(0); + String viewIDA = Countly.instance().views().startView("A"); + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("glob", "al"), "idv1", ""); + Countly.instance().views().addSegmentationToViewWithName("A", TestUtils.map("a", 1, "b", 2)); + Countly.instance().views().pauseViewWithID(viewIDA); + validateView("A", 0.0, 1, 2, false, false, TestUtils.map("a", 1, "b", 2, "glob", "al"), "idv1", ""); + Countly.instance().views().stopViewWithID(viewIDA); + validateView("A", 0.0, 2, 3, false, false, TestUtils.map("a", 1, "b", 2, "glob", "al"), "idv1", ""); + } + + /** + *
+     * Resume already running view
+     *
+     * - start view A
+     * - wait a moment
+     * - pause view A
+     * - wait a moment
+     * - pause view A again
+     * - wait a moment
+     * - stop view A
+     *
+     * Total time should be 1 seconds because it was paused already
+     *
+     * 
+ * + * @throws InterruptedException to wait + */ + @Test + public void pauseViewWithId_pausePaused() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + String viewIDA = Countly.instance().views().startView("A"); + + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID(viewIDA); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID(viewIDA); + Thread.sleep(1000); + + Countly.instance().views().stopViewWithID(viewIDA); + validateView("A", 0.0, 0, 3, true, true, null, "idv1", ""); + validateView("A", 1.0, 1, 3, false, false, null, "idv1", ""); + validateView("A", 0.0, 2, 3, false, false, null, "idv1", ""); + } + + /** + *
+     * Resume already running view
+     *
+     * - start view A
+     * - wait a moment
+     * - resume view A
+     * - wait a moment
+     * - stop view A
+     *
+     * Total time should be 2 seconds because it was not paused
+     *
+     * 
+ * + * @throws InterruptedException to wait + */ + @Test + public void resumeViewWithId_resumeRunning() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + String viewIDA = Countly.instance().views().startView("A"); + + Thread.sleep(1000); + Countly.instance().views().resumeViewWithID(viewIDA); + Thread.sleep(1000); + + Countly.instance().views().stopViewWithID(viewIDA); + validateView("A", 0.0, 0, 2, true, true, null, "idv1", ""); + validateView("A", 2.0, 1, 2, false, false, null, "idv1", ""); + } + + /** + *
+     * A mixed flow of sessions and views
+     *
+     * - start session
+     * - start view A - firstView true- event is created
+     * - wait a moment
+     * - end session - this call ends existing views so it stops A
+     * - start view B - firstView true - event is created
+     * - start session
+     * - start view C - firstView false - event is created
+     *
+     * There should be 5 events
+     * 
+ * + * @throws InterruptedException for wait + */ + @Test + public void mixedFlow_sessions() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().enableFeatures(Config.Feature.Sessions)); + TestUtils.validateEQSize(0); + Countly.session().begin(); + + Countly.instance().view("A"); + + Thread.sleep(1000); + Countly.session().end(); // A will auto stop + + Countly.instance().view("B"); + Countly.session().begin(); + + Countly.instance().views().startView("C"); + + validateView("A", 0.0, 0, 5, true, true, null, "idv1", ""); + validateView("A", 1.0, 1, 5, false, false, null, "idv1", ""); + validateView("B", 0.0, 2, 5, true, true, null, "idv2", "idv1"); + validateView("B", 0.0, 3, 5, false, false, null, "idv2", "idv1"); + validateView("C", 0.0, 4, 5, false, true, null, "idv3", "idv2"); + } + + /** + * "setGlobalSegmentation" flow + * - set global segmentation to different one that has all accepted data types and an invalid data type in init + * - start view A + * - sleep for 1 sec + * - pause view A + * - start view B that has new segmentation + * - sleep for 1 sec + * - stop all views with segmentation + * ------ + * Validate that all events are created and segmentation is correct, and init given segmentation should exist in all events + */ + @Test + public void setGlobalSegmentation_initGiven() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "arr", new ArrayList<>(), "double", Double.MAX_VALUE, "long", Long.MAX_VALUE))); + Map clearedSegmentation = TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "double", BigDecimal.valueOf(Double.MAX_VALUE), "long", Long.MAX_VALUE); + TestUtils.validateEQSize(0); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID("idv1"); + Countly.instance().views().startView("B", TestUtils.map("c", 3, "d", 4)); + Thread.sleep(1000); + Countly.instance().views().stopAllViews(TestUtils.map("e", 5, "f", 6)); + + validateView("A", 0.0, 0, 5, true, true, TestUtils.map(clearedSegmentation, "a", 1, "b", 2), "idv1", ""); + validateView("A", 1.0, 1, 5, false, false, clearedSegmentation, "idv1", ""); + validateView("B", 0.0, 2, 5, false, true, TestUtils.map(clearedSegmentation, "c", 3, "d", 4), "idv2", "idv1"); + validateView("A", 0.0, 3, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv1", "idv1"); + validateView("B", 1.0, 4, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv2", "idv1"); + } + + /** + * "setGlobalSegmentation" flow + * - init countly with global segmentation + * - start view A that overrides one of the global segmentation + * - set global segmentation to different one that has all accepted data types and an invalid data type + * - sleep for 1 sec + * - pause view A + * - start view B that has new segmentation + * - sleep for 1 sec + * - stop all views with segmentation + * ------ + * Validate that all events are created and segmentation is correct, and things should be overridden correctly + */ + @Test + public void setGlobalSegmentation() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("ab", 5, "a", 5))); + Map clearedSegmentation = TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "double", BigDecimal.valueOf(Double.MAX_VALUE), "long", Long.MAX_VALUE); + TestUtils.validateEQSize(0); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + Countly.instance().views().setGlobalViewSegmentation(TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "arr", new ArrayList<>(), "double", Double.MAX_VALUE, "long", Long.MAX_VALUE)); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID("idv1"); + Countly.instance().views().startView("B", TestUtils.map("c", 3, "d", 4)); + Thread.sleep(1000); + Countly.instance().views().stopAllViews(TestUtils.map("e", 5, "f", 6)); + + validateView("A", 0.0, 0, 5, true, true, TestUtils.map("ab", 5, "a", 1, "b", 2), "idv1", ""); + validateView("A", 1.0, 1, 5, false, false, clearedSegmentation, "idv1", ""); + validateView("B", 0.0, 2, 5, false, true, TestUtils.map(clearedSegmentation, "c", 3, "d", 4), "idv2", "idv1"); + validateView("A", 0.0, 3, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv1", "idv1"); + validateView("B", 1.0, 4, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv2", "idv1"); + } + + /** + * "updateGlobalViewSegmentation" flow + * - init countly with global segmentation + * - start view A that overrides one of the global segmentation + * - set global segmentation to different one that has all accepted data types and an invalid data type + * - sleep for 1 sec + * - pause view A + * - update global segmentation with new values and override one of the old values + * - start view B that has new segmentation + * - sleep for 1 sec + * - stop all views with segmentation + * ------ + * Validate that all events are created and segmentation is correct, and things should be overridden correctly + */ + @Test + public void updateGlobalSegmentation() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("ab", 5, "a", 5))); + Map clearedSegmentation = TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "double", BigDecimal.valueOf(Double.MAX_VALUE), "long", Long.MAX_VALUE); + TestUtils.validateEQSize(0); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + Countly.instance().views().setGlobalViewSegmentation(TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "arr", new ArrayList<>(), "double", Double.MAX_VALUE, "long", Long.MAX_VALUE)); + Thread.sleep(1000); + Countly.instance().views().pauseViewWithID("idv1"); + Countly.instance().views().updateGlobalViewSegmentation(TestUtils.map("int", Integer.MIN_VALUE, "all", "glob")); + Countly.instance().views().startView("B", TestUtils.map("c", 3, "d", 4)); + Thread.sleep(1000); + Countly.instance().views().stopAllViews(TestUtils.map("e", 5, "f", 6)); + + validateView("A", 0.0, 0, 5, true, true, TestUtils.map("ab", 5, "a", 1, "b", 2), "idv1", ""); + validateView("A", 1.0, 1, 5, false, false, clearedSegmentation, "idv1", ""); + clearedSegmentation.put("int", Integer.MIN_VALUE); + clearedSegmentation.put("all", "glob"); + validateView("B", 0.0, 2, 5, false, true, TestUtils.map(clearedSegmentation, "c", 3, "d", 4), "idv2", "idv1"); + validateView("A", 0.0, 3, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv1", "idv1"); + validateView("B", 1.0, 4, 5, false, false, TestUtils.map(clearedSegmentation, "e", 5, "f", 6), "idv2", "idv1"); + } + + /** + * "setGlobalViewSegmentation" init empty null + * should not override global segmentation with empty map given + * Global segmentation should stay as it is + */ + @Test + public void setGlobalSegmentation_initGiven_empty() { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map())); + TestUtils.validateEQSize(0); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("a", 1, "b", 2), "idv1", ""); + } + + /** + * "setGlobalViewSegmentation" init given null + * should not override global segmentation with null map given + * Global segmentation should stay as it is + */ + @Test + public void setGlobalSegmentation_initGiven_null() { + Countly.instance().init(TestUtils.getConfigViews(null)); + TestUtils.validateEQSize(0); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("a", 1, "b", 2), "idv1", ""); + } + + /** + * "setGlobalViewSegmentation" + * should not override global segmentation with empty map given + * Global segmentation should stay as it is + */ + @Test + public void setGlobalSegmentation_empty() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Countly.instance().views().setGlobalViewSegmentation(TestUtils.map()); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("a", 1, "b", 2), "idv1", ""); + } + + /** + * "setGlobalViewSegmentation" + * should not override global segmentation with null map given + * Global segmentation should stay as it is + */ + @Test + public void setGlobalSegmentation_null() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + + Countly.instance().views().setGlobalViewSegmentation(null); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("a", 1, "b", 2), "idv1", ""); + } + + /** + * "updateGlobalSegmentation" + * should not override global segmentation with empty map given + * Global segmentation should stay as it is + */ + @Test + public void updateGlobalSegmentation_empty() { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("glob", "all"))); + TestUtils.validateEQSize(0); + + Countly.instance().views().updateGlobalViewSegmentation(TestUtils.map()); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("glob", "all", "a", 1, "b", 2), "idv1", ""); + } + + /** + * "updateGlobalSegmentation" + * should not override global segmentation with null given + * Global segmentation should stay as it is + */ + @Test + public void updateGlobalSegmentation_null() { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("glob", "all"))); + TestUtils.validateEQSize(0); + + Countly.instance().views().updateGlobalViewSegmentation(null); + Countly.instance().views().startView("A", TestUtils.map("a", 1, "b", 2)); + + validateView("A", 0.0, 0, 1, true, true, TestUtils.map("glob", "all", "a", 1, "b", 2), "idv1", ""); + } + + /** + * We make sure that "setGlobalSegmentation" and "updateGlobalSegmentation" updates/sets global segmentation + * ------ + * initialize countly with init given global segmentation that includes all accepted data types and couple of incorrect ones (like arr=list()) + * start view A with segmentation that overrides one of the global segmentations + * validate start view event is recorded for A with correct segmentation + * setGlobalViewSegmentation with all accepted data types and couple of not accepted data types + * sleep for 1 second + * pause view A + * validate pause view event, and setGlobalViewSegmentation values are exists + * call updateGlobalViewSegmentation that overrides some of the globalSegmentation and also add couple of incorrect data types + * start view B with segment that overrides couple of not overridden global segmentation values + * validate start view event for B is recorded and global segmentation values are overwridden and incorrect data types removed + * sleep for 1 second + * stopAllViews with segmentation that has incorrect data types, 1 global segm override and new segm values + * validate 2 stop view event is recorded in order of A,B and correct segm values are existed + * ------ + * + * @throws InterruptedException for wait + */ + @Test + public void updateGlobalSegmentation_flow() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews(TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "double", BigDecimal.valueOf(Double.MAX_VALUE), "long", Long.MAX_VALUE, "arr", new ArrayList<>(), "map", TestUtils.map()))); + + Map clearedSegmentation = TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "double", BigDecimal.valueOf(Double.MAX_VALUE), "long", Long.MAX_VALUE); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A", TestUtils.map("glob", "no")); + validateView("A", 0.0, 0, 1, true, true, TestUtils.map(clearedSegmentation, "glob", "no"), "idv1", ""); + + Countly.instance().views().setGlobalViewSegmentation(TestUtils.map("glob", "al", "int", Integer.MAX_VALUE, "float", BigDecimal.valueOf(Float.MAX_VALUE), "bool", true, "arr", new ArrayList<>(), "double", Double.MAX_VALUE, "long", Long.MAX_VALUE)); + Thread.sleep(1000); + + Countly.instance().views().pauseViewWithID("idv1"); + validateView("A", 1.0, 1, 2, false, false, clearedSegmentation, "idv1", ""); + + Countly.instance().views().updateGlobalViewSegmentation(TestUtils.map("int", Integer.MIN_VALUE, "arr", new ArrayList<>(), "all", "glob")); + + Countly.instance().views().startView("B", TestUtils.map("float", BigDecimal.valueOf(Float.MIN_VALUE), "in", "case")); + validateView("B", 0.0, 2, 3, false, true, TestUtils.map(clearedSegmentation, "all", "glob", "int", Integer.MIN_VALUE, "float", BigDecimal.valueOf(Float.MIN_VALUE), "in", "case"), "idv2", "idv1"); + + Thread.sleep(1000); + Countly.instance().views().stopAllViews(TestUtils.map("bool", false)); + validateView("A", 0.0, 3, 5, false, false, TestUtils.map(clearedSegmentation, "all", "glob", "int", Integer.MIN_VALUE, "bool", false), "idv1", "idv1"); + validateView("B", 1.0, 4, 5, false, false, TestUtils.map(clearedSegmentation, "all", "glob", "int", Integer.MIN_VALUE, "bool", false), "idv2", "idv1"); + } + + static void validateView(String viewName, Double viewDuration, int idx, int size, boolean start, boolean visit, Map customSegmentation, String id, String pvid) { + Map viewSegmentation = TestUtils.map("name", viewName, "segment", TestUtils.getOS()); + if (start) { + viewSegmentation.put("start", "1"); + } + if (visit) { + viewSegmentation.put("visit", "1"); + } + if (customSegmentation != null) { + viewSegmentation.putAll(customSegmentation); + } + + TestUtils.validateEventInEQ(ModuleViews.KEY_VIEW_EVENT, viewSegmentation, 1, 0.0, viewDuration, idx, size, id, pvid, null, null); + } +} 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 ba5eb8413..2f0655eff 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 @@ -1,6 +1,5 @@ package ly.count.sdk.java.internal; -import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; @@ -605,7 +604,8 @@ public void view() { Countly.instance().init(TestUtils.getConfigSessions(Config.Feature.Views, Config.Feature.Events).setEventQueueSizeToSend(4)); SessionImpl session = (SessionImpl) Countly.session(); TestUtils.validateEQSize(0); - validateViewInEQ((ViewImpl) session.view("view"), 0, 1); + session.view("view"); + ModuleViewsTests.validateView("view", 0.0, 0, 1, true, true, null, "_CLY_", ""); } /** @@ -615,25 +615,18 @@ public void view() { */ @Test public void view_stopStartedAndNext() { - Countly.instance().init(TestUtils.getConfigSessions(Config.Feature.Views, Config.Feature.Events).setEventQueueSizeToSend(4)); + InternalConfig config = new InternalConfig(TestUtils.getConfigSessions(Config.Feature.Views, Config.Feature.Events).setEventQueueSizeToSend(4)); + config.viewIdGenerator = TestUtils.idGenerator(); + Countly.instance().init(config); + SessionImpl session = (SessionImpl) Countly.session(); TestUtils.validateEQSize(0); session.view("start"); TestUtils.validateEQSize(1); - validateViewInEQ((ViewImpl) session.view("next"), 2, 3); - } - - private void validateViewInEQ(ViewImpl view, int eqIdx, int eqSize) { - List eventList = TestUtils.getCurrentEQ(); - assertEquals(eqSize, eventList.size()); - EventImpl event = eventList.get(eqIdx); - assertEquals(event.sum, view.start.sum); - assertEquals(event.count, view.start.count); - assertEquals(event.key, view.start.key); - assertEquals(event.segmentation, view.start.segmentation); - assertEquals(event.hour, view.start.hour); - assertEquals(event.dow, view.start.dow); - assertEquals(event.duration, view.start.duration); + session.view("next"); + ModuleViewsTests.validateView("start", 0.0, 0, 3, true, true, null, TestUtils.keysValues[0], ""); + ModuleViewsTests.validateView("start", 0.0, 1, 3, false, false, null, TestUtils.keysValues[0], ""); + ModuleViewsTests.validateView("next", 0.0, 2, 3, false, true, null, TestUtils.keysValues[1], TestUtils.keysValues[0]); } private void validateNotEquals(int idOffset, BiFunction> setter) { diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java index 8697a7c39..94f148e76 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/TestUtils.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Scanner; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -308,28 +309,56 @@ public static Map parseQueryParams(String data) { return paramMap; } - static void validateEvent(EventImpl gonnaValidate, String key, Map segmentation, int count, Double sum, Double duration) { + static void validateEvent(EventImpl gonnaValidate, String key, Map segmentation, int count, Double sum, Double duration, String id, String pvid, String cvid, String peid) { Assert.assertEquals(key, gonnaValidate.key); - Assert.assertEquals(segmentation, gonnaValidate.segmentation); + + if (segmentation != null) { + Assert.assertEquals("Event segmentation size are not equal", segmentation.size(), gonnaValidate.segmentation.size()); + for (Map.Entry entry : segmentation.entrySet()) { + Assert.assertEquals(entry.getValue(), gonnaValidate.segmentation.get(entry.getKey())); + } + } + Assert.assertEquals(count, gonnaValidate.count); Assert.assertEquals(sum, gonnaValidate.sum); if (duration != null) { double delta = 0.5; - Assert.assertTrue(Math.abs(duration - gonnaValidate.duration) < delta); + Assert.assertTrue(duration + " expected duration, got " + gonnaValidate.duration, Math.abs(duration - gonnaValidate.duration) < delta); } Assert.assertTrue(gonnaValidate.dow >= 0 && gonnaValidate.dow < 7); Assert.assertTrue(gonnaValidate.hour >= 0 && gonnaValidate.hour < 24); Assert.assertTrue(gonnaValidate.timestamp >= 0); + validateId(id, gonnaValidate.id, "Event ID"); + validateId(pvid, gonnaValidate.pvid, "Previous View ID"); + validateId(cvid, gonnaValidate.cvid, "Current View ID"); + validateId(peid, gonnaValidate.peid, "Previous Event ID"); } - static void validateEventInEQ(String key, Map segmentation, int count, Double sum, Double duration, int index, int size) { + // if id null + private static void validateId(String id, String gonnaValidate, String name) { + if (id != null && id.equals("_CLY_")) { + validateSafeRandomVal(gonnaValidate); + } else { + Assert.assertEquals(name + " is not validated", id, gonnaValidate); + } + } + + static void validateEvent(EventImpl gonnaValidate, String key, Map segmentation, int count, Double sum, Double duration) { + validateEvent(gonnaValidate, key, segmentation, count, sum, duration, null, null, null, null); + } + + static void validateEventInEQ(String key, Map segmentation, int count, Double sum, Double duration, int index, int size, String id, String pvid, String cvid, String peid) { List events = getCurrentEQ(); - validateEvent(events.get(index), key, segmentation, count, sum, duration); + validateEvent(events.get(index), key, segmentation, count, sum, duration, id, pvid, cvid, peid); validateEQSize(size); } + static void validateEventInEQ(String key, Map segmentation, int count, Double sum, Double duration, int index, int size) { + validateEventInEQ(key, segmentation, count, sum, duration, index, size, null, null, null, null); + } + static List readEventsFromRequest() { return readEventsFromRequest(0, TestUtils.DEVICE_ID); } @@ -615,4 +644,27 @@ static void validateSafeRandomVal(String val) { Assert.fail("No match for " + val); } } + + static IdGenerator idGenerator() { + AtomicInteger counter = new AtomicInteger(0); + return () -> TestUtils.keysValues[counter.getAndIncrement() % TestUtils.keysValues.length]; + } + + static IdGenerator incrementalViewIdGenerator() { + AtomicInteger counter = new AtomicInteger(0); + return () -> "idv" + counter.incrementAndGet(); + } + + static InternalConfig getConfigViews() { + InternalConfig config = new InternalConfig(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + config.viewIdGenerator = TestUtils.incrementalViewIdGenerator(); + return config; + } + + static InternalConfig getConfigViews(Map segmentation) { + InternalConfig config = new InternalConfig(TestUtils.getBaseConfig().enableFeatures(Config.Feature.Views, Config.Feature.Events)); + config.viewIdGenerator = TestUtils.incrementalViewIdGenerator(); + config.views.setGlobalViewSegmentation(segmentation); + return config; + } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/TimedEventsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/TimedEventsTests.java index 12df8e064..2b0a9c285 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/TimedEventsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/TimedEventsTests.java @@ -70,6 +70,6 @@ public void recordEventRegularFlow_base(boolean regularRecord) throws Interrupte tEvent.endAndRecord(); } - TestUtils.validateEventInEQ("key", targetSegm, 5, 133.0, targetDuration, 0, 1); + TestUtils.validateEventInEQ("key", targetSegm, 5, 133.0, targetDuration, 0, 1, "_CLY_", null, "", null); } } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ViewImplTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ViewImplTests.java new file mode 100644 index 000000000..4dff4cbaf --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ViewImplTests.java @@ -0,0 +1,215 @@ +package ly.count.sdk.java.internal; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import ly.count.sdk.java.View; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class ViewImplTests { + + @Before + public void beforeTest() { + TestUtils.createCleanTestState(); + } + + @After + public void afterTest() { + Countly.instance().halt(); + } + + /** + * "constructor" with null session + * Validating default values + * Values should be set to default + */ + @Test + public void constructor_defaults() { + ViewImpl view = new ViewImpl(null, TestUtils.keysValues[0], Mockito.mock(Log.class)); + Assert.assertNull(view.session); + Assert.assertEquals(TestUtils.keysValues[0], view.name); + Assert.assertEquals("start", ModuleViews.KEY_START); + Assert.assertEquals("1", ModuleViews.KEY_START_VALUE); + Assert.assertEquals("visit", ModuleViews.KEY_VISIT); + Assert.assertEquals("1", ModuleViews.KEY_VISIT_VALUE); + Assert.assertEquals("segment", ModuleViews.KEY_SEGMENT); + Assert.assertEquals("[CLY]_view", ModuleViews.KEY_VIEW_EVENT); + Assert.assertEquals("name", ModuleViews.KEY_NAME); + Assert.assertFalse(view.toString().isEmpty()); + } + + /** + * "start" with defaults + * Validating default values and that view is recorded + * Values should be set to default and view should be recorded + */ + @Test + public void start() { + InternalConfig config = new InternalConfig(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events)); + config.viewIdGenerator = TestUtils.idGenerator(); + Countly.instance().init(config); + + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(1); + Map segmentations = new ConcurrentHashMap<>(); + segmentations.put("name", TestUtils.keysValues[0]); + segmentations.put("visit", "1"); + segmentations.put("segment", TestUtils.getOS()); + segmentations.put("start", "1"); + TestUtils.validateEventInEQ("[CLY]_view", segmentations, 1, 0.0, null, 0, 1, TestUtils.keysValues[0], "", null, null); + } + + /** + * "start" with backendModeEnabled + * Validating that view is not recorded + * A view should not be recorded + */ + @Test + public void start_backendModeEnabled() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events).enableBackendMode()); + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(0); + } + + /** + * "start" with null and empty name + * Validating that views are not recorded + * Views should not be recorded + */ + @Test + public void start_nullAndEmptyName() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events).enableBackendMode()); + TestUtils.validateEQSize(0); + Countly.instance().view(null); // this calls start automatically + TestUtils.validateEQSize(0); + Countly.instance().view(""); // this calls start automatically + TestUtils.validateEQSize(0); + } + + /** + * "start" with no consent to Events + * Validating that view is not recorded + * A view should not be recorded + */ + @Test + public void start_noEventsConsent() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views)); + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(0); + } + + /** + * "stop" with defaults + * Validating that start view and stop view are recorded + * Start and stop views should be recorded + * + * @throws InterruptedException for the duration of the view + */ + @Test + public void stop() throws InterruptedException { + InternalConfig config = new InternalConfig(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events)); + config.viewIdGenerator = TestUtils.idGenerator(); + Countly.instance().init(config); + + TestUtils.validateEQSize(0); + View view = Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(1); + Map segmentations = new ConcurrentHashMap<>(); + segmentations.put("name", TestUtils.keysValues[0]); + segmentations.put("visit", "1"); + segmentations.put("segment", TestUtils.getOS()); + segmentations.put("start", "1"); + TestUtils.validateEventInEQ("[CLY]_view", segmentations, 1, 0.0, null, 0, 1, TestUtils.keysValues[0], "", null, null); + + segmentations.remove("start"); + segmentations.remove("visit"); + Thread.sleep(1000); + view.stop(false); + TestUtils.validateEventInEQ("[CLY]_view", segmentations, 1, 0.0, 1.0, 1, 2, TestUtils.keysValues[0], "", null, null); + } + + /** + * "stop" with defaults via calling stop on Countly + * Validating that start view and stop view are recorded but there should be 4 events recorded + * Start and stop views should be recorded and expected numbers of events should be in the queue + * + * @throws InterruptedException for the duration of the view + */ + @Test + public void stop_sdkCall() throws InterruptedException { + InternalConfig config = new InternalConfig(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events)); + config.viewIdGenerator = TestUtils.idGenerator(); + Countly.instance().init(config); + + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(1); + Map segmentations = new ConcurrentHashMap<>(); + segmentations.put("name", TestUtils.keysValues[0]); + segmentations.put("visit", "1"); + segmentations.put("segment", TestUtils.getOS()); + segmentations.put("start", "1"); + TestUtils.validateEventInEQ("[CLY]_view", segmentations, 1, 0.0, null, 0, 1, TestUtils.keysValues[0], "", null, null); + + segmentations.remove("start"); + segmentations.remove("visit"); + Thread.sleep(1000); + Countly.instance().view(TestUtils.keysValues[0]).stop(false); // this call stop previous view and creates new one and stops it + TestUtils.validateEventInEQ("[CLY]_view", segmentations, 1, 0.0, null, 3, 4, TestUtils.keysValues[1], TestUtils.keysValues[0], null, null); + } + + /** + * "stop" with backend mode enabled + * Validating that nothing should be recorded + * Event queue should be empty + */ + @Test + public void stop_backendModeEnabled() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events).enableBackendMode()); + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]).stop(false); // this call stop previous view and creates new one and stops it + TestUtils.validateEQSize(0); + } + + /** + * "stop" with no consent to events + * Validating that nothing should be recorded + * Event queue should be empty + */ + @Test + public void stop_noConsentForEvents() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views)); + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]); // this calls start automatically + TestUtils.validateEQSize(0); + Countly.instance().view(TestUtils.keysValues[0]).stop(false); // this call stop previous view and creates new one and stops it + TestUtils.validateEQSize(0); + } + + /** + * "stop" a not started view + * Validating that stop call does not generate any events + * Event queue should be empty + */ + @Test + public void stop_notStartedView() { + Countly.instance().init(TestUtils.getBaseConfig().setFeatures(Config.Feature.Views, Config.Feature.Events)); + ViewImpl view = new ViewImpl(Countly.session(), TestUtils.keysValues[0], Mockito.mock(Log.class)); + TestUtils.validateEQSize(0); + view.stop(false); + TestUtils.validateEQSize(0); + } +} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/sc_MV_ManualViewTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/sc_MV_ManualViewTests.java new file mode 100644 index 000000000..b94a9eadd --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/sc_MV_ManualViewTests.java @@ -0,0 +1,535 @@ +package ly.count.sdk.java.internal; + +import ly.count.sdk.java.Countly; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for manual view tracking + * Notes: + * - legacy call recordView is view() call in the Java SDK + */ +@RunWith(JUnit4.class) +public class sc_MV_ManualViewTests { + @Before + public void beforeTest() { + TestUtils.createCleanTestState(); + } + + @After + public void afterTest() { + Countly.instance().halt(); + } + + //(1XX) Value sanitation, wrong usage, simple tests + + /** + * recordView(x2), startAutoStoppedView(x2), startView(x2), pauseViewWithID, resumeViewWithID, stopViewWithName(x2), stopViewWithID(x2), + * addSegmentationToViewWithID, addSegmentationToViewWithName, setGlobalViewSegmentation, updateGlobalViewSegmentation + * ---- + * called with "null" values. versions with and without segmentation. nothing should crash, no events should be recorded + * ---- + * Note: legacy call is called with "true" version of it additionally + */ + @Test + public void MV_100_badValues_null() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + + // recordView(x2) + true version + Countly.instance().view(null); + Countly.instance().view(null, false); + Countly.instance().view(null, true); + + // startAutoStoppedView(x2) + Countly.instance().views().startAutoStoppedView(null); + Countly.instance().views().startAutoStoppedView(null, TestUtils.map()); + + // startView(x2) + Countly.instance().views().startView(null); + Countly.instance().views().startView(null, TestUtils.map()); + + // pauseViewWithID, resumeViewWithID + Countly.instance().views().pauseViewWithID(null); + Countly.instance().views().resumeViewWithID(null); + + // stopViewWithName(x2), stopViewWithID(x2) + Countly.instance().views().stopViewWithName(null); + Countly.instance().views().stopViewWithName(null, TestUtils.map()); + Countly.instance().views().stopViewWithID(null); + Countly.instance().views().stopViewWithID(null, TestUtils.map()); + + // addSegmentationToViewWithID, addSegmentationToViewWithName + Countly.instance().views().addSegmentationToViewWithID(null, TestUtils.map()); + Countly.instance().views().addSegmentationToViewWithName(null, TestUtils.map()); + + // setGlobalViewSegmentation, updateGlobalViewSegmentation + Countly.instance().views().setGlobalViewSegmentation(null); + Countly.instance().views().updateGlobalViewSegmentation(null); + + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + } + + /** + * recordView(x2), startAutoStoppedView(x2), startView(x2), pauseViewWithID, resumeViewWithID, stopViewWithName(x2), + * stopViewWithID(x2), addSegmentationToViewWithID, addSegmentationToViewWithName + * ---- + * called with empty string values + * versions with and without segmentation + * nothing should crash, no events should be recorded + * ---- + * Note: legacy call is called with "true" version of it additionally + */ + @Test + public void MV_101_badValues_emptyString() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + + // recordView(x2) + true version + Countly.instance().view(""); + Countly.instance().view("", false); + Countly.instance().view("", true); + + // startAutoStoppedView(x2) + Countly.instance().views().startAutoStoppedView(""); + Countly.instance().views().startAutoStoppedView("", TestUtils.map()); + + // startView(x2) + Countly.instance().views().startView(""); + Countly.instance().views().startView("", TestUtils.map()); + + // pauseViewWithID, resumeViewWithID + Countly.instance().views().pauseViewWithID(""); + Countly.instance().views().resumeViewWithID(""); + + // stopViewWithName(x2), stopViewWithID(x2) + Countly.instance().views().stopViewWithName(""); + Countly.instance().views().stopViewWithName("", TestUtils.map()); + Countly.instance().views().stopViewWithID(""); + Countly.instance().views().stopViewWithID("", TestUtils.map()); + + // addSegmentationToViewWithID, addSegmentationToViewWithName + Countly.instance().views().addSegmentationToViewWithID("", TestUtils.map()); + Countly.instance().views().addSegmentationToViewWithName("", TestUtils.map()); + + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + } + + /** + * pauseViewWithID, resumeViewWithID, stopViewWithName(x2), + * stopViewWithID(x2), addSegmentationToViewWithID, addSegmentationToViewWithName + * ---- + * called with empty string values + * versions with and without segmentation + * nothing should crash, no events should be recorded + */ + @Test + public void MV_102_badValues_nonExistingViews() { + Countly.instance().init(TestUtils.getConfigViews()); + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + + Countly.instance().views().pauseViewWithID("idv1"); + Countly.instance().views().resumeViewWithID(TestUtils.keysValues[1]); + Countly.instance().views().stopViewWithName(TestUtils.keysValues[2]); + Countly.instance().views().stopViewWithName(TestUtils.keysValues[3], TestUtils.map()); + Countly.instance().views().stopViewWithID(TestUtils.keysValues[4]); + Countly.instance().views().stopViewWithID(TestUtils.keysValues[5], TestUtils.map()); + Countly.instance().views().addSegmentationToViewWithID("idv1", TestUtils.map()); + Countly.instance().views().addSegmentationToViewWithName(TestUtils.keysValues[1], TestUtils.map()); + + TestUtils.validateEQSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRQ().length); + } + + //(2XX) Usage flows + + /** + * Make sure auto closing views behave correctly + * Steps: + * ---------- + * recordView view A (sE_A id=idv1 pvid="" segm={visit="1" start="1"}) + * wait 1 sec + * recordView view B (eE_A d=1 id=idv1 pvid="", segm={}) (sE_B id=idv2 pvid=idv1 segm={visit="1"}) + * wait 1 sec + * start view C (eE_B d=1 id=idv2 pvid=idv1, segm={}) (sE_C id=idv3 pvid=idv2 segm={visit="1"}) + * wait 1 sec + * startAutoStoppedView D (sE_D id=idv4 pvid=idv3 segm={visit="1"}) + * wait 1 sec + * startAutoStoppedView E (eE_D d=1 id=idv4 pvid=idv3, segm={}) (sE_E id=idv5 pvid=idv4 segm={visit="1"}) + * wait 1 sec + * start view F (eE_E d=1 id=idv5 pvid=idv4, segm={}) (sE_F id=idv6 pvid=idv5 segm={visit="1"}) + * wait 1 sec + * recordView view G (sE_G id=idv7 pvid=idv6 segm={visit="1"}) + * wait 1 sec + * startAutoStoppedView H (sE_H id=idv8 pvid=idv7 segm={visit="1"}) + * wait 1 sec + * recordView view I (eE_H d=1 id=idv8 pvid=idv7, segm={}) (sE_I id=idv8 pvid=idv8 segm={visit="1"}) + */ + @Test + public void MV_200A_autostartView_autoClose_legacy() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().view("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().view("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 1.0, 1, 3, false, false, null, "idv1", ""); + ModuleViewsTests.validateView("B", 0.0, 2, 3, false, true, null, "idv2", "idv1"); + + Countly.instance().views().startView("C", TestUtils.map("a", 1)); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 1.0, 3, 5, false, false, null, "idv2", "idv1"); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, TestUtils.map("a", 1), "idv3", "idv2"); + + Countly.instance().views().startAutoStoppedView("D"); + Thread.sleep(1000); + ModuleViewsTests.validateView("D", 0.0, 5, 6, false, true, null, "idv4", "idv3"); + + Countly.instance().views().startAutoStoppedView("E"); + Thread.sleep(1000); + ModuleViewsTests.validateView("D", 1.0, 6, 8, false, false, null, "idv4", "idv3"); + ModuleViewsTests.validateView("E", 0.0, 7, 8, false, true, null, "idv5", "idv4"); + + Countly.instance().views().startView("F"); + Thread.sleep(1000); + ModuleViewsTests.validateView("E", 1.0, 8, 10, false, false, null, "idv5", "idv4"); + ModuleViewsTests.validateView("F", 0.0, 9, 10, false, true, null, "idv6", "idv5"); + + Countly.instance().view("G"); + Thread.sleep(1000); + ModuleViewsTests.validateView("G", 0.0, 10, 11, false, true, null, "idv7", "idv6"); + + Countly.instance().views().startAutoStoppedView("H"); + Thread.sleep(1000); + ModuleViewsTests.validateView("G", 1.0, 11, 13, false, false, null, "idv7", "idv6"); + ModuleViewsTests.validateView("H", 0.0, 12, 13, false, true, null, "idv8", "idv7"); + + Countly.instance().view("I"); + ModuleViewsTests.validateView("H", 1.0, 13, 15, false, false, null, "idv8", "idv7"); + ModuleViewsTests.validateView("I", 0.0, 14, 15, false, true, null, "idv9", "idv8"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("C", 6.0, 15, 18, false, false, null, "idv3", "idv8"); + ModuleViewsTests.validateView("F", 3.0, 16, 18, false, false, null, "idv6", "idv8"); + ModuleViewsTests.validateView("I", 0.0, 17, 18, false, false, null, "idv9", "idv8"); + } + + /** + * without the deprecated "recordViewCall" After every action, the EQ should be validated so make sure that the correct event is recorded + * ---------- + * startAutoStoppedView view A (sE_A id=idv1 pvid="" segm={visit="1" start="1"}) + * wait 1 sec + * startAutoStoppedView view B (eE_A d=1 id=idv1 pvid="", segm={}) (sE_B id=idv2 pvid=idv1 segm={visit="1"}) + * wait 1 sec + * start view C (eE_B d=1 id=idv2 pvid=idv1, segm={}) (sE_C id=idv3 pvid=idv2 segm={visit="1"}) + * stopAllViews (eE_X d=0 id=idv3 pvid=idv2, segm={}) + */ + @Test + public void MV_200B_autoStoppedView_autoClose() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startAutoStoppedView("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().startAutoStoppedView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 1.0, 1, 3, false, false, null, "idv1", ""); + ModuleViewsTests.validateView("B", 0.0, 2, 3, false, true, null, "idv2", "idv1"); + + Countly.instance().views().startView("C", TestUtils.map("a", 1)); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 1.0, 3, 5, false, false, null, "idv2", "idv1"); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, TestUtils.map("a", 1), "idv3", "idv2"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("C", 1.0, 5, 6, false, false, null, "idv3", "idv2"); + } + + /** + * Steps: + * ---------- + * start view A + * startAutoStoppedView B + * wait 1 sec + * pause view B + * wait 1 sec + * resume view B + * wait 1 sec + * RecordView C + * wait 1 sec + * pause view C + * wait 1 sec + * resume view C + * stopAllViews + * should record 8 events + */ + @Test + public void MV_201A_autoStopped_pausedResumed_Legacy() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A"); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().startAutoStoppedView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 0.0, 1, 2, false, true, null, "idv2", "idv1"); + + Countly.instance().views().pauseViewWithID("idv2"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 1.0, 2, 3, false, false, null, "idv2", "idv1"); + + Countly.instance().views().resumeViewWithID("idv2"); + Thread.sleep(1000); + + Countly.instance().view("C"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 1.0, 3, 5, false, false, null, "idv2", "idv1"); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, null, "idv3", "idv2"); + + Countly.instance().views().pauseViewWithID("idv3"); + Thread.sleep(1000); + ModuleViewsTests.validateView("C", 1.0, 5, 6, false, false, null, "idv3", "idv2"); + + Countly.instance().views().resumeViewWithID("idv3"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("A", 5.0, 6, 8, false, false, null, "idv1", "idv2"); + ModuleViewsTests.validateView("C", 0.0, 7, 8, false, false, null, "idv3", "idv2"); + } + + /** + * Steps: + * ---------- + * start view A + * start startAutoStoppedView B + * wait 1 sec + * pause view B + * wait 1 sec + * resume view B + * stopAllViews + * should record 5 events + */ + @Test + public void MV_201B_autoStopped_pausedResumed() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A"); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().startAutoStoppedView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 0.0, 1, 2, false, true, null, "idv2", "idv1"); + + Countly.instance().views().pauseViewWithID("idv2"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 1.0, 2, 3, false, false, null, "idv2", "idv1"); + + Countly.instance().views().resumeViewWithID("idv2"); + Thread.sleep(1000); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("A", 3.0, 3, 5, false, false, null, "idv1", "idv1"); + ModuleViewsTests.validateView("B", 1.0, 4, 5, false, false, null, "idv2", "idv1"); + } + + /** + * Steps: + * ---------- + * startAutoStoppedView A + * wait 1 sec + * stop by name + * startAutoStoppedView B + * wait 1 sec + * stop by ID + * startAutoStoppedView C + * wait 1 sec + * stopAllViews + * record view D + * wait 1 sec + * stop by name + * record view E + * wait 1 sec + * stop by ID + * record view F + * wait 1 sec + * stopAllViews + * should record 12 events + */ + @Test + public void MV_202A_autoStopped_stopped_legacy() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startAutoStoppedView("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().stopViewWithName("A"); + ModuleViewsTests.validateView("A", 1.0, 1, 2, false, false, null, "idv1", ""); + + Countly.instance().views().startAutoStoppedView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 0.0, 2, 3, false, true, null, "idv2", "idv1"); + + Countly.instance().views().stopViewWithID("idv2"); + ModuleViewsTests.validateView("B", 1.0, 3, 4, false, false, null, "idv2", "idv1"); + + Countly.instance().views().startAutoStoppedView("C"); + Thread.sleep(1000); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, null, "idv3", "idv2"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("C", 1.0, 5, 6, false, false, null, "idv3", "idv2"); + + Countly.instance().view("D"); + Thread.sleep(1000); + ModuleViewsTests.validateView("D", 0.0, 6, 7, false, true, null, "idv4", "idv3"); + + Countly.instance().views().stopViewWithName("D"); + ModuleViewsTests.validateView("D", 1.0, 7, 8, false, false, null, "idv4", "idv3"); + + Countly.instance().view("E"); + Thread.sleep(1000); + ModuleViewsTests.validateView("E", 0.0, 8, 9, false, true, null, "idv5", "idv4"); + + Countly.instance().views().stopViewWithID("idv5"); + ModuleViewsTests.validateView("E", 1.0, 9, 10, false, false, null, "idv5", "idv4"); + + Countly.instance().view("F"); + Thread.sleep(1000); + ModuleViewsTests.validateView("F", 0.0, 10, 11, false, true, null, "idv6", "idv5"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("F", 1.0, 11, 12, false, false, null, "idv6", "idv5"); + } + + /** + * Steps: + * ---------- + * startAutoStoppedView A + * wait 1 sec + * stop by name + * startAutoStoppedView B + * wait 1 sec + * stop by ID + * startAutoStoppedView C + * wait 1 sec + * stopAllViews + * should record 6 events + */ + @Test + public void MV_202B_autoStopped_stopped() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startAutoStoppedView("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().stopViewWithName("A"); + ModuleViewsTests.validateView("A", 1.0, 1, 2, false, false, null, "idv1", ""); + + Countly.instance().views().startAutoStoppedView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 0.0, 2, 3, false, true, null, "idv2", "idv1"); + + Countly.instance().views().stopViewWithID("idv2"); + ModuleViewsTests.validateView("B", 1.0, 3, 4, false, false, null, "idv2", "idv1"); + + Countly.instance().views().startAutoStoppedView("C"); + Thread.sleep(1000); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, null, "idv3", "idv2"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("C", 1.0, 5, 6, false, false, null, "idv3", "idv2"); + } + + /** + * Steps: + * ---------- + * start view A + * wait 1 sec + * pause view A + * wait 1 sec + * resume view A + * wait 1 sec + * stopAllViews + * 3 events + */ + @Test + public void MV_203_startView_PausedResumed() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().pauseViewWithID("idv1"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 1.0, 1, 2, false, false, null, "idv1", ""); + + Countly.instance().views().resumeViewWithID("idv1"); + Thread.sleep(1000); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("A", 1.0, 2, 3, false, false, null, "idv1", ""); + } + + /** + * Steps: + * ---------- + * start view A + * wait 1 sec + * stop by name + * start view B + * wait 1 sec + * stop by ID + * start view c + * wait 1 sec + * stopAllViews + * should record 6 events + */ + @Test + public void MV_203_startView_stopped() throws InterruptedException { + Countly.instance().init(TestUtils.getConfigViews().setEventQueueSizeToSend(20)); + TestUtils.validateEQSize(0); + + Countly.instance().views().startView("A"); + Thread.sleep(1000); + ModuleViewsTests.validateView("A", 0.0, 0, 1, true, true, null, "idv1", ""); + + Countly.instance().views().stopViewWithName("A"); + ModuleViewsTests.validateView("A", 1.0, 1, 2, false, false, null, "idv1", ""); + + Countly.instance().views().startView("B"); + Thread.sleep(1000); + ModuleViewsTests.validateView("B", 0.0, 2, 3, false, true, null, "idv2", "idv1"); + + Countly.instance().views().stopViewWithID("idv2"); + ModuleViewsTests.validateView("B", 1.0, 3, 4, false, false, null, "idv2", "idv1"); + + Countly.instance().views().startView("C"); + Thread.sleep(1000); + ModuleViewsTests.validateView("C", 0.0, 4, 5, false, true, null, "idv3", "idv2"); + + Countly.instance().views().stopAllViews(null); + ModuleViewsTests.validateView("C", 1.0, 5, 6, false, false, null, "idv3", "idv2"); + } +} diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioUtilsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/sc_UA_UtilsTests.java similarity index 61% rename from sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioUtilsTests.java rename to sdk-java/src/test/java/ly/count/sdk/java/internal/sc_UA_UtilsTests.java index f857f4a9d..b776dacb5 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ScenarioUtilsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/sc_UA_UtilsTests.java @@ -6,24 +6,22 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class ScenarioUtilsTests { +public class sc_UA_UtilsTests { /** - * "safeRandomVal" - * ### 001_validatingIDGenerator - *

+ *

      * testing the ID generator function that is used for events and views
-     * 

+ * * Generate 2 values - *

- * they should be different. - * They should be 21 chars long. - * They should contain on base64 characters. first 8 one is base64 string and last 13 one is timestamp + * + * they should be different. They should be 21 chars long. They should contain only base64 characters. + * first 8 one is base64 string and last 13 one is timestamp * * @throws NumberFormatException for parsing part 2 + *

*/ @Test - public void _001_validatingIDGenerator() throws NumberFormatException { + public void UA_001_validatingIDGenerator() throws NumberFormatException { String val1 = Utils.safeRandomVal(); String val2 = Utils.safeRandomVal();