mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-05 21:20:45 +00:00
For #27950: add first week days of use growth data
This commit is contained in:
parent
bca7ecc7d6
commit
1fd2bab054
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user