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 SessionEvent data class #4763

Merged
merged 6 commits into from
Mar 13, 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 @@ -24,7 +24,6 @@ import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app

class FirebaseSessions internal constructor(firebaseApp: FirebaseApp) {

private val sessionGenerator = SessionGenerator(collectEvents = true)

init {
Expand All @@ -41,10 +40,10 @@ class FirebaseSessions internal constructor(firebaseApp: FirebaseApp) {
fun greeting(): String = "Matt says hi!"

private fun initiateSessionStart() {
// TODO(mrober): Generate a session
Log.i(TAG, "Initiate session start")
val sessionState = sessionGenerator.generateNewSession()
val sessionEvent = SessionEvent.sessionStart(sessionState)
Comment on lines +43 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is very close to the iOS implementation. Just to confirm, here is how I think the current piece of code would evolve for event dispatch. Please let me know if you think otherwise.

  1. Session Generator is stateful and it returns sessions related information for the current session event. (ID, Index, previous ID, parent ID and likes)
    2.SessionEvent class takes those details to fill in more details
  2. Firebase Sessions later embellishes the event with details like data collection state.
  3. Firebase Sessions will use Firelog services to hand over the event.

Curious as to where would the config layer fit in here and how would sampling be done to either capture or skip an event?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, and for sampling we'll handle that when we do Settings. We will pass in something like collectEvents = shouldCollectEvents(...) to the SessionGenerator.


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

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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

/**
* Contains the relevant information around a Firebase Session Event.
*
* See go/app-quality-unified-session-definition for more details. Keep in sync with
* https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseSessions/ProtoSupport/Protos/sessions.proto
*/
// TODO(mrober): Add and populate all fields from sessions.proto
internal data class SessionEvent(
Copy link
Contributor

Choose a reason for hiding this comment

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

Just confirming that this class will eventually do more things than what it is today: Add more App related fields to the session event?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup this will eventually have all the fields from the proto.

/** The type of event being reported. */
val eventType: EventType,
Comment on lines +27 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to remember this state? It is already a part of SessionStartEvent, can we use it from there? I'm trying to avoid multiple source of truth here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is there to match the proto 1:1, this data class will be serialized and sent to firelog.


/** Information about the session triggering the event. */
val sessionData: SessionInfo,
) {
companion object {
fun sessionStart(sessionDetails: SessionDetails) =
SessionEvent(
eventType = EventType.SESSION_START,
sessionData =
SessionInfo(
sessionDetails.sessionId,
sessionDetails.firstSessionId,
sessionDetails.sessionIndex,
),
)
}
}

/** Enum denoting all possible session event types. */
internal enum class EventType(val number: Int) {
EVENT_TYPE_UNKNOWN(0),

/** This event type is fired as soon as a new session begins. */
SESSION_START(1),
}

/** Contains session-specific information relating to the event being uploaded. */
internal data class SessionInfo(
/** A globally unique identifier for the session. */
val sessionId: String,

/**
* Will point to the first Session for the run of the app.
*
* For the first session, this will be the same as session_id.
*/
val firstSessionId: String,

/** What order this Session came in this run of the app. For the first Session this will be 0. */
val sessionIndex: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ package com.google.firebase.sessions
import java.util.UUID

/**
* [SessionInfo] is a data class responsible for storing information about the current Session.
* [SessionDetails] is a data class responsible for storing information about the current Session.
*
* @hide
*/
internal data class SessionInfo(
internal data class SessionDetails(
val sessionId: String,
val firstSessionId: String,
val collectEvents: Boolean,
Expand All @@ -32,43 +32,39 @@ internal data class SessionInfo(

/**
* The [SessionGenerator] is responsible for generating the Session ID, and keeping the
* [SessionInfo] up to date with the latest values.
* [SessionDetails] up to date with the latest values.
*
* @hide
*/
internal class SessionGenerator(collectEvents: Boolean) {
internal class SessionGenerator(private var collectEvents: Boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

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

wow! We can call parameters with access protectors? private var is so cool! TIL!

private var firstSessionId = ""
private var sessionIndex: Int = -1
private var collectEvents = collectEvents

private var thisSession: SessionInfo =
SessionInfo(
private var thisSession: SessionDetails =
SessionDetails(
sessionId = "",
firstSessionId = "",
collectEvents = collectEvents,
sessionIndex = sessionIndex
collectEvents,
sessionIndex,
)

// Generates a new Session ID. If there was already a generated Session ID
// from the last session during the app's lifecycle, it will also set the last Session ID
fun generateNewSession() {
fun generateNewSession(): SessionDetails {
val newSessionId = UUID.randomUUID().toString().replace("-", "").lowercase()

// If firstSessionId is set, use it. Otherwise set it to the
// first generated Session ID
firstSessionId = if (firstSessionId.isEmpty()) newSessionId else firstSessionId
firstSessionId = firstSessionId.ifEmpty { newSessionId }

sessionIndex += 1

thisSession =
SessionInfo(
sessionId = newSessionId,
firstSessionId = firstSessionId,
collectEvents = collectEvents,
sessionIndex = sessionIndex
)
SessionDetails(sessionId = newSessionId, firstSessionId, collectEvents, sessionIndex)

return thisSession
}

val currentSession: SessionInfo
val currentSession: SessionDetails
get() = thisSession
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 com.google.common.truth.Truth.assertThat
import org.junit.Test

class SessionEventTest {
@Test
fun sessionStart_populatesSessionDetailsCorrectly() {
val sessionDetails =
SessionDetails(
sessionId = "a1b2c3",
firstSessionId = "a1a1a1",
collectEvents = true,
sessionIndex = 3,
)

val sessionEvent = SessionEvent.sessionStart(sessionDetails)

assertThat(sessionEvent)
.isEqualTo(
SessionEvent(
eventType = EventType.SESSION_START,
sessionData =
SessionInfo(
sessionId = "a1b2c3",
firstSessionId = "a1a1a1",
sessionIndex = 3,
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Test

class SessionGeneratorTest {
fun isValidSessionId(sessionId: String): Boolean {
private fun isValidSessionId(sessionId: String): Boolean {
if (sessionId.length != 32) {
return false
}
Expand All @@ -42,21 +42,21 @@ class SessionGeneratorTest {

assertThat(sessionGenerator.currentSession.sessionId).isEqualTo("")
assertThat(sessionGenerator.currentSession.firstSessionId).isEqualTo("")
assertThat(sessionGenerator.currentSession.collectEvents).isEqualTo(false)
assertThat(sessionGenerator.currentSession.collectEvents).isFalse()
assertThat(sessionGenerator.currentSession.sessionIndex).isEqualTo(-1)
}

@Test
fun generateNewSessionID_generatesValidSessionInfo() {
fun generateNewSessionID_generatesValidSessionDetails() {
val sessionGenerator = SessionGenerator(collectEvents = true)

sessionGenerator.generateNewSession()

assertThat(isValidSessionId(sessionGenerator.currentSession.sessionId)).isEqualTo(true)
assertThat(isValidSessionId(sessionGenerator.currentSession.firstSessionId)).isEqualTo(true)
assertThat(isValidSessionId(sessionGenerator.currentSession.sessionId)).isTrue()
assertThat(isValidSessionId(sessionGenerator.currentSession.firstSessionId)).isTrue()
assertThat(sessionGenerator.currentSession.firstSessionId)
.isEqualTo(sessionGenerator.currentSession.sessionId)
assertThat(sessionGenerator.currentSession.collectEvents).isEqualTo(true)
assertThat(sessionGenerator.currentSession.collectEvents).isTrue()
assertThat(sessionGenerator.currentSession.sessionIndex).isEqualTo(0)
}

Expand All @@ -68,32 +68,32 @@ class SessionGeneratorTest {

sessionGenerator.generateNewSession()

val firstSessionInfo = sessionGenerator.currentSession
val firstSessionDetails = sessionGenerator.currentSession

assertThat(isValidSessionId(firstSessionInfo.sessionId)).isEqualTo(true)
assertThat(isValidSessionId(firstSessionInfo.firstSessionId)).isEqualTo(true)
assertThat(firstSessionInfo.firstSessionId).isEqualTo(firstSessionInfo.sessionId)
assertThat(firstSessionInfo.sessionIndex).isEqualTo(0)
assertThat(isValidSessionId(firstSessionDetails.sessionId)).isTrue()
assertThat(isValidSessionId(firstSessionDetails.firstSessionId)).isTrue()
assertThat(firstSessionDetails.firstSessionId).isEqualTo(firstSessionDetails.sessionId)
assertThat(firstSessionDetails.sessionIndex).isEqualTo(0)

sessionGenerator.generateNewSession()
val secondSessionInfo = sessionGenerator.currentSession
val secondSessionDetails = sessionGenerator.currentSession

assertThat(isValidSessionId(secondSessionInfo.sessionId)).isEqualTo(true)
assertThat(isValidSessionId(secondSessionInfo.firstSessionId)).isEqualTo(true)
assertThat(isValidSessionId(secondSessionDetails.sessionId)).isTrue()
assertThat(isValidSessionId(secondSessionDetails.firstSessionId)).isTrue()
// Ensure the new firstSessionId is equal to the first Session ID from earlier
assertThat(secondSessionInfo.firstSessionId).isEqualTo(firstSessionInfo.sessionId)
assertThat(secondSessionDetails.firstSessionId).isEqualTo(firstSessionDetails.sessionId)
// Session Index should increase
assertThat(secondSessionInfo.sessionIndex).isEqualTo(1)
assertThat(secondSessionDetails.sessionIndex).isEqualTo(1)

// Do a third round just in case
sessionGenerator.generateNewSession()
val thirdSessionInfo = sessionGenerator.currentSession
val thirdSessionDetails = sessionGenerator.currentSession

assertThat(isValidSessionId(thirdSessionInfo.sessionId)).isEqualTo(true)
assertThat(isValidSessionId(thirdSessionInfo.firstSessionId)).isEqualTo(true)
assertThat(isValidSessionId(thirdSessionDetails.sessionId)).isTrue()
assertThat(isValidSessionId(thirdSessionDetails.firstSessionId)).isTrue()
// Ensure the new firstSessionId is equal to the first Session ID from earlier
assertThat(thirdSessionInfo.firstSessionId).isEqualTo(firstSessionInfo.sessionId)
assertThat(thirdSessionDetails.firstSessionId).isEqualTo(firstSessionDetails.sessionId)
// Session Index should increase
assertThat(thirdSessionInfo.sessionIndex).isEqualTo(2)
assertThat(thirdSessionDetails.sessionIndex).isEqualTo(2)
}
}