Skip to content

Commit

Permalink
feat: add autocapture in configs (#207)
Browse files Browse the repository at this point in the history
* feat: add autocapture options and deprecate default tracking

* feat: track target hierarchy

* fix: fix lint

* fix: fix a bug that occurs when default tracking options are set after configuration is created

* fix: refactor default tracking deprecation

* fix: handle changes to defaultTrackingOptions after being assigned to Configuration

* fix: fix lint

* fix: attach autocapture option to defaultTracking to monitor changes to defaultTracking

* fix: remove redundant code

* refactor: refactor code to reduce object creation

* feat: set autocapture configs as a mutable set of options

* fix: fix lint

* refactor: refactor to use simple remove and add instead of augmented assignment

* refactor: remove redundant code

* feat: add experimental annotation to element interactions option

* fix: fix failing test

* fix: fix failing test

* fix: fix failing test

* feat: make autocapture options immutable and discard changes to defaultTracking options

* fix: fix lint

* test: add test for deprecated parameter.

* Revert "fix: fix lint"

This reverts commit afad034.

* feat: make changes to defaultTracking and trackingSessionEvents effective for autocapture

* fix: add a secondary constructor for Configuration to deprecate defaultTracking

* fix: fix the bug when a new DefaultTrackingOptions is passed to the Configuration

* test: add test for deprecation logic

* test: add test for deprecation logic

* fix: changes to the default tracking options replace the recent autocapture options entirely.

* fix: fix lint
  • Loading branch information
PouriaAmini authored Aug 7, 2024
1 parent 4b834bd commit dcb9393
Show file tree
Hide file tree
Showing 20 changed files with 524 additions and 104 deletions.
66 changes: 66 additions & 0 deletions android/src/main/java/com/amplitude/android/AutocaptureOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.amplitude.android

/**
* Autocapture options to enable or disable specific types of autocapture events.
*/
enum class AutocaptureOption {
/**
* Enable session tracking.
*/
SESSIONS,
/**
* Enable app lifecycle tracking.
*/
APP_LIFECYCLES,
/**
* Enable deep link tracking.
*/
DEEP_LINKS,
/**
* Enable screen view tracking.
*/
SCREEN_VIEWS,
/**
* Enable element interaction tracking.
*/
@ExperimentalAmplitudeFeature
ELEMENT_INTERACTIONS
}

class AutocaptureOptionsBuilder {
private val options = mutableSetOf<AutocaptureOption>()

operator fun AutocaptureOption.unaryPlus() {
options.add(this)
}

val sessions = AutocaptureOption.SESSIONS
val appLifecycles = AutocaptureOption.APP_LIFECYCLES
val deepLinks = AutocaptureOption.DEEP_LINKS
val screenViews = AutocaptureOption.SCREEN_VIEWS
@ExperimentalAmplitudeFeature
val elementInteractions = AutocaptureOption.ELEMENT_INTERACTIONS

fun build(): Set<AutocaptureOption> = options.toSet()
}

/**
* Helper function to create a set of autocapture options.
*
* Example usage:
* ```
* val options = autocaptureOptions {
* +sessions
* +appLifecycles
* +deepLinks
* +screenViews
* +elementInteractions
* }
* ```
*
* @param init Function to build the set of autocapture options.
* @return Set of autocapture options.
*/
fun autocaptureOptions(init: AutocaptureOptionsBuilder.() -> Unit): Set<AutocaptureOption> {
return AutocaptureOptionsBuilder().apply(init).build()
}
113 changes: 109 additions & 4 deletions android/src/main/java/com/amplitude/android/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.amplitude.core.events.Plan
import com.amplitude.id.FileIdentityStorageProvider
import com.amplitude.id.IdentityStorageProvider

open class Configuration @JvmOverloads constructor(
open class Configuration(
apiKey: String,
val context: Context,
override var flushQueueSize: Int = FLUSH_QUEUE_SIZE,
Expand All @@ -39,9 +39,7 @@ 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(),
autocapture: Set<AutocaptureOption> = setOf(AutocaptureOption.SESSIONS),
override var identifyBatchIntervalMillis: Long = IDENTIFY_BATCH_INTERVAL_MILLIS,
override var identifyInterceptStorageProvider: StorageProvider = AndroidStorageProvider(),
override var identityStorageProvider: IdentityStorageProvider = FileIdentityStorageProvider(),
Expand Down Expand Up @@ -76,4 +74,111 @@ open class Configuration @JvmOverloads constructor(
companion object {
const val MIN_TIME_BETWEEN_SESSIONS_MILLIS: Long = 300000
}

@Deprecated("Please use the 'autocapture' parameter instead.")
@JvmOverloads
constructor(
apiKey: String,
context: Context,
flushQueueSize: Int = FLUSH_QUEUE_SIZE,
flushIntervalMillis: Int = FLUSH_INTERVAL_MILLIS,
instanceName: String = DEFAULT_INSTANCE,
optOut: Boolean = false,
storageProvider: StorageProvider = AndroidStorageProvider(),
loggerProvider: LoggerProvider = AndroidLoggerProvider(),
minIdLength: Int? = null,
partnerId: String? = null,
callback: EventCallBack? = null,
flushMaxRetries: Int = FLUSH_MAX_RETRIES,
useBatch: Boolean = false,
serverZone: ServerZone = ServerZone.US,
serverUrl: String? = null,
plan: Plan? = null,
ingestionMetadata: IngestionMetadata? = null,
useAdvertisingIdForDeviceId: Boolean = false,
useAppSetIdForDeviceId: Boolean = false,
newDeviceIdPerInstall: Boolean = false,
trackingOptions: TrackingOptions = TrackingOptions(),
enableCoppaControl: Boolean = false,
locationListening: Boolean = true,
flushEventsOnClose: Boolean = true,
minTimeBetweenSessionsMillis: Long = MIN_TIME_BETWEEN_SESSIONS_MILLIS,
trackingSessionEvents: Boolean = true,
@Suppress("DEPRECATION") defaultTracking: DefaultTrackingOptions = DefaultTrackingOptions(),
identifyBatchIntervalMillis: Long = IDENTIFY_BATCH_INTERVAL_MILLIS,
identifyInterceptStorageProvider: StorageProvider = AndroidStorageProvider(),
identityStorageProvider: IdentityStorageProvider = FileIdentityStorageProvider(),
migrateLegacyData: Boolean = true,
offline: Boolean? = false,
deviceId: String? = null,
sessionId: Long? = null,
) : this(
apiKey,
context,
flushQueueSize,
flushIntervalMillis,
instanceName,
optOut,
storageProvider,
loggerProvider,
minIdLength,
partnerId,
callback,
flushMaxRetries,
useBatch,
serverZone,
serverUrl,
plan,
ingestionMetadata,
useAdvertisingIdForDeviceId,
useAppSetIdForDeviceId,
newDeviceIdPerInstall,
trackingOptions,
enableCoppaControl,
locationListening,
flushEventsOnClose,
minTimeBetweenSessionsMillis,
defaultTracking.autocaptureOptions,
identifyBatchIntervalMillis,
identifyInterceptStorageProvider,
identityStorageProvider,
migrateLegacyData,
offline,
deviceId,
sessionId,
) {
if (!trackingSessionEvents) {
defaultTracking.sessions = false
}
@Suppress("DEPRECATION")
this.defaultTracking = defaultTracking
}

// A backing property to store the autocapture options. Any changes to `trackingSessionEvents`
// or the `defaultTracking` options will be reflected in this property.
private var _autocapture: MutableSet<AutocaptureOption> = autocapture.toMutableSet()
val autocapture: Set<AutocaptureOption> get() = _autocapture

@Deprecated("Please use 'autocapture' instead and set 'AutocaptureOptions.SESSIONS' to enable the option.")
var trackingSessionEvents: Boolean
get() = AutocaptureOption.SESSIONS in _autocapture
set(value) {
if (value) _autocapture.add(AutocaptureOption.SESSIONS)
else _autocapture.remove(AutocaptureOption.SESSIONS)
}

@Suppress("DEPRECATION")
@Deprecated("Please use 'autocapture' instead", ReplaceWith("autocapture"))
// Any changes to the default tracking options replace the recent autocapture options entirely.
var defaultTracking: DefaultTrackingOptions = DefaultTrackingOptions { updateAutocaptureOnPropertyChange() }
set(value) {
field = value
_autocapture = value.autocaptureOptions
value.addPropertyChangeListener { updateAutocaptureOnPropertyChange() }
}

@Suppress("DEPRECATION")
private fun DefaultTrackingOptions.updateAutocaptureOnPropertyChange() {
_autocapture = autocaptureOptions
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package com.amplitude.android

@Suppress("DEPRECATION")
@Deprecated("Use AutocaptureOption instead")
open class DefaultTrackingOptions
@JvmOverloads
constructor(
var sessions: Boolean = true,
var appLifecycles: Boolean = false,
var deepLinks: Boolean = false,
var screenViews: Boolean = false,
sessions: Boolean = true,
appLifecycles: Boolean = false,
deepLinks: Boolean = false,
screenViews: Boolean = false,
) {
var userInteractions = false
@ExperimentalAmplitudeFeature
set

// Prebuilt options for easier usage
companion object {
@JvmField
@Deprecated("Use AutocaptureOption instead.")
val ALL =
DefaultTrackingOptions(
sessions = true,
Expand All @@ -24,6 +23,7 @@ constructor(
)

@JvmField
@Deprecated("Use AutocaptureOption instead.")
val NONE =
DefaultTrackingOptions(
sessions = false,
Expand All @@ -33,14 +33,51 @@ constructor(
)
}

@ExperimentalAmplitudeFeature
constructor(
sessions: Boolean = true,
appLifecycles: Boolean = false,
deepLinks: Boolean = false,
screenViews: Boolean = false,
userInteractions: Boolean = false,
) : this(sessions, appLifecycles, deepLinks, screenViews) {
this.userInteractions = userInteractions
var sessions: Boolean = sessions
set(value) {
field = value
notifyChanged()
}

var appLifecycles: Boolean = appLifecycles
set(value) {
field = value
notifyChanged()
}

var deepLinks: Boolean = deepLinks
set(value) {
field = value
notifyChanged()
}

var screenViews: Boolean = screenViews
set(value) {
field = value
notifyChanged()
}

private val propertyChangeListeners: MutableList<DefaultTrackingOptions.() -> Unit> = mutableListOf()

internal val autocaptureOptions: MutableSet<AutocaptureOption>
get() = mutableSetOf<AutocaptureOption>().apply {
if (sessions) add(AutocaptureOption.SESSIONS)
if (appLifecycles) add(AutocaptureOption.APP_LIFECYCLES)
if (deepLinks) add(AutocaptureOption.DEEP_LINKS)
if (screenViews) add(AutocaptureOption.SCREEN_VIEWS)
}

private fun notifyChanged() {
for (listener in propertyChangeListeners) {
this.listener()
}
}

internal constructor(listener: (DefaultTrackingOptions.() -> Unit)) : this() {
propertyChangeListeners.add(listener)
}

internal fun addPropertyChangeListener(listener: DefaultTrackingOptions.() -> Unit) {
propertyChangeListeners.add(listener)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ package com.amplitude.android
"This feature is experimental, and may change or break at any time. Use with caution.",
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CONSTRUCTOR)
annotation class ExperimentalAmplitudeFeature
4 changes: 1 addition & 3 deletions android/src/main/java/com/amplitude/android/Timeline.kt
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ class Timeline(
private suspend fun startNewSession(timestamp: Long): Iterable<BaseEvent> {
val sessionEvents = mutableListOf<BaseEvent>()
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
val trackingSessionEvents = AutocaptureOption.SESSIONS in configuration.autocapture

// end previous session
if (trackingSessionEvents && inSession()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ data class ViewTarget(
val resourceName: String?,
val tag: String?,
val source: String,
val hierarchy: String?,
) {
private val viewRef: WeakReference<Any> = WeakReference(_view)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import androidx.annotation.VisibleForTesting
import com.amplitude.android.internal.ViewHierarchyScanner.findTarget
import com.amplitude.android.internal.ViewTarget
import com.amplitude.android.internal.locators.ViewTargetLocator
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_CLASS
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_RESOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_SOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ELEMENT_TAG
import com.amplitude.android.utilities.DefaultEventUtils.EventTypes.ELEMENT_CLICKED
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.ACTION
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.HIERARCHY
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.TARGET_CLASS
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.TARGET_RESOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.TARGET_SOURCE
import com.amplitude.android.utilities.DefaultEventUtils.EventProperties.TARGET_TAG
import com.amplitude.android.utilities.DefaultEventUtils.EventTypes.ELEMENT_INTERACTED
import com.amplitude.common.Logger
import java.lang.ref.WeakReference

Expand Down Expand Up @@ -43,15 +45,17 @@ class AutocaptureGestureListener(
) ?: logger.warn("Unable to find click target. No event captured.").let { return false }

mapOf(
ELEMENT_CLASS to target.className,
ELEMENT_RESOURCE to target.resourceName,
ELEMENT_TAG to target.tag,
ELEMENT_SOURCE to
ACTION to "touch",
TARGET_CLASS to target.className,
TARGET_RESOURCE to target.resourceName,
TARGET_TAG to target.tag,
TARGET_SOURCE to
target.source
.replace("_", " ")
.split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
).let { track(ELEMENT_CLICKED, it) }
HIERARCHY to target.hierarchy,
).let { track(ELEMENT_INTERACTED, it) }

return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ internal class AndroidViewTargetLocator : ViewTargetLocator {
private val coordinates = IntArray(2)

companion object {
private const val HIERARCHY_DELIMITER = ""

private const val SOURCE = "android_view"
}

Expand All @@ -24,7 +26,8 @@ internal class AndroidViewTargetLocator : ViewTargetLocator {
private fun View.createViewTarget(): ViewTarget {
val className = javaClass.canonicalName ?: javaClass.simpleName ?: null
val resourceName = resourceIdWithFallback
return ViewTarget(this, className, resourceName, null, SOURCE)
val hierarchy = hierarchy
return ViewTarget(this, className, resourceName, null, SOURCE, hierarchy)
}

private fun View.touchWithinBounds(position: Pair<Float, Float>): Boolean {
Expand All @@ -43,4 +46,15 @@ internal class AndroidViewTargetLocator : ViewTargetLocator {
private fun View.isViewTappable(): Boolean {
return isClickable && visibility == View.VISIBLE
}

private val View.hierarchy: String
get() {
val hierarchy = mutableListOf<String>()
var currentView: View? = this
while (currentView != null) {
hierarchy.add(currentView.javaClass.simpleName)
currentView = currentView.parent as? View
}
return hierarchy.joinToString(separator = HIERARCHY_DELIMITER)
}
}
Loading

0 comments on commit dcb9393

Please sign in to comment.