diff --git a/CHANGELOG.md b/CHANGELOG.md index 61767e3b..6cc68fa4 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,9 @@ * 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. +* 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. @@ -47,6 +49,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/Config.java b/sdk-java/src/main/java/ly/count/sdk/java/Config.java index 54fdfd92..783022c0 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/Countly.java b/sdk-java/src/main/java/ly/count/sdk/java/Countly.java index 0b91c093..c1dbbab4 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 @@ -14,6 +14,7 @@ import ly.count.sdk.java.internal.ModuleLocation; 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; /** @@ -475,6 +476,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/View.java b/sdk-java/src/main/java/ly/count/sdk/java/View.java index bab08f04..d449bcd6 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,4 +1,5 @@ package ly.count.sdk.java; +import ly.count.sdk.java.internal.ModuleViews; import ly.count.sdk.java.internal.ModuleViews; 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 00000000..6886181b --- /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/EventImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/EventImpl.java index 43c53e89..978fbc0e 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/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index 2add2b0e..5c51185b 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 @@ -31,7 +31,7 @@ public class InternalConfig extends Config { 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 b4a6c771..4cae3bb5 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; @@ -12,6 +10,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) { @@ -20,6 +21,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 @@ -85,37 +94,43 @@ private synchronized void addEventsToRequestQ(String deviceId) { ModuleRequests.pushAsync(internalConfig, request); } - protected void removeInvalidDataFromSegments(Map segments) { + 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."); + return; + } - if (segments == null || segments.isEmpty()) { + if (key == null || key.isEmpty()) { + L.w("[ModuleEvents] recordEventInternal, Key can't be null or empty, ignoring this event."); return; } - List toRemove = segments.entrySet().stream() - .filter(entry -> !Utils.isValidDataType(entry.getValue())) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); + L.d("[ModuleEvents] recordEventInternal, Recording event with key: [" + key + "] and provided event ID of:[" + eventIdOverride + "] and segmentation with:[" + (segmentation == null ? "null" : segmentation.size()) + "] keys"); - toRemove.forEach(key -> { - L.w("[ModuleEvents] RemoveSegmentInvalidDataTypes: In segmentation Data type '" + segments.get(key) + "' of item '" + key + "' isn't valid."); - segments.remove(key); - }); - } + Utils.removeInvalidDataFromSegments(segmentation, L); - protected void recordEventInternal(String key, int count, Double sum, Double dur, Map segmentation) { - if (count <= 0) { - L.w("[ModuleEvents] recordEventInternal: Count can't be less than 1, ignoring this event."); - return; + 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 == null || key.isEmpty()) { - L.w("[ModuleEvents] recordEventInternal: Key can't be null or empty, ignoring this event."); - return; + if (key.equals(ModuleViews.KEY_VIEW_EVENT)) { + pvid = viewIdProvider.getPreviousViewId(); + } else { + cvid = viewIdProvider.getCurrentViewId(); + } + + String previousEventIdToSend = this.previousEventId; + 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; } - removeInvalidDataFromSegments(segmentation); - EventImpl event = new EventImpl(key, count, sum, dur, segmentation, L); - addEventToQueue(event); + addEventToQueue(new EventImpl(key, count, sum, dur, segmentation, L, eventId, pvid, cvid, previousEventIdToSend)); } private void addEventToQueue(EventImpl event) { @@ -148,7 +163,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; @@ -179,7 +194,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; } @@ -207,7 +222,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 8f45c091..dd176c3e 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,568 @@ package ly.count.sdk.java.internal; -/** - * Views support - */ +import java.util.ArrayList; +import java.util.LinkedHashMap; +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 { +public class ModuleViews extends ModuleBase implements ViewIdProvider { + String currentViewID = null; + String previousViewID = null; + private boolean firstView = true; + 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[] { KEY_NAME, KEY_VISIT, KEY_START, KEY_SEGMENT }; + //interface for SDK users + Views viewsInterface; + IdGenerator idGenerator; + Map globalViewSegmentation = new ConcurrentHashMap<>(); + 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. + Map viewSegmentation = new ConcurrentHashMap<>(); + } + + ModuleViews() { + } + + @Override + public void init(InternalConfig config) { + super.init(config); + L.v("[ModuleViews] Initializing"); + viewsInterface = new Views(); + + setGlobalViewSegmentationInternal(config.views.globalViewSegmentation); + + idGenerator = config.viewIdGenerator; + config.viewIdProvider = this; + } + + @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; + viewDataMap.clear(); + if (globalViewSegmentation != null) { + globalViewSegmentation.clear(); + globalViewSegmentation = null; + } + } + + private void removeReservedKeysFromViewSegmentation(Map segmentation) { + if (segmentation == null) { + return; + } + + 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"); + } + } + } + + /** + * 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 && !segmentation.isEmpty()) { + removeReservedKeysFromViewSegmentation(segmentation); + Utils.removeInvalidDataFromSegments(segmentation, L); + globalViewSegmentation.putAll(segmentation); + } + } + + public void updateGlobalViewSegmentationInternal(@Nonnull Map segmentation) { + removeReservedKeysFromViewSegmentation(segmentation); + Utils.removeInvalidDataFromSegments(segmentation, L); + + globalViewSegmentation.putAll(segmentation); + } + + 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) { + viewSegmentation.put(KEY_VISIT, KEY_VISIT_VALUE); + } + if (firstView) { + viewSegmentation.put(KEY_START, KEY_START_VALUE); + } + viewSegmentation.put(KEY_SEGMENT, internalConfig.getSdkPlatform()); + return viewSegmentation; + } + + private void autoCloseRequiredViews(boolean closeAllViews, Map customViewSegmentation) { + L.d("[ModuleViews] autoCloseRequiredViews"); + List viewsToRemove = new ArrayList<>(); + + 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"); + } + + removeReservedKeysFromViewSegmentation(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; + } + + removeReservedKeysFromViewSegmentation(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 = idGenerator.generateId(); + currentViewData.viewName = viewName; + currentViewData.viewStartTimeSeconds = TimeUtils.uniqueTimestampS(); + currentViewData.isAutoStoppedView = viewShouldBeAutomaticallyStopped; + + viewDataMap.put(currentViewData.viewID, currentViewData); + previousViewID = currentViewID; + currentViewID = currentViewData.viewID; + + Map viewSegmentation = createViewEventSegmentation(currentViewData, firstView, true, customViewSegmentation); + + if (firstView) { + L.d("[ModuleViews] Recording view as the first one in the session. [" + viewName + "]"); + firstView = false; + } + + recordView(currentViewID, 0.0, viewSegmentation); + 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) { + return; + } + + stopViewWithIDInternal(viewID, customViewSegmentation); + } + + void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map customViewSegmentation) { + ViewData vd = validateViewID(viewID, "stopViewWithIDInternal"); + if (vd == null) { + return; + } + removeReservedKeysFromViewSegmentation(customViewSegmentation); + + 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); + } + + 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"); + return; + } + + events.recordEventInternal(KEY_VIEW_EVENT, 1, 0.0, duration, segmentation, id); + } + + 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 = (double) (TimeUtils.uniqueTimestampS() - vd.viewStartTimeSeconds); + } + + //only record view if the view name is not null + if (vd.viewName == null) { + L.e("[ModuleViews] " + viewRecordingSource + " , view has no internal name, ignoring it"); + return; + } + + Map segments = createViewEventSegmentation(vd, false, false, filteredCustomViewSegmentation); + recordView(vd.viewID, lastElapsedDurationSeconds, segments); + } + + void pauseViewWithIDInternal(String viewID) { + ViewData vd = validateViewID(viewID, "pauseViewWithIDInternal"); + if (vd == null) { + 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; + } + + recordViewEndEvent(vd, null, "pauseViewWithIDInternal"); + + vd.viewStartTimeSeconds = 0; + } + + void resumeViewWithIDInternal(String viewID) { + ViewData vd = validateViewID(viewID, "resumeViewWithIDInternal"); + if (vd == null) { + 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(); + } + + void stopAllViewsInternal(Map viewSegmentation) { + L.d("[ModuleViews] stopAllViewsInternal"); + + 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; + } + + 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 add segmentation with null or empty view segmentation, ignoring request"); + return; + } + removeReservedKeysFromViewSegmentation(viewSegmentation); + vd.viewSegmentation.putAll(viewSegmentation); + } + + public @Nonnull String getCurrentViewId() { + return currentViewID == null ? "" : currentViewID; + } + + public @Nonnull String getPreviousViewId() { + return previousViewID == null ? "" : previousViewID; + } + + 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(@Nonnull String viewName) { + synchronized (Countly.instance()) { + 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(@Nonnull String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startAutoStoppedView [" + viewName + "] sg[" + viewSegmentation + "]"); + 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(@Nonnull String viewName) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startView vn[" + viewName + "]"); + 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(@Nonnull String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling startView vn[" + viewName + "] sg[" + viewSegmentation + "]"); + return startViewInternal(viewName, viewSegmentation, false); + } + } + + /** + * Stops a view with the given name if it was open + * + * @param viewName String - view name + */ + public void stopViewWithName(@Nonnull 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(@Nonnull String viewName, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithName vn[" + viewName + "] sg[" + viewSegmentation + "]"); + stopViewWithNameInternal(viewName, viewSegmentation); + } + } + + /** + * Stops a view with the given ID if it was open + * + * @param viewID String - view ID + */ + public void stopViewWithID(@Nonnull 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(@Nonnull String viewID, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling stopViewWithID vi[" + viewID + "] sg[" + viewSegmentation + "]"); + stopViewWithIDInternal(viewID, viewSegmentation); + } + } + + /** + * Pauses a view with the given ID + * + * @param viewID String - view ID + */ + public void pauseViewWithID(@Nonnull String viewID) { + synchronized (Countly.instance()) { + L.i("[Views] Calling pauseViewWithID vi[" + viewID + "]"); + pauseViewWithIDInternal(viewID); + } + } + + /** + * Resumes a view with the given ID + * + * @param viewID String - view ID + */ + public void resumeViewWithID(@Nonnull String viewID) { + synchronized (Countly.instance()) { + L.i("[Views] Calling resumeViewWithID vi[" + viewID + "]"); + resumeViewWithIDInternal(viewID); + } + } + + /** + * 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 + "]"); + stopAllViewsInternal(viewSegmentation); + } + } + + /** + * Adds segmentation to a view with the given name + * + * @param viewName String + * @param viewSegmentation Map + */ + public void addSegmentationToViewWithName(@Nonnull 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(@Nonnull String viewId, @Nullable Map viewSegmentation) { + synchronized (Countly.instance()) { + L.i("[Views] Calling addSegmentationToViewWithID vi[" + viewId + "] sg[" + viewSegmentation + "]"); + addSegmentationToViewWithIDInternal(viewId, viewSegmentation); + } + } + + /** + * 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 ? 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); + } + } + } } 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 61fb5c21..ee5a3859 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 @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Queue; import java.util.TreeMap; +import javax.annotation.Nonnull; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; import ly.count.sdk.java.Config; @@ -430,6 +431,18 @@ public void init(final InternalConfig givenConfig) { config.eventIdGenerator = Utils::safeRandomVal; } + if (config.viewIdProvider == null) { + config.viewIdProvider = new ViewIdProvider() { + @Nonnull public String getCurrentViewId() { + return ""; + } + + @Nonnull public String getPreviousViewId() { + return ""; + } + }; + } + // ModuleSessions is always enabled, even without consent int consents = config.getFeatures1() | CoreFeature.Sessions.getIndex(); // build modules @@ -728,6 +741,16 @@ public ModuleCrashes.Crashes crashes() { return module(ModuleCrashes.class).crashInterface; } + public ModuleViews.Views views() { + ModuleViews module = module(ModuleViews.class); + if (module == null) { + L.v("[SDKCore] views, Views feature has no consent, returning null"); + return null; + } + + return module.viewsInterface; + } + public ModuleDeviceIdCore.DeviceId deviceId() { return module(ModuleDeviceIdCore.class).deviceIdInterface; } 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 d2e1b277..22a59e8d 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 @@ -311,7 +311,7 @@ public void recordEvent(Event event) { ModuleEvents eventsModule = (ModuleEvents) SDKCore.instance.module(CoreFeature.Events); 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 @@ -368,6 +368,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 +393,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/Utils.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/Utils.java index e8c11b6b..20e784a6 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); + }); + } } 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 00000000..a37fdd1f --- /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/main/java/ly/count/sdk/java/internal/ViewImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ViewImpl.java index 7b70fbeb..be165d3c 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().startAutoStoppedView(name); } @Override @@ -57,27 +44,16 @@ 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."); + if (stop) { + L.w("[ViewImpl] stop: View already stopped!"); return; } - - L.d("[ViewImpl] stop: lastView = " + lastView); - - if (ended) { - return; + stop = true; + Countly.instance().views().stopViewWithName(name); + ModuleViews viewsModule = (ModuleViews) SDKCore.instance.module(CoreFeature.Views.getIndex()); + if (viewsModule != null) { + viewsModule.setFirstViewInternal(lastView); } - 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(); } @Override @@ -85,9 +61,6 @@ public String toString() { return "ViewImpl{" + "name='" + name + '\'' + ", session=" + session + - ", start=" + start + - ", started=" + started + - ", ended=" + ended + '}'; } } 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 cad1f0a6..dd33ba11 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); } /** @@ -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, 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 e8313eae..a0882287 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 a1a854b9..66f7c7ed 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 21110ac2..815dbbdd 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 00000000..3575f17e --- /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 ba5eb841..2f0655ef 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 2265cec5..59ea53c0 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 12df8e06..2b0a9c28 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 00000000..4dff4cba --- /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 00000000..b94a9ead --- /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 f857f4a9..b776dacb 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();