From 06aab22041ca6c6bce3e7021672f6be78496f491 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Thu, 11 Jan 2024 13:33:20 -0500 Subject: [PATCH 1/7] Introduce Tracker & EventQueuer interfaces --- .../parsely/parselyandroid/FunctionalTests.kt | 2 +- .../parselyandroid/EngagementManager.kt | 4 +- .../parsely/parselyandroid/ParselyTracker.kt | 92 +++++++++++++------ .../parselyandroid/EngagementManagerTest.kt | 29 +++--- 4 files changed, 79 insertions(+), 48 deletions(-) diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt index 1554a90c..11ddfaff 100644 --- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt +++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt @@ -314,7 +314,7 @@ class FunctionalTests { private fun initializeTracker( activity: Activity, flushInterval: Duration = defaultFlushInterval - ): ParselyTracker { + ): Tracker { val field: Field = ParselyTracker::class.java.getDeclaredField("ROOT_URL") field.isAccessible = true field.set(this, url) 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/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt index c14a1099..eae13cf4 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -22,6 +22,45 @@ import androidx.lifecycle.ProcessLifecycleOwner import com.parsely.parselyandroid.Logging.log import java.util.UUID +public interface Tracker { + public val engagementInterval: Double? + public val videoEngagementInterval: Double? + public val flushInterval: Long + + public fun engagementIsActive(): Boolean + public fun videoIsActive(): Boolean + + public fun trackPageview( + url: String, + urlRef: String = "", + urlMetadata: ParselyMetadata? = null, + extraData: Map? = null, + ) + + public fun startEngagement( + url: String, + urlRef: String? = null, + extraData: Map? = null + ) + + public fun stopEngagement() + + public fun trackPlay( + url: String, + urlRef: String = "", + videoMetadata: ParselyVideoMetadata, + extraData: Map? = null, + ) + public fun trackPause() + public fun resetVideo() + public fun flushEventQueue() + public fun flushTimerIsActive(): Boolean +} + +internal interface EventQueuer { + fun enqueueEvent(event: Map) +} + /** * Tracks Parse.ly app views in Android apps * @@ -29,12 +68,12 @@ 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 protected constructor( +public class ParselyTracker protected constructor( siteId: String, flushInterval: Int, c: Context, private val dryRun: Boolean -) { +): Tracker, EventQueuer { private val flushManager: FlushManager private var engagementManager: EngagementManager? = null private var videoEngagementManager: EngagementManager? = null @@ -94,10 +133,10 @@ public open class ParselyTracker protected constructor( * * @return The base engagement tracking interval. */ - public val engagementInterval: Double? + override val engagementInterval: Double? get() = engagementManager?.intervalMillis - public val videoEngagementInterval: Double? + override val videoEngagementInterval: Double? get() = videoEngagementManager?.intervalMillis /** @@ -105,7 +144,7 @@ public open class ParselyTracker protected constructor( * * @return Whether the engagement tracker is running. */ - public fun engagementIsActive(): Boolean { + override fun engagementIsActive(): Boolean { return engagementManager?.isRunning ?: false } @@ -114,7 +153,7 @@ public open class ParselyTracker protected constructor( * * @return Whether video tracking is active. */ - public fun videoIsActive(): Boolean { + override fun videoIsActive(): Boolean { return videoEngagementManager?.isRunning ?: false } @@ -123,7 +162,7 @@ public open class ParselyTracker protected constructor( * * @return The interval at which the event queue is flushed to Parse.ly. */ - public val flushInterval: Long + override val flushInterval: Long get() = flushManager.intervalMillis / 1000 /** @@ -138,12 +177,11 @@ public open class ParselyTracker protected constructor( * would normally crawl. * @param extraData A Map of additional information to send with the event. */ - @JvmOverloads - public fun trackPageview( + override fun trackPageview( url: String, - urlRef: String = "", - urlMetadata: ParselyMetadata? = null, - extraData: Map? = null, + urlRef: String, + urlMetadata: ParselyMetadata?, + extraData: Map?, ) { if (url.isBlank()) { log("url cannot be empty") @@ -176,11 +214,10 @@ public open class ParselyTracker protected constructor( * @param url The URL to track engaged time for. * @param urlRef Referrer URL associated with this video view. */ - @JvmOverloads - public fun startEngagement( + override fun startEngagement( url: String, - urlRef: String? = null, - extraData: Map? = null + urlRef: String?, + extraData: Map? ) { if (url.isBlank()) { log("url cannot be empty") @@ -217,7 +254,7 @@ public open class ParselyTracker protected constructor( * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background * and Parse.ly values may be inaccurate. */ - public fun stopEngagement() { + override fun stopEngagement() { engagementManager?.let { it.stop() log("Engagement session has been stopped") @@ -247,12 +284,11 @@ public open class ParselyTracker protected constructor( * @param videoMetadata Metadata about the video being tracked. * @param extraData A Map of additional information to send with the event. */ - @JvmOverloads - public fun trackPlay( + override fun trackPlay( url: String, - urlRef: String = "", + urlRef: String, videoMetadata: ParselyVideoMetadata, - extraData: Map? = null, + extraData: Map?, ) { if (url.isBlank()) { log("url cannot be empty") @@ -305,7 +341,7 @@ public open class ParselyTracker protected constructor( * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background * and Parse.ly values may be inaccurate. */ - public fun trackPause() { + override fun trackPause() { videoEngagementManager?.stop() } @@ -322,7 +358,7 @@ public open class ParselyTracker protected constructor( * like `onPause` or `onStop`. Otherwise, engaged time tracking may keep running in the background * and Parse.ly values may be inaccurate. */ - public fun resetVideo() { + override fun resetVideo() { videoEngagementManager?.stop() videoEngagementManager = null } @@ -332,7 +368,7 @@ public open class ParselyTracker protected constructor( * 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) { + override fun enqueueEvent(event: Map) { // Push it onto the queue inMemoryBuffer.add(event) } @@ -342,7 +378,7 @@ public open class ParselyTracker protected constructor( * 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() { + override fun flushEventQueue() { // no-op } @@ -363,7 +399,7 @@ public open class ParselyTracker protected constructor( * * @return Whether the event queue flush timer is running. */ - public fun flushTimerIsActive(): Boolean { + override fun flushTimerIsActive(): Boolean { return flushManager.isRunning } @@ -387,7 +423,7 @@ public open class ParselyTracker protected constructor( * @return The singleton instance */ @JvmStatic - public fun sharedInstance(): ParselyTracker? { + public fun sharedInstance(): Tracker? { return instance } @@ -407,7 +443,7 @@ public open class ParselyTracker protected constructor( flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS, context: Context, dryRun: Boolean = false, - ): ParselyTracker { + ): Tracker { return instance ?: run { val newInstance = ParselyTracker(siteId, flushInterval, context, dryRun) instance = newInstance 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) { From f02483e975383aece51c3c4ed6d8ec5c0517dcfb Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:02:11 +0100 Subject: [PATCH 2/7] refactor: move `sharedInstance` methods to the interface --- .../parsely/parselyandroid/ParselyTracker.kt | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt index eae13cf4..25a3edf3 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -55,6 +55,45 @@ public interface Tracker { public fun resetVideo() public fun flushEventQueue() public fun flushTimerIsActive(): Boolean + + public companion object { + private const val DEFAULT_FLUSH_INTERVAL_SECS = 60 + private var instance: ParselyTracker? = null + + /** + * Singleton instance accessor. Note: This must be called after [.sharedInstance] + * + * @return The singleton instance + */ + @JvmStatic + public fun sharedInstance(): Tracker? { + return instance + } + + /** + * Singleton instance factory Note: this must be called before [.sharedInstance] + * + * @param siteId The Parsely public site id (eg "example.com") + * @param flushInterval The interval at which the events queue should flush, in seconds + * @param context The current Android application context + * @param dryRun If set to `true`, events **won't** be sent to Parse.ly server + * @return The singleton instance + */ + @JvmStatic + @JvmOverloads + public fun sharedInstance( + siteId: String, + flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS, + context: Context, + dryRun: Boolean = false, + ): Tracker { + return instance ?: run { + val newInstance = ParselyTracker(siteId, flushInterval, context, dryRun) + instance = newInstance + return newInstance + } + } + } } internal interface EventQueuer { @@ -68,12 +107,12 @@ internal interface EventQueuer { * 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 class ParselyTracker protected constructor( +internal class ParselyTracker internal constructor( siteId: String, flushInterval: Int, c: Context, private val dryRun: Boolean -): Tracker, EventQueuer { +) : Tracker, EventQueuer { private val flushManager: FlushManager private var engagementManager: EngagementManager? = null private var videoEngagementManager: EngagementManager? = null @@ -412,43 +451,7 @@ public class ParselyTracker protected constructor( } 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 public val ROOT_URL: String = "https://p1.parsely.com/".intern() - - /** - * Singleton instance accessor. Note: This must be called after [.sharedInstance] - * - * @return The singleton instance - */ - @JvmStatic - public fun sharedInstance(): Tracker? { - return instance - } - - /** - * Singleton instance factory Note: this must be called before [.sharedInstance] - * - * @param siteId The Parsely public site id (eg "example.com") - * @param flushInterval The interval at which the events queue should flush, in seconds - * @param context The current Android application context - * @param dryRun If set to `true`, events **won't** be sent to Parse.ly server - * @return The singleton instance - */ - @JvmStatic - @JvmOverloads - public fun sharedInstance( - siteId: String, - flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS, - context: Context, - dryRun: Boolean = false, - ): Tracker { - return instance ?: run { - val newInstance = ParselyTracker(siteId, flushInterval, context, dryRun) - instance = newInstance - return newInstance - } - } } } From f0020c0c6c6ba91fe90dbee37952cc77b370cb7c Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:09:09 +0100 Subject: [PATCH 3/7] refactor: rename interface `Tracker` to `ParselyTracker` Reason: to minimize breaking changes for the clients --- .../java/com/parsely/parselyandroid/FlushQueue.kt | 2 +- .../com/parsely/parselyandroid/ParselyTracker.kt | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) 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 25a3edf3..c057f286 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.ProcessLifecycleOwner import com.parsely.parselyandroid.Logging.log import java.util.UUID -public interface Tracker { +public interface ParselyTracker { public val engagementInterval: Double? public val videoEngagementInterval: Double? public val flushInterval: Long @@ -58,7 +58,7 @@ public interface Tracker { public companion object { private const val DEFAULT_FLUSH_INTERVAL_SECS = 60 - private var instance: ParselyTracker? = null + private var instance: ParselyTrackerInternal? = null /** * Singleton instance accessor. Note: This must be called after [.sharedInstance] @@ -66,7 +66,7 @@ public interface Tracker { * @return The singleton instance */ @JvmStatic - public fun sharedInstance(): Tracker? { + public fun sharedInstance(): ParselyTracker? { return instance } @@ -86,9 +86,9 @@ public interface Tracker { flushInterval: Int = DEFAULT_FLUSH_INTERVAL_SECS, context: Context, dryRun: Boolean = false, - ): Tracker { + ): ParselyTracker { return instance ?: run { - val newInstance = ParselyTracker(siteId, flushInterval, context, dryRun) + val newInstance = ParselyTrackerInternal(siteId, flushInterval, context, dryRun) instance = newInstance return newInstance } @@ -107,12 +107,12 @@ internal interface EventQueuer { * Accessed as a singleton. Maintains a queue of pageview events in memory and periodically * flushes the queue to the Parse.ly pixel proxy server. */ -internal class ParselyTracker internal constructor( +internal class ParselyTrackerInternal internal constructor( siteId: String, flushInterval: Int, c: Context, private val dryRun: Boolean -) : Tracker, EventQueuer { +) : ParselyTracker, EventQueuer { private val flushManager: FlushManager private var engagementManager: EngagementManager? = null private var videoEngagementManager: EngagementManager? = null From 63e228c93c0ae0f5c6b16b22bf46559340cb1363 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:09:26 +0100 Subject: [PATCH 4/7] tests: fix functional tests after rename --- .../java/com/parsely/parselyandroid/FunctionalTests.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt index 11ddfaff..65de31e5 100644 --- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt +++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt @@ -314,8 +314,8 @@ class FunctionalTests { private fun initializeTracker( activity: Activity, flushInterval: Duration = defaultFlushInterval - ): Tracker { - val field: Field = ParselyTracker::class.java.getDeclaredField("ROOT_URL") + ): ParselyTracker { + val field: Field = ParselyTrackerInternal::class.java.getDeclaredField("ROOT_URL") field.isAccessible = true field.set(this, url) return ParselyTracker.sharedInstance( From cfabce72942684891803af8bbfec2c6cd7f6bdb2 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:14:00 +0100 Subject: [PATCH 5/7] refactor: move interfaces to separate files --- .../com/parsely/parselyandroid/EventQueuer.kt | 5 + .../parsely/parselyandroid/ParselyTracker.kt | 372 +----------------- .../parselyandroid/ParselyTrackerInternal.kt | 356 +++++++++++++++++ 3 files changed, 368 insertions(+), 365 deletions(-) create mode 100644 parsely/src/main/java/com/parsely/parselyandroid/EventQueuer.kt create mode 100644 parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt 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/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt index c057f286..11cc2207 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -16,12 +16,14 @@ 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 + * + * + * 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 interface ParselyTracker { public val engagementInterval: Double? public val videoEngagementInterval: Double? @@ -95,363 +97,3 @@ public interface ParselyTracker { } } } - -internal interface EventQueuer { - fun enqueueEvent(event: Map) -} - -/** - * Tracks Parse.ly app views in Android apps - * - * - * Accessed as a singleton. Maintains a queue of pageview events in memory and periodically - * flushes the queue to the Parse.ly pixel proxy server. - */ -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() - 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()) { - 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()) { - 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() } - } - - /** - * 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() - 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()) { - 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. - */ - 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) - } - - public companion object { - private const val DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500 - @JvmField public val ROOT_URL: String = "https://p1.parsely.com/".intern() - } -} 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..ba269ac3 --- /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.orEmpty(), "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.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. + */ + 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) + } + + public companion object { + private const val DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500 + @JvmField public val ROOT_URL: String = "https://p1.parsely.com/".intern() + } +} From 697c0516e16a2dafbc749c258b71903e0f38a38b Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:17:32 +0100 Subject: [PATCH 6/7] fix: make `urlRef` not nullable in `startEngagement` --- .../java/com/parsely/parselyandroid/FunctionalTests.kt | 2 +- .../java/com/parsely/parselyandroid/ParselyTracker.kt | 2 +- .../com/parsely/parselyandroid/ParselyTrackerInternal.kt | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt index 65de31e5..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) diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt index 11cc2207..7e57efae 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt @@ -41,7 +41,7 @@ public interface ParselyTracker { public fun startEngagement( url: String, - urlRef: String? = null, + urlRef: String = "", extraData: Map? = null ) diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt index ba269ac3..86f52f66 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt @@ -154,7 +154,7 @@ internal class ParselyTrackerInternal internal constructor( */ override fun startEngagement( url: String, - urlRef: String?, + urlRef: String, extraData: Map? ) { if (url.isBlank()) { @@ -172,7 +172,7 @@ internal class ParselyTrackerInternal internal constructor( // Start a new EngagementTask val event = - eventsBuilder.buildEvent(url, urlRef.orEmpty(), "heartbeat", null, extraData, pageViewUuid) + eventsBuilder.buildEvent(url, urlRef, "heartbeat", null, extraData, pageViewUuid) engagementManager = EngagementManager( this, DEFAULT_ENGAGEMENT_INTERVAL_MILLIS.toLong(), @@ -249,12 +249,12 @@ internal class ParselyTrackerInternal internal constructor( // Enqueue the videostart val videostartEvent = - eventsBuilder.buildEvent(url, urlRef.orEmpty(), "videostart", videoMetadata, extraData, uuid) + eventsBuilder.buildEvent(url, urlRef, "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) + eventsBuilder.buildEvent(url, urlRef, "vheartbeat", videoMetadata, extraData, uuid) // TODO: Can we remove some metadata fields from this request? videoEngagementManager = EngagementManager( this, From c2cb6177da1922b322f14b650ffd833db31db835 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Mon, 15 Jan 2024 15:18:49 +0100 Subject: [PATCH 7/7] fix: visibility modifiers for `ParselyTrackerInternal` companion object --- .../java/com/parsely/parselyandroid/ParselyTrackerInternal.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt index 86f52f66..05cec30a 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTrackerInternal.kt @@ -349,8 +349,8 @@ internal class ParselyTrackerInternal internal constructor( flushQueue.invoke(dryRun) } - public companion object { + companion object { private const val DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500 - @JvmField public val ROOT_URL: String = "https://p1.parsely.com/".intern() + @JvmField val ROOT_URL: String = "https://p1.parsely.com/".intern() } }