Skip to content

Populate firebaseInstallationId field in session #4801

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

Merged
merged 1 commit into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions firebase-sessions/firebase-sessions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dependencies {
testImplementation(libs.androidx.test.junit)
testImplementation(libs.androidx.test.runner)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
testImplementation(libs.robolectric)
testImplementation(libs.truth)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ class FirebaseSessionsTests {
fun setUp() {
Firebase.initialize(
ApplicationProvider.getApplicationContext(),
FirebaseOptions.Builder().setApplicationId("APP_ID").build()
FirebaseOptions.Builder()
.setApplicationId(APP_ID)
.setApiKey(API_KEY)
.setProjectId(PROJECT_ID)
.build()
)
}

Expand All @@ -53,4 +57,10 @@ class FirebaseSessionsTests {
// This will be replaced with real tests.
assertThat(FirebaseSessions.instance.greeting()).isEqualTo("Matt says hi!")
}

companion object {
private const val APP_ID = "1:1:android:1a"
private const val API_KEY = "API-KEY-API-KEY-API-KEY-API-KEY-API-KEY"
private const val PROJECT_ID = "PROJECT-ID"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
import kotlinx.coroutines.CoroutineDispatcher

class FirebaseSessions
internal constructor(firebaseApp: FirebaseApp, firebaseInstallations: FirebaseInstallationsApi) {
internal constructor(
firebaseApp: FirebaseApp,
firebaseInstallations: FirebaseInstallationsApi,
backgroundDispatcher: CoroutineDispatcher
) {
private val sessionGenerator = SessionGenerator(collectEvents = true)
private val sessionCoordinator = SessionCoordinator(firebaseInstallations, backgroundDispatcher)

init {
val sessionInitiator = SessionInitiator(WallClock::elapsedRealtime, this::initiateSessionStart)
Expand All @@ -36,7 +42,6 @@ internal constructor(firebaseApp: FirebaseApp, firebaseInstallations: FirebaseIn
} else {
Log.w(TAG, "Failed to register lifecycle callbacks, unexpected context ${context.javaClass}.")
}
Log.i(TAG, "Firebase Installations ID: ${firebaseInstallations.id}")
}

@Discouraged(message = "This will be replaced with a real API.")
Expand All @@ -46,7 +51,9 @@ internal constructor(firebaseApp: FirebaseApp, firebaseInstallations: FirebaseIn
val sessionDetails = sessionGenerator.generateNewSession()
val sessionEvent = SessionEvents.startSession(sessionDetails)

Log.i(TAG, "Initiate session start: $sessionEvent")
if (sessionDetails.collectEvents) {
sessionCoordinator.attemptLoggingSessionEvent(sessionEvent)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ package com.google.firebase.sessions

import androidx.annotation.Keep
import com.google.firebase.FirebaseApp
import com.google.firebase.annotations.concurrent.Background
import com.google.firebase.components.Component
import com.google.firebase.components.ComponentRegistrar
import com.google.firebase.components.Dependency
import com.google.firebase.components.Qualified.qualified
import com.google.firebase.components.Qualified.unqualified
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.platforminfo.LibraryVersionComponent
import kotlinx.coroutines.CoroutineDispatcher

/**
* [ComponentRegistrar] for setting up [FirebaseSessions].
Expand All @@ -33,12 +37,14 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
listOf(
Component.builder(FirebaseSessions::class.java)
.name(LIBRARY_NAME)
.add(Dependency.required(FirebaseApp::class.java))
.add(Dependency.required(FirebaseInstallationsApi::class.java))
.add(Dependency.required(firebaseApp))
.add(Dependency.required(firebaseInstallationsApi))
.add(Dependency.required(backgroundDispatcher))
.factory { container ->
FirebaseSessions(
container.get(FirebaseApp::class.java),
container.get(FirebaseInstallationsApi::class.java)
container.get(firebaseApp),
container.get(firebaseInstallationsApi),
container.get(backgroundDispatcher),
)
}
.eagerInDefaultApp()
Expand All @@ -48,5 +54,10 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {

companion object {
private const val LIBRARY_NAME = "fire-sessions"

private val firebaseApp = unqualified(FirebaseApp::class.java)
private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java)
private val backgroundDispatcher =
qualified(Background::class.java, CoroutineDispatcher::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions

import android.util.Log
import com.google.firebase.installations.FirebaseInstallationsApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await

/**
* [SessionCoordinator] is responsible for coordinating the systems in this SDK involved with
* sending a [SessionEvent].
*
* @hide
*/
internal class SessionCoordinator(
private val firebaseInstallations: FirebaseInstallationsApi,
backgroundDispatcher: CoroutineDispatcher
) {
private val scope = CoroutineScope(backgroundDispatcher)

fun attemptLoggingSessionEvent(sessionEvent: SessionEvent) =
scope.launch {
sessionEvent.sessionData.firebaseInstallationId =
try {
firebaseInstallations.id.await()
} catch (ex: Exception) {
Log.w(TAG, "Session Installations Error", ex)
// Use an empty fid if there is any failure.
""
}

Log.i(TAG, "Initiate session start: $sessionEvent")
}

companion object {
private const val TAG = "SessionCoordinator"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ internal data class SessionInfo(

/** What order this Session came in this run of the app. For the first Session this will be 0. */
val sessionIndex: Int,

/** Identifies a unique device+app installation: go/firebase-installations */
var firebaseInstallationId: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,20 @@ internal object SessionEvents {
ctx.add(FieldDescriptor.of("session_id"), sessionInfo.sessionId)
ctx.add(FieldDescriptor.of("first_session_id"), sessionInfo.firstSessionId)
ctx.add(FieldDescriptor.of("session_index"), sessionInfo.sessionIndex)
ctx.add(
FieldDescriptor.of("firebase_installation_id"),
sessionInfo.firebaseInstallationId
)
}
}
}
.build()

/** Construct a Session Start event */
/**
* Construct a Session Start event.
*
* Some mutable fields, e.g. firebaseInstallationId, get populated later.
*/
fun startSession(sessionDetails: SessionDetails) =
SessionEvent(
eventType = EventType.SESSION_START,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class SessionCoordinatorTest {
@Test
fun attemptLoggingSessionEvent_populatesFid() = runTest {
val sessionCoordinator =
SessionCoordinator(
firebaseInstallations = FakeFirebaseInstallations("FaKeFiD"),
backgroundDispatcher = StandardTestDispatcher(testScheduler),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

:integ-testing doesn't have any test CoroutineDispatchers to work well with runTesting.

Copy link
Contributor

Choose a reason for hiding this comment

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

How does that impact us?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:integ-testing is to provide executors for testing that mimic the executors provided by firebase-common with policies e.g. don't do blocking network requests on Background. See https://firebase.github.io/firebase-android-sdk/components/executors/#testing

So these tests don't mimic the Background policy. I tried to use TestOnlyExecutors.background().asCoroutineDispatcher() when I first wrote this test, but then I couldn't call runCurrent() to wait for the suspend functions to finish before doing the assert.

)

// Construct an event with no fid set.
val sessionEvent =
SessionEvent(
eventType = EventType.SESSION_START,
sessionData =
SessionInfo(
sessionId = "id",
firstSessionId = "first",
sessionIndex = 3,
),
)

sessionCoordinator.attemptLoggingSessionEvent(sessionEvent)

runCurrent()

assertThat(sessionEvent.sessionData.firebaseInstallationId).isEqualTo("FaKeFiD")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SessionEventEncoderTest {
sessionId = "id",
firstSessionId = "first",
sessionIndex = 9,
firebaseInstallationId = "fid"
),
)

Expand All @@ -47,7 +48,8 @@ class SessionEventEncoderTest {
"session_data":{
"session_id":"id",
"first_session_id":"first",
"session_index":9
"session_index":9,
"firebase_installation_id":"fid"
}
}
"""
Expand Down Expand Up @@ -79,7 +81,8 @@ class SessionEventEncoderTest {
"session_data":{
"session_id":"",
"first_session_id":"",
"session_index":0
"session_index":0,
"firebase_installation_id":""
}
}
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions.testing

import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.installations.InstallationTokenResult
import com.google.firebase.installations.internal.FidListener
import com.google.firebase.installations.internal.FidListenerHandle

/** Fake [FirebaseInstallationsApi] that implements [getId] and always returns the given [fid]. */
internal class FakeFirebaseInstallations(private val fid: String = "") : FirebaseInstallationsApi {
override fun getId(): Task<String> = Tasks.forResult(fid)

override fun getToken(forceRefresh: Boolean): Task<InstallationTokenResult> =
throw NotImplementedError("getToken not faked.")

override fun delete(): Task<Void> = throw NotImplementedError("delete not faked.")

override fun registerFidListener(listener: FidListener): FidListenerHandle =
throw NotImplementedError("registerFidListener not faked.")
}