diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt index 1554a90c..6bda073e 100644 --- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt +++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt @@ -222,7 +222,7 @@ class FunctionalTests { // when startTimestamp = System.currentTimeMillis().milliseconds parselyTracker.trackPageview("url") - parselyTracker.startEngagement(engagementUrl, null) + parselyTracker.startEngagement(engagementUrl) } Thread.sleep((firstInterval + secondInterval + pauseInterval).inWholeMilliseconds) @@ -315,7 +315,7 @@ class FunctionalTests { activity: Activity, flushInterval: Duration = defaultFlushInterval ): ParselyTracker { - val field: Field = ParselyTracker::class.java.getDeclaredField("ROOT_URL") + val field: Field = ParselyTrackerInternal::class.java.getDeclaredField("ROOT_URL") field.isAccessible = true field.set(this, url) return ParselyTracker.sharedInstance( diff --git a/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt b/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt index 750b2a65..4bb39ac3 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch * 2. Progressive backoff for long engagements to save data. */ internal class EngagementManager( - private val parselyTracker: ParselyTracker, + private val eventQueuer: EventQueuer, private var latestDelayMillis: Long, private val baseEvent: Map, private val intervalCalculator: HeartbeatIntervalCalculator, @@ -76,7 +76,7 @@ internal class EngagementManager( totalTime += inc event["inc"] = inc / 1000 event["tt"] = totalTime - parselyTracker.enqueueEvent(event) + eventQueuer.enqueueEvent(event) } val intervalMillis: Double diff --git a/parsely/src/main/java/com/parsely/parselyandroid/EventQueuer.kt b/parsely/src/main/java/com/parsely/parselyandroid/EventQueuer.kt new file mode 100644 index 00000000..37b6ee63 --- /dev/null +++ b/parsely/src/main/java/com/parsely/parselyandroid/EventQueuer.kt @@ -0,0 +1,5 @@ +package com.parsely.parselyandroid + +internal interface EventQueuer { + fun enqueueEvent(event: Map) +} diff --git a/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt b/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt index 01de870c..b5779d5f 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt @@ -39,7 +39,7 @@ internal class FlushQueue( log("Sending request with %d events", eventsToSend.size) val jsonPayload = toParselyEventsPayload(eventsToSend) log("POST Data %s", jsonPayload) - log("Requested %s", ParselyTracker.ROOT_URL) + log("Requested %s", ParselyTrackerInternal.ROOT_URL) restClient.send(jsonPayload) .fold( onSuccess = { diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt index 5d2623cb..7e57efae 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -16,11 +16,6 @@ package com.parsely.parselyandroid import android.content.Context -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.ProcessLifecycleOwner -import com.parsely.parselyandroid.Logging.log -import java.util.UUID /** * Tracks Parse.ly app views in Android apps @@ -29,357 +24,43 @@ import java.util.UUID * Accessed as a singleton. Maintains a queue of pageview events in memory and periodically * flushes the queue to the Parse.ly pixel proxy server. */ -public open class ParselyTracker internal constructor( - siteId: String, - flushInterval: Int, - c: Context, - private val dryRun: Boolean -) { - private val flushManager: FlushManager - private var engagementManager: EngagementManager? = null - private var videoEngagementManager: EngagementManager? = null - private var lastPageviewUuid: String? = null - private val eventsBuilder: EventsBuilder - private val clock: Clock - private val intervalCalculator: HeartbeatIntervalCalculator - private val inMemoryBuffer: InMemoryBuffer - private val flushQueue: FlushQueue - - /** - * Create a new ParselyTracker instance. - */ - init { - val context = c.applicationContext - clock = Clock() - eventsBuilder = EventsBuilder( - AndroidDeviceInfoRepository( - AdvertisementIdProvider(context, sdkScope), - AndroidIdProvider(context) - ), siteId, clock - ) - val localStorageRepository = LocalStorageRepository(context) - flushManager = ParselyFlushManager( - { - flushEvents() - }, flushInterval * 1000L, - sdkScope - ) - inMemoryBuffer = InMemoryBuffer(sdkScope, localStorageRepository) { - if (!flushTimerIsActive()) { - startFlushTimer() - log("Flush flushTimer set to %ds", flushManager.intervalMillis / 1000) - } - } - flushQueue = FlushQueue( - flushManager, - localStorageRepository, - ParselyAPIConnection(ROOT_URL + "mobileproxy"), - sdkScope, - AndroidConnectivityStatusProvider(context) - ) - intervalCalculator = HeartbeatIntervalCalculator(clock) - - flushManager.start() - ProcessLifecycleOwner.get().lifecycle.addObserver( - LifecycleEventObserver { _, event: Lifecycle.Event -> - if (event == Lifecycle.Event.ON_STOP) { - flushEvents() - } - } - ) - } - - /** - * Get the heartbeat interval - * - * @return The base engagement tracking interval. - */ +public interface ParselyTracker { public val engagementInterval: Double? - get() = engagementManager?.intervalMillis - public val videoEngagementInterval: Double? - get() = videoEngagementManager?.intervalMillis - - /** - * Returns whether the engagement tracker is running. - * - * @return Whether the engagement tracker is running. - */ - public fun engagementIsActive(): Boolean { - return engagementManager?.isRunning ?: false - } - - /** - * Returns whether video tracking is active. - * - * @return Whether video tracking is active. - */ - public fun videoIsActive(): Boolean { - return videoEngagementManager?.isRunning ?: false - } - - /** - * Returns the interval at which the event queue is flushed to Parse.ly. - * - * @return The interval at which the event queue is flushed to Parse.ly. - */ public val flushInterval: Long - get() = flushManager.intervalMillis / 1000 - /** - * Register a pageview event using a URL and optional metadata. - * - * @param url The URL of the article being tracked - * (eg: "http://example.com/some-old/article.html") - * @param urlRef Referrer URL associated with this video view. - * @param urlMetadata Optional metadata for the URL -- not used in most cases. Only needed - * when `url` isn't accessible over the Internet (i.e. app-only - * content). Do not use this for **content also hosted on** URLs Parse.ly - * would normally crawl. - * @param extraData A Map of additional information to send with the event. - */ - @JvmOverloads + public fun engagementIsActive(): Boolean + public fun videoIsActive(): Boolean + public fun trackPageview( url: String, urlRef: String = "", urlMetadata: ParselyMetadata? = null, extraData: Map? = null, - ) { - if (url.isBlank()) { - log("url cannot be empty") - return - } - - val pageViewUuid = generatePixelId() - lastPageviewUuid = pageViewUuid + ) - enqueueEvent( - eventsBuilder.buildEvent( - url, - urlRef, - "pageview", - urlMetadata, - extraData, - pageViewUuid - ) - ) - } - - /** - * Start engaged time tracking for the given URL. - * - * - * This starts a timer which will send events to Parse.ly on a regular basis - * to capture engaged time for this URL. The value of `url` should be a URL for - * which `trackPageview` has been called. - * - * @param url The URL to track engaged time for. - * @param urlRef Referrer URL associated with this video view. - */ - @JvmOverloads public fun startEngagement( url: String, - urlRef: String? = null, + urlRef: String = "", extraData: Map? = null - ) { - if (url.isBlank()) { - log("url cannot be empty") - return - } - val pageViewUuid = lastPageviewUuid - if (pageViewUuid == null) { - log("engagement session cannot start without calling trackPageview first") - return - } - - // Cancel anything running - stopEngagement() + ) - // Start a new EngagementTask - val event = - eventsBuilder.buildEvent(url, urlRef.orEmpty(), "heartbeat", null, extraData, pageViewUuid) - engagementManager = EngagementManager( - this, - DEFAULT_ENGAGEMENT_INTERVAL_MILLIS.toLong(), - event, - intervalCalculator, - sdkScope, - clock - ).also { it.start() } - } + public fun stopEngagement() - /** - * Stop engaged time tracking. - * - * - * Stops the engaged time tracker, sending any accumulated engaged time to Parse.ly. - * NOTE: This **must** be called in your `MainActivity` during various Android lifecycle events - * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background - * and Parse.ly values may be inaccurate. - */ - public fun stopEngagement() { - engagementManager?.let { - it.stop() - log("Engagement session has been stopped") - } - engagementManager = null - } - - /** - * Start video tracking. - * - * - * Starts tracking view time for a video being viewed at a given url. Will send a `videostart` - * event unless the same url/videoId had previously been paused. - * Video metadata must be provided, specifically the video ID and video duration. - * - * - * The `url` value is *not* the URL of a video, but the post which contains the video. If the video - * is not embedded in a post, then this should contain a well-formatted URL on the customer's - * domain (e.g. http:///app-videos). This URL doesn't need to return a 200 status - * when crawled, but must but well-formatted so Parse.ly systems recognize it as belonging to - * the customer. - * - * @param url URL of post the video is embedded in. If videos is not embedded, a - * valid URL for the customer should still be provided. - * (e.g. http:///app-videos) - * @param urlRef Referrer URL associated with this video view. - * @param videoMetadata Metadata about the video being tracked. - * @param extraData A Map of additional information to send with the event. - */ - @JvmOverloads public fun trackPlay( url: String, urlRef: String = "", videoMetadata: ParselyVideoMetadata, extraData: Map? = null, - ) { - if (url.isBlank()) { - log("url cannot be empty") - return - } - - // If there is already an engagement manager for this video make sure it is started. - videoEngagementManager?.let { manager -> - if (manager.isSameVideo(url, urlRef, videoMetadata)) { - if (!manager.isRunning) { - manager.start() - } - return // all done here. early exit. - } else { - // Different video. Stop and remove it so we can start fresh. - manager.stop() - } - } - val uuid = generatePixelId() - - // Enqueue the videostart - val videostartEvent = - eventsBuilder.buildEvent(url, urlRef.orEmpty(), "videostart", videoMetadata, extraData, uuid) - enqueueEvent(videostartEvent) - - // Start a new engagement manager for the video. - val hbEvent = - eventsBuilder.buildEvent(url, urlRef.orEmpty(), "vheartbeat", videoMetadata, extraData, uuid) - // TODO: Can we remove some metadata fields from this request? - videoEngagementManager = EngagementManager( - this, - DEFAULT_ENGAGEMENT_INTERVAL_MILLIS.toLong(), - hbEvent, - intervalCalculator, - sdkScope, - clock - ).also { it.start() } - } - - /** - * Pause video tracking. - * - * - * Pauses video tracking for an ongoing video. If [.trackPlay] is immediately called again for - * the same video, a new video start event will not be sent. This models a user pausing a - * playing video. - * - * - * NOTE: This or [.resetVideo] **must** be called in your `MainActivity` during various Android lifecycle events - * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background - * and Parse.ly values may be inaccurate. - */ - public fun trackPause() { - videoEngagementManager?.stop() - } - - /** - * Reset tracking on a video. - * - * - * Stops video tracking and resets internal state for the video. If [.trackPlay] is immediately - * called for the same video, a new video start event is set. This models a user stopping a - * video and (on [.trackPlay] being called again) starting it over. - * - * - * NOTE: This or [.trackPause] **must** be called in your `MainActivity` during various Android lifecycle events - * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background - * and Parse.ly values may be inaccurate. - */ - public fun resetVideo() { - videoEngagementManager?.stop() - videoEngagementManager = null - } - - /** - * Add an event Map to the queue. - * Place a data structure representing the event into the in-memory queue for later use. - * @param event The event Map to enqueue. - */ - internal open fun enqueueEvent(event: Map) { - // Push it onto the queue - inMemoryBuffer.add(event) - } - - /** - * Deprecated since 3.1.1. The SDK now automatically flushes the queue on app lifecycle events. - * Any usage of this method is safe to remove and will have no effect. Keeping for backwards compatibility. - */ - @Deprecated("The SDK now automatically flushes the queue on app lifecycle events. Any usage of this method is safe to remove and will have no effect") - public fun flushEventQueue() { - // no-op - } - - /** - * Start the timer to flush events to Parsely. - * - * - * Instantiates the callback timer responsible for flushing the events queue. - * Can be called before of after `stop`, but has no effect if used before instantiating the - * singleton - */ - private fun startFlushTimer() { - flushManager.start() - } - - /** - * Returns whether the event queue flush timer is running. - * - * @return Whether the event queue flush timer is running. - */ - public fun flushTimerIsActive(): Boolean { - return flushManager.isRunning - } - - private fun generatePixelId(): String { - return UUID.randomUUID().toString() - } - - private fun flushEvents() { - flushQueue.invoke(dryRun) - } + ) + public fun trackPause() + public fun resetVideo() + public fun flushEventQueue() + public fun flushTimerIsActive(): Boolean public companion object { - private var instance: ParselyTracker? = null private const val DEFAULT_FLUSH_INTERVAL_SECS = 60 - private const val DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500 - @JvmField internal val ROOT_URL: String = "https://p1.parsely.com/".intern() + private var instance: ParselyTrackerInternal? = null /** * Singleton instance accessor. Note: This must be called after [.sharedInstance] @@ -409,7 +90,7 @@ public open class ParselyTracker internal constructor( dryRun: Boolean = false, ): ParselyTracker { return instance ?: run { - val newInstance = ParselyTracker(siteId, flushInterval, context, dryRun) + val newInstance = ParselyTrackerInternal(siteId, flushInterval, context, dryRun) instance = newInstance return newInstance } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt new file mode 100644 index 00000000..05cec30a --- /dev/null +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt @@ -0,0 +1,356 @@ +package com.parsely.parselyandroid + +import android.content.Context +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.ProcessLifecycleOwner +import java.util.UUID + +internal class ParselyTrackerInternal internal constructor( + siteId: String, + flushInterval: Int, + c: Context, + private val dryRun: Boolean +) : ParselyTracker, EventQueuer { + private val flushManager: FlushManager + private var engagementManager: EngagementManager? = null + private var videoEngagementManager: EngagementManager? = null + private var lastPageviewUuid: String? = null + private val eventsBuilder: EventsBuilder + private val clock: Clock + private val intervalCalculator: HeartbeatIntervalCalculator + private val inMemoryBuffer: InMemoryBuffer + private val flushQueue: FlushQueue + + /** + * Create a new ParselyTracker instance. + */ + init { + val context = c.applicationContext + clock = Clock() + eventsBuilder = EventsBuilder( + AndroidDeviceInfoRepository( + AdvertisementIdProvider(context, sdkScope), + AndroidIdProvider(context) + ), siteId, clock + ) + val localStorageRepository = LocalStorageRepository(context) + flushManager = ParselyFlushManager( + { + flushEvents() + }, flushInterval * 1000L, + sdkScope + ) + inMemoryBuffer = InMemoryBuffer(sdkScope, localStorageRepository) { + if (!flushTimerIsActive()) { + startFlushTimer() + Logging.log("Flush flushTimer set to %ds", flushManager.intervalMillis / 1000) + } + } + flushQueue = FlushQueue( + flushManager, + localStorageRepository, + ParselyAPIConnection(ROOT_URL + "mobileproxy"), + sdkScope, + AndroidConnectivityStatusProvider(context) + ) + intervalCalculator = HeartbeatIntervalCalculator(clock) + + flushManager.start() + ProcessLifecycleOwner.get().lifecycle.addObserver( + LifecycleEventObserver { _, event: Lifecycle.Event -> + if (event == Lifecycle.Event.ON_STOP) { + flushEvents() + } + } + ) + } + + /** + * Get the heartbeat interval + * + * @return The base engagement tracking interval. + */ + override val engagementInterval: Double? + get() = engagementManager?.intervalMillis + + override val videoEngagementInterval: Double? + get() = videoEngagementManager?.intervalMillis + + /** + * Returns whether the engagement tracker is running. + * + * @return Whether the engagement tracker is running. + */ + override fun engagementIsActive(): Boolean { + return engagementManager?.isRunning ?: false + } + + /** + * Returns whether video tracking is active. + * + * @return Whether video tracking is active. + */ + override fun videoIsActive(): Boolean { + return videoEngagementManager?.isRunning ?: false + } + + /** + * Returns the interval at which the event queue is flushed to Parse.ly. + * + * @return The interval at which the event queue is flushed to Parse.ly. + */ + override val flushInterval: Long + get() = flushManager.intervalMillis / 1000 + + /** + * Register a pageview event using a URL and optional metadata. + * + * @param url The URL of the article being tracked + * (eg: "http://example.com/some-old/article.html") + * @param urlRef Referrer URL associated with this video view. + * @param urlMetadata Optional metadata for the URL -- not used in most cases. Only needed + * when `url` isn't accessible over the Internet (i.e. app-only + * content). Do not use this for **content also hosted on** URLs Parse.ly + * would normally crawl. + * @param extraData A Map of additional information to send with the event. + */ + override fun trackPageview( + url: String, + urlRef: String, + urlMetadata: ParselyMetadata?, + extraData: Map?, + ) { + if (url.isBlank()) { + Logging.log("url cannot be empty") + return + } + + val pageViewUuid = generatePixelId() + lastPageviewUuid = pageViewUuid + + enqueueEvent( + eventsBuilder.buildEvent( + url, + urlRef, + "pageview", + urlMetadata, + extraData, + pageViewUuid + ) + ) + } + + /** + * Start engaged time tracking for the given URL. + * + * + * This starts a timer which will send events to Parse.ly on a regular basis + * to capture engaged time for this URL. The value of `url` should be a URL for + * which `trackPageview` has been called. + * + * @param url The URL to track engaged time for. + * @param urlRef Referrer URL associated with this video view. + */ + override fun startEngagement( + url: String, + urlRef: String, + extraData: Map? + ) { + if (url.isBlank()) { + Logging.log("url cannot be empty") + return + } + val pageViewUuid = lastPageviewUuid + if (pageViewUuid == null) { + Logging.log("engagement session cannot start without calling trackPageview first") + return + } + + // Cancel anything running + stopEngagement() + + // Start a new EngagementTask + val event = + eventsBuilder.buildEvent(url, urlRef, "heartbeat", null, extraData, pageViewUuid) + engagementManager = EngagementManager( + this, + DEFAULT_ENGAGEMENT_INTERVAL_MILLIS.toLong(), + event, + intervalCalculator, + sdkScope, + clock + ).also { it.start() } + } + + /** + * Stop engaged time tracking. + * + * + * Stops the engaged time tracker, sending any accumulated engaged time to Parse.ly. + * NOTE: This **must** be called in your `MainActivity` during various Android lifecycle events + * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background + * and Parse.ly values may be inaccurate. + */ + override fun stopEngagement() { + engagementManager?.let { + it.stop() + Logging.log("Engagement session has been stopped") + } + engagementManager = null + } + + /** + * Start video tracking. + * + * + * Starts tracking view time for a video being viewed at a given url. Will send a `videostart` + * event unless the same url/videoId had previously been paused. + * Video metadata must be provided, specifically the video ID and video duration. + * + * + * The `url` value is *not* the URL of a video, but the post which contains the video. If the video + * is not embedded in a post, then this should contain a well-formatted URL on the customer's + * domain (e.g. http:///app-videos). This URL doesn't need to return a 200 status + * when crawled, but must but well-formatted so Parse.ly systems recognize it as belonging to + * the customer. + * + * @param url URL of post the video is embedded in. If videos is not embedded, a + * valid URL for the customer should still be provided. + * (e.g. http:///app-videos) + * @param urlRef Referrer URL associated with this video view. + * @param videoMetadata Metadata about the video being tracked. + * @param extraData A Map of additional information to send with the event. + */ + override fun trackPlay( + url: String, + urlRef: String, + videoMetadata: ParselyVideoMetadata, + extraData: Map?, + ) { + if (url.isBlank()) { + Logging.log("url cannot be empty") + return + } + + // If there is already an engagement manager for this video make sure it is started. + videoEngagementManager?.let { manager -> + if (manager.isSameVideo(url, urlRef, videoMetadata)) { + if (!manager.isRunning) { + manager.start() + } + return // all done here. early exit. + } else { + // Different video. Stop and remove it so we can start fresh. + manager.stop() + } + } + val uuid = generatePixelId() + + // Enqueue the videostart + val videostartEvent = + eventsBuilder.buildEvent(url, urlRef, "videostart", videoMetadata, extraData, uuid) + enqueueEvent(videostartEvent) + + // Start a new engagement manager for the video. + val hbEvent = + eventsBuilder.buildEvent(url, urlRef, "vheartbeat", videoMetadata, extraData, uuid) + // TODO: Can we remove some metadata fields from this request? + videoEngagementManager = EngagementManager( + this, + DEFAULT_ENGAGEMENT_INTERVAL_MILLIS.toLong(), + hbEvent, + intervalCalculator, + sdkScope, + clock + ).also { it.start() } + } + + /** + * Pause video tracking. + * + * + * Pauses video tracking for an ongoing video. If [.trackPlay] is immediately called again for + * the same video, a new video start event will not be sent. This models a user pausing a + * playing video. + * + * + * NOTE: This or [.resetVideo] **must** be called in your `MainActivity` during various Android lifecycle events + * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background + * and Parse.ly values may be inaccurate. + */ + override fun trackPause() { + videoEngagementManager?.stop() + } + + /** + * Reset tracking on a video. + * + * + * Stops video tracking and resets internal state for the video. If [.trackPlay] is immediately + * called for the same video, a new video start event is set. This models a user stopping a + * video and (on [.trackPlay] being called again) starting it over. + * + * + * NOTE: This or [.trackPause] **must** be called in your `MainActivity` during various Android lifecycle events + * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background + * and Parse.ly values may be inaccurate. + */ + override fun resetVideo() { + videoEngagementManager?.stop() + videoEngagementManager = null + } + + /** + * Add an event Map to the queue. + * Place a data structure representing the event into the in-memory queue for later use. + * @param event The event Map to enqueue. + */ + override fun enqueueEvent(event: Map) { + // Push it onto the queue + inMemoryBuffer.add(event) + } + + /** + * Deprecated since 3.1.1. The SDK now automatically flushes the queue on app lifecycle events. + * Any usage of this method is safe to remove and will have no effect. Keeping for backwards compatibility. + */ + @Deprecated("The SDK now automatically flushes the queue on app lifecycle events. Any usage of this method is safe to remove and will have no effect") + override fun flushEventQueue() { + // no-op + } + + /** + * Start the timer to flush events to Parsely. + * + * + * Instantiates the callback timer responsible for flushing the events queue. + * Can be called before of after `stop`, but has no effect if used before instantiating the + * singleton + */ + private fun startFlushTimer() { + flushManager.start() + } + + /** + * Returns whether the event queue flush timer is running. + * + * @return Whether the event queue flush timer is running. + */ + override fun flushTimerIsActive(): Boolean { + return flushManager.isRunning + } + + private fun generatePixelId(): String { + return UUID.randomUUID().toString() + } + + private fun flushEvents() { + flushQueue.invoke(dryRun) + } + + companion object { + private const val DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500 + @JvmField val ROOT_URL: String = "https://p1.parsely.com/".intern() + } +} diff --git a/parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt index 47ac9bcf..45b797c5 100644 --- a/parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt +++ b/parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt @@ -24,7 +24,7 @@ private typealias Event = Map internal class EngagementManagerTest { private lateinit var sut: EngagementManager - private val tracker = FakeTracker() + private val eventQueuer = FakeEventQueuer() private val baseEvent: Event = mutableMapOf( "action" to "heartbeat", "data" to testData @@ -34,7 +34,7 @@ internal class EngagementManagerTest { fun `when starting manager, then record the correct event after interval millis`() = runTest { // given sut = EngagementManager( - tracker, + eventQueuer, DEFAULT_INTERVAL.inWholeMilliseconds, baseEvent, FakeIntervalCalculator(), @@ -48,7 +48,7 @@ internal class EngagementManagerTest { runCurrent() // then - assertThat(tracker.events[0]).isCorrectEvent( + assertThat(eventQueuer.events[0]).isCorrectEvent( withIncrementalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeSeconds)}, withTotalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeMilliseconds) }, withTimestamp = { isEqualTo(currentTime) } @@ -59,7 +59,7 @@ internal class EngagementManagerTest { fun `when starting manager, then schedule task each interval period`() = runTest { // given sut = EngagementManager( - tracker, + eventQueuer, DEFAULT_INTERVAL.inWholeMilliseconds, baseEvent, FakeIntervalCalculator(), @@ -80,19 +80,19 @@ internal class EngagementManagerTest { val thirdTimestamp = currentTime // then - val firstEvent = tracker.events[0] + val firstEvent = eventQueuer.events[0] assertThat(firstEvent).isCorrectEvent( withIncrementalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeSeconds) }, withTotalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeMilliseconds) }, withTimestamp = { isEqualTo(firstTimestamp) } ) - val secondEvent = tracker.events[1] + val secondEvent = eventQueuer.events[1] assertThat(secondEvent).isCorrectEvent( withIncrementalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeSeconds) }, withTotalTime = { isEqualTo((DEFAULT_INTERVAL * 2).inWholeMilliseconds) }, withTimestamp = { isEqualTo(secondTimestamp) } ) - val thirdEvent = tracker.events[2] + val thirdEvent = eventQueuer.events[2] assertThat(thirdEvent).isCorrectEvent( withIncrementalTime = { isEqualTo(DEFAULT_INTERVAL.inWholeSeconds) }, withTotalTime = { isEqualTo((DEFAULT_INTERVAL * 3).inWholeMilliseconds) }, @@ -104,7 +104,7 @@ internal class EngagementManagerTest { fun `given started manager, when stopping manager before interval ticks, then schedule an event`() = runTest { // given sut = EngagementManager( - tracker, + eventQueuer, DEFAULT_INTERVAL.inWholeMilliseconds, baseEvent, FakeIntervalCalculator(), @@ -121,7 +121,7 @@ internal class EngagementManagerTest { // first tick: after initial delay 30s, incremental addition 30s // second tick: after regular delay 30s, incremental addition 30s // third tick: after cancellation after 10s, incremental addition 10s - assertThat(tracker.events).hasSize(3).satisfies({ + assertThat(eventQueuer.events).hasSize(3).satisfies({ assertThat(it[0]).containsEntry("inc", 30L) assertThat(it[1]).containsEntry("inc", 30L) assertThat(it[2]).containsEntry("inc", 10L) @@ -132,7 +132,7 @@ internal class EngagementManagerTest { fun `when starting manager, then it should return true for isRunning`() = runTest { // given sut = EngagementManager( - tracker, + eventQueuer, DEFAULT_INTERVAL.inWholeMilliseconds, baseEvent, FakeIntervalCalculator(), @@ -151,7 +151,7 @@ internal class EngagementManagerTest { fun `given started manager, when stoping manager, then it should return false for isRunning`() = runTest { // given sut = EngagementManager( - tracker, + eventQueuer, DEFAULT_INTERVAL.inWholeMilliseconds, baseEvent, FakeIntervalCalculator(), @@ -191,12 +191,7 @@ internal class EngagementManagerTest { } } - class FakeTracker : ParselyTracker( - "", - 0, - ApplicationProvider.getApplicationContext(), - false, - ) { + class FakeEventQueuer : EventQueuer { val events = mutableListOf() override fun enqueueEvent(event: Event) {