For #27950: add first week days of use growth data

This commit is contained in:
MatthewTighe 2022-11-21 13:18:34 -08:00 committed by mergify[bot]
parent bca7ecc7d6
commit 1fd2bab054
6 changed files with 234 additions and 4 deletions

View File

@ -31,5 +31,10 @@ 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 the first time Firefox is used 3 days in a row in the first week of install.
*/
object FirstWeekSeriesActivity : GrowthData("20ay7u")
}
}

View File

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

View File

@ -12,6 +12,9 @@ import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
/**
* Interface defining functions around persisted local state for certain metrics.
@ -33,13 +36,20 @@ internal class DefaultMetricsStorage(
private val settings: Settings,
private val checkDefaultBrowser: () -> Boolean,
private val shouldSendGenerally: () -> Boolean = { shouldSendGenerally(context) },
private val getInstalledTime: () -> Long = { getInstalledTime(context) },
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : MetricsStorage {
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
/**
* Checks local state to see whether the [event] should be sent.
*/
override suspend fun shouldTrack(event: Event): Boolean =
withContext(dispatcher) {
// The side-effect of storing days of use needs to happen during the first two days after
// install, which would normally be skipped by shouldSendGenerally.
updateDaysOfUse()
shouldSendGenerally() && when (event) {
Event.GrowthData.SetAsDefault -> {
!settings.setAsDefaultGrowthSent && checkDefaultBrowser()
@ -50,6 +60,9 @@ internal class DefaultMetricsStorage(
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
}
Event.GrowthData.FirstWeekSeriesActivity -> {
shouldTrackFirstWeekActivity()
}
}
}
@ -64,21 +77,80 @@ internal class DefaultMetricsStorage(
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent = System.currentTimeMillis()
}
Event.GrowthData.FirstWeekSeriesActivity -> {
settings.firstWeekSeriesGrowthSent = true
}
}
}
private fun updateDaysOfUse() {
val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US)
val currentDateString = dateFormatter.format(currentDate.time)
if (currentDate.timeInMillis.withinFirstWeek() && daysOfUse.none { it == currentDateString }) {
settings.firstWeekDaysOfUseGrowthData = daysOfUse + currentDateString
}
}
private fun shouldTrackFirstWeekActivity(): Boolean = Result.runCatching {
if (!System.currentTimeMillis().withinFirstWeek() || settings.firstWeekSeriesGrowthSent) {
return false
}
val daysOfUse = settings.firstWeekDaysOfUseGrowthData.map {
dateFormatter.parse(it)
}.sorted()
// This loop will check whether the existing list of days of use, combined with the
// current date, contains any periods of 3 days of use in a row.
for (idx in daysOfUse.indices) {
if (idx + 1 > daysOfUse.lastIndex || idx + 2 > daysOfUse.lastIndex) {
continue
}
val referenceDate = daysOfUse[idx]!!.time.toCalendar()
val secondDateEntry = daysOfUse[idx + 1]!!.time.toCalendar()
val thirdDateEntry = daysOfUse[idx + 2]!!.time.toCalendar()
val oneDayAfterReference = referenceDate.createNextDay()
val twoDaysAfterReference = oneDayAfterReference.createNextDay()
if (oneDayAfterReference == secondDateEntry && thirdDateEntry == twoDaysAfterReference) {
return true
}
}
return false
}.getOrDefault(false)
private fun Long.hasBeenMoreThanDaySince(): Boolean =
System.currentTimeMillis() - this > dayMillis
private fun Long.toCalendar(): Calendar = Calendar.getInstance(Locale.US).also { calendar ->
calendar.timeInMillis = this
}
private fun Long.withinFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Calendar.createNextDay() = (this.clone() as Calendar).also { calendar ->
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val windowStartMillis: Long = dayMillis * 2
private const val windowEndMillis: Long = dayMillis * 28
// Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length
// of the 7th day after install
private const val fullWeekMillis: Long = dayMillis * 8
/**
* Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign
* - user is within 2-28 days of install
* - tracking is still enabled through Nimbus
*/
fun shouldSendGenerally(context: Context): Boolean {
val installedTime = context.packageManager
.getPackageInfoCompat(context.packageName, 0)
.firstInstallTime
val installedTime = getInstalledTime(context)
val timeDifference = System.currentTimeMillis() - installedTime
val withinWindow = timeDifference in windowStartMillis..windowEndMillis
@ -86,5 +158,9 @@ internal class DefaultMetricsStorage(
FxNimbus.features.growthData.value().enabled &&
withinWindow
}
fun getInstalledTime(context: Context): Long = context.packageManager
.getPackageInfoCompat(context.packageName, 0)
.firstInstallTime
}
}

View File

@ -1444,4 +1444,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
key = appContext.getPreferenceKey(R.string.pref_key_growth_uri_load_last_sent),
default = 0,
)
var firstWeekSeriesGrowthSent by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_first_week_series_sent),
default = false,
)
var firstWeekDaysOfUseGrowthData by stringSetPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_first_week_days_of_use),
default = setOf(),
)
}

View File

@ -316,4 +316,6 @@
<string name="pref_key_growth_set_as_default" translatable="false">pref_key_growth_set_as_default</string>
<string name="pref_key_growth_resume_last_sent" translatable="false">pref_key_growth_last_resumed</string>
<string name="pref_key_growth_uri_load_last_sent" translatable="false">pref_key_growth_uri_load_last_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>
</resources>

View File

@ -14,13 +14,22 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.utils.Settings
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
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 var checkDefaultBrowser = false
private val doCheckDefaultBrowser = { checkDefaultBrowser }
private var shouldSendGenerally = true
private val doShouldSendGenerally = { shouldSendGenerally }
private var installTime = 0L
private val doGetInstallTime = { installTime }
private val settings = mockk<Settings>()
@ -32,7 +41,12 @@ class DefaultMetricsStorageTest {
fun setup() {
checkDefaultBrowser = false
shouldSendGenerally = true
storage = DefaultMetricsStorage(mockk(), settings, doCheckDefaultBrowser, doShouldSendGenerally, dispatcher)
installTime = System.currentTimeMillis()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf()
every { settings.firstWeekDaysOfUseGrowthData = any() } returns Unit
storage = DefaultMetricsStorage(mockk(), settings, doCheckDefaultBrowser, doShouldSendGenerally, doGetInstallTime, dispatcher)
}
@Test
@ -147,4 +161,126 @@ class DefaultMetricsStorageTest {
assertTrue(updateSlot.captured > 0)
}
@Test
fun `GIVEN that app has been used for less than 3 days in a row WHEN checked for first week activity THEN event will not be sent`() = runTest(dispatcher) {
val tomorrow = calendarStart.createNextDay()
every { settings.firstWeekDaysOfUseGrowthData = any() } returns Unit
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(calendarStart, tomorrow).toStrings()
every { settings.firstWeekSeriesGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertFalse(result)
}
@Test
fun `GIVEN that app has only been used for 3 days in a row WHEN checked for first week activity THEN event will be sent`() = runTest(dispatcher) {
val tomorrow = calendarStart.createNextDay()
val thirdDay = tomorrow.createNextDay()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(calendarStart, tomorrow, thirdDay).toStrings()
every { settings.firstWeekSeriesGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertTrue(result)
}
@Test
fun `GIVEN that app has been used for 3 days but not consecutively WHEN checked for first week activity THEN event will be not sent`() = runTest(dispatcher) {
val tomorrow = calendarStart.createNextDay()
val fourDaysFromNow = tomorrow.createNextDay().createNextDay()
every { settings.firstWeekDaysOfUseGrowthData = any() } returns Unit
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(calendarStart, tomorrow, fourDaysFromNow).toStrings()
every { settings.firstWeekSeriesGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertFalse(result)
}
@Test
fun `GIVEN that app has been used for 3 days consecutively but not within first week WHEN checked for first week activity THEN event will be not sent`() = runTest(dispatcher) {
val tomorrow = calendarStart.createNextDay()
val thirdDay = tomorrow.createNextDay()
val installTime9DaysEarlier = calendarStart.timeInMillis - (dayMillis * 9)
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(calendarStart, tomorrow, thirdDay).toStrings()
every { settings.firstWeekSeriesGrowthSent } returns false
installTime = installTime9DaysEarlier
val result = storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertFalse(result)
}
@Test
fun `GIVEN that first week activity has already been sent WHEN checked for first week activity THEN event will be not sent`() = runTest(dispatcher) {
val tomorrow = calendarStart.createNextDay()
val thirdDay = tomorrow.createNextDay()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(calendarStart, tomorrow, thirdDay).toStrings()
every { settings.firstWeekSeriesGrowthSent } returns true
val result = storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertFalse(result)
}
@Test
fun `GIVEN that first week activity is not sent WHEN checked to send THEN current day is added to rolling days`() = runTest(dispatcher) {
val captureRolling = slot<Set<String>>()
val previousDay = calendarStart.createPreviousDay()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf(previousDay).toStrings()
every { settings.firstWeekDaysOfUseGrowthData = capture(captureRolling) } returns Unit
every { settings.firstWeekSeriesGrowthSent } returns false
storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertTrue(captureRolling.captured.contains(formatter.format(calendarStart.time)))
}
@Test
fun `WHEN first week activity state updated THEN settings updated accordingly`() = runTest(dispatcher) {
val captureSent = slot<Boolean>()
every { settings.firstWeekSeriesGrowthSent } returns false
every { settings.firstWeekSeriesGrowthSent = capture(captureSent) } returns Unit
storage.updateSentState(Event.GrowthData.FirstWeekSeriesActivity)
assertTrue(captureSent.captured)
}
@Test
fun `GIVEN not yet in recording window WHEN checking to track THEN days of use still updated`() = runTest(dispatcher) {
shouldSendGenerally = false
val captureSlot = slot<Set<String>>()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf()
every { settings.firstWeekDaysOfUseGrowthData = capture(captureSlot) } returns Unit
storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertTrue(captureSlot.captured.isNotEmpty())
}
@Test
fun `GIVEN outside first week after install WHEN checking to track THEN days of use is not updated`() = runTest(dispatcher) {
val captureSlot = slot<Set<String>>()
every { settings.firstWeekDaysOfUseGrowthData } returns setOf()
every { settings.firstWeekDaysOfUseGrowthData = capture(captureSlot) } returns Unit
installTime = calendarStart.timeInMillis - (dayMillis * 9)
storage.shouldTrack(Event.GrowthData.FirstWeekSeriesActivity)
assertFalse(captureSlot.isCaptured)
}
private fun Calendar.copy() = clone() as Calendar
private fun Calendar.createNextDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, 1)
}
private fun Calendar.createPreviousDay() = copy().apply {
add(Calendar.DAY_OF_MONTH, -1)
}
private fun Set<Calendar>.toStrings() = map {
formatter.format(it.time)
}.toSet()
}