Skip to content

Commit

Permalink
Introduce Tracker & EventQueuer interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
oguzkocer committed Jan 11, 2024
1 parent a8bf91d commit 74a9f18
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>,
private val intervalCalculator: HeartbeatIntervalCalculator,
Expand Down Expand Up @@ -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
Expand Down
92 changes: 64 additions & 28 deletions parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,58 @@ 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<String, Any>? = null,
)

public fun startEngagement(
url: String,
urlRef: String? = null,
extraData: Map<String, Any>? = null
)

public fun stopEngagement()

public fun trackPlay(
url: String,
urlRef: String = "",
videoMetadata: ParselyVideoMetadata,
extraData: Map<String, Any>? = null,
)
public fun trackPause()
public fun resetVideo()
public fun flushEventQueue()
public fun flushTimerIsActive(): Boolean
}

internal interface EventQueuer {
fun enqueueEvent(event: Map<String, Any>)
}

/**
* 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 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
Expand Down Expand Up @@ -94,18 +133,18 @@ 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

/**
* Returns whether the engagement tracker is running.
*
* @return Whether the engagement tracker is running.
*/
public fun engagementIsActive(): Boolean {
override fun engagementIsActive(): Boolean {
return engagementManager?.isRunning ?: false
}

Expand All @@ -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
}

Expand All @@ -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

/**
Expand All @@ -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<String, Any>? = null,
urlRef: String,
urlMetadata: ParselyMetadata?,
extraData: Map<String, Any>?,
) {
if (url.isBlank()) {
log("url cannot be empty")
Expand Down Expand Up @@ -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<String, Any>? = null
urlRef: String?,
extraData: Map<String, Any>?
) {
if (url.isBlank()) {
log("url cannot be empty")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
</CUSTOMERDOMAIN></CUSTOMERDOMAIN> */
@JvmOverloads
public fun trackPlay(
override fun trackPlay(
url: String,
urlRef: String = "",
urlRef: String,
videoMetadata: ParselyVideoMetadata,
extraData: Map<String, Any>? = null,
extraData: Map<String, Any>?,
) {
if (url.isBlank()) {
log("url cannot be empty")
Expand Down Expand Up @@ -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()
}

Expand All @@ -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
}
Expand All @@ -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<String, Any>) {
override fun enqueueEvent(event: Map<String, Any>) {
// Push it onto the queue
inMemoryBuffer.add(event)
}
Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private typealias Event = Map<String, Any>
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
Expand All @@ -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(),
Expand All @@ -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) }
Expand All @@ -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(),
Expand All @@ -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) },
Expand All @@ -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(),
Expand All @@ -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)
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -191,12 +191,7 @@ internal class EngagementManagerTest {
}
}

class FakeTracker : ParselyTracker(
"",
0,
ApplicationProvider.getApplicationContext(),
false,
) {
class FakeEventQueuer : EventQueuer {
val events = mutableListOf<Event>()

override fun enqueueEvent(event: Event) {
Expand Down

0 comments on commit 74a9f18

Please sign in to comment.