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

Add sampling, data collection, and disabled checks to newSession #5451

Merged
merged 3 commits into from
Oct 20, 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
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
}
}
Loading