Skip to content

Commit

Permalink
feat: add default events (#126)
Browse files Browse the repository at this point in the history
* feat: add deep link opened default event

* feat: add screen viewed event

* feat: update the sample app to test the screen navigation

* feat: add app lifecycle default events

* test: add tests for lifecycle plugin

* refactor: add empty last lines

* refactor: fix lint issues

* build: fix ci test failure

* refactor: fix lint

* refactor: use const for default event properties

* refactor: use shared casted amplitude and configuration vars

* feat: use defaultTracking in the options

* refactor: rename default tracking options

* refactor: update comment
  • Loading branch information
liuyang1520 authored Jun 20, 2023
1 parent 6abcee6 commit 12a743b
Show file tree
Hide file tree
Showing 15 changed files with 947 additions and 41 deletions.
2 changes: 2 additions & 0 deletions android/src/main/java/com/amplitude/android/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ open class Configuration @JvmOverloads constructor(
var locationListening: Boolean = true,
var flushEventsOnClose: Boolean = true,
var minTimeBetweenSessionsMillis: Long = MIN_TIME_BETWEEN_SESSIONS_MILLIS,
@Deprecated("Please use 'defaultTracking.sessions' instead.", ReplaceWith("defaultTracking.sessions"))
var trackingSessionEvents: Boolean = true,
var defaultTracking: DefaultTrackingOptions = DefaultTrackingOptions(),
override var identifyBatchIntervalMillis: Long = IDENTIFY_BATCH_INTERVAL_MILLIS,
override var identifyInterceptStorageProvider: StorageProvider = AndroidStorageProvider(),
override var identityStorageProvider: IdentityStorageProvider = FileIdentityStorageProvider(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.amplitude.android

class DefaultTrackingOptions(
var sessions: Boolean = true,
var appLifecycles: Boolean = false,
var deepLinks: Boolean = false,
var screenViews: Boolean = false
) {
// Prebuilt options for easier usage
companion object {
val ALL = DefaultTrackingOptions(
sessions = true,
appLifecycles = true,
deepLinks = true,
screenViews = true
)
val NONE = DefaultTrackingOptions(
sessions = false,
appLifecycles = false,
deepLinks = false,
screenViews = false
)
}
}
9 changes: 6 additions & 3 deletions android/src/main/java/com/amplitude/android/Timeline.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class Timeline : Timeline() {

val savedLastEventId = lastEventId

sessionEvents ?. let {
sessionEvents?.let {
it.forEach { e ->
e.eventId ?: let {
val newEventId = lastEventId + 1
Expand All @@ -97,7 +97,7 @@ class Timeline : Timeline() {
amplitude.storage.write(Storage.Constants.LAST_EVENT_ID, lastEventId.toString())
}

sessionEvents ?. let {
sessionEvents?.let {
it.forEach { e ->
super.process(e)
}
Expand All @@ -123,7 +123,10 @@ class Timeline : Timeline() {

private suspend fun startNewSession(timestamp: Long): Iterable<BaseEvent> {
val sessionEvents = mutableListOf<BaseEvent>()
val trackingSessionEvents = (amplitude.configuration as Configuration).trackingSessionEvents
val configuration = amplitude.configuration as Configuration
// If any trackingSessionEvents is false (default value is true), means it is manually set
@Suppress("DEPRECATION")
val trackingSessionEvents = configuration.trackingSessionEvents && configuration.defaultTracking.sessions

// end previous session
if (trackingSessionEvents && inSession()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,80 @@ package com.amplitude.android.plugins

import android.app.Activity
import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Bundle
import com.amplitude.android.Configuration
import com.amplitude.android.utilities.DefaultEventUtils
import com.amplitude.core.Amplitude
import com.amplitude.core.platform.Plugin
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger

class AndroidLifecyclePlugin : Application.ActivityLifecycleCallbacks, Plugin {
override val type: Plugin.Type = Plugin.Type.Utility
override lateinit var amplitude: Amplitude
private lateinit var packageInfo: PackageInfo
private lateinit var androidAmplitude: com.amplitude.android.Amplitude
private lateinit var androidConfiguration: Configuration

private val hasTrackedApplicationLifecycleEvents = AtomicBoolean(false)
private val numberOfActivities = AtomicInteger(1)
private val isFirstLaunch = AtomicBoolean(false)

override fun setup(amplitude: Amplitude) {
super.setup(amplitude)
((amplitude.configuration as Configuration).context as Application).registerActivityLifecycleCallbacks(this)
androidAmplitude = amplitude as com.amplitude.android.Amplitude
androidConfiguration = amplitude.configuration as Configuration

val application = androidConfiguration.context as Application
val packageManager: PackageManager = application.packageManager
packageInfo = try {
packageManager.getPackageInfo(application.packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
// This shouldn't happen, but in case it happens, fallback to empty package info.
amplitude.logger.error("Cannot find package with application.packageName: " + application.packageName)
PackageInfo()
}
application.registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
if (!hasTrackedApplicationLifecycleEvents.getAndSet(true) && androidConfiguration.defaultTracking.appLifecycles) {
numberOfActivities.set(0)
isFirstLaunch.set(true)
DefaultEventUtils(androidAmplitude).trackAppUpdatedInstalledEvent(packageInfo)
}
if (androidConfiguration.defaultTracking.deepLinks) {
DefaultEventUtils(androidAmplitude).trackDeepLinkOpenedEvent(activity)
}
}

override fun onActivityStarted(activity: Activity) {
if (androidConfiguration.defaultTracking.screenViews) {
DefaultEventUtils(androidAmplitude).trackScreenViewedEvent(activity)
}
}

override fun onActivityResumed(activity: Activity) {
(amplitude as com.amplitude.android.Amplitude).onEnterForeground(getCurrentTimeMillis())
androidAmplitude.onEnterForeground(getCurrentTimeMillis())

// numberOfActivities makes sure it only fires after activity creation or activity stopped
if (androidConfiguration.defaultTracking.appLifecycles && numberOfActivities.incrementAndGet() == 1) {
val isFromBackground = !isFirstLaunch.getAndSet(false)
DefaultEventUtils(androidAmplitude).trackAppOpenedEvent(packageInfo, isFromBackground)
}
}

override fun onActivityPaused(activity: Activity) {
(amplitude as com.amplitude.android.Amplitude).onExitForeground(getCurrentTimeMillis())
androidAmplitude.onExitForeground(getCurrentTimeMillis())
}

override fun onActivityStopped(activity: Activity) {
// numberOfActivities makes sure it only fires after setup or activity resumed
if (androidConfiguration.defaultTracking.appLifecycles && numberOfActivities.decrementAndGet() == 0) {
DefaultEventUtils(androidAmplitude).trackAppBackgroundedEvent()
}
}

override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.amplitude.android.utilities

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.ParseException
import android.net.Uri
import android.os.Build
import com.amplitude.android.Amplitude
import com.amplitude.core.Storage
import kotlinx.coroutines.launch

class DefaultEventUtils(private val amplitude: Amplitude) {
object EventTypes {
const val APPLICATION_INSTALLED = "[Amplitude] Application Installed"
const val APPLICATION_UPDATED = "[Amplitude] Application Updated"
const val APPLICATION_OPENED = "[Amplitude] Application Opened"
const val APPLICATION_BACKGROUNDED = "[Amplitude] Application Backgrounded"
const val DEEP_LINK_OPENED = "[Amplitude] Deep Link Opened"
const val SCREEN_VIEWED = "[Amplitude] Screen Viewed"
}

object EventProperties {
const val VERSION = "[Amplitude] Version"
const val BUILD = "[Amplitude] Build"
const val PREVIOUS_VERSION = "[Amplitude] Previous Version"
const val PREVIOUS_BUILD = "[Amplitude] Previous Build"
const val FROM_BACKGROUND = "[Amplitude] From Background"
const val LINK_URL = "[Amplitude] Link URL"
const val LINK_REFERRER = "[Amplitude] Link Referrer"
const val SCREEN_NAME = "[Amplitude] Screen Name"
}

fun trackAppUpdatedInstalledEvent(packageInfo: PackageInfo) {
// Get current version/build and previously stored version/build information
val currentVersion = packageInfo.versionName
val currentBuild = packageInfo.getVersionCode().toString()
val storage = amplitude.storage
val previousVersion = storage.read(Storage.Constants.APP_VERSION)
val previousBuild = storage.read(Storage.Constants.APP_BUILD)

if (previousBuild == null) {
// No stored build, treat it as fresh installed
amplitude.track(
EventTypes.APPLICATION_INSTALLED,
mapOf(
EventProperties.VERSION to currentVersion,
EventProperties.BUILD to currentBuild,
),
)
} else if (currentBuild != previousBuild) {
// Has stored build, but different from current build
amplitude.track(
EventTypes.APPLICATION_UPDATED,
mapOf(
EventProperties.PREVIOUS_VERSION to previousVersion,
EventProperties.PREVIOUS_BUILD to previousBuild,
EventProperties.VERSION to currentVersion,
EventProperties.BUILD to currentBuild,
),
)
}

// Write the current version/build into persistent storage
amplitude.amplitudeScope.launch(amplitude.storageIODispatcher) {
storage.write(Storage.Constants.APP_VERSION, currentVersion)
storage.write(Storage.Constants.APP_BUILD, currentBuild)
}
}

fun trackAppOpenedEvent(packageInfo: PackageInfo, isFromBackground: Boolean) {
val currentVersion = packageInfo.versionName
val currentBuild = packageInfo.getVersionCode().toString()

amplitude.track(
EventTypes.APPLICATION_OPENED,
mapOf(
EventProperties.FROM_BACKGROUND to isFromBackground,
EventProperties.VERSION to currentVersion,
EventProperties.BUILD to currentBuild,
),
)
}

fun trackAppBackgroundedEvent() {
amplitude.track(EventTypes.APPLICATION_BACKGROUNDED)
}

fun trackDeepLinkOpenedEvent(activity: Activity) {
val intent = activity.intent
intent?.let {
val referrer = getReferrer(activity)?.toString()
val url = it.data?.toString()
amplitude.track(
EventTypes.DEEP_LINK_OPENED,
mapOf(
EventProperties.LINK_URL to url,
EventProperties.LINK_REFERRER to referrer,
),
)
}
}

fun trackScreenViewedEvent(activity: Activity) {
try {
val packageManager = activity.packageManager
val info = packageManager?.getActivityInfo(
activity.componentName,
PackageManager.GET_META_DATA,
)
/* Get the label metadata in following order
1. activity label
2. if 1 is missing, fallback to parent application label
3. if 2 is missing, use the activity name
*/
val activityLabel = info?.loadLabel(packageManager)?.toString() ?: info?.name
amplitude.track(EventTypes.SCREEN_VIEWED, mapOf(EventProperties.SCREEN_NAME to activityLabel))
} catch (e: PackageManager.NameNotFoundException) {
amplitude.logger.error("Failed to get activity info: $e")
} catch (e: Exception) {
amplitude.logger.error("Failed to track screen viewed event: $e")
}
}

private fun getReferrer(activity: Activity): Uri? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
return activity.referrer
} else {
var referrerUri: Uri? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val intent = activity.intent
referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER)

if (referrerUri == null) {
referrerUri = intent.getStringExtra("android.intent.extra.REFERRER_NAME")?.let {
try {
Uri.parse(it)
} catch (e: ParseException) {
amplitude.logger.error("Failed to parse the referrer uri: $it")
null
}
}
}
}
return referrerUri
}
}
}

private fun PackageInfo.getVersionCode(): Number =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
this.longVersionCode
} else {
@Suppress("DEPRECATION")
this.versionCode
}
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,57 @@ class AmplitudeSessionTest {
Assertions.assertEquals(1100, event.timestamp)
}

@Suppress("DEPRECATION")
@Test
fun amplitude_noSessionEventsWhenDisabledWithTrackingSessionEvents() = runTest {
val configuration = createConfiguration()
configuration.trackingSessionEvents = false
val amplitude = Amplitude(configuration)
setDispatcher(amplitude, testScheduler)

val mockedPlugin = spyk(StubPlugin())
amplitude.add(mockedPlugin)

amplitude.isBuilt.await()

amplitude.track(createEvent(1000, "test event"))

advanceUntilIdle()
Thread.sleep(100)

val tracks = mutableListOf<BaseEvent>()

verify {
mockedPlugin.track(capture(tracks))
}
Assertions.assertEquals(1, tracks.count())
}

@Test
fun amplitude_noSessionEventsWhenDisabledWithDefaultTrackingOptions() = runTest {
val configuration = createConfiguration()
configuration.defaultTracking.sessions = false
val amplitude = Amplitude(configuration)
setDispatcher(amplitude, testScheduler)

val mockedPlugin = spyk(StubPlugin())
amplitude.add(mockedPlugin)

amplitude.isBuilt.await()

amplitude.track(createEvent(1000, "test event"))

advanceUntilIdle()
Thread.sleep(100)

val tracks = mutableListOf<BaseEvent>()

verify {
mockedPlugin.track(capture(tracks))
}
Assertions.assertEquals(1, tracks.count())
}

private fun createEvent(timestamp: Long, eventType: String, sessionId: Long? = null): BaseEvent {
val event = BaseEvent()
event.userId = "user"
Expand Down
Loading

0 comments on commit 12a743b

Please sign in to comment.