Skip to content

Commit 1c5bb5d

Browse files
authored
fix: broken sessions forground tracking (#308)
* fix: Broken sessions foreground tracking
1 parent 94ab920 commit 1c5bb5d

File tree

4 files changed

+66
-35
lines changed

4 files changed

+66
-35
lines changed

android/src/main/java/com/amplitude/android/Amplitude.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,14 @@ open class Amplitude internal constructor(
126126
return this
127127
}
128128

129-
@Deprecated("This method is deprecated and a no-op.")
129+
@GuardedAmplitudeFeature
130130
fun onEnterForeground(timestamp: Long) {
131-
// no-op
131+
(timeline as Timeline).onEnterForeground(timestamp)
132132
}
133133

134-
@Deprecated("This method is deprecated and a no-op.")
134+
@GuardedAmplitudeFeature
135135
fun onExitForeground(timestamp: Long) {
136-
// no-op
136+
(timeline as Timeline).onExitForeground(timestamp)
137137
}
138138

139139
private fun registerShutdownHook() {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.amplitude.android
2+
3+
@RequiresOptIn(
4+
message =
5+
"This feature is guarded and should only be used by Amplitude SDK developers. " +
6+
"It is not intended for public use and may change without notice.",
7+
)
8+
@Retention(AnnotationRetention.BINARY)
9+
annotation class GuardedAmplitudeFeature

android/src/main/java/com/amplitude/android/Timeline.kt

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import java.util.concurrent.atomic.AtomicBoolean
1515
import java.util.concurrent.atomic.AtomicLong
1616

1717
private const val DEFAULT_SESSION_ID = -1L
18-
private const val DEFAULT = 0L
18+
private const val DEFAULT_EVENT_ID_OR_TIME = 0L
1919

2020
class Timeline(
2121
private val initialSessionId: Long? = null,
@@ -29,9 +29,9 @@ class Timeline(
2929
return _sessionId.get()
3030
}
3131

32-
internal var lastEventId: Long = DEFAULT
32+
internal var lastEventId: Long = DEFAULT_EVENT_ID_OR_TIME
3333
private set
34-
internal var lastEventTime: Long = DEFAULT
34+
internal var lastEventTime: Long = DEFAULT_EVENT_ID_OR_TIME
3535
private set
3636

3737
internal fun start() {
@@ -43,8 +43,8 @@ class Timeline(
4343
if (initialSessionId == null) {
4444
_sessionId.set(storage.readLong(PREVIOUS_SESSION_ID, DEFAULT_SESSION_ID))
4545
}
46-
lastEventId = storage.readLong(LAST_EVENT_ID, DEFAULT)
47-
lastEventTime = storage.readLong(LAST_EVENT_TIME, DEFAULT)
46+
lastEventId = storage.readLong(LAST_EVENT_ID, DEFAULT_EVENT_ID_OR_TIME)
47+
lastEventTime = storage.readLong(LAST_EVENT_TIME, DEFAULT_EVENT_ID_OR_TIME)
4848

4949
for (message in eventMessageChannel) {
5050
processEventMessage(message)
@@ -57,39 +57,58 @@ class Timeline(
5757
this.eventMessageChannel.cancel()
5858
}
5959

60+
/**
61+
* Enqueue an event to be processed by the timeline.
62+
*/
6063
override fun process(incomingEvent: BaseEvent) {
6164
if (incomingEvent.timestamp == null) {
6265
incomingEvent.timestamp = System.currentTimeMillis()
6366
}
6467

65-
eventMessageChannel.trySend(EventQueueMessage(incomingEvent))
68+
val result = eventMessageChannel.trySend(EventQueueMessage.Event(incomingEvent))
69+
if (result.isFailure) {
70+
amplitude.logger.error("Failed to enqueue event: ${incomingEvent.eventType}. Channel is closed or full.")
71+
}
6672
}
6773

6874
internal fun onEnterForeground(timestamp: Long) {
6975
amplitude.amplitudeScope.launch(amplitude.storageIODispatcher) {
70-
val localSessionEvents = startNewSessionIfNeeded(timestamp)
71-
foreground.set(true)
72-
73-
// Process any local session events
74-
processAndPersistEvents(localSessionEvents)
76+
eventMessageChannel.trySend(EventQueueMessage.EnterForeground(timestamp))
7577
}
7678
}
7779

7880
internal fun onExitForeground(timestamp: Long) {
7981
amplitude.amplitudeScope.launch(amplitude.storageIODispatcher) {
80-
refreshSessionTime(timestamp)
81-
foreground.set(false)
82+
eventMessageChannel.trySend(EventQueueMessage.ExitForeground(timestamp))
8283
}
8384
}
8485

86+
/**
87+
* Process an event message from the event queue.
88+
*/
8589
private suspend fun processEventMessage(message: EventQueueMessage) {
86-
val event = message.event
87-
val eventTimestamp = event.timestamp!! // Guaranteed non-null by process()
88-
val eventSessionId = event.sessionId
90+
when (message) {
91+
is EventQueueMessage.EnterForeground -> {
92+
foreground.set(true)
93+
val sessionEvents = startNewSessionIfNeeded(message.timestamp)
94+
processAndPersistEvents(sessionEvents)
95+
}
96+
is EventQueueMessage.Event -> {
97+
processEvent(message.event)
98+
}
99+
is EventQueueMessage.ExitForeground -> {
100+
foreground.set(false)
101+
refreshSessionTime(message.timestamp)
102+
}
103+
}
104+
}
105+
106+
private suspend fun processEvent(event: BaseEvent) {
107+
val eventTimestamp = event.timestamp ?: System.currentTimeMillis()
89108

90109
when (event.eventType) {
91110
START_SESSION_EVENT -> {
92-
setSessionId(eventSessionId ?: eventTimestamp)
111+
setSessionId(event.sessionId ?: eventTimestamp)
93112
refreshSessionTime(eventTimestamp)
94113
}
95114

@@ -99,17 +118,14 @@ class Timeline(
99118

100119
else -> {
101120
if (!foreground.get()) {
102-
val localSessionEvents = startNewSessionIfNeeded(eventTimestamp)
103-
processAndPersistEvents(localSessionEvents)
121+
val sessionEvents = startNewSessionIfNeeded(eventTimestamp)
122+
processAndPersistEvents(sessionEvents)
104123
} else {
105124
refreshSessionTime(eventTimestamp)
106125
}
107126
}
108127
}
109128

110-
// Assign sessionId to the current event if it doesn't have one
111-
event.sessionId = event.sessionId ?: this.sessionId
112-
113129
// Process the incoming event
114130
processAndPersistEvents(listOf(event))
115131
}
@@ -119,6 +135,9 @@ class Timeline(
119135

120136
val initialLastEventId = lastEventId
121137
for (event in events) {
138+
// Assign sessionId to the current event if it doesn't have one
139+
event.sessionId = event.sessionId ?: this.sessionId
140+
// Increment and set eventId if it is not set
122141
event.eventId = event.eventId ?: ++lastEventId
123142
super.process(event)
124143
}
@@ -151,7 +170,7 @@ class Timeline(
151170
if (trackingSessionEvents && inSession()) {
152171
val sessionEndEvent = BaseEvent()
153172
sessionEndEvent.eventType = END_SESSION_EVENT
154-
sessionEndEvent.timestamp = lastEventTime.takeIf { lastEventTime > DEFAULT }
173+
sessionEndEvent.timestamp = lastEventTime.takeIf { lastEventTime > DEFAULT_EVENT_ID_OR_TIME }
155174
sessionEndEvent.sessionId = sessionId
156175
sessionEvents.add(sessionEndEvent)
157176
}
@@ -196,6 +215,10 @@ class Timeline(
196215
}
197216
}
198217

199-
data class EventQueueMessage(
200-
val event: BaseEvent,
201-
)
218+
sealed class EventQueueMessage {
219+
data class Event(val event: BaseEvent) : EventQueueMessage()
220+
221+
data class EnterForeground(val timestamp: Long) : EventQueueMessage()
222+
223+
data class ExitForeground(val timestamp: Long) : EventQueueMessage()
224+
}

android/src/main/java/com/amplitude/android/plugins/AndroidLifecyclePlugin.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.amplitude.android.AutocaptureOption.DEEP_LINKS
1212
import com.amplitude.android.AutocaptureOption.ELEMENT_INTERACTIONS
1313
import com.amplitude.android.AutocaptureOption.SCREEN_VIEWS
1414
import com.amplitude.android.Configuration
15+
import com.amplitude.android.GuardedAmplitudeFeature
1516
import com.amplitude.android.utilities.ActivityCallbackType
1617
import com.amplitude.android.utilities.ActivityLifecycleObserver
1718
import com.amplitude.android.utilities.DefaultEventUtils
@@ -21,8 +22,8 @@ import kotlinx.coroutines.Dispatchers
2122
import kotlinx.coroutines.Job
2223
import kotlinx.coroutines.launch
2324
import com.amplitude.android.Amplitude as AndroidAmplitude
24-
import com.amplitude.android.Timeline as AndroidTimeline
2525

26+
@OptIn(GuardedAmplitudeFeature::class)
2627
class AndroidLifecyclePlugin(
2728
private val activityLifecycleObserver: ActivityLifecycleObserver,
2829
) : Application.ActivityLifecycleCallbacks, Plugin {
@@ -118,9 +119,7 @@ class AndroidLifecyclePlugin(
118119
}
119120

120121
override fun onActivityResumed(activity: Activity) {
121-
with(androidAmplitude) {
122-
(timeline as AndroidTimeline).onEnterForeground(System.currentTimeMillis())
123-
}
122+
androidAmplitude.onEnterForeground(System.currentTimeMillis())
124123

125124
if (ELEMENT_INTERACTIONS in autocapture) {
126125
DefaultEventUtils(androidAmplitude).startUserInteractionEventTracking(activity)
@@ -129,7 +128,7 @@ class AndroidLifecyclePlugin(
129128

130129
override fun onActivityPaused(activity: Activity) {
131130
with(androidAmplitude) {
132-
(timeline as AndroidTimeline).onExitForeground(System.currentTimeMillis())
131+
onExitForeground(System.currentTimeMillis())
133132

134133
if ((configuration as Configuration).flushEventsOnClose) {
135134
flush()

0 commit comments

Comments
 (0)