For #27698: add set as default growth data

This commit is contained in:
MatthewTighe 2022-11-03 15:06:38 -07:00 committed by Jonathan Almeida
parent c59b0845a0
commit 2bccb86a9b
14 changed files with 276 additions and 5 deletions

View File

@ -1,4 +1,12 @@
---
growth-data:
description: A feature measuring campaign growth data
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active"
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
hasExposure: true

View File

@ -377,6 +377,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
if (settings().isMarketingTelemetryEnabled) {
components.analytics.metrics.start(MetricServiceType.Marketing)
}
components.appStore.dispatch(AppAction.MetricsInitializedAction)
}
protected open fun setupLeakCanary() {

View File

@ -22,6 +22,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
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.experiments.createNimbus
@ -31,6 +32,7 @@ import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION
@ -119,7 +121,15 @@ class Analytics(
MetricController.create(
listOf(
GleanMetricsService(context),
AdjustMetricsService(context as Application),
AdjustMetricsService(
application = context as Application,
storage = DefaultMetricsStorage(
context = context,
settings = context.settings(),
checkDefaultBrowser = { BrowsersCache.all(context).isDefaultBrowser },
),
crashReporter = crashReporter,
),
),
isDataTelemetryEnabled = { context.settings().isTelemetryEnabled },
isMarketingDataTelemetryEnabled = { context.settings().isMarketingTelemetryEnabled },

View File

@ -25,6 +25,7 @@ import org.mozilla.fenix.autofill.AutofillConfirmActivity
import org.mozilla.fenix.autofill.AutofillSearchActivity
import org.mozilla.fenix.autofill.AutofillUnlockActivity
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.metrics.MetricsMiddleware
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
@ -207,6 +208,7 @@ class Components(private val context: Context) {
context.pocketStoriesSelectedCategoriesDataStore,
),
MessagingMiddleware(messagingStorage = analytics.messagingStorage),
MetricsMiddleware(metrics = analytics.metrics),
),
)
}

View File

@ -191,4 +191,9 @@ sealed class AppAction : Action {
val imageState: Wallpaper.ImageFileState,
) : WallpaperAction()
}
/**
* Indicates that the app's metrics have been initialized and startup data can be sent.
*/
object MetricsInitializedAction : AppAction()
}

View File

@ -220,6 +220,7 @@ internal object AppStoreReducer {
val wallpaperState = state.wallpaperState.copy(availableWallpapers = wallpapers)
state.copy(wallpaperState = wallpaperState)
}
is AppAction.MetricsInitializedAction -> state
}
}

View File

@ -10,12 +10,23 @@ import android.os.Bundle
import android.util.Log
import com.adjust.sdk.Adjust
import com.adjust.sdk.AdjustConfig
import com.adjust.sdk.AdjustEvent
import com.adjust.sdk.LogLevel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.crash.CrashReporter
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.settings
class AdjustMetricsService(private val application: Application) : MetricsService {
class AdjustMetricsService(
private val application: Application,
private val storage: MetricsStorage,
private val crashReporter: CrashReporter,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : MetricsService {
override val type = MetricServiceType.Marketing
override fun start() {
@ -70,9 +81,22 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
Adjust.gdprForgetMe(application.applicationContext)
}
// We're not currently sending events directly to Adjust
override fun track(event: Event) { /* noop */ }
override fun shouldTrack(event: Event): Boolean = false
@Suppress("TooGenericExceptionCaught")
override fun track(event: Event) {
CoroutineScope(dispatcher).launch {
try {
if (event is Event.GrowthData && storage.shouldTrack(event)) {
Adjust.trackEvent(AdjustEvent(event.tokenName))
storage.updateSentState(event)
}
} catch (e: Exception) {
crashReporter.submitCaughtException(e)
}
}
}
override fun shouldTrack(event: Event): Boolean =
event is Event.GrowthData
companion object {
private const val LOGTAG = "AdjustMetricsService"

View File

@ -12,4 +12,14 @@ sealed class Event {
internal open val extras: Map<*, String>?
get() = null
/**
* Events related to growth campaigns.
*/
sealed class GrowthData(val tokenName: String) : Event() {
/**
* Event recording whether Firefox has been set as the default browser.
*/
object SetAsDefault : GrowthData("xgpcgt")
}
}

View File

@ -0,0 +1,29 @@
package org.mozilla.fenix.components.metrics
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState
/**
* A middleware that will map incoming actions to relevant events for [metrics].
*/
class MetricsMiddleware(
private val metrics: MetricController,
) : Middleware<AppState, AppAction> {
override fun invoke(
context: MiddlewareContext<AppState, AppAction>,
next: (AppAction) -> Unit,
action: AppAction,
) {
handleAction(action)
next(action)
}
private fun handleAction(action: AppAction) = when (action) {
is AppAction.MetricsInitializedAction -> {
metrics.track(Event.GrowthData.SetAsDefault)
}
else -> Unit
}
}

View File

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
/**
* Interface defining functions around persisted local state for certain metrics.
*/
interface MetricsStorage {
/**
* Determines whether an [event] should be sent based on locally-stored state.
*/
suspend fun shouldTrack(event: Event): Boolean
/**
* Updates locally-stored state for an [event] that has just been sent.
*/
suspend fun updateSentState(event: Event)
}
internal class DefaultMetricsStorage(
context: Context,
private val settings: Settings,
private val checkDefaultBrowser: () -> Boolean,
private val shouldSendGenerally: () -> Boolean = { shouldSendGenerally(context) },
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : MetricsStorage {
/**
* Checks local state to see whether the [event] should be sent.
*/
override suspend fun shouldTrack(event: Event): Boolean =
withContext(dispatcher) {
shouldSendGenerally() && when (event) {
Event.GrowthData.SetAsDefault -> {
!settings.setAsDefaultGrowthSent && checkDefaultBrowser()
}
}
}
override suspend fun updateSentState(event: Event) = withContext(dispatcher) {
when (event) {
Event.GrowthData.SetAsDefault -> settings.setAsDefaultGrowthSent = true
}
}
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
fun shouldSendGenerally(context: Context): Boolean {
val installedTime = context.packageManager
.getPackageInfo(context.packageName, 0)
.firstInstallTime
val timeDifference = System.currentTimeMillis() - installedTime
val withinWindow = timeDifference in windowStartMillis..windowEndMillis
return context.settings().adjustCampaignId.isNotEmpty() &&
FxNimbus.features.growthData.value().enabled &&
withinWindow
}
}
}

View File

@ -1413,4 +1413,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
HttpsOnlyMode.ENABLED
}
}
var setAsDefaultGrowthSent by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_growth_set_as_default),
default = false,
)
}

View File

@ -307,4 +307,7 @@
<string name="pref_key_history_metadata_feature" translatable="false">pref_key_history_metadata_feature</string>
<string name="pref_key_show_unified_search" translatable="false">pref_key_show_unified_search</string>
<string name="pref_key_custom_glean_server_url" translatable="false">pref_key_custom_glean_server_url</string>
<!-- Growth Data -->
<string name="pref_key_growth_set_as_default" translatable="false">pref_key_growth_set_as_default</string>
</resources>

View File

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.utils.Settings
class DefaultMetricsStorageTest {
private var checkDefaultBrowser = false
private val doCheckDefaultBrowser = { checkDefaultBrowser }
private var shouldSendGenerally = true
private val doShouldSendGenerally = { shouldSendGenerally }
private val settings = mockk<Settings>()
private val dispatcher = StandardTestDispatcher()
private lateinit var storage: DefaultMetricsStorage
@Before
fun setup() {
checkDefaultBrowser = false
shouldSendGenerally = true
storage = DefaultMetricsStorage(mockk(), settings, doCheckDefaultBrowser, doShouldSendGenerally, dispatcher)
}
@Test
fun `GIVEN that events should not be generally sent WHEN event would be tracked THEN it is not`() = runTest(dispatcher) {
shouldSendGenerally = false
checkDefaultBrowser = true
every { settings.setAsDefaultGrowthSent } returns false
val result = storage.shouldTrack(Event.GrowthData.SetAsDefault)
assertFalse(result)
}
@Test
fun `GIVEN set as default has not been sent and app is not default WHEN checked for sending THEN will not be sent`() = runTest(dispatcher) {
every { settings.setAsDefaultGrowthSent } returns false
checkDefaultBrowser = false
val result = storage.shouldTrack(Event.GrowthData.SetAsDefault)
assertFalse(result)
}
@Test
fun `GIVEN set as default has not been sent and app is default WHEN checked for sending THEN will be sent`() = runTest(dispatcher) {
every { settings.setAsDefaultGrowthSent } returns false
checkDefaultBrowser = true
val result = storage.shouldTrack(Event.GrowthData.SetAsDefault)
assertTrue(result)
}
@Test
fun `GIVEN set as default has been sent and app is default WHEN checked for sending THEN will be not sent`() = runTest(dispatcher) {
every { settings.setAsDefaultGrowthSent } returns true
checkDefaultBrowser = true
val result = storage.shouldTrack(Event.GrowthData.SetAsDefault)
assertFalse(result)
}
@Test
fun `WHEN set as default updated THEN settings will be updated accordingly`() = runTest(dispatcher) {
val updateSlot = slot<Boolean>()
every { settings.setAsDefaultGrowthSent = capture(updateSlot) } returns Unit
storage.updateSentState(Event.GrowthData.SetAsDefault)
assertTrue(updateSlot.captured)
}
}

View File

@ -219,6 +219,18 @@ features:
value:
enabled: false
growth-data:
description: A feature measuring campaign growth data
variables:
enabled:
description: If true, the feature is active
type: Boolean
default: false
defaults:
- channel: release
value:
enabled: true
types:
objects:
MessageData: