[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))
components.analytics.metricsStorage.tryRegisterAsUsageRecorder(this)
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.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
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 {
MetricController.create(
listOf(
GleanMetricsService(context),
AdjustMetricsService(
application = context as Application,
storage = DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
),
storage = metricsStorage,
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.
*/
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 -> {
metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
metrics.track(Event.GrowthData.UsageThreshold)
}
else -> Unit
}

@ -4,11 +4,14 @@
package org.mozilla.fenix.components.metrics
import android.app.Activity
import android.app.Application
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
@ -29,6 +32,18 @@ interface MetricsStorage {
* Updates locally-stored state for an [event] that has just been sent.
*/
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(
@ -62,6 +77,10 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
currentTime.duringFirstMonth() && !settings.adClickGrowthSent
}
Event.GrowthData.UsageThreshold -> {
!settings.usageTimeGrowthSent &&
settings.usageTimeGrowthData > usageThresholdMillis
}
}
}
@ -76,9 +95,23 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SerpAdClicked -> {
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() {
val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US)
@ -121,6 +154,8 @@ internal class DefaultMetricsStorage(
calendar.timeInMillis = this
}
private fun Long.duringFirstDay() = this < getInstalledTime() + dayMillis
private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
@ -129,6 +164,28 @@ internal class DefaultMetricsStorage(
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 {
private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val shortestMonthMillis: Long = dayMillis * 28
@ -137,6 +194,9 @@ internal class DefaultMetricsStorage(
// of the 7th day after install
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:
* - 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),
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_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_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 -->
<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
import android.app.Activity
import android.app.Application
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.StandardTestDispatcher
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.assertTrue
import org.junit.Before
@ -23,6 +28,7 @@ class DefaultMetricsStorageTest {
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
private val calendarStart = Calendar.getInstance(Locale.US)
private val dayMillis: Long = 1000 * 60 * 60 * 24
private val usageThresholdMillis: Long = 340 * 1000
private var checkDefaultBrowser = false
private val doCheckDefaultBrowser = { checkDefaultBrowser }
@ -229,6 +235,89 @@ class DefaultMetricsStorageTest {
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.createNextDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, 1)

Loading…
Cancel
Save