Skip to content

Commit

Permalink
Add sampling, data collection, and disabled checks to newSession (#5451)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrober authored Oct 20, 2023
1 parent 4beb35a commit f75bbf2
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ 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,

/** Tracks when the event was initiated */
var eventTimestampUs: Long,
/** Tracks when the event was initiated. */
val eventTimestampUs: Long,

/** Data collection status of the dependent product SDKs. */
var dataCollectionStatus: DataCollectionStatus = DataCollectionStatus(),
val dataCollectionStatus: DataCollectionStatus = DataCollectionStatus(),

/** Identifies a unique device+app installation: go/firebase-installations */
var firebaseInstallationId: String = "",
val firebaseInstallationId: String = "",
)

/** Contains the data collection state for all dependent SDKs and sampling info */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,13 @@ internal object SessionEvents {
.ignoreNullValues(true)
.build()

/**
* Construct a Session Start event.
*
* Some mutable fields, e.g. firebaseInstallationId, get populated later.
*/
/** Construct a Session Start event. */
fun buildSession(
firebaseApp: FirebaseApp,
sessionDetails: SessionDetails,
sessionsSettings: SessionsSettings,
subscribers: Map<SessionSubscriber.Name, SessionSubscriber> = emptyMap(),
firebaseInstallationId: String = "",
) =
SessionEvent(
eventType = EventType.SESSION_START,
Expand All @@ -56,6 +53,7 @@ internal object SessionEvents {
crashlytics = toDataCollectionState(subscribers[SessionSubscriber.Name.CRASHLYTICS]),
sessionSamplingRate = sessionsSettings.samplingRate,
),
firebaseInstallationId,
),
applicationInfo = getApplicationInfo(firebaseApp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,23 @@ internal class SessionFirelogPublisher(
* This will pull all the necessary information about the device in order to create a full
* [SessionEvent], and then upload that through the Firelog interface.
*/
fun logSession(sessionDetails: SessionDetails) {
fun logSession(sessionDetails: SessionDetails) =
CoroutineScope(backgroundDispatcher).launch {
val sessionEvent =
SessionEvents.buildSession(
firebaseApp,
sessionDetails,
sessionSettings,
FirebaseSessionsDependencies.getRegisteredSubscribers(),
if (shouldLogSession()) {
attemptLoggingSessionEvent(
SessionEvents.buildSession(
firebaseApp,
sessionDetails,
sessionSettings,
FirebaseSessionsDependencies.getRegisteredSubscribers(),
getFirebaseInstallationId(),
)
)
sessionEvent.sessionData.firebaseInstallationId = getFid()
attemptLoggingSessionEvent(sessionEvent)
}
}
}

/** Attempts to write the given [SessionEvent] to firelog. Failures are logged and ignored. */
private suspend fun attemptLoggingSessionEvent(sessionEvent: SessionEvent) {
private fun attemptLoggingSessionEvent(sessionEvent: SessionEvent) {
try {
eventGDTLogger.log(sessionEvent)
Log.d(TAG, "Successfully logged Session Start event: ${sessionEvent.sessionData.sessionId}")
Expand All @@ -71,8 +72,28 @@ internal class SessionFirelogPublisher(
}
}

/** Determines if the SDK should log a session to Firelog. */
private suspend fun shouldLogSession(): Boolean {
Log.d(TAG, "Data Collection is enabled for at least one Subscriber")

// This will cause remote settings to be fetched if the cache is expired.
sessionSettings.updateSettings()

if (!sessionSettings.sessionsEnabled) {
Log.d(TAG, "Sessions SDK disabled. Events will not be sent.")
return false
}

if (!shouldCollectEvents()) {
Log.d(TAG, "Sessions SDK has dropped this session due to sampling.")
return false
}

return true
}

/** Gets the Firebase Installation ID for the current app installation. */
private suspend fun getFid() =
private suspend fun getFirebaseInstallationId() =
try {
firebaseInstallations.id.await()
} catch (ex: Exception) {
Expand All @@ -81,8 +102,16 @@ internal class SessionFirelogPublisher(
""
}

/** Calculate whether we should sample events using [SessionsSettings] data. */
private fun shouldCollectEvents(): Boolean {
// Sampling rate of 1 means the SDK will send every event.
return randomValueForSampling <= sessionSettings.samplingRate
}

internal companion object {
const val TAG = "SessionFirelogPublisher"
private const val TAG = "SessionFirelogPublisher"

private val randomValueForSampling: Double = Math.random()

val instance: SessionFirelogPublisher
get() = Firebase.app.get(SessionFirelogPublisher::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,23 +152,36 @@ internal object SessionLifecycleClient {
private fun sendLifecycleEvent(messageCode: Int) {
val allMessages = drainQueue()
allMessages.add(Message.obtain(null, messageCode, 0, 0))
sendLifecycleEvents(allMessages)
CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch {
sendLifecycleEvents(allMessages)
}
}

/**
* Sends lifecycle events to the [SessionLifecycleService]. This will only send the latest
* FOREGROUND and BACKGROUND events to the service that are included in the given list. Running
* through the full backlog of messages is not useful since the service only cares about the
* current state and transitions from background -> foreground.
*
* Does not send events unless data collection is enabled for at least one subscriber.
*/
private fun sendLifecycleEvents(messages: List<Message>) {
val latest = ArrayList<Message>()
getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED)?.let { latest.add(it) }
getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED)?.let { latest.add(it) }
latest.sortBy { it.getWhen() }

latest.forEach { sendMessageToServer(it) }
}
private fun sendLifecycleEvents(messages: List<Message>) =
CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch {
val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers()
if (subscribers.isEmpty()) {
Log.d(
TAG,
"Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent."
)
} else if (subscribers.values.none { it.isDataCollectionEnabled }) {
Log.d(TAG, "Data Collection is disabled for all subscribers. Skipping this Event")
} else {
val latest = ArrayList<Message>(2)
getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED)?.let { latest.add(it) }
getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED)?.let { latest.add(it) }
latest.sortedBy { it.getWhen() }.forEach { sendMessageToServer(it) }
}
}

/** Sends the given [Message] to the [SessionLifecycleService]. */
private fun sendMessageToServer(msg: Message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ import kotlinx.coroutines.launch
* be generated. When this happens, the service will broadcast the updated session id to all
* connected clients.
*/
internal class SessionLifecycleService() : Service() {
internal class SessionLifecycleService : Service() {

/** The thread that will be used to process all lifecycle messages from connected clients. */
private var handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread")
private val handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread")

/** The handler that will process all lifecycle messages from connected clients . */
private var messageHandler: MessageHandler? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,41 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
import com.google.firebase.concurrent.TestOnlyExecutors
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.api.SessionSubscriber
import com.google.firebase.sessions.settings.SessionsSettings
import com.google.firebase.sessions.testing.FakeEventGDTLogger
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeFirebaseInstallations
import com.google.firebase.sessions.testing.FakeSessionSubscriber
import com.google.firebase.sessions.testing.FakeSettingsProvider
import com.google.firebase.sessions.testing.TestSessionEventData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class SessionFirelogPublisherTest {
@Before
fun setUp() {
val crashlyticsSubscriber =
FakeSessionSubscriber(sessionSubscriberName = SessionSubscriber.Name.CRASHLYTICS)
FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.CRASHLYTICS)
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
}

@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
FirebaseSessionsDependencies.reset()
}

@Test
fun logSession_populatesFid() = runTest {
val fakeFirebaseApp = FakeFirebaseApp()
Expand All @@ -61,13 +79,7 @@ class SessionFirelogPublisherTest {

runCurrent()

System.out.println("FakeEventGDTLogger: $fakeEventGDTLogger")
assertThat(fakeEventGDTLogger.loggedEvent!!.sessionData.firebaseInstallationId)
.isEqualTo("FaKeFiD")
}

@After
fun cleanUp() {
FirebaseApp.clearInstancesForTest()
}
}
6 changes: 3 additions & 3 deletions firebase-sessions/test-app/test-app.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ dependencies {
println("Using sessions released version: $latestReleasedVersion")
// TODO(mrober): How to find the released versions of crashlytics and perf?
implementation("com.google.firebase:firebase-crashlytics:18.4.3")
implementation("com.google.firebase:firebase-perf:20.4.1")
// implementation("com.google.firebase:firebase-perf:20.4.1")
implementation("com.google.firebase:firebase-sessions:$latestReleasedVersion")
} else {
implementation(project(":firebase-crashlytics"))
implementation(project(":firebase-perf"))
// implementation(project(":firebase-perf"))
implementation(project(":firebase-sessions"))
}

Expand All @@ -80,4 +80,4 @@ apply<FirebaseTestLabPlugin>()

configure<FirebaseTestLabExtension> {
device("model=panther,version=33") // Pixel7
}
}

0 comments on commit f75bbf2

Please sign in to comment.