Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure the settings cache has loaded from disk before reading any values #5103

Merged
merged 6 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion firebase-sessions/firebase-sessions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

@file:Suppress("UnstableApiUsage")

plugins {
id("firebase-library")
id("kotlin-android")
Expand Down Expand Up @@ -45,7 +47,7 @@ android {
dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("com.google.android.datatransport:transport-api:3.0.0")
implementation("com.google.firebase:firebase-common-ktx:20.3.2")
implementation("com.google.firebase:firebase-common-ktx:20.3.3")
implementation("com.google.firebase:firebase-components:17.1.0")
implementation("com.google.firebase:firebase-encoders-json:18.0.1")
implementation("com.google.firebase:firebase-encoders:17.0.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ internal constructor(

/** Calculate whether we should sample events using [sessionSettings] data. */
private fun shouldCollectEvents(): Boolean {
// Sampling rate of 1 means we do not sample.
// Sampling rate of 1 means the SDK will send every event.
val randomValue = Math.random()
return randomValue <= sessionSettings.samplingRate
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,57 @@ package com.google.firebase.sessions.settings

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

internal class LocalOverrideSettings(val context: Context) : SettingsProvider {

private val sessions_metadata_flag_sessionsEnabled = "firebase_sessions_enabled"
private val sessions_metadata_flag_sessionRestartTimeout =
"firebase_sessions_sessions_restart_timeout"
private val sessions_metadata_flag_samplingRate = "firebase_sessions_sampling_rate"
internal class LocalOverrideSettings(context: Context) : SettingsProvider {
private val metadata =
context.packageManager
.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()),
)
} else {
@Suppress("DEPRECATION") // For older API levels.
context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we encapsulate this if statement? To me seeing an if statement with a hanging .metaData makes it hard to read what we're getting the .metaData from

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit hard to clean this up because if I do any other than inline everything like this, I run into https://youtrack.jetbrains.com/issue/KT-53650

I tried to make it clearer, I moved the .metaData inside the if and else bodies, and added a comment for the default value.

.metaData
?: Bundle.EMPTY
mrober marked this conversation as resolved.
Show resolved Hide resolved

override val sessionEnabled: Boolean?
get() {
metadata?.let {
if (it.containsKey(sessions_metadata_flag_sessionsEnabled)) {
return it.getBoolean(sessions_metadata_flag_sessionsEnabled)
}
get() =
if (metadata.containsKey(SESSIONS_ENABLED)) {
metadata.getBoolean(SESSIONS_ENABLED)
} else {
null
}
return null
}

override val sessionRestartTimeout: Duration?
get() {
metadata?.let {
if (it.containsKey(sessions_metadata_flag_sessionRestartTimeout)) {
val timeoutInSeconds = it.getInt(sessions_metadata_flag_sessionRestartTimeout)
val duration = timeoutInSeconds.toDuration(DurationUnit.SECONDS)
return duration
}
get() =
if (metadata.containsKey(SESSION_RESTART_TIMEOUT)) {
val timeoutInSeconds = metadata.getInt(SESSION_RESTART_TIMEOUT)
timeoutInSeconds.toDuration(DurationUnit.SECONDS)
} else {
null
}
return null
}

override val samplingRate: Double?
get() {
metadata?.let {
if (it.containsKey(sessions_metadata_flag_samplingRate)) {
return it.getDouble(sessions_metadata_flag_samplingRate)
}
get() =
if (metadata.containsKey(SAMPLING_RATE)) {
metadata.getDouble(SAMPLING_RATE)
} else {
null
}
return null
}

override fun updateSettings() {
// Nothing to be done here since there is nothing to be updated.
}

override fun isSettingsStale(): Boolean {
// Settings are never stale since all of these are from Manifest file.
return false
private companion object {
const val SESSIONS_ENABLED = "firebase_sessions_enabled"
const val SESSION_RESTART_TIMEOUT = "firebase_sessions_sessions_restart_timeout"
const val SAMPLING_RATE = "firebase_sessions_sampling_rate"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

package com.google.firebase.sessions.settings

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.datastore.preferences.preferencesDataStore
import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import java.util.concurrent.atomic.AtomicBoolean
Expand All @@ -33,19 +34,14 @@ import org.json.JSONException
import org.json.JSONObject

internal class RemoteSettings(
context: Context,
blockingDispatcher: CoroutineContext,
private val backgroundDispatcher: CoroutineContext,
private val firebaseInstallationsApi: FirebaseInstallationsApi,
private val appInfo: ApplicationInfo,
private val configsFetcher: CrashlyticsSettingsFetcher =
RemoteSettingsFetcher(appInfo, blockingDispatcher),
dataStoreName: String = SESSION_CONFIGS_NAME,
private val configsFetcher: CrashlyticsSettingsFetcher,
dataStore: DataStore<Preferences>,
) : SettingsProvider {
private val Context.dataStore by
preferencesDataStore(name = dataStoreName, scope = CoroutineScope(backgroundDispatcher))
private val settingsCache = SettingsCache(context.dataStore)
private var fetchInProgress = AtomicBoolean(false)
private val settingsCache = SettingsCache(dataStore)
private val fetchInProgress = AtomicBoolean(false)

override val sessionEnabled: Boolean?
get() = settingsCache.sessionsEnabled()
Expand All @@ -63,6 +59,7 @@ internal class RemoteSettings(

override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired()

@VisibleForTesting
internal fun clearCachedSettings() {
val scope = CoroutineScope(backgroundDispatcher)
scope.launch { settingsCache.removeConfigs() }
Expand All @@ -81,6 +78,7 @@ internal class RemoteSettings(

fetchInProgress.set(true)

// TODO(mrober): Avoid sending the fid here, and avoid fetching it when data collection is off.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so I looked in the iOS SDK - the way we handle this is just not kicking off the fetch unless we start sending events. So like where we log the event to FireLog is where we'd also kick off settings fetch (but settings must happen before we check the sampling rate)

In the iOS SDK there's a separate method for fetching settings and reading from the cache. Reading from the cache always happens, but fetching happens only when we're cleared to send events

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did something very similar in #5107

// Get the installations ID before making a remote config fetch
val installationId = firebaseInstallationsApi.id.await()
if (installationId == null) {
Expand Down Expand Up @@ -155,9 +153,9 @@ internal class RemoteSettings(
return s.replace(FORWARD_SLASH_STRING.toRegex(), "")
}

companion object {
private const val SESSION_CONFIGS_NAME = "firebase_session_settings"
private const val TAG = "SessionConfigFetcher"
private const val FORWARD_SLASH_STRING: String = "/"
private companion object {
const val TAG = "SessionConfigFetcher"

const val FORWARD_SLASH_STRING: String = "/"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,48 @@
package com.google.firebase.sessions.settings

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.sessions.ApplicationInfo
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

/**
* [SessionsSettings] manages all the configs that are relevant to the sessions library.
*
* @hide
*/
/** [SessionsSettings] manages all the configs that are relevant to the sessions library. */
internal class SessionsSettings(
val context: Context,
val blockingDispatcher: CoroutineContext,
val backgroundDispatcher: CoroutineContext,
val firebaseInstallationsApi: FirebaseInstallationsApi,
val appInfo: ApplicationInfo,
private val localOverrideSettings: LocalOverrideSettings = LocalOverrideSettings(context),
private val remoteSettings: RemoteSettings =
RemoteSettings(
context,
blockingDispatcher,
backgroundDispatcher,
firebaseInstallationsApi,
appInfo
)
private val localOverrideSettings: SettingsProvider,
private val remoteSettings: SettingsProvider,
) {
constructor(
context: Context,
blockingDispatcher: CoroutineContext,
backgroundDispatcher: CoroutineContext,
firebaseInstallationsApi: FirebaseInstallationsApi,
appInfo: ApplicationInfo,
) : this(
localOverrideSettings = LocalOverrideSettings(context),
remoteSettings =
RemoteSettings(
backgroundDispatcher,
firebaseInstallationsApi,
appInfo,
configsFetcher =
RemoteSettingsFetcher(
appInfo,
blockingDispatcher,
),
dataStore = context.dataStore,
),
)

// Order of preference for all the configs below:
// 1. Honor local overrides
// 2. If no local overrides, use remote config
// 3. If no remote config, fall back to SDK defaults.

// Setting to qualify if sessions service is enabled.
/** Setting to qualify if sessions service is enabled. */
val sessionsEnabled: Boolean
get() {
localOverrideSettings.sessionEnabled?.let {
Expand All @@ -62,36 +71,56 @@ internal class SessionsSettings(
return true
}

// Setting that provides the sessions sampling rate.
/** Setting that provides the sessions sampling rate. */
val samplingRate: Double
get() {
localOverrideSettings.samplingRate?.let {
return it
if (isValidSamplingRate(it)) {
return it
}
}
remoteSettings.samplingRate?.let {
return it
if (isValidSamplingRate(it)) {
return it
}
}
// SDK Default
return 1.0
}

// Background timeout config value before which a new session is generated
/** Background timeout config value before which a new session is generated. */
val sessionRestartTimeout: Duration
get() {
localOverrideSettings.sessionRestartTimeout?.let {
return it
if (isValidSessionRestartTimeout(it)) {
return it
}
}
remoteSettings.sessionRestartTimeout?.let {
return it
if (isValidSessionRestartTimeout(it)) {
return it
}
}
// SDK Default
return 30.minutes
}

// Update the settings for all the settings providers
private fun isValidSamplingRate(samplingRate: Double): Boolean = samplingRate in 0.0..1.0

private fun isValidSessionRestartTimeout(sessionRestartTimeout: Duration): Boolean {
return sessionRestartTimeout.isPositive() && sessionRestartTimeout.isFinite()
}

/** Update the settings for all the settings providers. */
fun updateSettings() {
// Placeholder to initiate settings update on different sources
localOverrideSettings.updateSettings()
remoteSettings.updateSettings()
}

private companion object {
const val SESSION_CONFIGS_NAME = "firebase_session_settings"

private val Context.dataStore: DataStore<Preferences> by
preferencesDataStore(name = SESSION_CONFIGS_NAME)
}
}
Loading