|
|
|
@ -6,7 +6,6 @@ package org.mozilla.fenix.experiments
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.os.StrictMode
|
|
|
|
|
import mozilla.components.service.nimbus.Nimbus
|
|
|
|
|
import mozilla.components.service.nimbus.NimbusApi
|
|
|
|
|
import mozilla.components.service.nimbus.NimbusAppInfo
|
|
|
|
@ -46,6 +45,9 @@ private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L
|
|
|
|
|
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
|
fun createNimbus(context: Context, url: String?): NimbusApi {
|
|
|
|
|
// Once application-services has switched to using the new
|
|
|
|
|
// error reporting system, we may not need this anymore.
|
|
|
|
|
// https://mozilla-hub.atlassian.net/browse/EXP-2868
|
|
|
|
|
val errorReporter: ((String, Throwable) -> Unit) = reporter@{ message, e ->
|
|
|
|
|
Logger.error("Nimbus error: $message", e)
|
|
|
|
|
|
|
|
|
@ -55,75 +57,69 @@ fun createNimbus(context: Context, url: String?): NimbusApi {
|
|
|
|
|
|
|
|
|
|
context.components.analytics.crashReporter.submitCaughtException(e)
|
|
|
|
|
}
|
|
|
|
|
return try {
|
|
|
|
|
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
|
|
|
|
|
// but we keep this here to not mix feature flags and how we configure Nimbus.
|
|
|
|
|
val serverSettings = if (!url.isNullOrBlank()) {
|
|
|
|
|
if (context.settings().nimbusUsePreview) {
|
|
|
|
|
NimbusServerSettings(url = Uri.parse(url), collection = "nimbus-preview")
|
|
|
|
|
} else {
|
|
|
|
|
NimbusServerSettings(url = Uri.parse(url))
|
|
|
|
|
}
|
|
|
|
|
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
|
|
|
|
|
// but we keep this here to not mix feature flags and how we configure Nimbus.
|
|
|
|
|
val serverSettings = if (!url.isNullOrBlank()) {
|
|
|
|
|
if (context.settings().nimbusUsePreview) {
|
|
|
|
|
NimbusServerSettings(url = Uri.parse(url), collection = "nimbus-preview")
|
|
|
|
|
} else {
|
|
|
|
|
null
|
|
|
|
|
NimbusServerSettings(url = Uri.parse(url))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global opt out state is stored in Nimbus, and shouldn't be toggled to `true`
|
|
|
|
|
// from the app unless the user does so from a UI control.
|
|
|
|
|
// However, the user may have opt-ed out of mako experiments already, so
|
|
|
|
|
// we should respect that setting here.
|
|
|
|
|
val enabled =
|
|
|
|
|
context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
|
|
|
|
|
context.settings().isExperimentationEnabled
|
|
|
|
|
}
|
|
|
|
|
val isFirstNimbusRun = context.settings().isFirstNimbusRun
|
|
|
|
|
if (isFirstNimbusRun) {
|
|
|
|
|
context.settings().isFirstNimbusRun = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The name "fenix" here corresponds to the app_name defined for the family of apps
|
|
|
|
|
// that encompasses all of the channels for the Fenix app. This is defined upstream in
|
|
|
|
|
// the telemetry system. For more context on where the app_name come from see:
|
|
|
|
|
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
|
|
|
|
|
// and
|
|
|
|
|
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
|
|
|
|
|
val appInfo = NimbusAppInfo(
|
|
|
|
|
appName = "fenix",
|
|
|
|
|
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
|
|
|
|
|
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
|
|
|
|
|
// and would mostly produce the value `Beta` and rarely would produce `beta`.
|
|
|
|
|
channel = BuildConfig.BUILD_TYPE,
|
|
|
|
|
customTargetingAttributes = mapOf(
|
|
|
|
|
"isFirstRun" to context.settings().isFirstNimbusRun.toString(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
// The name "fenix" here corresponds to the app_name defined for the family of apps
|
|
|
|
|
// that encompasses all of the channels for the Fenix app. This is defined upstream in
|
|
|
|
|
// the telemetry system. For more context on where the app_name come from see:
|
|
|
|
|
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
|
|
|
|
|
// and
|
|
|
|
|
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
|
|
|
|
|
val appInfo = NimbusAppInfo(
|
|
|
|
|
appName = "fenix",
|
|
|
|
|
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
|
|
|
|
|
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
|
|
|
|
|
// and would mostly produce the value `Beta` and rarely would produce `beta`.
|
|
|
|
|
channel = BuildConfig.BUILD_TYPE.let { if (it == "debug") "developer" else it },
|
|
|
|
|
customTargetingAttributes = mapOf(
|
|
|
|
|
"isFirstRun" to isFirstNimbusRun.toString(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
return try {
|
|
|
|
|
Nimbus(context, appInfo, serverSettings, errorReporter).apply {
|
|
|
|
|
// We register our own internal observer for housekeeping the Nimbus SDK and
|
|
|
|
|
// generated code.
|
|
|
|
|
register(observer)
|
|
|
|
|
|
|
|
|
|
val isFirstNimbusRun = context.settings().isFirstNimbusRun
|
|
|
|
|
// Apply any experiment recipes we downloaded last time, or
|
|
|
|
|
// if this is the first time, we load the ones bundled in the res/raw
|
|
|
|
|
// directory.
|
|
|
|
|
val job = if (isFirstNimbusRun || url.isNullOrBlank()) {
|
|
|
|
|
applyLocalExperiments(R.raw.initial_experiments)
|
|
|
|
|
} else {
|
|
|
|
|
applyPendingExperiments()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We always want `Nimbus.initialize` to happen ASAP and before any features (engine/UI)
|
|
|
|
|
// We always want initialize Nimbus to happen ASAP and before any features (engine/UI)
|
|
|
|
|
// have been initialized. For that reason, we use runBlocking here to avoid
|
|
|
|
|
// inconsistency in the experiments.
|
|
|
|
|
// We can safely do this because Nimbus does most of it's work on background threads,
|
|
|
|
|
// except for loading the initial experiments from disk. For this reason, we have a
|
|
|
|
|
// We can safely do this because Nimbus does most of its work on background threads,
|
|
|
|
|
// including the loading the initial experiments from disk. For this reason, we have a
|
|
|
|
|
// `joinOrTimeout` to limit the blocking until TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS.
|
|
|
|
|
runBlockingIncrement {
|
|
|
|
|
val job = initialize(
|
|
|
|
|
isFirstNimbusRun || url.isNullOrBlank(),
|
|
|
|
|
R.raw.initial_experiments,
|
|
|
|
|
)
|
|
|
|
|
// We only read from disk when loading first-run experiments. This is the only time
|
|
|
|
|
// that we should join and block. Otherwise, we don't want to wait.
|
|
|
|
|
if (isFirstNimbusRun) {
|
|
|
|
|
context.settings().isFirstNimbusRun = false
|
|
|
|
|
job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
// This opts out of nimbus experiments. It involves writing to disk, so does its
|
|
|
|
|
// work on the db thread.
|
|
|
|
|
globalUserParticipation = enabled
|
|
|
|
|
job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS)
|
|
|
|
|
}
|
|
|
|
|
// By now, on this thread, we have a fully initialized Nimbus object, ready for use:
|
|
|
|
|
// * we gave a 200ms timeout to the loading of a file from res/raw
|
|
|
|
|
// * on completion or cancellation, applyPendingExperiments or initialize was
|
|
|
|
|
// called, and this thread waited for that to complete.
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
// Something went wrong. We'd like not to, but stability of the app is more important than
|
|
|
|
|