Skip to content
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
32 changes: 16 additions & 16 deletions android/measure/src/main/java/sh/measure/android/MeasureInternal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import sh.measure.android.utils.AttachmentHelper
/**
* Initializes the Measure SDK and hides the internal dependencies from public API.
*/
internal class MeasureInternal(private val measure: MeasureInitializer) : AppLifecycleListener {
internal class MeasureInternal(private val measure: MeasureInitializer) :
AppLifecycleListener,
SessionStartListener {
val timeProvider = measure.timeProvider
val processInfoProvider = measure.processInfoProvider
val logger = measure.logger
Expand All @@ -33,21 +35,9 @@ internal class MeasureInternal(private val measure: MeasureInitializer) : AppLif
return
}

measure.sessionManager.setSessionStartListener(this)
// start a session
val sessionId = measure.sessionManager.init()

// All events are processed on a single thread in a queue.
// So, the first event will always be a session start event
// as we initialize all other collectors after this event
// is triggered.
measure.signalProcessor.track(
SessionStartData,
timestamp = measure.sessionManager.getSessionStartTime(),
type = EventType.SESSION_START,
sessionId = sessionId,
// always sample session start event
isSampled = true,
)
measure.sessionManager.init()

// setup lifecycle state
measure.resumedActivityProvider.register()
Expand Down Expand Up @@ -127,6 +117,17 @@ internal class MeasureInternal(private val measure: MeasureInitializer) : AppLif
}
}

override fun onSessionStart(sessionId: String, startTime: Long) {
measure.signalProcessor.track(
SessionStartData,
timestamp = startTime,
type = EventType.SESSION_START,
sessionId = sessionId,
// always sample session start event
isSampled = true,
)
}

fun setUserId(userId: String) {
measure.userAttributeProcessor.setUserId(userId)
}
Expand Down Expand Up @@ -392,6 +393,5 @@ internal class MeasureInternal(private val measure: MeasureInitializer) : AppLif
measure.unhandledExceptionCollector.unregister()
measure.anrCollector.unregister()
}

fun getDynamicConfigPath(): String? = measure.fileStorage.getConfigPath()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import sh.measure.android.utils.Sampler
import sh.measure.android.utils.TimeProvider
import java.util.concurrent.RejectedExecutionException

/**
* Callback to be called when a new session is created.
*/
internal interface SessionStartListener {
fun onSessionStart(sessionId: String, startTime: Long)
}

internal interface SessionManager {
/**
* Creates a new session, to be used only when the SDK is initialized.
Expand Down Expand Up @@ -57,6 +64,13 @@ internal interface SessionManager {
* Called when config is loaded.
*/
fun onConfigLoaded()

/**
* Sets a listener to be called when a new session is created.
*
* @param listener the listener to be called
*/
fun setSessionStartListener(listener: SessionStartListener)
}

/**
Expand All @@ -76,6 +90,7 @@ internal class SessionManagerImpl(
private val packageInfoProvider: PackageInfoProvider,
private val sampler: Sampler,
) : SessionManager {
private var sessionStartListener: SessionStartListener? = null
private var sessionId: String? = null
private var sessionStartTime: Long? = null

Expand Down Expand Up @@ -151,10 +166,16 @@ internal class SessionManagerImpl(
}
}

override fun setSessionStartListener(listener: SessionStartListener) {
this.sessionStartListener = listener
}

private fun createNewSession(): String {
val id = idProvider.uuid()
val startTime = timeProvider.now()
this.sessionId = id
this.sessionStartTime = timeProvider.now()
this.sessionStartTime = startTime
sessionStartListener?.onSessionStart(id, startTime)
storeSession(id)
return id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import sh.measure.android.config.DynamicConfig
import sh.measure.android.events.EventType
import sh.measure.android.events.SignalProcessor
import sh.measure.android.fakes.FakeSessionManager
import sh.measure.android.utils.ManifestMetadata

class MeasureInternalTest {
private val sessionManager = mock<SessionManager>()
private val signalProcessor = mock<SignalProcessor>()

private fun mockMeasureInitializer(): MeasureInitializer {
val initializer = mock(MeasureInitializer::class.java)

// Stubbing all fields with mocks
`when`(initializer.logger).thenReturn(mock())
`when`(initializer.signalProcessor).thenReturn(mock())
`when`(initializer.signalProcessor).thenReturn(signalProcessor)
`when`(initializer.httpEventCollector).thenReturn(mock())
`when`(initializer.processInfoProvider).thenReturn(mock())
`when`(initializer.timeProvider).thenReturn(mock())
`when`(initializer.bugReportCollector).thenReturn(mock())
`when`(initializer.spanCollector).thenReturn(mock())
`when`(initializer.customEventCollector).thenReturn(mock())
`when`(initializer.sessionManager).thenReturn(mock())
`when`(initializer.sessionManager).thenReturn(sessionManager)
`when`(initializer.userTriggeredEventCollector).thenReturn(mock())
`when`(initializer.resumedActivityProvider).thenReturn(mock())
`when`(initializer.networkClient).thenReturn(mock())
Expand Down Expand Up @@ -368,6 +376,26 @@ class MeasureInternalTest {
verify(initializer.unhandledExceptionCollector, never()).unregister()
}

@Test
fun `session start callback triggers a session start event`() {
val initializer = mockMeasureInitializer()
val measureInternal = MeasureInternal(initializer)
measureInternal.onSessionStart("session-id", 123456789L)

verify(signalProcessor).track(
data = eq(SessionStartData),
timestamp = eq(123456789L),
type = eq(EventType.SESSION_START),
attributes = any(),
userDefinedAttributes = any(),
attachments = any(),
threadName = anyOrNull(),
sessionId = eq("session-id"),
userTriggered = any(),
isSampled = eq(true),
)
}

private fun initWithValidCredentials(): MeasureInitializer {
val initializer = mockMeasureInitializer()
val manifest = ManifestMetadata("https://api.measure.sh", "msrsh_123")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,45 @@ class SessionManagerTest {

verify(database, never()).sampleJourneyEvents(any(), any())
}

@Test
fun `onSessionStart listener is invoked on init`() {
var callbackSessionId: String? = null
var callbackSessionTime: Long? = null
sessionManager.setSessionStartListener(object : SessionStartListener {
override fun onSessionStart(sessionId: String, startTime: Long) {
callbackSessionId = sessionId
callbackSessionTime = startTime
}
})

sessionManager.init()

assertEquals(sessionManager.getSessionId(), callbackSessionId)
assertEquals(sessionManager.getSessionStartTime(), callbackSessionTime)
}

@Test
fun `onSessionStart listener is invoked on new session when app comes back to foreground`() {
var callbackSessionId: String? = null
var callbackSessionTime: Long? = null
sessionManager.setSessionStartListener(object : SessionStartListener {
override fun onSessionStart(sessionId: String, startTime: Long) {
callbackSessionId = sessionId
callbackSessionTime = startTime
}
})

sessionManager.init()
sessionManager.onAppBackground()

testClock.advance(Duration.ofMillis(configProvider.sessionBackgroundTimeoutThresholdMs + 1))
val updatedSessionId = "next-uuid"
idProvider.id = updatedSessionId

sessionManager.onAppForeground()

assertEquals(updatedSessionId, callbackSessionId)
assertEquals(sessionManager.getSessionStartTime(), callbackSessionTime)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package sh.measure.android.fakes

import sh.measure.android.SessionManager
import sh.measure.android.SessionStartListener

internal class FakeSessionManager : SessionManager {
var id = "fake-session-id"
var startTime: Long = 0L
var onSessionStartListener: SessionStartListener? = null

override fun init(): String = id

Expand All @@ -23,4 +25,8 @@ internal class FakeSessionManager : SessionManager {
override fun onConfigLoaded() {
// no-op
}

override fun setSessionStartListener(listener: SessionStartListener) {
onSessionStartListener = listener
}
}
Loading