Skip to content

Instantly share code, notes, and snippets.

@BenHenning
Forked from Sarthak2601/AnalyticsController.kt
Last active July 15, 2020 19:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BenHenning/37ee667b00f07c514b10621c57f5c02b to your computer and use it in GitHub Desktop.
Save BenHenning/37ee667b00f07c514b10621c57f5c02b to your computer and use it in GitHub Desktop.
package org.oppia.domain.oppialogger.analytics
import android.content.Context
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.oppia.app.model.EventLog
import org.oppia.app.model.EventLog.EventAction
import org.oppia.app.model.EventLog.Priority
import org.oppia.app.model.OppiaEventLogs
import org.oppia.data.persistence.PersistentCacheStore
import org.oppia.domain.oppialogger.EventLogStorageCacheSize
import org.oppia.util.data.AsyncResult
import org.oppia.util.data.DataProviders
import org.oppia.util.logging.ConsoleLogger
import org.oppia.util.logging.EventLogger
import org.oppia.util.networking.NetworkConnectionUtil
import org.oppia.util.networking.NetworkConnectionUtil.ConnectionStatus.CELLULAR
import org.oppia.util.networking.NetworkConnectionUtil.ConnectionStatus.LOCAL
import org.oppia.util.networking.NetworkConnectionUtil.ConnectionStatus.NONE
import org.oppia.util.threading.BackgroundDispatcher
import javax.inject.Inject
/** Controller for handling analytics event logging. */
class AnalyticsController @Inject constructor(
private val eventLogger: EventLogger,
cacheStoreFactory: PersistentCacheStore.Factory,
private val dataProviders: DataProviders,
private val consoleLogger: ConsoleLogger,
private val networkConnectionUtil: NetworkConnectionUtil,
@BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher,
@EventLogStorageCacheSize private val eventLogStorageCacheSize: Int
) {
private val eventLogStore =
cacheStoreFactory.create("event_logs", OppiaEventLogs.getDefaultInstance())
private val coroutineScope = CoroutineScope(backgroundCoroutineDispatcher)
/**
* Logs transition events.
* These events are given HIGH priority.
*/
fun logTransitionEvent(
context: Context,
timestamp: Long,
eventAction: EventAction,
eventContext: EventLog.Context?
) {
eventLogger.logEvent(
context,
createEventLog(
timestamp,
eventAction,
eventContext,
Priority.ESSENTIAL
)
)
}
/**
* Logs click events.
* These events are given LOW priority.
*/
fun logClickEvent(
context: Context,
timestamp: Long,
eventAction: EventAction,
eventContext: EventLog.Context?
) {
eventLogger.logEvent(
context,
createEventLog(
timestamp,
eventAction,
eventContext,
Priority.OPTIONAL
)
)
}
/** Returns an event log containing relevant data for event reporting. */
private fun createEventLog(
timestamp: Long,
eventAction: EventAction,
eventContext: EventLog.Context?,
priority: Priority
): EventLog {
val event: EventLog.Builder = EventLog.newBuilder()
event.timestamp = timestamp
event.actionName = eventAction
event.priority = priority
if (eventContext != null)
event.context = eventContext
return event.build()
}
/** Returns the context of an event related to exploration. */
fun createExplorationContext(
topicId: String,
storyId: String,
explorationId: String
): EventLog.Context {
return EventLog.Context.newBuilder()
.setExplorationContext(
EventLog.ExplorationContext.newBuilder()
.setTopicId(topicId)
.setStoryId(storyId)
.setExplorationId(explorationId)
.build()
)
.build()
}
/** Returns the context of an event related to question. */
fun createQuestionContext(
questionId: String,
skillId: List<String>
): EventLog.Context {
return EventLog.Context.newBuilder()
.setQuestionContext(
EventLog.QuestionContext.newBuilder()
.setQuestionId(questionId)
.addAllSkillId(skillId)
.build()
)
.build()
}
/** Returns the context of an event related to topic. */
fun createTopicContext(
topicId: String
): EventLog.Context {
return EventLog.Context.newBuilder()
.setTopicContext(
EventLog.TopicContext.newBuilder()
.setTopicId(topicId)
.build()
)
.build()
}
/** Returns the context of an event related to story. */
fun createStoryContext(
topicId: String,
storyId: String
): EventLog.Context {
return EventLog.Context.newBuilder()
.setStoryContext(
EventLog.StoryContext.newBuilder()
.setTopicId(topicId)
.setStoryId(storyId)
.build()
)
.build()
}
/** Returns the context of an event related to concept card. */
fun createConceptCardContext(
skillId: String
): EventLog.Context {
return EventLog.Context.newBuilder()
.setConceptCardContext(
EventLog.ConceptCardContext.newBuilder()
.setSkillId(skillId)
.build()
)
.build()
}
/** Returns the context of an event related to revision card. */
fun createRevisionCardContext(
topicId: String,
subTopicId: String
): EventLog.Context {
return EventLog.Context.newBuilder()
.setRevisionCardContext(
EventLog.RevisionCardContext.newBuilder()
.setTopicId(topicId)
.setSubTopicId(subTopicId)
.build()
)
.build()
}
fun checkNetworkAndUpload(eventLog: EventLog, context: Context){
when(networkConnectionUtil.getCurrentConnectionStatus()){
NONE -> addEventLog(eventLog)
else -> eventLogger.logEvent(context, eventLog)
}
}
/** Adds an event to the storage. */
private fun addEventLog(eventLog: EventLog) {
eventLogStore.storeDataAsync(updateInMemoryCache = true) { oppiaEventLogs ->
val eventLogBuilder = oppiaEventLogs.toBuilder()
if (oppiaEventLogs.eventLogList.size + 1 > eventLogStorageCacheSize) {
val leastRecentEventIndex = getLeastRecentPriorityEventIndex(oppiaEventLogs)
// NOTE TO SARTHAK: this index being absent indicates a critical failure--it means eventLogStoreCacheSize is 0. You should probably log an error in this case.
leastRecentEventIndex?.let { eventLogBuilder.removeEventLog(it) }
}
return@storeDataAsync eventLogBuilder.addEventLog(eventLog).build()
}.invokeOnCompletion {
it?.let {
consoleLogger.e(
"DOMAIN",
"Failed to store event log",
it
)
}
}
}
/**
* Returns the least recent event from the existing store on the basis of recency and priority.
* At first, it checks the least recent event which has OPTIONAL priority.
* If that returns null, then the least recent event regardless of the priority is returned.
*/
private fun getLeastRecentPriorityEventIndex(oppiaEventLogs: OppiaEventLogs): Int? =
oppiaEventLogs.eventLogList.withIndex()
.filter { it.value.priority == EventLog.Priority.OPTIONAL }
.minBy { it.value.timestamp }?.index ?: getLeastRecentEventIndex(oppiaEventLogs)
private fun getLeastRecentEventIndex(oppiaEventLogs: OppiaEventLogs): Int? =
oppiaEventLogs.eventLogList.withIndex().minBy { it.value.timestamp }?.index
/**
* Returns a [LiveData] result which can be used to get [OppiaEventLogs]
* for the purpose of uploading in the presence of network connectivity.
*/
fun getEventLogs(): LiveData<AsyncResult<OppiaEventLogs>> {
return dataProviders.convertToLiveData(eventLogStore)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment