mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/27950: add first week days of use growth data
This commit is contained in:
parent
5ced63a1fa
commit
034172beeb
@ -31,5 +31,10 @@ sealed class Event {
|
|||||||
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
|
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
|
||||||
*/
|
*/
|
||||||
object FirstUriLoadForDay : GrowthData("ja86ek")
|
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 -> {
|
is AppAction.ResumedMetricsAction -> {
|
||||||
metrics.track(Event.GrowthData.SetAsDefault)
|
metrics.track(Event.GrowthData.SetAsDefault)
|
||||||
metrics.track(Event.GrowthData.FirstAppOpenForDay)
|
metrics.track(Event.GrowthData.FirstAppOpenForDay)
|
||||||
|
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,9 @@ import mozilla.components.support.utils.ext.getPackageInfoCompat
|
|||||||
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
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface defining functions around persisted local state for certain metrics.
|
* Interface defining functions around persisted local state for certain metrics.
|
||||||
@ -33,13 +36,20 @@ internal class DefaultMetricsStorage(
|
|||||||
private val settings: Settings,
|
private val settings: Settings,
|
||||||
private val checkDefaultBrowser: () -> Boolean,
|
private val checkDefaultBrowser: () -> Boolean,
|
||||||
private val shouldSendGenerally: () -> Boolean = { shouldSendGenerally(context) },
|
private val shouldSendGenerally: () -> Boolean = { shouldSendGenerally(context) },
|
||||||
|
private val getInstalledTime: () -> Long = { getInstalledTime(context) },
|
||||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) : MetricsStorage {
|
) : MetricsStorage {
|
||||||
|
|
||||||
|
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks local state to see whether the [event] should be sent.
|
* Checks local state to see whether the [event] should be sent.
|
||||||
*/
|
*/
|
||||||
override suspend fun shouldTrack(event: Event): Boolean =
|
override suspend fun shouldTrack(event: Event): Boolean =
|
||||||
withContext(dispatcher) {
|
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) {
|
shouldSendGenerally() && when (event) {
|
||||||
Event.GrowthData.SetAsDefault -> {
|
Event.GrowthData.SetAsDefault -> {
|
||||||
!settings.setAsDefaultGrowthSent && checkDefaultBrowser()
|
!settings.setAsDefaultGrowthSent && checkDefaultBrowser()
|
||||||
@ -50,6 +60,9 @@ internal class DefaultMetricsStorage(
|
|||||||
Event.GrowthData.FirstUriLoadForDay -> {
|
Event.GrowthData.FirstUriLoadForDay -> {
|
||||||
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
|
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
|
||||||
}
|
}
|
||||||
|
Event.GrowthData.FirstWeekSeriesActivity -> {
|
||||||
|
shouldTrackFirstWeekActivity()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,21 +77,80 @@ internal class DefaultMetricsStorage(
|
|||||||
Event.GrowthData.FirstUriLoadForDay -> {
|
Event.GrowthData.FirstUriLoadForDay -> {
|
||||||
settings.uriLoadGrowthLastSent = System.currentTimeMillis()
|
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 =
|
private fun Long.hasBeenMoreThanDaySince(): Boolean =
|
||||||
System.currentTimeMillis() - this > dayMillis
|
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 {
|
companion object {
|
||||||
private const val dayMillis: Long = 1000 * 60 * 60 * 24
|
private const val dayMillis: Long = 1000 * 60 * 60 * 24
|
||||||
private const val windowStartMillis: Long = dayMillis * 2
|
private const val windowStartMillis: Long = dayMillis * 2
|
||||||
private const val windowEndMillis: Long = dayMillis * 28
|
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 {
|
fun shouldSendGenerally(context: Context): Boolean {
|
||||||
val installedTime = context.packageManager
|
val installedTime = getInstalledTime(context)
|
||||||
.getPackageInfoCompat(context.packageName, 0)
|
|
||||||
.firstInstallTime
|
|
||||||
val timeDifference = System.currentTimeMillis() - installedTime
|
val timeDifference = System.currentTimeMillis() - installedTime
|
||||||
val withinWindow = timeDifference in windowStartMillis..windowEndMillis
|
val withinWindow = timeDifference in windowStartMillis..windowEndMillis
|
||||||
|
|
||||||
@ -86,5 +158,9 @@ internal class DefaultMetricsStorage(
|
|||||||
FxNimbus.features.growthData.value().enabled &&
|
FxNimbus.features.growthData.value().enabled &&
|
||||||
withinWindow
|
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),
|
key = appContext.getPreferenceKey(R.string.pref_key_growth_uri_load_last_sent),
|
||||||
default = 0,
|
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_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_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_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>
|
</resources>
|
||||||
|
@ -14,13 +14,22 @@ import org.junit.Assert.assertTrue
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class DefaultMetricsStorageTest {
|
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 var checkDefaultBrowser = false
|
||||||
private val doCheckDefaultBrowser = { checkDefaultBrowser }
|
private val doCheckDefaultBrowser = { checkDefaultBrowser }
|
||||||
private var shouldSendGenerally = true
|
private var shouldSendGenerally = true
|
||||||
private val doShouldSendGenerally = { shouldSendGenerally }
|
private val doShouldSendGenerally = { shouldSendGenerally }
|
||||||
|
private var installTime = 0L
|
||||||
|
private val doGetInstallTime = { installTime }
|
||||||
|
|
||||||
private val settings = mockk<Settings>()
|
private val settings = mockk<Settings>()
|
||||||
|
|
||||||
@ -32,7 +41,12 @@ class DefaultMetricsStorageTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
checkDefaultBrowser = false
|
checkDefaultBrowser = false
|
||||||
shouldSendGenerally = true
|
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
|
@Test
|
||||||
@ -147,4 +161,126 @@ class DefaultMetricsStorageTest {
|
|||||||
|
|
||||||
assertTrue(updateSlot.captured > 0)
|
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