Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.metarouter.analytics

import android.net.Uri

/**
* Public API for the MetaRouter Analytics SDK.
*
Expand Down Expand Up @@ -160,4 +162,22 @@ interface AnalyticsInterface {
* @param enabled Whether to enable tracing
*/
fun setTracing(enabled: Boolean)

/**
* Buffer deep-link properties for the next `Application Opened` event.
*
* Call this from the receiving Activity's `onCreate` / `onNewIntent`, typically
* passing `intent.data` and the referrer host (e.g. read via
* `intent.getStringExtra(Intent.EXTRA_REFERRER)` or `Activity.referrer?.host`).
*
* The next `Application Opened` event the SDK emits will carry `url` and
* (when provided) `referring_application` properties. The buffer is one-shot —
* subsequent `Application Opened` events without a fresh call omit the props.
*
* No-op when `InitOptions.trackLifecycleEvents` is `false`.
*
* @param uri The deep-link URI that opened the app
* @param sourceApplication Optional referring application identifier
*/
fun handleDeepLink(uri: Uri, sourceApplication: String? = null)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.metarouter.analytics

import android.net.Uri
import com.metarouter.analytics.utils.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.sync.Mutex
Expand Down Expand Up @@ -202,6 +203,15 @@ class AnalyticsProxy(
}
}

override fun handleDeepLink(uri: Uri, sourceApplication: String?) {
val client = realClient.get()
if (client != null) {
client.handleDeepLink(uri, sourceApplication)
} else {
enqueue(PendingCall.HandleDeepLink(uri, sourceApplication))
}
}


/**
* Enqueue a pending call, dropping oldest if at capacity.
Expand Down Expand Up @@ -230,6 +240,7 @@ class AnalyticsProxy(
is PendingCall.SetTracing -> client.setTracing(call.enabled)
is PendingCall.SetAdvertisingId -> client.setAdvertisingId(call.advertisingId)
is PendingCall.ClearAdvertisingId -> client.clearAdvertisingId()
is PendingCall.HandleDeepLink -> client.handleDeepLink(call.uri, call.sourceApplication)
is PendingCall.Flush -> client.flush()
is PendingCall.Reset -> client.reset()
is PendingCall.EnableDebugLogging -> client.enableDebugLogging()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import com.metarouter.analytics.utils.Logger
* (default: 10000). Set to `0` to opt out of disk persistence entirely — the queue then
* operates as a purely in-memory ring buffer, dropping the oldest event when full.
* Negative values are rejected.
* @property trackLifecycleEvents Whether the SDK should automatically emit
* `Application Installed`, `Application Updated`, `Application Opened`, and
* `Application Backgrounded` events (default: `true`). Set to `false` to opt out.
*
* @throws IllegalArgumentException if validation fails
*/
Expand All @@ -25,7 +28,8 @@ data class InitOptions(
val flushIntervalSeconds: Int = 10,
val debug: Boolean = false,
val maxQueueEvents: Int = 2000,
val maxDiskEvents: Int = 10000
val maxDiskEvents: Int = 10000,
val trackLifecycleEvents: Boolean = true
) {
init {
validateWriteKey()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import android.app.Application
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.res.Configuration
import android.net.Uri
import com.metarouter.analytics.context.DeviceContextProvider
import com.metarouter.analytics.dispatcher.Dispatcher
import com.metarouter.analytics.dispatcher.DispatcherConfig
import com.metarouter.analytics.enrichment.EventEnrichmentService
import com.metarouter.analytics.identity.IdentityManager
import com.metarouter.analytics.lifecycle.LifecycleEventTracker
import com.metarouter.analytics.network.AndroidNetworkMonitor
import com.metarouter.analytics.network.CircuitBreaker
import com.metarouter.analytics.network.DebouncedNetworkMonitor
Expand All @@ -18,6 +20,7 @@ import com.metarouter.analytics.network.OkHttpNetworkClient
import com.metarouter.analytics.queue.EventQueueInterface
import com.metarouter.analytics.queue.PersistableEventQueue
import com.metarouter.analytics.storage.EventDiskStore
import com.metarouter.analytics.storage.LifecycleStorage
import com.metarouter.analytics.types.BaseEvent
import com.metarouter.analytics.types.EventType
import com.metarouter.analytics.types.LifecycleState
Expand All @@ -43,7 +46,9 @@ class MetaRouterAnalyticsClient private constructor(
private val injectedCircuitBreaker: CircuitBreaker? = null,
private val injectedDispatcher: Dispatcher? = null,
private val injectedDiskStore: EventDiskStore? = null,
private val injectedNetworkMonitor: NetworkMonitor? = null
private val injectedNetworkMonitor: NetworkMonitor? = null,
private val injectedLifecycleStorage: LifecycleStorage? = null,
private val injectedLifecycleTracker: LifecycleEventTracker? = null
) : AnalyticsInterface {

companion object {
Expand Down Expand Up @@ -75,7 +80,9 @@ class MetaRouterAnalyticsClient private constructor(
circuitBreaker: CircuitBreaker? = null,
dispatcher: Dispatcher? = null,
diskStore: EventDiskStore? = null,
networkMonitor: NetworkMonitor? = null
networkMonitor: NetworkMonitor? = null,
lifecycleStorage: LifecycleStorage? = null,
lifecycleTracker: LifecycleEventTracker? = null
): MetaRouterAnalyticsClient {
val client = MetaRouterAnalyticsClient(
context.applicationContext,
Expand All @@ -88,7 +95,9 @@ class MetaRouterAnalyticsClient private constructor(
circuitBreaker,
dispatcher,
diskStore,
networkMonitor
networkMonitor,
lifecycleStorage,
lifecycleTracker
)
client.initializeInternal()
return client
Expand Down Expand Up @@ -119,6 +128,9 @@ class MetaRouterAnalyticsClient private constructor(
private var persistableEventQueue: PersistableEventQueue? = null
private var componentCallbacks: ComponentCallbacks2? = null

// Lifecycle event tracker (null when InitOptions.trackLifecycleEvents is false)
private var lifecycleTracker: LifecycleEventTracker? = null

/**
* Internal initialization. Sets up all components.
*/
Expand Down Expand Up @@ -230,9 +242,29 @@ class MetaRouterAnalyticsClient private constructor(
componentCallbacks = callbacks
}

// Build lifecycle event tracker before flipping to READY so onSdkReady() can run
// immediately after the state transition. The tracker emits via track(), which
// requires the lifecycle state to be READY.
lifecycleTracker = injectedLifecycleTracker ?: if (options.trackLifecycleEvents) {
val lifecycleStorage = injectedLifecycleStorage ?: LifecycleStorage(context)
LifecycleEventTracker(
analytics = this,
storage = lifecycleStorage,
contextProvider = contextProvider,
identityManager = identityManager,
enabled = true
)
} else {
null
}

lifecycleState.set(LifecycleState.READY)
Logger.log("MetaRouter SDK initialized")

// Run cold-launch detection + emission. Must happen after READY so enqueueEvent
// accepts the events.
lifecycleTracker?.onSdkReady()

} catch (e: Exception) {
Logger.error("Failed to initialize SDK: ${e.message}")
lifecycleState.set(LifecycleState.IDLE)
Expand Down Expand Up @@ -426,33 +458,46 @@ class MetaRouterAnalyticsClient private constructor(

/**
* Called when app goes to background.
* Flushes pending events and pauses the dispatcher.
* Emits `Application Backgrounded` (when enabled), flushes pending events,
* and pauses the dispatcher. The lifecycle event must be emitted before flush
* so it lands in the same drain.
*/
internal suspend fun onBackground() {
if (lifecycleState.get() != LifecycleState.READY) {
Logger.log("onBackground ignored - SDK not ready (state: ${lifecycleState.get()})")
return
}
lifecycleTracker?.onBackground()
flush()
persistableEventQueue?.flushToDisk()
dispatcher.pause()
}

/**
* Called when app comes to foreground.
* Flushes pending events and resumes the dispatcher.
* Emits `Application Opened` (when enabled), flushes pending events, and
* resumes the dispatcher.
*/
internal fun onForeground() {
if (lifecycleState.get() != LifecycleState.READY) {
Logger.log("onForeground ignored - SDK not ready (state: ${lifecycleState.get()})")
return
}
lifecycleTracker?.onForeground()
scope.launch {
flush()
}
dispatcher.resume()
}

override fun handleDeepLink(uri: Uri, sourceApplication: String?) {
if (lifecycleState.get() != LifecycleState.READY) {
Logger.log("handleDeepLink ignored - SDK not ready (state: ${lifecycleState.get()})")
return
}
lifecycleTracker?.handleDeepLink(uri, sourceApplication)
}

override suspend fun reset() {
if (!lifecycleState.compareAndSet(LifecycleState.READY, LifecycleState.RESETTING)) {
Logger.warn("Cannot reset - SDK not in READY state (current: ${lifecycleState.get()})")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.metarouter.analytics

import android.net.Uri

/**
* Represents a queued analytics method call that will be replayed
* once the real client is bound to the proxy.
Expand All @@ -15,6 +17,7 @@ sealed class PendingCall {
data class Alias(val newUserId: String) : PendingCall()
data class SetTracing(val enabled: Boolean) : PendingCall()
data class SetAdvertisingId(val advertisingId: String) : PendingCall()
data class HandleDeepLink(val uri: Uri, val sourceApplication: String?) : PendingCall()
object ClearAdvertisingId : PendingCall()
object Flush : PendingCall()
object Reset : PendingCall()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,11 @@ class IdentityManager(
storage.clear()
Logger.log("Identity manager reset")
}

/**
* Returns true if identity storage already has any value. Used by lifecycle-event
* detection to tell a true fresh install from a user upgrading from a pre-lifecycle
* SDK build (where the version/build keys would be missing despite prior usage).
*/
fun hasAnyValue(): Boolean = storage.hasAnyValue()
}
Loading
Loading