From 00da535913273ba36858de87095a0d1166ce7763 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Wed, 10 Jul 2024 18:05:30 +0200 Subject: [PATCH 1/2] Kotlin Tracker --- .../src/main/java/org/matomo/demo/DemoApp.kt | 11 +- .../org/matomo/sdk/LegacySettingsPorter.kt | 31 +- .../src/main/java/org/matomo/sdk/Tracker.java | 586 ------------------ .../src/main/java/org/matomo/sdk/Tracker.kt | 539 ++++++++++++++++ 4 files changed, 559 insertions(+), 608 deletions(-) delete mode 100644 tracker/src/main/java/org/matomo/sdk/Tracker.java create mode 100644 tracker/src/main/java/org/matomo/sdk/Tracker.kt diff --git a/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt b/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt index 68b412a0..b84f3a12 100644 --- a/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt +++ b/exampleapp/src/main/java/org/matomo/demo/DemoApp.kt @@ -8,6 +8,7 @@ package org.matomo.demo import info.hannes.timber.DebugFormatTree import org.matomo.sdk.TrackMe +import org.matomo.sdk.Tracker import org.matomo.sdk.TrackerBuilder import org.matomo.sdk.extra.DimensionQueue import org.matomo.sdk.extra.DownloadTracker.Extra @@ -48,9 +49,11 @@ class DemoApp : MatomoApplication() { // This will be send the next time something is tracked. dimensionQueue.add(0, "test") - tracker.addTrackingCallback { trackMe: TrackMe? -> - Timber.i("Tracker.Callback.onTrack(%s)", trackMe) - trackMe - } + tracker.addTrackingCallback(object: Tracker.Callback { + override fun onTrack(trackMe: TrackMe?): TrackMe? { + Timber.i("Tracker.Callback.onTrack(%s)", trackMe) + return trackMe + } + }) } } \ No newline at end of file diff --git a/tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt b/tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt index 6f333191..55da0321 100644 --- a/tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt +++ b/tracker/src/main/java/org/matomo/sdk/LegacySettingsPorter.kt @@ -4,51 +4,46 @@ import android.content.SharedPreferences import java.util.UUID class LegacySettingsPorter(matomo: Matomo) { - private val mLegacyPrefs: SharedPreferences - - init { - mLegacyPrefs = matomo.preferences - } + private val mLegacyPrefs: SharedPreferences = matomo.preferences fun port(tracker: Tracker) { val newSettings = tracker.preferences if (mLegacyPrefs.getBoolean(LEGACY_PREF_OPT_OUT, false)) { - newSettings.edit() - .putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, true) - .apply() + newSettings?.edit()?.putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, true) + ?.apply() mLegacyPrefs.edit().remove(LEGACY_PREF_OPT_OUT).apply() } if (mLegacyPrefs.contains(LEGACY_PREF_USER_ID)) { - newSettings.edit() - .putString(Tracker.PREF_KEY_TRACKER_USERID, mLegacyPrefs.getString(LEGACY_PREF_USER_ID, UUID.randomUUID().toString())) - .apply() + newSettings?.edit() + ?.putString(Tracker.PREF_KEY_TRACKER_USERID, mLegacyPrefs.getString(LEGACY_PREF_USER_ID, UUID.randomUUID().toString())) + ?.apply() mLegacyPrefs.edit().remove(LEGACY_PREF_USER_ID).apply() } if (mLegacyPrefs.contains(LEGACY_PREF_FIRST_VISIT)) { - newSettings.edit().putLong( + newSettings?.edit()?.putLong( Tracker.PREF_KEY_TRACKER_FIRSTVISIT, mLegacyPrefs.getLong(LEGACY_PREF_FIRST_VISIT, -1L) - ).apply() + )?.apply() mLegacyPrefs.edit().remove(LEGACY_PREF_FIRST_VISIT).apply() } if (mLegacyPrefs.contains(LEGACY_PREF_VISITCOUNT)) { - newSettings.edit().putLong( + newSettings?.edit()?.putLong( Tracker.PREF_KEY_TRACKER_VISITCOUNT, mLegacyPrefs.getInt(LEGACY_PREF_VISITCOUNT, 0) .toLong() - ).apply() + )?.apply() mLegacyPrefs.edit().remove(LEGACY_PREF_VISITCOUNT).apply() } if (mLegacyPrefs.contains(LEGACY_PREF_PREV_VISIT)) { - newSettings.edit().putLong( + newSettings?.edit()?.putLong( Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, mLegacyPrefs.getLong(LEGACY_PREF_PREV_VISIT, -1) - ).apply() + )?.apply() mLegacyPrefs.edit().remove(LEGACY_PREF_PREV_VISIT).apply() } for ((key) in mLegacyPrefs.all) { if (key.startsWith("downloaded:")) { - newSettings.edit().putBoolean(key, true).apply() + newSettings?.edit()?.putBoolean(key, true)?.apply() mLegacyPrefs.edit().remove(key).apply() } } diff --git a/tracker/src/main/java/org/matomo/sdk/Tracker.java b/tracker/src/main/java/org/matomo/sdk/Tracker.java deleted file mode 100644 index 5a7f50e6..00000000 --- a/tracker/src/main/java/org/matomo/sdk/Tracker.java +++ /dev/null @@ -1,586 +0,0 @@ -/* - * Android SDK for Matomo - * - * @link https://github.com/matomo-org/matomo-android-sdk - * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause - */ - -package org.matomo.sdk; - -import android.content.SharedPreferences; - -import org.matomo.sdk.dispatcher.DispatchMode; -import org.matomo.sdk.dispatcher.Dispatcher; -import org.matomo.sdk.dispatcher.Packet; -import org.matomo.sdk.tools.DeviceHelper; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Random; -import java.util.UUID; -import java.util.regex.Pattern; - -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import timber.log.Timber; - - -/** - * Main tracking class - * This class is threadsafe. - */ -@SuppressWarnings("WeakerAccess") -public class Tracker { - private static final String TAG = Matomo.tag(Tracker.class); - - // Matomo default parameter values - private static final String DEFAULT_UNKNOWN_VALUE = "unknown"; - private static final String DEFAULT_TRUE_VALUE = "1"; - private static final String DEFAULT_RECORD_VALUE = DEFAULT_TRUE_VALUE; - private static final String DEFAULT_API_VERSION_VALUE = "1"; - - // Sharedpreference keys for persisted values - protected static final String PREF_KEY_TRACKER_OPTOUT = "tracker.optout"; - protected static final String PREF_KEY_TRACKER_USERID = "tracker.userid"; - protected static final String PREF_KEY_TRACKER_VISITORID = "tracker.visitorid"; - protected static final String PREF_KEY_TRACKER_FIRSTVISIT = "tracker.firstvisit"; - protected static final String PREF_KEY_TRACKER_VISITCOUNT = "tracker.visitcount"; - protected static final String PREF_KEY_TRACKER_PREVIOUSVISIT = "tracker.previousvisit"; - protected static final String PREF_KEY_OFFLINE_CACHE_AGE = "tracker.cache.age"; - protected static final String PREF_KEY_OFFLINE_CACHE_SIZE = "tracker.cache.size"; - protected static final String PREF_KEY_DISPATCHER_MODE = "tracker.dispatcher.mode"; - - private static final Pattern VALID_URLS = Pattern.compile("^(\\w+)(?:://)(.+?)$"); - - private final Matomo mMatomo; - private final String mApiUrl; - private final int mSiteId; - private final String mDefaultApplicationBaseUrl; - private final Object mTrackingLock = new Object(); - private final Dispatcher mDispatcher; - private final String mName; - private final Random mRandomAntiCachingValue = new Random(new Date().getTime()); - private final TrackMe mDefaultTrackMe = new TrackMe(); - - private TrackMe mLastEvent; - private long mSessionTimeout = 30 * 60 * 1000; - private long mSessionStartTime = 0; - private boolean mOptOut; - private SharedPreferences mPreferences; - - private final LinkedHashSet mTrackingCallbacks = new LinkedHashSet<>(); - private DispatchMode mDispatchMode; - - protected Tracker(Matomo matomo, TrackerBuilder config) { - mMatomo = matomo; - mApiUrl = config.getApiUrl(); - mSiteId = config.getSiteId(); - mName = config.getTrackerName(); - mDefaultApplicationBaseUrl = config.getApplicationBaseUrl(); - - new LegacySettingsPorter(mMatomo).port(this); - - mOptOut = getPreferences().getBoolean(PREF_KEY_TRACKER_OPTOUT, false); - - mDispatcher = mMatomo.getDispatcherFactory().build(this); - mDispatcher.setDispatchMode(getDispatchMode()); - - String userId = getPreferences().getString(PREF_KEY_TRACKER_USERID, null); - mDefaultTrackMe.set(QueryParams.USER_ID, userId); - - String visitorId = getPreferences().getString(PREF_KEY_TRACKER_VISITORID, null); - if (visitorId == null) { - visitorId = makeRandomVisitorId(); - getPreferences().edit().putString(PREF_KEY_TRACKER_VISITORID, visitorId).apply(); - } - mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId); - - mDefaultTrackMe.set(QueryParams.SESSION_START, DEFAULT_TRUE_VALUE); - - DeviceHelper deviceHelper = mMatomo.getDeviceHelper(); - - String resolution = DEFAULT_UNKNOWN_VALUE; - int[] res = deviceHelper.getResolution(); - if (res != null) resolution = String.format("%sx%s", res[0], res[1]); - mDefaultTrackMe.set(QueryParams.SCREEN_RESOLUTION, resolution); - - mDefaultTrackMe.set(QueryParams.USER_AGENT, deviceHelper.getUserAgent()); - mDefaultTrackMe.set(QueryParams.LANGUAGE, deviceHelper.getUserLanguage()); - mDefaultTrackMe.set(QueryParams.URL_PATH, config.getApplicationBaseUrl()); - } - - public void addTrackingCallback(Callback callback) { - this.mTrackingCallbacks.add(callback); - } - - public void removeTrackingCallback(Callback callback) { - this.mTrackingCallbacks.remove(callback); - } - - public void reset() { - dispatch(); - - String visitorId = makeRandomVisitorId(); - - SharedPreferences prefs = getPreferences(); - - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (prefs) { - SharedPreferences.Editor editor = mPreferences.edit(); - - editor.remove(PREF_KEY_TRACKER_VISITCOUNT); - editor.remove(PREF_KEY_TRACKER_PREVIOUSVISIT); - editor.remove(PREF_KEY_TRACKER_FIRSTVISIT); - editor.remove(PREF_KEY_TRACKER_USERID); - editor.remove(PREF_KEY_TRACKER_OPTOUT); - - editor.putString(PREF_KEY_TRACKER_VISITORID, visitorId); - - editor.apply(); - } - - mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId); - mDefaultTrackMe.set(QueryParams.USER_ID, null); - mDefaultTrackMe.set(QueryParams.FIRST_VISIT_TIMESTAMP, null); - mDefaultTrackMe.set(QueryParams.TOTAL_NUMBER_OF_VISITS, null); - mDefaultTrackMe.set(QueryParams.PREVIOUS_VISIT_TIMESTAMP, null); - mDefaultTrackMe.set(QueryParams.SESSION_START, DEFAULT_TRUE_VALUE); - mDefaultTrackMe.set(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES, null); - mDefaultTrackMe.set(QueryParams.CAMPAIGN_NAME, null); - mDefaultTrackMe.set(QueryParams.CAMPAIGN_KEYWORD, null); - - startNewSession(); - } - - /** - * Use this to disable this Tracker, e.g. if the user opted out of tracking. - * The Tracker will persist the choice and remain disable on next instance creation.

- * - * @param optOut true to disable reporting - */ - public void setOptOut(boolean optOut) { - mOptOut = optOut; - getPreferences().edit().putBoolean(PREF_KEY_TRACKER_OPTOUT, optOut).apply(); - } - - /** - * @return true if Matomo is currently disabled - */ - public boolean isOptOut() { - return mOptOut; - } - - public String getName() { - return mName; - } - - public Matomo getMatomo() { - return mMatomo; - } - - public String getAPIUrl() { - return mApiUrl; - } - - protected int getSiteId() { - return mSiteId; - } - - /** - * Matomo will use the content of this object to fill in missing values before any transmission. - * While you can modify it's values, you can also just set them in your {@link TrackMe} object as already set values will not be overwritten. - * - * @return the default TrackMe object - */ - public TrackMe getDefaultTrackMe() { - return mDefaultTrackMe; - } - - public void startNewSession() { - synchronized (mTrackingLock) { - mSessionStartTime = 0; - } - } - - public void setSessionTimeout(int milliseconds) { - synchronized (mTrackingLock) { - mSessionTimeout = milliseconds; - } - } - - /** - * Default is 30min (30*60*1000). - * - * @return session timeout value in miliseconds - */ - public long getSessionTimeout() { - return mSessionTimeout; - } - - /** - * {@link Dispatcher#getConnectionTimeOut()} - */ - public int getDispatchTimeout() { - return mDispatcher.getConnectionTimeOut(); - } - - /** - * {@link Dispatcher#setConnectionTimeOut(int)} - */ - public void setDispatchTimeout(int timeout) { - mDispatcher.setConnectionTimeOut(timeout); - } - - /** - * Processes all queued events in background thread - */ - public void dispatch() { - if (mOptOut) return; - mDispatcher.forceDispatch(); - } - - /** - * Process all queued events and block until processing is complete - */ - public void dispatchBlocking() { - if (mOptOut) return; - mDispatcher.forceDispatchBlocking(); - } - - /** - * Set the interval to 0 to dispatch events as soon as they are queued. - * If a negative value is used the dispatch timer will never run, a manual dispatch must be used. - * - * @param dispatchInterval in milliseconds - */ - public Tracker setDispatchInterval(long dispatchInterval) { - mDispatcher.setDispatchInterval(dispatchInterval); - return this; - } - - /** - * Defines if when dispatched, posted JSON must be Gzipped. - * Need to be handle from web server side with mod_deflate/APACHE lua_zlib/NGINX. - * - * @param dispatchGzipped boolean - */ - public Tracker setDispatchGzipped(boolean dispatchGzipped) { - mDispatcher.setDispatchGzipped(dispatchGzipped); - return this; - } - - /** - * @return in milliseconds - */ - public long getDispatchInterval() { - return mDispatcher.getDispatchInterval(); - } - - /** - * For how long events should be stored if they could not be send. - * Events older than the set limit will be discarded on the next dispatch attempt.
- * The Matomo backend accepts backdated events for up to 24 hours by default. - *

- * >0 = limit in ms
- * 0 = unlimited
- * -1 = disabled offline cache
- * - * @param age in milliseconds - */ - public void setOfflineCacheAge(long age) { - getPreferences().edit().putLong(PREF_KEY_OFFLINE_CACHE_AGE, age).apply(); - } - - /** - * See {@link #setOfflineCacheAge(long)} - * - * @return maximum cache age in milliseconds - */ - public long getOfflineCacheAge() { - return getPreferences().getLong(PREF_KEY_OFFLINE_CACHE_AGE, 24 * 60 * 60 * 1000); - } - - /** - * How large the offline cache may be. - * If the limit is reached the oldest files will be deleted first. - * Events older than the set limit will be discarded on the next dispatch attempt.
- * The Matomo backend accepts backdated events for up to 24 hours by default. - *

- * >0 = limit in byte
- * 0 = unlimited
- * - * @param size in byte - */ - public void setOfflineCacheSize(long size) { - getPreferences().edit().putLong(PREF_KEY_OFFLINE_CACHE_SIZE, size).apply(); - } - - /** - * Maximum size the offline cache is allowed to grow to. - * - * @return size in byte - */ - public long getOfflineCacheSize() { - return getPreferences().getLong(PREF_KEY_OFFLINE_CACHE_SIZE, 4 * 1024 * 1024); - } - - /** - * The current dispatch behavior. - * - * @see DispatchMode - */ - public DispatchMode getDispatchMode() { - if (mDispatchMode == null) { - String raw = getPreferences().getString(PREF_KEY_DISPATCHER_MODE, null); - mDispatchMode = DispatchMode.fromString(raw); - if (mDispatchMode == null) mDispatchMode = DispatchMode.ALWAYS; - } - return mDispatchMode; - } - - /** - * Sets the dispatch mode. - * - * @see DispatchMode - */ - public void setDispatchMode(DispatchMode mode) { - mDispatchMode = mode; - if (mode != DispatchMode.EXCEPTION) { - getPreferences().edit().putString(PREF_KEY_DISPATCHER_MODE, mode.toString()).apply(); - } - mDispatcher.setDispatchMode(mode); - } - - /** - * Defines the User ID for this request. - * User ID is any non empty unique string identifying the user (such as an email address or a username). - * To access this value, users must be logged-in in your system so you can - * fetch this user ID from your system, and pass it to Matomo. - *

- * When specified, the User ID will be "enforced". - * This means that if there is no recent visit with this User ID, a new one will be created. - * If a visit is found in the last 30 minutes with your specified User ID, - * then the new action will be recorded to this existing visit. - * - * @param userId passing null will delete the current user-id. - */ - public Tracker setUserId(String userId) { - mDefaultTrackMe.set(QueryParams.USER_ID, userId); - getPreferences().edit().putString(PREF_KEY_TRACKER_USERID, userId).apply(); - return this; - } - - /** - * @return a user-id string, either the one you set or the one Matomo generated for you. - */ - public String getUserId() { - return mDefaultTrackMe.get(QueryParams.USER_ID); - } - - /** - * The unique visitor ID, must be a 16 characters hexadecimal string. - * Every unique visitor must be assigned a different ID and this ID must not change after it is assigned. - * If this value is not set Matomo will still track visits, but the unique visitors metric might be less accurate. - */ - public Tracker setVisitorId(String visitorId) throws IllegalArgumentException { - if (confirmVisitorIdFormat(visitorId)) mDefaultTrackMe.set(QueryParams.VISITOR_ID, visitorId); - return this; - } - - public String getVisitorId() { - return mDefaultTrackMe.get(QueryParams.VISITOR_ID); - } - - private static final Pattern PATTERN_VISITOR_ID = Pattern.compile("^[0-9a-f]{16}$"); - - private boolean confirmVisitorIdFormat(String visitorId) throws IllegalArgumentException { - if (PATTERN_VISITOR_ID.matcher(visitorId).matches()) return true; - - throw new IllegalArgumentException("VisitorId: " + visitorId + " is not of valid format, " + - " the format must match the regular expression: " + PATTERN_VISITOR_ID.pattern()); - } - - /** - * There parameters are only interesting for the very first query. - */ - private void injectInitialParams(TrackMe trackMe) { - long firstVisitTime; - long visitCount; - long previousVisit; - - SharedPreferences prefs = getPreferences(); - // Protected against Trackers on other threads trying to do the same thing. - // This works because they would use the same preference object. - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (prefs) { - SharedPreferences.Editor editor = prefs.edit(); - visitCount = 1 + getPreferences().getLong(PREF_KEY_TRACKER_VISITCOUNT, 0); - editor.putLong(PREF_KEY_TRACKER_VISITCOUNT, visitCount); - - firstVisitTime = prefs.getLong(PREF_KEY_TRACKER_FIRSTVISIT, -1); - if (firstVisitTime == -1) { - firstVisitTime = System.currentTimeMillis() / 1000; - editor.putLong(PREF_KEY_TRACKER_FIRSTVISIT, firstVisitTime); - } - - previousVisit = prefs.getLong(PREF_KEY_TRACKER_PREVIOUSVISIT, -1); - editor.putLong(PREF_KEY_TRACKER_PREVIOUSVISIT, System.currentTimeMillis() / 1000); - - editor.apply(); - } - - // trySet because the developer could have modded these after creating the Tracker - mDefaultTrackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, firstVisitTime); - mDefaultTrackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, visitCount); - - if (previousVisit != -1) mDefaultTrackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, previousVisit); - - trackMe.trySet(QueryParams.SESSION_START, mDefaultTrackMe.get(QueryParams.SESSION_START)); - trackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, mDefaultTrackMe.get(QueryParams.FIRST_VISIT_TIMESTAMP)); - trackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, mDefaultTrackMe.get(QueryParams.TOTAL_NUMBER_OF_VISITS)); - trackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, mDefaultTrackMe.get(QueryParams.PREVIOUS_VISIT_TIMESTAMP)); - } - - /** - * These parameters are required for all queries. - */ - private void injectBaseParams(TrackMe trackMe) { - trackMe.trySet(QueryParams.SITE_ID, mSiteId); - trackMe.trySet(QueryParams.RECORD, DEFAULT_RECORD_VALUE); - trackMe.trySet(QueryParams.API_VERSION, DEFAULT_API_VERSION_VALUE); - trackMe.trySet(QueryParams.RANDOM_NUMBER, mRandomAntiCachingValue.nextInt(100000)); - trackMe.trySet(QueryParams.DATETIME_OF_REQUEST, new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US).format(new Date())); - trackMe.trySet(QueryParams.SEND_IMAGE, "0"); - - trackMe.trySet(QueryParams.VISITOR_ID, mDefaultTrackMe.get(QueryParams.VISITOR_ID)); - trackMe.trySet(QueryParams.USER_ID, mDefaultTrackMe.get(QueryParams.USER_ID)); - - trackMe.trySet(QueryParams.SCREEN_RESOLUTION, mDefaultTrackMe.get(QueryParams.SCREEN_RESOLUTION)); - trackMe.trySet(QueryParams.USER_AGENT, mDefaultTrackMe.get(QueryParams.USER_AGENT)); - trackMe.trySet(QueryParams.LANGUAGE, mDefaultTrackMe.get(QueryParams.LANGUAGE)); - - String urlPath = trackMe.get(QueryParams.URL_PATH); - if (urlPath == null) { - urlPath = mDefaultTrackMe.get(QueryParams.URL_PATH); - } else if (!VALID_URLS.matcher(urlPath).matches()) { - StringBuilder urlBuilder = new StringBuilder(mDefaultApplicationBaseUrl); - if (!mDefaultApplicationBaseUrl.endsWith("/") && !urlPath.startsWith("/")) { - urlBuilder.append("/"); - } else if (mDefaultApplicationBaseUrl.endsWith("/") && urlPath.startsWith("/")) { - urlPath = urlPath.substring(1); - } - urlPath = urlBuilder.append(urlPath).toString(); - } - - // https://github.com/matomo-org/matomo-sdk-android/issues/92 - mDefaultTrackMe.set(QueryParams.URL_PATH, urlPath); - trackMe.set(QueryParams.URL_PATH, urlPath); - } - - public Tracker track(TrackMe trackMe) { - synchronized (mTrackingLock) { - final boolean newSession = System.currentTimeMillis() - mSessionStartTime > mSessionTimeout; - - if (newSession) { - mSessionStartTime = System.currentTimeMillis(); - injectInitialParams(trackMe); - } - - injectBaseParams(trackMe); - - for (Callback callback : mTrackingCallbacks) { - trackMe = callback.onTrack(trackMe); - if (trackMe == null) { - Timber.tag(TAG).d("Tracking aborted by %s", callback); - return this; - } - } - - mLastEvent = trackMe; - if (!mOptOut) { - mDispatcher.submit(trackMe); - Timber.tag(TAG).d("Event added to the queue: %s", trackMe); - } else { - Timber.tag(TAG).d("Event omitted due to opt out: %s", trackMe); - } - - return this; - } - } - - public static String makeRandomVisitorId() { - return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16); - } - - - public SharedPreferences getPreferences() { - if (mPreferences == null) mPreferences = mMatomo.getTrackerPreferences(this); - return mPreferences; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Tracker tracker = (Tracker) o; - - if (mSiteId != tracker.mSiteId) return false; - if (!mApiUrl.equals(tracker.mApiUrl)) return false; - return mName.equals(tracker.mName); - - } - - @Override - public int hashCode() { - int result = mApiUrl.hashCode(); - result = 31 * result + mSiteId; - result = 31 * result + mName.hashCode(); - return result; - } - - /** - * For testing purposes - * - * @return query of the event - */ - @VisibleForTesting - public TrackMe getLastEventX() { - return mLastEvent; - } - - /** - * Set a data structure here to put the Dispatcher into dry-run-mode. - * Data will be processed but at the last step just stored instead of transmitted. - * Set it to null to disable it. - * - * @param dryRunTarget a data structure the data should be passed into - */ - public void setDryRunTarget(List dryRunTarget) { - mDispatcher.setDryRunTarget(dryRunTarget); - } - - /** - * If we are in dry-run mode then this will return a datastructure. - * - * @return a datastructure or null - */ - public List getDryRunTarget() { - return mDispatcher.getDryRunTarget(); - } - - public interface Callback { - /** - * This method will be called after parameter injection and before transmission within {@link Tracker#track(TrackMe)}. - * Blocking within this method will block tracking. - * - * @param trackMe The `TrackMe` that was passed to {@link Tracker#track(TrackMe)} after all data has been injected. - * @return The `TrackMe` that will be send, returning NULL here will abort transmission. - */ - @Nullable - TrackMe onTrack(TrackMe trackMe); - } -} diff --git a/tracker/src/main/java/org/matomo/sdk/Tracker.kt b/tracker/src/main/java/org/matomo/sdk/Tracker.kt new file mode 100644 index 00000000..1c49ad53 --- /dev/null +++ b/tracker/src/main/java/org/matomo/sdk/Tracker.kt @@ -0,0 +1,539 @@ +/* + * Android SDK for Matomo + * + * @link https://github.com/matomo-org/matomo-android-sdk + * @license https://github.com/matomo-org/matomo-sdk-android/blob/master/LICENSE BSD-3 Clause + */ +package org.matomo.sdk + +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import org.matomo.sdk.dispatcher.DispatchMode +import org.matomo.sdk.dispatcher.Dispatcher +import org.matomo.sdk.dispatcher.Packet +import org.matomo.sdk.tools.DeviceHelper +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.Random +import java.util.UUID +import java.util.regex.Pattern + +/** + * Main tracking class + * This class is threadsafe. + */ +class Tracker(val matomo: Matomo, config: TrackerBuilder) { + val aPIUrl: String = config.apiUrl + val siteId: Int = config.siteId + private val defaultApplicationBaseUrl: String = config.applicationBaseUrl + private val trackingLock = Any() + private val localDispatcher: Dispatcher + val name: String = config.trackerName + private val mRandomAntiCachingValue = Random(Date().time) + + /** + * Matomo will use the content of this object to fill in missing values before any transmission. + * While you can modify it's values, you can also just set them in your [TrackMe] object as already set values will not be overwritten. + * + * @return the default TrackMe object + */ + val defaultTrackMe: TrackMe = TrackMe() + + /** + * For testing purposes + * + * @return query of the event + */ + @get:VisibleForTesting + var lastEventX: TrackMe? = null + private set + + /** + * Default is 30min (30*60*1000). + * + * @return session timeout value in miliseconds + */ + var sessionTimeout: Long = (30 * 60 * 1000).toLong() + private set + private var sessionStartTime: Long = 0 + private var localOptOut: Boolean + private var mPreferences: SharedPreferences? = null + + private val mTrackingCallbacks = LinkedHashSet() + private var localDispatchMode: DispatchMode? = null + + fun addTrackingCallback(callback: Callback) { + mTrackingCallbacks.add(callback) + } + + fun removeTrackingCallback(callback: Callback) { + mTrackingCallbacks.remove(callback) + } + + fun reset() { + dispatch() + + val visitorId = makeRandomVisitorId() + + preferences?.let { + val prefs: SharedPreferences = it + + synchronized(prefs) { + val editor: SharedPreferences.Editor = it.edit() + editor.remove(PREF_KEY_TRACKER_VISITCOUNT) + editor.remove(PREF_KEY_TRACKER_PREVIOUSVISIT) + editor.remove(PREF_KEY_TRACKER_FIRSTVISIT) + editor.remove(PREF_KEY_TRACKER_USERID) + editor.remove(PREF_KEY_TRACKER_OPTOUT) + + editor.putString(PREF_KEY_TRACKER_VISITORID, visitorId) + editor.apply() + } + + } + defaultTrackMe[QueryParams.VISITOR_ID] = visitorId + defaultTrackMe[QueryParams.USER_ID] = null + defaultTrackMe[QueryParams.FIRST_VISIT_TIMESTAMP] = null + defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS] = null + defaultTrackMe[QueryParams.PREVIOUS_VISIT_TIMESTAMP] = null + defaultTrackMe[QueryParams.SESSION_START] = DEFAULT_TRUE_VALUE + defaultTrackMe[QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES] = null + defaultTrackMe[QueryParams.CAMPAIGN_NAME] = null + defaultTrackMe[QueryParams.CAMPAIGN_KEYWORD] = null + startNewSession() + } + + var isOptOut: Boolean + /** + * @return true if Matomo is currently disabled + */ + get() = localOptOut + /** + * Use this to disable this Tracker, e.g. if the user opted out of tracking. + * The Tracker will persist the choice and remain disable on next instance creation. + * + * + * + * @param optOut true to disable reporting + */ + set(optOut) { + localOptOut = optOut + preferences?.edit()?.putBoolean(PREF_KEY_TRACKER_OPTOUT, optOut)?.apply() + } + + fun startNewSession() { + synchronized(trackingLock) { + sessionStartTime = 0 + } + } + + fun setSessionTimeout(milliseconds: Int) { + synchronized(trackingLock) { + sessionTimeout = milliseconds.toLong() + } + } + + var dispatchTimeout: Int + /** + * [Dispatcher.getConnectionTimeOut] + */ + get() = localDispatcher.connectionTimeOut + /** + * [Dispatcher.setConnectionTimeOut] + */ + set(timeout) { + localDispatcher.connectionTimeOut = timeout + } + + /** + * Processes all queued events in background thread + */ + fun dispatch() { + if (localOptOut) return + localDispatcher.forceDispatch() + } + + /** + * Process all queued events and block until processing is complete + */ + fun dispatchBlocking() { + if (localOptOut) return + localDispatcher.forceDispatchBlocking() + } + + /** + * Set the interval to 0 to dispatch events as soon as they are queued. + * If a negative value is used the dispatch timer will never run, a manual dispatch must be used. + * + * @param dispatchInterval in milliseconds + */ + fun setDispatchInterval(dispatchInterval: Long): Tracker { + localDispatcher.dispatchInterval = dispatchInterval + return this + } + + /** + * Defines if when dispatched, posted JSON must be Gzipped. + * Need to be handle from web server side with mod_deflate/APACHE lua_zlib/NGINX. + * + * @param dispatchGzipped boolean + */ + fun setDispatchGzipped(dispatchGzipped: Boolean): Tracker { + localDispatcher.dispatchGzipped = dispatchGzipped + return this + } + + val dispatchInterval: Long + /** + * @return in milliseconds + */ + get() = localDispatcher.dispatchInterval + + var offlineCacheAge: Long + /** + * See [.setOfflineCacheAge] + * + * @return maximum cache age in milliseconds + */ + get() = preferences?.getLong(PREF_KEY_OFFLINE_CACHE_AGE, (24 * 60 * 60 * 1000).toLong()) ?: -1L + /** + * For how long events should be stored if they could not be send. + * Events older than the set limit will be discarded on the next dispatch attempt.

+ * The Matomo backend accepts backdated events for up to 24 hours by default. + * + * + * >0 = limit in ms

+ * 0 = unlimited

+ * -1 = disabled offline cache

+ * + * @param age in milliseconds + */ + set(age) { + preferences?.edit()?.putLong(PREF_KEY_OFFLINE_CACHE_AGE, age)?.apply() + } + + var offlineCacheSize: Long + /** + * Maximum size the offline cache is allowed to grow to. + * + * @return size in byte + */ + get() = preferences?.getLong(PREF_KEY_OFFLINE_CACHE_SIZE, (4 * 1024 * 1024).toLong()) ?: -1L + /** + * How large the offline cache may be. + * If the limit is reached the oldest files will be deleted first. + * Events older than the set limit will be discarded on the next dispatch attempt.

+ * The Matomo backend accepts backdated events for up to 24 hours by default. + * + * + * >0 = limit in byte

+ * 0 = unlimited

+ * + * @param size in byte + */ + set(size) { + preferences?.edit()?.putLong(PREF_KEY_OFFLINE_CACHE_SIZE, size)?.apply() + } + + var dispatchMode: DispatchMode? + /** + * The current dispatch behavior. + * + * @see DispatchMode + */ + get() { + if (localDispatchMode == null) { + val raw: String? = preferences?.getString(PREF_KEY_DISPATCHER_MODE, null) + localDispatchMode = DispatchMode.fromString(raw) + if (localDispatchMode == null) localDispatchMode = DispatchMode.ALWAYS + } + return localDispatchMode + } + /** + * Sets the dispatch mode. + * + * @see DispatchMode + */ + set(mode) { + localDispatchMode = mode + if (mode != DispatchMode.EXCEPTION) { + preferences?.edit()?.putString(PREF_KEY_DISPATCHER_MODE, mode.toString())?.apply() + } + localDispatcher.dispatchMode = mode + } + + /** + * Defines the User ID for this request. + * User ID is any non empty unique string identifying the user (such as an email address or a username). + * To access this value, users must be logged-in in your system so you can + * fetch this user ID from your system, and pass it to Matomo. + * + * + * When specified, the User ID will be "enforced". + * This means that if there is no recent visit with this User ID, a new one will be created. + * If a visit is found in the last 30 minutes with your specified User ID, + * then the new action will be recorded to this existing visit. + * + * @param userId passing null will delete the current user-id. + */ + fun setUserId(userId: String?): Tracker { + defaultTrackMe[QueryParams.USER_ID] = userId + preferences?.edit()?.putString(PREF_KEY_TRACKER_USERID, userId)?.apply() + return this + } + + val userId: String + /** + * @return a user-id string, either the one you set or the one Matomo generated for you. + */ + get() = defaultTrackMe[QueryParams.USER_ID] + + /** + * The unique visitor ID, must be a 16 characters hexadecimal string. + * Every unique visitor must be assigned a different ID and this ID must not change after it is assigned. + * If this value is not set Matomo will still track visits, but the unique visitors metric might be less accurate. + */ + @Throws(IllegalArgumentException::class) + fun setVisitorId(visitorId: String): Tracker { + if (confirmVisitorIdFormat(visitorId)) defaultTrackMe[QueryParams.VISITOR_ID] = visitorId + return this + } + + val visitorId: String + get() = defaultTrackMe[QueryParams.VISITOR_ID] + + init { + LegacySettingsPorter(matomo).port(this) + + localOptOut = preferences?.getBoolean(PREF_KEY_TRACKER_OPTOUT, false) ?: false + + localDispatcher = matomo.dispatcherFactory.build(this) + localDispatcher.dispatchMode = dispatchMode + + val userId: String? = preferences?.getString(PREF_KEY_TRACKER_USERID, null) + defaultTrackMe[QueryParams.USER_ID] = userId + + var visitorId: String? = preferences?.getString(PREF_KEY_TRACKER_VISITORID, null) + if (visitorId == null) { + visitorId = makeRandomVisitorId() + preferences?.edit()?.putString(PREF_KEY_TRACKER_VISITORID, visitorId)?.apply() + } + defaultTrackMe[QueryParams.VISITOR_ID] = visitorId + + defaultTrackMe[QueryParams.SESSION_START] = DEFAULT_TRUE_VALUE + + val deviceHelper: DeviceHelper = matomo.deviceHelper + + val resolution: String + val res: IntArray = deviceHelper.getResolution() + resolution = String.format("%sx%s", res[0], res[1]) + defaultTrackMe[QueryParams.SCREEN_RESOLUTION] = resolution + + defaultTrackMe.set(QueryParams.USER_AGENT, deviceHelper.getUserAgent()) + defaultTrackMe.set(QueryParams.LANGUAGE, deviceHelper.getUserLanguage()) + defaultTrackMe[QueryParams.URL_PATH] = config.applicationBaseUrl + } + + @Throws(IllegalArgumentException::class) + private fun confirmVisitorIdFormat(visitorId: String): Boolean { + if (PATTERN_VISITOR_ID.matcher(visitorId).matches()) return true + + throw IllegalArgumentException( + "VisitorId: " + visitorId + " is not of valid format, " + + " the format must match the regular expression: " + PATTERN_VISITOR_ID.pattern() + ) + } + + /** + * There parameters are only interesting for the very first query. + */ + private fun injectInitialParams(trackMe: TrackMe?) { + var firstVisitTime = -1L + var visitCount: Long = 0 + var previousVisit = -1L + + preferences?.let { + val prefs: SharedPreferences = it + // Protected against Trackers on other threads trying to do the same thing. + // This works because they would use the same preference object. + synchronized(prefs) { + val editor: SharedPreferences.Editor = prefs.edit() + visitCount = 1 + it.getLong(PREF_KEY_TRACKER_VISITCOUNT, 0) + editor.putLong(PREF_KEY_TRACKER_VISITCOUNT, visitCount) + + firstVisitTime = prefs.getLong(PREF_KEY_TRACKER_FIRSTVISIT, -1) + if (firstVisitTime == -1L) { + firstVisitTime = System.currentTimeMillis() / 1000 + editor.putLong(PREF_KEY_TRACKER_FIRSTVISIT, firstVisitTime) + } + + previousVisit = prefs.getLong(PREF_KEY_TRACKER_PREVIOUSVISIT, -1) + editor.putLong(PREF_KEY_TRACKER_PREVIOUSVISIT, System.currentTimeMillis() / 1000) + editor.apply() + } + } + + // trySet because the developer could have modded these after creating the Tracker + defaultTrackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, firstVisitTime) + defaultTrackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, visitCount) + + if (previousVisit != -1L) + defaultTrackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, previousVisit) + + trackMe!!.trySet(QueryParams.SESSION_START, defaultTrackMe[QueryParams.SESSION_START]) + trackMe.trySet(QueryParams.FIRST_VISIT_TIMESTAMP, defaultTrackMe[QueryParams.FIRST_VISIT_TIMESTAMP]) + trackMe.trySet(QueryParams.TOTAL_NUMBER_OF_VISITS, defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS]) + trackMe.trySet(QueryParams.PREVIOUS_VISIT_TIMESTAMP, defaultTrackMe[QueryParams.PREVIOUS_VISIT_TIMESTAMP]) + } + + /** + * These parameters are required for all queries. + */ + private fun injectBaseParams(trackMe: TrackMe?) { + trackMe!!.trySet(QueryParams.SITE_ID, siteId) + trackMe.trySet(QueryParams.RECORD, DEFAULT_RECORD_VALUE) + trackMe.trySet(QueryParams.API_VERSION, DEFAULT_API_VERSION_VALUE) + trackMe.trySet(QueryParams.RANDOM_NUMBER, mRandomAntiCachingValue.nextInt(100000)) + trackMe.trySet(QueryParams.DATETIME_OF_REQUEST, SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US).format(Date())) + trackMe.trySet(QueryParams.SEND_IMAGE, "0") + + trackMe.trySet(QueryParams.VISITOR_ID, defaultTrackMe[QueryParams.VISITOR_ID]) + trackMe.trySet(QueryParams.USER_ID, defaultTrackMe[QueryParams.USER_ID]) + + trackMe.trySet(QueryParams.SCREEN_RESOLUTION, defaultTrackMe[QueryParams.SCREEN_RESOLUTION]) + trackMe.trySet(QueryParams.USER_AGENT, defaultTrackMe[QueryParams.USER_AGENT]) + trackMe.trySet(QueryParams.LANGUAGE, defaultTrackMe[QueryParams.LANGUAGE]) + + var urlPath = trackMe[QueryParams.URL_PATH] + if (urlPath == null) { + urlPath = defaultTrackMe[QueryParams.URL_PATH] + } else if (!VALID_URLS.matcher(urlPath).matches()) { + val urlBuilder = StringBuilder(defaultApplicationBaseUrl) + if (!defaultApplicationBaseUrl.endsWith("/") && !urlPath.startsWith("/")) { + urlBuilder.append("/") + } else if (defaultApplicationBaseUrl.endsWith("/") && urlPath.startsWith("/")) { + urlPath = urlPath.substring(1) + } + urlPath = urlBuilder.append(urlPath).toString() + } + + // https://github.com/matomo-org/matomo-sdk-android/issues/92 + defaultTrackMe[QueryParams.URL_PATH] = urlPath + trackMe[QueryParams.URL_PATH] = urlPath + } + + fun track(givenTrackMe: TrackMe?): Tracker { + var trackMe = givenTrackMe + synchronized(trackingLock) { + val newSession = System.currentTimeMillis() - sessionStartTime > sessionTimeout + if (newSession) { + sessionStartTime = System.currentTimeMillis() + injectInitialParams(trackMe) + } + + injectBaseParams(trackMe) + + for (callback in mTrackingCallbacks) { + trackMe = callback.onTrack(trackMe) + if (trackMe == null) { + Timber.tag(TAG).d("Tracking aborted by %s", callback) + return this + } + } + + lastEventX = trackMe + if (!localOptOut) { + localDispatcher.submit(trackMe) + Timber.tag(TAG).d("Event added to the queue: %s", trackMe) + } else { + Timber.tag(TAG).d("Event omitted due to opt out: %s", trackMe) + } + return this + } + } + + val preferences: SharedPreferences? + get() { + if (mPreferences == null) + mPreferences = matomo.getTrackerPreferences(this) + return mPreferences + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + val tracker = other as Tracker + + if (siteId != tracker.siteId) return false + if (aPIUrl != tracker.aPIUrl) return false + return name == tracker.name + } + + override fun hashCode(): Int { + var result = aPIUrl.hashCode() + result = 31 * result + siteId + result = 31 * result + name.hashCode() + return result + } + + var dryRunTarget: List? + /** + * If we are in dry-run mode then this will return a datastructure. + * + * @return a datastructure or null + */ + get() = localDispatcher.dryRunTarget + /** + * Set a data structure here to put the Dispatcher into dry-run-mode. + * Data will be processed but at the last step just stored instead of transmitted. + * Set it to null to disable it. + * + * @param dryRunTarget a data structure the data should be passed into + */ + set(dryRunTarget) { + localDispatcher.dryRunTarget = dryRunTarget + } + + interface Callback { + /** + * This method will be called after parameter injection and before transmission within [Tracker.track]. + * Blocking within this method will block tracking. + * + * @param trackMe The `TrackMe` that was passed to [Tracker.track] after all data has been injected. + * @return The `TrackMe` that will be send, returning NULL here will abort transmission. + */ + fun onTrack(trackMe: TrackMe?): TrackMe? + } + + companion object { + private val TAG = Matomo.tag(Tracker::class.java) + + // Matomo default parameter values + private const val DEFAULT_UNKNOWN_VALUE = "unknown" + private const val DEFAULT_TRUE_VALUE = "1" + private const val DEFAULT_RECORD_VALUE = DEFAULT_TRUE_VALUE + private const val DEFAULT_API_VERSION_VALUE = "1" + + // Sharedpreference keys for persisted values + const val PREF_KEY_TRACKER_OPTOUT: String = "tracker.optout" + const val PREF_KEY_TRACKER_USERID: String = "tracker.userid" + const val PREF_KEY_TRACKER_VISITORID: String = "tracker.visitorid" + const val PREF_KEY_TRACKER_FIRSTVISIT: String = "tracker.firstvisit" + const val PREF_KEY_TRACKER_VISITCOUNT: String = "tracker.visitcount" + const val PREF_KEY_TRACKER_PREVIOUSVISIT: String = "tracker.previousvisit" + protected const val PREF_KEY_OFFLINE_CACHE_AGE: String = "tracker.cache.age" + protected const val PREF_KEY_OFFLINE_CACHE_SIZE: String = "tracker.cache.size" + const val PREF_KEY_DISPATCHER_MODE: String = "tracker.dispatcher.mode" + + private val VALID_URLS: Pattern = Pattern.compile("^(\\w+)(?:://)(.+?)$") + + private val PATTERN_VISITOR_ID: Pattern = Pattern.compile("^[0-9a-f]{16}$") + + fun makeRandomVisitorId(): String { + return UUID.randomUUID().toString().replace("-".toRegex(), "").substring(0, 16) + } + } +} From 68915264af78ac1744b15c9a3a2dbf6ec70b7d2f Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Thu, 11 Jul 2024 07:30:25 +0200 Subject: [PATCH 2/2] Test in Kotlin --- .../test/java/org/matomo/sdk/TrackerTest.java | 878 ------------------ .../test/java/org/matomo/sdk/TrackerTest.kt | 870 +++++++++++++++++ 2 files changed, 870 insertions(+), 878 deletions(-) delete mode 100644 tracker/src/test/java/org/matomo/sdk/TrackerTest.java create mode 100644 tracker/src/test/java/org/matomo/sdk/TrackerTest.kt diff --git a/tracker/src/test/java/org/matomo/sdk/TrackerTest.java b/tracker/src/test/java/org/matomo/sdk/TrackerTest.java deleted file mode 100644 index 711e260c..00000000 --- a/tracker/src/test/java/org/matomo/sdk/TrackerTest.java +++ /dev/null @@ -1,878 +0,0 @@ -package org.matomo.sdk; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.junit.Before; -import org.junit.Test; -import org.matomo.sdk.dispatcher.DispatchMode; -import org.matomo.sdk.dispatcher.Dispatcher; -import org.matomo.sdk.dispatcher.DispatcherFactory; -import org.matomo.sdk.extra.TrackHelper; -import org.matomo.sdk.tools.DeviceHelper; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.prefs.Preferences; - -import testhelpers.TestHelper; -import testhelpers.TestPreferences; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.matomo.sdk.QueryParams.FIRST_VISIT_TIMESTAMP; -import static org.matomo.sdk.QueryParams.PREVIOUS_VISIT_TIMESTAMP; -import static org.matomo.sdk.QueryParams.SESSION_START; -import static org.matomo.sdk.QueryParams.TOTAL_NUMBER_OF_VISITS; -import static org.matomo.sdk.QueryParams.VISITOR_ID; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import androidx.annotation.Nullable; - - -@SuppressWarnings("PointlessArithmeticExpression") -public class TrackerTest { - ArgumentCaptor mCaptor = ArgumentCaptor.forClass(TrackMe.class); - @Mock Matomo mMatomo; - @Mock Context mContext; - @Mock Dispatcher mDispatcher; - @Mock DispatcherFactory mDispatcherFactory; - @Mock DeviceHelper mDeviceHelper; - SharedPreferences mTrackerPreferences = new TestPreferences(); - SharedPreferences mPreferences = new TestPreferences(); - @Mock TrackerBuilder mTrackerBuilder; - - @Before - public void setup() { - MockitoAnnotations.openMocks(this); - when(mMatomo.getContext()).thenReturn(mContext); - when(mMatomo.getTrackerPreferences(any(Tracker.class))).thenReturn(mTrackerPreferences); - when(mMatomo.getPreferences()).thenReturn(mPreferences); - when(mMatomo.getDispatcherFactory()).thenReturn(mDispatcherFactory); - when(mDispatcherFactory.build(any(Tracker.class))).thenReturn(mDispatcher); - when(mMatomo.getDeviceHelper()).thenReturn(mDeviceHelper); - when(mDeviceHelper.getResolution()).thenReturn(new int[]{480, 800}); - when(mDeviceHelper.getUserAgent()).thenReturn("aUserAgent"); - when(mDeviceHelper.getUserLanguage()).thenReturn("en"); - - String mApiUrl = "http://example.com"; - when(mTrackerBuilder.getApiUrl()).thenReturn(mApiUrl); - int mSiteId = 11; - when(mTrackerBuilder.getSiteId()).thenReturn(mSiteId); - String mTrackerName = "Default Tracker"; - when(mTrackerBuilder.getTrackerName()).thenReturn(mTrackerName); - when(mTrackerBuilder.getApplicationBaseUrl()).thenReturn("http://this.is.our.package/"); - - mTrackerPreferences.edit().clear(); - mPreferences.edit().clear(); - } - - @Test - public void testGetPreferences() { - Tracker tracker1 = new Tracker(mMatomo, mTrackerBuilder); - verify(mMatomo).getTrackerPreferences(tracker1); - } - - /** - * https://github.com/matomo-org/matomo-sdk-android/issues/92 - */ - @Test - public void testLastScreenUrl() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - - tracker.track(new TrackMe()); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals("http://this.is.our.package/", mCaptor.getValue().get(QueryParams.URL_PATH)); - - tracker.track(new TrackMe().set(QueryParams.URL_PATH, "http://some.thing.com/foo/bar")); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals("http://some.thing.com/foo/bar", mCaptor.getValue().get(QueryParams.URL_PATH)); - - tracker.track(new TrackMe().set(QueryParams.URL_PATH, "http://some.other/thing")); - verify(mDispatcher, times(3)).submit(mCaptor.capture()); - assertEquals("http://some.other/thing", mCaptor.getValue().get(QueryParams.URL_PATH)); - - tracker.track(new TrackMe()); - verify(mDispatcher, times(4)).submit(mCaptor.capture()); - assertEquals("http://some.other/thing", mCaptor.getValue().get(QueryParams.URL_PATH)); - - tracker.track(new TrackMe().set(QueryParams.URL_PATH, "thang")); - verify(mDispatcher, times(5)).submit(mCaptor.capture()); - assertEquals("http://this.is.our.package/thang", mCaptor.getValue().get(QueryParams.URL_PATH)); - } - - @Test - public void testSetDispatchInterval() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchInterval(1); - verify(mDispatcher).setDispatchInterval(1); - tracker.getDispatchInterval(); - verify(mDispatcher).getDispatchInterval(); - } - - @Test - public void testSetDispatchTimeout() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - int timeout = 1337; - tracker.setDispatchTimeout(timeout); - verify(mDispatcher).setConnectionTimeOut(timeout); - tracker.getDispatchTimeout(); - verify(mDispatcher).getConnectionTimeOut(); - } - - @Test - public void testGetOfflineCacheAge_defaultValue() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(24 * 60 * 60 * 1000, tracker.getOfflineCacheAge()); - } - - @Test - public void testSetOfflineCacheAge() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setOfflineCacheAge(80085); - assertEquals(80085, tracker.getOfflineCacheAge()); - } - - @Test - public void testGetOfflineCacheSize_defaultValue() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(4 * 1024 * 1024, tracker.getOfflineCacheSize()); - } - - @Test - public void testSetOfflineCacheSize() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setOfflineCacheSize(16 * 1000 * 1000); - assertEquals(16 * 1000 * 1000, tracker.getOfflineCacheSize()); - } - - @Test - public void testDispatchMode_default() { - mTrackerPreferences.edit().clear(); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(DispatchMode.ALWAYS, tracker.getDispatchMode()); - verify(mDispatcher, times(1)).setDispatchMode(DispatchMode.ALWAYS); - } - - @Test - public void testDispatchMode_change() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchMode(DispatchMode.WIFI_ONLY); - assertEquals(DispatchMode.WIFI_ONLY, tracker.getDispatchMode()); - verify(mDispatcher, times(1)).setDispatchMode(DispatchMode.WIFI_ONLY); - } - - @Test - public void testDispatchMode_fallback() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.getPreferences().edit().putString(Tracker.PREF_KEY_DISPATCHER_MODE, "lol").apply(); - assertEquals(DispatchMode.ALWAYS, tracker.getDispatchMode()); - verify(mDispatcher, times(1)).setDispatchMode(DispatchMode.ALWAYS); - } - - @Test - public void testSetDispatchMode_propagation() { - mTrackerPreferences.edit().clear(); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - verify(mDispatcher, times(1)).setDispatchMode(any()); - } - - @Test - public void testSetDispatchMode_propagation_change() { - mTrackerPreferences.edit().clear(); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchMode(DispatchMode.WIFI_ONLY); - tracker.setDispatchMode(DispatchMode.WIFI_ONLY); - assertEquals(DispatchMode.WIFI_ONLY, tracker.getDispatchMode()); - verify(mDispatcher, times(2)).setDispatchMode(DispatchMode.WIFI_ONLY); - verify(mDispatcher, times(3)).setDispatchMode(any()); - } - - @Test - public void testSetDispatchMode_exception() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchMode(DispatchMode.WIFI_ONLY); // This is persisted - tracker.setDispatchMode(DispatchMode.EXCEPTION); // This isn't - assertEquals(DispatchMode.EXCEPTION, tracker.getDispatchMode()); - verify(mDispatcher, times(1)).setDispatchMode(DispatchMode.EXCEPTION); - - tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(DispatchMode.WIFI_ONLY, tracker.getDispatchMode()); - } - - @Test - public void testsetDispatchGzip() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchGzipped(true); - verify(mDispatcher).setDispatchGzipped(true); - } - - @Test - public void testOptOut_set() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setOptOut(true); - assertTrue(tracker.isOptOut()); - tracker.setOptOut(false); - assertFalse(tracker.isOptOut()); - } - - @Test - public void testOptOut_init() { - mTrackerPreferences.edit().putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, false).apply(); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertFalse(tracker.isOptOut()); - mTrackerPreferences.edit().putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, true).apply(); - tracker = new Tracker(mMatomo, mTrackerBuilder); - assertTrue(tracker.isOptOut()); - } - - @Test - public void testDispatch() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.dispatch(); - verify(mDispatcher).forceDispatch(); - tracker.dispatch(); - verify(mDispatcher, times(2)).forceDispatch(); - } - - @Test - public void testDispatch_optOut() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setOptOut(true); - tracker.dispatch(); - verify(mDispatcher, never()).forceDispatch(); - tracker.setOptOut(false); - tracker.dispatch(); - verify(mDispatcher).forceDispatch(); - } - - @Test - public void testGetSiteId() { - when(mTrackerBuilder.getSiteId()).thenReturn(11); - assertEquals(new Tracker(mMatomo, mTrackerBuilder).getSiteId(), 11); - } - - @Test - public void testGetMatomo() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(mMatomo, tracker.getMatomo()); - } - - @Test - public void testSetURL() { - when(mTrackerBuilder.getApplicationBaseUrl()).thenReturn("http://test.com/"); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - - TrackMe trackMe = new TrackMe(); - tracker.track(trackMe); - assertEquals("http://test.com/", trackMe.get(QueryParams.URL_PATH)); - - trackMe.set(QueryParams.URL_PATH, "me"); - tracker.track(trackMe); - assertEquals("http://test.com/me", trackMe.get(QueryParams.URL_PATH)); - - // override protocol - trackMe.set(QueryParams.URL_PATH, "https://my.com/secure"); - tracker.track(trackMe); - assertEquals("https://my.com/secure", trackMe.get(QueryParams.URL_PATH)); - } - - @Test - public void testApplicationDomain() { - when(mTrackerBuilder.getApplicationBaseUrl()).thenReturn("http://my-domain.com"); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - - TrackHelper.track().screen("test/test").title("Test title").with(tracker); - verify(mDispatcher).submit(mCaptor.capture()); - validateDefaultQuery(mCaptor.getValue()); - assertEquals("http://my-domain.com/test/test", mCaptor.getValue().get(QueryParams.URL_PATH)); - } - - @Test(expected = IllegalArgumentException.class) - public void testVisitorId_invalid_short() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String tooShortVisitorId = "0123456789ab"; - tracker.setVisitorId(tooShortVisitorId); - assertNotEquals(tooShortVisitorId, tracker.getVisitorId()); - } - - @Test(expected = IllegalArgumentException.class) - public void testVisitorId_invalid_long() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String tooLongVisitorId = "0123456789abcdefghi"; - tracker.setVisitorId(tooLongVisitorId); - assertNotEquals(tooLongVisitorId, tracker.getVisitorId()); - } - - @Test(expected = IllegalArgumentException.class) - public void testVisitorId_invalid_charset() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String invalidCharacterVisitorId = "01234-6789-ghief"; - tracker.setVisitorId(invalidCharacterVisitorId); - assertNotEquals(invalidCharacterVisitorId, tracker.getVisitorId()); - } - - @Test - public void testVisitorId_init() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertThat(tracker.getVisitorId(), is(notNullValue())); - } - - @Test - public void testVisitorId_restore() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertThat(tracker.getVisitorId(), is(notNullValue())); - String visitorId = tracker.getVisitorId(); - - tracker = new Tracker(mMatomo, mTrackerBuilder); - assertThat(tracker.getVisitorId(), is(visitorId)); - } - - @Test - public void testVisitorId_dispatch() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String visitorId = "0123456789abcdef"; - tracker.setVisitorId(visitorId); - assertEquals(visitorId, tracker.getVisitorId()); - - tracker.track(new TrackMe()); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals(visitorId, mCaptor.getValue().get(QueryParams.VISITOR_ID)); - - tracker.track(new TrackMe()); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals(visitorId, mCaptor.getValue().get(QueryParams.VISITOR_ID)); - } - - @Test - public void testUserID_init() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertNull(tracker.getDefaultTrackMe().get(QueryParams.USER_ID)); - assertNull(tracker.getUserId()); - } - - @Test - public void testUserID_restore() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertNull(tracker.getUserId()); - tracker.setUserId("cake"); - assertThat(tracker.getUserId(), is("cake")); - - tracker = new Tracker(mMatomo, mTrackerBuilder); - assertThat(tracker.getUserId(), is("cake")); - assertThat(tracker.getDefaultTrackMe().get(QueryParams.USER_ID), is("cake")); - } - - @Test - public void testUserID_invalid() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertNull(tracker.getUserId()); - - tracker.setUserId("test"); - assertEquals(tracker.getUserId(), "test"); - - tracker.setUserId(""); - assertEquals(tracker.getUserId(), "test"); - - tracker.setUserId(null); - assertNull(tracker.getUserId()); - - String uuid = UUID.randomUUID().toString(); - tracker.setUserId(uuid); - assertEquals(uuid, tracker.getUserId()); - assertEquals(uuid, tracker.getUserId()); - } - - @Test - public void testUserID_dispatch() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String uuid = UUID.randomUUID().toString(); - tracker.setUserId(uuid); - - tracker.track(new TrackMe()); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals(uuid, mCaptor.getValue().get(QueryParams.USER_ID)); - - tracker.track(new TrackMe()); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals(uuid, mCaptor.getValue().get(QueryParams.USER_ID)); - } - - @Test - public void testGetResolution() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackMe trackMe = new TrackMe(); - tracker.track(trackMe); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals("480x800", mCaptor.getValue().get(QueryParams.SCREEN_RESOLUTION)); - } - - @Test - public void testSetNewSession() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackMe trackMe = new TrackMe(); - tracker.track(trackMe); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals("1", mCaptor.getValue().get(QueryParams.SESSION_START)); - - tracker.startNewSession(); - TrackHelper.track().screen("").with(tracker); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals("1", mCaptor.getValue().get(QueryParams.SESSION_START)); - } - - @Test - public void testSetNewSessionRaceCondition() { - for (int retry = 0; retry < 5; retry++) { - final List trackMes = Collections.synchronizedList(new ArrayList<>()); - doAnswer(invocation -> { - trackMes.add(invocation.getArgument(0)); - return null; - }).when(mDispatcher).submit(any(TrackMe.class)); - final Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setDispatchInterval(0); - int count = 20; - for (int i = 0; i < count; i++) { - new Thread(() -> { - TestHelper.sleep(10); - TrackHelper.track().screen("Test").with(tracker); - }).start(); - } - TestHelper.sleep(500); - assertEquals(count, trackMes.size()); - int found = 0; - for (TrackMe trackMe : trackMes) { - if (trackMe.get(QueryParams.SESSION_START) != null) found++; - } - assertEquals(1, found); - } - } - - @Test - public void testSetSessionTimeout() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setSessionTimeout(10000); - - TrackHelper.track().screen("test1").with(tracker); - assertThat(tracker.getLastEventX().get(QueryParams.SESSION_START), notNullValue()); - - TrackHelper.track().screen("test2").with(tracker); - assertThat(tracker.getLastEventX().get(QueryParams.SESSION_START), nullValue()); - - tracker.setSessionTimeout(0); - TestHelper.sleep(1); - TrackHelper.track().screen("test3").with(tracker); - assertThat(tracker.getLastEventX().get(QueryParams.SESSION_START), notNullValue()); - - tracker.setSessionTimeout(10000); - assertEquals(tracker.getSessionTimeout(), 10000); - TrackHelper.track().screen("test3").with(tracker); - assertThat(tracker.getLastEventX().get(QueryParams.SESSION_START), nullValue()); - } - - @Test - public void testCheckSessionTimeout() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - tracker.setSessionTimeout(0); - TrackHelper.track().screen("test").with(tracker); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals("1", mCaptor.getValue().get(QueryParams.SESSION_START)); - TestHelper.sleep(1); - TrackHelper.track().screen("test").with(tracker); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals("1", mCaptor.getValue().get(QueryParams.SESSION_START)); - tracker.setSessionTimeout(60000); - TrackHelper.track().screen("test").with(tracker); - verify(mDispatcher, times(3)).submit(mCaptor.capture()); - assertNull(mCaptor.getValue().get(SESSION_START)); - } - - @Test - public void testReset() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - Tracker.Callback callback = new Tracker.Callback() { - @Nullable - @Override - public TrackMe onTrack(TrackMe trackMe) { - return null; - } - }; - tracker.addTrackingCallback(callback); - tracker.getDefaultTrackMe().set(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES, "custom1"); - tracker.getDefaultTrackMe().set(QueryParams.CAMPAIGN_NAME, "campaign_name"); - tracker.getDefaultTrackMe().set(QueryParams.CAMPAIGN_KEYWORD, "campaign_keyword"); - - TrackHelper.track().screen("test1").with(tracker); - tracker.startNewSession(); - TrackHelper.track().screen("test2").with(tracker); - - String preResetDefaultVisitorId = tracker.getDefaultTrackMe().get(VISITOR_ID); - String preResetFirstVisitTimestamp = tracker.getDefaultTrackMe().get(FIRST_VISIT_TIMESTAMP); - String preResetTotalNumberOfVisits = tracker.getDefaultTrackMe().get(TOTAL_NUMBER_OF_VISITS); - String preResetPreviousVisitTimestamp = tracker.getDefaultTrackMe().get(PREVIOUS_VISIT_TIMESTAMP); - - tracker.reset(); - - SharedPreferences prefs = tracker.getPreferences(); - - assertNotEquals(preResetDefaultVisitorId, tracker.getVisitorId()); - assertNotEquals(preResetDefaultVisitorId, tracker.getDefaultTrackMe().get(VISITOR_ID)); - assertNotEquals(preResetDefaultVisitorId, prefs.getString(Tracker.PREF_KEY_TRACKER_VISITORID, "")); - - assertNotEquals(preResetFirstVisitTimestamp, tracker.getDefaultTrackMe().get(FIRST_VISIT_TIMESTAMP)); - assertNotEquals(Long.parseLong(preResetFirstVisitTimestamp), prefs.getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1)); - - assertNotEquals(preResetPreviousVisitTimestamp, tracker.getDefaultTrackMe().get(PREVIOUS_VISIT_TIMESTAMP)); - assertNotEquals(Long.parseLong(preResetPreviousVisitTimestamp), prefs.getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1)); - - assertNotEquals(preResetTotalNumberOfVisits, tracker.getDefaultTrackMe().get(TOTAL_NUMBER_OF_VISITS)); - assertNotEquals(preResetTotalNumberOfVisits, prefs.getString(Tracker.PREF_KEY_TRACKER_VISITCOUNT, "")); - - assertNull(tracker.getDefaultTrackMe().get(QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES)); - assertNull(tracker.getDefaultTrackMe().get(QueryParams.CAMPAIGN_NAME)); - assertNull(tracker.getDefaultTrackMe().get(QueryParams.CAMPAIGN_KEYWORD)); - } - - @Test - public void testTrackerEquals() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackerBuilder builder2 = mock(TrackerBuilder.class); - when(builder2.getApiUrl()).thenReturn("http://localhost"); - when(builder2.getSiteId()).thenReturn(100); - when(builder2.getTrackerName()).thenReturn("Default Tracker"); - Tracker tracker2 = new Tracker(mMatomo, builder2); - - TrackerBuilder builder3 = mock(TrackerBuilder.class); - when(builder3.getApiUrl()).thenReturn("http://example.com"); - when(builder3.getSiteId()).thenReturn(11); - when(builder3.getTrackerName()).thenReturn("Default Tracker"); - Tracker tracker3 = new Tracker(mMatomo, builder3); - - assertNotNull(tracker); - assertNotEquals(tracker, tracker2); - assertEquals(tracker, tracker3); - } - - @Test - public void testTrackerHashCode() { - assertEquals(new Tracker(mMatomo, mTrackerBuilder).hashCode(), new Tracker(mMatomo, mTrackerBuilder).hashCode()); - } - - @Test - public void testUrlPathCorrection() { - when(mTrackerBuilder.getApplicationBaseUrl()).thenReturn("https://package/"); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - String[] paths = new String[]{null, "", "/",}; - for (String path : paths) { - TrackMe trackMe = new TrackMe(); - trackMe.set(QueryParams.URL_PATH, path); - tracker.track(trackMe); - assertEquals("https://package/", trackMe.get(QueryParams.URL_PATH)); - } - } - - @Test - public void testSetUserAgent() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackMe trackMe = new TrackMe(); - tracker.track(trackMe); - assertEquals("aUserAgent", trackMe.get(QueryParams.USER_AGENT)); - - // Custom developer specified useragent - trackMe = new TrackMe(); - String customUserAgent = "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0"; - trackMe.set(QueryParams.USER_AGENT, customUserAgent); - tracker.track(trackMe); - assertEquals(customUserAgent, trackMe.get(QueryParams.USER_AGENT)); - - // Modifying default TrackMe, no USER_AGENT - trackMe = new TrackMe(); - tracker.getDefaultTrackMe().set(QueryParams.USER_AGENT, null); - tracker.track(trackMe); - assertNull(trackMe.get(QueryParams.USER_AGENT)); - } - - @Test - public void testFirstVisitTimeStamp() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(-1, tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1)); - - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher).submit(mCaptor.capture()); - TrackMe trackMe1 = mCaptor.getValue(); - TestHelper.sleep(10); - // make sure we are tracking in seconds - assertTrue(Math.abs((System.currentTimeMillis() / 1000) - Long.parseLong(trackMe1.get(FIRST_VISIT_TIMESTAMP))) < 2); - - tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - TrackMe trackMe2 = mCaptor.getValue(); - assertEquals(Long.parseLong(trackMe1.get(FIRST_VISIT_TIMESTAMP)), Long.parseLong(trackMe2.get(FIRST_VISIT_TIMESTAMP))); - assertEquals(tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1), Long.parseLong(trackMe1.get(FIRST_VISIT_TIMESTAMP))); - } - - @Test - public void testTotalVisitCount() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(-1, tracker.getPreferences().getInt(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1)); - assertNull(tracker.getDefaultTrackMe().get(QueryParams.TOTAL_NUMBER_OF_VISITS)); - - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher).submit(mCaptor.capture()); - assertEquals(1, Integer.parseInt(mCaptor.getValue().get(QueryParams.TOTAL_NUMBER_OF_VISITS))); - - tracker = new Tracker(mMatomo, mTrackerBuilder); - assertEquals(1, tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1)); - assertNull(tracker.getDefaultTrackMe().get(QueryParams.TOTAL_NUMBER_OF_VISITS)); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - assertEquals(2, Integer.parseInt(mCaptor.getValue().get(QueryParams.TOTAL_NUMBER_OF_VISITS))); - assertEquals(2, tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1)); - } - - @Test - public void testVisitCountMultipleThreads() throws Exception { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - int threadCount = 1000; - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount; i++) { - new Thread(() -> { - TestHelper.sleep(new Random().nextInt(20 - 0) + 0); - TrackHelper.track().event("TestCategory", "TestAction").with(new Tracker(mMatomo, mTrackerBuilder)); - countDownLatch.countDown(); - }).start(); - } - countDownLatch.await(); - assertEquals(threadCount, mTrackerPreferences.getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, 0)); - } - - @Test - public void testSessionStartRaceCondition() throws Exception { - final List trackMes = Collections.synchronizedList(new ArrayList<>()); - doAnswer(invocation -> { - trackMes.add(invocation.getArgument(0)); - return null; - }).when(mDispatcher).submit(any(TrackMe.class)); - when(mDispatcher.getConnectionTimeOut()).thenReturn(1000); - for (int i = 0; i < 1000; i++) { - trackMes.clear(); - final Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - final CountDownLatch countDownLatch = new CountDownLatch(10); - for (int j = 0; j < 10; j++) { - new Thread(() -> { - try { - TestHelper.sleep(new Random().nextInt(4 - 0) + 0); - TrackMe trackMe = new TrackMe() - .set(QueryParams.EVENT_ACTION, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_CATEGORY, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_NAME, UUID.randomUUID().toString()) - .set(QueryParams.EVENT_VALUE, 1); - tracker.track(trackMe); - countDownLatch.countDown(); - } catch (Exception e) { - e.printStackTrace(); - fail(); - } - }).start(); - } - countDownLatch.await(); - for (TrackMe out : trackMes) { - if (trackMes.indexOf(out) == 0) { - assertNotNull(i + "#" + out.toMap().size(), out.get(QueryParams.LANGUAGE)); - assertNotNull(out.get(FIRST_VISIT_TIMESTAMP)); - assertNotNull(out.get(SESSION_START)); - } else { - assertNull(out.get(FIRST_VISIT_TIMESTAMP)); - assertNull(out.get(SESSION_START)); - } - } - } - } - - @Test - public void testFirstVisitMultipleThreads() throws Exception { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - int threadCount = 100; - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - final List firstVisitTimes = Collections.synchronizedList(new ArrayList<>()); - for (int i = 0; i < threadCount; i++) { - new Thread(() -> { - TestHelper.sleep(new Random().nextInt(20 - 0) + 0); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - long firstVisit = Long.parseLong(tracker.getDefaultTrackMe().get(FIRST_VISIT_TIMESTAMP)); - firstVisitTimes.add(firstVisit); - countDownLatch.countDown(); - }).start(); - } - countDownLatch.await(); - for (Long firstVisit : firstVisitTimes) assertEquals(firstVisitTimes.get(0), firstVisit); - } - - @Test - public void testPreviousVisits() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - final List previousVisitTimes = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - - - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - String previousVisit = tracker.getDefaultTrackMe().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP); - if (previousVisit != null) - previousVisitTimes.add(Long.parseLong(previousVisit)); - TestHelper.sleep(1010); - - } - assertFalse(previousVisitTimes.contains(0L)); - long lastTime = 0L; - for (Long time : previousVisitTimes) { - assertTrue(lastTime < time); - lastTime = time; - } - } - - @Test - public void testPreviousVisit() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - // No timestamp yet - assertEquals(-1, tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1)); - tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher).submit(mCaptor.capture()); - long _startTime = System.currentTimeMillis() / 1000; - // There was no previous visit - assertNull(mCaptor.getValue().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP)); - TestHelper.sleep(1000); - - // After the first visit we now have a timestamp for the previous visit - long previousVisit = tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1); - assertTrue(previousVisit - _startTime < 2000); - assertNotEquals(-1, previousVisit); - tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher, times(2)).submit(mCaptor.capture()); - // Transmitted timestamp is the one from the first visit visit - assertEquals(previousVisit, Long.parseLong(mCaptor.getValue().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP))); - - TestHelper.sleep(1000); - tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher, times(3)).submit(mCaptor.capture()); - // Now the timestamp changed as this is the 3rd visit. - assertNotEquals(previousVisit, Long.parseLong(mCaptor.getValue().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP))); - TestHelper.sleep(1000); - - previousVisit = tracker.getPreferences().getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1); - tracker = new Tracker(mMatomo, mTrackerBuilder); - TrackHelper.track().event("TestCategory", "TestAction").with(tracker); - verify(mDispatcher, times(4)).submit(mCaptor.capture()); - // Just make sure the timestamp in the 4th visit is from the 3rd visit - assertEquals(previousVisit, Long.parseLong(mCaptor.getValue().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP))); - - // Test setting a custom timestamp - TrackMe custom = new TrackMe(); - custom.set(QueryParams.PREVIOUS_VISIT_TIMESTAMP, 1000L); - tracker.track(custom); - verify(mDispatcher, times(5)).submit(mCaptor.capture()); - assertEquals(1000L, Long.parseLong(mCaptor.getValue().get(QueryParams.PREVIOUS_VISIT_TIMESTAMP))); - } - - @Test - public void testTrackingCallback() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - Tracker.Callback callback = mock(Tracker.Callback.class); - - TrackMe pre = new TrackMe(); - tracker.track(pre); - verify(mDispatcher).submit(pre); - verify(callback, never()).onTrack(mCaptor.capture()); - - reset(mDispatcher, callback); - tracker.addTrackingCallback(callback); - tracker.track(new TrackMe()); - verify(callback).onTrack(mCaptor.capture()); - verify(mDispatcher, never()).submit(any()); - - reset(mDispatcher, callback); - TrackMe orig = new TrackMe(); - TrackMe replaced = new TrackMe().set("some", "thing"); - when(callback.onTrack(orig)).thenReturn(replaced); - tracker.track(orig); - verify(callback).onTrack(orig); - verify(mDispatcher).submit(replaced); - - reset(mDispatcher, callback); - TrackMe post = new TrackMe(); - tracker.removeTrackingCallback(callback); - tracker.track(post); - verify(callback, never()).onTrack(any()); - verify(mDispatcher).submit(post); - } - - @Test - public void testTrackingCallbacks() { - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - Tracker.Callback callback1 = mock(Tracker.Callback.class); - Tracker.Callback callback2 = mock(Tracker.Callback.class); - - TrackMe orig = new TrackMe(); - TrackMe replaced = new TrackMe(); - when(callback1.onTrack(orig)).thenReturn(replaced); - when(callback2.onTrack(replaced)).thenReturn(replaced); - - tracker.addTrackingCallback(callback1); - tracker.addTrackingCallback(callback1); - tracker.addTrackingCallback(callback2); - tracker.track(orig); - verify(callback1).onTrack(orig); - verify(callback2).onTrack(replaced); - verify(mDispatcher).submit(replaced); - - tracker.removeTrackingCallback(callback1); - tracker.track(orig); - - verify(callback2).onTrack(orig); - } - - private static void validateDefaultQuery(TrackMe params) { - assertEquals(params.get(QueryParams.SITE_ID), "11"); - assertEquals(params.get(QueryParams.RECORD), "1"); - assertEquals(params.get(QueryParams.SEND_IMAGE), "0"); - assertEquals(params.get(QueryParams.VISITOR_ID).length(), 16); - assertTrue(params.get(QueryParams.URL_PATH).startsWith("http://")); - assertTrue(Integer.parseInt(params.get(QueryParams.RANDOM_NUMBER)) > 0); - } - - @Test - public void testCustomDispatcherFactory() { - Dispatcher dispatcher = mock(Dispatcher.class); - DispatcherFactory factory = mock(DispatcherFactory.class); - when(factory.build(any(Tracker.class))).thenReturn(dispatcher); - when(mMatomo.getDispatcherFactory()).thenReturn(factory); - Tracker tracker = new Tracker(mMatomo, mTrackerBuilder); - verify(factory).build(tracker); - } -} diff --git a/tracker/src/test/java/org/matomo/sdk/TrackerTest.kt b/tracker/src/test/java/org/matomo/sdk/TrackerTest.kt new file mode 100644 index 00000000..3f2a1771 --- /dev/null +++ b/tracker/src/test/java/org/matomo/sdk/TrackerTest.kt @@ -0,0 +1,870 @@ +package org.matomo.sdk + +import android.content.Context +import android.content.SharedPreferences +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.core.Is +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.matomo.sdk.dispatcher.DispatchMode +import org.matomo.sdk.dispatcher.Dispatcher +import org.matomo.sdk.dispatcher.DispatcherFactory +import org.matomo.sdk.extra.TrackHelper +import org.matomo.sdk.tools.DeviceHelper +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.invocation.InvocationOnMock +import testhelpers.TestHelper +import testhelpers.TestPreferences +import java.util.Collections +import java.util.Random +import java.util.UUID +import java.util.concurrent.CountDownLatch +import kotlin.math.abs + +class TrackerTest { + private var argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(TrackMe::class.java) + + @Mock + var matomo: Matomo? = null + + @Mock + var context: Context? = null + + @Mock + var dispatcher: Dispatcher? = null + + @Mock + var dispatcherFactory: DispatcherFactory? = null + + @Mock + var deviceHelper: DeviceHelper? = null + var trackerPreferences: SharedPreferences = TestPreferences() + var preferences: SharedPreferences = TestPreferences() + + @Mock + var mTrackerBuilder: TrackerBuilder? = null + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + Mockito.`when`(matomo!!.context).thenReturn(context) + Mockito.`when`(matomo!!.getTrackerPreferences(ArgumentMatchers.any(Tracker::class.java))).thenReturn(trackerPreferences) + Mockito.`when`(matomo!!.preferences).thenReturn(preferences) + Mockito.`when`(matomo!!.dispatcherFactory).thenReturn(dispatcherFactory) + Mockito.`when`(dispatcherFactory!!.build(ArgumentMatchers.any(Tracker::class.java))).thenReturn(dispatcher) + Mockito.`when`(matomo!!.deviceHelper).thenReturn(deviceHelper) + Mockito.`when`(deviceHelper!!.resolution).thenReturn(intArrayOf(480, 800)) + Mockito.`when`(deviceHelper!!.userAgent).thenReturn("aUserAgent") + Mockito.`when`(deviceHelper!!.userLanguage).thenReturn("en") + + val mApiUrl = "http://example.com" + Mockito.`when`(mTrackerBuilder!!.apiUrl).thenReturn(mApiUrl) + val mSiteId = 11 + Mockito.`when`(mTrackerBuilder!!.siteId).thenReturn(mSiteId) + val mTrackerName = "Default Tracker" + Mockito.`when`(mTrackerBuilder!!.trackerName).thenReturn(mTrackerName) + Mockito.`when`(mTrackerBuilder!!.applicationBaseUrl).thenReturn("http://this.is.our.package/") + + trackerPreferences.edit().clear() + preferences.edit().clear() + } + + @Test + fun testGetPreferences() { + val tracker1 = Tracker(matomo!!, mTrackerBuilder!!) + Mockito.verify(matomo)?.getTrackerPreferences(tracker1) + } + + /** + * https://github.com/matomo-org/matomo-sdk-android/issues/92 + */ + @Test + fun testLastScreenUrl() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals("http://this.is.our.package/", argumentCaptor.value[QueryParams.URL_PATH]) + + tracker.track(TrackMe().set(QueryParams.URL_PATH, "http://some.thing.com/foo/bar")) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals("http://some.thing.com/foo/bar", argumentCaptor.value[QueryParams.URL_PATH]) + + tracker.track(TrackMe().set(QueryParams.URL_PATH, "http://some.other/thing")) + Mockito.verify(dispatcher, Mockito.times(3))?.submit(argumentCaptor.capture()) + Assert.assertEquals("http://some.other/thing", argumentCaptor.value[QueryParams.URL_PATH]) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher, Mockito.times(4))?.submit(argumentCaptor.capture()) + Assert.assertEquals("http://some.other/thing", argumentCaptor.value[QueryParams.URL_PATH]) + + tracker.track(TrackMe().set(QueryParams.URL_PATH, "thang")) + Mockito.verify(dispatcher, Mockito.times(5))?.submit(argumentCaptor.capture()) + Assert.assertEquals("http://this.is.our.package/thang", argumentCaptor.value[QueryParams.URL_PATH]) + } + + @Test + fun testSetDispatchInterval() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.setDispatchInterval(1) + Mockito.verify(dispatcher)?.dispatchInterval = 1 + tracker.dispatchInterval + Mockito.verify(dispatcher)?.dispatchInterval + } + + @Test + fun testSetDispatchTimeout() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val timeout = 1337 + tracker.dispatchTimeout = timeout + Mockito.verify(dispatcher)?.connectionTimeOut = timeout + tracker.dispatchTimeout + Mockito.verify(dispatcher)?.connectionTimeOut + } + + @Test + fun testGetOfflineCacheAge_defaultValue() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals((24 * 60 * 60 * 1000).toLong(), tracker.offlineCacheAge) + } + + @Test + fun testSetOfflineCacheAge() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.offlineCacheAge = 80085 + Assert.assertEquals(80085, tracker.offlineCacheAge) + } + + @Test + fun testGetOfflineCacheSize_defaultValue() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals((4 * 1024 * 1024).toLong(), tracker.offlineCacheSize) + } + + @Test + fun testSetOfflineCacheSize() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.offlineCacheSize = 16 * 1000 * 1000 + Assert.assertEquals((16 * 1000 * 1000).toLong(), tracker.offlineCacheSize) + } + + @Test + fun testDispatchMode_default() { + trackerPreferences.edit().clear() + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(DispatchMode.ALWAYS, tracker.dispatchMode) + Mockito.verify(dispatcher, Mockito.times(1))?.dispatchMode = DispatchMode.ALWAYS + } + + @Test + fun testDispatchMode_change() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.dispatchMode = DispatchMode.WIFI_ONLY + Assert.assertEquals(DispatchMode.WIFI_ONLY, tracker.dispatchMode) + Mockito.verify(dispatcher, Mockito.times(1))?.dispatchMode = + DispatchMode.WIFI_ONLY + } + + @Test + fun testDispatchMode_fallback() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.preferences!!.edit().putString(Tracker.PREF_KEY_DISPATCHER_MODE, "lol").apply() + Assert.assertEquals(DispatchMode.ALWAYS, tracker.dispatchMode) + Mockito.verify(dispatcher, Mockito.times(1))?.dispatchMode = DispatchMode.ALWAYS + } + + @Test + fun testSetDispatchMode_propagation() { + trackerPreferences.edit().clear() + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Mockito.verify(dispatcher, Mockito.times(1))?.dispatchMode = + ArgumentMatchers.any() + } + + @Test + fun testSetDispatchMode_propagation_change() { + trackerPreferences.edit().clear() + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.dispatchMode = DispatchMode.WIFI_ONLY + tracker.dispatchMode = DispatchMode.WIFI_ONLY + Assert.assertEquals(DispatchMode.WIFI_ONLY, tracker.dispatchMode) + Mockito.verify(dispatcher, Mockito.times(2))?.dispatchMode = + DispatchMode.WIFI_ONLY + Mockito.verify(dispatcher, Mockito.times(3))?.dispatchMode = + ArgumentMatchers.any() + } + + @Test + fun testSetDispatchMode_exception() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.dispatchMode = DispatchMode.WIFI_ONLY // This is persisted + tracker.dispatchMode = DispatchMode.EXCEPTION // This isn't + Assert.assertEquals(DispatchMode.EXCEPTION, tracker.dispatchMode) + Mockito.verify(dispatcher, Mockito.times(1))?.dispatchMode = + DispatchMode.EXCEPTION + + tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(DispatchMode.WIFI_ONLY, tracker.dispatchMode) + } + + @Test + fun testsetDispatchGzip() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.setDispatchGzipped(true) + Mockito.verify(dispatcher)?.dispatchGzipped = true + } + + @Test + fun testOptOut_set() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.isOptOut = true + Assert.assertTrue(tracker.isOptOut) + tracker.isOptOut = false + Assert.assertFalse(tracker.isOptOut) + } + + @Test + fun testOptOut_init() { + trackerPreferences.edit().putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, false).apply() + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertFalse(tracker.isOptOut) + trackerPreferences.edit().putBoolean(Tracker.PREF_KEY_TRACKER_OPTOUT, true).apply() + tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertTrue(tracker.isOptOut) + } + + @Test + fun testDispatch() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.dispatch() + Mockito.verify(dispatcher)?.forceDispatch() + tracker.dispatch() + Mockito.verify(dispatcher, Mockito.times(2))?.forceDispatch() + } + + @Test + fun testDispatch_optOut() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.isOptOut = true + tracker.dispatch() + Mockito.verify(dispatcher, Mockito.never())?.forceDispatch() + tracker.isOptOut = false + tracker.dispatch() + Mockito.verify(dispatcher)?.forceDispatch() + } + + @Test + fun testGetSiteId() { + Mockito.`when`(mTrackerBuilder!!.siteId).thenReturn(11) + Assert.assertEquals(Tracker(matomo!!, mTrackerBuilder!!).siteId.toLong(), 11) + } + + @Test + fun testGetMatomo() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(matomo, tracker.matomo) + } + + @Test + fun testSetURL() { + Mockito.`when`(mTrackerBuilder!!.applicationBaseUrl).thenReturn("http://test.com/") + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + + val trackMe = TrackMe() + tracker.track(trackMe) + Assert.assertEquals("http://test.com/", trackMe[QueryParams.URL_PATH]) + + trackMe[QueryParams.URL_PATH] = "me" + tracker.track(trackMe) + Assert.assertEquals("http://test.com/me", trackMe[QueryParams.URL_PATH]) + + // override protocol + trackMe[QueryParams.URL_PATH] = "https://my.com/secure" + tracker.track(trackMe) + Assert.assertEquals("https://my.com/secure", trackMe[QueryParams.URL_PATH]) + } + + @Test + fun testApplicationDomain() { + Mockito.`when`(mTrackerBuilder!!.applicationBaseUrl).thenReturn("http://my-domain.com") + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + + TrackHelper.track().screen("test/test").title("Test title").with(tracker) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + validateDefaultQuery(argumentCaptor.value) + Assert.assertEquals("http://my-domain.com/test/test", argumentCaptor.value[QueryParams.URL_PATH]) + } + + @Test(expected = IllegalArgumentException::class) + fun testVisitorId_invalid_short() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val tooShortVisitorId = "0123456789ab" + tracker.setVisitorId(tooShortVisitorId) + Assert.assertNotEquals(tooShortVisitorId, tracker.visitorId) + } + + @Test(expected = IllegalArgumentException::class) + fun testVisitorId_invalid_long() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val tooLongVisitorId = "0123456789abcdefghi" + tracker.setVisitorId(tooLongVisitorId) + Assert.assertNotEquals(tooLongVisitorId, tracker.visitorId) + } + + @Test(expected = IllegalArgumentException::class) + fun testVisitorId_invalid_charset() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val invalidCharacterVisitorId = "01234-6789-ghief" + tracker.setVisitorId(invalidCharacterVisitorId) + Assert.assertNotEquals(invalidCharacterVisitorId, tracker.visitorId) + } + + @Test + fun testVisitorId_init() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + MatcherAssert.assertThat(tracker.visitorId, Is.`is`(Matchers.notNullValue())) + } + + @Test + fun testVisitorId_restore() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + MatcherAssert.assertThat(tracker.visitorId, Is.`is`(Matchers.notNullValue())) + val visitorId = tracker.visitorId + + tracker = Tracker(matomo!!, mTrackerBuilder!!) + MatcherAssert.assertThat(tracker.visitorId, Is.`is`(visitorId)) + } + + @Test + fun testVisitorId_dispatch() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val visitorId = "0123456789abcdef" + tracker.setVisitorId(visitorId) + Assert.assertEquals(visitorId, tracker.visitorId) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals(visitorId, argumentCaptor.value[QueryParams.VISITOR_ID]) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals(visitorId, argumentCaptor.value[QueryParams.VISITOR_ID]) + } + + @Test + fun testUserID_init() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertNull(tracker.defaultTrackMe[QueryParams.USER_ID]) + Assert.assertNull(tracker.userId) + } + + @Test + fun testUserID_restore() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertNull(tracker.userId) + tracker.setUserId("cake") + MatcherAssert.assertThat(tracker.userId, Is.`is`("cake")) + + tracker = Tracker(matomo!!, mTrackerBuilder!!) + MatcherAssert.assertThat(tracker.userId, Is.`is`("cake")) + MatcherAssert.assertThat(tracker.defaultTrackMe[QueryParams.USER_ID], Is.`is`("cake")) + } + + @Test + fun testUserID_invalid() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertNull(tracker.userId) + + tracker.setUserId("test") + Assert.assertEquals(tracker.userId, "test") + + tracker.setUserId("") + Assert.assertEquals(tracker.userId, "test") + + tracker.setUserId(null) + Assert.assertNull(tracker.userId) + + val uuid = UUID.randomUUID().toString() + tracker.setUserId(uuid) + Assert.assertEquals(uuid, tracker.userId) + Assert.assertEquals(uuid, tracker.userId) + } + + @Test + fun testUserID_dispatch() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val uuid = UUID.randomUUID().toString() + tracker.setUserId(uuid) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals(uuid, argumentCaptor.value[QueryParams.USER_ID]) + + tracker.track(TrackMe()) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals(uuid, argumentCaptor.value[QueryParams.USER_ID]) + } + + @Test + fun testGetResolution() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val trackMe = TrackMe() + tracker.track(trackMe) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals("480x800", argumentCaptor.value[QueryParams.SCREEN_RESOLUTION]) + } + + @Test + fun testSetNewSession() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val trackMe = TrackMe() + tracker.track(trackMe) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals("1", argumentCaptor.value[QueryParams.SESSION_START]) + + tracker.startNewSession() + TrackHelper.track().screen("").with(tracker) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals("1", argumentCaptor.value[QueryParams.SESSION_START]) + } + + @Test + fun testSetNewSessionRaceCondition() { + for (retry in 0..4) { + val trackMes = Collections.synchronizedList(ArrayList()) + Mockito.doAnswer { invocation: InvocationOnMock -> + trackMes.add(invocation.getArgument(0)) + null + }.`when`(dispatcher).submit(ArgumentMatchers.any(TrackMe::class.java)) + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.setDispatchInterval(0) + val count = 20 + for (i in 0 until count) { + Thread { + TestHelper.sleep(10) + TrackHelper.track().screen("Test").with(tracker) + }.start() + } + TestHelper.sleep(500) + Assert.assertEquals(count.toLong(), trackMes.size.toLong()) + var found = 0 + for (trackMe in trackMes) { + if (trackMe[QueryParams.SESSION_START] != null) found++ + } + Assert.assertEquals(1, found.toLong()) + } + } + + @Test + fun testSetSessionTimeout() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.setSessionTimeout(10000) + + TrackHelper.track().screen("test1").with(tracker) + MatcherAssert.assertThat(tracker.lastEventX!![QueryParams.SESSION_START], Matchers.notNullValue()) + + TrackHelper.track().screen("test2").with(tracker) + MatcherAssert.assertThat(tracker.lastEventX!![QueryParams.SESSION_START], Matchers.nullValue()) + + tracker.setSessionTimeout(0) + TestHelper.sleep(1) + TrackHelper.track().screen("test3").with(tracker) + MatcherAssert.assertThat(tracker.lastEventX!![QueryParams.SESSION_START], Matchers.notNullValue()) + + tracker.setSessionTimeout(10000) + Assert.assertEquals(tracker.sessionTimeout, 10000) + TrackHelper.track().screen("test3").with(tracker) + MatcherAssert.assertThat(tracker.lastEventX!![QueryParams.SESSION_START], Matchers.nullValue()) + } + + @Test + fun testCheckSessionTimeout() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + tracker.setSessionTimeout(0) + TrackHelper.track().screen("test").with(tracker) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals("1", argumentCaptor.value[QueryParams.SESSION_START]) + TestHelper.sleep(1) + TrackHelper.track().screen("test").with(tracker) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals("1", argumentCaptor.value[QueryParams.SESSION_START]) + tracker.setSessionTimeout(60000) + TrackHelper.track().screen("test").with(tracker) + Mockito.verify(dispatcher, Mockito.times(3))?.submit(argumentCaptor.capture()) + Assert.assertNull(argumentCaptor.value[QueryParams.SESSION_START]) + } + + @Test + fun testReset() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val callback: Tracker.Callback = object : Tracker.Callback { + override fun onTrack(trackMe: TrackMe?): TrackMe? { + return null + } + } + tracker.addTrackingCallback(callback) + tracker.defaultTrackMe[QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES] = "custom1" + tracker.defaultTrackMe[QueryParams.CAMPAIGN_NAME] = "campaign_name" + tracker.defaultTrackMe[QueryParams.CAMPAIGN_KEYWORD] = "campaign_keyword" + + TrackHelper.track().screen("test1").with(tracker) + tracker.startNewSession() + TrackHelper.track().screen("test2").with(tracker) + + val preResetDefaultVisitorId = tracker.defaultTrackMe[QueryParams.VISITOR_ID] + val preResetFirstVisitTimestamp = tracker.defaultTrackMe[QueryParams.FIRST_VISIT_TIMESTAMP] + val preResetTotalNumberOfVisits = tracker.defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS] + val preResetPreviousVisitTimestamp = tracker.defaultTrackMe[QueryParams.PREVIOUS_VISIT_TIMESTAMP] + + tracker.reset() + + val prefs = tracker.preferences + + Assert.assertNotEquals(preResetDefaultVisitorId, tracker.visitorId) + Assert.assertNotEquals(preResetDefaultVisitorId, tracker.defaultTrackMe[QueryParams.VISITOR_ID]) + Assert.assertNotEquals(preResetDefaultVisitorId, prefs!!.getString(Tracker.PREF_KEY_TRACKER_VISITORID, "")) + + Assert.assertNotEquals(preResetFirstVisitTimestamp, tracker.defaultTrackMe[QueryParams.FIRST_VISIT_TIMESTAMP]) + Assert.assertNotEquals(preResetFirstVisitTimestamp.toLong(), prefs.getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1)) + + Assert.assertNotEquals(preResetPreviousVisitTimestamp, tracker.defaultTrackMe[QueryParams.PREVIOUS_VISIT_TIMESTAMP]) + Assert.assertNotEquals(preResetPreviousVisitTimestamp.toLong(), prefs.getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1)) + + Assert.assertNotEquals(preResetTotalNumberOfVisits, tracker.defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS]) + Assert.assertNotEquals(preResetTotalNumberOfVisits, prefs.getString(Tracker.PREF_KEY_TRACKER_VISITCOUNT, "")) + + Assert.assertNull(tracker.defaultTrackMe[QueryParams.VISIT_SCOPE_CUSTOM_VARIABLES]) + Assert.assertNull(tracker.defaultTrackMe[QueryParams.CAMPAIGN_NAME]) + Assert.assertNull(tracker.defaultTrackMe[QueryParams.CAMPAIGN_KEYWORD]) + } + + @Test + fun testTrackerEquals() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val builder2 = Mockito.mock(TrackerBuilder::class.java) + Mockito.`when`(builder2.apiUrl).thenReturn("http://localhost") + Mockito.`when`(builder2.siteId).thenReturn(100) + Mockito.`when`(builder2.trackerName).thenReturn("Default Tracker") + val tracker2 = Tracker(matomo!!, builder2) + + val builder3 = Mockito.mock(TrackerBuilder::class.java) + Mockito.`when`(builder3.apiUrl).thenReturn("http://example.com") + Mockito.`when`(builder3.siteId).thenReturn(11) + Mockito.`when`(builder3.trackerName).thenReturn("Default Tracker") + val tracker3 = Tracker(matomo!!, builder3) + + Assert.assertNotNull(tracker) + Assert.assertNotEquals(tracker, tracker2) + Assert.assertEquals(tracker, tracker3) + } + + @Test + fun testTrackerHashCode() { + Assert.assertEquals(Tracker(matomo!!, mTrackerBuilder!!).hashCode().toLong(), Tracker(matomo!!, mTrackerBuilder!!).hashCode().toLong()) + } + + @Test + fun testUrlPathCorrection() { + Mockito.`when`(mTrackerBuilder!!.applicationBaseUrl).thenReturn("https://package/") + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val paths = arrayOf(null, "", "/") + for (path in paths) { + val trackMe = TrackMe() + trackMe[QueryParams.URL_PATH] = path + tracker.track(trackMe) + Assert.assertEquals("https://package/", trackMe[QueryParams.URL_PATH]) + } + } + + @Test + fun testSetUserAgent() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + var trackMe = TrackMe() + tracker.track(trackMe) + Assert.assertEquals("aUserAgent", trackMe[QueryParams.USER_AGENT]) + + // Custom developer specified useragent + trackMe = TrackMe() + val customUserAgent = "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0" + trackMe[QueryParams.USER_AGENT] = customUserAgent + tracker.track(trackMe) + Assert.assertEquals(customUserAgent, trackMe[QueryParams.USER_AGENT]) + + // Modifying default TrackMe, no USER_AGENT + trackMe = TrackMe() + tracker.defaultTrackMe[QueryParams.USER_AGENT] = null + tracker.track(trackMe) + Assert.assertNull(trackMe[QueryParams.USER_AGENT]) + } + + @Test + fun testFirstVisitTimeStamp() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(-1, tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1)) + + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + val trackMe1 = argumentCaptor.value + TestHelper.sleep(10) + // make sure we are tracking in seconds + Assert.assertTrue(abs(((System.currentTimeMillis() / 1000) - trackMe1[QueryParams.FIRST_VISIT_TIMESTAMP].toLong()).toDouble()) < 2) + + tracker = Tracker(matomo!!, mTrackerBuilder!!) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + val trackMe2 = argumentCaptor.value + Assert.assertEquals(trackMe1[QueryParams.FIRST_VISIT_TIMESTAMP].toLong(), trackMe2[QueryParams.FIRST_VISIT_TIMESTAMP].toLong()) + Assert.assertEquals( + tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_FIRSTVISIT, -1), + trackMe1[QueryParams.FIRST_VISIT_TIMESTAMP].toLong() + ) + } + + @Test + fun testTotalVisitCount() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(-1, tracker.preferences!!.getInt(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1).toLong()) + Assert.assertNull(tracker.defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS]) + + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + Assert.assertEquals(1, argumentCaptor.value[QueryParams.TOTAL_NUMBER_OF_VISITS].toInt()) + + tracker = Tracker(matomo!!, mTrackerBuilder!!) + Assert.assertEquals(1, tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1)) + Assert.assertNull(tracker.defaultTrackMe[QueryParams.TOTAL_NUMBER_OF_VISITS]) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + Assert.assertEquals(2, argumentCaptor.value[QueryParams.TOTAL_NUMBER_OF_VISITS].toInt()) + Assert.assertEquals(2, tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, -1)) + } + + @Test + @Throws(Exception::class) + fun testVisitCountMultipleThreads() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val threadCount = 1000 + val countDownLatch = CountDownLatch(threadCount) + for (i in 0 until threadCount) { + Thread { + TestHelper.sleep((Random().nextInt(20 - 0) + 0).toLong()) + TrackHelper.track().event("TestCategory", "TestAction").with(Tracker(matomo!!, mTrackerBuilder!!)) + countDownLatch.countDown() + }.start() + } + countDownLatch.await() + Assert.assertEquals(threadCount.toLong(), trackerPreferences.getLong(Tracker.PREF_KEY_TRACKER_VISITCOUNT, 0)) + } + + @Test + @Throws(Exception::class) + fun testSessionStartRaceCondition() { + val trackMes = Collections.synchronizedList(ArrayList()) + Mockito.doAnswer { invocation: InvocationOnMock -> + trackMes.add(invocation.getArgument(0)) + null + }.`when`(dispatcher).submit(ArgumentMatchers.any(TrackMe::class.java)) + Mockito.`when`(dispatcher!!.connectionTimeOut).thenReturn(1000) + for (i in 0..999) { + trackMes.clear() + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val countDownLatch = CountDownLatch(10) + for (j in 0..9) { + Thread { + try { + TestHelper.sleep((Random().nextInt(4 - 0) + 0).toLong()) + val trackMe = TrackMe() + .set(QueryParams.EVENT_ACTION, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_CATEGORY, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_NAME, UUID.randomUUID().toString()) + .set(QueryParams.EVENT_VALUE, 1) + tracker.track(trackMe) + countDownLatch.countDown() + } catch (e: Exception) { + e.printStackTrace() + Assert.fail() + } + }.start() + } + countDownLatch.await() + for (out in trackMes) { + if (trackMes.indexOf(out) == 0) { + Assert.assertNotNull(i.toString() + "#" + out.toMap().size, out[QueryParams.LANGUAGE]) + Assert.assertNotNull(out[QueryParams.FIRST_VISIT_TIMESTAMP]) + Assert.assertNotNull(out[QueryParams.SESSION_START]) + } else { + Assert.assertNull(out[QueryParams.FIRST_VISIT_TIMESTAMP]) + Assert.assertNull(out[QueryParams.SESSION_START]) + } + } + } + } + + @Test + @Throws(Exception::class) + fun testFirstVisitMultipleThreads() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val threadCount = 100 + val countDownLatch = CountDownLatch(threadCount) + val firstVisitTimes = Collections.synchronizedList(ArrayList()) + for (i in 0 until threadCount) { + Thread { + TestHelper.sleep((Random().nextInt(20 - 0) + 0).toLong()) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + val firstVisit = tracker.defaultTrackMe[QueryParams.FIRST_VISIT_TIMESTAMP].toLong() + firstVisitTimes.add(firstVisit) + countDownLatch.countDown() + }.start() + } + countDownLatch.await() + for (firstVisit in firstVisitTimes) Assert.assertEquals(firstVisitTimes[0], firstVisit) + } + + @Test + fun testPreviousVisits() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val previousVisitTimes: MutableList = ArrayList() + for (i in 0..4) { + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + val previousVisit = tracker.defaultTrackMe[QueryParams.PREVIOUS_VISIT_TIMESTAMP] + if (previousVisit != null) previousVisitTimes.add(previousVisit.toLong()) + TestHelper.sleep(1010) + } + Assert.assertFalse(previousVisitTimes.contains(0L)) + var lastTime = 0L + for (time in previousVisitTimes) { + Assert.assertTrue(lastTime < time) + lastTime = time + } + } + + @Test + fun testPreviousVisit() { + var tracker = Tracker(matomo!!, mTrackerBuilder!!) + // No timestamp yet + Assert.assertEquals(-1, tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1)) + tracker = Tracker(matomo!!, mTrackerBuilder!!) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher)?.submit(argumentCaptor.capture()) + val _startTime = System.currentTimeMillis() / 1000 + // There was no previous visit + Assert.assertNull(argumentCaptor.value[QueryParams.PREVIOUS_VISIT_TIMESTAMP]) + TestHelper.sleep(1000) + + // After the first visit we now have a timestamp for the previous visit + var previousVisit = tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1) + Assert.assertTrue(previousVisit - _startTime < 2000) + Assert.assertNotEquals(-1, previousVisit) + tracker = Tracker(matomo!!, mTrackerBuilder!!) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher, Mockito.times(2))?.submit(argumentCaptor.capture()) + // Transmitted timestamp is the one from the first visit visit + Assert.assertEquals(previousVisit, argumentCaptor.value[QueryParams.PREVIOUS_VISIT_TIMESTAMP].toLong()) + + TestHelper.sleep(1000) + tracker = Tracker(matomo!!, mTrackerBuilder!!) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher, Mockito.times(3))?.submit(argumentCaptor.capture()) + // Now the timestamp changed as this is the 3rd visit. + Assert.assertNotEquals(previousVisit, argumentCaptor.value[QueryParams.PREVIOUS_VISIT_TIMESTAMP].toLong()) + TestHelper.sleep(1000) + + previousVisit = tracker.preferences!!.getLong(Tracker.PREF_KEY_TRACKER_PREVIOUSVISIT, -1) + tracker = Tracker(matomo!!, mTrackerBuilder!!) + TrackHelper.track().event("TestCategory", "TestAction").with(tracker) + Mockito.verify(dispatcher, Mockito.times(4))?.submit(argumentCaptor.capture()) + // Just make sure the timestamp in the 4th visit is from the 3rd visit + Assert.assertEquals(previousVisit, argumentCaptor.value[QueryParams.PREVIOUS_VISIT_TIMESTAMP].toLong()) + + // Test setting a custom timestamp + val custom = TrackMe() + custom[QueryParams.PREVIOUS_VISIT_TIMESTAMP] = 1000L + tracker.track(custom) + Mockito.verify(dispatcher, Mockito.times(5))?.submit(argumentCaptor.capture()) + Assert.assertEquals(1000L, argumentCaptor.value[QueryParams.PREVIOUS_VISIT_TIMESTAMP].toLong()) + } + + @Test + fun testTrackingCallback() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val callback = Mockito.mock(Tracker.Callback::class.java) + + val pre = TrackMe() + tracker.track(pre) + Mockito.verify(dispatcher)?.submit(pre) + Mockito.verify(callback, Mockito.never()).onTrack(argumentCaptor.capture()) + + Mockito.reset(dispatcher, callback) + tracker.addTrackingCallback(callback) + tracker.track(TrackMe()) + Mockito.verify(callback).onTrack(argumentCaptor.capture()) + Mockito.verify(dispatcher, Mockito.never())?.submit(ArgumentMatchers.any()) + + Mockito.reset(dispatcher, callback) + val orig = TrackMe() + val replaced = TrackMe().set("some", "thing") + Mockito.`when`(callback.onTrack(orig)).thenReturn(replaced) + tracker.track(orig) + Mockito.verify(callback).onTrack(orig) + Mockito.verify(dispatcher)?.submit(replaced) + + Mockito.reset(dispatcher, callback) + val post = TrackMe() + tracker.removeTrackingCallback(callback) + tracker.track(post) + Mockito.verify(callback, Mockito.never()).onTrack(ArgumentMatchers.any()) + Mockito.verify(dispatcher)?.submit(post) + } + + @Test + fun testTrackingCallbacks() { + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + val callback1 = Mockito.mock(Tracker.Callback::class.java) + val callback2 = Mockito.mock(Tracker.Callback::class.java) + + val orig = TrackMe() + val replaced = TrackMe() + Mockito.`when`(callback1.onTrack(orig)).thenReturn(replaced) + Mockito.`when`(callback2.onTrack(replaced)).thenReturn(replaced) + + tracker.addTrackingCallback(callback1) + tracker.addTrackingCallback(callback1) + tracker.addTrackingCallback(callback2) + tracker.track(orig) + Mockito.verify(callback1).onTrack(orig) + Mockito.verify(callback2).onTrack(replaced) + Mockito.verify(dispatcher)?.submit(replaced) + + tracker.removeTrackingCallback(callback1) + tracker.track(orig) + + Mockito.verify(callback2).onTrack(orig) + } + + @Test + fun testCustomDispatcherFactory() { + val dispatcherLocal = Mockito.mock(Dispatcher::class.java) + val factory = Mockito.mock(DispatcherFactory::class.java) + Mockito.`when`(factory.build(ArgumentMatchers.any(Tracker::class.java))).thenReturn(dispatcherLocal) + Mockito.`when`(matomo!!.dispatcherFactory).thenReturn(factory) + val tracker = Tracker(matomo!!, mTrackerBuilder!!) + Mockito.verify(factory).build(tracker) + } + + companion object { + private fun validateDefaultQuery(params: TrackMe) { + Assert.assertEquals(params[QueryParams.SITE_ID], "11") + Assert.assertEquals(params[QueryParams.RECORD], "1") + Assert.assertEquals(params[QueryParams.SEND_IMAGE], "0") + Assert.assertEquals(params[QueryParams.VISITOR_ID].length.toLong(), 16) + Assert.assertTrue(params[QueryParams.URL_PATH].startsWith("http://")) + Assert.assertTrue(params[QueryParams.RANDOM_NUMBER].toInt() > 0) + } + } +}