diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa72ce19..c5106ea3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ * "removeFromCohort(key)" * In Countly class, the old "init(directory,config)" method is deprecated, use "init(config)" instead via "instance()" call. +* Deprecated "Countly::event" call, deprecated builder pattern. Use "Countly::events" instead. +* Deprecated "Usage::event" call, deprecated builder pattern. Use "Countly::events" instead. +* Deprecated "Countly::stop(boolean)" call, use "Countly::halt" instead via "instance()" call. + +* The following methods are deprecated from the "Event" interface: + * "record" + * "endAndRecord" + * "addSegment" + * "addSegments" + * "setSegmentation" + * "setSum" + * "setCount" + * "setDuration" + * "isInvalid" 22.09.2 * Fixed internal log calls that did not respect the configured log level and did not work with the log listener. diff --git a/build.gradle b/build.gradle index 1e78ec271..b30901824 100644 --- a/build.gradle +++ b/build.gradle @@ -1,37 +1,37 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - mavenCentral() - jcenter() - maven { - url "https://maven.google.com" - } - } - dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' - classpath 'com.github.dcendents:android-maven-plugin:1.2' - classpath 'com.google.gms:google-services:4.3.0' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - ext.CLY_VERSION = "23.8.0" - ext.POWERMOCK_VERSION = "1.7.4" - - tasks.withType(Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - } - repositories { - google() - jcenter() - //mavenLocal() - maven { - url "https://maven.google.com" // Google's Maven repository - } - } -} +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + jcenter() + maven { + url "https://maven.google.com" + } + } + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.github.dcendents:android-maven-plugin:1.2' + classpath 'com.google.gms:google-services:4.3.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + ext.CLY_VERSION = "23.8.0" + ext.POWERMOCK_VERSION = "1.7.4" + + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } + repositories { + google() + jcenter() + //mavenLocal() + maven { + url "https://maven.google.com" // Google's Maven repository + } + } +} 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 45db43efe..33fa273d5 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 @@ -367,7 +367,7 @@ public boolean restore(byte[] data, Log L) { * Maximum amount of time in seconds between two update requests to the server * reporting session duration and other parameters if any added between update requests. * - * Update request is also sent when number of unsent events reached {@link #eventsBufferSize}. + * Update request is also sent when number of unsent events reached {@link #eventQueueThreshold}. * * Set to 0 to disable update requests based on time. */ @@ -380,7 +380,7 @@ public boolean restore(byte[] data, Log L) { * * Set to 0 to disable buffering. */ - protected int eventsBufferSize = 10; + protected int eventQueueThreshold = 10; /** * {@link CrashProcessor}-implementing class which is instantiated when application @@ -911,12 +911,12 @@ public Config setUpdateSessionTimerDelay(int delay) { * * Update request is also sent when last update request was sent more than {@link #setSendUpdateEachSeconds(int)} seconds ago. * - * @param eventsBufferSize max number of events between two update requests, set to 0 to disable update requests based on events. + * @param eventQueueThreshold max number of events between two update requests, set to 0 to disable update requests based on events. * @return {@code this} instance for method chaining * @deprecated this will be removed, please use {@link #setEventQueueSizeToSend(int)} */ - public Config setEventsBufferSize(int eventsBufferSize) { - return setEventQueueSizeToSend(eventsBufferSize); + public Config setEventsBufferSize(int eventQueueThreshold) { + return setEventQueueSizeToSend(eventQueueThreshold); } /** @@ -933,7 +933,7 @@ public Config setEventQueueSizeToSend(int eventsQueueSize) { configLog.e("[Config] eventsQueueSize cannot be negative"); } } else { - this.eventsBufferSize = eventsQueueSize; + this.eventQueueThreshold = eventsQueueSize; } return this; } @@ -1414,12 +1414,13 @@ public int getSendUpdateEachSeconds() { } /** - * Getter for {@link #eventsBufferSize} + * Getter for {@link #eventQueueThreshold} * - * @return {@link #eventsBufferSize} value + * @return {@link #eventQueueThreshold} value + * @deprecated */ public int getEventsBufferSize() { - return eventsBufferSize; + return eventQueueThreshold; } /** 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 d337020d6..0e4d249f5 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 @@ -7,6 +7,7 @@ import ly.count.sdk.java.internal.InternalConfig; import ly.count.sdk.java.internal.Log; import ly.count.sdk.java.internal.ModuleBackendMode; +import ly.count.sdk.java.internal.ModuleEvents; import ly.count.sdk.java.internal.SDKCore; /** @@ -132,6 +133,7 @@ public static void init(final File sdkStorageRootDirectory, final Config config) * Also clears all the data if called with {@code clearData = true}. * * @param clearData whether to clear all Countly data or not + * @deprecated use {@link #halt()} instead via instance() call */ public static void stop(boolean clearData) { if (isInitialized()) { @@ -148,6 +150,13 @@ public static void stop(boolean clearData) { } } + /** + * Stop Countly SDK. Stops all tasks and releases resources. + */ + public void halt() { + stop(true); + } + /** * Returns whether Countly SDK has been already initialized or not. * @@ -315,12 +324,34 @@ public static void onConsentRemoval(Config.Feature... features) { } } + /** + * Record event with provided key. + * + * @param key key for this event, cannot be null or empty + * @return Builder object for this event + * @deprecated use {@link #events()} instead via instance() call + */ @Override public Event event(String key) { L.d("[Countly] event: key = " + key); return ((Session) sdk.session(ctx, null)).event(key); } + /** + * Event module calls + * + * @return event module otherwise null if SDK is not initialized + */ + public ModuleEvents.Events events() { + if (!isInitialized()) { + if (L != null) { + L.e("[Countly] SDK is not initialized yet."); + } + return null; + } + return sdk.events(); + } + @Override public Event timedEvent(String key) { L.d("[Countly] timedEvent: key = " + key); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Event.java b/sdk-java/src/main/java/ly/count/sdk/java/Event.java index 40ea98a4f..90cf054e0 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Event.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Event.java @@ -1,5 +1,6 @@ package ly.count.sdk.java; +import ly.count.sdk.java.internal.ModuleEvents; import javax.annotation.Nonnull; import java.util.Map; @@ -10,7 +11,9 @@ public interface Event { /** * Add event to the buffer, send it to the server in case number of events in the session - * is equal or bigger than {@link Config#eventsBufferSize} or wait until next {@link Session#update()}. + * is equal or bigger than {@link Config#eventQueueThreshold} or wait until next {@link Session#update()}. + * + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ void record(); @@ -18,7 +21,9 @@ public interface Event { * Set timed {@link Event} duration as difference between moment {@link Event} was created * and current time in seconds. Then add the event to its session (if they're enabled), * send it to the server in case number of events in the session is equal or bigger - * than {@link Config#eventsBufferSize} or wait until next {@link Session#update()}. + * than {@link Config#eventQueueThreshold} or wait until next {@link Session#update()}. + * + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#endEvent(String, Map, int, double)} instead */ void endAndRecord(); @@ -28,6 +33,7 @@ public interface Event { * @param key key of segment, must not be null or empty * @param value value of segment, must not be null or empty * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event addSegment(@Nonnull String key, @Nonnull String value); @@ -38,6 +44,7 @@ public interface Event { * segmentation from; cannot contain nulls or empty strings; must have * even length * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event addSegments(@Nonnull String... segmentation); @@ -46,6 +53,7 @@ public interface Event { * * @param segmentation map of segment pairs ({key1: value1, key2: value2} * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event setSegmentation(@Nonnull Map segmentation); @@ -54,6 +62,7 @@ public interface Event { * * @param count event count, cannot be 0 * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event setCount(int count); @@ -62,6 +71,7 @@ public interface Event { * * @param sum event sum * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event setSum(double sum); @@ -70,6 +80,7 @@ public interface Event { * * @param duration event duration * @return this instance for method chaining + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead */ Event setDuration(double duration); @@ -81,6 +92,7 @@ public interface Event { *
  • Invalid data supplied (count < 0, NaN as sum, duration < 0, etc.) while in production mode
  • *
  • Event has been already recorded in session, thus should be discarded
  • * + * @deprecated this function is deprecated */ boolean isInvalid(); } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Usage.java b/sdk-java/src/main/java/ly/count/sdk/java/Usage.java index e68d72be4..4210e2fea 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Usage.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Usage.java @@ -1,5 +1,6 @@ package ly.count.sdk.java; +import ly.count.sdk.java.internal.ModuleEvents; import java.util.Map; /** @@ -20,6 +21,7 @@ public interface Usage { * @param key key for this event, cannot be null or empty * @return Event instance. * @see Event#record() + * @deprecated this function is deprecated, use {@link ModuleEvents.Events#recordEvent} instead */ Event event(String key); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java index 55e365b2c..767404cc3 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/CoreFeature.java @@ -5,7 +5,7 @@ public enum CoreFeature { Sessions(1 << 1, ModuleSessions::new), - Events(1 << 2), + Events(1 << 2, ModuleEvents::new), Views(1 << 3, ModuleViews::new), CrashReporting(1 << 4, ModuleCrash::new), Location(1 << 5), 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 fee69edc4..822a2ebf0 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 @@ -38,6 +38,22 @@ public interface EventRecorder { */ private boolean invalid = false; + EventImpl(@Nonnull String key, int count, Double sum, Double duration, @Nonnull Map segmentation, @Nonnull Log givenL) { + L = givenL; + + this.recorder = null; + this.key = key; + this.count = count; + this.sum = sum; + this.duration = duration; + this.segmentation = segmentation; + this.timestamp = Device.dev.uniqueTimestamp(); + this.hour = Device.dev.currentHour(); + this.dow = Device.dev.currentDayOfWeek(); + } + + + EventImpl(@Nonnull EventRecorder recorder, @Nonnull String key, @Nonnull Log givenL) { L = givenL; if (recorder == null) { diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/EventQueue.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/EventQueue.java new file mode 100644 index 000000000..ececc5622 --- /dev/null +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/EventQueue.java @@ -0,0 +1,96 @@ +package ly.count.sdk.java.internal; + +import ly.count.sdk.java.Countly; +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class EventQueue { + + static final String DELIMITER = ":::"; + Log L; + List eventQueueMemoryCache; + + protected EventQueue() { + } + + protected EventQueue(@Nonnull Log logger, int eventThreshold) { + L = logger; + eventQueueMemoryCache = new ArrayList<>(eventThreshold); + } + + /** + * Returns the number of events currently stored in the queue. + */ + protected int eqSize() { + return eventQueueMemoryCache.size(); + } + + void addEvent(@Nonnull final EventImpl event) { + if (event == null) { + L.w("[EventQueue] Event is null, skipping"); + return; + } + L.d("[EventQueue] Adding event: " + event.key); + eventQueueMemoryCache.add(event); + writeEventQueueToStorage(); + } + + /** + * set the new value in event data storage + */ + void writeEventQueueToStorage() { + if (eventQueueMemoryCache.isEmpty()) { + L.d("[EventQueue] No events to write to disk"); + return; + } + + final String eventQueue = joinEvents(eventQueueMemoryCache); + + L.d("[EventQueue] Setting event data: " + eventQueue); + SDKCore.instance.sdkStorage.storeEventQueue(eventQueue); + } + + /** + * Restores events from disk + */ + void restoreFromDisk() { + L.d("[EventQueue] Restoring events from disk"); + eventQueueMemoryCache.clear(); + + final String[] array = getEvents(); + for (String s : array) { + + final EventImpl event = EventImpl.fromJSON(s, (ev) -> { + }, L); + if (event != null) { + eventQueueMemoryCache.add(event); + } + } + // order the events from least to most recent + eventQueueMemoryCache.sort((e1, e2) -> (int) (e1.timestamp - e2.timestamp)); + } + + @Nonnull String joinEvents(@Nonnull final Collection collection) { + final List strings = new ArrayList<>(); + for (EventImpl e : collection) { + strings.add(e.toJSON(L)); + } + return Utils.join(strings, EventQueue.DELIMITER); + } + + /** + * Returns an unsorted array of the current stored event JSON strings. + */ + private synchronized @Nonnull String[] getEvents() { + L.d("[EventQueue] Getting events from disk"); + final String joinedEventsStr = SDKCore.instance.sdkStorage.readEventQueue(); + return joinedEventsStr.isEmpty() ? new String[0] : joinedEventsStr.split(DELIMITER); + } + + public void clear() { + SDKCore.instance.sdkStorage.storeEventQueue(""); + eventQueueMemoryCache.clear(); + } +} 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 c65efe240..98c418417 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 @@ -138,7 +138,7 @@ public byte[] store(Log L) { } } stream.writeInt(sendUpdateEachSeconds); - stream.writeInt(eventsBufferSize); + stream.writeInt(eventQueueThreshold); stream.writeInt(0);//for keeping backwards compatibility, remove in the future. sessionCooldownPeriod stream.writeBoolean(false);//for keeping backwards compatibility, remove in the future stream.writeInt(5);//for keeping backwards compatibility, remove in the future (crashReportingANRCheckingPeriod) @@ -235,7 +235,7 @@ public boolean restore(byte[] data, Log L) { certificatePins.add(stream.readUTF()); } sendUpdateEachSeconds = stream.readInt(); - eventsBufferSize = stream.readInt(); + eventQueueThreshold = stream.readInt(); int throwawaySessionCooldownPeriod = stream.readInt();//we are only reading this for backwards compatibility. Throw away in the future boolean throwawayCountlyTestMode = stream.readBoolean();//we are only reading this for backwards compatibility. Throw away in the future int throwawayCrashReportingANRCheckingPeriod = stream.readInt();//we are only reading this for backwards compatibility. Throw away in the future. crashReportingANRCheckingPeriod 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 58b046fca..28ea85e81 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,29 +1,167 @@ package ly.count.sdk.java.internal; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import ly.count.sdk.java.Countly; public class ModuleEvents extends ModuleBase { + protected CtxCore ctx = null; + protected EventQueue eventQueue = null; + final Map timedEvents = new HashMap<>(); + private ScheduledExecutorService executor = null; + protected Events eventsInterface = null; + @Override public void init(InternalConfig config, Log logger) { super.init(config, logger); - internalConfig = config; + L.d("[ModuleEvents] init: config = " + config); + eventQueue = new EventQueue(L, config.getEventsBufferSize()); + eventQueue.restoreFromDisk(); + eventsInterface = new Events(); + } + + @Override + public void onContextAcquired(@Nonnull CtxCore ctx) { + this.ctx = ctx; + L.d("[ModuleEvents] onContextAcquired: " + ctx); + + if (ctx.getConfig().getSendUpdateEachSeconds() > 0 && executor == null) { + executor = Executors.newScheduledThreadPool(1); + executor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + addEventsToRequestQ(); + } + }, ctx.getConfig().getSendUpdateEachSeconds(), ctx.getConfig().getSendUpdateEachSeconds(), TimeUnit.SECONDS); + } + } + + @Override + public Boolean onRequest(Request request) { + return true; + } + + @Override + public void stop(CtxCore ctx, boolean clear) { + super.stop(ctx, clear); + if (clear) { + eventQueue.clear(); + } + } + + private synchronized void addEventsToRequestQ() { + L.d("[ModuleEvents] addEventsToRequestQ"); + + Request request = new Request(); + request.params.add("device_id", Countly.instance().getDeviceId()); + request.params.arr("events").put(eventQueue.eventQueueMemoryCache).add(); + request.own(ModuleEvents.class); + + eventQueue.clear(); + ModuleRequests.pushAsync(ctx, request); + } + + protected void removeInvalidDataFromSegments(Map segments) { + + if (segments == null || segments.isEmpty()) { + return; + } + + List toRemove = segments.entrySet().stream() + .filter(entry -> !Utils.isValidDataType(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + toRemove.forEach(key -> { + L.w("[ModuleEvents] RemoveSegmentInvalidDataTypes: In segmentation Data type '" + segments.get(key) + "' of item '" + key + "' isn't valid."); + segments.remove(key); + }); + } + + protected void recordEventInternal(String key, int count, Double sum, Map segmentation, Double dur) { + if (count <= 0) { + L.w("[ModuleEvents] recordEventInternal: Count can't be less than 1, ignoring this event."); + return; + } + + if (key == null || key.isEmpty()) { + L.w("[ModuleEvents] recordEventInternal: Key can't be null or empty, ignoring this event."); + return; + } + + removeInvalidDataFromSegments(segmentation); + EventImpl event = new EventImpl(key, count, sum, dur, segmentation, L); + addEventToQueue(event); } - private void recordEventInternal(String key, int count, double sum, Map segmentation, double dur) { + private void addEventToQueue(EventImpl event) { + L.d("[ModuleEvents] addEventToQueue"); + eventQueue.addEvent(event); + checkEventQueueToSend(false); + } + private void checkEventQueueToSend(boolean forceSend) { + L.d("[ModuleEvents] queue size:[" + eventQueue.eqSize() + "] || forceSend: " + forceSend); + if (forceSend || (eventQueue.eqSize() >= internalConfig.getEventsBufferSize())) { + addEventsToRequestQ(); + } } - private boolean startEventInternal(String key) { - return false; + boolean startEventInternal(final String key) { + if (key == null || key.isEmpty()) { + L.e("[ModuleEvents] Can't start event with a null or empty key"); + return false; + } + if (timedEvents.containsKey(key)) { + return false; + } + L.d("[ModuleEvents] Starting event: [" + key + "]"); + timedEvents.put(key, new EventImpl(null, key, L)); + return true; } - private boolean endEventInternal(String key) { - return false; + boolean endEventInternal(final String key, final Map segmentation, final int count, final Double sum) { + L.d("[ModuleEvents] Ending event: [" + key + "]"); + + if (key == null || key.isEmpty()) { + L.e("[ModuleEvents] Can't end event with a null or empty key"); + return false; + } + + EventImpl event = timedEvents.remove(key); + + if (event != null) { + if (count < 1) { + throw new IllegalArgumentException("Countly event count should be greater than zero"); + } + L.d("[ModuleEvents] Ending event: [" + key + "]"); + + long currentTimestamp = Device.dev.uniqueTimestamp(); + double duration = (currentTimestamp - event.timestamp) / 1000.0; + + recordEventInternal(key, count, sum, segmentation, duration); + return true; + } else { + return false; + } } - private boolean cancelEventInternal(String key) { - return false; + boolean cancelEventInternal(final String key) { + if (key == null || key.isEmpty()) { + L.e("[ModuleEvents] Can't cancel event with a null or empty key"); + return false; + } + + EventImpl event = timedEvents.remove(key); + + return event != null; } public class Events { @@ -33,81 +171,81 @@ public class Events { * * @param key key for this event, cannot be null or empty * @param count how many of these events have occurred, default value is "1", must be greater than 0 - * @param sum set sum parameter of the event default value is "0" - * @param dur set duration of event, default value is "0" + * @param sum set sum parameter of the event, can be null + * @param dur set duration of event, can be null * @param segmentation additional segmentation data that you want to set, leave null if you don't want to add anything */ - public void recordEvent(String key, int count, double sum, Map segmentation, double dur) { + public void recordEvent(String key, int count, Double sum, Map segmentation, Double dur) { L.i("[Events] recordEvent: key = " + key + ", count = " + count + ", sum = " + sum + ", segmentation = " + segmentation + ", dur = " + dur); recordEventInternal(key, count, sum, segmentation, dur); } /** - * Record an event with "duration" 0 + * Record an event with "duration" null by default * * @param key key for this event, cannot be null or empty * @param count how many of these events have occurred, default value is "1", must be greater than 0 - * @param sum set sum parameter of the event default value is "0" + * @param sum set sum parameter of the event, can be null * @param segmentation additional segmentation data that you want to set, leave null if you don't want to add anything */ - public void recordEvent(String key, int count, double sum, Map segmentation) { - recordEvent(key, count, sum, segmentation, 0.0); + public void recordEvent(String key, int count, Double sum, Map segmentation) { + recordEvent(key, count, sum, segmentation, null); } /** * Record an event with "segmentation","key" and "count" value only - * "duration" is zero by default + * "duration" is null by default * * @param key key for this event, cannot be null or empty * @param count how many of these events have occurred, default value is "1", must be greater than 0 * @param segmentation additional segmentation data that you want to set, leave null if you don't want to add anything */ public void recordEvent(String key, int count, Map segmentation) { - recordEvent(key, count, 0.0, segmentation); + recordEvent(key, count, null, segmentation); } /** * Record an event with "segmentation" and "key" value only - * "sum" and "duration" is zero by default + * "sum" and "duration" is null by default * * @param key key for this event, cannot be null or empty * @param segmentation additional segmentation data that you want to set, leave null if you don't want to add anything */ public void recordEvent(String key, Map segmentation) { - recordEvent(key, 1, 0.0, segmentation); + recordEvent(key, 1, null, segmentation); } /** * Record an event with "key" only - * "sum" and "duration" is zero by default + * "sum" and "duration" is null by default * "count" is 1 by default * * @param key key for this event, cannot be null or empty */ public void recordEvent(String key) { - recordEvent(key, 1, 0, null); + recordEvent(key, 1, null, null); } /** * Record an event with "key" and "count" only - * "sum" and "duration" is zero by default + * "sum" and "duration" is null by default * * @param key key for this event, cannot be null or empty * @param count how many of these events have occurred, default value is "1", must be greater than 0 */ public void recordEvent(String key, int count) { - recordEvent(key, count, 0, null); + recordEvent(key, count, null, null); } /** * Record an event with "key", "sum" and "count" only - * "duration" is zero by default + * "duration" is null by default * * @param key key for this event, cannot be null or empty * @param count how many of these events have occurred, default value is "1", must be greater than 0 - * @param sum set sum parameter of the event default value is "0" + * @param sum set sum parameter of the event, can be null */ - public void recordEvent(String key, double sum, int count) { + public void recordEvent(String key, int count, Double sum) { recordEvent(key, count, sum, null); } @@ -130,7 +268,23 @@ public boolean startEvent(final String key) { */ public boolean endEvent(final String key) { L.i("[Events] endEvent: key = " + key); - return endEventInternal(key); + return endEventInternal(key, null, 1, null); + } + + /** + * End timed event with a specified key + * + * @param key name of the custom event, required, must not be the empty string + * @param segmentation segmentation dictionary to associate with the event, can be null + * @param count count to associate with the event, should be more than zero, default value is 1 + * @param sum sum to associate with the event, can be null + * @return true if event with this key has been previously started, false otherwise + * @throws IllegalStateException if Countly SDK has not been initialized + * @throws IllegalArgumentException if key is null or empty, count is less than 1, or if segmentation contains null or empty keys or values + */ + public boolean endEvent(final String key, final Map segmentation, final int count, final Double sum) { + L.i("[Events] endEvent: key = " + key + ", segmentation = " + segmentation + ", count = " + count + ", sum = " + sum); + return endEventInternal(key, segmentation, count, sum); } /** 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 fb5b6be98..7c01b4899 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 @@ -57,6 +57,7 @@ protected static void registerDefaultModuleMappings() { moduleMappings.put(CoreFeature.Sessions.getIndex(), ModuleSessions.class); moduleMappings.put(CoreFeature.CrashReporting.getIndex(), ModuleCrash.class); moduleMappings.put(CoreFeature.BackendMode.getIndex(), ModuleBackendMode.class); + moduleMappings.put(CoreFeature.Events.getIndex(), ModuleEvents.class); } public interface Modulator { @@ -100,6 +101,13 @@ public void init(CtxCore ctx) { prepareMappings(ctx); } + /** + * Stop sdk core + * + * @param ctx {@link CtxCore} object + * @param clear if true, clear all data + * @deprecated use {@link #halt(CtxCore)} instead + */ public void stop(final CtxCore ctx, final boolean clear) { if (instance == null) { return; @@ -128,6 +136,15 @@ public void stop(final CtxCore ctx, final boolean clear) { sdkStorage.stop(ctx, clear);//from original super class } + /** + * Stop sdk core + * + * @param ctxCore {@link CtxCore} object + */ + public void halt(CtxCore ctxCore) { + stop(ctxCore, true); + } + private boolean addingConsent(int adding, CoreFeature feature) { return (consents & feature.getIndex()) == 0 && (adding & feature.getIndex()) > 0; } @@ -500,6 +517,10 @@ TimedEvents timedEvents() { return ((ModuleSessions) module(CoreFeature.Sessions.getIndex())).timedEvents(); } + public ModuleEvents.Events events() { + return ((ModuleEvents) module(CoreFeature.Events.getIndex())).eventsInterface; + } + public InternalConfig config() { return config; } 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 0b53d5add..501999213 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 @@ -10,7 +10,6 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; - import ly.count.sdk.java.Config; import ly.count.sdk.java.Event; import ly.count.sdk.java.Session; @@ -286,6 +285,12 @@ protected TimedEvents timedEvents() { return SDKCore.instance.timedEvents(); } + /** + * Record event to session. + * + * @param event + * @deprecated use {@link ModuleEvents.Events#recordEvent(String, int, double, Map, double)} instead + */ @Override public void recordEvent(Event event) { L.d("[SessionImpl] recordEvent: " + event.toString()); @@ -293,21 +298,14 @@ public void recordEvent(Event event) { L.i("[SessionImpl] recordEvent: Skipping event - feature is not enabled"); return; } + if (began == null) { begin(); } - synchronized (storageId()) { - events.add(event); - if (pushOnChange) { - Storage.pushAsync(ctx, this); - } - - Config config = SDKCore.instance.config(); - if (config != null && ctx.getConfig().getEventsBufferSize() <= events.size()) { - update(); - } - } + ModuleEvents eventsModule = (ModuleEvents) SDKCore.instance.module(CoreFeature.Events.getIndex()); + EventImpl eventImpl = (EventImpl) event; + eventsModule.recordEventInternal(eventImpl.key, eventImpl.count, eventImpl.sum, eventImpl.segmentation, eventImpl.duration); } @Override diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java index becdeadd5..c1cddd126 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/BackendModeTests.java @@ -31,7 +31,7 @@ public static void init() { cc.setEventQueueSizeToSend(4).enableBackendMode(); // System specific folder structure - File sdkStorageRootDirectory = TestUtils.getSdkStorageRootDirectory(); + File sdkStorageRootDirectory = TestUtils.getTestSDirectory(); TestUtils.checkSdkStorageRootDirectoryExist(sdkStorageRootDirectory); Countly.init(sdkStorageRootDirectory, cc); 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 new file mode 100644 index 000000000..f6481f77e --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/EventQueueTests.java @@ -0,0 +1,270 @@ +package ly.count.sdk.java.internal; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static ly.count.sdk.java.internal.SDKStorage.EVENT_QUEUE_FILE_NAME; +import static ly.count.sdk.java.internal.SDKStorage.FILE_NAME_PREFIX; +import static ly.count.sdk.java.internal.SDKStorage.FILE_NAME_SEPARATOR; +import static ly.count.sdk.java.internal.TestUtils.validateEvent; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(JUnit4.class) +public class EventQueueTests { + + Log L = mock(Log.class); + + EventQueue eventQueue; + + private void init(Config cc) { + Countly.instance().init(cc); + eventQueue = new EventQueue(L, cc.getEventsBufferSize()); + } + + @After + public void stop() { + Countly.instance().halt(); + eventQueue = null; + } + + /** + * Add an event to queue + * "addEvent" function should add event to both queue and memory + * in memory and cache queue should contain it + */ + @Test + public void addEvent() { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + EventImpl event = createEvent("test-addEvent", null, 1, null, null); + eventQueue.addEvent(event); + validateEventInQueue(event.key, null, 1, null, null, 1, 0); + } + + /** + * Add a null event to queue + * "addEvent" function should not add event to both queue and memory + * in memory and cache queue size should be 0 + */ + @Test + public void addEvent_null() { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + eventQueue.addEvent(null); + validateQueueSize(0); + } + + /** + * Write in memory events to storage + * "writeEventQueueToStorage" function should write events from memory to storage + * in memory and cache queue size should be 1 and should contain event + */ + @Test + public void writeEventQueueToStorage() { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + EventImpl event = createEvent("test-writeEventQueueToStorage", null, 1, null, null); + eventQueue.eventQueueMemoryCache.add(event); + eventQueue.writeEventQueueToStorage(); + validateEventInQueue(event.key, null, 1, null, null, 1, 0); + } + + /** + * Write empty in memory events + * "writeEventQueueToStorage" function should not call "joinEvents" + * joinEvents should not be called + */ + @Test + public void writeEventQueueToStorage_emptyCache() { + eventQueue = spy(EventQueue.class); + eventQueue.L = mock(Log.class); + eventQueue.eventQueueMemoryCache = new ArrayList<>(); + + eventQueue.writeEventQueueToStorage(); + verify(eventQueue, never()).joinEvents(anyCollection()); + } + + /** + * Join events with delimiter + * "joinEvents" function should join events + * joinEvents should return expected String + */ + @Test + public void joinEvents() { + init(TestUtils.getConfigEvents(2)); + + List list = new ArrayList<>(); + list.add(createEvent("test-joinEvents-1", null, 1, null, null)); + list.add(createEvent("test-joinEvents-2", null, 1, null, null)); + + String result = eventQueue.joinEvents(list); + + String expected = list.stream().map(event -> event.toJSON(L)).reduce((a, b) -> a + EventQueue.DELIMITER + b).orElse(""); + Assert.assertEquals(expected, result); + } + + /** + * Join events with empty collection + * "joinEvents" function should join events + * joinEvents should return expected String + */ + @Test + public void joinEvents_emptyCollection() { + init(TestUtils.getConfigEvents(2)); + + List list = new ArrayList<>(); + + String result = eventQueue.joinEvents(list); + Assert.assertEquals("", result); + } + + /** + * Join events with delimiter null collection + * "joinEvents" function should not work + * joinEvents should throw NullPointerException + */ + @Test(expected = NullPointerException.class) + public void joinEvents_nullCollection() { + init(TestUtils.getConfigEvents(2)); + + eventQueue.joinEvents(null); + } + + /** + * Clear events from storage and cache + * "clear" function should work + * both memory and cache should be empty + */ + @Test + public void clear() { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + + eventQueue.addEvent(createEvent("test-clear", null, 1, null, null)); + validateQueueSize(1); + eventQueue.addEvent(createEvent("test-clear", null, 1, null, null)); + validateQueueSize(2); + + eventQueue.clear(); + validateQueueSize(0); + } + + /** + * Restore events from storage + * "restoreFromDisk" function should work + * both memory and cache should have desired size + */ + @Test + public void restoreFromDisk() throws IOException { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + 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); + + eventQueue.restoreFromDisk(); + validateQueueSize(2); + validateEvent(eventQueue.eventQueueMemoryCache.get(0), "test-joinEvents-1", null, 1, null, null); + validateEvent(eventQueue.eventQueueMemoryCache.get(1), "test-joinEvents-2", null, 1, null, null); + } + + /** + * Restore events from storage not existing file + * "restoreFromDisk" function should work + * both memory and cache should be empty + */ + @Test + public void restoreFromDisk_notExist() throws IOException { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + writeToEventQueue(null, true); + + eventQueue.restoreFromDisk(); + validateQueueSize(0); + } + + /** + * Restore events from storage garbage file + * "restoreFromDisk" function should work + * both memory and cache should be empty + */ + @Test + public void restoreFromDisk_garbageFile() throws IOException { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + writeToEventQueue("{\"hour\":10,\"asdasd\":\"askjdn\",\"timestamp\":1695887006647}::{\"hour\":10,\"count\":1,\"dow\":4,\"asda\":\"test-joinEvents-2\"}", false); + + eventQueue.restoreFromDisk(); + validateQueueSize(0); + } + + /** + * Restore events from storage corrupted file + * "restoreFromDisk" function should read only not corrupted events + * both memory and cache should have desired size and contain only not corrupted events + */ + @Test + public void restoreFromDisk_corruptedData() throws IOException { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + writeToEventQueue("{\"hour\":10,\"count\":1,\"dow\":4,\"key\":\"test-joinEvents-1\",\"timestamp\":1695887006647}:::{\"hour\":10,\"count\":1,\"dow\":4,\"keya\":\"test-joinEvents-2\",\"timestamp\":1695887006657}", false); + + eventQueue.restoreFromDisk(); + validateQueueSize(1); + validateEvent(eventQueue.eventQueueMemoryCache.get(0), "test-joinEvents-1", null, 1, null, null); + } + + static void writeToEventQueue(String fileContent, boolean delete) throws IOException { + File file = new File(TestUtils.getTestSDirectory(), FILE_NAME_PREFIX + FILE_NAME_SEPARATOR + EVENT_QUEUE_FILE_NAME); + file.createNewFile(); + if (delete) { + file.delete(); + return; + } + FileWriter writer = new FileWriter(file); + writer.write(fileContent); + writer.close(); + } + + private EventImpl createEvent(String key, Map segmentation, int count, Double sum, Double dur) { + return new EventImpl(key, count, sum, dur, segmentation, L); + } + + private void validateQueueSize(int expectedSize) { + Assert.assertEquals(expectedSize, TestUtils.getCurrentEventQueue(TestUtils.getTestSDirectory(), L).size()); + Assert.assertEquals(expectedSize, eventQueue.eqSize()); + } + + void validateEventInQueue(String key, Map segmentation, + int count, Double sum, Double duration, int queueSize, int elementInQueue) { + List events = TestUtils.getCurrentEventQueue(TestUtils.getTestSDirectory(), L); + validateQueueSize(queueSize); + + //check if event was recorded correctly + EventImpl event = events.get(elementInQueue); + EventImpl eventInMemory = eventQueue.eventQueueMemoryCache.get(0); + validateEvent(event, key, segmentation, count, sum, duration); + validateEvent(eventInMemory, key, segmentation, count, sum, duration); + } +} 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 new file mode 100644 index 000000000..f9ce2be44 --- /dev/null +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java @@ -0,0 +1,488 @@ +package ly.count.sdk.java.internal; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import ly.count.sdk.java.Config; +import ly.count.sdk.java.Countly; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static ly.count.sdk.java.internal.TestUtils.validateEvent; + +@RunWith(JUnit4.class) +public class ModuleEventsTests { + + private ModuleEvents moduleEvents; + + private void init(Config cc) { + Countly.instance().init(cc); + moduleEvents = (ModuleEvents) SDKCore.instance.module(CoreFeature.Events.getIndex()); + } + + @After + public void stop() { + Countly.instance().halt(); + } + + /** + * Recording an event with segmentation + * "recordEvent" function should create an event with given key and segmentation and add it to event queue + * recorded event should have correct key, segmentation, count, sum, duration, dow, hour and timestamp + */ + @Test + public void recordEvent() { + init(TestUtils.getConfigEvents(4)); + + List events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + validateQueueSize(0, events); + + //create segmentation + Map segmentation = new HashMap<>(); + segmentation.put("name", "Johny"); + segmentation.put("weight", 67); + segmentation.put("bald", true); + + //record event with key segmentation and count + Countly.instance().events().recordEvent(TestUtils.eKeys[0], 1, 45.9, segmentation, 32.0); + + //check if event was recorded correctly and size of event queue is equal to size of events in queue + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], segmentation, 1, 45.9, 32.0, 1, 0); + } + + /** + * Recording an event and no event to recover + * "recordEvent" function should create an event with given key + * event queue should be empty when reached to event queue size to send + */ + @Test + public void recordEvent_queueSizeOver() { + init(TestUtils.getConfigEvents(2)); + + validateQueueSize(0); + Assert.assertEquals(0, TestUtils.getCurrentRequestQueue().length); + + Countly.instance().events().recordEvent("recordEvent_queueSizeOver1", 1, 45.9, null, 32.0); + validateQueueSize(1); + Assert.assertEquals(0, TestUtils.getCurrentRequestQueue().length); + + Countly.instance().events().recordEvent("recordEvent_queueSizeOver2", 1, 45.9, null, 32.0); + validateQueueSize(0); + Assert.assertEquals(1, TestUtils.getCurrentRequestQueue().length); + + Map request = TestUtils.getCurrentRequestQueue()[0]; + Assert.assertTrue(request.get("events").contains("recordEvent_queueSizeOver1") && request.get("events").contains("recordEvent_queueSizeOver2")); + } + + /** + * Recording an event with recovered events + * "recordEvent" function should create an event with given key and create a request with memory data + * event queue should be empty when reached to event queue size to send + */ + @Test + public void recordEvent_queueSizeOverMemory() throws IOException { + EventQueueTests.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); + init(TestUtils.getConfigEvents(2)); + + Assert.assertEquals(0, TestUtils.getCurrentRequestQueue().length); + validateQueueSize(2); + Countly.instance().events().recordEvent("recordEvent_queueSizeOver", 1, 45.9, null, 32.0); + validateQueueSize(0); + Assert.assertEquals(1, TestUtils.getCurrentRequestQueue().length); + + Map request = TestUtils.getCurrentRequestQueue()[0]; + Assert.assertTrue(request.get("events").contains("recordEvent_queueSizeOver") && request.get("events").contains("test-joinEvents-1") && request.get("events").contains("test-joinEvents-2")); + } + + /** + * Recording an event with negative count + * "recordEvent" function should not create an event with given key and negative count + * in memory and cache queue should be empty + */ + @Test + public void recordEvent_negativeCount() { + init(TestUtils.getConfigEvents(4)); + + List events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + + validateQueueSize(0, events); + + Countly.instance().events().recordEvent("recordEvent_negativeCount", -1); + events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + + validateQueueSize(0, events); + } + + /** + * Recording an event with null key + * "recordEvent" function should not create an event with given key null key + * in memory and cache queue should be empty + */ + @Test + public void recordEvent_nullKey() { + init(TestUtils.getConfigEvents(4)); + + List events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + + validateQueueSize(0, events); + + Countly.instance().events().recordEvent(null); + events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + + validateQueueSize(0, events); + } + + /** + * Recording an event with empty key + * "recordEvent" function should not create an event with given key empty key + * in memory and cache queue should be empty + */ + @Test + public void recordEvent_emptyKey() { + init(TestUtils.getConfigEvents(4)); + + List events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + validateQueueSize(0, events); + + Countly.instance().events().recordEvent(""); + events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + + validateQueueSize(0, events); + } + + /** + * Recording an event with invalid segment data + * "recordEvent" function should create an event with given segment + * in memory and cache queue should contain it and invalid segment should not exist + */ + @Test + public void recordEvent_invalidSegment() { + init(TestUtils.getConfigEvents(4)); + + List events = TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L); + validateQueueSize(0, events); + //create segmentation + Map segmentation = new HashMap<>(); + segmentation.put("exam_name", "CENG 101"); + segmentation.put("score", 67); + segmentation.put("cheated", false); + segmentation.put("invalid", new HashMap<>()); + + Map expectedSegmentation = new HashMap<>(); + expectedSegmentation.put("exam_name", "CENG 101"); + expectedSegmentation.put("score", 67); + expectedSegmentation.put("cheated", false); + + //record event with key segmentation + Countly.instance().events().recordEvent(TestUtils.eKeys[0], segmentation); + + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], expectedSegmentation, 1, null, null, 1, 0); + } + + /** + * Start an event + * "startEvent" function should create a timed event + * in memory and cache queue should contain it + */ + @Test + public void startEvent() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); + validateTimedEventSize(0, 1); + + EventImpl timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + + endEvent(TestUtils.eKeys[0], null, 1, null); + + Assert.assertEquals(0, moduleEvents.timedEvents.size()); + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], null, 1, null, 0.0, 1, 0); + } + + /** + * Start an event + * "startEvent" function should not create a timed event with empty key + * in memory , timed events and cache queue should not contain it + */ + @Test + public void startEvent_emptyKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().startEvent("")); + validateTimedEventSize(0, 0); + } + + /** + * Start an event + * "startEvent" function should not create a timed event with null key + * in memory , timed events and cache queue should not contain it + */ + @Test + public void startEvent_nullKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().startEvent(null)); + validateTimedEventSize(0, 0); + } + + /** + * Start an event with already started key + * "startEvent" function should not create a timed event with same key as already started + * in memory , timed events and cache queue not contain it + */ + @Test + public void startEvent_alreadyStarted() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); + + validateTimedEventSize(0, 1); + + EventImpl timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + + boolean result = Countly.instance().events().startEvent(TestUtils.eKeys[0]); + Assert.assertFalse(result); + + validateTimedEventSize(0, 1); + + endEvent(TestUtils.eKeys[0], null, 1, null); + + Assert.assertEquals(0, moduleEvents.timedEvents.size()); + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], null, 1, null, 0.0, 1, 0); + } + + /** + * End an event with empty key + * "endEvent" function should not work with empty key + * in memory , timed events and cache queue not contain it + */ + @Test + public void endEvent_emptyKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().endEvent("")); + validateTimedEventSize(0, 0); + } + + /** + * End an event with null key + * "endEvent" function should not work with null key + * in memory , timed events and cache queue not contain it + */ + @Test + public void endEvent_nullKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().endEvent(null)); + validateTimedEventSize(0, 0); + } + + /** + * End a not existing event + * "endEvent" function should not work with not existing event key + * in memory , timed events and cache queue not contain it + */ + @Test + public void endEvent_notExist() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().endEvent("endEvent_notExist")); + validateTimedEventSize(0, 0); + } + + /** + * End an event with segmentation + * "endEvent" function should end an event with segmentation + * in memory , timed events and cache queue should contain it + */ + @Test + public void endEvent_withSegmentation() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); // start event to end it + validateTimedEventSize(0, 1); + + EventImpl timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + + Map segmentation = new HashMap<>(); + segmentation.put("hair_color", "red"); + segmentation.put("hair_length", "short"); + segmentation.put("chauffeur", "g3chauffeur"); // + + endEvent(TestUtils.eKeys[0], segmentation, 1, 5.0); + + Assert.assertEquals(0, moduleEvents.timedEvents.size()); + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], segmentation, 1, 5.0, 0.0, 1, 0); + } + + /** + * End an event with segmentation and negative count + * "endEvent" function should not end an event with negative count + * in memory and cache queue should not contain it, timed events should + * and data should not be set + */ + @Test(expected = IllegalArgumentException.class) + public void endEvent_withSegmentation_negativeCount() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); // start event to end it + validateTimedEventSize(0, 1); + EventImpl timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + + Map segmentation = new HashMap<>(); + segmentation.put("horse_name", "Alice"); + segmentation.put("bet_amount", 300); + segmentation.put("currency", "Dollar"); // + + endEvent(TestUtils.eKeys[0], segmentation, -7, 67.0); + validateTimedEventSize(0, 1); + timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + } + + /** + * Cancel an event with empty key + * "cancelEvent" function should not work with empty key + * in memory, timed events and cache queue not contain it + */ + @Test + public void cancelEvent_emptyKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().cancelEvent("")); + validateTimedEventSize(0, 0); + } + + /** + * Cancel an event with null key + * "cancelEvent" function should not work with null key + * in memory, timed events and cache queue not contain it + */ + @Test + public void cancelEvent_nullKey() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().cancelEvent(null)); + validateTimedEventSize(0, 0); + } + + /** + * Cancel a not existing event + * "cancelEvent" function should not work with not existing event key + * in memory, timed events and cache queue not contain it + */ + @Test + public void cancelEvent_notExist() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + Assert.assertFalse(Countly.instance().events().cancelEvent("cancelEvent_notExist")); + validateTimedEventSize(0, 0); + } + + /** + * Cancel an event + * "cancelEvent" function should cancel an event + * in memory, timed events and cache queue should contain it + */ + @Test + public void cancelEvent() { + init(TestUtils.getConfigEvents(4)); + + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); // start event to end it + validateTimedEventSize(0, 1); + + EventImpl timedEvent = moduleEvents.timedEvents.get(TestUtils.eKeys[0]); + validateEvent(timedEvent, TestUtils.eKeys[0], null, 1, null, null); + + Assert.assertTrue(Countly.instance().events().cancelEvent(TestUtils.eKeys[0])); + Assert.assertEquals(0, moduleEvents.timedEvents.size()); + validateQueueSize(0); + } + + @Test + public void timedEventFlow() throws InterruptedException { + init(TestUtils.getConfigEvents(4)); + validateTimedEventSize(0, 0); + + startEvent(TestUtils.eKeys[0]); // start event to end it + validateTimedEventSize(0, 1); + + Thread.sleep(1000); + startEvent(TestUtils.eKeys[1]); // start event to end it + validateTimedEventSize(0, 2); + + Thread.sleep(1000); + endEvent(TestUtils.eKeys[1], null, 3, 15.0); + + Assert.assertEquals(1, moduleEvents.timedEvents.size()); + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[1], null, 3, 15.0, 1.0, 1, 0); + + endEvent(TestUtils.eKeys[0], null, 2, 4.0); + + Assert.assertEquals(0, moduleEvents.timedEvents.size()); + validateEventInEventQueue(TestUtils.getTestSDirectory(), TestUtils.eKeys[0], null, 2, 4.0, 2.0, 2, 1); + } + + private void validateTimedEventSize(int expectedQueueSize, int expectedTimedEventSize) { + validateQueueSize(expectedQueueSize, TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L)); + Assert.assertEquals(expectedTimedEventSize, moduleEvents.timedEvents.size()); + } + + private void validateQueueSize(int expectedSize, List events) { + Assert.assertEquals(expectedSize, events.size()); + Assert.assertEquals(expectedSize, moduleEvents.eventQueue.eqSize()); + } + + private void validateQueueSize(int expectedSize) { + validateQueueSize(expectedSize, TestUtils.getCurrentEventQueue(moduleEvents.ctx.getContext(), moduleEvents.L)); + } + + private void endEvent(String key, Map segmentation, int count, Double sum) { + boolean result = Countly.instance().events().endEvent(key, segmentation, count, sum); + Assert.assertTrue(result); + } + + private void startEvent(String key) { + boolean result = Countly.instance().events().startEvent(key); + Assert.assertTrue(result); + } + + void validateEventInEventQueue(File targetFolder, String key, Map segmentation, + int count, Double sum, Double duration, int queueSize, int elementInQueue) { + List events = TestUtils.getCurrentEventQueue(targetFolder, moduleEvents.L); + validateQueueSize(queueSize, events); + + //check if event was recorded correctly + EventImpl event = events.get(elementInQueue); + EventImpl eventInMemory = moduleEvents.eventQueue.eventQueueMemoryCache.get(elementInQueue); + validateEvent(event, key, segmentation, count, sum, duration); + validateEvent(eventInMemory, key, segmentation, count, sum, duration); + } +} 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 6fd2976e3..75826c5c5 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 @@ -11,23 +11,25 @@ import java.util.Scanner; import java.util.stream.Stream; import ly.count.sdk.java.Config; +import org.junit.Assert; import static ly.count.sdk.java.internal.SDKStorage.EVENT_QUEUE_FILE_NAME; import static ly.count.sdk.java.internal.SDKStorage.FILE_NAME_PREFIX; import static ly.count.sdk.java.internal.SDKStorage.FILE_NAME_SEPARATOR; +import static org.mockito.Mockito.mock; public class TestUtils { - - static String DELIMETER = ":::"; static String SERVER_URL = "https://test.count.ly"; static String SERVER_APP_KEY = "COUNTLY_APP_KEY"; static String DEVICE_ID = "some_random_test_device_id"; + public static final String[] eKeys = new String[] { "eventKey1", "eventKey2", "eventKey3", "eventKey4", "eventKey5", "eventKey6", "eventKey7" }; + private TestUtils() { } static Config getBaseConfig() { - File sdkStorageRootDirectory = getSdkStorageRootDirectory(); + File sdkStorageRootDirectory = getTestSDirectory(); checkSdkStorageRootDirectoryExist(sdkStorageRootDirectory); Config config = new Config(SERVER_URL, SERVER_APP_KEY, sdkStorageRootDirectory); config.setCustomDeviceId(DEVICE_ID); @@ -35,15 +37,22 @@ static Config getBaseConfig() { return config; } - static Config getVariantConfig(ImmediateRequestGenerator generator) { - Config config = getBaseConfig(); - InternalConfig internalConfig = new InternalConfig(config); + static Config getConfigEvents(Integer eventThreshold) { + File sdkStorageRootDirectory = getTestSDirectory(); + checkSdkStorageRootDirectoryExist(sdkStorageRootDirectory); + Config config = new Config(SERVER_URL, SERVER_APP_KEY, sdkStorageRootDirectory); + config.setCustomDeviceId(DEVICE_ID); + + config.enableFeatures(Config.Feature.Events); + + if (eventThreshold != null) { + config.setEventQueueSizeToSend(eventThreshold); + } - internalConfig.immediateRequestGenerator = generator; return config; } - static File getSdkStorageRootDirectory() { + public static File getTestSDirectory() { // System specific folder structure String[] sdkStorageRootPath = { System.getProperty("user.home"), "__COUNTLY", "java_test" }; return new File(String.join(File.separator, sdkStorageRootPath)); @@ -57,6 +66,10 @@ static void checkSdkStorageRootDirectoryExist(File directory) { } } + protected static Map[] getCurrentRequestQueue() { + return getCurrentRequestQueue(getTestSDirectory(), mock(Log.class)); + } + /** * Get current request queue from target folder * @@ -115,7 +128,7 @@ protected static List getCurrentEventQueue(File targetFolder, Log log //do nothing } - Arrays.stream(fileContent.split(DELIMETER)).forEach(s -> { + Arrays.stream(fileContent.split(EventQueue.DELIMITER)).forEach(s -> { final EventImpl event = EventImpl.fromJSON(s, (ev) -> { }, logger); if (event != null) { @@ -185,4 +198,20 @@ private static Map parseRequestParams(File file) throws IOExcept return paramMap; } } + + static void validateEvent(EventImpl gonnaValidate, String key, Map segmentation, int count, Double sum, Double duration) { + Assert.assertEquals(key, gonnaValidate.key); + Assert.assertEquals(segmentation, gonnaValidate.segmentation); + Assert.assertEquals(count, gonnaValidate.count); + Assert.assertEquals(sum, gonnaValidate.sum); + + if (duration != null) { + double delta = 0.1; + Assert.assertTrue(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); + } } \ No newline at end of file diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java index 717bf2e31..046f1567a 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/UtilsTests.java @@ -295,7 +295,11 @@ public void readFileContent_fileNotReadable() throws IOException { file.setReadable(false); String content = Utils.readFileContent(file, logger); - Assert.assertEquals("", content); + if (System.getProperty("os.name").toLowerCase().contains("win")) { + Assert.assertEquals(fileContent, content); + } else { + Assert.assertEquals("", content); + } } finally { File file = new File(TEST_FILE_NAME); if (file.exists()) {