Bug 1830401 - add Adjust event for Activated Users

fenix/114.0
Harrison Oglesby 12 months ago
parent bdf2cb4c40
commit fa51e99d75

@ -90,6 +90,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.GrowthDataWorker
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
@ -452,6 +453,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
Events.defaultBrowserChanged.record(NoExtras())
}
GrowthDataWorker.sendActivatedSignalIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
MessageNotificationWorker.setMessageNotificationWorker(applicationContext)
}

@ -88,9 +88,13 @@ class AdjustMetricsService(
override fun track(event: Event) {
CoroutineScope(dispatcher).launch {
try {
if (event is Event.GrowthData && storage.shouldTrack(event)) {
Adjust.trackEvent(AdjustEvent(event.tokenName))
storage.updateSentState(event)
if (event is Event.GrowthData) {
if (storage.shouldTrack(event)) {
Adjust.trackEvent(AdjustEvent(event.tokenName))
storage.updateSentState(event)
} else {
storage.updatePersistentState(event)
}
}
} catch (e: Exception) {
crashReporter.submitCaughtException(e)

@ -46,5 +46,12 @@ sealed class Event {
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
*/
object FirstUriLoadForDay : GrowthData("ja86ek")
/**
* Event recording when User is "activated" in first week of usage.
* Activated = if the user is active 3 days in their first week and
* if they search once in the latter half of that week (days 4-7).
*/
data class UserActivated(val fromSearch: Boolean) : GrowthData("imgpmr")
}
}

@ -0,0 +1,74 @@
package org.mozilla.fenix.components.metrics
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.settings
import java.util.concurrent.TimeUnit
/**
* Worker that will send the User Activated event at the end of the first week.
*/
class GrowthDataWorker(
context: Context,
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
override fun doWork(): Result {
val settings = applicationContext.settings()
if (!System.currentTimeMillis().isAfterFirstWeekFromInstall(applicationContext) ||
settings.growthUserActivatedSent
) {
return Result.success()
}
applicationContext.metrics.track(Event.GrowthData.UserActivated(fromSearch = false))
return Result.success()
}
companion object {
private const val GROWTH_USER_ACTIVATED_WORK_NAME = "org.mozilla.fenix.growth.work"
private const val DAY_MILLIS: Long = 1000 * 60 * 60 * 24
private const val FULL_WEEK_MILLIS: Long = DAY_MILLIS * 7
/**
* Schedules the Activated User event if needed.
*/
fun sendActivatedSignalIfNeeded(context: Context) {
val instanceWorkManager = WorkManager.getInstance(context)
if (context.settings().growthUserActivatedSent) {
return
}
val growthSignalWork = OneTimeWorkRequest.Builder(GrowthDataWorker::class.java)
.setInitialDelay(FULL_WEEK_MILLIS, TimeUnit.MILLISECONDS)
.build()
instanceWorkManager.beginUniqueWork(
GROWTH_USER_ACTIVATED_WORK_NAME,
ExistingWorkPolicy.KEEP,
growthSignalWork,
).enqueue()
}
/**
* Returns [Boolean] value signaling if current time is after the first week after install.
*/
private fun Long.isAfterFirstWeekFromInstall(context: Context): Boolean {
val timeDifference = this - getInstalledTime(context)
return (FULL_WEEK_MILLIS <= timeDifference)
}
private fun getInstalledTime(context: Context): Long = context.packageManager
.getPackageInfoCompat(context.packageName, 0)
.firstInstallTime
}
}

@ -277,6 +277,7 @@ internal class ReleaseMetricController(
}
Component.FEATURE_SEARCH to InContentTelemetry.IN_CONTENT_SEARCH -> {
BrowserSearch.inContent[value!!].add()
track(Event.GrowthData.UserActivated(fromSearch = true))
}
Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> {
metadata?.get("installed")?.let { installedAddons ->

@ -30,6 +30,7 @@ class MetricsMiddleware(
metrics.track(Event.GrowthData.FirstAppOpenForDay)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
metrics.track(Event.GrowthData.UsageThreshold)
metrics.track(Event.GrowthData.UserActivated(fromSearch = false))
}
else -> Unit
}

@ -33,6 +33,11 @@ interface MetricsStorage {
*/
suspend fun updateSentState(event: Event)
/**
* Updates locally-stored data related to an [event] that has just been sent.
*/
suspend fun updatePersistentState(event: Event)
/**
* Will try to register this as a recorder of app usage based on whether usage recording is still
* needed. It will measure usage by to monitoring lifecycle callbacks from [application]'s
@ -60,6 +65,7 @@ internal class DefaultMetricsStorage(
/**
* Checks local state to see whether the [event] should be sent.
*/
@Suppress("ComplexMethod", "CyclomaticComplexMethod")
override suspend fun shouldTrack(event: Event): Boolean =
withContext(dispatcher) {
// The side-effect of storing days of use always needs to happen.
@ -91,6 +97,9 @@ internal class DefaultMetricsStorage(
currentTime.duringFirstMonth() &&
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
}
is Event.GrowthData.UserActivated -> {
hasUserReachedActivatedThreshold()
}
}
}
@ -114,6 +123,23 @@ internal class DefaultMetricsStorage(
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent = System.currentTimeMillis()
}
is Event.GrowthData.UserActivated -> {
settings.growthUserActivatedSent = true
}
}
}
override suspend fun updatePersistentState(event: Event) {
when (event) {
is Event.GrowthData.UserActivated -> {
if (event.fromSearch && shouldUpdateSearchUsage()) {
settings.growthEarlySearchUsed = true
} else if (!event.fromSearch && shouldUpdateUsageCount()) {
settings.growthEarlyUseCount.increment()
settings.growthEarlyUseCountLastIncrement = System.currentTimeMillis()
}
}
else -> Unit
}
}
@ -176,6 +202,8 @@ internal class DefaultMetricsStorage(
private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis
private fun Long.afterThirdDay() = this > getInstalledTime() + threeDayMillis
private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
@ -184,6 +212,25 @@ internal class DefaultMetricsStorage(
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
private fun hasUserReachedActivatedThreshold(): Boolean {
return !settings.growthUserActivatedSent &&
settings.growthEarlyUseCount.value >= daysActivatedThreshold &&
settings.growthEarlySearchUsed
}
private fun shouldUpdateUsageCount(): Boolean {
val currentTime = System.currentTimeMillis()
return currentTime.afterFirstDay() &&
currentTime.duringFirstWeek() &&
settings.growthEarlyUseCountLastIncrement.hasBeenMoreThanDaySince()
}
private fun shouldUpdateSearchUsage(): Boolean {
val currentTime = System.currentTimeMillis()
return currentTime.afterThirdDay() &&
currentTime.duringFirstWeek()
}
/**
* This will store app usage time to disk, based on Resume and Pause lifecycle events. Currently,
* there is only interest in usage during the first day after install.
@ -208,6 +255,7 @@ internal class DefaultMetricsStorage(
companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val threeDayMillis: Long = 3 * dayMillis
private const val shortestMonthMillis: Long = dayMillis * 28
// Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length
@ -217,6 +265,9 @@ internal class DefaultMetricsStorage(
// The usage threshold we are interested in is currently 340 seconds.
private const val usageThresholdMillis = 1000 * 340
// The usage threshold for "activated" growth users.
private const val daysActivatedThreshold = 3
/**
* Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign

@ -1735,4 +1735,33 @@ class Settings(private val appContext: Context) : PreferencesHolder {
key = appContext.getPreferenceKey(R.string.pref_key_enable_tabs_tray_to_compose),
default = FeatureFlags.composeTabsTray,
)
/**
* Adjust Activated User sent
*/
var growthUserActivatedSent by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_user_activated_sent),
default = false,
)
/**
* Indicates how many days in the first week user opened the app.
*/
val growthEarlyUseCount = counterPreference(
appContext.getPreferenceKey(R.string.pref_key_growth_early_browse_count),
maxCount = 3,
)
var growthEarlyUseCountLastIncrement by longPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_early_browse_count_last_increment),
default = 0L,
)
/**
* Indicates how many days in the first week user searched in the app.
*/
var growthEarlySearchUsed by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_early_search),
default = false,
)
}

@ -290,6 +290,12 @@
<string name="pref_key_show_collections_placeholder_home" translatable="false">pref_key_show_collections_home</string>
<!-- Adjust Activated User values-->
<string name="pref_key_growth_user_activated_sent" translatable="false">pref_key_growth_user_activated_sent</string>
<string name="pref_key_growth_early_browse_count" translatable="false">pref_key_growth_early_browse_count</string>
<string name="pref_key_growth_early_browse_count_last_increment" translatable="false">pref_key_growth_early_browse_count_last_increment</string>
<string name="pref_key_growth_early_search" translatable="false">pref_key_growth_early_search</string>
<!-- Tabs Settings -->
<!-- pref_key_tab_view_list_do_not_use is needed for UI implementation only and should be expected to be useful. -->
<string name="pref_key_tab_view_list_do_not_use" translatable="false">pref_key_tab_view_list</string>

@ -6,7 +6,9 @@ package org.mozilla.fenix.components.metrics
import android.app.Activity
import android.app.Application
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
@ -382,6 +384,114 @@ class DefaultMetricsStorageTest {
assertTrue(updateSlot.captured > 0)
}
@Test
fun `GIVEN first week activated days of use and search use thresholds reached THEN will be sent`() = runTest(dispatcher) {
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 5)
every { settings.growthEarlyUseCount.value } returns 3
every { settings.growthEarlySearchUsed } returns true
every { settings.growthUserActivatedSent } returns false
val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false))
assertTrue(result)
}
@Test
fun `GIVEN first week activated days of use threshold not reached THEN will not be sent`() = runTest(dispatcher) {
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 5)
every { settings.growthEarlyUseCount.value } returns 1
every { settings.growthEarlySearchUsed } returns true
every { settings.growthUserActivatedSent } returns false
val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false))
assertFalse(result)
}
@Test
fun `GIVEN first week activated search use threshold not reached THEN will not be sent`() = runTest(dispatcher) {
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 5)
every { settings.growthEarlyUseCount.value } returns 3
every { settings.growthEarlySearchUsed } returns false
every { settings.growthUserActivatedSent } returns false
val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false))
assertFalse(result)
}
@Test
fun `GIVEN first week activated already sent WHEN first week activated signal sent THEN userActivated will not be sent`() = runTest(dispatcher) {
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 5)
every { settings.growthEarlyUseCount.value } returns 3
every { settings.growthEarlySearchUsed } returns true
every { settings.growthUserActivatedSent } returns true
val result = storage.shouldTrack(Event.GrowthData.UserActivated(fromSearch = false))
assertFalse(result)
}
@Test
fun `WHEN first week usage signal is sent a full day after last sent THEN settings will be updated accordingly`() = runTest(dispatcher) {
val captureSent = slot<Long>()
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 3)
every { settings.growthEarlyUseCount.value } returns 1
every { settings.growthEarlyUseCount.increment() } just Runs
every { settings.growthEarlyUseCountLastIncrement } returns 0L
every { settings.growthEarlyUseCountLastIncrement = capture(captureSent) } returns Unit
storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = false))
assertTrue(captureSent.captured > 0L)
}
@Test
fun `WHEN first week usage signal is sent less than a full day after last sent THEN settings will not be updated`() = runTest(dispatcher) {
val captureSent = slot<Long>()
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 3)
val lastUsageIncrementTime = currentTime - (dayMillis / 2)
every { settings.growthEarlyUseCount.value } returns 1
every { settings.growthEarlyUseCountLastIncrement } returns lastUsageIncrementTime
every { settings.growthEarlyUseCountLastIncrement = capture(captureSent) } returns Unit
storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = false))
assertFalse(captureSent.isCaptured)
}
@Test
fun `WHEN first week search activity is sent in second half of first week THEN settings will be updated`() = runTest(dispatcher) {
val captureSent = slot<Boolean>()
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 3) - 100
every { settings.growthEarlySearchUsed } returns false
every { settings.growthEarlySearchUsed = capture(captureSent) } returns Unit
storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = true))
assertTrue(captureSent.captured)
}
@Test
fun `WHEN first week search activity is sent in first half of first week THEN settings will not be updated`() = runTest(dispatcher) {
val captureSent = slot<Boolean>()
val currentTime = System.currentTimeMillis()
installTime = currentTime - (dayMillis * 3) + 100
every { settings.growthEarlySearchUsed } returns false
every { settings.growthEarlySearchUsed = capture(captureSent) } returns Unit
storage.updatePersistentState(Event.GrowthData.UserActivated(fromSearch = true))
assertFalse(captureSent.isCaptured)
}
private fun Calendar.copy() = clone() as Calendar
private fun Calendar.createNextDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, 1)

Loading…
Cancel
Save