[fenix] Bug 1812204 - Add usage growth data event

pull/600/head
MatthewTighe 1 year ago committed by mergify[bot]
parent da781ab7bf
commit 96e95ad1fb

@ -259,6 +259,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store)) ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
downloadWallpapers() downloadWallpapers()
} }

@ -25,6 +25,7 @@ import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.DefaultMetricsStorage import org.mozilla.fenix.components.metrics.DefaultMetricsStorage
import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
@ -117,17 +118,21 @@ class Analytics(
) )
} }
val metricsStorage: MetricsStorage by lazyMonitored {
DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
)
}
val metrics: MetricController by lazyMonitored { val metrics: MetricController by lazyMonitored {
MetricController.create( MetricController.create(
listOf( listOf(
GleanMetricsService(context), GleanMetricsService(context),
AdjustMetricsService( AdjustMetricsService(
application = context as Application, application = context as Application,
storage = DefaultMetricsStorage( storage = metricsStorage,
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
),
crashReporter = crashReporter, crashReporter = crashReporter,
), ),
), ),

@ -31,5 +31,10 @@ sealed class Event {
* Event recording the first time Firefox is used 3 days in a row in the first week of install. * Event recording the first time Firefox is used 3 days in a row in the first week of install.
*/ */
object FirstWeekSeriesActivity : GrowthData("20ay7u") object FirstWeekSeriesActivity : GrowthData("20ay7u")
/**
* Event recording that usage time has reached a threshold.
*/
object UsageThreshold : GrowthData("m66prt")
} }
} }

@ -28,6 +28,7 @@ class MetricsMiddleware(
is AppAction.ResumedMetricsAction -> { is AppAction.ResumedMetricsAction -> {
metrics.track(Event.GrowthData.SetAsDefault) metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity) metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
metrics.track(Event.GrowthData.UsageThreshold)
} }
else -> Unit else -> Unit
} }

@ -4,11 +4,14 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import android.app.Activity
import android.app.Application
import android.content.Context import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.support.utils.ext.getPackageInfoCompat import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -29,6 +32,18 @@ interface MetricsStorage {
* Updates locally-stored state for an [event] that has just been sent. * Updates locally-stored state for an [event] that has just been sent.
*/ */
suspend fun updateSentState(event: Event) suspend fun updateSentState(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
* activities and should update local state using [updateUsageState].
*/
fun tryRegisterAsUsageRecorder(application: Application)
/**
* Update local state with a [usageLength] measurement.
*/
fun updateUsageState(usageLength: Long)
} }
internal class DefaultMetricsStorage( internal class DefaultMetricsStorage(
@ -62,6 +77,10 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> { Event.GrowthData.SerpAdClicked -> {
currentTime.duringFirstMonth() && !settings.adClickGrowthSent currentTime.duringFirstMonth() && !settings.adClickGrowthSent
} }
Event.GrowthData.UsageThreshold -> {
!settings.usageTimeGrowthSent &&
settings.usageTimeGrowthData > usageThresholdMillis
}
} }
} }
@ -76,9 +95,23 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> { Event.GrowthData.SerpAdClicked -> {
settings.adClickGrowthSent = true settings.adClickGrowthSent = true
} }
Event.GrowthData.UsageThreshold -> {
settings.usageTimeGrowthSent = true
}
}
}
override fun tryRegisterAsUsageRecorder(application: Application) {
// Currently there is only interest in measuring usage during the first day of install.
if (!settings.usageTimeGrowthSent && System.currentTimeMillis().duringFirstDay()) {
application.registerActivityLifecycleCallbacks(UsageRecorder(this))
} }
} }
override fun updateUsageState(usageLength: Long) {
settings.usageTimeGrowthData += usageLength
}
private fun updateDaysOfUse() { private fun updateDaysOfUse() {
val daysOfUse = settings.firstWeekDaysOfUseGrowthData val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US) val currentDate = Calendar.getInstance(Locale.US)
@ -121,6 +154,8 @@ internal class DefaultMetricsStorage(
calendar.timeInMillis = this calendar.timeInMillis = this
} }
private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis
private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
@ -129,6 +164,28 @@ internal class DefaultMetricsStorage(
calendar.add(Calendar.DAY_OF_MONTH, 1) calendar.add(Calendar.DAY_OF_MONTH, 1)
} }
/**
* 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.
*/
internal class UsageRecorder(
private val metricsStorage: MetricsStorage,
) : DefaultActivityLifecycleCallbacks {
private val activityStartTimes: MutableMap<String, Long?> = mutableMapOf()
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
activityStartTimes[activity.componentName.toString()] = System.currentTimeMillis()
}
override fun onActivityPaused(activity: Activity) {
super.onActivityPaused(activity)
val startTime = activityStartTimes[activity.componentName.toString()] ?: return
val elapsedTimeMillis = System.currentTimeMillis() - startTime
metricsStorage.updateUsageState(elapsedTimeMillis)
}
}
companion object { companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24 private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val shortestMonthMillis: Long = dayMillis * 28 private const val shortestMonthMillis: Long = dayMillis * 28
@ -137,6 +194,9 @@ internal class DefaultMetricsStorage(
// of the 7th day after install // of the 7th day after install
private const val fullWeekMillis: Long = dayMillis * 8 private const val fullWeekMillis: Long = dayMillis * 8
// The usage threshold we are interested in is currently 340 seconds.
private const val usageThresholdMillis = 1000 * 340
/** /**
* Determines whether events should be tracked based on some general criteria: * Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign * - user has installed as a result of a campaign

@ -1557,4 +1557,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
key = appContext.getPreferenceKey(R.string.pref_key_growth_ad_click_sent), key = appContext.getPreferenceKey(R.string.pref_key_growth_ad_click_sent),
default = false, default = false,
) )
var usageTimeGrowthData by longPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_usage_time),
default = -1,
)
var usageTimeGrowthSent by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_usage_time_sent),
default = false,
)
} }

@ -322,6 +322,8 @@
<string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string> <string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string>
<string name="pref_key_growth_first_week_days_of_use" translatable="false">pref_key_growth_first_week_days_of_use</string> <string name="pref_key_growth_first_week_days_of_use" translatable="false">pref_key_growth_first_week_days_of_use</string>
<string name="pref_key_growth_ad_click_sent" translatable="false">pref_key_growth_ad_click_sent</string> <string name="pref_key_growth_ad_click_sent" translatable="false">pref_key_growth_ad_click_sent</string>
<string name="pref_key_growth_usage_time" translatable="false">pref_key_growth_usage_time</string>
<string name="pref_key_growth_usage_time_sent" translatable="false">pref_key_growth_usage_time_sent</string>
<!-- Notification Pre Permission Prompt --> <!-- Notification Pre Permission Prompt -->
<string name="pref_key_notification_pre_permission_prompt_enabled">pref_key_notification_pre_permission_prompt_enabled</string> <string name="pref_key_notification_pre_permission_prompt_enabled">pref_key_notification_pre_permission_prompt_enabled</string>

@ -4,11 +4,16 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import android.app.Activity
import android.app.Application
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
@ -23,6 +28,7 @@ class DefaultMetricsStorageTest {
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
private val calendarStart = Calendar.getInstance(Locale.US) private val calendarStart = Calendar.getInstance(Locale.US)
private val dayMillis: Long = 1000 * 60 * 60 * 24 private val dayMillis: Long = 1000 * 60 * 60 * 24
private val usageThresholdMillis: Long = 340 * 1000
private var checkDefaultBrowser = false private var checkDefaultBrowser = false
private val doCheckDefaultBrowser = { checkDefaultBrowser } private val doCheckDefaultBrowser = { checkDefaultBrowser }
@ -229,6 +235,89 @@ class DefaultMetricsStorageTest {
assertTrue(result) assertTrue(result)
} }
@Test
fun `GIVEN usage time has not passed threshold and has not been sent WHEN checking to track THEN event will not be sent`() = runTest(dispatcher) {
every { settings.usageTimeGrowthData } returns usageThresholdMillis - 1
every { settings.usageTimeGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.UsageThreshold)
assertFalse(result)
}
@Test
fun `GIVEN usage time has passed threshold and has not been sent WHEN checking to track THEN event will be sent`() = runTest(dispatcher) {
every { settings.usageTimeGrowthData } returns usageThresholdMillis + 1
every { settings.usageTimeGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.UsageThreshold)
assertTrue(result)
}
@Test
fun `GIVEN usage time growth has not been sent and within first day WHEN registering as usage recorder THEN will be registered`() {
val application = mockk<Application>()
every { settings.usageTimeGrowthSent } returns false
every { application.registerActivityLifecycleCallbacks(any()) } returns Unit
storage.tryRegisterAsUsageRecorder(application)
verify { application.registerActivityLifecycleCallbacks(any()) }
}
@Test
fun `GIVEN usage time growth has not been sent and not within first day WHEN registering as usage recorder THEN will not be registered`() {
val application = mockk<Application>()
installTime = System.currentTimeMillis() - dayMillis * 2
every { settings.usageTimeGrowthSent } returns false
storage.tryRegisterAsUsageRecorder(application)
verify(exactly = 0) { application.registerActivityLifecycleCallbacks(any()) }
}
@Test
fun `GIVEN usage time growth has been sent WHEN registering as usage recorder THEN will not be registered`() {
val application = mockk<Application>()
every { settings.usageTimeGrowthSent } returns true
storage.tryRegisterAsUsageRecorder(application)
verify(exactly = 0) { application.registerActivityLifecycleCallbacks(any()) }
}
@Test
fun `WHEN updating usage state THEN storage will be delegated to settings`() {
val initial = 10L
val update = 15L
val slot = slot<Long>()
every { settings.usageTimeGrowthData } returns initial
every { settings.usageTimeGrowthData = capture(slot) } returns Unit
storage.updateUsageState(update)
assertEquals(slot.captured, initial + update)
}
@Test
fun `WHEN usage recorder receives onResume and onPause callbacks THEN it will store usage length`() {
val storage = mockk<MetricsStorage>()
val activity = mockk<Activity>()
val slot = slot<Long>()
every { storage.updateUsageState(capture(slot)) } returns Unit
every { activity.componentName } returns mock()
val usageRecorder = DefaultMetricsStorage.UsageRecorder(storage)
val startTime = System.currentTimeMillis()
usageRecorder.onActivityResumed(activity)
usageRecorder.onActivityPaused(activity)
val stopTime = System.currentTimeMillis()
assertTrue(slot.captured < stopTime - startTime)
}
private fun Calendar.copy() = clone() as Calendar private fun Calendar.copy() = clone() as Calendar
private fun Calendar.createNextDay() = copy().apply { private fun Calendar.createNextDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, 1) add(Calendar.DAY_OF_MONTH, 1)

Loading…
Cancel
Save