diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e8b2ddc3..b13aee90 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,10 +33,8 @@ android { useSupportLibrary = true } - buildConfigField("String", "ACCOUNT_ID", "\"${props.getProperty("accountId")}\"") - buildConfigField("String", "SECRET_KEY", "\"${props.getProperty("secretKey")}\"") buildConfigField("String", "ENDPOINT", "\"${props.getProperty("endpoint")}\"") - buildConfigField("String", "STUB_BEARER", "\"${props.getProperty("bearer")}\"") + buildConfigField("String", "API_KEY", "\"${props.getProperty("apiKey")}\"") } sourceSets { diff --git a/app/src/main/java/com/clickstream/app/App.kt b/app/src/main/java/com/clickstream/app/App.kt index c8582a3a..b1d52f22 100644 --- a/app/src/main/java/com/clickstream/app/App.kt +++ b/app/src/main/java/com/clickstream/app/App.kt @@ -1,25 +1,22 @@ package com.clickstream.app import android.app.Application +import android.provider.Settings +import android.util.Base64 import clickstream.ClickStream import clickstream.config.CSConfiguration import clickstream.connection.CSConnectionEvent import clickstream.connection.CSSocketConnectionListener import clickstream.eventvisualiser.CSEventVisualiserListener import clickstream.eventvisualiser.ui.CSEventVisualiserUI -import clickstream.health.constant.CSTrackedVia -import clickstream.lifecycle.impl.DefaultCSAppLifeCycleObserver import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import com.clickstream.app.config.AccountId -import com.clickstream.app.config.EndPoint -import com.clickstream.app.config.SecretKey -import com.clickstream.app.config.StubBearer import com.clickstream.app.config.csConfig import com.clickstream.app.config.csInfo +import com.clickstream.app.config.getHealthGateway import com.clickstream.app.helper.printMessage import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.ExperimentalCoroutinesApi +import java.util.* @OptIn(ExperimentalCoroutinesApi::class) @HiltAndroidApp @@ -28,25 +25,25 @@ class App : Application() { override fun onCreate() { super.onCreate() - val csLogger = CSLogger(CSLogLevel.DEBUG) - ClickStream.initialize( configuration = CSConfiguration.Builder( context = this, - info = csInfo(), config = csConfig( - AccountId(BuildConfig.ACCOUNT_ID), - SecretKey(BuildConfig.SECRET_KEY), - EndPoint(BuildConfig.ENDPOINT), - StubBearer(BuildConfig.STUB_BEARER), - CSTrackedVia.Both + url = BuildConfig.ENDPOINT, + deviceId = deviceId(), + apiKey = String( + Base64.encode( + BuildConfig.API_KEY.toByteArray(), + Base64.NO_WRAP + ) + ) ), - appLifeCycle = DefaultCSAppLifeCycleObserver(csLogger) - ) - .applyLogLevel() - .applyEventListener() - .applySocketConnectionListener() - .build() + info = csInfo() + ).applyLogLevel() + .applyEventListener() + .applyHealthFactory() + .applySocketConnectionListener() + .build() ) CSEventVisualiserUI.initialise(this) @@ -77,4 +74,16 @@ class App : Application() { }) return this } + + private fun CSConfiguration.Builder.applyHealthFactory(): CSConfiguration.Builder { + setHealthGateway(getHealthGateway(this@App)) + return this + } + + private fun deviceId(): String { + return Settings.Secure.getString( + contentResolver, + Settings.Secure.ANDROID_ID + ) ?: UUID.randomUUID().toString() + } } diff --git a/app/src/main/java/com/clickstream/app/config/CSInfo.kt b/app/src/main/java/com/clickstream/app/config/CSInfo.kt index 1635f911..5c8e3c91 100644 --- a/app/src/main/java/com/clickstream/app/config/CSInfo.kt +++ b/app/src/main/java/com/clickstream/app/config/CSInfo.kt @@ -7,7 +7,7 @@ import clickstream.api.CSSessionInfo import clickstream.api.CSUserInfo fun csInfo() = CSInfo( - appInfo = CSAppInfo(appVersion = "1.1.0"), + appInfo = CSAppInfo(appVersion = "2.1.0"), locationInfo = CSLocationInfo( latitude = -6.1753871, longitude = 106.8249641, diff --git a/app/src/main/java/com/clickstream/app/config/DefaultCSConfig.kt b/app/src/main/java/com/clickstream/app/config/DefaultCSConfig.kt index 9aac5f7a..d69c2715 100644 --- a/app/src/main/java/com/clickstream/app/config/DefaultCSConfig.kt +++ b/app/src/main/java/com/clickstream/app/config/DefaultCSConfig.kt @@ -4,16 +4,13 @@ import clickstream.config.CSConfig import clickstream.config.CSEventProcessorConfig import clickstream.config.CSEventSchedulerConfig import clickstream.config.CSNetworkConfig -import clickstream.health.constant.CSTrackedVia import clickstream.health.model.CSHealthEventConfig import com.clickstream.app.helper.load fun csConfig( - accountId: AccountId, - secretKey: SecretKey, - endpoint: EndPoint, - stubBearer: StubBearer, - trackedVia: CSTrackedVia + url: String, + deviceId: String, + apiKey: String, ): CSConfig { val eventClassification = CSEventClassificationParser::class.java.load("clickstream_classifier.json")!! @@ -23,10 +20,12 @@ fun csConfig( realtimeEvents = eventClassification.realTimeEvents(), instantEvent = eventClassification.instantEvents() ), - eventSchedulerConfig = CSEventSchedulerConfig.default(), + eventSchedulerConfig = CSEventSchedulerConfig.default().copy(backgroundTaskEnabled = true), networkConfig = CSNetworkConfig.default( - CSNetworkModule.create(accountId, secretKey, stubBearer) - ).copy(endPoint = endpoint.value), - healthEventConfig = CSHealthEventConfig.default(trackedVia) + url = url, mapOf( + "Authorization" to "Basic $apiKey", + "X-UniqueId" to deviceId + ) + ), ) } diff --git a/app/src/main/java/com/clickstream/app/config/DefaultHealthConfig.kt b/app/src/main/java/com/clickstream/app/config/DefaultHealthConfig.kt new file mode 100644 index 00000000..7b34b093 --- /dev/null +++ b/app/src/main/java/com/clickstream/app/config/DefaultHealthConfig.kt @@ -0,0 +1,50 @@ +package com.clickstream.app.config + +import DefaultCSHealthGateway +import android.content.Context +import android.util.Log +import clickstream.health.intermediate.CSHealthEventLoggerListener +import clickstream.health.intermediate.CSMemoryStatusProvider +import clickstream.health.model.CSHealthEventConfig +import clickstream.health.time.CSHealthTimeStampGenerator +import clickstream.logger.CSLogLevel +import clickstream.logger.CSLogger + + +fun getHealthGateway(context: Context) = DefaultCSHealthGateway( + context = context, + csInfo = csInfo(), + logger = CSLogger(CSLogLevel.DEBUG), + timeStampGenerator = timeStampGenerator(), + csMemoryStatusProvider = memoryStatusProvider(), + csHealthEventConfig = healthConfig(), + csHealthEventLoggerListener = healthEventLogger() +) + + +fun healthEventLogger() = object : CSHealthEventLoggerListener { + override fun logEvent(eventName: String, healthData: HashMap) { + Log.d("CS External Logger", "$eventName: $healthData") + } + +} + +fun memoryStatusProvider() = object : CSMemoryStatusProvider { + override fun isLowMemory(): Boolean { + return false + } +} + +fun timeStampGenerator() = object : CSHealthTimeStampGenerator { + override fun getTimeStamp(): Long { + return System.currentTimeMillis() + } +} + +fun healthConfig() = + CSHealthEventConfig( + minTrackedVersion = "1.1.0", + randomUserIdRemainder = (0..9).toList(), + destination = listOf("CS", "CT"), + verbosityLevel = "minimum" + ) \ No newline at end of file diff --git a/app/src/main/java/com/clickstream/app/main/MainViewModel.kt b/app/src/main/java/com/clickstream/app/main/MainViewModel.kt index 407943da..00caa663 100644 --- a/app/src/main/java/com/clickstream/app/main/MainViewModel.kt +++ b/app/src/main/java/com/clickstream/app/main/MainViewModel.kt @@ -2,8 +2,8 @@ package com.clickstream.app.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import clickstream.CSEvent import clickstream.ClickStream -import clickstream.model.CSEvent import com.clickstream.app.helper.Dispatcher import com.clickstream.app.helper.printMessage import com.clickstream.app.main.MainIntent.ConnectIntent diff --git a/build.gradle.kts b/build.gradle.kts index ed4fd990..09b44ff5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import plugin.DetektConfigurationPlugin plugins { - id("org.jetbrains.dokka") version "1.4.32" + id("org.jetbrains.dokka") version "1.6.0" } apply(plugin = "binary-compatibility-validator") @@ -17,7 +17,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:7.0.4") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.32") classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.14") classpath("org.jetbrains.kotlinx:binary-compatibility-validator:0.8.0") classpath("com.google.dagger:hilt-android-gradle-plugin:2.41") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c3a44fb1..6c772291 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -17,7 +17,7 @@ plugins { dependencies { implementation("com.android.tools.build:gradle:7.0.4") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0") implementation("com.github.node-gradle:gradle-node-plugin:2.2.0") implementation("org.codehaus.groovy:groovy:3.0.9") implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.15.0") diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt index d831c0c8..6eab2151 100644 --- a/buildSrc/src/main/kotlin/deps.kt +++ b/buildSrc/src/main/kotlin/deps.kt @@ -2,20 +2,22 @@ object versions { internal const val jacoco = "0.8.4" - internal const val detekt = "1.1.1" + internal const val detekt = "1.16.0" - internal const val kotlin = "1.4.32" + internal const val kotlin = "1.5.32" internal const val coroutines = "1.5.2" internal const val scarlet = "0.1.10" internal const val okHttp = "3.12.1" - internal const val room = "2.2.3" - internal const val lifecycle = "2.2.0" + internal const val room = "2.4.2" + internal const val lifecycle = "2.4.1" internal const val workManagerVersion = "2.7.1" internal const val csProtoVersion = "1.18.2" + + internal const val csEventListenerVersion = "2.0.0-alpha-1" } object deps { diff --git a/clickstream-health-metrics-api/build.gradle.kts b/clickstream-health-metrics-api/build.gradle.kts index c5de58e5..31bf454c 100644 --- a/clickstream-health-metrics-api/build.gradle.kts +++ b/clickstream-health-metrics-api/build.gradle.kts @@ -35,6 +35,8 @@ android { dependencies { // Clickstream compileOnly(files("$rootDir/libs/proto-sdk-1.18.6.jar")) + implementation(deps.kotlin.coroutines.core) + implementation(deps.android.core.annotation) } diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/CSHealthGateway.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/CSHealthGateway.kt index 7ec610b3..ff3538e5 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/CSHealthGateway.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/CSHealthGateway.kt @@ -1,18 +1,25 @@ package clickstream.health import androidx.annotation.RestrictTo -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository +/** + * Wrapper class that creates [CSHealthEventProcessor]. + * + * */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public interface CSHealthGateway { - public val eventHealthListener: CSEventHealthListener - public val healthEventRepository: CSHealthEventRepository + /** + * Class to process health events. + * + * */ + public val healthEventProcessor: CSHealthEventProcessor? - public val healthEventProcessor: CSHealthEventProcessor + /** + * Clears health events on app version upgrade. + * + * */ + public suspend fun clearHealthEventsForVersionChange() - public val healthEventFactory: CSHealthEventFactory } \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSErrorConstant.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSErrorConstant.kt new file mode 100644 index 00000000..0573407a --- /dev/null +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSErrorConstant.kt @@ -0,0 +1,13 @@ +package clickstream.health.constant + +public object CSErrorConstant { + public const val PARSING_EXCEPTION: String = "parsing_exception" + public const val LOW_BATTERY: String = "low_battery" + public const val NETWORK_UNAVAILABLE: String = "network_unavailable" + public const val SOCKET_NOT_OPEN: String = "socket_not_open" + public const val UNKNOWN: String = "unknown" + public const val USER_UNAUTHORIZED: String = "401 Unauthorized" + public const val SOCKET_TIMEOUT: String = "socket_timeout" + public const val MAX_USER_LIMIT_REACHED: String = "max_user_limit_reached" + public const val MAX_CONNECTION_LIMIT_REACHED: String = "max_connection_limit_reached" +} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventNamesConstant.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventNamesConstant.kt index 6e0b26ab..82528ffc 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventNamesConstant.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventNamesConstant.kt @@ -3,134 +3,23 @@ package clickstream.health.constant import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public enum class CSEventNamesConstant(public val value: String) {; +public enum class CSHealthEventName(public val value: String) { - public enum class Instant(public val value: String) { - /** - * Tracks the instances where the connection gets dropped. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamConnectionDropped("Clickstream Connection Dropped"), + // Batch sent events + ClickStreamBatchSent("ClickStream Batch Sent"), + ClickStreamEventBatchTriggerFailed("Clickstream Event Batch Trigger Failed"), + ClickStreamBatchWriteFailed("ClickStream Write to Socket Failed"), - /** - * Tracks the connection attempt instances. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamConnectionAttempt("Clickstream Connection Attempt"), + // Socket connection events + ClickStreamConnectionFailed("ClickStream Connection Failed"), - /** - * Tracks the connection attempt success instances. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamConnectionSuccess("Clickstream Connection Success"), + // Batch acknowledgement events + ClickStreamEventBatchErrorResponse("Clickstream Event Batch Error response"), + ClickStreamEventBatchAck("Clickstream Event Batch Success Ack"), + ClickStreamEventBatchTimeout("Clickstream Event Batch Timeout"), - /** - * Tracks the connection attempt failure instances. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamConnectionFailure("Clickstream Connection Failure"), - - /** - * Tracks the instances when the clickstream event batch gets timed out. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamEventBatchTimeout("Clickstream Event Batch Timeout"), - - /** - * Tracks the instances when the clickstream event batch fails to get written on the socket. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamWriteToSocketFailed("ClickStream Write to Socket Failed"), - - /** - * Tracks the instances when the batch fails to get triggered. - * - * Type: Instant - * Priority: Low - */ - ClickStreamEventBatchTriggerFailed("Clickstream Event Batch Trigger Failed"), - - /** - * Tracks the instances when the clickstream request results in a error response. - * - * Type: Instant - * Priority: Critical - */ - ClickStreamEventBatchErrorResponse("Clickstream Event Batch Error response"), - } - - public enum class Flushed(public val value: String) { - /** - * This event is track the drop rate comparison only and not the part of the funnel. - * Would be triggered for the event which is used to track the drops Eg. `AdCardEvent` - * - * Type: Flushed - * Priority: Critical - */ - ClickStreamEventReceivedForDropRate("Clickstream Event Received For Drop Rate"), - - /** - * Tracks the instances where the event is received by the Clickstream library - * - * Type: Flushed - * Priority: Critical - */ - ClickStreamEventReceived("Clickstream Event Received"), - } - - public enum class AggregatedAndFlushed(public val value: String) { - /** - * Tracks the instances when the clickstream event object is cached. - * - * Type: Aggregated and Flushed - * Priority: Low - */ - ClickStreamEventCached("Clickstream Event Cached"), - - /** - * Tracks the instances when the clickstream event batch is created. - * - * Type: Aggregated and Flushed - * Priority: Low - */ - ClickStreamEventBatchCreated("Clickstream Event Batch Created"), - - /** - * Tracks the instances when the clickstream batch gets successfully sent to raccoon. - * - * Type: Aggregated and Flushed - * Priority: Low - */ - ClickStreamBatchSent("ClickStream Batch Sent"), - - /** - * Tracks the instances when raccoon acks the event request. - * - * Type: Aggregated and Flushed - * Priority: Low - */ - ClickStreamEventBatchSuccessAck("Clickstream Event Batch Success Ack"), - - /** - * Tracks the instances when the clickstream batches are flushed on background. - * - * Type: Aggregated and Flushed - * Priority: Critical - */ - ClickStreamFlushOnBackground("ClickStream Flush On Background"), - - } + // Flush events + ClickStreamFlushOnBackground("ClickStream Flush On Background"), + ClickStreamFlushOnForeground("Clickstream Flush On Foreground"), } diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventTypesConstant.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventTypesConstant.kt index cb5af6b2..d622ff62 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventTypesConstant.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSEventTypesConstant.kt @@ -2,6 +2,7 @@ package clickstream.health.constant import androidx.annotation.RestrictTo + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public object CSEventTypesConstant { public const val INSTANT: String = "instant" diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSHealthEventConstant.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSHealthEventConstant.kt deleted file mode 100644 index 2144f588..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSHealthEventConstant.kt +++ /dev/null @@ -1,22 +0,0 @@ -package clickstream.health.constant - -import androidx.annotation.RestrictTo - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public object CSHealthEventConstant { - public const val HEALTH_ID: String = "healthEventID" - public const val HEALTH_EVENT_NAME: String = "eventName" - public const val HEALTH_EVENT_TYPE: String = "eventType" - public const val HEALTH_EVENT_TIMESTAMP: String = "timestamp" - public const val HEALTH_EVENT_ID: String = "eventId" - public const val HEALTH_EVENT_BATCH_ID: String = "eventBatchId" - public const val HEALTH_ERROR: String = "error" - public const val HEALTH_SESSION_ID: String = "sessionId" - public const val HEALTH_EVENT_COUNT: String = "count" - public const val HEALTH_NETWORK_TYPE: String = "networkType" - public const val HEALTH_START_TIME: String = "startTime" - public const val HEALTH_STOP_TIME: String = "stopTime" - public const val HEALTH_BUCKET_TYPE: String = "bucketType" - public const val HEALTH_BATCH_SIZE: String = "batchSize" - public const val HEALTH_APP_VERSION: String = "appVersion" -} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSTrackedVia.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSTrackedVia.kt deleted file mode 100644 index fa97e252..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/constant/CSTrackedVia.kt +++ /dev/null @@ -1,7 +0,0 @@ -package clickstream.health.constant - -public enum class CSTrackedVia { - External, - Internal, - Both -} diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSEventHealthListener.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSEventHealthListener.kt deleted file mode 100644 index f92ba173..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSEventHealthListener.kt +++ /dev/null @@ -1,41 +0,0 @@ -package clickstream.health.intermediate - -import androidx.annotation.RestrictTo -import clickstream.health.model.CSEventHealth - -/** - * [CSEventHealthListener] Essentially an optional listener which being used for - * perform an analytic metrics to check every event size. We're exposed listener - * so that if the host app wants to check each event size they can simply add the listener. - * - * Proto `MessageLite` provide an API that we're able to use to check the byte size which is - * [messageSerializedSizeInBytes]. - * - * **Example:** - * ```kotlin - * private fun applyEventHealthMetrics(config: CSClickStreamConfig): CSEventHealthListener { - * if (config.isEventHealthListenerEnabled.not()) return NoOpCSEventHealthListener() - * - * return object : CSEventHealthListener { - * override fun onEventCreated(healthEvent: CSEventHealth) { - * executor.execute { - * val trace = FirebasePerformance.getInstance().newTrace("CS_Event_Health_Metrics") - * trace.start() - * trace.putMetric( - * healthEvent.messageName, - * healthEvent.messageSerializedSizeInBytes.toLong() - * ) - * trace.stop() - * } - * } - * } - * } - * ``` - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public interface CSEventHealthListener { - /** - * [CSEventHealth] hold event meta information. - */ - public fun onEventCreated(healthEvent: CSEventHealth) -} diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventLoggerListener.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventLoggerListener.kt index 8dc9e850..e68701b8 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventLoggerListener.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventLoggerListener.kt @@ -1,16 +1,27 @@ package clickstream.health.intermediate -import clickstream.health.model.CSHealthEvent +import clickstream.health.constant.CSHealthKeysConstant /** - * ClickStreamTracker can be implemented by host app to provide observer for ClickStream events + * + * Listener to observe Health events during background flush. Client app needs to + * provide an implementation for this. To be used from [CSHealthEventProcessor.getHealthEventFlow]. + * + * This can be useful if client wants to track clickstream health events on some + * third party service (like firebase) and monitor clickstream health. + * + * Flow is as follows: + * + * App goes to background -> Events flush -> Health event flush -> notify client app about health events + * via [CSHealthEventLoggerListener]. + * */ public interface CSHealthEventLoggerListener { /** - * Method called to notify observer about ClickStream events + * Method called to notify client app. * @param eventName - * @param healthEvent + * @param healthData, Check [CSHealthKeysConstant] for keys. */ - public fun logEvent(eventName: String, healthEvent: CSHealthEvent) + public fun logEvent(eventName: String, healthData: HashMap) } diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventProcessor.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventProcessor.kt index 0160ba13..de7ac249 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventProcessor.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventProcessor.kt @@ -1,41 +1,64 @@ package clickstream.health.intermediate -import androidx.annotation.RestrictTo +import clickstream.health.model.CSEventForHealth +import clickstream.health.model.CSHealthEvent import com.gojek.clickstream.internal.Health +import kotlinx.coroutines.flow.Flow /** - * [CSHealthEventProcessor] is the Heart of the Clickstream Library. The [CSHealthEventProcessor] - * is only for pushing events to the backend. [CSHealthEventProcessor] is respect to the - * Application lifecycle where on the active state, we have a ticker that will collect events from database - * and the send that to the backend. The ticker will run on every 10seconds and will be stopped - * whenever application on the inactive state. + * Class dealing with health events. It has following responsibilities : * - * On the inactive state we will running flush for both Events and HealthEvents, where - * it would be transformed and send to the backend. + * 1) Logging clickstream health events. + * 2) Returning stream of health events for processing. + * 3) Sending health event data to upstream listener. * - * **Sequence Diagram** - * ``` - * App Clickstream - * +---+---+---+---+---+---+ +---+---+---+---+---+---+ - * | Sending Events | --------> | Received the Events | - * +---+---+---+---+---+---+ +---+---+---+---+---+---+ - * | - * | - * | +---+---+---+---+---+---+---+---+----+ - * if app on active state ---------> | - run the ticker with 10s delay | - * | | - collect events from db | - * | | - transform and send to backend | - * | +---+---+---+---+---+---+---+---+----+ - * | - * | +---+---+---+---+---+---+---+---+---+---+----+ - * else if app on inactive state --> | - run flushEvents and flushHealthMetrics | - * | - transform and send to backend | - * +---+---+---+---+---+---+---+---+---+----+---+ - *``` - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + * + * */ public interface CSHealthEventProcessor { - public suspend fun getAggregateEvents(): List - public suspend fun getInstantEvents(): List -} + /** + * Process non batch health events (like socket connection). + * These events do not require batchSize related data. + * + * @param csEvent: [CSHealthEvent] instance to be logged. + * */ + public suspend fun insertNonBatchEvent(csEvent: CSHealthEvent): Boolean + + /** + * Process batch events health events (like batch failed, success). + * + * @param csEvent: [CSHealthEvent] instance to be logged. + * @param list: [CSEventForHealth] list required for tracking data like eventCount, guids etc. + * + * */ + public suspend fun insertBatchEvent( + csEvent: CSHealthEvent, + list: List + ): Boolean + + /** + * Process batch events health events (like batch failed, success). Call this if only batchSize + * matters. + * + * @param csEvent: [CSHealthEvent] instance to be logged. + * @param eventCount: number of events in the batch. + * + * */ + public suspend fun insertBatchEvent( + csEvent: CSHealthEvent, + eventCount: Long, + ): Boolean + + /** + * Returns flow of health events with type. + * + * */ + public fun getHealthEventFlow(type: String, deleteEvents: Boolean = false): Flow> + + + /** + * Pushing events to upstream listener. + * + */ + public suspend fun pushEventToUpstream(type: String, deleteEvents: Boolean) +} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventRepository.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventRepository.kt deleted file mode 100644 index 972f8a31..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventRepository.kt +++ /dev/null @@ -1,50 +0,0 @@ -package clickstream.health.intermediate - -import androidx.annotation.RestrictTo -import clickstream.health.model.CSHealthEventDTO - -/** - * [CSHealthEventRepository] Act as repository pattern where internally it doing DAO operation - * to insert, delete, and read the [CSHealthEvent]'s. - * - * If you're using `com.gojek.clickstream:clickstream-health-metrics-noop`, the - * [CSHealthEventRepository] internally will doing nothing. - * - * Do consider to use `com.gojek.clickstream:clickstream-health-metrics`, to operate - * [CSHealthEventRepository] as expected. Whenever you opt in the `com.gojek.clickstream:clickstream-health-metrics`, - * you should never touch the [DefaultCSHealthEventRepository] explicitly. All the wiring - * is happening through [DefaultCSHealthGateway.factory(/*args*/)] - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public interface CSHealthEventRepository { - - /** - * A function to insert the health event into the DB - */ - public suspend fun insertHealthEvent(healthEvent: CSHealthEventDTO) - - /** - * A function to insert the health event list into the DB - */ - public suspend fun insertHealthEventList(healthEventList: List) - - /** - * A function to retrieve all the instant health events in the DB - */ - public suspend fun getInstantEvents(): List - - /** - * A function to retrieve all the aggregate health events in the DB - */ - public suspend fun getAggregateEvents(): List - - /** - * A function to delete all the health events for a sessionID - */ - public suspend fun deleteHealthEventsBySessionId(sessionId: String) - - /** - * A function to delete the given health events - */ - public suspend fun deleteHealthEvents(events: List) -} diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSMemoryStatusProvider.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSMemoryStatusProvider.kt new file mode 100644 index 00000000..4f79db39 --- /dev/null +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSMemoryStatusProvider.kt @@ -0,0 +1,14 @@ +package clickstream.health.intermediate + +/** + * Class responsible to provide current RAM status of device. Client needs to implement this. + * This is used inside [CSHealthEventProcessor] to check memory before processing health events. + * + * */ +public interface CSMemoryStatusProvider { + + /** + * @return true if memory is under pressure. + */ + public fun isLowMemory(): Boolean +} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventForHealth.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventForHealth.kt new file mode 100644 index 00000000..5a29701a --- /dev/null +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventForHealth.kt @@ -0,0 +1,12 @@ +package clickstream.health.model + +import clickstream.health.intermediate.CSHealthEventProcessor + +/** + * DTO class used by [CSHealthEventProcessor] for filling in events related details in health. + * + * */ +public data class CSEventForHealth( + public val eventGuid: String, + public val batchGuid: String, +) \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventHealth.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventHealth.kt deleted file mode 100644 index 37205236..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSEventHealth.kt +++ /dev/null @@ -1,12 +0,0 @@ -package clickstream.health.model - -/** - * A data class which expose event meta. - */ -public data class CSEventHealth( - val eventGuid: String, - val eventTimeStamp: Long, - val messageSerializedSizeInBytes: Int, - val messageName: String, - val eventName: String -) diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEvent.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEvent.kt index e328270e..fb55e1a5 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEvent.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEvent.kt @@ -1,16 +1,68 @@ package clickstream.health.model -/** - * Stream of Health events generated by Clickstream which a client can listen and - * send it to any third party analytics tracker. This can be used to measure - * the Clickstream SDK event drop rate, socket failure or any other backend issues. - */ +import androidx.annotation.RestrictTo +import clickstream.health.constant.CSHealthKeysConstant +import clickstream.health.intermediate.CSHealthEventProcessor + + +/*** + * Actual health object used by [CSHealthEventProcessor]. + * + * */ public data class CSHealthEvent( + val healthEventID: Int = 0, val eventName: String, - val failureReason: String?, - val timeToConnection: Long?, - val eventGuids: List?, - val eventBatchGuids: List?, - val sessionId: String?, - val eventCount: Int? -) \ No newline at end of file + val eventType: String, + val timestamp: String = "", + val eventGuid: String = "", + val eventBatchGuid: String = "", + val error: String = "", + val sessionId: String = "", + val count: Int = 0, + val networkType: String = "", + val batchSize: Long = 0L, + val appVersion: String, +) { + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun eventData(): HashMap { + val eventData: HashMap = hashMapOf() + + if (eventType.isNotBlank()) { + eventData[CSHealthKeysConstant.EVENT_TYPE] = eventType + } + + if (sessionId.isNotBlank()) { + eventData[CSHealthKeysConstant.SESSION_ID] = sessionId + } + + if (error.isNotBlank()) { + eventData[CSHealthKeysConstant.REASON] = error + } + + if (eventGuid.isNotBlank()) { + eventData[CSHealthKeysConstant.EVENT_ID] = eventGuid + } + + if (eventBatchGuid.isNotBlank()) { + eventData[CSHealthKeysConstant.EVENT_BATCH_ID] = eventBatchGuid + } + if (timestamp.isNotBlank()) { + eventData[CSHealthKeysConstant.TIMESTAMP] = timestamp + } + + if (count != 0) { + eventData[CSHealthKeysConstant.COUNT] = count + } + + if (appVersion.isNotBlank()) { + eventData[CSHealthKeysConstant.APP_VERSION] = appVersion + } + + if (batchSize != 0L) { + eventData[CSHealthKeysConstant.EVENT_BATCHES] = batchSize + } + + return eventData + } +} diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventConfig.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventConfig.kt index d8a3dd02..3d12a867 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventConfig.kt +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventConfig.kt @@ -1,40 +1,81 @@ package clickstream.health.model -import clickstream.health.constant.CSTrackedVia +import androidx.annotation.RestrictTo +import androidx.annotation.VisibleForTesting +import clickstream.health.constant.CSHealthEventName +import java.util.Locale + +private const val DIVIDING_FACTOR: Int = 10 +internal const val MAX_VERBOSITY_LEVEL: String = "maximum" +public const val INTERNAL: String = "CS" +public const val EXTERNAL: String = "CT" +public const val ALPHA: String = "alpha" + +private val internalEventList = listOf( + CSHealthEventName.ClickStreamEventBatchAck.value, + CSHealthEventName.ClickStreamBatchSent.value, + CSHealthEventName.ClickStreamFlushOnBackground.value, + CSHealthEventName.ClickStreamFlushOnForeground.value +) + +private val externalEventList = listOf( + CSHealthEventName.ClickStreamEventBatchTimeout.value, + CSHealthEventName.ClickStreamConnectionFailed.value, + CSHealthEventName.ClickStreamEventBatchErrorResponse.value, + CSHealthEventName.ClickStreamBatchWriteFailed.value, + CSHealthEventName.ClickStreamEventBatchTriggerFailed.value +) /** * Config for HealthEventTracker, based on which the health events are handled * - * @param minimumTrackedVersion - The minimum app version above which the event will be sent. - * @param randomisingUserIdRemainders - A list with the last char of userID for whom the health events are tracked. + * @param minTrackedVersion - The minimum app version above which the event will be sent. + * @param randomUserIdRemainder - A list with the last char of userID for whom the health events are tracked. */ public data class CSHealthEventConfig( - val minimumTrackedVersion: String, - val randomisingUserIdRemainders: List, - val trackedVia: CSTrackedVia + val minTrackedVersion: String, + val randomUserIdRemainder: List, + val destination: List, + val verbosityLevel: String, ) { + /** * Checking whether the current app version is greater than the version in the config. */ - private fun isAppVersionGreater(appVersion: String): Boolean { - return appVersion.isNotBlank() && - minimumTrackedVersion.isNotBlank() && - convertVersionToNumber(appVersion) >= convertVersionToNumber(minimumTrackedVersion) + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun isAppVersionGreater(appVersion: String, minAppVersion: String): Boolean { + var appVersionIterator = 0 + var minAppVersionIterator = 0 + var isAppVersionGreater = false + + while (appVersionIterator < appVersion.length && minAppVersionIterator < minAppVersion.length) { + if (appVersion[appVersionIterator] > minAppVersion[minAppVersionIterator]) { + isAppVersionGreater = true + break + } else if (appVersion[appVersionIterator] < minAppVersion[minAppVersionIterator]) { + isAppVersionGreater = false + break + } else { + appVersionIterator++ + minAppVersionIterator++ + } + } + return isAppVersionGreater } /** * Checking whether the userID is present in the randomUserIdRemainder list */ - private fun isRandomUser(userId: Int): Boolean { - return randomisingUserIdRemainders.isNotEmpty() && randomisingUserIdRemainders.contains(userId % DIVIDING_FACTOR) + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun isHealthEnabledUser(userId: Int): Boolean { + return randomUserIdRemainder.isNotEmpty() && randomUserIdRemainder.contains(userId % DIVIDING_FACTOR) } /** * Checking whether the user is on alpha */ - private fun isAlpha(appVersion: String): Boolean { - return appVersion.contains(ALPHA, true) - } + private fun isAlpha(appVersion: String): Boolean = + appVersion.contains(ALPHA, true) /** * With the given app version and user ID, it is @@ -45,51 +86,41 @@ public data class CSHealthEventConfig( * * @return Boolean - True if the condition satisfies else false */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun isEnabled(appVersion: String, userId: Int): Boolean { - return isAppVersionGreater(appVersion) && (isRandomUser(userId) || isAlpha(appVersion)) - } - - public fun isTrackedForExternal(): Boolean { - return trackedVia == CSTrackedVia.External - } - - public fun isTrackedForInternal(): Boolean { - return trackedVia == CSTrackedVia.Internal + return isAppVersionGreater( + appVersion, minTrackedVersion + ) && (isHealthEnabledUser(userId) || isAlpha(appVersion)) } - public fun isTrackedForBoth(): Boolean { - return trackedVia == CSTrackedVia.Both - } + /** + * Returns whether logging level is set to Maximum or not + * + * @return Boolean - True if the condition satisfies else false + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun isVerboseLoggingEnabled(): Boolean = + verbosityLevel.lowercase(Locale.getDefault()) == MAX_VERBOSITY_LEVEL public companion object { - private const val DIVIDING_FACTOR: Int = 10 - private const val MULTIPLICATION_FACTOR: Int = 10 - private const val ALPHA: String = "alpha" - - public const val MAX_VERBOSITY_LEVEL: String = "maximum" - /** * Creates the default instance of the config */ - public fun default(trackedVia: CSTrackedVia): CSHealthEventConfig = CSHealthEventConfig( - minimumTrackedVersion = "", - randomisingUserIdRemainders = emptyList(), - trackedVia = trackedVia + public fun default(): CSHealthEventConfig = CSHealthEventConfig( + minTrackedVersion = "", + randomUserIdRemainder = emptyList(), + destination = emptyList(), + verbosityLevel = "", ) - /** - * Converts the app version to the integer format. - * For example,if the app version is "1.2.1.beta1", it's - * converted as "121" - */ - private fun convertVersionToNumber(version: String): Int { - var versionNum: Int = 0 - version.split(".").asIterable() - .filter { it.matches("-?\\d+(\\.\\d+)?".toRegex()) } - .map { - versionNum = (versionNum * MULTIPLICATION_FACTOR) + it.toInt() - } - return versionNum + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun isTrackedViaInternal(eventName: String): Boolean { + return internalEventList.contains(eventName) + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun isTrackedViaExternal(eventName: String): Boolean { + return externalEventList.contains(eventName) } } } diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventDTO.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventDTO.kt deleted file mode 100644 index 1017929f..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/model/CSHealthEventDTO.kt +++ /dev/null @@ -1,20 +0,0 @@ -package clickstream.health.model - -public data class CSHealthEventDTO( - val healthEventID: Int = 0, - val eventName: String, - val eventType: String, - val timestamp: String = "", - val eventGuid: String = "", - val eventBatchGuid: String = "", - val error: String = "", - val sessionId: String = "", - val count: Int = 0, - val networkType: String = "", - val startTime: Long = 0L, - val stopTime: Long = 0L, - val bucketType: String = "", - val batchSize: Long = 0L, - val appVersion: String, - val timeToConnection: Long = 0L -) diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSHealthTimeStampGenerator.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSHealthTimeStampGenerator.kt new file mode 100644 index 00000000..92505d6d --- /dev/null +++ b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSHealthTimeStampGenerator.kt @@ -0,0 +1,10 @@ +package clickstream.health.time + + +/*** + * To be implemented by client to provide timestamp. + * + * */ +public interface CSHealthTimeStampGenerator { + public fun getTimeStamp(): Long +} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSTimeStampGenerator.kt b/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSTimeStampGenerator.kt deleted file mode 100644 index 958e1821..00000000 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSTimeStampGenerator.kt +++ /dev/null @@ -1,16 +0,0 @@ -package clickstream.health.time - -public interface CSTimeStampGenerator { - public fun getTimeStamp(): Long -} - -public class DefaultCSTimeStampGenerator( - private val timestampListener: CSEventGeneratedTimestampListener -) : CSTimeStampGenerator { - - override fun getTimeStamp(): Long { - return runCatching { - timestampListener.now() - }.getOrDefault(System.currentTimeMillis()) - } -} diff --git a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/NoOpCSHealthGateway.kt b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/NoOpCSHealthGateway.kt index 7b9011ba..dad997ec 100644 --- a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/NoOpCSHealthGateway.kt +++ b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/NoOpCSHealthGateway.kt @@ -1,30 +1,18 @@ package clickstream.health -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.internal.NoOpCSEventHealthListener -import clickstream.health.internal.NoOpCSHealthEventFactory -import clickstream.health.internal.NoOpCSHealthEventProcessor -import clickstream.health.internal.NoOpCSHealthEventRepository public object NoOpCSHealthGateway { public fun factory(): CSHealthGateway { return object : CSHealthGateway { - override val eventHealthListener: CSEventHealthListener by lazy { - NoOpCSEventHealthListener() - } - override val healthEventRepository: CSHealthEventRepository by lazy { - NoOpCSHealthEventRepository() - } - override val healthEventProcessor: CSHealthEventProcessor by lazy { - NoOpCSHealthEventProcessor() - } - override val healthEventFactory: CSHealthEventFactory by lazy { - NoOpCSHealthEventFactory() + + override val healthEventProcessor: CSHealthEventProcessor? = null + + override suspend fun clearHealthEventsForVersionChange() { + /*NoOp*/ } + } } } diff --git a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSEventHealthListener.kt b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSEventHealthListener.kt deleted file mode 100644 index 636a08ea..00000000 --- a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSEventHealthListener.kt +++ /dev/null @@ -1,10 +0,0 @@ -package clickstream.health.internal - -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.model.CSEventHealth - -internal class NoOpCSEventHealthListener : CSEventHealthListener { - override fun onEventCreated(healthEvent: CSEventHealth) { - /*NoOp*/ - } -} diff --git a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventFactory.kt b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventFactory.kt deleted file mode 100644 index 1c1aab6e..00000000 --- a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventFactory.kt +++ /dev/null @@ -1,10 +0,0 @@ -package clickstream.health.internal - -import clickstream.health.intermediate.CSHealthEventFactory -import com.gojek.clickstream.internal.Health - -internal class NoOpCSHealthEventFactory : CSHealthEventFactory { - override suspend fun create(message: Health): Health { - throw IllegalAccessException("Not allowed") - } -} \ No newline at end of file diff --git a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventProcessor.kt b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventProcessor.kt deleted file mode 100644 index 9e4674c3..00000000 --- a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventProcessor.kt +++ /dev/null @@ -1,14 +0,0 @@ -package clickstream.health.internal - -import clickstream.health.intermediate.CSHealthEventProcessor -import com.gojek.clickstream.internal.Health - -internal class NoOpCSHealthEventProcessor : CSHealthEventProcessor { - override suspend fun getAggregateEvents(): List { - return emptyList() - } - - override suspend fun getInstantEvents(): List { - return emptyList() - } -} diff --git a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventRepository.kt b/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventRepository.kt deleted file mode 100644 index 87b9c327..00000000 --- a/clickstream-health-metrics-noop/src/main/kotlin/clickstream/health/internal/NoOpCSHealthEventRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -package clickstream.health.internal - -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO - -internal class NoOpCSHealthEventRepository : CSHealthEventRepository { - override suspend fun insertHealthEvent(healthEvent: CSHealthEventDTO) { - /*NoOp*/ - } - - override suspend fun insertHealthEventList(healthEventList: List) { - /*NoOp*/ - } - - override suspend fun getInstantEvents(): List { - return emptyList() - } - - override suspend fun getAggregateEvents(): List { - return emptyList() - } - - override suspend fun deleteHealthEventsBySessionId(sessionId: String) { - /*NoOp*/ - } - - override suspend fun deleteHealthEvents(events: List) { - /*NoOp*/ - } -} \ No newline at end of file diff --git a/clickstream-health-metrics/build.gradle.kts b/clickstream-health-metrics/build.gradle.kts index 7587dbcc..53ac9e9e 100644 --- a/clickstream-health-metrics/build.gradle.kts +++ b/clickstream-health-metrics/build.gradle.kts @@ -10,7 +10,7 @@ ext { set("PUBLISH_VERSION", ext.get("gitVersionName")) } -if(!project.hasProperty("isLocal")) { +if (!project.hasProperty("isLocal")) { apply(from = "$rootDir/scripts/publish-module.gradle") } @@ -33,6 +33,7 @@ android { } dependencies { + // Clickstream compileOnly(files("$rootDir/libs/proto-sdk-1.18.6.jar")) compileOnly(projects.clickstreamLogger) @@ -41,6 +42,9 @@ dependencies { api(projects.clickstreamLifecycle) api(projects.clickstreamUtil) + testImplementation(project(mapOf("path" to ":clickstream-logger"))) + testImplementation(files("$rootDir/libs/proto-sdk-1.18.6.jar")) + // Common deps.common.list.forEach(::implementation) diff --git a/clickstream-health-metrics/src/main/kotlin/DefaultCSHealthGateway.kt b/clickstream-health-metrics/src/main/kotlin/DefaultCSHealthGateway.kt new file mode 100644 index 00000000..487146e2 --- /dev/null +++ b/clickstream-health-metrics/src/main/kotlin/DefaultCSHealthGateway.kt @@ -0,0 +1,79 @@ +import android.content.Context +import clickstream.api.CSInfo +import clickstream.health.CSHealthGateway +import clickstream.health.intermediate.CSHealthEventLoggerListener +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.intermediate.CSMemoryStatusProvider +import clickstream.health.internal.DefaultCSGuIdGenerator +import clickstream.health.internal.database.CSHealthDatabase +import clickstream.health.internal.database.CSHealthEventDao +import clickstream.health.internal.factory.CSHealthEventFactory +import clickstream.health.internal.processor.CSHealthEventProcessorImpl +import clickstream.health.internal.repository.CSHealthEventRepository +import clickstream.health.internal.factory.DefaultCSHealthEventFactory +import clickstream.health.internal.repository.DefaultCSHealthEventRepository +import clickstream.health.model.CSHealthEventConfig +import clickstream.health.time.CSHealthTimeStampGenerator +import clickstream.logger.CSLogger +import clickstream.util.CSAppVersionSharedPref +import clickstream.util.impl.DefaultCSAppVersionSharedPref + +public class DefaultCSHealthGateway( + private val context: Context, + private val csMemoryStatusProvider: CSMemoryStatusProvider, + private val csHealthEventConfig: CSHealthEventConfig, + private val csInfo: CSInfo, + private val logger: CSLogger, + private val timeStampGenerator: CSHealthTimeStampGenerator, + private val csHealthEventLoggerListener: CSHealthEventLoggerListener, +) : CSHealthGateway { + + override val healthEventProcessor: CSHealthEventProcessor? by lazy { + if (isHealthEventEnabled()) { + CSHealthEventProcessorImpl( + healthEventRepository = healthRepository, + healthEventConfig = csHealthEventConfig, + info = csInfo, + logger = logger, + healthEventFactory = csHealthEventFactory, + memoryStatusProvider = csMemoryStatusProvider, + csHealthEventLogger = csHealthEventLoggerListener + ) + } else { + null + } + } + + override suspend fun clearHealthEventsForVersionChange() { + CSHealthEventProcessorImpl.clearHealthEventsForVersionChange( + csAppVersionSharedPref, + csInfo.appInfo.appVersion, + healthRepository, + logger + ) + } + + private fun isHealthEventEnabled(): Boolean { + return CSHealthEventProcessorImpl.isHealthEventEnabled( + csMemoryStatusProvider, + csHealthEventConfig, + csInfo + ) + } + + private val csAppVersionSharedPref: CSAppVersionSharedPref by lazy { + DefaultCSAppVersionSharedPref(context) + } + + private val healthRepository: CSHealthEventRepository by lazy { + DefaultCSHealthEventRepository(csHealthEventDao, csInfo) + } + + private val csHealthEventDao: CSHealthEventDao by lazy { + CSHealthDatabase.getInstance(context).healthEventDao() + } + + private val csHealthEventFactory: CSHealthEventFactory by lazy { + DefaultCSHealthEventFactory(DefaultCSGuIdGenerator(), timeStampGenerator, csInfo) + } +} \ No newline at end of file diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/DefaultCSHealthGateway.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/DefaultCSHealthGateway.kt deleted file mode 100644 index 6ef436fd..00000000 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/DefaultCSHealthGateway.kt +++ /dev/null @@ -1,77 +0,0 @@ -package clickstream.health - -import android.content.Context -import clickstream.api.CSInfo -import clickstream.api.CSMetaProvider -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.identity.DefaultCSGuIdGenerator -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory -import clickstream.health.intermediate.CSHealthEventLoggerListener -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.internal.CSHealthDatabase -import clickstream.health.internal.DefaultCSHealthEventFactory -import clickstream.health.internal.DefaultCSHealthEventProcessor -import clickstream.health.internal.DefaultCSHealthEventRepository -import clickstream.health.model.CSHealthEventConfig -import clickstream.health.time.CSTimeStampGenerator -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.logger.CSLogger -import clickstream.util.CSAppVersionSharedPref -import clickstream.util.impl.DefaultCSAppVersionSharedPref -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -public object DefaultCSHealthGateway { - - public fun factory( - appVersion: String, - sessionId: String, - context: Context, - healthEventConfig: CSHealthEventConfig, - csInfo: CSInfo, - logger: CSLogger, - healthEventLogger: CSHealthEventLoggerListener, - timeStampGenerator: CSTimeStampGenerator, - metaProvider: CSMetaProvider, - eventHealthListener: CSEventHealthListener, - appLifeCycle: CSAppLifeCycle, - guIdGenerator: CSGuIdGenerator = DefaultCSGuIdGenerator(), - dispatcher: CoroutineDispatcher = Dispatchers.Default, - appVersionPreference: CSAppVersionSharedPref = DefaultCSAppVersionSharedPref(context) - ): CSHealthGateway { - - return object : CSHealthGateway { - override val eventHealthListener: CSEventHealthListener = eventHealthListener - override val healthEventRepository: CSHealthEventRepository by lazy { - DefaultCSHealthEventRepository( - sessionId = sessionId, - healthEventDao = CSHealthDatabase.getInstance(context).healthEventDao(), - info = csInfo - ) - } - override val healthEventProcessor: CSHealthEventProcessor by lazy { - DefaultCSHealthEventProcessor( - appLifeCycleObserver = appLifeCycle, - healthEventRepository = healthEventRepository, - dispatcher = dispatcher, - healthEventConfig = healthEventConfig, - info = csInfo, - logger = logger, - healthEventLoggerListener = healthEventLogger, - healthEventFactory = healthEventFactory, - appVersion = appVersion, - appVersionPreference = appVersionPreference - ) - } - override val healthEventFactory: CSHealthEventFactory by lazy { - DefaultCSHealthEventFactory( - guIdGenerator = guIdGenerator, - timeStampGenerator = timeStampGenerator, - metaProvider = metaProvider - ) - } - } - } -} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSBucketTypes.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSBucketTypes.kt deleted file mode 100644 index c76851ee..00000000 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSBucketTypes.kt +++ /dev/null @@ -1,29 +0,0 @@ -package clickstream.health.internal - -internal object CSBucketTypes { - // Batch Latency - const val LT_1sec_2G = "2G_LT_1sec" - const val LT_1sec_3G = "3G_LT_1sec" - const val LT_1sec_4G = "4G_LT_1sec" - const val LT_1sec_WIFI = "WIFI_LT_1sec" - const val MT_1sec_2G = "2G_MT_1sec" - const val MT_1sec_3G = "3G_MT_1sec" - const val MT_1sec_4G = "4G_MT_1sec" - const val MT_1sec_WIFI = "WIFI_MT_1sec" - const val MT_3sec_2G = "2G_MT_3sec" - const val MT_3sec_3G = "3G_MT_3sec" - const val MT_3sec_4G = "4G_MT_3sec" - const val MT_3sec_WIFI = "WIFI_MT_3sec" - - // Batch Size - const val LT_10KB = "LT_10KB" - const val MT_10KB = "MT_10KB" - const val MT_20KB = "MT_20KB" - const val MT_50KB = "MT_50KB" - - // Event and Event Batch Wait Time - const val LT_5sec = "LT_5sec" - const val LT_10sec = "LT_10sec" - const val MT_10sec = "MT_10sec" - const val MT_20sec = "MT_20sec" -} \ No newline at end of file diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventFactory.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventFactory.kt deleted file mode 100644 index 27be101e..00000000 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventFactory.kt +++ /dev/null @@ -1,53 +0,0 @@ -package clickstream.health.internal - -import androidx.annotation.RestrictTo -import clickstream.api.CSMetaProvider -import clickstream.health.time.CSTimeStampGenerator -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventFactory -import com.gojek.clickstream.internal.Health -import com.gojek.clickstream.internal.HealthMeta - -/** - * This is the implementation of [CSHealthEventFactory] - * - * @param guIdGenerator used for generating random guid - * @param timeStampGenerator used for generating a time stamp - * @param metaProvider used for getting meta data - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class DefaultCSHealthEventFactory( - private val guIdGenerator: CSGuIdGenerator, - private val timeStampGenerator: CSTimeStampGenerator, - private val metaProvider: CSMetaProvider -) : CSHealthEventFactory { - - override suspend fun create(message: Health): Health { - val builder = message.toBuilder() - updateMeta(builder) - updateTimeStamp(builder) - return builder.build() - } - - @Throws(Exception::class) - private suspend fun updateMeta(message: Health.Builder) { - val guid = message.healthMeta.eventGuid - message.healthMeta = HealthMeta.newBuilder().apply { - eventGuid = if (guid.isNullOrBlank()) guIdGenerator.getId() else guid - location = metaProvider.location() - customer = metaProvider.customer - session = metaProvider.session - device = metaProvider.device - app = metaProvider.app - }.build() - } - - @Throws(Exception::class) - private fun updateTimeStamp(message: Health.Builder) { - // eventTimestamp uses NTP time - message.eventTimestamp = CSTimeStampMessageBuilder.build(timeStampGenerator.getTimeStamp()) - - // deviceTimestamp uses system clock - message.deviceTimestamp = CSTimeStampMessageBuilder.build(System.currentTimeMillis()) - } -} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessor.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessor.kt deleted file mode 100644 index a96d380f..00000000 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessor.kt +++ /dev/null @@ -1,313 +0,0 @@ -package clickstream.health.internal - -import androidx.annotation.RestrictTo -import clickstream.api.CSInfo -import clickstream.health.intermediate.CSHealthEventFactory -import clickstream.health.intermediate.CSHealthEventLoggerListener -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.internal.CSHealthEventEntity.Companion.dtosMapTo -import clickstream.health.model.CSHealthEventConfig -import clickstream.health.model.CSHealthEventDTO -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSLifeCycleManager -import clickstream.logger.CSLogger -import clickstream.util.CSAppVersionSharedPref -import com.gojek.clickstream.internal.ErrorDetails -import com.gojek.clickstream.internal.Health -import com.gojek.clickstream.internal.HealthDetails -import com.gojek.clickstream.internal.TraceDetails -import com.google.protobuf.Timestamp -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.collections.Map.Entry - -private const val MAX_BATCH_THRESHOLD = 13 - -/** - * [CSHealthEventProcessor] is the Heart of the Clickstream Library. The [CSHealthEventProcessor] - * is only for pushing events to the backend. [CSHealthEventProcessor] is respect to the - * Application lifecycle where on the active state, we have a ticker that will collect events from database - * and the send that to the backend. The ticker will run on every 10seconds and will be stopped - * whenever application on the inactive state. - * - * On the inactive state we will running flush for both Events and HealthEvents, where - * it would be transformed and send to the backend. - * - * **Sequence Diagram** - * ``` - * App Clickstream - * +---+---+---+---+---+---+ +---+---+---+---+---+---+ - * | Sending Events | --------> | Received the Events | - * +---+---+---+---+---+---+ +---+---+---+---+---+---+ - * | - * | - * | +---+---+---+---+---+---+---+---+----+ - * if app on active state ---------> | - run the ticker with 10s delay | | - * | | - collect events from db | - * | | - transform and send to backend | - * | +---+---+---+---+---+---+---+---+----+ - * | - * | +---+---+---+---+---+---+---+---+---+---+----+ - * else if app on inactive state --> | - run flushEvents and flushHealthMetrics | - * | - transform and send to backend | - * +---+---+---+---+---+---+---+---+---+----+---+ - *``` - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class DefaultCSHealthEventProcessor( - appLifeCycleObserver: CSAppLifeCycle, - private val healthEventRepository: CSHealthEventRepository, - private val dispatcher: CoroutineDispatcher, - private val healthEventConfig: CSHealthEventConfig, - private val info: CSInfo, - private val logger: CSLogger, - private val healthEventLoggerListener: CSHealthEventLoggerListener, - private val healthEventFactory: CSHealthEventFactory, - private val appVersion: String, - private val appVersionPreference: CSAppVersionSharedPref -) : CSLifeCycleManager(appLifeCycleObserver), CSHealthEventProcessor { - - private var scope: CoroutineScope? = CoroutineScope(SupervisorJob() + dispatcher) - private val scopeForAppUpgrading = CoroutineScope(SupervisorJob() + dispatcher) - - init { - logger.debug { "DefaultCSHealthEventProcessor#init" } - addObserver() - flushOnAppUpgrade() - } - - override fun onStart() { - logger.debug { "DefaultCSHealthEventProcessor#onStart" } - - scope?.cancel() - scope = null - } - - override fun onStop() { - logger.debug { "DefaultCSHealthEventProcessor#onStop" } - - scope = CoroutineScope(SupervisorJob() + dispatcher) - trySendEventsToAnalyticsUpstream() - } - - override suspend fun getInstantEvents(): List { - logger.debug { "DefaultCSHealthEventProcessor#getInstantEvents" } - - if (isTrackedViaBothOrInternal().not()) { - logger.debug { "DefaultCSHealthEventProcessor#getInstantEvents : Operation is not allowed" } - return emptyList() - } - - val instantEvents = healthEventRepository.getInstantEvents() - val transformedEvents = instantEvents.map { event -> - val health = Health.newBuilder() - .setEventName(event.eventName) - .setNumberOfEvents(1) // Since instant events are fired one at a time - //.setNumberOfBatches() no need to set setNumberOfBatches for instant event - //.setHealthMeta() will be override through healthEventFactory.create below - .setHealthDetails( - HealthDetails.newBuilder() - .addEventGuids(event.eventGuid) - .addEventBatchGuids(event.eventBatchGuid) - .build() - ) - .setTraceDetails( - TraceDetails.newBuilder() - .setTimeToConnection(event.timeToConnection.toString()) - .setErrorDetails( - ErrorDetails.newBuilder() - .setReason(event.error) - .build() - ) - .build() - ) - .build() - logger.debug { "DefaultCSHealthEventProcessor#getInstantEvents : Health Events $health" } - healthEventFactory.create(health) - } - - // Deleted collected aggregate events from health db, - // since we're going to insert transformed events to event db anyway - // so deleting health events here is expected - healthEventRepository.deleteHealthEvents(instantEvents) - return transformedEvents - } - - override suspend fun getAggregateEvents(): List { - logger.debug { "DefaultCSHealthEventProcessor#getAggregateEvents" } - - if (isTrackedViaBothOrInternal().not()) { - logger.debug { "DefaultCSHealthEventProcessor#getInstantEvents : Operation is not allowed" } - return emptyList() - } - - val aggregateEvents = healthEventRepository.getAggregateEvents() - val transformedEvents = aggregateEvents.groupBy { it.eventName } - .map { entry: Entry> -> - val events = entry.value.dtosMapTo() - val eventGuids = mutableListOf() - events.forEach { event -> - val eventIdArray = event.eventId.split(",").map { it.trim() } - eventGuids += eventIdArray - } - val eventBatchGuids = - events.filter { it.eventBatchId.isNotBlank() }.map { it.eventBatchId } - val health = Health.newBuilder() - .setEventName(entry.key) - .setNumberOfEvents(eventGuids.size.toLong()) - .setNumberOfBatches(eventBatchGuids.size.toLong()) - //.setHealthMeta() will be override through healthEventFactory.create below - .setEventTimestamp(Timestamp.getDefaultInstance()) - .setDeviceTimestamp(Timestamp.getDefaultInstance()) - .setHealthDetails( - HealthDetails.newBuilder() - .addAllEventGuids(eventGuids) - .addAllEventBatchGuids(eventBatchGuids) - .build() - ) - // This not necessary at the moment - // As in RFC see: https://github.com/gojek/clickstream-android/discussions/18 - // state that, the error along with timeToConnected only for instant event. - //.setTraceDetails() - .build() - - logger.debug { "DefaultCSHealthEventProcessor#getAggregateEvents : Health Events $health" } - healthEventFactory.create(health) - } - - // Deleted collected aggregate events from health db, - // since we're going to insert transformed events to event db anyway - // so deleting health events here is expected - healthEventRepository.deleteHealthEvents(aggregateEvents) - return transformedEvents - } - - private fun trySendEventsToAnalyticsUpstream() { - logger.debug { "CSHealthEventProcessor#sendEvents" } - - scope!!.launch { - logger.debug { "CSHealthEventProcessor#sendEvents : isCoroutineActive $isActive" } - - if (isActive.not()) { - logger.debug { "CSHealthEventProcessor#sendEvents : coroutineScope is not longer active" } - return@launch - } - if (isHealthEventEnabled().not()) { - logger.debug { "CSHealthEventProcessor#sendEvents : Health Event condition is not satisfied for this user" } - return@launch - } - if (healthEventConfig.isTrackedForBoth()) { - logger.debug { "CSHealthEventProcessor#sendEvents : sendEventsToAnalytics" } - trySendUpstream() - } - } - } - - // TrySendUpstream will be send events to the upstream via listener. - private suspend fun trySendUpstream() { - trySendUpstreamInstantEvents() - trySendUpstreamAggregateEvents() - } - - private suspend fun trySendUpstreamInstantEvents() { - val instantEvents: List = healthEventRepository.getInstantEvents() - trySendToUpstream(instantEvents.dtosMapTo()) - - // Delete Events only if TrackedVia is for External or Internal - if (healthEventConfig.isTrackedForInternal() || healthEventConfig.isTrackedForExternal()) { - healthEventRepository.deleteHealthEvents(instantEvents) - } - } - - private suspend fun trySendUpstreamAggregateEvents() { - val aggregateEvents: List = healthEventRepository.getAggregateEvents() - aggregateEvents - .groupBy { it.eventName } - .forEach { entry: Entry> -> - val errorEvents: Map> = - entry.value.filter { it.error.isNotBlank() }.groupBy { it.error } - if (errorEvents.isEmpty()) { - sendAggregateEventsBasedOnEventName(entry.value.dtosMapTo()) - } else { - sendAggregateEventsBasedOnError(errorEvents) - } - } - - // Delete Events only if TrackedVia is for External or Internal - if (healthEventConfig.isTrackedForInternal() || healthEventConfig.isTrackedForExternal()) { - healthEventRepository.deleteHealthEvents(aggregateEvents) - } - } - - private fun sendAggregateEventsBasedOnEventName(events: List) { - fun eventIdOrEventBatchIdNotBlank(events: List): Boolean { - return events.joinToString("") { it.eventId }.isNotBlank() || - events.joinToString("") { it.eventBatchId }.isNotBlank() - } - - val batchSize = - if (eventIdOrEventBatchIdNotBlank(events)) MAX_BATCH_THRESHOLD else events.size - - events.chunked(batchSize) - .forEach { batch -> - val healthEvent = events[0].copy( - eventId = batch.filter { it.eventId.isNotBlank() }.joinToString { it.eventId }, - eventBatchId = batch.filter { it.eventBatchId.isNotBlank() } - .joinToString { it.eventBatchId }, - timestamp = batch.filter { it.timestamp.isNotBlank() } - .joinToString { it.timestamp }, - count = batch.size - ) - trySendToUpstream(listOf(healthEvent)) - } - } - - private fun sendAggregateEventsBasedOnError(events: Map>) { - events.forEach { entry: Entry> -> - val batch: List = entry.value.dtosMapTo() - val healthEvent = batch[0].copy( - eventId = batch.filter { it.eventId.isNotBlank() }.joinToString { it.eventId }, - eventBatchId = batch.filter { it.eventBatchId.isNotBlank() } - .joinToString { it.eventBatchId }, - timestamp = batch.filter { it.timestamp.isNotBlank() } - .joinToString { it.timestamp }, - count = batch.size - ) - trySendToUpstream(listOf(healthEvent)) - } - } - - private fun isHealthEventEnabled(): Boolean { - return healthEventConfig.isEnabled(info.appInfo.appVersion, info.userInfo.identity) - } - - private fun trySendToUpstream(events: List) { - events.forEach { - if (healthEventConfig.isTrackedForBoth()) { - healthEventLoggerListener.logEvent( - eventName = it.eventName, - healthEvent = it.mapToHealthEventDTO() - ) - } - } - } - - // Flushes health events, being tracked via Clickstream, in case of an app upgrade. - private fun flushOnAppUpgrade() { - scopeForAppUpgrading.launch { - val isAppVersionEqual = appVersionPreference.isAppVersionEqual(appVersion) - if (isAppVersionEqual.not() && isActive) { - healthEventRepository.deleteHealthEvents(healthEventRepository.getAggregateEvents()) - } - } - } - - private fun isTrackedViaBothOrInternal(): Boolean { - return healthEventConfig.isTrackedForInternal() || healthEventConfig.isTrackedForBoth() - } -} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventRepository.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventRepository.kt deleted file mode 100644 index bf962067..00000000 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/DefaultCSHealthEventRepository.kt +++ /dev/null @@ -1,71 +0,0 @@ -package clickstream.health.internal - -import androidx.annotation.RestrictTo -import clickstream.api.CSInfo -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.internal.CSHealthEventEntity.Companion.dtoMapTo -import clickstream.health.internal.CSHealthEventEntity.Companion.dtosMapTo -import clickstream.health.internal.CSHealthEventEntity.Companion.mapToDtos -import clickstream.health.model.CSHealthEventDTO - -/** - * [CSHealthEventRepository] Act as repository pattern where internally it doing DAO operation - * to insert, delete, and read the [CSHealthEventEntity]'s. - * - * If you're using `com.gojek.clickstream:clickstream-health-metrics-noop`, the - * [CSHealthEventRepository] internally will doing nothing. - * - * Do consider to use `com.gojek.clickstream:clickstream-health-metrics`, to operate - * [CSHealthEventRepository] as expected. Whenever you opt in the `com.gojek.clickstream:clickstream-health-metrics`, - * you should never touch the [DefaultCSHealthEventRepository] explicitly. All the wiring - * is happening through [DefaultCSHealthGateway.factory(/*args*/)] - * - * @param sessionId in form of UUID - * @param healthEventDao the [CSHealthEventDao] - * @param info the [CSInfo] - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -internal class DefaultCSHealthEventRepository( - private val sessionId: String, - private val healthEventDao: CSHealthEventDao, - private val info: CSInfo -) : CSHealthEventRepository { - - override suspend fun insertHealthEvent(healthEvent: CSHealthEventDTO) { - val event: CSHealthEventEntity = healthEvent.dtoMapTo() - .copy( - sessionId = sessionId, - timestamp = System.currentTimeMillis().toString(), - appVersion = info.appInfo.appVersion - ) - healthEventDao.insert(healthEvent = event) - } - - override suspend fun insertHealthEventList(healthEventList: List) { - val eventList: List = healthEventList.map { eventDto -> - eventDto.dtoMapTo() - .copy( - sessionId = sessionId, - timestamp = System.currentTimeMillis().toString(), - appVersion = info.appInfo.appVersion - ) - }.toList() - healthEventDao.insertAll(healthEventList = eventList) - } - - override suspend fun getInstantEvents(): List { - return healthEventDao.getEventByType(INSTANT_EVENT_TYPE).mapToDtos() - } - - override suspend fun getAggregateEvents(): List { - return healthEventDao.getEventByType(AGGREGATE_EVENT_TYPE).mapToDtos() - } - - override suspend fun deleteHealthEventsBySessionId(sessionId: String) { - healthEventDao.deleteBySessionId(sessionId = sessionId) - } - - override suspend fun deleteHealthEvents(events: List) { - healthEventDao.deleteHealthEvent(events.dtosMapTo()) - } -} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthDatabase.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthDatabase.kt similarity index 92% rename from clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthDatabase.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthDatabase.kt index 8c8d839d..4a7f994a 100644 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthDatabase.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthDatabase.kt @@ -1,4 +1,4 @@ -package clickstream.health.internal +package clickstream.health.internal.database import android.content.Context import androidx.annotation.GuardedBy @@ -11,7 +11,7 @@ import androidx.room.RoomDatabase * * The Events are cached, processed and then cleared. */ -@Database(entities = [CSHealthEventEntity::class], version = 9) +@Database(entities = [CSHealthEventEntity::class], version = 10) internal abstract class CSHealthDatabase : RoomDatabase() { /** diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventDao.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventDao.kt similarity index 72% rename from clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventDao.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventDao.kt index 9818da18..4057ba3a 100644 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventDao.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventDao.kt @@ -1,4 +1,4 @@ -package clickstream.health.internal +package clickstream.health.internal.database import androidx.room.Dao import androidx.room.Delete @@ -6,9 +6,6 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -internal const val INSTANT_EVENT_TYPE = "instant" -internal const val AGGREGATE_EVENT_TYPE = "aggregate" - /** * A collection of function to accommodate and Communicates with the implementation of DAO. * Thread switching should be handled by the caller side in they implementation scope. @@ -45,8 +42,8 @@ internal interface CSHealthEventDao { * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] * */ - @Query("SELECT * FROM HealthStats WHERE eventType = :eventType") - suspend fun getEventByType(eventType: String): List + @Query("SELECT * FROM HealthStats WHERE eventType = :eventType ORDER BY timestamp DESC limit:size") + suspend fun getEventByType(eventType: String, size: Int): List /** * A function [deleteBySessionId] that accommodate an action to delete of [CSHealthEventEntity] objects @@ -68,4 +65,22 @@ internal interface CSHealthEventDao { */ @Delete suspend fun deleteHealthEvent(events: List) + + /** + * function [deleteHealthEvent] that accommodate an action to delete of [CSHealthEventEntity] objects. + * + * **Note:** + * Thread switching must be handled by the caller side + */ + @Query("DELETE FROM HealthStats WHERE eventType = :type") + suspend fun deleteHealthEventByType(type: String) + + /** + * function [deleteHealthEvent] that accommodate an action to delete of [CSHealthEventEntity] objects. + * + * **Note:** + * Thread switching must be handled by the caller side + */ + @Query("SELECT COUNT (*) FROM HealthStats WHERE eventType = :type") + suspend fun getHealthEventCountByType(type: String): Int } diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventEntity.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventEntity.kt similarity index 76% rename from clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventEntity.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventEntity.kt index 44e5c32b..354b09ed 100644 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSHealthEventEntity.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/database/CSHealthEventEntity.kt @@ -1,11 +1,10 @@ -package clickstream.health.internal +package clickstream.health.internal.database import androidx.annotation.RestrictTo import androidx.room.Entity import androidx.room.PrimaryKey import clickstream.health.constant.CSEventTypesConstant import clickstream.health.model.CSHealthEvent -import clickstream.health.model.CSHealthEventDTO /** * The data class for health events which will be stored to the DB @@ -51,7 +50,7 @@ public data class CSHealthEventEntity( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public companion object { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public fun CSHealthEventDTO.dtoMapTo(): CSHealthEventEntity { + public fun CSHealthEvent.dtoMapTo(): CSHealthEventEntity { return CSHealthEventEntity( healthEventID = healthEventID, eventName = eventName, @@ -63,18 +62,14 @@ public data class CSHealthEventEntity( sessionId = sessionId, count = count, networkType = networkType, - startTime = startTime, - stopTime = stopTime, - bucketType = bucketType, batchSize = batchSize, appVersion = appVersion, - timeToConnection = timeToConnection ) } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public fun CSHealthEventEntity.mapToDto(): CSHealthEventDTO { - return CSHealthEventDTO( + public fun CSHealthEventEntity.mapToDto(): CSHealthEvent { + return CSHealthEvent( healthEventID = healthEventID, eventName = eventName, eventType = eventType, @@ -85,35 +80,19 @@ public data class CSHealthEventEntity( sessionId = sessionId, count = count, networkType = networkType, - startTime = startTime, - stopTime = stopTime, - bucketType = bucketType, batchSize = batchSize, appVersion = appVersion, - timeToConnection = timeToConnection ) } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public fun List.mapToDtos(): List { + public fun List.mapToDtos(): List { return this.map { healthEvent -> healthEvent.mapToDto() } } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public fun List.dtosMapTo(): List { + public fun List.dtosMapTo(): List { return this.map { healthEvent -> healthEvent.dtoMapTo() } } } - - public fun mapToHealthEventDTO(): CSHealthEvent { - return CSHealthEvent( - eventName = eventName, - sessionId = sessionId, - eventGuids = if (eventId.isNotBlank()) eventId.split(",") else null, - eventBatchGuids = if (eventBatchId.isNotBlank()) eventBatchId.split(",") else null, - failureReason = error, - timeToConnection = timeToConnection, - eventCount = count - ) - } } diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventFactory.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/CSHealthEventFactory.kt similarity index 93% rename from clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventFactory.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/CSHealthEventFactory.kt index 3199b444..89e28695 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/intermediate/CSHealthEventFactory.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/CSHealthEventFactory.kt @@ -1,4 +1,4 @@ -package clickstream.health.intermediate +package clickstream.health.internal.factory import androidx.annotation.RestrictTo import com.gojek.clickstream.internal.Health diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/DefaultCSHealthEventFactory.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/DefaultCSHealthEventFactory.kt new file mode 100644 index 00000000..00a61360 --- /dev/null +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/factory/DefaultCSHealthEventFactory.kt @@ -0,0 +1,81 @@ +package clickstream.health.internal.factory + +import androidx.annotation.RestrictTo +import clickstream.api.CSInfo +import clickstream.health.internal.CSGuIdGenerator +import clickstream.health.internal.time.CSTimeStampMessageBuilder +import clickstream.health.time.CSHealthTimeStampGenerator +import com.gojek.clickstream.internal.Health +import com.gojek.clickstream.internal.HealthMeta + +/** + * This is the implementation of [CSHealthEventFactory] + * + * @param guIdGenerator used for generating random guid + * @param timeStampGenerator used for generating a time stamp + * @param csInfo used for meta info. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class DefaultCSHealthEventFactory( + private val guIdGenerator: CSGuIdGenerator, + private val timeStampGenerator: CSHealthTimeStampGenerator, + private val csInfo: CSInfo +) : CSHealthEventFactory { + + override suspend fun create(message: Health): Health { + val builder = message.toBuilder() + updateMeta(builder) + updateTimeStamp(builder) + return builder.build() + } + + @Throws(Exception::class) + private fun updateMeta(message: Health.Builder) { + val guid = message.healthMeta.eventGuid + message.healthMeta = HealthMeta.newBuilder().apply { + eventGuid = if (guid.isNullOrBlank()) guIdGenerator.getId() else guid + location = location() + customer = customer() + session = session() + device = device() + app = app() + }.build() + } + + private fun location() = HealthMeta.Location.newBuilder().apply { + latitude = csInfo.locationInfo.latitude + longitude = csInfo.locationInfo.longitude + }.build() + + private fun customer() = HealthMeta.Customer.newBuilder().apply { + signedUpCountry = csInfo.userInfo.signedUpCountry + currentCountry = csInfo.userInfo.currentCountry + email = csInfo.userInfo.email + identity = csInfo.userInfo.identity + }.build() + + private fun session() = HealthMeta.Session.newBuilder().apply { + sessionId = csInfo.sessionInfo.sessionID + }.build() + + private fun device() = HealthMeta.Device.newBuilder().apply { + operatingSystem = csInfo.deviceInfo.getOperatingSystem() + operatingSystemVersion = csInfo.deviceInfo.getOperatingSystem() + deviceMake = csInfo.deviceInfo.getDeviceManufacturer() + deviceModel = csInfo.deviceInfo.getDeviceModel() + + }.build() + + private fun app() = HealthMeta.App.newBuilder().apply { + version = csInfo.appInfo.appVersion + }.build() + + @Throws(Exception::class) + private fun updateTimeStamp(message: Health.Builder) { + // eventTimestamp uses NTP time + message.eventTimestamp = CSTimeStampMessageBuilder.build(timeStampGenerator.getTimeStamp()) + + // deviceTimestamp uses system clock + message.deviceTimestamp = CSTimeStampMessageBuilder.build(System.currentTimeMillis()) + } +} diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/identity/CSGuIdGenerator.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/guid/CSGuIdGenerator.kt similarity index 86% rename from clickstream-health-metrics-api/src/main/kotlin/clickstream/health/identity/CSGuIdGenerator.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/guid/CSGuIdGenerator.kt index fadd1d19..5f0d50a6 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/identity/CSGuIdGenerator.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/guid/CSGuIdGenerator.kt @@ -1,4 +1,4 @@ -package clickstream.health.identity +package clickstream.health.internal import java.util.UUID diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/processor/CSHealthEventProcessorImpl.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/processor/CSHealthEventProcessorImpl.kt new file mode 100644 index 00000000..d014597a --- /dev/null +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/processor/CSHealthEventProcessorImpl.kt @@ -0,0 +1,305 @@ +package clickstream.health.internal.processor + +import clickstream.api.CSInfo +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.intermediate.CSHealthEventLoggerListener +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.intermediate.CSMemoryStatusProvider +import clickstream.health.internal.factory.CSHealthEventFactory +import clickstream.health.internal.repository.CSHealthEventRepository +import clickstream.health.model.CSEventForHealth +import clickstream.health.model.CSHealthEvent +import clickstream.health.model.CSHealthEventConfig +import clickstream.health.model.EXTERNAL +import clickstream.health.model.INTERNAL +import clickstream.logger.CSLogger +import clickstream.util.CSAppVersionSharedPref +import com.gojek.clickstream.internal.Health +import com.gojek.clickstream.internal.HealthDetails +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +private const val MAX_CHUNK_THRESHOLD = 13 + +/** + * The HealthEventProcessor is responsible for aggregating, sending and clearing health events for the sdk. + * + */ +internal open class CSHealthEventProcessorImpl( + private val healthEventRepository: CSHealthEventRepository, + private val healthEventConfig: CSHealthEventConfig, + private val info: CSInfo, + private val logger: CSLogger, + private val healthEventFactory: CSHealthEventFactory, + private val memoryStatusProvider: CSMemoryStatusProvider, + private val csHealthEventLogger: CSHealthEventLoggerListener, +) : CSHealthEventProcessor { + + private val tag: String + get() = "CSHealthEventProcessor" + + override suspend fun insertNonBatchEvent(csEvent: CSHealthEvent): Boolean { + return doSuspendedIfHealthEnabled { + healthEventRepository.insertHealthEvent(csEvent) + } + } + + override suspend fun insertBatchEvent( + csEvent: CSHealthEvent, list: List + ): Boolean { + return doSuspendedIfHealthEnabled { + + // Set event guids only if verbosity is enabled + var eventGuids = "" + doIfVerbosityEnabled { + eventGuids = list.joinToString(",") { it.eventGuid } + } + + val eventCount = list.size.toLong() + + val batchId = if (list.isNotEmpty()) list[0].batchGuid ?: "" else "" + + healthEventRepository.insertHealthEvent( + csEvent.copy( + eventGuid = eventGuids, batchSize = eventCount, eventBatchGuid = batchId + ) + ) + } + } + + override suspend fun insertBatchEvent(csEvent: CSHealthEvent, eventCount: Long): Boolean { + return doSuspendedIfHealthEnabled { + healthEventRepository.insertHealthEvent( + csEvent.copy( + batchSize = eventCount + ) + ) + } + } + + /** + * Events are controlled via [CSHealthEventConfig.destination]. If destination contains [INTERNAL] + * then events are pushed to flow. + * + * */ + override fun getHealthEventFlow(type: String, deleteEvents: Boolean): Flow> = + if (healthEventConfig.destination.contains(INTERNAL)) { + getHealthEventFlowInternal(type, deleteEvents).map { + getProtoListForInternalTracking(it) + } + } else emptyFlow() + + + /** + * If it contains [EXTERNAL] they are pushed to upstream. + * */ + override suspend fun pushEventToUpstream(type: String, deleteEvents: Boolean) { + doSuspendedIfHealthEnabled { + if (healthEventConfig.destination.contains(EXTERNAL)) { + getHealthEventFlowInternal(type, deleteEvents).collect { + pushEventToUpstream(it) + } + } + } + } + + private fun getHealthEventFlowInternal(type: String, deleteEvents: Boolean) = flow { + logger.debug { "$tag#getAggregateEventsBasedOnEventName" } + var totalEventCount = healthEventRepository.getEventCount(type) + if (totalEventCount != 0 && !doSuspendedIfHealthEnabled { + while (totalEventCount <= healthEventRepository.getEventCount(type)) { + val batch = healthEventRepository.getEventsByTypeAndLimit(type, 30) + emit(batch) + totalEventCount += batch.size + if (deleteEvents) { + healthEventRepository.deleteHealthEvents(batch) + } + } + }) { + emit(emptyList()) + } + }.flowOn(Dispatchers.Default) + + + private fun pushEventToUpstream(list: List) { + list.filter { CSHealthEventConfig.isTrackedViaExternal(it.eventName) } + .groupBy { it.eventName }.forEach { entry -> + val errorEvents = entry.value.filter { it.error.isNotBlank() }.groupBy { it.error } + if (errorEvents.isNotEmpty()) { + pushEventsBasedOnError(errorEvents) + } else { + pushEventsBasedOnEventName(entry.value) + } + } + } + + private fun pushEventToUpstream(eventName: String, mapData: HashMap) { + csHealthEventLogger.logEvent(eventName, mapData) + } + + private fun pushEventsBasedOnError(events: Map>) { + logger.debug { "$tag#sendAggregateEventsBasedOnError" } + events.forEach { entry -> + chunkEventsAndPushToUpstream(entry.value) + } + } + + private fun pushEventsBasedOnEventName(events: List) { + logger.debug { "$tag#sendAggregateEventsBasedOnEventName" } + chunkEventsAndPushToUpstream(events) + } + + private fun chunkEventsAndPushToUpstream(events: List) { + var eventId = "" + var batchId = "" + + if (events.isEmpty()) { + return + } + + val batchSize = if (events.size > MAX_CHUNK_THRESHOLD) MAX_CHUNK_THRESHOLD else events.size + + events.chunked(batchSize).forEach { list -> + if (list.isNotEmpty()) { + + doIfVerbosityEnabled { + eventId = + events.filter { it.eventGuid.isNotBlank() }.joinToString { it.eventGuid } + batchId = events.filter { it.eventBatchGuid.isNotBlank() } + .joinToString { it.eventBatchGuid } + } + + val data = events[0].copy( + eventGuid = eventId, eventBatchGuid = batchId, count = list.size + ).eventData() + + pushEventToUpstream(events[0].eventName, data) + } + } + } + + private suspend fun clearHealthEvents(list: List) { + doSuspendedIfHealthEnabled { + healthEventRepository.deleteHealthEvents(list) + } + } + + private suspend fun getProtoListForInternalTracking(batch: List): List { + val healthEvents = mutableListOf() + + val batchGroupOnEventName = + batch.filter { CSHealthEventConfig.isTrackedViaInternal(it.eventName) } + .groupBy { it.eventName } + + batchGroupOnEventName.forEach { entry -> + val health = createHealthProto(entry.key, entry.value) + healthEvents += healthEventFactory.create(health) + logger.debug { "$tag#getAggregateEventsBasedOnEventName - Health events: $health" } + } + return healthEvents + } + + private fun createHealthProto(eventName: String, events: List): Health { + logger.debug { "$tag#createHealthProto" } + + // Calculating total events + var eventGuidCount = 0L + events.forEach { eventGuidCount += it.batchSize } + + // Calculating batch size + val batchSize = events.filter { it.eventBatchGuid.isNotEmpty() }.size.toLong() + + logger.debug { "$tag#createHealthProto - eventGuids $eventGuidCount" } + logger.debug { "$tag#createHealthProto - eventBatchGuids $batchSize" } + + return Health.newBuilder().apply { + this.eventName = eventName + numberOfEvents = eventGuidCount + numberOfBatches = batchSize + + // Only fill health details if verbosity is maximum + doIfVerbosityEnabled { + + val eventGuids = mutableListOf() + + // List of event guids + events.forEach { event -> + val eventIdArray = event.eventGuid.split(",").map { it.trim() } + if (eventIdArray.isNotEmpty()) { + eventGuids += eventIdArray + } + } + + // List of batch ids + val eventBatchGuids = + events.filter { it.eventBatchGuid.isNotBlank() }.map { it.eventBatchGuid } + + healthDetails = HealthDetails.newBuilder().apply { + addAllEventGuids(eventGuids) + addAllEventBatchGuids(eventBatchGuids) + }.build() + logger.debug { "$tag#createHealthProto - HealthDetails $healthDetails" } + } + }.build() + } + + private fun isHealthEventEnabled(): Boolean { + return isHealthEventEnabled(memoryStatusProvider, healthEventConfig, info) + } + + private inline fun doIfVerbosityEnabled(crossinline executable: () -> T) { + logger.debug { "$tag#createHealthProto# - isVerboseLoggingEnabled ${healthEventConfig.isVerboseLoggingEnabled()}" } + if (healthEventConfig.isVerboseLoggingEnabled()) { + executable() + } + } + + private suspend inline fun doSuspendedIfHealthEnabled(crossinline executable: suspend () -> Unit): Boolean { + return if (isHealthEventEnabled()) { + executable() + true + } else { + false + } + } + + private inline fun doIfHealthEnabled(crossinline executable: () -> Unit): Boolean { + return if (isHealthEventEnabled()) { + executable() + true + } else { + false + } + } + + companion object { + + internal fun isHealthEventEnabled( + memoryStatusProvider: CSMemoryStatusProvider, + healthEventConfig: CSHealthEventConfig, + info: CSInfo, + ): Boolean { + return !memoryStatusProvider.isLowMemory() && healthEventConfig.isEnabled( + info.appInfo.appVersion, info.userInfo.identity + ) + } + + suspend fun clearHealthEventsForVersionChange( + appSharedPref: CSAppVersionSharedPref, + currentAppVersion: String, + healthEventRepository: CSHealthEventRepository, + logger: CSLogger + ) { + if (!appSharedPref.isAppVersionEqual(currentAppVersion)) { + healthEventRepository.deleteHealthEventsByType(CSEventTypesConstant.AGGREGATE) + logger.debug { "CSHealthEventProcessorImpl#Deleted events on version change" } + } + } + } +} \ No newline at end of file diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/CSHealthEventRepository.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/CSHealthEventRepository.kt new file mode 100644 index 00000000..5d9d2555 --- /dev/null +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/CSHealthEventRepository.kt @@ -0,0 +1,58 @@ +package clickstream.health.internal.repository + +import clickstream.health.model.CSHealthEvent + +/** + * The HealthRepository acts as a wrapper above storage/cache. + * It read, writes, deletes the data in the storage + */ +internal interface CSHealthEventRepository { + + /** + * A function to insert the health event into the DB + */ + suspend fun insertHealthEvent(healthEvent: CSHealthEvent) + + /** + * A function to insert the health event list into the DB + */ + suspend fun insertHealthEventList(healthEventList: List) + + /** + * A function to retrieve all the instant health events in the DB + */ + suspend fun getInstantEvents(size: Int): List + + /** + * A function to retrieve all the aggregate health events in the DB + */ + suspend fun getAggregateEvents(size: Int): List + + /** + * A function to delete all the health events for a sessionID + */ + suspend fun deleteHealthEventsBySessionId(sessionId: String) + + /** + * A function to delete the given health events + */ + suspend fun deleteHealthEvents(events: List) + + /** + * Delete health events by name and type + * + * */ + suspend fun deleteHealthEventsByType(type: String) + + /** + * Returns health events count for a type + * + * */ + suspend fun getEventCount(type: String): Int + + /** + * Returns health events for a type + * + * */ + suspend fun getEventsByTypeAndLimit(type: String, limit: Int): List +} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/DefaultCSHealthEventRepository.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/DefaultCSHealthEventRepository.kt new file mode 100644 index 00000000..99dd8031 --- /dev/null +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/repository/DefaultCSHealthEventRepository.kt @@ -0,0 +1,70 @@ +package clickstream.health.internal.repository + +import clickstream.api.CSInfo +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.internal.database.CSHealthEventEntity.Companion.dtoMapTo +import clickstream.health.internal.database.CSHealthEventEntity.Companion.mapToDto +import clickstream.health.internal.database.CSHealthEventDao +import clickstream.health.model.CSHealthEvent +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * The HealthRepositoryImpl is the implementation detail of the [CSHealthEventRepository]. + * + * @param healthEventDao - The Dao object to communicate to the DB + */ +@ExperimentalCoroutinesApi +internal class DefaultCSHealthEventRepository( + private val healthEventDao: CSHealthEventDao, + private val info: CSInfo +) : CSHealthEventRepository { + + override suspend fun insertHealthEvent(healthEvent: CSHealthEvent) { + val event = healthEvent.copy( + sessionId = info.sessionInfo.sessionID, + timestamp = System.currentTimeMillis().toString(), + appVersion = info.appInfo.appVersion + ) + healthEventDao.insert(healthEvent = event.dtoMapTo()) + } + + override suspend fun insertHealthEventList(healthEventList: List) { + val eventList = healthEventList.map { + it.copy( + sessionId = info.sessionInfo.sessionID, + timestamp = System.currentTimeMillis().toString() + ) + }.toList() + healthEventDao.insertAll(healthEventList = eventList.map { it.dtoMapTo() }) + } + + override suspend fun getInstantEvents(size: Int): List { + return healthEventDao.getEventByType(CSEventTypesConstant.INSTANT, size) + .map { it.mapToDto() } + } + + override suspend fun getAggregateEvents(size: Int): List { + return healthEventDao.getEventByType(CSEventTypesConstant.AGGREGATE, size) + .map { it.mapToDto() } + } + + override suspend fun deleteHealthEventsBySessionId(sessionId: String) { + healthEventDao.deleteBySessionId(sessionId = sessionId) + } + + override suspend fun deleteHealthEvents(events: List) { + healthEventDao.deleteHealthEvent(events.map { it.dtoMapTo() }) + } + + override suspend fun deleteHealthEventsByType(type: String) { + healthEventDao.deleteHealthEventByType(type) + } + + override suspend fun getEventCount(type: String): Int { + return healthEventDao.getHealthEventCountByType(type) + } + + override suspend fun getEventsByTypeAndLimit(type: String, limit: Int): List { + return healthEventDao.getEventByType(type, limit).map { it.mapToDto() } + } +} diff --git a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSTimeStampMessageBuilder.kt b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/time/CSTimeStampMessageBuilder.kt similarity index 93% rename from clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSTimeStampMessageBuilder.kt rename to clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/time/CSTimeStampMessageBuilder.kt index d8d2e9cf..af91756d 100644 --- a/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/CSTimeStampMessageBuilder.kt +++ b/clickstream-health-metrics/src/main/kotlin/clickstream/health/internal/time/CSTimeStampMessageBuilder.kt @@ -1,4 +1,4 @@ -package clickstream.health.internal +package clickstream.health.internal.time import com.google.protobuf.Timestamp diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSAppInfo.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSAppInfo.kt new file mode 100644 index 00000000..5338900a --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSAppInfo.kt @@ -0,0 +1,5 @@ +package clickstream.fake + +import clickstream.api.CSAppInfo + +internal val fakeAppInfo = CSAppInfo(appVersion = "1.0.0") diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt new file mode 100644 index 00000000..31fade15 --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt @@ -0,0 +1,10 @@ +package clickstream.fake + +import clickstream.health.model.CSHealthEventConfig + +internal val fakeCSHealthEventConfig = CSHealthEventConfig( + minTrackedVersion = "4.37.0", + randomUserIdRemainder = listOf(123453, 5), + destination = emptyList(), + verbosityLevel = "maximum", +) \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSInfo.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSInfo.kt new file mode 100644 index 00000000..a94e08aa --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSInfo.kt @@ -0,0 +1,14 @@ +package clickstream.fake + +import clickstream.api.CSDeviceInfo +import clickstream.api.CSInfo + +internal fun fakeCSInfo( + deviceInfo: CSDeviceInfo? = null +) = CSInfo( + deviceInfo = deviceInfo ?: fakeDeviceInfo(), + userInfo = fakeUserInfo(), + locationInfo = fakeLocationInfo, + appInfo = fakeAppInfo, + sessionInfo = fakeCSSessionInfo +) \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSLocationInfo.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSLocationInfo.kt new file mode 100644 index 00000000..a9c83b98 --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSLocationInfo.kt @@ -0,0 +1,9 @@ +package clickstream.fake + +import clickstream.api.CSLocationInfo + +internal val fakeLocationInfo = CSLocationInfo( + longitude = 10.00, + latitude = 12.00, + s2Ids = mapOf("key" to "value") +) \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSSessionInfo.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSSessionInfo.kt new file mode 100644 index 00000000..aa8e7b65 --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeCSSessionInfo.kt @@ -0,0 +1,5 @@ +package clickstream.fake + +import clickstream.api.CSSessionInfo + +internal val fakeCSSessionInfo = CSSessionInfo(sessionID = "QWER-123-DFGH") \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeHealthRepository.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeHealthRepository.kt new file mode 100644 index 00000000..91dd60ed --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeHealthRepository.kt @@ -0,0 +1,46 @@ +package clickstream.fake + +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.internal.repository.CSHealthEventRepository +import clickstream.health.model.CSHealthEvent + +internal class FakeHealthRepository : CSHealthEventRepository { + + private val healthList = mutableListOf() + + override suspend fun insertHealthEvent(healthEvent: CSHealthEvent) { + healthList.add(healthEvent) + } + + override suspend fun insertHealthEventList(healthEventList: List) { + healthList.addAll(healthEventList) + } + + override suspend fun getInstantEvents(size: Int): List { + return healthList.filter { it.eventType == CSEventTypesConstant.INSTANT } + } + + override suspend fun getAggregateEvents(size: Int): List { + return healthList.filter { it.eventType == CSEventTypesConstant.AGGREGATE } + } + + override suspend fun deleteHealthEventsBySessionId(sessionId: String) { + /*NoOp*/ + } + + override suspend fun deleteHealthEvents(events: List) { + healthList.removeAll(events) + } + + override suspend fun deleteHealthEventsByType(type: String) { + healthList.removeIf { it.eventType == type } + } + + override suspend fun getEventCount(type: String): Int { + return healthList.size + } + + override suspend fun getEventsByTypeAndLimit(type: String, limit: Int): List { + return healthList.filter { it.eventType == type } + } +} \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeInfo.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeInfo.kt new file mode 100644 index 00000000..ba734cb0 --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/fake/FakeInfo.kt @@ -0,0 +1,59 @@ +package clickstream.fake + +import clickstream.api.CSAppInfo +import clickstream.api.CSDeviceInfo +import clickstream.api.CSInfo +import clickstream.api.CSLocationInfo +import clickstream.api.CSSessionInfo +import clickstream.api.CSUserInfo + +internal fun fakeInfo(): CSInfo { + return CSInfo( + appInfo = fakeAppInfo(), + locationInfo = fakeLocationInfo(), + sessionInfo = fakeSessionInfo(), + deviceInfo = fakeDeviceInfo(), + userInfo = fakeUserInfo() + ) +} + +internal fun fakeAppInfo( + appVersion: String = "1" +): CSAppInfo { + return CSAppInfo(appVersion) +} + +internal fun fakeLocationInfo( + userLatitude: Double = -6.1753924, + userLongitude: Double = 106.8249641, + s2Ids: Map = emptyMap() +): CSLocationInfo { + return CSLocationInfo(userLatitude, userLongitude, s2Ids) +} + +internal fun fakeUserInfo( + currentCountry: String = "ID", + signedUpCountry: String = "ID", + identity: Int = 12345, + email: String = "test@gmail.com" +): CSUserInfo { + return CSUserInfo(currentCountry, signedUpCountry, identity, email) +} + +internal fun fakeSessionInfo( + sessionId: String = "123456" +): CSSessionInfo { + return CSSessionInfo(sessionId) +} + +internal fun fakeDeviceInfo(): CSDeviceInfo { + + return object : CSDeviceInfo { + override fun getDeviceManufacturer(): String = "Samsung" + override fun getDeviceModel(): String = "IPhone X" + override fun getSDKVersion(): String = "15" + override fun getOperatingSystem(): String = "IOS" + override fun getDeviceHeight(): String = "1024" + override fun getDeviceWidth(): String = "400" + } +} diff --git a/clickstream-health-metrics/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt b/clickstream-health-metrics/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt new file mode 100644 index 00000000..0b2aa8ad --- /dev/null +++ b/clickstream-health-metrics/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt @@ -0,0 +1,591 @@ +package clickstream.internal.analytics + +import clickstream.api.CSInfo +import clickstream.fake.FakeHealthRepository +import clickstream.fake.fakeAppInfo +import clickstream.fake.fakeCSHealthEventConfig +import clickstream.fake.fakeCSInfo +import clickstream.fake.fakeCSSessionInfo +import clickstream.fake.fakeUserInfo +import clickstream.health.constant.CSErrorConstant +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.constant.CSHealthEventName +import clickstream.health.intermediate.CSHealthEventLoggerListener +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.intermediate.CSMemoryStatusProvider +import clickstream.health.internal.CSGuIdGenerator +import clickstream.health.internal.factory.CSHealthEventFactory +import clickstream.health.internal.factory.DefaultCSHealthEventFactory +import clickstream.health.internal.processor.CSHealthEventProcessorImpl +import clickstream.health.model.CSEventForHealth +import clickstream.health.model.CSHealthEvent +import clickstream.health.model.CSHealthEventConfig +import clickstream.health.model.EXTERNAL +import clickstream.health.model.INTERNAL +import clickstream.health.time.CSHealthTimeStampGenerator +import clickstream.logger.CSLogLevel.OFF +import clickstream.logger.CSLogger +import clickstream.util.CSAppVersionSharedPref +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.UUID + +@ExperimentalCoroutinesApi +internal class CSHealthEventProcessorTest { + + private val csHealthEventRepository = FakeHealthRepository() + private val csHealthEventFactory = fakeCSHealthEventFactory() + private val csAppVersionSharedPref = mock(CSAppVersionSharedPref::class.java) + private val memoryStatusProvider = mock(CSMemoryStatusProvider::class.java) + private val healthEventLogger = mock(CSHealthEventLoggerListener::class.java) + private val loggerMock = mock(CSLogger::class.java) + + private lateinit var sut: CSHealthEventProcessor + + @Before + fun setup() { + runBlocking { + whenever(memoryStatusProvider.isLowMemory()).thenReturn(false) + } + } + + @Test + fun `Given app version is less than minimum app version verify that health is not tracked`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.38.0"), + sessionInfo = fakeCSSessionInfo.copy(sessionID = "566") + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "5.1", + randomUserIdRemainder = listOf(6, 9) + ) + ) + + val events = fakeCSHealthEvent(1) + + sut.insertBatchEvent(events, 1) + sut.insertBatchEvent(events, emptyList()) + sut.insertNonBatchEvent(events) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + } + } + + @Test + fun `Given user is not whitelisted verify that health is not tracked`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.38.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.60.0", + randomUserIdRemainder = listOf(1, 0) + ) + ) + + val events = fakeCSHealthEvent(1) + + sut.insertBatchEvent(events, 1) + sut.insertBatchEvent(events, emptyList()) + sut.insertNonBatchEvent(events) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + } + } + + @Test + fun `Given app is low on memory verify that health is not tracked`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0) + ) + ) + + whenever(memoryStatusProvider.isLowMemory()).thenReturn(true) + + val events = fakeCSHealthEvent(1) + + sut.insertBatchEvent(events, 1) + sut.insertBatchEvent(events, emptyList()) + sut.insertNonBatchEvent(events) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + } + } + + @Test + fun `Given user is whitelisted and app version is correct for health verify that health is tracked`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0) + ) + ) + + val events = fakeCSHealthEvent(1) + + sut.insertBatchEvent(events, 1) + sut.insertBatchEvent(events, emptyList()) + sut.insertNonBatchEvent(events) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 3) + } + } + + @Test + fun `Given destination is empty verify that health flow returns empty list`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = emptyList(), + verbosityLevel = "minimum" + ) + ) + + val batchId = UUID.randomUUID().toString() + val csEvents = (0..50).map { fakeCSEvent(it.toString(), batchId) } + + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamBatchSent.name, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE + ) + + sut.insertBatchEvent(healthEvent, csEvents) + + val list = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE).toList() + + assert(list.isEmpty()) + verify(healthEventLogger, never()).logEvent(any(), any()) + } + } + + @Test + fun `Given destination is contains INTERNAL verify that health flow returns correct list`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(INTERNAL), + verbosityLevel = "minimum" + ) + ) + + val batchId = UUID.randomUUID().toString() + val csEvents = (0..50).map { fakeCSEvent(it.toString(), batchId) } + + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamBatchSent.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + ) + + sut.insertBatchEvent(healthEvent, csEvents) + + val list = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE).toList() + val fetchedHealth = list[0][0] + + assert(fetchedHealth.numberOfEvents == 51L) + assert(fetchedHealth.numberOfBatches == 1L) + verify(healthEventLogger, never()).logEvent(any(), any()) + } + } + + @Test + fun `Given destination is contains EXTERNAL verify that push event to upstream works correctly`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(EXTERNAL), + verbosityLevel = "minimum" + ) + ) + + val batchId = UUID.randomUUID().toString() + val csEvents = (0..50).map { fakeCSEvent(it.toString(), batchId) } + + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + ) + + sut.insertBatchEvent(healthEvent, csEvents) + + sut.pushEventToUpstream(CSEventTypesConstant.AGGREGATE, true) + verify(healthEventLogger, atLeast(1)).logEvent(any(), any()) + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + + } + } + + @Test + fun `Given destination is contains EXTERNAL verify that upstream listener is invoked with correct data`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(EXTERNAL), + verbosityLevel = "minimum" + ) + ) + + (1..7).forEach { _ -> + val health = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.LOW_BATTERY + ) + sut.insertBatchEvent(health, 20) + } + + (1..30).forEach { _ -> + val health = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.NETWORK_UNAVAILABLE + ) + sut.insertBatchEvent(health, 20) + } + + (1..28).forEach { _ -> + val health = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.SOCKET_NOT_OPEN + ) + sut.insertBatchEvent(health, 30) + } + + (1..19).forEach { _ -> + val health = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + error = "java io exception" + ) + sut.insertBatchEvent(health, 34) + } + + sut.pushEventToUpstream(CSEventTypesConstant.AGGREGATE, true) + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + + verify(healthEventLogger, times(9)).logEvent(any(), any()) + } + } + + @Test + fun `Given health repository has no events verify that health flow returns empty list`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(EXTERNAL, INTERNAL), + verbosityLevel = "minimum" + ) + ) + + val list = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE, true).toList() + + assert(list.isEmpty()) + } + } + + @Test + fun `Given verbosity is minimum verify that healthDetails is null `() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(INTERNAL), + verbosityLevel = "minimum" + ) + ) + + val batchId = UUID.randomUUID().toString() + val csEvents = (0..50).map { fakeCSEvent(it.toString(), batchId) } + + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamBatchSent.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE + ) + + sut.insertBatchEvent(healthEvent, csEvents) + + val list = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE).toList() + val fetchedHealth = list[0][0] + + assert(!fetchedHealth.hasHealthDetails()) + } + } + + @Test + fun `Given verbosity is maximum verify that healthDetails is set correctly `() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(INTERNAL), + verbosityLevel = "maximum" + ) + ) + + val batchId = UUID.randomUUID().toString() + val csEvents = (0..50).map { fakeCSEvent(it.toString(), batchId) } + + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamBatchSent.value, + appVersion = "4.60.0", + eventType = CSEventTypesConstant.AGGREGATE, + eventBatchGuid = batchId + ) + + sut.insertBatchEvent(healthEvent, csEvents) + + val list = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE).toList() + val fetchedHealth = list[0][0] + + assert(fetchedHealth.hasHealthDetails()) + assert(fetchedHealth.healthDetails.eventGuidsList.isNotEmpty()) + assert(fetchedHealth.healthDetails.eventBatchGuidsList.isNotEmpty()) + } + } + + @Test + fun `Given app version is changes verify health events are deleted sucessfully`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + verbosityLevel = "maximum" + ) + ) + + val healthEvent = (0..50).map { fakeCSHealthEvent(it) } + + csHealthEventRepository.insertHealthEventList(healthEvent) + + whenever(csAppVersionSharedPref.isAppVersionEqual(any())).thenReturn(false) + CSHealthEventProcessorImpl.clearHealthEventsForVersionChange( + csAppVersionSharedPref, + "4.60.1", + csHealthEventRepository, + loggerMock + ) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 0) + } + } + + @Test + fun `Given app version is not changed verify health events are not deleted`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + verbosityLevel = "maximum" + ) + ) + + val healthEvent = (0..50).map { fakeCSHealthEvent(it) } + + csHealthEventRepository.insertHealthEventList(healthEvent) + whenever(csAppVersionSharedPref.isAppVersionEqual(any())).thenReturn(true) + CSHealthEventProcessorImpl.clearHealthEventsForVersionChange( + csAppVersionSharedPref, + "4.60.0", + csHealthEventRepository, + loggerMock + ) + + assert(csHealthEventRepository.getEventCount(CSEventTypesConstant.AGGREGATE) == 51) + } + } + + @Test + fun `Given health events verify health meta is correct`() { + runBlocking { + sut = getEventProcessor( + fakeCSInfo().copy( + appInfo = fakeAppInfo.copy(appVersion = "4.60.0"), + userInfo = fakeUserInfo().copy(identity = 122345) + ), + fakeCSHealthEventConfig.copy( + minTrackedVersion = "4.38.0", + randomUserIdRemainder = listOf(5, 0), + destination = listOf(INTERNAL), + verbosityLevel = "maximum" + ) + ) + + val healthEvent = fakeCSHealthEvent(1) + sut.insertBatchEvent(healthEvent, 20) + val healthList = sut.getHealthEventFlow(CSEventTypesConstant.AGGREGATE).toList() + val healthProtoEvent = healthList[0][0] + + with(healthProtoEvent.healthMeta) { + assert(customer.email == fakeCSInfo().userInfo.email) + assert(app.version == fakeCSInfo().appInfo.appVersion) + assert(session.sessionId == fakeCSInfo().sessionInfo.sessionID) + } + } + } + + @Test + fun `Given health config verify isAppVersionGreater function`() { + assert(fakeCSHealthEventConfig.isAppVersionGreater("4.56", "5.0.1").not()) + assert(fakeCSHealthEventConfig.isAppVersionGreater("", "").not()) + assert( + fakeCSHealthEventConfig.isAppVersionGreater( + "4.56.1-Alpha-3456e4", + "4.56.0-Alpha-3456e4" + ) + ) + assert(fakeCSHealthEventConfig.isAppVersionGreater("4.36.1", "1")) + assert(fakeCSHealthEventConfig.isAppVersionGreater("0.0.1", "0.0.4").not()) + } + + @Test + fun `Given health config verify isHealthEnabledUser function`() { + val config = fakeCSHealthEventConfig.copy(randomUserIdRemainder = listOf(1, 6, 9)) + assert(config.isHealthEnabledUser(22546)) + assert(config.isHealthEnabledUser(4559)) + assert(config.isHealthEnabledUser(21091)) + assert(config.isHealthEnabledUser(0).not()) + assert(config.isHealthEnabledUser(288764).not()) + assert(config.isHealthEnabledUser(288763).not()) + } + + private fun getEventProcessor( + csInfo: CSInfo, + healthConfig: CSHealthEventConfig = fakeCSHealthEventConfig.copy( + destination = listOf(EXTERNAL) + ), + ) = CSHealthEventProcessorImpl( + healthEventRepository = csHealthEventRepository, + healthEventConfig = healthConfig, + info = csInfo, + logger = CSLogger(OFF), + healthEventFactory = csHealthEventFactory, + csHealthEventLogger = healthEventLogger, + memoryStatusProvider = memoryStatusProvider, + ) + + private fun fakeCSHealthEvent( + id: Int, + eventName: CSHealthEventName = CSHealthEventName.ClickStreamBatchSent + ): CSHealthEvent { + return CSHealthEvent( + healthEventID = id, + eventName = eventName.value, + eventType = CSEventTypesConstant.AGGREGATE, + timestamp = System.currentTimeMillis().toString(), + error = "", + sessionId = "13455", + count = 0, + networkType = "LTE", + batchSize = 1, + appVersion = "4.37.0" + ) + } + + private fun fakeCSEvent( + id: String = UUID.randomUUID().toString(), + reqId: String = UUID.randomUUID().toString() + ): CSEventForHealth { + return CSEventForHealth( + eventGuid = id, + batchGuid = reqId, + ) + } + + private fun fakeCSHealthEventFactory(): CSHealthEventFactory { + return DefaultCSHealthEventFactory(object : CSGuIdGenerator { + override fun getId(): String { + return UUID.randomUUID().toString() + } + }, object : CSHealthTimeStampGenerator { + + override fun getTimeStamp(): Long { + return System.currentTimeMillis() + } + }, fakeCSInfo()) + } +} \ No newline at end of file diff --git a/clickstream-health-metrics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/clickstream-health-metrics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/clickstream-health-metrics/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/CSLifeCycleManager.kt b/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/CSLifeCycleManager.kt index b1c9db97..e12328fd 100644 --- a/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/CSLifeCycleManager.kt +++ b/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/CSLifeCycleManager.kt @@ -13,6 +13,8 @@ public abstract class CSLifeCycleManager( private val appLifeCycleObserver: CSAppLifeCycle ) : LifecycleObserver { + public abstract val tag: String + /** * Subscribes to the application LifeCycle */ diff --git a/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/internal/CSLifecycleOwnerResumedLifecycle.kt b/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/internal/CSLifecycleOwnerResumedLifecycle.kt index aff03b99..12c9a8aa 100644 --- a/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/internal/CSLifecycleOwnerResumedLifecycle.kt +++ b/clickstream-lifecycle/src/main/kotlin/clickstream/lifecycle/internal/CSLifecycleOwnerResumedLifecycle.kt @@ -23,7 +23,9 @@ internal class CSLifecycleOwnerResumedLifecycle( private inner class ALifecycleObserver : LifecycleObserver { @OnLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_PAUSE) fun onPause() { - logger.debug { "CSLifecycleOwnerResumedLifecycle#onPause" } + logger.debug { + "CSLifecycleOwnerResumedLifecycle#onPause" + } lifecycleRegistry.onNext( Lifecycle.State.Stopped.WithReason(ShutdownReason(1000, "Paused")) diff --git a/clickstream-logger/src/main/kotlin/clickstream/logger/CSLogger.kt b/clickstream-logger/src/main/kotlin/clickstream/logger/CSLogger.kt index 9f5ebf09..a7361563 100644 --- a/clickstream-logger/src/main/kotlin/clickstream/logger/CSLogger.kt +++ b/clickstream-logger/src/main/kotlin/clickstream/logger/CSLogger.kt @@ -17,7 +17,7 @@ public class CSLogger( * * @param message which will be printed */ - public inline fun debug(message: () -> String) { + public inline fun debug(crossinline message: () -> String) { if (isDebug()) Log.d(CLICK_STREAM_LOG_TAG, message()) } @@ -31,7 +31,7 @@ public class CSLogger( * @param suffix which holds some additional info or tag * @param message which will be printed */ - public fun debug(suffix: () -> String, message: () -> String) { + public inline fun debug(suffix: () -> String, message: () -> String) { if (isDebug()) Log.d("$CLICK_STREAM_LOG_TAG:${suffix()}", message()) } @@ -43,9 +43,10 @@ public class CSLogger( * @param message which will be printed * @param t which holds the exception */ - public fun debug(suffix: () -> String, message: () -> String, t: () -> Throwable) { + public inline fun debug(suffix: () -> String, message: () -> String, t: () -> Throwable) { if (isDebug()) Log.d("$CLICK_STREAM_LOG_TAG:${suffix()}", message(), t()) } - public fun isDebug(): Boolean = logLevel.getValue() > CSLogLevel.INFO.getValue() + @PublishedApi + internal fun isDebug(): Boolean = logLevel.getValue() > CSLogLevel.INFO.getValue() } \ No newline at end of file diff --git a/clickstream/build.gradle.kts b/clickstream/build.gradle.kts index 1006cdda..39435f83 100644 --- a/clickstream/build.gradle.kts +++ b/clickstream/build.gradle.kts @@ -1,4 +1,4 @@ - import plugin.AndroidLibraryConfigurationPlugin +import plugin.AndroidLibraryConfigurationPlugin apply() apply(from = "$rootDir/scripts/versioning.gradle") @@ -25,11 +25,17 @@ android { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = listOf("-XXLanguage:+InlineClasses", "-Xexplicit-api=strict") + } defaultConfig { multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("$rootDir/proguard/consumer-proguard-rules.pro") + javaCompileOptions { + annotationProcessorOptions { + argument("room.schemaLocation", "$projectDir/schemas") + } + } } } @@ -37,8 +43,8 @@ dependencies { // Clickstream implementation(files("$rootDir/libs/proto-sdk-1.18.6.jar")) api(projects.clickstreamLogger) - api(projects.clickstreamHealthMetricsNoop) api(projects.clickstreamEventListener) + api(projects.clickstreamHealthMetricsNoop) compileOnly(projects.clickstreamApi) compileOnly(projects.clickstreamHealthMetricsApi) compileOnly(projects.clickstreamLifecycle) diff --git a/clickstream/schemas/clickstream.internal.db.CSDatabase/7.json b/clickstream/schemas/clickstream.internal.db.CSDatabase/7.json new file mode 100644 index 00000000..7934e301 --- /dev/null +++ b/clickstream/schemas/clickstream.internal.db.CSDatabase/7.json @@ -0,0 +1,168 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "a70167a035fbe00ca5eed3a0b6a3fae7", + "entities": [ + { + "tableName": "EventData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventGuid` TEXT NOT NULL, `eventRequestGuid` TEXT, `eventTimeStamp` INTEGER NOT NULL, `isOnGoing` INTEGER NOT NULL, `messageAsBytes` BLOB NOT NULL, `messageName` TEXT NOT NULL, PRIMARY KEY(`eventGuid`))", + "fields": [ + { + "fieldPath": "eventGuid", + "columnName": "eventGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventRequestGuid", + "columnName": "eventRequestGuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eventTimeStamp", + "columnName": "eventTimeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnGoing", + "columnName": "isOnGoing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageAsBytes", + "columnName": "messageAsBytes", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "messageName", + "columnName": "messageName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "eventGuid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HealthStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`healthEventID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventName` TEXT NOT NULL, `eventType` TEXT NOT NULL, `timestamp` TEXT NOT NULL, `eventId` TEXT NOT NULL, `eventBatchId` TEXT NOT NULL, `error` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `count` INTEGER NOT NULL, `networkType` TEXT NOT NULL, `startTime` INTEGER NOT NULL, `stopTime` INTEGER NOT NULL, `bucketType` TEXT NOT NULL, `batchSize` INTEGER NOT NULL, `appVersion` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "healthEventID", + "columnName": "healthEventID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventName", + "columnName": "eventName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventBatchId", + "columnName": "eventBatchId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkType", + "columnName": "networkType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stopTime", + "columnName": "stopTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bucketType", + "columnName": "bucketType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "batchSize", + "columnName": "batchSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appVersion", + "columnName": "appVersion", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "healthEventID" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a70167a035fbe00ca5eed3a0b6a3fae7')" + ] + } +} \ No newline at end of file diff --git a/clickstream/schemas/clickstream.internal.db.CSDatabase/8.json b/clickstream/schemas/clickstream.internal.db.CSDatabase/8.json new file mode 100644 index 00000000..f1a43669 --- /dev/null +++ b/clickstream/schemas/clickstream.internal.db.CSDatabase/8.json @@ -0,0 +1,64 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "f5221e9fbd9aaf0faca6ed4654347879", + "entities": [ + { + "tableName": "EventData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventGuid` TEXT NOT NULL, `eventRequestGuid` TEXT, `eventTimeStamp` INTEGER NOT NULL, `isOnGoing` INTEGER NOT NULL, `messageAsBytes` BLOB NOT NULL, `messageName` TEXT NOT NULL, PRIMARY KEY(`eventGuid`))", + "fields": [ + { + "fieldPath": "eventGuid", + "columnName": "eventGuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventRequestGuid", + "columnName": "eventRequestGuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eventTimeStamp", + "columnName": "eventTimeStamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnGoing", + "columnName": "isOnGoing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageAsBytes", + "columnName": "messageAsBytes", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "messageName", + "columnName": "messageName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "eventGuid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5221e9fbd9aaf0faca6ed4654347879')" + ] + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/CSBytesEvent.kt b/clickstream/src/main/kotlin/clickstream/CSBytesEvent.kt new file mode 100644 index 00000000..4f501de7 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/CSBytesEvent.kt @@ -0,0 +1,44 @@ +package clickstream + +import clickstream.internal.CSEventInternal +import com.google.protobuf.Timestamp + +/*** + * CSBytesEvent is a wrapper which holds guid, timestamp, + * event name and ByteArray of the MessageLite + */ +public data class CSBytesEvent( + val guid: String, + val timestamp: Timestamp, + val eventName: String, + val eventData: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CSBytesEvent + + if (guid != other.guid) return false + if (timestamp != other.timestamp) return false + if (eventName != other.eventName) return false + if (!eventData.contentEquals(other.eventData)) return false + + return true + } + + override fun hashCode(): Int { + var result = guid.hashCode() + result = 31 * result + timestamp.hashCode() + result = 31 * result + eventName.hashCode() + result = 31 * result + eventData.contentHashCode() + return result + } +} + +internal fun CSBytesEvent.toInternal(): CSEventInternal { + return CSEventInternal.CSBytesEvent( + guid, timestamp, + eventName, eventData + ) +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/model/CSEvent.kt b/clickstream/src/main/kotlin/clickstream/CSEvent.kt similarity index 52% rename from clickstream/src/main/kotlin/clickstream/model/CSEvent.kt rename to clickstream/src/main/kotlin/clickstream/CSEvent.kt index 3a45d2de..54adcae4 100644 --- a/clickstream/src/main/kotlin/clickstream/model/CSEvent.kt +++ b/clickstream/src/main/kotlin/clickstream/CSEvent.kt @@ -1,5 +1,6 @@ -package clickstream.model +package clickstream +import clickstream.internal.CSEventInternal import com.google.protobuf.MessageLite import com.google.protobuf.Timestamp @@ -11,3 +12,11 @@ public data class CSEvent( val timestamp: Timestamp, val message: MessageLite ) + +internal fun CSEvent.toInternal(): CSEventInternal.CSEvent { + return CSEventInternal.CSEvent( + guid = guid, + timestamp = timestamp, + message = message + ) +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/extension/CSMessageExt.kt b/clickstream/src/main/kotlin/clickstream/CSMessageExt.kt similarity index 71% rename from clickstream/src/main/kotlin/clickstream/extension/CSMessageExt.kt rename to clickstream/src/main/kotlin/clickstream/CSMessageExt.kt index 9f82c14f..e4b3f5f3 100644 --- a/clickstream/src/main/kotlin/clickstream/extension/CSMessageExt.kt +++ b/clickstream/src/main/kotlin/clickstream/CSMessageExt.kt @@ -1,4 +1,4 @@ -package clickstream.extension +package clickstream import com.gojek.clickstream.de.Event import com.gojek.clickstream.internal.Health @@ -10,9 +10,8 @@ import java.util.Locale /** * Gets the field for the given field name */ -public fun MessageLite.getField(fieldName: String): Field { - return this.javaClass.getDeclaredField(fieldName) -} +public fun MessageLite.getField(fieldName: String): Field = + this.javaClass.getDeclaredField(fieldName) /** * Checks whether the given message contains valid UTF8 characters. @@ -21,7 +20,8 @@ public fun MessageLite.getField(fieldName: String): Field { */ public fun MessageLite.isValidMessage(): Boolean { fun isNestedType(field: Field): Boolean { - return field.type.name.contains("com.gojek.clickstream") && (field.name == "DEFAULT_INSTANCE").not() + return field.type.name.contains("com.gojek.clickstream") && + (field.name == "DEFAULT_INSTANCE").not() } fun isStringType(field: Field): Boolean = field.type == String::class.java @@ -32,8 +32,11 @@ public fun MessageLite.isValidMessage(): Boolean { isNestedType(field) -> { field.isAccessible = true val messageLite = field.get(this) as? MessageLite - isValidMessage = - if (messageLite != null) isValidMessage && messageLite.isValidMessage() else isValidMessage + isValidMessage = if (messageLite != null) { + isValidMessage && messageLite.isValidMessage() + } else { + isValidMessage + } } isStringType(field) -> { field.isAccessible = true @@ -76,10 +79,6 @@ public fun MessageLite.eventName(): String? { }.getOrNull() } -public fun MessageLite.messageName(): String { - return this::class.qualifiedName.orEmpty() -} - /** * Converts [MessageLite] to [Map] using reflection. * */ @@ -96,16 +95,20 @@ public fun MessageLite.toFlatMap(): Map { val propertyMap = mutableMapOf() fun populatePropertyMap(messageLite: MessageLite, prefix: String) { - val declaredMethods = messageLite.javaClass.declaredFields - val validFields = declaredMethods.filter { isValidField(it) } - return validFields.forEach { - it.isAccessible = true - val fieldName = getPropertyNameFromField(it) - val key = if (prefix.isNotEmpty()) "$prefix.$fieldName" else fieldName - when (val fieldValue = it.get(messageLite)) { - is MessageLite -> populatePropertyMap(fieldValue, key) - else -> propertyMap[key] = fieldValue + try { + val declaredMethods = messageLite.javaClass.declaredFields + val validFields = declaredMethods.filter { isValidField(it) } + return validFields.forEach { + it.isAccessible = true + val fieldName = getPropertyNameFromField(it) + val key = if (prefix.isNotEmpty()) "$prefix.$fieldName" else fieldName + when (val fieldValue = it.get(messageLite)) { + is MessageLite -> populatePropertyMap(fieldValue, key) + else -> propertyMap[key] = fieldValue + } } + } catch (e: Exception) { + /*NoOp*/ } } populatePropertyMap(this, "") diff --git a/clickstream/src/main/kotlin/clickstream/ClickStream.kt b/clickstream/src/main/kotlin/clickstream/ClickStream.kt index e354f91f..e3e9f5be 100644 --- a/clickstream/src/main/kotlin/clickstream/ClickStream.kt +++ b/clickstream/src/main/kotlin/clickstream/ClickStream.kt @@ -5,7 +5,6 @@ import clickstream.ClickStream.Companion.initialize import clickstream.config.CSConfiguration import clickstream.internal.DefaultClickStream import clickstream.internal.DefaultClickStream.Companion.initialize -import clickstream.model.CSEvent import kotlinx.coroutines.ExperimentalCoroutinesApi /** @@ -28,6 +27,26 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi * // get the instance * val clickstream = ClickStream.getInstance() * ``` + * + * **Sequence Diagram** + * ``` + * App Clickstream + * +---+---+---+---+---+---+ +---+---+---+---+---+---+ + * | Sending Events | --------> | Received the Events | + * +---+---+---+---+---+---+ +---+---+---+---+---+---+ + * | + * | + * | +---+---+---+---+---+---+---+---+----+ + * if app on active state ---------> | - run the ticker with 10s delay | + * | | - collect events from db | + * | | - transform and send to backend | + * | +---+---+---+---+---+---+---+---+----+ + * | + * | +---+---+---+---+---+---+---+---+---+---+----+ + * else if app on inactive state --> | - run flushEvents and flushHealthMetrics | + * | - transform and send to backend | + * +---+---+---+---+---+---+---+---+---+----+---+ + *``` */ public interface ClickStream { @@ -39,6 +58,15 @@ public interface ClickStream { */ public fun trackEvent(event: CSEvent, expedited: Boolean) + /** + * Push an event with event name and byte array of proto. + * Tracking events through this method does not support event visualiser + * + * @param event a [CSBytesEvent] to be sent. + * @param expedited a flag to determine whether [CSEvent] should be sent expedited. + */ + public fun trackEvent(event: CSBytesEvent, expedited: Boolean) + @ExperimentalCoroutinesApi public companion object { diff --git a/clickstream/src/main/kotlin/clickstream/config/CSConfig.kt b/clickstream/src/main/kotlin/clickstream/config/CSConfig.kt index 28c66da6..be6a7244 100644 --- a/clickstream/src/main/kotlin/clickstream/config/CSConfig.kt +++ b/clickstream/src/main/kotlin/clickstream/config/CSConfig.kt @@ -1,7 +1,5 @@ package clickstream.config -import clickstream.health.model.CSHealthEventConfig - /** * The config which holds the configuration for processor, scheduler & network manager * @@ -13,5 +11,4 @@ public data class CSConfig( val eventProcessorConfiguration: CSEventProcessorConfig, val eventSchedulerConfig: CSEventSchedulerConfig, val networkConfig: CSNetworkConfig, - val healthEventConfig: CSHealthEventConfig ) \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/config/CSConfiguration.kt b/clickstream/src/main/kotlin/clickstream/config/CSConfiguration.kt index 070dcee5..d0f959f2 100644 --- a/clickstream/src/main/kotlin/clickstream/config/CSConfiguration.kt +++ b/clickstream/src/main/kotlin/clickstream/config/CSConfiguration.kt @@ -1,22 +1,21 @@ package clickstream.config import android.content.Context -import clickstream.api.CSDeviceInfo import clickstream.api.CSInfo +import clickstream.config.timestamp.CSEventGeneratedTimestampListener import clickstream.config.timestamp.DefaultCSEventGeneratedTimestampListener import clickstream.connection.CSSocketConnectionListener import clickstream.connection.NoOpCSConnectionListener -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository import clickstream.health.CSHealthGateway import clickstream.health.NoOpCSHealthGateway -import clickstream.health.time.CSEventGeneratedTimestampListener +import clickstream.health.intermediate.CSHealthEventLoggerListener import clickstream.internal.di.CSServiceLocator -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.logger.CSLogLevel +import clickstream.internal.eventscheduler.CSEventSchedulerErrorListener +import clickstream.internal.eventscheduler.impl.NoOpEventSchedulerErrorListener import clickstream.listener.CSEventListener +import clickstream.logger.CSLogLevel +import clickstream.report.CSReportDataListener +import clickstream.report.CSReportDataTracker import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -32,6 +31,7 @@ import kotlinx.coroutines.Dispatchers * @param dispatcher A [CoroutineDispatcher] object for threading related work. * @param info An object that wraps [CSAppInfo], [CSLocationInfo], [CSUserInfo], [CSSessionInfo], [CSDeviceInfo] * @param config An object which holds the configuration for processor, scheduler & network manager + * @param healthLogger An object which hold the configuration for Health Metrics. * @param logLevel ClickStream Loglevel for debugging purposes. * @param eventGeneratedTimeStamp An object which provide a plugin for exposes a timestamp where call side able to use * for provides NTP timestamp @@ -48,12 +48,11 @@ public class CSConfiguration private constructor( internal val eventGeneratedTimeStamp: CSEventGeneratedTimestampListener, internal val socketConnectionListener: CSSocketConnectionListener, internal val remoteConfig: CSRemoteConfig, - internal val eventHealthListener: CSEventHealthListener, - internal val healthEventRepository: CSHealthEventRepository, - internal val healthEventProcessor: CSHealthEventProcessor, - internal val healthEventFactory: CSHealthEventFactory, - internal val appLifeCycle: CSAppLifeCycle, + internal val healthGateway: CSHealthGateway, internal val eventListeners: List = listOf(), + internal val eventSchedulerErrorListener: CSEventSchedulerErrorListener, + internal val csReportDataTracker: CSReportDataTracker? + ) { /** * A Builder for [CSConfiguration]'s. @@ -85,21 +84,17 @@ public class CSConfiguration private constructor( * - NetworkConfig, to define endpoint and timout related things. * - HealthConfig, to define verbosity and health related things. */ - private val config: CSConfig, - - /** - * Specify Clicstream lifecycle, this is needed in order to send events - * to the backend. - */ - private val appLifeCycle: CSAppLifeCycle + private val config: CSConfig ) { private lateinit var dispatcher: CoroutineDispatcher private lateinit var eventGeneratedListener: CSEventGeneratedTimestampListener private lateinit var socketConnectionListener: CSSocketConnectionListener - private lateinit var remoteConfig: CSRemoteConfig private lateinit var healthGateway: CSHealthGateway + private lateinit var remoteConfig: CSRemoteConfig private var logLevel: CSLogLevel = CSLogLevel.OFF private val eventListeners = mutableListOf() + private lateinit var eventSchedulerErrorListener: CSEventSchedulerErrorListener + private var csReportDataTracker: CSReportDataTracker? = null /** * Specifies a custom [CoroutineDispatcher] for [ClickStream]. @@ -147,6 +142,15 @@ public class CSConfiguration private constructor( this.socketConnectionListener = listener } + /** + * Add any [CSEventInterceptor] to intercept clickstream events. + * + * @return This [Builder] instance + */ + public fun addEventListener(listener: CSEventListener): Builder = apply { + this.eventListeners.add(listener) + } + /** * Specifies a custom [CSRemoteConfig] for [ClickStream]. * @@ -159,27 +163,39 @@ public class CSConfiguration private constructor( } /** - * Specify implementation of [CSHealthGateway], by default it would use - * [NoOpCSHealthGateway] + * Specifies a custom [CSEventSchedulerErrorListener] for Event schedulers. * + * @param eventSchedulerErrorListener A [CSEventSchedulerErrorListener] * @return This [Builder] instance */ - public fun setHealthGateway(healthGateway: CSHealthGateway): Builder = + public fun setEventSchedulerErrorListener(eventSchedulerErrorListener: CSEventSchedulerErrorListener): Builder = apply { - this.healthGateway = healthGateway + this.eventSchedulerErrorListener = eventSchedulerErrorListener } - /** - * Configure a single client scoped listener that will receive all analytic events - * for this client. + * Specifies a custom [CSReportDataListener] for Clickstream. + * This should only be set on non prod build for generating reports because of memory overhead. * - * @see CSEventListener for semantics and restrictions on listener implementations. + * @param csBatchReportListener A [CSReportDataListener] + * @return This [Builder] instance */ - public fun addEventListener(eventListener: CSEventListener): Builder = apply { - this.eventListeners.add(eventListener) - } + public fun setReportDataListener(csBatchReportListener: CSReportDataListener): Builder = + apply { + this.csReportDataTracker = CSReportDataTracker(csBatchReportListener) + } + + public fun setHealthGateway(csHealthGateway: CSHealthGateway): Builder = + apply { + this.healthGateway = csHealthGateway + } + + /** + * Builds a [CSConfiguration] object. + * + * @return A [CSConfiguration] object with this [Builder]'s parameters. + */ public fun build(): CSConfiguration { if (::dispatcher.isInitialized.not()) { dispatcher = Dispatchers.Default @@ -193,6 +209,10 @@ public class CSConfiguration private constructor( if (::remoteConfig.isInitialized.not()) { remoteConfig = NoOpCSRemoteConfig() } + if (::eventSchedulerErrorListener.isInitialized.not()) { + eventSchedulerErrorListener = NoOpEventSchedulerErrorListener() + } + if (::healthGateway.isInitialized.not()) { healthGateway = NoOpCSHealthGateway.factory() } @@ -203,12 +223,10 @@ public class CSConfiguration private constructor( eventGeneratedListener, socketConnectionListener, remoteConfig, - healthGateway.eventHealthListener, - healthGateway.healthEventRepository, - healthGateway.healthEventProcessor, - healthGateway.healthEventFactory, - appLifeCycle, - eventListeners + healthGateway, + eventListeners, + eventSchedulerErrorListener, + csReportDataTracker ) } } diff --git a/clickstream/src/main/kotlin/clickstream/config/CSEventSchedulerConfig.kt b/clickstream/src/main/kotlin/clickstream/config/CSEventSchedulerConfig.kt index c23c467a..52e6e4cc 100644 --- a/clickstream/src/main/kotlin/clickstream/config/CSEventSchedulerConfig.kt +++ b/clickstream/src/main/kotlin/clickstream/config/CSEventSchedulerConfig.kt @@ -6,12 +6,13 @@ import java.util.concurrent.TimeUnit.SECONDS * EventSchedulerConfig holds the configuration properties * for the EventScheduler to process the event data * - * @param eventsPerBatch Number of events to combine in a single request - * @param batchPeriod Delay between batches + * @param eventsPerBatch Maximum payload size of events combined in a single request in bytes + * @param batchPeriod Delay between two request * @param flushOnBackground Flag for enabling forced flushing of events * @param connectionTerminationTimerWaitTimeInMillis Wait time after which socket gets disconnected * @param backgroundTaskEnabled Flag for enabling flushing of events by background task * @param workRequestDelayInHr Initial delay for background task + * @param eventTypePrefix prefix that is appended to event type before sending to Racoon */ public data class CSEventSchedulerConfig( val eventsPerBatch: Int, @@ -20,28 +21,33 @@ public data class CSEventSchedulerConfig( val connectionTerminationTimerWaitTimeInMillis: Long, val backgroundTaskEnabled: Boolean, val workRequestDelayInHr: Long, - val utf8ValidatorEnabled: Boolean + val utf8ValidatorEnabled: Boolean, + val eventTypePrefix: String? = null, + val enableForegroundFlushing: Boolean, ) { public companion object { - private const val DEFAULT_EVENTS_PER_BATCH: Int = 20 + private const val DEFAULT_BATCH_SIZE_IN_BYTES: Int = 50000 private const val DEFAULT_BATCH_PERIOD: Long = 10000 private const val FORCED_FLUSH: Boolean = false private const val BACKGROUND_TASK_ENABLED: Boolean = false private const val CONNECTION_TERMINATION_TIMER_WAIT_TIME_IN_S: Long = 5 private const val WORK_REQUEST_DELAY_TIME_IN_HR: Long = 1 private const val UTF8_VALIDATOR_ENABLED: Boolean = true + private const val ENABLE_FOREGROUND_FLUSH: Boolean = false /** * Returns the config with default values */ public fun default(): CSEventSchedulerConfig = CSEventSchedulerConfig( - DEFAULT_EVENTS_PER_BATCH, DEFAULT_BATCH_PERIOD, + DEFAULT_BATCH_SIZE_IN_BYTES, DEFAULT_BATCH_PERIOD, FORCED_FLUSH, SECONDS.toMillis(CONNECTION_TERMINATION_TIMER_WAIT_TIME_IN_S), BACKGROUND_TASK_ENABLED, WORK_REQUEST_DELAY_TIME_IN_HR, - UTF8_VALIDATOR_ENABLED + UTF8_VALIDATOR_ENABLED, + null, + ENABLE_FOREGROUND_FLUSH ) } } diff --git a/clickstream/src/main/kotlin/clickstream/config/CSNetworkConfig.kt b/clickstream/src/main/kotlin/clickstream/config/CSNetworkConfig.kt index 84da020e..5b32c8a9 100644 --- a/clickstream/src/main/kotlin/clickstream/config/CSNetworkConfig.kt +++ b/clickstream/src/main/kotlin/clickstream/config/CSNetworkConfig.kt @@ -1,8 +1,8 @@ package clickstream.config import com.google.gson.annotations.SerializedName -import java.util.concurrent.TimeUnit.SECONDS import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit.SECONDS private const val CONNECT_TIMEOUT = 10L private const val READ_TIMEOUT = 10L @@ -22,13 +22,10 @@ private const val MAX_REQUEST_ACK_TIMEOUT = 10L * @param readTimeout Read timeout to be used by okhttp (in seconds) * @param writeTimeout Write timeout to be used by okhttp (in seconds) * @param pingInterval Interval between pings initiated by client (in seconds) - * @param initialRetryDurationInMs - * Initial retry duration to be used for retry backoff strategy (in milliseconds) - * @param maxConnectionRetryDurationInMs - * Maximum retry duration for retry backoff strategy (in milliseconds) + * @param initialRetryDurationInMs Initial retry duration to be used for retry backoff strategy (in milliseconds) + * @param maxConnectionRetryDurationInMs Maximum retry duration for retry backoff strategy (in milliseconds) * @param maxRetriesPerBatch Maximum retries per batch request * @param maxRequestAckTimeout Maximum timeout for a request to receive Ack (in milliseconds) - * @param okHttpClient OkHttpClient instance that passed from client */ public data class CSNetworkConfig( @SerializedName("endPoint") val endPoint: String, @@ -41,26 +38,32 @@ public data class CSNetworkConfig( @SerializedName("minBatteryLevel") val minBatteryLevel: Int, @SerializedName("maxRetriesPerBatch") val maxRetriesPerBatch: Int, @SerializedName("maxRequestAckTimeout") val maxRequestAckTimeout: Long, - @SerializedName("okHttpClient") val okHttpClient: OkHttpClient + @SerializedName("headers") val headers: Map = mapOf(), + @SerializedName("okHttpClient") val okHttpClient: OkHttpClient? = null ) { public companion object { - /** - * Helper method to create instance of NetworkConfiguration with default values + * A default configuration for CSNetworkConfig */ - public fun default(okHttpClient: OkHttpClient): CSNetworkConfig = CSNetworkConfig( - "", - SECONDS.toSeconds(CONNECT_TIMEOUT), - SECONDS.toSeconds(READ_TIMEOUT), - SECONDS.toSeconds(WRITE_TIMEOUT), - SECONDS.toSeconds(PING_INTERVAL), - SECONDS.toMillis(INITIAL_RETRY_DURATION), - SECONDS.toMillis(MAX_RETRY_DURATION), - MIN_BATTERY_LEVEL, - MAX_RETRIES_PER_BATCH, - SECONDS.toMillis(MAX_REQUEST_ACK_TIMEOUT), - okHttpClient - ) + public fun default( + url: String, + headers: Map, + okHttpClient: OkHttpClient? = null + ): CSNetworkConfig = + CSNetworkConfig( + endPoint = url, + connectTimeout = SECONDS.toSeconds(CONNECT_TIMEOUT), + readTimeout = SECONDS.toSeconds(READ_TIMEOUT), + writeTimeout = SECONDS.toSeconds(WRITE_TIMEOUT), + pingInterval = SECONDS.toSeconds(PING_INTERVAL), + initialRetryDurationInMs = SECONDS.toMillis(INITIAL_RETRY_DURATION), + maxConnectionRetryDurationInMs = SECONDS.toMillis(MAX_RETRY_DURATION), + minBatteryLevel = MIN_BATTERY_LEVEL, + maxRetriesPerBatch = MAX_RETRIES_PER_BATCH, + maxRequestAckTimeout = SECONDS.toMillis(MAX_REQUEST_ACK_TIMEOUT), + headers = headers, + okHttpClient = okHttpClient, + ) } } diff --git a/clickstream/src/main/kotlin/clickstream/config/CSRemoteConfig.kt b/clickstream/src/main/kotlin/clickstream/config/CSRemoteConfig.kt index 5fe025cd..aaf4a543 100644 --- a/clickstream/src/main/kotlin/clickstream/config/CSRemoteConfig.kt +++ b/clickstream/src/main/kotlin/clickstream/config/CSRemoteConfig.kt @@ -8,11 +8,23 @@ public interface CSRemoteConfig { * True if we flushed events on the foreground */ public val isForegroundEventFlushEnabled: Boolean + + public val ignoreBatteryLvlOnFlush: Boolean + + public val batchFlushedEvents: Boolean } /** * A NoOp implementation of Remote Configuration A/B testing for ClickStream. */ -public class NoOpCSRemoteConfig( - override val isForegroundEventFlushEnabled: Boolean = false -) : CSRemoteConfig \ No newline at end of file +public class NoOpCSRemoteConfig : CSRemoteConfig { + + override val isForegroundEventFlushEnabled: Boolean + get() = false + + override val ignoreBatteryLvlOnFlush: Boolean + get() = false + + override val batchFlushedEvents: Boolean + get() = false +} \ No newline at end of file diff --git a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSEventGeneratedTimestampListener.kt b/clickstream/src/main/kotlin/clickstream/config/timestamp/CSEventGeneratedTimestampListener.kt similarity index 89% rename from clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSEventGeneratedTimestampListener.kt rename to clickstream/src/main/kotlin/clickstream/config/timestamp/CSEventGeneratedTimestampListener.kt index 2bf14ffd..7670d35b 100644 --- a/clickstream-health-metrics-api/src/main/kotlin/clickstream/health/time/CSEventGeneratedTimestampListener.kt +++ b/clickstream/src/main/kotlin/clickstream/config/timestamp/CSEventGeneratedTimestampListener.kt @@ -1,4 +1,4 @@ -package clickstream.health.time +package clickstream.config.timestamp /** * An interface to generate time that will being use by internal events. diff --git a/clickstream/src/main/kotlin/clickstream/config/timestamp/DefaultCSEventGeneratedTimestampListener.kt b/clickstream/src/main/kotlin/clickstream/config/timestamp/DefaultCSEventGeneratedTimestampListener.kt index 678ed8ae..ddd5b764 100644 --- a/clickstream/src/main/kotlin/clickstream/config/timestamp/DefaultCSEventGeneratedTimestampListener.kt +++ b/clickstream/src/main/kotlin/clickstream/config/timestamp/DefaultCSEventGeneratedTimestampListener.kt @@ -1,7 +1,5 @@ package clickstream.config.timestamp -import clickstream.health.time.CSEventGeneratedTimestampListener - /** * A default implementation of [CSEventGeneratedTimestampListener]. */ diff --git a/clickstream/src/main/kotlin/clickstream/connection/CSConnectionEvent.kt b/clickstream/src/main/kotlin/clickstream/connection/CSConnectionEvent.kt index d14098da..bd384679 100644 --- a/clickstream/src/main/kotlin/clickstream/connection/CSConnectionEvent.kt +++ b/clickstream/src/main/kotlin/clickstream/connection/CSConnectionEvent.kt @@ -34,11 +34,7 @@ public sealed class CSConnectionEvent { * * @property shutdownReason Reason to shutdown from the peer. */ - public data class OnConnectionClosed(val shutdownReason: CSShutdownReason) : CSConnectionEvent() { - override fun toString(): String { - return "Code : ${shutdownReason.code}, Reason : ${shutdownReason.reason}" - } - } + public data class OnConnectionClosed(val shutdownReason: CSShutdownReason) : CSConnectionEvent() /** * Invoked when a web socket has been closed due to an error reading from or writing to the network. Both outgoing diff --git a/clickstream/src/main/kotlin/clickstream/connection/CSShutdownReason.kt b/clickstream/src/main/kotlin/clickstream/connection/CSShutdownReason.kt index 0366818d..917b8d20 100644 --- a/clickstream/src/main/kotlin/clickstream/connection/CSShutdownReason.kt +++ b/clickstream/src/main/kotlin/clickstream/connection/CSShutdownReason.kt @@ -10,10 +10,6 @@ import com.tinder.scarlet.ShutdownReason * @property reason Reason for shutting down. */ public data class CSShutdownReason(val code: Int, val reason: String) { - override fun toString(): String { - return "Code : ${code}, Reason : ${reason}" - } - public companion object { private const val NORMAL_CLOSURE_STATUS_CODE = 1000 private const val NORMAL_CLOSURE_REASON = "Normal closure" diff --git a/clickstream/src/main/kotlin/clickstream/internal/CSEventInternal.kt b/clickstream/src/main/kotlin/clickstream/internal/CSEventInternal.kt new file mode 100644 index 00000000..317d0c1d --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/CSEventInternal.kt @@ -0,0 +1,18 @@ +package clickstream.internal + +import com.google.protobuf.MessageLite +import com.google.protobuf.Timestamp + +internal sealed class CSEventInternal { + data class CSEvent( + val guid: String, + val timestamp: Timestamp, + val message: MessageLite + ) : CSEventInternal() + data class CSBytesEvent( + val guid: String, + val timestamp: Timestamp, + val eventName: String, + val eventData: ByteArray + ) : CSEventInternal() +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/DefaultClickStream.kt b/clickstream/src/main/kotlin/clickstream/internal/DefaultClickStream.kt index f32cedcb..9c79ac9e 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/DefaultClickStream.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/DefaultClickStream.kt @@ -2,6 +2,8 @@ package clickstream.internal import androidx.annotation.GuardedBy import androidx.annotation.RestrictTo +import clickstream.CSBytesEvent +import clickstream.CSEvent import clickstream.ClickStream import clickstream.config.CSConfiguration import clickstream.internal.di.CSServiceLocator @@ -9,7 +11,7 @@ import clickstream.internal.di.impl.DefaultCServiceLocator import clickstream.internal.eventprocessor.CSEventProcessor import clickstream.internal.workmanager.CSWorkManager import clickstream.logger.CSLogger -import clickstream.model.CSEvent +import clickstream.toInternal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -28,7 +30,7 @@ internal class DefaultClickStream private constructor( private val handler: CoroutineExceptionHandler by lazy { CoroutineExceptionHandler { _, throwable -> - logger.debug { "DefaultClickStream#error : ${throwable.message}" } + logger.debug { throwable.message.toString() } } } private val scope: CoroutineScope by lazy { @@ -36,6 +38,14 @@ internal class DefaultClickStream private constructor( } override fun trackEvent(event: CSEvent, expedited: Boolean) { + trackEventInternal(event.toInternal(), expedited) + } + + override fun trackEvent(event: CSBytesEvent, expedited: Boolean) { + trackEventInternal(event.toInternal(), expedited) + } + + private fun trackEventInternal(event: CSEventInternal, expedited: Boolean) { scope.launch { processor.trackEvent(event) if (expedited) { @@ -86,16 +96,16 @@ internal class DefaultClickStream private constructor( if (sInstance != null) { throw IllegalStateException( "ClickStream is already initialized. " + - "If you want to re-initialize ClickStream with new CSConfiguration, " + - "please call ClickStream#release first. " + - "See ClickStream#initialize(Context, CSConfiguration) or " + - "the class level. " + - "KotlinDoc for more information." + "If you want to re-initialize ClickStream with new CSConfiguration, " + + "please call ClickStream#release first. " + + "See ClickStream#initialize(Context, CSConfiguration) or " + + "the class level. " + + "KotlinDoc for more information." ) } if (sInstance == null) { - val ctx = configuration.context.applicationContext + val ctx = configuration.context val serviceLocator = DefaultCServiceLocator( context = ctx, info = configuration.info, @@ -105,12 +115,10 @@ internal class DefaultClickStream private constructor( eventGeneratedTimestampListener = configuration.eventGeneratedTimeStamp, socketConnectionListener = configuration.socketConnectionListener, remoteConfig = configuration.remoteConfig, - eventHealthListener = configuration.eventHealthListener, - healthEventRepository = configuration.healthEventRepository, - healthEventProcessor = configuration.healthEventProcessor, - healthEventFactory = configuration.healthEventFactory, - appLifeCycle = configuration.appLifeCycle, - eventListener = configuration.eventListeners + eventListeners = configuration.eventListeners, + eventSchedulerErrorListener = configuration.eventSchedulerErrorListener, + csReportDataTracker = configuration.csReportDataTracker, + healthGateway = configuration.healthGateway ) CSServiceLocator.setServiceLocator(serviceLocator) @@ -138,7 +146,6 @@ internal class DefaultClickStream private constructor( synchronized(lock) { if (sInstance != null) { sInstance = null - CSServiceLocator.release() } } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/NoOpCSEventHealthListener.kt b/clickstream/src/main/kotlin/clickstream/internal/NoOpCSEventHealthListener.kt deleted file mode 100644 index b8a56005..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/NoOpCSEventHealthListener.kt +++ /dev/null @@ -1,10 +0,0 @@ -package clickstream.internal - -import clickstream.health.model.CSEventHealth -import clickstream.health.intermediate.CSEventHealthListener - -internal class NoOpCSEventHealthListener : CSEventHealthListener { - override fun onEventCreated(healthEvent: CSEventHealth) { - /*No Op*/ - } -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/analytics/CSErrorReasons.kt b/clickstream/src/main/kotlin/clickstream/internal/analytics/CSErrorReasons.kt deleted file mode 100644 index ef3a95c6..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/analytics/CSErrorReasons.kt +++ /dev/null @@ -1,14 +0,0 @@ -package clickstream.internal.analytics - -internal object CSErrorReasons { - const val PARSING_EXCEPTION = "parsing_exception" - const val LOW_BATTERY = "low_battery" - const val NETWORK_UNAVAILABLE = "network_unavailable" - const val SOCKET_NOT_OPEN = "socket_not_open" - const val UNKNOWN = "unknown" - const val USER_UNAUTHORIZED = "401 Unauthorized" - const val SOCKET_TIMEOUT = "socket_timeout" - const val EOFException = "EOFException" - const val MAX_USER_LIMIT_REACHED = "max_user_limit_reached" - const val MAX_CONNECTION_LIMIT_REACHED = "max_connection_limit_reached" -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/analytics/impl/NoOpCSHealthEventLogger.kt b/clickstream/src/main/kotlin/clickstream/internal/analytics/impl/NoOpCSHealthEventLogger.kt deleted file mode 100644 index aba4efde..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/analytics/impl/NoOpCSHealthEventLogger.kt +++ /dev/null @@ -1,11 +0,0 @@ -package clickstream.internal.analytics.impl - -import clickstream.health.intermediate.CSHealthEventLoggerListener -import clickstream.health.model.CSHealthEvent - -internal class NoOpCSHealthEventLogger : CSHealthEventLoggerListener { - - override fun logEvent(eventName: String, healthEvent: CSHealthEvent) { - /*No Op*/ - } -} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/db/CSBatchSizeSharedPref.kt b/clickstream/src/main/kotlin/clickstream/internal/db/CSBatchSizeSharedPref.kt new file mode 100644 index 00000000..b7139c8c --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/db/CSBatchSizeSharedPref.kt @@ -0,0 +1,13 @@ +package clickstream.internal.db + +/*** + * Stores the batch size generated through the batching strategy + * to be used while flushing in Shared prefs .The batching strategy determines the size + * of batch based on the inflow of events and their byte size. This will + * store the batch size and it is used while batching the events when we flush + * events in background. + */ +internal interface CSBatchSizeSharedPref { + suspend fun saveBatchSize(batchSize: Int) + suspend fun getSavedBatchSize(): Int +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/db/CSDatabase.kt b/clickstream/src/main/kotlin/clickstream/internal/db/CSDatabase.kt index 8b2999e0..f0fda5af 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/db/CSDatabase.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/db/CSDatabase.kt @@ -2,10 +2,13 @@ package clickstream.internal.db import android.content.Context import androidx.annotation.GuardedBy +import androidx.room.AutoMigration import androidx.room.Database +import androidx.room.DeleteTable import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec import clickstream.internal.eventscheduler.CSEventData import clickstream.internal.eventscheduler.CSEventDataDao import clickstream.internal.eventscheduler.CSEventDataTypeConverters @@ -15,7 +18,10 @@ import clickstream.internal.eventscheduler.CSEventDataTypeConverters * * The Events are cached, processed and then cleared. */ -@Database(entities = [CSEventData::class], version = 9) +@Database( + entities = [CSEventData::class], version = 8, + autoMigrations = [AutoMigration(from = 7, to = 8, spec = CSDbAutoMigrationFrom7to8::class)], +) @TypeConverters(CSEventDataTypeConverters::class) internal abstract class CSDatabase : RoomDatabase() { @@ -53,3 +59,7 @@ internal abstract class CSDatabase : RoomDatabase() { .build() } } + +@DeleteTable(tableName = "HealthStats") +private class CSDbAutoMigrationFrom7to8 : AutoMigrationSpec + diff --git a/clickstream/src/main/kotlin/clickstream/internal/db/impl/DefaultCSBatchSizeSharedPref.kt b/clickstream/src/main/kotlin/clickstream/internal/db/impl/DefaultCSBatchSizeSharedPref.kt new file mode 100644 index 00000000..450665b0 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/db/impl/DefaultCSBatchSizeSharedPref.kt @@ -0,0 +1,36 @@ +package clickstream.internal.db.impl + +import android.content.Context +import clickstream.internal.db.CSBatchSizeSharedPref +import kotlinx.coroutines.coroutineScope + +private const val CLICKSTREAM_BATCH_PREF = "Clickstream_Batch_Pref" +private const val BATCH_SIZE_PREF_KEY = "batch_size" +private const val DEFAULT_BATCH_SIZE = 250 + +internal class DefaultCSBatchSizeSharedPref( + private val context: Context +) : CSBatchSizeSharedPref { + + override suspend fun saveBatchSize(batchSize: Int) { + coroutineScope { + val sharedPref = + context.getSharedPreferences(CLICKSTREAM_BATCH_PREF, Context.MODE_PRIVATE) + val finalBatchSize = when (batchSize < DEFAULT_BATCH_SIZE) { + true -> DEFAULT_BATCH_SIZE + false -> batchSize + } + with(sharedPref.edit()) { + putInt(BATCH_SIZE_PREF_KEY, finalBatchSize) + apply() + } + } + } + + override suspend fun getSavedBatchSize(): Int { + return coroutineScope { + val sharedPref = context.getSharedPreferences(CLICKSTREAM_BATCH_PREF, Context.MODE_PRIVATE) + sharedPref.getInt(BATCH_SIZE_PREF_KEY, DEFAULT_BATCH_SIZE) + } + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/di/CSServiceLocator.kt b/clickstream/src/main/kotlin/clickstream/internal/di/CSServiceLocator.kt index aacccc06..28986c13 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/di/CSServiceLocator.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/di/CSServiceLocator.kt @@ -2,25 +2,14 @@ package clickstream.internal.di import androidx.annotation.GuardedBy import clickstream.config.CSEventSchedulerConfig -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository import clickstream.internal.eventprocessor.CSEventProcessor -import clickstream.internal.eventscheduler.CSBackgroundEventScheduler -import clickstream.internal.eventscheduler.CSForegroundEventScheduler -import clickstream.internal.eventscheduler.CSWorkManagerEventScheduler +import clickstream.internal.eventscheduler.CSBackgroundScheduler +import clickstream.internal.eventscheduler.CSEventScheduler import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.workmanager.CSEventFlushOneTimeWorkManager -import clickstream.internal.workmanager.CSEventFlushPeriodicWorkManager import clickstream.internal.workmanager.CSWorkManager -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSBackgroundLifecycleManager -import clickstream.listener.CSEventListener import clickstream.logger.CSLogLevel import clickstream.logger.CSLogger -import com.gojek.clickstream.internal.Health -import com.tinder.scarlet.lifecycle.LifecycleRegistry import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -48,11 +37,8 @@ internal interface CSServiceLocator { synchronized(lock) { if (sInstance == null) { requireNotNull(sInstance) { - "CSServiceLocator is not initialized yet, " + - "please call setServiceLocator#release first. " + - "See CSServiceLocator#setServiceLocator(CSServiceLocator) or " + - "the class level. " + - "KotlinDoc for more information." + "Service Locator should be created and set by using " + + "[setServiceLocator] function." } } } @@ -66,24 +52,6 @@ internal interface CSServiceLocator { fun setServiceLocator(serviceLocator: CSServiceLocator) { sInstance = serviceLocator } - - /** - * Dispose services - */ - fun release() { - sInstance?.foregroundLifecycleRegistry?.onComplete() - sInstance?.backgroundLifecycleManager?.lifecycleRegistry?.onComplete() - sInstance?.foregroundEventScheduler?.cancelJob() - sInstance?.backgroundEventScheduler?.cancelJob() - sInstance?.foregroundNetworkManager?.cancelJob() - - // cancel enqueue work - sInstance?.workManager?.context?.run { - CSEventFlushPeriodicWorkManager.cancelWork(this) - CSEventFlushOneTimeWorkManager.cancelWork(this) - } - sInstance = null - } } /** @@ -94,22 +62,22 @@ internal interface CSServiceLocator { /** * Network Manage which communicates with the backend */ - val foregroundNetworkManager: CSNetworkManager + val networkManager: CSNetworkManager /** * Event scheduler which schedules and dispatches events to the backend */ - val foregroundEventScheduler: CSForegroundEventScheduler + val eventScheduler: CSEventScheduler /** - * Event scheduler which schedules and dispatches events to the backend + * EventProcessor which processes and dispatches to scheduler */ - val backgroundEventScheduler: CSBackgroundEventScheduler + val eventProcessor: CSEventProcessor /** - * EventProcessor which processes and dispatches to scheduler + * Processes the health events and sends to BE */ - val eventProcessor: CSEventProcessor + val healthEventProcessor: CSHealthEventProcessor? /** * The background work manager which flushes the event @@ -119,7 +87,7 @@ internal interface CSServiceLocator { /** * The Background scheduler which flushes the event */ - val workManagerEventScheduler: CSWorkManagerEventScheduler + val backgroundScheduler: CSBackgroundScheduler /** * Configuration reference to hold metadata @@ -131,86 +99,9 @@ internal interface CSServiceLocator { */ val logLevel: CSLogLevel - val foregroundLifecycleRegistry: LifecycleRegistry - - val backgroundLifecycleManager: CSBackgroundLifecycleManager - /** * Internal Logger */ val logger: CSLogger - /** - * [CSEventHealthListener] Essentially an optional listener which being used for - * perform an analytic metrics to check every event size. We're exposed listener - * so that if the host app wants to check each event size they can simply add the listener. - * - * Proto `MessageLite` provide an API that we're able to use to check the byte size which is - * [messageSerializedSizeInBytes]. - * - * **Example:** - * ```kotlin - * private fun applyEventHealthMetrics(config: CSClickStreamConfig): CSEventHealthListener { - * if (config.isEventHealthListenerEnabled.not()) return NoOpCSEventHealthListener() - * - * return object : CSEventHealthListener { - * override fun onEventCreated(healthEvent: CSEventHealth) { - * executor.execute { - * val trace = FirebasePerformance.getInstance().newTrace("CS_Event_Health_Metrics") - * trace.start() - * trace.putMetric( - * healthEvent.messageName, - * healthEvent.messageSerializedSizeInBytes.toLong() - * ) - * trace.stop() - * } - * } - * } - * } - * ``` - */ - val eventHealthListener: CSEventHealthListener - - /** - * [CSHealthEventRepository] Act as repository pattern where internally it doing DAO operation - * to insert, delete, and read the [CSHealthEvent]'s. - * - * If you're using `com.gojek.clickstream:clickstream-health-metrics-noop`, the - * [CSHealthEventRepository] internally will doing nothing. - * - * Do consider to use `com.gojek.clickstream:clickstream-health-metrics`, to operate - * [CSHealthEventRepository] as expected. Whenever you opt in the `com.gojek.clickstream:clickstream-health-metrics`, - * you should never touch the [DefaultCSHealthEventRepository] explicitly. All the wiring - * is happening through [DefaultCSHealthGateway.factory(/*args*/)] - */ - val healthEventRepository: CSHealthEventRepository - - /** - * [CSHealthEventProcessor] is the Heart of the Clickstream Library. The [CSHealthEventProcessor] - * is only for pushing events to the backend. [CSHealthEventProcessor] is respect to the - * Application lifecycle where on the active state, we have a ticker that will collect events from database - * and the send that to the backend. The ticker will run on every 10seconds and will be stopped - * whenever application on the inactive state. - * - * On the inactive state we will running flush for both Events and HealthEvents, where - * it would be transformed and send to the backend. - */ - val healthEventProcessor: CSHealthEventProcessor - - /** - * [CSHealthEventFactory] is act as proxy that would mutate the meta of any incoming - * [Health] events. The mutation is needed in order to set value in the meta that would being - * used by the internal metrics. - */ - val healthEventFactory: CSHealthEventFactory - - /** - * [CSAppLifeCycle] is an interface which provides onStart and onStop lifecycle based - * on the concrete implementation, as for now we have 2 implementation, such as: - * - [DefaultCSAppLifeCycleObserver] which respect to the Application Lifecycle - * - [DefaultCSActivityLifeCycleObserver] which respect to the Activities Lifecycle - */ - val appLifeCycle: CSAppLifeCycle - - val eventListener: List } diff --git a/clickstream/src/main/kotlin/clickstream/internal/di/impl/DefaultCServiceLocator.kt b/clickstream/src/main/kotlin/clickstream/internal/di/impl/DefaultCServiceLocator.kt index 27be73fc..c73258aa 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/di/impl/DefaultCServiceLocator.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/di/impl/DefaultCServiceLocator.kt @@ -6,48 +6,50 @@ import clickstream.api.CSInfo import clickstream.config.CSConfig import clickstream.config.CSEventSchedulerConfig import clickstream.config.CSRemoteConfig +import clickstream.config.timestamp.CSEventGeneratedTimestampListener import clickstream.connection.CSSocketConnectionListener -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.identity.DefaultCSGuIdGenerator -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory +import clickstream.health.CSHealthGateway import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.time.CSEventGeneratedTimestampListener -import clickstream.health.time.CSTimeStampGenerator -import clickstream.health.time.DefaultCSTimeStampGenerator +import clickstream.internal.db.CSBatchSizeSharedPref import clickstream.internal.db.CSDatabase +import clickstream.internal.db.impl.DefaultCSBatchSizeSharedPref import clickstream.internal.di.CSServiceLocator import clickstream.internal.eventprocessor.CSEventProcessor -import clickstream.internal.eventscheduler.CSBackgroundEventScheduler +import clickstream.internal.eventscheduler.CSBackgroundScheduler +import clickstream.internal.eventscheduler.CSEventSchedulerErrorListener +import clickstream.internal.eventscheduler.CSEventBatchSizeStrategy import clickstream.internal.eventscheduler.CSEventRepository -import clickstream.internal.eventscheduler.CSForegroundEventScheduler -import clickstream.internal.eventscheduler.CSWorkManagerEventScheduler +import clickstream.internal.eventscheduler.CSEventScheduler +import clickstream.internal.eventscheduler.impl.EventByteSizeBasedBatchStrategy import clickstream.internal.eventscheduler.impl.DefaultCSEventRepository +import clickstream.internal.networklayer.CSBackgroundNetworkManager import clickstream.internal.networklayer.CSEventService import clickstream.internal.networklayer.CSNetworkManager import clickstream.internal.networklayer.CSNetworkRepository import clickstream.internal.networklayer.CSNetworkRepositoryImpl -import clickstream.internal.networklayer.CSWorkManagerNetworkManager +import clickstream.internal.networklayer.socket.CSSocketConnectionManager import clickstream.internal.utils.CSBatteryStatusObserver import clickstream.internal.utils.CSFlowStreamAdapterFactory +import clickstream.internal.utils.CSGuIdGenerator +import clickstream.internal.utils.CSGuIdGeneratorImpl import clickstream.internal.utils.CSNetworkStatusObserver +import clickstream.internal.utils.CSTimeStampGenerator +import clickstream.internal.utils.DefaultCSTimeStampGenerator import clickstream.internal.workmanager.CSWorkManager -import clickstream.lifecycle.CSAndroidLifecycle -import clickstream.lifecycle.CSAndroidLifecycle.APPLICATION_THROTTLE_TIMEOUT_MILLIS -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSBackgroundLifecycleManager +import clickstream.lifecycle.impl.DefaultCSAppLifeCycleObserver import clickstream.listener.CSEventListener import clickstream.logger.CSLogLevel import clickstream.logger.CSLogger -import com.tinder.scarlet.Lifecycle import com.tinder.scarlet.Scarlet -import com.tinder.scarlet.lifecycle.LifecycleRegistry import com.tinder.scarlet.messageadapter.protobuf.ProtobufMessageAdapter import com.tinder.scarlet.retry.ExponentialBackoffStrategy import com.tinder.scarlet.websocket.okhttp.newWebSocketFactory +import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi +import okhttp3.OkHttpClient +import clickstream.report.CSReportDataTracker +import com.tinder.scarlet.lifecycle.LifecycleRegistry /** * The Default implementation of the Service Locator which will be used for injection for @@ -58,25 +60,19 @@ internal class DefaultCServiceLocator( private val context: Context, private val info: CSInfo, private val config: CSConfig, + override val logLevel: CSLogLevel, + override val dispatcher: CoroutineDispatcher, private val eventGeneratedTimestampListener: CSEventGeneratedTimestampListener, private val socketConnectionListener: CSSocketConnectionListener, private val remoteConfig: CSRemoteConfig, - override val logLevel: CSLogLevel, - override val dispatcher: CoroutineDispatcher, - override val eventHealthListener: CSEventHealthListener, - override val healthEventRepository: CSHealthEventRepository, - override val healthEventProcessor: CSHealthEventProcessor, - override val healthEventFactory: CSHealthEventFactory, - override val appLifeCycle: CSAppLifeCycle, - override val eventListener: List + private val healthGateway: CSHealthGateway, + private val eventListeners: List, + private val eventSchedulerErrorListener: CSEventSchedulerErrorListener, + private val csReportDataTracker: CSReportDataTracker? ) : CSServiceLocator { - private val db: CSDatabase by lazy { - CSDatabase.getInstance(context) - } - private val guidGenerator: CSGuIdGenerator by lazy { - DefaultCSGuIdGenerator() + CSGuIdGeneratorImpl() } private val timeStampGenerator: CSTimeStampGenerator by lazy { @@ -91,159 +87,189 @@ internal class DefaultCServiceLocator( CSNetworkStatusObserver(context) } - private val eventRepository: CSEventRepository by lazy { - DefaultCSEventRepository(eventDataDao = db.eventDataDao()) - } - - override val foregroundLifecycleRegistry: LifecycleRegistry by lazy { - LifecycleRegistry(APPLICATION_THROTTLE_TIMEOUT_MILLIS) - } - - private val foregroundLifecycleManager: Lifecycle by lazy { - CSAndroidLifecycle.ofApplicationForeground(context.applicationContext as Application, logger, foregroundLifecycleRegistry) - } - - override val backgroundLifecycleManager: CSBackgroundLifecycleManager by lazy { - CSBackgroundLifecycleManager() + /** + * The Db will which will store the events sent to the sdk + */ + private val db: CSDatabase by lazy { + CSDatabase.getInstance(context) } - private val backgroundEventService: CSEventService by lazy { - Scarlet.Builder().lifecycle(backgroundLifecycleManager).apply() + private val socketConnectionManager: CSSocketConnectionManager by lazy { + CSSocketConnectionManager(LifecycleRegistry(), context.applicationContext as Application) } - private val foregroundEventService: CSEventService by lazy { - Scarlet.Builder().lifecycle(foregroundLifecycleManager).apply() + private val eventService: CSEventService by lazy { + Scarlet.Builder().lifecycle(socketConnectionManager).apply() } private val networkRepository: CSNetworkRepository by lazy { CSNetworkRepositoryImpl( networkConfig = config.networkConfig, - eventService = foregroundEventService, + eventService = eventService, dispatcher = dispatcher, timeStampGenerator = timeStampGenerator, logger = logger, - healthEventRepository = healthEventRepository, + healthProcessor = healthEventProcessor, info = info ) } - private val workManagerNetworkManager: CSWorkManagerNetworkManager by lazy { - CSWorkManagerNetworkManager( - appLifeCycle = appLifeCycle, - networkRepository = CSNetworkRepositoryImpl( - networkConfig = config.networkConfig, - eventService = backgroundEventService, - dispatcher = dispatcher, - timeStampGenerator = timeStampGenerator, - logger = logger, - healthEventRepository = healthEventRepository, - info = info - ), - dispatcher = dispatcher, - logger = logger, - healthEventRepository = healthEventRepository, - info = info, - connectionListener = socketConnectionListener + private val eventRepository: CSEventRepository by lazy { + DefaultCSEventRepository( + eventDataDao = db.eventDataDao() ) } - override val logger: CSLogger by lazy { CSLogger(logLevel) } + private val appLifeCycleObserver: clickstream.lifecycle.CSAppLifeCycle by lazy { + DefaultCSAppLifeCycleObserver(logger) + } - override val eventSchedulerConfig: CSEventSchedulerConfig by lazy { config.eventSchedulerConfig } + override val logger: CSLogger by lazy { + CSLogger(logLevel) + } - override val foregroundNetworkManager: CSNetworkManager by lazy { - CSNetworkManager( - appLifeCycle = appLifeCycle, - networkRepository = networkRepository, + override val healthEventProcessor: CSHealthEventProcessor? = healthGateway.healthEventProcessor + + private val backgroundNetworkManager: CSNetworkManager = CSBackgroundNetworkManager( + appLifeCycleObserver = appLifeCycleObserver, + networkRepository = CSNetworkRepositoryImpl( + networkConfig = config.networkConfig, + eventService = eventService, dispatcher = dispatcher, + timeStampGenerator = timeStampGenerator, logger = logger, - healthEventRepository = healthEventRepository, - info = info, - connectionListener = socketConnectionListener - ) - } + healthProcessor = healthEventProcessor, + info = info + ), + dispatcher = dispatcher, + logger = logger, + healthEventProcessor = healthEventProcessor, + info = info, + connectionListener = socketConnectionListener, + csReportDataTracker = csReportDataTracker, + eventListeners = eventListeners + ) - override val backgroundEventScheduler: CSBackgroundEventScheduler by lazy { - CSBackgroundEventScheduler( - appLifeCycle = appLifeCycle, + override val networkManager: CSNetworkManager by lazy { + CSNetworkManager( + appLifeCycleObserver = appLifeCycleObserver, + networkRepository = networkRepository, dispatcher = dispatcher, - guIdGenerator = guidGenerator, - timeStampGenerator = timeStampGenerator, - batteryStatusObserver = batteryStatusObserver, - networkStatusObserver = networkStatusObserver, - eventListeners = eventListener, + logger = logger, healthEventProcessor = healthEventProcessor, info = info, - eventRepository = eventRepository, - healthEventRepository = healthEventRepository, - logger = logger, - networkManager = foregroundNetworkManager + connectionListener = socketConnectionListener, + csReportDataTracker = csReportDataTracker, + csEventListeners = eventListeners ) } - override val foregroundEventScheduler: CSForegroundEventScheduler by lazy { - CSForegroundEventScheduler( - appLifeCycle = appLifeCycle, - networkManager = foregroundNetworkManager, + override val eventScheduler: CSEventScheduler by lazy { + CSEventScheduler( + appLifeCycleObserver = appLifeCycleObserver, + networkManager = networkManager, config = config.eventSchedulerConfig, logger = logger, eventRepository = eventRepository, - healthEventRepository = healthEventRepository, + healthEventProcessor = healthEventProcessor, dispatcher = dispatcher, guIdGenerator = guidGenerator, timeStampGenerator = timeStampGenerator, batteryStatusObserver = batteryStatusObserver, networkStatusObserver = networkStatusObserver, info = info, - eventHealthListener = eventHealthListener, - eventListeners = eventListener - ).also { - // initialise backgroundEventScheduler - backgroundEventScheduler - } - } - - override val workManagerEventScheduler: CSWorkManagerEventScheduler by lazy { - CSWorkManagerEventScheduler( - appLifeCycle = appLifeCycle, - guIdGenerator = guidGenerator, - timeStampGenerator = timeStampGenerator, - batteryStatusObserver = batteryStatusObserver, - networkStatusObserver = networkStatusObserver, - eventListeners = eventListener, - dispatcher = dispatcher, - healthEventProcessor = healthEventProcessor, - backgroundLifecycleManager = backgroundLifecycleManager, - info = info, - eventRepository = eventRepository, - healthEventRepository = healthEventRepository, - logger = logger, - networkManager = workManagerNetworkManager + eventListeners = eventListeners, + errorListener = eventSchedulerErrorListener, + csReportDataTracker = csReportDataTracker, + batchSizeRegulator = batchSizeStrategy, + socketConnectionManager = socketConnectionManager, + remoteConfig = remoteConfig, + csHealthGateway = healthGateway ) } override val eventProcessor: CSEventProcessor by lazy { CSEventProcessor( config = config.eventProcessorConfiguration, - eventScheduler = foregroundEventScheduler, + eventScheduler = eventScheduler, dispatcher = dispatcher, - healthEventRepository = healthEventRepository, logger = logger, - info = info ) } + private val batchSizeStrategy: CSEventBatchSizeStrategy by lazy { + EventByteSizeBasedBatchStrategy(logger, batchSizeSharedPref) + } + + private val batchSizeSharedPref: CSBatchSizeSharedPref by lazy { + DefaultCSBatchSizeSharedPref(context) + } + override val workManager: CSWorkManager by lazy { CSWorkManager( - appLifeCycle = appLifeCycle, + appLifeCycleObserver = appLifeCycleObserver, context = context, eventSchedulerConfig = config.eventSchedulerConfig, logger = logger, - remoteConfig = remoteConfig + remoteConfig = remoteConfig, ) } + override val backgroundScheduler: CSBackgroundScheduler = CSBackgroundScheduler( + appLifeCycleObserver = appLifeCycleObserver, + networkManager = backgroundNetworkManager, + config = config.eventSchedulerConfig, + logger = logger, + eventRepository = eventRepository, + healthProcessor = healthEventProcessor, + dispatcher = dispatcher, + guIdGenerator = guidGenerator, + timeStampGenerator = timeStampGenerator, + csSocketConnectionManager = socketConnectionManager, + batteryStatusObserver = batteryStatusObserver, + networkStatusObserver = networkStatusObserver, + info = info, + eventListeners = eventListeners, + errorListener = eventSchedulerErrorListener, + csReportDataTracker = csReportDataTracker, + batchSizeRegulator = batchSizeStrategy, + remoteConfig = remoteConfig, + batchSizeSharedPref = batchSizeSharedPref, + csHealthGateway = healthGateway + ) + + override val eventSchedulerConfig: CSEventSchedulerConfig by lazy { + config.eventSchedulerConfig + } + private inline fun Scarlet.Builder.apply(): T { + val okHttpClient = if (config.networkConfig.okHttpClient != null) { + logger.debug { "DefaultCSServiceLocator#connectionUnauth - false" } + config.networkConfig.okHttpClient + } else { + logger.debug { "DefaultCSServiceLocator#connectionUnauth - true" } + OkHttpClient.Builder() + .addInterceptor { chain -> + val request = chain.request() + val newRequest = request.newBuilder().apply { + config.networkConfig.headers.forEach { + header(it.key, it.value) + } + }.build() + + newRequest.headers().names().forEach { name -> + val v = newRequest.header(name) + logger.debug { "Header -> $name : $v" } + } + chain.proceed(newRequest) + } + .writeTimeout(config.networkConfig.writeTimeout, TimeUnit.SECONDS) + .readTimeout(config.networkConfig.readTimeout, TimeUnit.SECONDS) + .connectTimeout(config.networkConfig.connectTimeout, TimeUnit.SECONDS) + .pingInterval(config.networkConfig.pingInterval, TimeUnit.SECONDS) + .build() + } + return with(config.networkConfig) { webSocketFactory(okHttpClient.newWebSocketFactory(endPoint)) .addStreamAdapterFactory(CSFlowStreamAdapterFactory()) @@ -258,4 +284,4 @@ internal class DefaultCServiceLocator( .create() } } -} +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSEventProcessor.kt b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSEventProcessor.kt index 49fd8493..1e2d8143 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSEventProcessor.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSEventProcessor.kt @@ -1,36 +1,31 @@ package clickstream.internal.eventprocessor +import clickstream.CSEvent import clickstream.api.CSInfo import clickstream.config.CSEventProcessorConfig -import clickstream.extension.protoName -import clickstream.health.constant.CSEventNamesConstant import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.internal.eventscheduler.CSForegroundEventScheduler +import clickstream.health.model.CSHealthEvent +import clickstream.internal.CSEventInternal +import clickstream.protoName +import clickstream.internal.eventscheduler.CSEventScheduler import clickstream.logger.CSLogger -import clickstream.model.CSEvent -import java.util.Locale import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi /** * This is responsible for ingesting analytics events generated by app,adding relevant keys, - * and forwarding the events to [CSForegroundEventScheduler]. + * and forwarding the events to [CSEventScheduler]. * * @param eventScheduler used for scheduling events * @param dispatcher used for dispatching events serially * @param logger used for logging - * @param healthEventRepository used for tracking health events */ @ExperimentalCoroutinesApi internal class CSEventProcessor( private val config: CSEventProcessorConfig, - private val eventScheduler: CSForegroundEventScheduler, + private val eventScheduler: CSEventScheduler, private val dispatcher: CoroutineDispatcher, private val logger: CSLogger, - private val healthEventRepository: CSHealthEventRepository, - private val info: CSInfo ) { /** @@ -38,32 +33,25 @@ internal class CSEventProcessor( * * @param event [CSEvent] which holds guid, timestamp and message */ - suspend fun trackEvent(event: CSEvent) { + suspend fun trackEvent(event: CSEventInternal) { logger.debug { "CSEventProcessor#trackEvent" } - recordHealthEvent( - eventName = CSEventNamesConstant.Flushed.ClickStreamEventReceived.value, - eventGuid = event.guid - ) - recordHealthEvent( - eventName = CSEventNamesConstant.Flushed.ClickStreamEventReceivedForDropRate.value, - eventGuid = event.guid.plus("_").plus(event.message.protoName().toLowerCase(Locale.getDefault())) - ) - val eventName = event.message.protoName() - when { - config.realtimeEvents.contains(eventName) -> eventScheduler.scheduleEvent(event) - config.instantEvent.contains(eventName) -> eventScheduler.sendInstantEvent(event) - else -> eventScheduler.scheduleEvent(event) + val (eventGuid, eventName) = when (event) { + is CSEventInternal.CSEvent -> event.guid to event.message.protoName() + is CSEventInternal.CSBytesEvent -> event.guid to event.eventName } - } - private suspend fun recordHealthEvent(eventName: String, eventGuid: String) { - healthEventRepository.insertHealthEvent( - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = eventGuid, - appVersion = info.appInfo.appVersion - ) - ) + logger.debug { "CSEventProcessor#trackEvent" } + + when { + config.realtimeEvents.contains(eventName) -> { + eventScheduler.scheduleEvent(event) + } + config.instantEvent.contains(eventName) -> { + eventScheduler.sendInstantEvent(event) + } + else -> { + eventScheduler.scheduleEvent(event) + } + } } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSMetaProvider.kt b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSMetaProvider.kt new file mode 100644 index 00000000..62dbe550 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/CSMetaProvider.kt @@ -0,0 +1,49 @@ +package clickstream.internal.eventprocessor + +import com.gojek.clickstream.internal.HealthMeta.App +import com.gojek.clickstream.internal.HealthMeta.Customer +import com.gojek.clickstream.internal.HealthMeta.Device +import com.gojek.clickstream.internal.HealthMeta.Location +import com.gojek.clickstream.internal.HealthMeta.Session + +/** + * This data source is responsible for providing values for those common keys + * so that the can be added to each event before being passed to the scheduler. + */ +internal interface CSMetaProvider { + + /** + * Fetches and returns values for location properties. + * + * @return [Location] + */ + suspend fun location(): Location + + /** + * Fetches and returns values for customer properties. + * + * @return [Customer] + */ + val customer: Customer + + /** + * Fetches and returns values for app properties. + * + * @return [App] + */ + val app: App + + /** + * Fetches and returns values for device properties. + * + * @return [Device] + */ + val device: Device + + /** + * Fetches and returns values for current session related properties. + * + * @return [Session] + */ + val session: Session +} diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProvider.kt b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProvider.kt index 566e9f21..34ee8cd1 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProvider.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProvider.kt @@ -2,7 +2,7 @@ package clickstream.internal.eventprocessor.impl import clickstream.api.CSInfo import clickstream.api.CSLocationInfo -import clickstream.api.CSMetaProvider +import clickstream.internal.eventprocessor.CSMetaProvider import com.gojek.clickstream.internal.HealthMeta.App import com.gojek.clickstream.internal.HealthMeta.Customer import com.gojek.clickstream.internal.HealthMeta.Device @@ -14,7 +14,7 @@ import com.gojek.clickstream.internal.HealthMeta.Session * * @param info contains data for location, device, customer, session */ -public class DefaultCSMetaProvider( +internal class DefaultCSMetaProvider( private val info: CSInfo ) : CSMetaProvider { diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventScheduler.kt deleted file mode 100644 index b2d898c8..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventScheduler.kt +++ /dev/null @@ -1,164 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.api.CSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.listener.CSEventListener -import clickstream.logger.CSLogger -import clickstream.model.CSEvent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch - -/** - * The [CSBackgroundEventScheduler] only mean to save flushed events for both health and non-health - * events to the EventData table whenever app goes to background. - * - * **Sequence Diagram** - * ``` - * ┌────────────────┐ ┌────────────────┐ ┌─────────────────────────────┐ ┌───────────────────────── ┌───────────────────────┐ ┌──────────────────────────┐ - * │ Application │ │ Clickstream │ │ CSBackgroundEventScheduler │ │ CSBaseEventScheduler │ │ CSEventRepository │ │ CSHealthEventRepository │ - * └───────┬────────┘ └───────┬────────┘ └─────────────┬───────────────┘ └───────────┬─────────────┘ └──────────┬────────────┘ └───────────┬──────────────┘ - * │ │ │ │ │ │ - * │ │ App goes │ │ │ │ │ - * │ │ to Background │ │ │ │ │ - * │ │ ------------------> │ │ onStop() │ │ │ │ - * │ │ │ ---------------------> │ │ flushAllEvents() │ │ │ - * │ │ │ │ --------------------------------------------------------> │ │ getAllEvents() - │ - * │ │ │ │ │ │ │ │ │ - * │ │ │ │ <-------------------------------------------------------- │ │ <-------------- │ - * │ │ │ │ if notEmpty │ │ │ - * │ │ │ │ forwardEventsFromBackground()│ │ insertEventDataList ----> │ │ inserted() │ - * │ │ │ │ else │ │ │ - * │ │ │ │ doNothing │ │ │ - * │ │ │ │ │ │ - * │ │ │ │ flushHealthEvents() │ │ │ - * │ │ │ │ ----------------------------------------------------------------------------------------> │ │ getAggregateEvents() + getInstantEvents() - - * │ │ │ │ │ │ │ │ │ - * │ │ │ │ <---------------------------------------------------------------------------------------- │ │ <----------------------------------------- - * │ │ │ │ if notEmpty │ │ │ - * │ │ │ │ forwardEventsFromBackground()│ │ insertHealthEvent() -----------------------------------> │ │ inserted() - * │ │ │ │ else │ │ │ - * │ │ │ │ doNothing │ │ │ - * │ │ │ │ │ │ - *``` - */ -@ExperimentalCoroutinesApi -internal class CSBackgroundEventScheduler( - appLifeCycle: CSAppLifeCycle, - guIdGenerator: CSGuIdGenerator, - timeStampGenerator: CSTimeStampGenerator, - batteryStatusObserver: CSBatteryStatusObserver, - networkStatusObserver: CSNetworkStatusObserver, - eventListeners: List, - networkManager: CSNetworkManager, - private val healthEventProcessor: CSHealthEventProcessor, - private val info: CSInfo, - private val eventRepository: CSEventRepository, - private val healthEventRepository: CSHealthEventRepository, - private val logger: CSLogger, - dispatcher: CoroutineDispatcher -) : CSBaseEventScheduler( - appLifeCycle, - dispatcher, - networkManager, - eventRepository, - healthEventRepository, - logger, - guIdGenerator, - timeStampGenerator, - batteryStatusObserver, - networkStatusObserver, - info, - eventListeners -) { - - private val job = SupervisorJob() - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSBackgroundEventScheduler = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } - - init { - logger.debug { "CSBackgroundEventScheduler#init" } - addObserver() - } - - override fun onStart() { - logger.debug { "CSBackgroundEventScheduler#onStart" } - cancelJob() - } - - override fun onStop() { - logger.debug { "CSBackgroundEventScheduler#onStop" } - coroutineScope.launch(coroutineExceptionHandler) { - flushEvents() - } - } - - fun cancelJob() { - logger.debug { "CSBackgroundEventScheduler#cancelJob" } - job.cancelChildren() - } - - private suspend fun flushEvents() { - logger.debug { "CSBackgroundEventScheduler#flushEvents" } - - flushAllEvents() - flushHealthEvents() - } - - private suspend fun flushAllEvents() { - logger.debug { "CSBackgroundEventScheduler#flushAllEvents" } - - val events = eventRepository.getAllEvents() - if (events.isEmpty()) return - - forwardEventsFromBackground(batch = events) - CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamFlushOnBackground.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = events.joinToString { event -> event.eventGuid }, - appVersion = info.appInfo.appVersion - ).let { healthEventRepository.insertHealthEvent(it) } - } - - private suspend fun flushHealthEvents() { - logger.debug { "CSBackgroundEventScheduler#flushHealthEvents" } - - val aggregateEvents = healthEventProcessor.getAggregateEvents() - val instantEvents = healthEventProcessor.getInstantEvents() - val healthEvents = (aggregateEvents + instantEvents).map { health -> - CSEvent( - guid = health.healthMeta.eventGuid, - timestamp = health.eventTimestamp, - message = health - ) - }.map { CSEventData.create(it).first } - - logger.debug { "CSWorkManagerEventScheduler#flushHealthEvents - healthEvents size ${healthEvents.size}" } - - if (healthEvents.isEmpty()) return - forwardEventsFromBackground(healthEvents) - } -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundScheduler.kt new file mode 100644 index 00000000..d6be8aa8 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBackgroundScheduler.kt @@ -0,0 +1,203 @@ +package clickstream.internal.eventscheduler + +import clickstream.CSEvent +import clickstream.api.CSInfo +import clickstream.config.CSEventSchedulerConfig +import clickstream.config.CSRemoteConfig +import clickstream.health.CSHealthGateway +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.constant.CSHealthEventName +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.model.CSHealthEvent +import clickstream.internal.db.CSBatchSizeSharedPref +import clickstream.internal.networklayer.CSNetworkManager +import clickstream.internal.networklayer.socket.CSSocketConnectionManager +import clickstream.internal.utils.CSBatteryStatusObserver +import clickstream.internal.utils.CSGuIdGenerator +import clickstream.internal.utils.CSNetworkStatusObserver +import clickstream.internal.utils.CSTimeStampGenerator +import clickstream.lifecycle.CSAppLifeCycle +import clickstream.listener.CSEventListener +import clickstream.logger.CSLogger +import clickstream.report.CSReportDataTracker +import com.gojek.clickstream.internal.Health +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect + +private const val TIMEOUT: Int = 10000 +private const val ONE_SEC: Long = 1000 + +/** + * The BackgroundEventScheduler acts an bridge between the BackgroundWorker & NetworkManager + * + * It flushes all the events present in database + */ +@ExperimentalCoroutinesApi +internal open class CSBackgroundScheduler( + appLifeCycleObserver: CSAppLifeCycle, + networkManager: CSNetworkManager, + dispatcher: CoroutineDispatcher, + config: CSEventSchedulerConfig, + eventRepository: CSEventRepository, + logger: CSLogger, + guIdGenerator: CSGuIdGenerator, + timeStampGenerator: CSTimeStampGenerator, + batteryStatusObserver: CSBatteryStatusObserver, + networkStatusObserver: CSNetworkStatusObserver, + private val info: CSInfo, + eventListeners: List, + errorListener: CSEventSchedulerErrorListener, + csReportDataTracker: CSReportDataTracker?, + batchSizeRegulator: CSEventBatchSizeStrategy, + csHealthGateway: CSHealthGateway, + private val csSocketConnectionManager: CSSocketConnectionManager, + private val remoteConfig: CSRemoteConfig, + private val batchSizeSharedPref: CSBatchSizeSharedPref, + private val healthProcessor: CSHealthEventProcessor? +) : CSEventScheduler( + appLifeCycleObserver, + networkManager, + dispatcher, + config, + eventRepository, + healthProcessor, + logger, + guIdGenerator, + timeStampGenerator, + batteryStatusObserver, + networkStatusObserver, + info, + eventListeners, + errorListener, + csReportDataTracker, + batchSizeRegulator, + csSocketConnectionManager, + remoteConfig, + csHealthGateway +) { + + override val tag: String + get() = "CSBackgroundScheduler" + + override fun onStart() { + logger.debug { "$tag#onStart" } + } + + override fun onStop() { + logger.debug { "$tag#onStop - backgroundTaskEnabled ${config.backgroundTaskEnabled}" } + job = SupervisorJob() + coroutineScope = CoroutineScope(job + dispatcher) + setupObservers() + } + + /** + * Flushes all the events present in database and + * waits for ack + */ + suspend fun sendEvents(): Boolean { + logger.debug { "$tag#sendEvents" } + csSocketConnectionManager.connect() + if (!waitForNetwork()) { + pushEventsToUpstream() + return false + } + flushAllEvents() + waitForAck() + flushHealthEvents() + pushEventsToUpstream() + return eventRepository.getEventCount() == 0 + } + + /** + * Terminates lifecycle, hence closes socket connection + */ + fun terminate() { + logger.debug { "$tag#terminate" } + coroutineScope.cancel() + csSocketConnectionManager.disconnect() + } + + private suspend fun flushAllEvents() { + logger.debug { "$tag#flushAllEvents" } + + if (remoteConfig.batchFlushedEvents) { + while (eventRepository.getAllUnprocessedEventsCount() != 0) { + val events = + eventRepository.getUnprocessedEventsWithLimit(batchSizeSharedPref.getSavedBatchSize()) + logger.debug { "$tag#Batched flushing batch size: ${events.size} " } + val reqId = forwardEvents(batch = events, forFlushing = true) + logFlushHealthEvent(reqId, events) + } + } else { + val events = eventRepository.getAllUnprocessedEvents() + if (events.isEmpty()) return + val reqId = forwardEvents(batch = events, forFlushing = true) + logFlushHealthEvent(reqId, events) + } + } + + private suspend fun logFlushHealthEvent(reqId: String?, events: List) { + logger.debug { "$tag#logFlushHealthEvent $reqId" } + if (reqId != null && healthProcessor != null) { + val healthEvent = CSHealthEvent( + eventName = CSHealthEventName.ClickStreamFlushOnBackground.value, + eventType = CSEventTypesConstant.AGGREGATE, + appVersion = info.appInfo.appVersion, + error = if (remoteConfig.batchFlushedEvents) "Flushed ${events.size} events with batching." else "Flushed ${events.size} events.", + count = events.size + ) + healthProcessor.insertBatchEvent( + healthEvent, + events.map { it.toCSEventForHealth(reqId) }) + } + } + + private suspend fun flushHealthEvents() { + healthProcessor?.getHealthEventFlow(CSEventTypesConstant.AGGREGATE)?.collect { + val healthMappedEvent = it.map { health -> + CSEvent( + guid = health.healthMeta.eventGuid, + timestamp = health.eventTimestamp, + message = health + ) + }.map { CSEventData.create(it) } + logger.debug { "$tag#flushHealthEvents - healthEvents size ${healthMappedEvent.size}" } + if (healthMappedEvent.isNotEmpty()) { + forwardEvents(healthMappedEvent, forFlushing = true) + } + } + } + + private suspend fun pushEventsToUpstream() { + healthProcessor?.pushEventToUpstream(CSEventTypesConstant.AGGREGATE, true) + } + + private suspend fun waitForAck() { + logger.debug { "$tag#waitForAck" } + + var timeElapsed = 0 + while (eventRepository.getEventCount() != 0 && timeElapsed <= TIMEOUT) { + delay(ONE_SEC) + timeElapsed += ONE_SEC.toInt() + } + } + + private suspend fun waitForNetwork(): Boolean { + logger.debug { "$tag#waitForNetwork" } + + var timeout = TIMEOUT + while (!networkManager.isSocketAvailable() && timeout > 0) { + logger.debug { "$tag#waitForNetwork - Waiting for socket to be open" } + + delay(ONE_SEC) + timeout -= ONE_SEC.toInt() + } + + return networkManager.isSocketAvailable() + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBaseEventScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBaseEventScheduler.kt deleted file mode 100644 index 3903ed33..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSBaseEventScheduler.kt +++ /dev/null @@ -1,308 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.api.CSInfo -import clickstream.extension.eventName -import clickstream.extension.protoName -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.analytics.CSErrorReasons -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryLevel -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.internal.utils.CSResult -import clickstream.internal.utils.CSTimeStampMessageBuilder -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSLifeCycleManager -import clickstream.listener.CSEventListener -import clickstream.listener.CSEventModel -import clickstream.logger.CSLogger -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.internal.Health -import com.google.protobuf.MessageLite -import java.util.concurrent.CopyOnWriteArrayList -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.coroutines.coroutineContext - -@OptIn(ExperimentalCoroutinesApi::class) -internal open class CSBaseEventScheduler( - appLifeCycle: CSAppLifeCycle, - dispatcher: CoroutineDispatcher, - private val networkManager: CSNetworkManager, - private val eventRepository: CSEventRepository, - private val healthEventRepository: CSHealthEventRepository, - private val logger: CSLogger, - private val guIdGenerator: CSGuIdGenerator, - private val timeStampGenerator: CSTimeStampGenerator, - private val batteryStatusObserver: CSBatteryStatusObserver, - private val networkStatusObserver: CSNetworkStatusObserver, - private val info: CSInfo, - private val eventListeners: List -) : CSLifeCycleManager(appLifeCycle) { - - private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - @Volatile - protected var eventData: List = CopyOnWriteArrayList() - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSBackgroundEventScheduler = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } - - init { - logger.debug { "CSBaseEventScheduler#init" } - } - - override fun onStart() { - logger.debug { "CSBaseEventScheduler#onStart" } - } - - override fun onStop() { - logger.debug { "CSBaseEventScheduler#onStop" } - } - - /** - * forwardEventsFromBackground not sending transformed event to the backend - * we only save the the transformed events to EventData table. - * - * The transformed events that we save to EventData table, will be get collect and send - * to the backend whenever app goes to foreground. - */ - protected suspend fun forwardEventsFromBackground(batch: List): String? { - logger.debug { "CSBaseEventScheduler#forwardEventsFromBackground" } - - dispatchToEventListener { - batch.map { csEvent -> - CSEventModel.Event.Dispatched( - eventId = csEvent.eventGuid, - eventName = getEventOrProtoName(csEvent.event()), - productName = csEvent.event().protoName(), - timeStamp = csEvent.eventTimeStamp - ) - } - } - - val eventRequest = transformToEventRequest(eventData = batch) - recordEventBatchCreated(batch, eventRequest) - - logger.debug { "CSBaseEventScheduler#forwardEventsFromBackground : event size before inserted ${eventRepository.getAllEvents().size}" } - updateEventsGuidAndInsertToDb(eventRequest, batch) - logger.debug { "CSBaseEventScheduler#forwardEventsFromBackground : event size after inserted ${eventRepository.getAllEvents().size}" } - return eventRequest.reqGuid - } - - /** - * Processes the batch and converts into EventRequest and then - * forwards it to the NetworkManager - */ - protected suspend fun forwardEvents(batch: List): String? { - logger.debug { "CSBaseEventScheduler#forwardEvents" } - - if (batteryStatusObserver.getBatteryStatus() == CSBatteryLevel.LOW_BATTERY) { - logger.debug { "CSBaseEventScheduler#forwardEvents : battery is low" } - return isBatteryLow() - } - if (networkStatusObserver.isNetworkAvailable().not()) { - logger.debug { "CSBaseEventScheduler#forwardEvents : network is not available" } - return isNetworkAvailable() - } - if (networkManager.isSocketConnected().not()) { - logger.debug { "CSBaseEventScheduler#forwardEvents : socket is not connected" } - return isSocketConnected() - } - dispatchToEventListener { - batch.map { csEvent -> - CSEventModel.Event.Dispatched( - eventId = csEvent.eventGuid, - eventName = getEventOrProtoName(csEvent.event()), - productName = csEvent.event().protoName(), - timeStamp = csEvent.eventTimeStamp - ) - } - } - - val eventRequest = transformToEventRequest(eventData = batch) - val eventGuids = recordEventBatchCreated(batch, eventRequest) - networkManager.processEvent(eventRequest = eventRequest, eventGuids = eventGuids) - updateEventsGuidAndInsertToDb(eventRequest, batch) - return eventRequest.reqGuid - } - - protected suspend fun runEventGuidCollector() { - logger.debug { "CSBaseEventScheduler#runEventGuidCollector" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSBaseEventScheduler#runEventGuidCollector : coroutine is not active" } - return - } - - suspend fun onCallAckEventListener(requestId: String) { - if (eventListeners.isEmpty()) return - - val currentRequestIdEvents = eventRepository.getEventsOnGuId(requestId) - dispatchToEventListener { - currentRequestIdEvents.map { csEvent -> - CSEventModel.Event.Acknowledged( - eventId = csEvent.eventGuid, - eventName = getEventOrProtoName(csEvent.event()), - productName = csEvent.event().protoName(), - timeStamp = csEvent.eventTimeStamp - ) - } - } - } - - coroutineScope { - launch { - eventRepository.getEventDataList().collect { - logger.debug { "CSBaseEventScheduler#runEventGuidCollector : getEventDataList().collect" } - eventData = it - } - } - - launch { - networkManager.eventGuidFlow.collect { - logger.debug { "CSBaseEventScheduler#runEventGuidCollector : eventGuidFlow.collect" } - - when (it) { - is CSResult.Success -> { - onCallAckEventListener(it.value) - eventRepository.deleteEventDataByGuId(it.value) - logger.debug { "CSBaseEventScheduler#runEventGuidCollector : Event Request sent successfully and deleted from DB: ${it.value}" } - } - is CSResult.Failure -> { - eventRepository.resetOnGoingForGuid(it.value) - logger.debug { "CSBaseEventScheduler#runEventGuidCollector : Event Request failed due to: ${it.exception.message}" } - } - } - } - } - } - } - - protected fun dispatchToEventListener(evaluator: () -> List) { - if (eventListeners.isEmpty()) return - logger.debug { "CSBaseEventScheduler#dispatchToEventListener" } - - coroutineScope.launch(coroutineExceptionHandler) { - eventListeners.forEach { - it.onCall(evaluator()) - } - } - } - - protected suspend fun recordHealthEvent(event: CSHealthEventDTO) { - logger.debug { "CSBaseEventScheduler#logHealthEvent : ${event.eventName}" } - - healthEventRepository.insertHealthEvent(event) - } - - protected fun transformToEventRequest(eventData: List): EventRequest { - logger.debug { "CSBaseEventScheduler#transformToEventRequest" } - - return EventRequest.newBuilder().apply { - reqGuid = guIdGenerator.getId() - sentTime = CSTimeStampMessageBuilder.build(timeStampGenerator.getTimeStamp()) - addAllEvents(eventData.map { it.event() }) - }.build() - } - - protected fun getEventOrProtoName(message: MessageLite): String { - return message.eventName() ?: message.protoName() - } - - private suspend fun updateEventsGuidAndInsertToDb( - eventRequest: EventRequest, - eventData: List - ) { - logger.debug { "CSBaseEventScheduler#updateEventsGuidAndInsertToDb" } - - val updatedList: List = - eventData.map { it.copy(eventRequestGuid = eventRequest.reqGuid, isOnGoing = true) } - eventRepository.insertEventDataList(updatedList) - } - - private suspend fun isBatteryLow(): String? { - recordHealthEvent( - CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchTriggerFailed.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.LOW_BATTERY, - appVersion = info.appInfo.appVersion - ) - ) - return null - } - - private suspend fun isNetworkAvailable(): String? { - recordHealthEvent( - CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchTriggerFailed.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.NETWORK_UNAVAILABLE, - appVersion = info.appInfo.appVersion - ) - ) - return null - } - - private suspend fun isSocketConnected(): String? { - recordHealthEvent( - CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchTriggerFailed.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.SOCKET_NOT_OPEN, - appVersion = info.appInfo.appVersion - ) - ) - return null - } - - private suspend fun recordEventBatchCreated( - batch: List, - eventRequest: EventRequest - ): String { - var eventGuids = "" - if (batch.isNotEmpty() && batch[0].messageName != Health::class.qualifiedName.orEmpty()) { - eventGuids = batch.joinToString { it.eventGuid } - logger.debug { "CSBaseEventScheduler#recordEventBatchCreated#batch eventBatchId : ${eventRequest.reqGuid} eventId : $eventGuids" } - logger.debug { "CSBaseEventScheduler#recordEventBatchCreated#batch : messageName : ${batch[0].messageName}" } - recordHealthEvent( - CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamEventBatchCreated.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventBatchGuid = eventRequest.reqGuid, - eventGuid = eventGuids, - appVersion = info.appInfo.appVersion - ) - ) - } - - return eventGuids - } - - protected fun dispatchConnectionEventToEventListener(isConnected: Boolean) { - if (eventListeners.isNotEmpty()) { - eventListeners.forEach { - it.onCall(listOf(CSEventModel.Connection(isConnected))) - } - } - } -} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventBatchSizeStrategy.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventBatchSizeStrategy.kt new file mode 100644 index 00000000..a29ddc59 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventBatchSizeStrategy.kt @@ -0,0 +1,9 @@ +package clickstream.internal.eventscheduler + +import clickstream.internal.CSEventInternal + +public const val DEFAULT_MIN_EVENT_COUNT: Int = 20 +internal interface CSEventBatchSizeStrategy { + suspend fun regulatedCountOfEventsPerBatch(expectedBatchSize: Int): Int + fun logEvent(event: CSEventInternal) { /*NO-OP*/ } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventData.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventData.kt index 2fe81be0..0563ae4e 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventData.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventData.kt @@ -3,9 +3,10 @@ package clickstream.internal.eventscheduler import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import clickstream.extension.eventName -import clickstream.health.model.CSEventHealth -import clickstream.model.CSEvent +import clickstream.CSEvent +import clickstream.eventName +import clickstream.health.model.CSEventForHealth +import clickstream.internal.CSEventInternal import com.gojek.clickstream.de.Event import com.google.protobuf.ByteString import com.google.protobuf.MessageLite @@ -37,8 +38,14 @@ public data class CSEventData( * * @return [Event] */ - public fun event(): Event { - val messageType = messageName.split(".").last().toLowerCase(Locale.getDefault()) + public fun event(appPrefix: String? = null): Event { + val messageType = with(messageName.split(".").last().lowercase(Locale.getDefault())) { + if (!appPrefix.isNullOrEmpty()) { + "$appPrefix-$this" + } else { + this + } + } return Event.newBuilder().apply { eventBytes = ByteString.copyFrom(messageAsBytes) type = messageType @@ -71,19 +78,20 @@ public data class CSEventData( return result } + internal fun toCSEventForHealth(batchId: String? = null) = + CSEventForHealth(eventGuid = eventGuid, batchGuid = batchId ?: eventRequestGuid ?: "") + public companion object { /** * Creates a new instance of EventData with the given [CSEvent] */ - public fun create(event: CSEvent): Pair { + public fun create(event: CSEvent): CSEventData { val eventGuid: String = event.guid val eventTimeStamp: Long = event.timestamp.seconds val message: MessageLite = event.message val messageAsBytes: ByteArray = message.toByteArray() - val messageSerializedSizeInBytes: Int = message.serializedSize val messageName: String = event.message::class.qualifiedName.orEmpty() - val eventName: String = message.eventName() ?: "" return CSEventData( eventGuid = eventGuid, @@ -92,15 +100,37 @@ public data class CSEventData( messageAsBytes = messageAsBytes, messageName = messageName, isOnGoing = false - ) to CSEventHealth( - eventGuid = eventGuid, - eventTimeStamp = eventTimeStamp, - messageSerializedSizeInBytes = messageSerializedSizeInBytes, - messageName = messageName, - eventName = eventName ) } + /** + * Creates a new instance of EventData with the given [CSEventInternal] + */ + internal fun create(event: CSEventInternal): CSEventData { + when (event) { + is CSEventInternal.CSEvent -> { + return CSEventData( + eventGuid = event.guid, + eventRequestGuid = null, + eventTimeStamp = event.timestamp.seconds, + messageAsBytes = event.message.toByteArray(), + messageName = event.message::class.qualifiedName.orEmpty(), + isOnGoing = false + ) + } + is CSEventInternal.CSBytesEvent -> { + return CSEventData( + eventGuid = event.guid, + eventRequestGuid = null, + eventTimeStamp = event.timestamp.seconds, + messageAsBytes = event.eventData, + messageName = event.eventName, + isOnGoing = false + ) + } + } + } + private const val IN_KB = 1024 } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventDataDao.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventDataDao.kt index d624c469..37511879 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventDataDao.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventDataDao.kt @@ -4,6 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Update import kotlinx.coroutines.flow.Flow /** @@ -88,7 +89,63 @@ internal interface CSEventDataDao { @Query("DELETE FROM EventData WHERE eventRequestGuid = :eventBatchGuId") suspend fun deleteByGuId(eventBatchGuId: String) - + /** + * Load event by request id + * + * @param guid + * @return + */ @Query("SELECT * FROM EventData WHERE eventRequestGuid =:guid") suspend fun loadEventByRequestId(guid: String): List + + /** + * A function [updateAll] that accommodate an action to update a [List] of [T] object to persistence of choice. + * + * **Note:** + * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] + * + * @param eventDataList - List of Event Data to be updated + */ + @Update + suspend fun updateAll(eventDataList: List) + + /** + * A function [getUnprocessedEventsWithLimit] that accommodate an action to retrieve first n events of [T] object from persistence of choice. + * + * **Note:** + * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] + * + */ + @Query("SELECT * FROM EventData where isOnGoing = 0 ORDER BY eventTimeStamp DESC limit:limit") + suspend fun getUnprocessedEventsWithLimit(limit: Int): List + + /** + * A function [getAllUnprocessedEvents] that accommodate an action to retrieve all unprocessed events from DB. + * + * **Note:** + * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] + * + */ + @Query("SELECT * FROM EventData where isOnGoing = 0") + suspend fun getAllUnprocessedEvents(): List + + /** + * A function [getAllUnprocessedEventsCount] that accommodate an action to retrieve the count of all unprocessed events from DB. + * + * **Note:** + * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] + * + */ + @Query("SELECT COUNT(*) FROM EventData where isOnGoing = 0") + suspend fun getAllUnprocessedEventsCount(): Int + + /** + * Returns current count of events. + * + * **Note:** + * Thread switching must be handled by the caller side. e.g wrapped in form of [IO] + * + */ + @Query("SELECT COUNT(*) FROM EventData") + suspend fun getEventCount(): Int } diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventRepository.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventRepository.kt index 523f39f3..8a25a32d 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventRepository.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventRepository.kt @@ -1,7 +1,5 @@ package clickstream.internal.eventscheduler -import kotlinx.coroutines.flow.Flow - /** * The Storage Repository communicates between storage/cache and the Scheduler * It read, writes, deletes the data in the storage @@ -18,11 +16,6 @@ internal interface CSEventRepository { */ suspend fun insertEventDataList(eventDataList: List) - /** - * A function to retrieve all the un processed events in the cache - */ - suspend fun getEventDataList(): Flow> - /** * A function to retrieve all the unprocessed events in the cache */ @@ -46,5 +39,30 @@ internal interface CSEventRepository { /** * A function to retrieve event data based on given batch ID */ - suspend fun getEventsOnGuId(eventBatchGuId: String) : List + suspend fun loadEventsByRequestId(eventBatchGuId: String): List + + /** + * A function to retrieve first n events data + */ + suspend fun getUnprocessedEventsWithLimit(limit: Int): List + + /** + * A function to update all the event data into the DB + */ + suspend fun updateEventDataList(eventDataList: List) + + /** + * A function to return all events with are not ongoing (isOnGoing flag is false) + */ + suspend fun getAllUnprocessedEvents(): List + + /** + * A function to return the count of all events which are not ongoing (isOnGoing flag is false) + */ + suspend fun getAllUnprocessedEventsCount(): Int + + /** + * Returns current count of events in DB + * */ + suspend fun getEventCount(): Int } diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventScheduler.kt new file mode 100644 index 00000000..11e0d554 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventScheduler.kt @@ -0,0 +1,476 @@ +package clickstream.internal.eventscheduler + +import clickstream.CSEvent +import clickstream.api.CSInfo +import clickstream.config.CSEventSchedulerConfig +import clickstream.config.CSRemoteConfig +import clickstream.eventName +import clickstream.health.CSHealthGateway +import clickstream.health.constant.CSErrorConstant +import clickstream.health.constant.CSEventTypesConstant +import clickstream.health.constant.CSHealthEventName +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.model.CSEventForHealth +import clickstream.health.model.CSHealthEvent +import clickstream.internal.CSEventInternal +import clickstream.internal.networklayer.CSNetworkManager +import clickstream.internal.networklayer.socket.CSSocketConnectionManager +import clickstream.internal.utils.CSBatteryLevel +import clickstream.internal.utils.CSBatteryStatusObserver +import clickstream.internal.utils.CSGuIdGenerator +import clickstream.internal.utils.CSNetworkStatusObserver +import clickstream.internal.utils.CSResult +import clickstream.internal.utils.CSTimeStampGenerator +import clickstream.internal.utils.CSTimeStampMessageBuilder +import clickstream.internal.utils.flowableTicker +import clickstream.lifecycle.CSAppLifeCycle +import clickstream.lifecycle.CSLifeCycleManager +import clickstream.listener.CSEventListener +import clickstream.listener.CSEventModel +import clickstream.logger.CSLogger +import clickstream.protoName +import clickstream.toFlatMap +import clickstream.report.CSReportDataTracker +import com.gojek.clickstream.de.EventRequest +import com.gojek.clickstream.internal.Health +import com.google.protobuf.MessageLite +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.cancel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import java.util.concurrent.CopyOnWriteArrayList + +/** + * The EventScheduler acts an bridge between the EventProcessor & NetworkManager + * + * It caches the events delivered to it. Based on the provided configuration, it processes + * the events into batch at regular intervals + * + * @param eventRepository - To cache the data + * @param networkManager - to send the analytic data to server + * @param config - config how the scheduler should process the events + * @param dispatcher - CoroutineDispatcher on which the events are observed + * @param logger - To create logs + * @param healthEventProcessor - Used for logging health events + * @param guIdGenerator - Used for generating a random ID + * @param timeStampGenerator - Used for generating current time stamp + * @param batteryStatusObserver - observes the battery status + */ +@ExperimentalCoroutinesApi +internal open class CSEventScheduler( + appLifeCycleObserver: CSAppLifeCycle, + protected val networkManager: CSNetworkManager, + protected val dispatcher: CoroutineDispatcher, + protected val config: CSEventSchedulerConfig, + protected val eventRepository: CSEventRepository, + protected val healthEventProcessor: CSHealthEventProcessor?, + protected val logger: CSLogger, + private val guIdGenerator: CSGuIdGenerator, + private val timeStampGenerator: CSTimeStampGenerator, + private val batteryStatusObserver: CSBatteryStatusObserver, + private val networkStatusObserver: CSNetworkStatusObserver, + private val info: CSInfo, + private val eventListeners: List, + private val errorListener: CSEventSchedulerErrorListener, + private val csReportDataTracker: CSReportDataTracker?, + private val batchSizeRegulator: CSEventBatchSizeStrategy, + private val socketConnectionManager: CSSocketConnectionManager, + private val remoteConfig: CSRemoteConfig, + private val csHealthGateway: CSHealthGateway, +) : CSLifeCycleManager(appLifeCycleObserver) { + + protected var job: CompletableJob = SupervisorJob() + protected var coroutineScope: CoroutineScope = CoroutineScope(job + dispatcher) + protected val handler = CoroutineExceptionHandler { _, throwable -> + logger.debug { throwable.message.toString() } + errorListener.onError(tag, throwable) + } + + private val appPrefix = config.eventTypePrefix + private var isForegroundFlushCompleted = false + + override val tag: String + get() = "CSEventScheduler" + + init { + logger.debug { "$tag#init" } + addObserver() + } + + override fun onStart() { + csReportDataTracker?.trackMessage(tag, "onStart") + logger.debug { "$tag#onStart" } + job = SupervisorJob() + coroutineScope = CoroutineScope(job + dispatcher) + socketConnectionManager.connect() + setupObservers() + setupTicker() + resetOnGoingData() + } + + override fun onStop() { + csReportDataTracker?.trackMessage(tag, "onStop") + logger.debug { "$tag#onStop" } + socketConnectionManager.disconnect() + coroutineScope.cancel() + } + + /** + * Converts the proto data into [CSEventData] and cache the data + * + * @param event [CSEvent] which holds guid, timestamp and message + */ + open suspend fun scheduleEvent(event: CSEventInternal) { + logger.debug { "$tag#scheduleEvent" } + batchSizeRegulator.logEvent(event) + val (eventName, eventProperties) = when (event) { + is CSEventInternal.CSEvent -> { + getEmptySafeEventName(event.message) to event.message.toFlatMap() + } + is CSEventInternal.CSBytesEvent -> { + event.eventName to mapOf() + } + } + + val eventData = CSEventData.create(event) + eventRepository.insertEventData(eventData) + dispatchToEventListener { + listOf( + CSEventModel.Event.Scheduled( + eventId = eventData.eventGuid, + eventName = eventName, + productName = eventName, + properties = eventProperties, + timeStamp = eventData.eventTimeStamp + ) + ) + } + } + + /** + * Converts the proto data into [CSEventData] and immediately forwards to the network layer + * This acts like Fire and Forget + * + * @param event [CSEvent] which holds guid, timestamp and message + */ + open fun sendInstantEvent(event: CSEventInternal) { + logger.debug { "$tag#sendInstantEvent" } + coroutineScope.launch(handler) { + ensureActive() + val (eventName, eventProperties) = when (event) { + is CSEventInternal.CSEvent -> { + getEmptySafeEventName(event.message) to event.message.toFlatMap() + } + is CSEventInternal.CSBytesEvent -> { + event.eventName to mapOf() + } + } + + val eventData = CSEventData.create(event) + dispatchToEventListener { + listOf( + CSEventModel.Event.Instant( + eventId = eventData.eventGuid, + eventName = eventName, + productName = eventName, + properties = eventProperties, + timeStamp = eventData.eventTimeStamp + ) + ) + } + val eventRequest = + transformToEventRequest(eventData = listOf(eventData)) + networkManager.processInstantEvent(eventRequest) + } + } + + /** + * Sets up the observers to listen the event data and response from network manager + */ + fun setupObservers() { + logger.debug { "$tag#setupObservers" } + + coroutineScope.launch(handler) { + ensureActive() + networkManager.eventGuidFlow.collect { + when (it) { + is CSResult.Success -> { + dispatchSuccessToEventListener(it.value) + eventRepository.deleteEventDataByGuId(it.value) + csReportDataTracker?.trackSuccess(tag, it.value) + logger.debug { + "$tag#setupObservers - " + + "Event Request sent successfully and deleted from DB: ${it.value}" + } + } + is CSResult.Failure -> { + eventRepository.resetOnGoingForGuid(it.value) + csReportDataTracker?.trackFailure(tag, it.value, it.exception) + logger.debug { + "$tag#setupObservers - " + + "Event Request failed due to: ${it.exception.message}" + } + } + } + } + } + } + + private fun setupTicker() { + logger.debug { "$tag#setupTicker" } + + coroutineScope.launch(handler) { + ensureActive() + flowableTicker(initialDelay = 10, delayMillis = config.batchPeriod) + .onEach { + logger.debug { "$tag#setupTicker - tick" } + } + .collect { + val batch = getEventBatchToSendToServer() + if (batch.isEmpty()) { + return@collect + } + + logger.debug { "$tag#setupTicker#collect - batch of ${batch.size} events: $batch" } + + forwardEvents(batch) + } + } + } + + /** + * Processes the batch and converts into EventRequest and then + * forwards it to the NetworkManager + */ + protected open suspend fun forwardEvents( + batch: List, + forFlushing: Boolean = false + ): String? { + + logger.debug { "$tag#forwardEvents" } + + if (isInvalidBatteryLevel(forFlushing)) { + return isBatteryLow(batch) + } + + if (networkStatusObserver.isNetworkAvailable().not()) { + return isNetworkAvailable(batch) + } + + if (!networkManager.isSocketAvailable()) { + return isSocketConnected(batch) + } + + dispatchToEventListener { + batch.map { csEvent -> + CSEventModel.Event.Dispatched( + eventId = csEvent.eventGuid, + eventName = getEmptySafeEventName(csEvent.event(appPrefix)), + productName = csEvent.event(appPrefix).protoName(), + timeStamp = csEvent.eventTimeStamp + ) + } + } + + val eventRequest = + transformToEventRequest(eventData = batch) + if (batch.isNotEmpty() && batch[0].messageName != Health::class.qualifiedName.orEmpty()) { + logger.debug { + "$tag#forwardEvents#batch - " + + "eventBatchId : ${eventRequest.reqGuid}, " + + "eventId : ${batch.joinToString { it.eventGuid }}" + } + logger.debug { + "$tag#forwardEvents#batch - " + + "messageName : ${batch[0].messageName}" + } + csReportDataTracker?.trackDupData(tag, batch) + } + + networkManager.processEvent(eventRequest = eventRequest) + updateEventsGuidAndInsert(eventRequest, batch) + return eventRequest.reqGuid + } + + private suspend fun isInvalidBatteryLevel(forFlushing: Boolean): Boolean { + return if (forFlushing && remoteConfig.ignoreBatteryLvlOnFlush) { + false + } else { + batteryStatusObserver.getBatteryStatus() == CSBatteryLevel.LOW_BATTERY + } + } + + private fun transformToEventRequest(eventData: List): EventRequest { + logger.debug { "$tag#transformToEventRequest" } + + return EventRequest.newBuilder().apply { + reqGuid = guIdGenerator.getId() + sentTime = CSTimeStampMessageBuilder.build(timeStampGenerator.getTimeStamp()) + addAllEvents(eventData.map { it.event(appPrefix) }) + }.build() + } + + private suspend fun updateEventsGuidAndInsert( + eventRequest: EventRequest, + eventData: List + ) { + logger.debug { "CSEventSchedulerDeDup#updateEventsGuidAndInsert" } + + val updatedList = + eventData.map { it.copy(eventRequestGuid = eventRequest.reqGuid, isOnGoing = true) } + .toList() + eventRepository.updateEventDataList(updatedList) + } + + private suspend fun logHealthEvent(event: CSHealthEvent, eventList: List) { + logger.debug { "$tag#logHealthEvent" } + + healthEventProcessor?.insertBatchEvent( + event, + eventList.map { + CSEventForHealth( + eventGuid = it.eventGuid, + batchGuid = it.eventRequestGuid ?: "" + ) + }) + } + + private fun resetOnGoingData() { + logger.debug { "$tag#resetOnGoingData" } + + coroutineScope.launch(handler) { + ensureActive() + val onGoingEvents = eventRepository.getOnGoingEvents() + val data = onGoingEvents.map { it.copy(isOnGoing = false) }.toList() + eventRepository.insertEventDataList(data) + csHealthGateway.clearHealthEventsForVersionChange() + } + } + + private suspend fun isSocketConnected(events: List): String? { + logHealthEvent( + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.SOCKET_NOT_OPEN, + appVersion = info.appInfo.appVersion + ), + events + ) + return null + } + + private suspend fun isNetworkAvailable(events: List): String? { + logHealthEvent( + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.NETWORK_UNAVAILABLE, + appVersion = info.appInfo.appVersion + ), + events + ) + return null + } + + private suspend fun isBatteryLow(events: List): String? { + logHealthEvent( + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.LOW_BATTERY, + appVersion = info.appInfo.appVersion + ), + events + ) + return null + } + + private fun dispatchToEventListener(evaluator: () -> List) { + if (eventListeners.isEmpty()) { + return + } + coroutineScope.launch { + eventListeners.forEach { + it.onCall(evaluator()) + } + } + } + + /** + * Fetched and dispatches events loaded by request id to event listener. + * Since this is called just before we call [eventRepository.deleteEventDataByGuId] + * in [setupObservers], this is a blocking call. + * + * @param requestId + */ + protected suspend fun dispatchSuccessToEventListener(requestId: String) { + if (eventListeners.isEmpty()) { + return + } + val currentRequestIdEvents = eventRepository.loadEventsByRequestId(requestId) + dispatchToEventListener { + currentRequestIdEvents.map { csEvent -> + CSEventModel.Event.Acknowledged( + eventId = csEvent.eventGuid, + eventName = getEmptySafeEventName(csEvent.event(appPrefix)), + productName = csEvent.event(appPrefix).protoName(), + timeStamp = csEvent.eventTimeStamp + ) + } + } + } + + /** + * If eventName is null or empty, this will return the protoName instead. + * + * */ + private fun getEmptySafeEventName(message: MessageLite): String { + val eventName = message.eventName() + return if (eventName.isNullOrEmpty()) { + message.protoName() + } else { + eventName + } + } + + private suspend fun getEventBatchToSendToServer(): List { + return if (shouldFlushInForeground()) { + val batch = eventRepository.getAllUnprocessedEvents() + logger.debug { "$tag#foregroundFlushing: ${batch.size} events" } + isForegroundFlushCompleted = true + logHealthEvent( + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamFlushOnForeground.value, + eventType = CSEventTypesConstant.AGGREGATE, + eventGuid = batch.joinToString { it.eventGuid }, + appVersion = info.appInfo.appVersion, + count = batch.size + ), + batch + ) + batch + } else { + getEventsForBatch() + } + } + + // Do not Flush in foreground if flushing with batching config flag is enabled. + // Flushing with batching is as good as normal batching. + private fun shouldFlushInForeground() = + config.enableForegroundFlushing && !isForegroundFlushCompleted && + networkManager.isSocketAvailable() && !remoteConfig.batchFlushedEvents + + private suspend fun getEventsForBatch(): List { + val countOfEventsPerBatch = + batchSizeRegulator.regulatedCountOfEventsPerBatch(config.eventsPerBatch) + return eventRepository.getUnprocessedEventsWithLimit(countOfEventsPerBatch) + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerErrorListener.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerErrorListener.kt new file mode 100644 index 00000000..2689ae93 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerErrorListener.kt @@ -0,0 +1,14 @@ +package clickstream.internal.eventscheduler + +/** + * A listener class that provides callback for error inside [CSEventScheduler] class. + * */ +public interface CSEventSchedulerErrorListener { + /** + * Callback for when error occurs + * + * @param tag: unique tag for the error. + * @param throwable: Throwable object associated with the error. + */ + public fun onError(tag: String, throwable: Throwable) +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSForegroundEventScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSForegroundEventScheduler.kt deleted file mode 100644 index 82e22d0c..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSForegroundEventScheduler.kt +++ /dev/null @@ -1,243 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.api.CSInfo -import clickstream.config.CSEventSchedulerConfig -import clickstream.extension.eventName -import clickstream.extension.isValidMessage -import clickstream.extension.protoName -import clickstream.extension.toFlatMap -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.internal.utils.flowableTicker -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.listener.CSEventListener -import clickstream.listener.CSEventModel -import clickstream.logger.CSLogger -import clickstream.model.CSEvent -import com.google.protobuf.MessageLite -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.coroutines.coroutineContext - -/** - * The EventScheduler acts an bridge between the EventProcessor & NetworkManager - * - * It caches the events delivered to it. Based on the provided configuration, it processes - * the events into batch at regular intervals - * - * @param eventRepository - To cache the data - * @param networkManager - to send the analytic data to server - * @param config - config how the scheduler should process the events - * @param dispatcher - CoroutineDispatcher on which the events are observed - * @param logger - To create logs - * @param healthEventRepository - Used for logging health events - * @param guIdGenerator - Used for generating a random ID - * @param timeStampGenerator - Used for generating current time stamp - * @param batteryStatusObserver - observes the battery status - */ -@ExperimentalCoroutinesApi -internal class CSForegroundEventScheduler( - appLifeCycle: CSAppLifeCycle, - private val networkManager: CSNetworkManager, - private val dispatcher: CoroutineDispatcher, - private val config: CSEventSchedulerConfig, - private val eventRepository: CSEventRepository, - private val healthEventRepository: CSHealthEventRepository, - private val logger: CSLogger, - private val guIdGenerator: CSGuIdGenerator, - private val timeStampGenerator: CSTimeStampGenerator, - private val batteryStatusObserver: CSBatteryStatusObserver, - networkStatusObserver: CSNetworkStatusObserver, - private val info: CSInfo, - private val eventHealthListener: CSEventHealthListener, - eventListeners: List -) : CSBaseEventScheduler( - appLifeCycle, - dispatcher, - networkManager, - eventRepository, - healthEventRepository, - logger, - guIdGenerator, - timeStampGenerator, - batteryStatusObserver, - networkStatusObserver, - info, - eventListeners -) { - - private val job = SupervisorJob() - private val coroutineScope: CoroutineScope = CoroutineScope(job + dispatcher) - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSForegroundEventScheduler = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } - - init { - logger.debug { "CSForegroundEventScheduler#init" } - addObserver() - } - - override fun onStart() { - logger.debug { "CSForegroundEventScheduler#onStart" } - coroutineScope.launch(coroutineExceptionHandler) { - launch { runEventGuidCollector() } - launch { runTicker() } - launch { runResetOnGoingEvents() } - } - } - - override fun onStop() { - logger.debug { "CSForegroundEventScheduler#onStop" } - cancelJob() - } - - fun cancelJob() { - logger.debug { "CSForegroundEventScheduler#cancelJob" } - job.cancelChildren() - } - - suspend fun scheduleEvent(event: CSEvent) { - logger.debug { "CSForegroundEventScheduler#scheduleEvent ${event.message.eventName()} ${event.message.protoName()}" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSForegroundEventScheduler#scheduleEvent : coroutine is not active" } - return - } - if (validateMessage(event.message).not()) { - logger.debug { "CSForegroundEventScheduler#scheduleEvent : message with name ${event.message.eventName()} is not valid" } - return - } - - val (eventData, eventHealthData) = CSEventData.create(event) - eventHealthListener.onEventCreated(eventHealthData) - eventRepository.insertEventData(eventData) - dispatchToEventListener { - listOf( - CSEventModel.Event.Scheduled( - eventId = eventData.eventGuid, - eventName = getEventOrProtoName(event.message), - productName = event.message.protoName(), - properties = event.message.toFlatMap(), - timeStamp = eventData.eventTimeStamp, - ) - ) - } - - recordHealthEvent( - CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamEventCached.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = eventData.eventGuid, - appVersion = info.appInfo.appVersion - ) - ) - } - - suspend fun sendInstantEvent(event: CSEvent) { - logger.debug { "CSForegroundEventScheduler#sendInstantEvent ${event.message.eventName()} ${event.message.protoName()}" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSForegroundEventScheduler#sendInstantEvent : coroutine is not active" } - return - } - if (validateMessage(event.message).not()) { - logger.debug { "CSForegroundEventScheduler#sendInstantEvent : message not valid ${event.message.eventName()}" } - return - } - val (eventData, eventHealthData) = CSEventData.create(event) - eventHealthListener.onEventCreated(eventHealthData) - dispatchToEventListener { - listOf( - CSEventModel.Event.Instant( - eventId = eventData.eventGuid, - eventName = getEventOrProtoName(event.message), - productName = event.message.protoName(), - properties = event.message.toFlatMap(), - timeStamp = eventData.eventTimeStamp, - ) - ) - } - val eventRequest = transformToEventRequest(eventData = listOf(eventData)) - networkManager.processInstantEvent(eventRequest) - } - - private suspend fun runTicker() { - logger.debug { "CSForegroundEventScheduler#runTicker" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSForegroundEventScheduler#setupTicker : coroutine is not active" } - return - } - - flowableTicker(initialDelay = 10, delayMillis = config.batchPeriod) - .onEach { - logger.debug { "CSForegroundEventScheduler#setupTicker : tick" } - dispatchConnectionEventToEventListener(networkManager.isSocketConnected()) - } - .catch { - logger.error { "CSForegroundEventScheduler#setupTicker : catch ${it.message}" } - } - .collect { - if (eventData.isEmpty()) { - return@collect - } - val batch = when { - eventData.isEmpty() -> emptyList() - isEventLessThanBatchCount() -> eventData - else -> eventData.subList(0, config.eventsPerBatch) - } - logger.debug { "CSForegroundEventScheduler#runTicker#collect : event batch size ${batch.size}" } - - forwardEvents(batch) - } - } - - private suspend fun runResetOnGoingEvents() { - logger.debug { "CSForegroundEventScheduler#resetOnGoingData" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSForegroundEventScheduler#resetOnGoingData : coroutine is not active" } - return - } - - val onGoingEvents = eventRepository.getOnGoingEvents() - val data: List = onGoingEvents.map { it.copy(isOnGoing = false) } - if (data.isNotEmpty()) { - eventRepository.insertEventDataList(data) - } - } - - private fun isEventLessThanBatchCount(): Boolean { - return config.eventsPerBatch > eventData.size - } - - private fun validateMessage(message: MessageLite): Boolean { - logger.debug { "CSForegroundEventScheduler#validateMessage : ${message.eventName()} ${message.protoName()}" } - - return config.utf8ValidatorEnabled && message.isValidMessage() - } -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSWorkManagerEventScheduler.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSWorkManagerEventScheduler.kt deleted file mode 100644 index e2ee7add..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/CSWorkManagerEventScheduler.kt +++ /dev/null @@ -1,142 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.api.CSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSBackgroundLifecycleManager -import clickstream.listener.CSEventListener -import clickstream.logger.CSLogger -import clickstream.model.CSEvent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay - -private const val TIMEOUT: Int = 5000 -private const val ONE_SEC: Long = 1000 - -@ExperimentalCoroutinesApi -internal class CSWorkManagerEventScheduler( - appLifeCycle: CSAppLifeCycle, - guIdGenerator: CSGuIdGenerator, - timeStampGenerator: CSTimeStampGenerator, - batteryStatusObserver: CSBatteryStatusObserver, - networkStatusObserver: CSNetworkStatusObserver, - eventListeners: List, - dispatcher: CoroutineDispatcher, - private val healthEventProcessor: CSHealthEventProcessor, - private val backgroundLifecycleManager: CSBackgroundLifecycleManager, - private val info: CSInfo, - private val eventRepository: CSEventRepository, - private val healthEventRepository: CSHealthEventRepository, - private val logger: CSLogger, - private val networkManager: CSNetworkManager -) : CSBaseEventScheduler( - appLifeCycle, - dispatcher, - networkManager, - eventRepository, - healthEventRepository, - logger, - guIdGenerator, - timeStampGenerator, - batteryStatusObserver, - networkStatusObserver, - info, - eventListeners -) { - - init { - logger.debug { "CSWorkManagerEventScheduler#init" } - } - - override fun onStart() { - logger.debug { "CSWorkManagerEventScheduler#onStart" } - } - - override fun onStop() { - logger.debug { "CSWorkManagerEventScheduler#onStop" } - } - - suspend fun sendEvents(): Boolean { - logger.debug { "CSWorkManagerEventScheduler#sendEvents" } - - backgroundLifecycleManager.onStart() - runEventGuidCollector() - waitForNetwork() - flushAllEvents() - waitForAck() - flushHealthEvents() - waitForAck() - backgroundLifecycleManager.onStop() - return eventRepository.getAllEvents().isEmpty() - } - - // we sending to backend - // if we get ack success, we remove the event from the EventData table - private suspend fun flushAllEvents() { - logger.debug { "CSWorkManagerEventScheduler#flushAllEvents" } - - val events = eventRepository.getAllEvents() - if (events.isEmpty()) return - - forwardEvents(batch = events) - CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamFlushOnBackground.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = events.joinToString { event -> event.eventGuid }, - appVersion = info.appInfo.appVersion - ).let { healthEventRepository.insertHealthEvent(it) } - } - - // we sending to backend - // if we get ack success, we remove the event from the EventData table - private suspend fun flushHealthEvents() { - logger.debug { "CSWorkManagerEventScheduler#flushHealthEvents" } - - val aggregateEvents = healthEventProcessor.getAggregateEvents() - val instantEvents = healthEventProcessor.getInstantEvents() - val healthEvents = (aggregateEvents + instantEvents).map { health -> - CSEvent( - guid = health.healthMeta.eventGuid, - timestamp = health.eventTimestamp, - message = health - ) - }.map { CSEventData.create(it).first } - - logger.debug { "CSWorkManagerEventScheduler#flushHealthEvents - healthEvents size ${healthEvents.size}" } - if (healthEvents.isNotEmpty()) { - forwardEvents(healthEvents) - } - } - - private suspend fun waitForAck() { - logger.debug { "CSWorkManagerEventScheduler#waitForAck" } - - var timeElapsed = 0 - while (eventRepository.getAllEvents().isNotEmpty() && timeElapsed <= TIMEOUT) { - delay(ONE_SEC) - timeElapsed += ONE_SEC.toInt() - } - } - - private suspend fun waitForNetwork() { - logger.debug { "CSWorkManagerEventScheduler#waitForNetwork" } - - var timeout = TIMEOUT - while (!networkManager.isSocketConnected() && timeout > 0) { - logger.debug { "CSWorkManagerEventScheduler#waitForNetwork - Waiting for socket to be open" } - - delay(ONE_SEC) - timeout -= ONE_SEC.toInt() - } - } -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepository.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepository.kt index eea8873e..524e90cf 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepository.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepository.kt @@ -3,7 +3,6 @@ package clickstream.internal.eventscheduler.impl import clickstream.internal.eventscheduler.CSEventData import clickstream.internal.eventscheduler.CSEventDataDao import clickstream.internal.eventscheduler.CSEventRepository -import kotlinx.coroutines.flow.Flow /** * The StorageRepositoryImpl is the implementation detail of the StorageRepository. @@ -14,35 +13,46 @@ internal class DefaultCSEventRepository( private val eventDataDao: CSEventDataDao ) : CSEventRepository { - override suspend fun insertEventData(eventData: CSEventData) { + override suspend fun insertEventData(eventData: CSEventData): Unit = eventDataDao.insert(eventData = eventData) - } - override suspend fun insertEventDataList(eventDataList: List) { + override suspend fun insertEventDataList(eventDataList: List): Unit = eventDataDao.insertAll(eventDataList = eventDataList) + + override suspend fun getAllEvents(): List = + eventDataDao.getAll() + + override suspend fun getOnGoingEvents(): List = + eventDataDao.loadOnGoingEvents() + + override suspend fun resetOnGoingForGuid(guid: String): Unit = + eventDataDao.setOnGoingEvent(guid, false) + + override suspend fun deleteEventDataByGuId(eventBatchGuId: String) { + eventDataDao.deleteByGuId(eventBatchGuId = eventBatchGuId) } - override suspend fun getEventDataList(): Flow> { - return eventDataDao.loadAll() + override suspend fun loadEventsByRequestId(eventBatchGuId: String): List { + return eventDataDao.loadEventByRequestId(eventBatchGuId) } - override suspend fun getAllEvents(): List { - return eventDataDao.getAll() + override suspend fun getUnprocessedEventsWithLimit(limit: Int): List { + return eventDataDao.getUnprocessedEventsWithLimit(limit) } - override suspend fun getOnGoingEvents(): List { - return eventDataDao.loadOnGoingEvents() + override suspend fun updateEventDataList(eventDataList: List) { + return eventDataDao.updateAll(eventDataList) } - override suspend fun resetOnGoingForGuid(guid: String) { - eventDataDao.setOnGoingEvent(guid, false) + override suspend fun getAllUnprocessedEvents(): List { + return eventDataDao.getAllUnprocessedEvents() } - override suspend fun deleteEventDataByGuId(eventBatchGuId: String) { - eventDataDao.deleteByGuId(eventBatchGuId = eventBatchGuId) + override suspend fun getAllUnprocessedEventsCount(): Int { + return eventDataDao.getAllUnprocessedEventsCount() } - override suspend fun getEventsOnGuId(eventBatchGuId: String): List { - return eventDataDao.loadEventByRequestId(eventBatchGuId) + override suspend fun getEventCount(): Int { + return eventDataDao.getEventCount() } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategy.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategy.kt new file mode 100644 index 00000000..3c40d26e --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategy.kt @@ -0,0 +1,63 @@ +package clickstream.internal.eventscheduler.impl + +import clickstream.config.CSEventSchedulerConfig +import clickstream.internal.CSEventInternal +import clickstream.internal.db.CSBatchSizeSharedPref +import clickstream.internal.eventscheduler.CSEventBatchSizeStrategy +import clickstream.internal.eventscheduler.DEFAULT_MIN_EVENT_COUNT +import clickstream.logger.CSLogger +import java.util.concurrent.atomic.AtomicInteger + +/** + * Provides the regulated number of events for batch. + */ +internal class EventByteSizeBasedBatchStrategy( + private val logger: CSLogger, + private val csBatchSizeSharedPref: CSBatchSizeSharedPref +) : CSEventBatchSizeStrategy { + + private var totalDataFlow = AtomicInteger(0) + private var totalEventCount = AtomicInteger(0) + + /** + * + * Calculates the count of events required to make the payload size of [CSEventSchedulerConfig.eventsPerBatch] + * based on average size of events flowing in to the SDK. + * + */ + override suspend fun regulatedCountOfEventsPerBatch(expectedBatchSize: Int): Int { + var regulatedCount: Int + if (totalDataFlow.get() == 0 || totalEventCount.get() == 0) { + regulatedCount = DEFAULT_MIN_EVENT_COUNT + } else { + val avgSizeOfEvents = totalDataFlow.get() / totalEventCount.get() + regulatedCount = expectedBatchSize / avgSizeOfEvents + // Rare case if average byte size of events is more than expected batch size. + if (regulatedCount < 1) { + regulatedCount = DEFAULT_MIN_EVENT_COUNT + } + logger.debug { + """EventByteSizeBasedBatchStrategy#regulatedCountOfEventsPerBatch + totalDataFlow: $totalDataFlow + totalEventCount: $totalEventCount + average Size of Events: $avgSizeOfEvents + regulated count for $expectedBatchSize: $regulatedCount + """.trimMargin() + } + } + csBatchSizeSharedPref.saveBatchSize(regulatedCount) + return regulatedCount + } + + /** + * Provides the tracked event to calculate regulated number of events per batch + */ + override fun logEvent(event: CSEventInternal) { + val dataSize = when (event) { + is CSEventInternal.CSBytesEvent -> event.eventData.size + is CSEventInternal.CSEvent -> event.message.serializedSize + } + totalDataFlow.getAndAdd(dataSize) + totalEventCount.getAndIncrement() + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/NoOpEventSchedulerErrorListener.kt b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/NoOpEventSchedulerErrorListener.kt new file mode 100644 index 00000000..215ea285 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/eventscheduler/impl/NoOpEventSchedulerErrorListener.kt @@ -0,0 +1,9 @@ +package clickstream.internal.eventscheduler.impl + +import clickstream.internal.eventscheduler.CSEventSchedulerErrorListener + +internal class NoOpEventSchedulerErrorListener : CSEventSchedulerErrorListener { + override fun onError(tag: String, throwable: Throwable) { + /*NoOp*/ + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSBackgroundNetworkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSBackgroundNetworkManager.kt new file mode 100644 index 00000000..22c2b59f --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSBackgroundNetworkManager.kt @@ -0,0 +1,64 @@ +package clickstream.internal.networklayer + +import clickstream.api.CSInfo +import clickstream.connection.CSSocketConnectionListener +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.lifecycle.CSAppLifeCycle +import clickstream.listener.CSEventListener +import clickstream.logger.CSLogger +import clickstream.report.CSReportDataTracker +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +/** + * The NetworkManager is responsible for communicating with repository and the event scheduler + * when the app is in Background + * + * @param networkRepository - Handles the communication with the server + * @param dispatcher - CoroutineDispatcher on which the events are observed and processed + * @param logger - To create logs + * @param healthEventProcessor - Used for logging health events + */ +@ExperimentalCoroutinesApi +internal class CSBackgroundNetworkManager( + appLifeCycleObserver: CSAppLifeCycle, + networkRepository: CSNetworkRepository, + dispatcher: CoroutineDispatcher, + logger: CSLogger, + healthEventProcessor: CSHealthEventProcessor?, + info: CSInfo, + connectionListener: CSSocketConnectionListener, + csReportDataTracker: CSReportDataTracker?, + eventListeners: List +) : CSNetworkManager( + appLifeCycleObserver, + networkRepository, + dispatcher, + logger, + healthEventProcessor, + info, + connectionListener, + csReportDataTracker, + eventListeners +) { + + override val tag: String + get() = "CSBackgroundNetworkManager" + + override fun onStart() { + logger.debug { "$tag#onStart" } + coroutineScope.cancel() + logger.debug { "$tag#onStart - coroutineScope cancelled" } + } + + override fun onStop() { + logger.debug { "$tag#onStop" } + job = SupervisorJob() + coroutineScope = CoroutineScope(job + dispatcher) + observeSocketState() + logger.debug { "$tag#onStart - coroutineScope started and observeSocketState" } + } +} diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkManager.kt index ab984d15..3ddd6132 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkManager.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkManager.kt @@ -10,34 +10,38 @@ import clickstream.connection.CSConnectionEvent.OnConnectionFailed import clickstream.connection.CSConnectionEvent.OnMessageReceived import clickstream.connection.CSSocketConnectionListener import clickstream.connection.mapTo -import clickstream.health.constant.CSEventNamesConstant +import clickstream.health.constant.CSErrorConstant import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.internal.analytics.CSErrorReasons +import clickstream.health.constant.CSHealthEventName +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.model.CSHealthEvent import clickstream.internal.utils.CSResult import clickstream.lifecycle.CSAppLifeCycle import clickstream.lifecycle.CSLifeCycleManager +import clickstream.listener.CSEventListener +import clickstream.listener.CSEventModel import clickstream.logger.CSLogger +import clickstream.report.CSReportDataTracker import com.gojek.clickstream.de.EventRequest import com.tinder.scarlet.WebSocket -import java.io.EOFException import java.net.SocketTimeoutException import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlin.coroutines.coroutineContext /** * The NetworkManager is responsible for communicating with repository and the event scheduler @@ -45,98 +49,100 @@ import kotlin.coroutines.coroutineContext * @param networkRepository - Handles the communication with the server * @param dispatcher - CoroutineDispatcher on which the events are observed and processed * @param logger - To create logs - * @param healthEventRepository - Used for logging health events + * @param healthEventProcessor - Used for logging health events */ @ExperimentalCoroutinesApi internal open class CSNetworkManager( - appLifeCycle: CSAppLifeCycle, + appLifeCycleObserver: CSAppLifeCycle, private val networkRepository: CSNetworkRepository, - private val dispatcher: CoroutineDispatcher, - private val logger: CSLogger, - private val healthEventRepository: CSHealthEventRepository, + protected val dispatcher: CoroutineDispatcher, + protected val logger: CSLogger, + private val healthEventProcessor: CSHealthEventProcessor?, private val info: CSInfo, - private val connectionListener: CSSocketConnectionListener -) : CSLifeCycleManager(appLifeCycle) { + private val connectionListener: CSSocketConnectionListener, + private val csReportDataTracker: CSReportDataTracker?, + private val csEventListeners: List, +) : CSLifeCycleManager(appLifeCycleObserver) { private val isConnected: AtomicBoolean = AtomicBoolean(false) - private val job = SupervisorJob() - protected val coroutineScope = CoroutineScope(job + dispatcher) + protected var job: CompletableJob = SupervisorJob() + protected var coroutineScope: CoroutineScope = CoroutineScope(job + dispatcher) + private val handler = CoroutineExceptionHandler { _, throwable -> + logger.debug { "$tag#handler : ${throwable.message}" } + } + @VisibleForTesting - internal var startConnectingTime: Long = 0L + var connectionStartTime = 0L + @VisibleForTesting - internal var endConnectedTime: Long = 0L - @Volatile - private lateinit var callback: CSEventGuidCallback - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSBackgroundEventScheduler = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } + var connectionEndTime = 0L + + override val tag: String + get() = "CSNetworkManager" init { - logger.debug { "CSNetworkManager#init" } + logger.debug { "$tag#init" } addObserver() } + /** + * Provides status of network manager + * + * @return isAvailable - Whether network manager is available or not + */ + fun isSocketAvailable(): Boolean { + logger.debug { "$tag#isAvailable - isSocketConnected : ${isConnected.get()}" } + + return isConnected.get() + } + + /** + * Updates the caller with the status of the request + */ + private lateinit var callback: CSEventGuidCallback + val eventGuidFlow: Flow> = callbackFlow { - logger.debug { "CSNetworkManager#eventGuidFlow" } + logger.debug { "$tag#eventGuidFlow" } callback = object : CSEventGuidCallback { override fun onSuccess(data: String) { - logger.debug { "CSNetworkManager#eventGuidFlow#onSuccess - $data" } - - offer(CSResult.Success(data)) + logger.debug { "$tag#eventGuidFlow#onSuccess - $data" } + val sendResult = trySend(CSResult.Success(data)) + logger.debug { "$tag#trySend#onSuccess $sendResult" } } override fun onError(error: Throwable, guid: String) { error.printStackTrace() - logger.debug { "CSNetworkManager#eventGuidFlow#onError : $guid errorMessage ${error.message}" } - - offer(CSResult.Failure(error, guid)) + logger.debug { "$tag#eventGuidFlow#onError - $guid errorMessage : ${error.message}" } + val sendResult = trySend(CSResult.Failure(error, guid)) + logger.debug { "$tag#trySend#onError $sendResult" } } } awaitClose() } - /** - * Starts the network manager by creating the coroutine scope - */ override fun onStart() { - logger.debug { "CSNetworkManager#onStart" } - coroutineScope.launch(coroutineExceptionHandler) { - observeSocketConnectionState() - } + logger.debug { "$tag#onStart" } + job = SupervisorJob() + coroutineScope = CoroutineScope(job + dispatcher) + observeSocketState() } - /** - * Terminates the network manager by cancelling the coroutine, will be called when - * clickstream is getting terminated. - */ override fun onStop() { - logger.debug { "CSNetworkManager#onStop" } - cancelJob() - } - - fun cancelJob() { - logger.debug { "CSNetworkManager#cancelJob" } - job.cancelChildren() + logger.debug { "$tag#onStop" } + coroutineScope.cancel() } /** * The analytic data which is sent to the server * * @param eventRequest - The data which hold the analytic events - * @param eventGuids - a guid list within string the comma separate "1, 2, 3" */ - fun processEvent(eventRequest: EventRequest, eventGuids: String) { - logger.debug { "CSNetworkManager#processEvent" } - - networkRepository.sendEvents(eventRequest, eventGuids, callback) + fun processEvent( + eventRequest: EventRequest + ) { + logger.debug { "$tag#processEvent" } + networkRepository.sendEvents(eventRequest = eventRequest, callback = callback) } /** @@ -147,167 +153,120 @@ internal open class CSNetworkManager( fun processInstantEvent( eventRequest: EventRequest ) { - logger.debug { "CSNetworkManager#processInstantEvent" } + logger.debug { "$tag#processInstantEvent" } networkRepository.sendInstantEvents(eventRequest = eventRequest) } - /** - * Provides status of network manager - * - * @return isAvailable - Whether network manager is available or not - */ - fun isSocketConnected(): Boolean { - logger.debug { "CSNetworkManager#isSocketConnected : ${isConnected.get()}" } - - return this.isConnected.get() - } - /** * Observes the web socket connection state. * * When the connection is closed, the scope is cancelled to unsubscribe the events */ - @VisibleForTesting - suspend fun observeSocketConnectionState() { - logger.debug { "CSNetworkManager#observeSocketConnectionState" } - - if (coroutineContext.isActive.not()) { - logger.debug { "CSNetworkManager#observeSocketConnectionState : coroutine is not active" } - return - } - - networkRepository.observeSocketState() - .onStart { - // start time for connecting - startConnectingTime = System.currentTimeMillis() - - connectionListener.onEventChanged(OnConnectionConnecting) - recordNonErrorHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionAttempt.value, - type = CSEventTypesConstant.INSTANT - ) - } - .collect { - when (it) { - is WebSocket.Event.OnConnectionOpened<*> -> { - logger.debug { "CSNetworkManager#observeSocketConnectionState - Socket State: OnConnectionOpened" } - isConnected.set(true) - endConnectedTime = System.currentTimeMillis() - connectionListener.onEventChanged(OnConnectionConnected) - - recordNonErrorHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionSuccess.value, - type = CSEventTypesConstant.INSTANT, - timeToConnection = endConnectedTime - startConnectingTime - ) - } - is WebSocket.Event.OnMessageReceived -> { - logger.debug { "CSNetworkManager#observeSocketConnectionState - Socket State: OnMessageReceived : " + it.message } - isConnected.set(true) - - connectionListener.onEventChanged(OnMessageReceived(it.message.mapTo())) - } - is WebSocket.Event.OnConnectionClosing -> { - logger.debug { "CSNetworkManager#observeSocketConnectionState - Socket State: OnConnectionClosing. Due to" + it.shutdownReason } - - connectionListener.onEventChanged(OnConnectionClosing(it.shutdownReason.mapTo())) - } - is WebSocket.Event.OnConnectionClosed -> { - logger.debug { "CSNetworkManager#observeSocketConnectionState - Socket State: OnConnectionClosed. Due to" + it.shutdownReason } - isConnected.set(false) - connectionListener.onEventChanged(OnConnectionClosed(it.shutdownReason.mapTo())) - - recordErrorHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionDropped.value, - throwable = Exception(it.shutdownReason.reason), - failureMessage = it.shutdownReason.reason - ) - } - is WebSocket.Event.OnConnectionFailed -> { - logger.debug { "CSNetworkManager#observeSocketConnectionState - Socket State: OnConnectionFailed. Due to " + it.throwable } - isConnected.set(false) - endConnectedTime = System.currentTimeMillis() - connectionListener.onEventChanged(OnConnectionFailed(it.throwable)) - - recordErrorHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionFailure.value, - throwable = it.throwable, - failureMessage = it.throwable.message, - timeToConnection = endConnectedTime - startConnectingTime - ) + protected fun observeSocketState(): Job { + logger.debug { "$tag#observeSocketState" } + return coroutineScope.launch(handler) { + logger.debug { "$tag#observeSocketState is coroutine active : $isActive" } + ensureActive() + networkRepository.observeSocketState() + .onStart { + connectionListener.onEventChanged(OnConnectionConnecting) + connectionStartTime = System.currentTimeMillis() + } + .collect { + when (it) { + is WebSocket.Event.OnConnectionOpened<*> -> { + logger.debug { "$tag#observeSocketState - Socket State: OnConnectionOpened" } + connectionEndTime = System.currentTimeMillis() + isConnected.set(true) + csReportDataTracker?.trackMessage(tag, "Socket Connected") + connectionListener.onEventChanged(OnConnectionConnected) + } + is WebSocket.Event.OnMessageReceived -> { + logger.debug { "$tag#observeSocketState - Socket State: OnMessageReceived : " + it.message } + isConnected.set(true) + dispatchSocketConnectionStatusToEv(true) + connectionListener.onEventChanged(OnMessageReceived(it.message.mapTo())) + } + is WebSocket.Event.OnConnectionClosing -> { + connectionListener.onEventChanged(OnConnectionClosing(it.shutdownReason.mapTo())) + csReportDataTracker?.trackMessage( + tag, + "\"ConnectionClosing due to ${it.shutdownReason}\"" + ) + logger.debug { "$tag#observeSocketState - Socket State: OnConnectionClosing. Due to" + it.shutdownReason } + } + is WebSocket.Event.OnConnectionClosed -> { + logger.debug { "$tag#observeSocketState - Socket State: OnConnectionClosed. Due to" + it.shutdownReason } + isConnected.set(false) + csReportDataTracker?.trackMessage( + tag, + "\"ConnectionClosed due to ${it.shutdownReason}\"" + ) + connectionListener.onEventChanged(OnConnectionClosed(it.shutdownReason.mapTo())) + dispatchSocketConnectionStatusToEv(false) + } + is WebSocket.Event.OnConnectionFailed -> { + logger.debug { "$tag#observeSocketState - Socket State: OnConnectionFailed. Due to " + it.throwable } + isConnected.set(false) + csReportDataTracker?.trackMessage( + tag, + "\"ConnectionFailed due to ${it.throwable.message}\"" + ) + connectionListener.onEventChanged(OnConnectionFailed(it.throwable)) + dispatchSocketConnectionStatusToEv(false) + trackConnectionFailure(it) + } } } - } + } } - private suspend fun recordErrorHealthEvent( - eventName: String, - throwable: Throwable, - failureMessage: String?, - timeToConnection: Long = 0L - ) { - val healthEvent: CSHealthEventDTO = when { - failureMessage?.contains(CSErrorReasons.USER_UNAUTHORIZED, true) ?: false -> { - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.USER_UNAUTHORIZED, - appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection - ) - } - throwable is SocketTimeoutException -> { - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.SOCKET_TIMEOUT, + private suspend fun trackConnectionFailure(failureResponse: WebSocket.Event.OnConnectionFailed) { + logger.debug { "$tag#trackConnectionFailure $failureResponse" } + + val healthEvent = when { + failureResponse.throwable.message?.contains(CSErrorConstant.USER_UNAUTHORIZED, true) + ?: false -> { + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.USER_UNAUTHORIZED, appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection ) } - throwable is EOFException -> { - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.EOFException, - appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection + failureResponse.throwable is SocketTimeoutException -> { + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.SOCKET_TIMEOUT, + appVersion = info.appInfo.appVersion ) } - failureMessage?.isNotEmpty() == true -> { - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.INSTANT, - error = failureMessage, - appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection + failureResponse.throwable.message?.isNotEmpty() ?: false -> { + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = failureResponse.throwable.toString(), + appVersion = info.appInfo.appVersion ) } else -> { - CSHealthEventDTO( - eventName = eventName, - eventType = CSEventTypesConstant.INSTANT, - error = failureMessage ?: CSErrorReasons.UNKNOWN, - appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection + CSHealthEvent( + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.UNKNOWN, + appVersion = info.appInfo.appVersion ) } } - logger.debug { "CSNetworkManager#trackConnectionFailure due to ${healthEvent.error}" } - healthEventRepository.insertHealthEvent(healthEvent = healthEvent) + healthEventProcessor?.insertNonBatchEvent(csEvent = healthEvent) } - private suspend fun recordNonErrorHealthEvent( - eventName: String, - type: String, - timeToConnection: Long = 0L - ) { - val event = CSHealthEventDTO( - eventName = eventName, - eventType = type, - appVersion = info.appInfo.appVersion, - timeToConnection = timeToConnection - ) - healthEventRepository.insertHealthEvent(healthEvent = event) + private fun dispatchSocketConnectionStatusToEv(isConnected: Boolean) { + coroutineScope.launch { + csEventListeners.forEach { + it.onCall(listOf(CSEventModel.Connection(isConnected))) + } + } } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkRepository.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkRepository.kt index 2ef6deeb..3ab69730 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkRepository.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSNetworkRepository.kt @@ -2,9 +2,9 @@ package clickstream.internal.networklayer import clickstream.api.CSInfo import clickstream.config.CSNetworkConfig -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.time.CSTimeStampGenerator +import clickstream.health.intermediate.CSHealthEventProcessor import clickstream.internal.utils.CSCallback +import clickstream.internal.utils.CSTimeStampGenerator import clickstream.logger.CSLogger import com.gojek.clickstream.de.EventRequest import com.gojek.clickstream.de.common.EventResponse @@ -36,7 +36,6 @@ internal interface CSNetworkRepository { */ public fun sendEvents( eventRequest: EventRequest, - eventGuids: String, callback: CSCallback ) @@ -59,7 +58,7 @@ internal class CSNetworkRepositoryImpl( private val dispatcher: CoroutineDispatcher, private val timeStampGenerator: CSTimeStampGenerator, private val logger: CSLogger, - private val healthEventRepository: CSHealthEventRepository, + private val healthProcessor: CSHealthEventProcessor?, private val info: CSInfo ) : CSNetworkRepository { @@ -77,7 +76,6 @@ internal class CSNetworkRepositoryImpl( override fun sendEvents( eventRequest: EventRequest, - eventGuids: String, callback: CSCallback ) { logger.debug { "CSNetworkRepositoryImpl#sendEvents" } @@ -86,11 +84,10 @@ internal class CSNetworkRepositoryImpl( networkConfig = networkConfig, eventService = eventService, eventRequest = eventRequest, - eventGuids = eventGuids, dispatcher = dispatcher, timeStampGenerator = timeStampGenerator, logger = logger, - healthEventRepository = healthEventRepository, + healthProcessor = healthProcessor, info = info ) { override fun onSuccess(guid: String) { @@ -100,7 +97,7 @@ internal class CSNetworkRepositoryImpl( } override fun onFailure(throwable: Throwable, guid: String) { - logger.debug { "CSNetworkRepositoryImpl#sendEvents#onFailure - $guid ${throwable.stackTraceToString()}" } + logger.debug { "CSNetworkRepositoryImpl#sendEvents#onFailure - $guid ${throwable.message}" } callback.onError(throwable, guid) } diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSRetryableCallback.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSRetryableCallback.kt index 6e2802e5..eabba490 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSRetryableCallback.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSRetryableCallback.kt @@ -2,14 +2,14 @@ package clickstream.internal.networklayer import clickstream.api.CSInfo import clickstream.config.CSNetworkConfig -import clickstream.extension.isHealthEvent -import clickstream.health.constant.CSEventNamesConstant +import clickstream.health.constant.CSErrorConstant import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.analytics.CSErrorReasons +import clickstream.health.constant.CSHealthEventName +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.health.model.CSHealthEvent +import clickstream.internal.utils.CSTimeStampGenerator import clickstream.internal.utils.CSTimeStampMessageBuilder +import clickstream.isHealthEvent import clickstream.logger.CSLogger import com.gojek.clickstream.de.EventRequest import com.gojek.clickstream.de.common.Code @@ -17,7 +17,6 @@ import com.gojek.clickstream.de.common.EventResponse import com.gojek.clickstream.de.common.Status import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -41,11 +40,10 @@ internal abstract class CSRetryableCallback( private val networkConfig: CSNetworkConfig, private val eventService: CSEventService, private var eventRequest: EventRequest, - private val eventGuids: String, private val dispatcher: CoroutineDispatcher, private val timeStampGenerator: CSTimeStampGenerator, private val logger: CSLogger, - private val healthEventRepository: CSHealthEventRepository, + private val healthProcessor: CSHealthEventProcessor?, private val info: CSInfo, private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) ) { @@ -64,23 +62,15 @@ internal abstract class CSRetryableCallback( */ abstract fun onFailure(throwable: Throwable, guid: String) - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSRetryableCallback = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } private var timeOutJob: Job = SupervisorJob() private var retryCount: AtomicInteger = AtomicInteger(0) init { - logger.debug { "CSRetryableCallback#init" } + logger.debug { + "CSRetryableCallback#init" + } - coroutineScope.launch(coroutineExceptionHandler) { + coroutineScope.launch { launch { observeCallback() } launch { sendEvent() } } @@ -94,30 +84,42 @@ internal abstract class CSRetryableCallback( * If success, it invokes onSuccess callback */ private suspend fun observeCallback() { - logger.debug { "CSRetryableCallback#observeCallback" } + logger.debug { + "CSRetryableCallback#observeCallback" + } eventService.observeResponse().onEach { response -> - logger.debug { "CSRetryableCallback#observeCallback#onEach - response $response" } + logger.debug { + "CSRetryableCallback#observeCallback#onEach - response $response" + } }.filter { it.dataMap[REQUEST_GUID_KEY] == eventRequest.reqGuid }.collect { - logger.debug { "CSRetryableCallback#observeCallback - Message received from the server: ${it.dataMap[REQUEST_GUID_KEY]}" } + logger.debug { + "CSRetryableCallback#observeCallback - Message received from the server: ${it.dataMap[REQUEST_GUID_KEY]}" + } val guid = it.dataMap[REQUEST_GUID_KEY]!! when { it.status == Status.SUCCESS -> { - logger.debug { "CSRetryableCallback#observeCallback - Success" } + logger.debug { + "CSRetryableCallback#observeCallback - Success" + } onSuccess(guid) sendAckAndComplete() } shouldRetry() -> { - logger.debug { "CSRetryableCallback#observeCallback - retried" } + logger.debug { + "CSRetryableCallback#observeCallback - retried" + } trackEventResponse(it, eventRequest.reqGuid) retry() } else -> { - logger.debug { "CSRetryableCallback#observeCallback - else" } + logger.debug { + "CSRetryableCallback#observeCallback - else" + } trackEventResponse(it, eventRequest.reqGuid) onFailure(Throwable(), guid) @@ -132,20 +134,40 @@ internal abstract class CSRetryableCallback( * Once the request is sent, the data is logged and also sent to CT */ private suspend fun sendEvent() { - if (eventService.sendEvent(eventRequest)) { + if (sendOOMSafeEvent()) { logBatchSentEvent() logger.debug { - "CSRetryableCallback#sendEvent : Request successfully sent to the server: $eventRequest" + "CSRetryableCallback#sendEvent - Request successfully sent to the server" } } else { logger.debug { - "CSRetryableCallback#sendEvent : Request sent to the server failed: ${eventRequest.reqGuid}" + "CSRetryableCallback#sendEvent - Request sent to the server failed: ${eventRequest.reqGuid}" } + recordHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + eventBatchId = eventRequest.reqGuid, + error = "Batch write failed" + ) } observeTimeout() } + private suspend fun sendOOMSafeEvent(): Boolean { + return try { + eventService.sendEvent(eventRequest) + } catch (e: OutOfMemoryError) { + recordHealthEvent( + eventName = CSHealthEventName.ClickStreamEventBatchTriggerFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + eventBatchId = eventRequest.reqGuid, + error = "Out of memory while sending ${eventRequest.eventsList.size} events", + ) + false + } + } + /** * Waits for x duration [CSNetworkConfig.maxRequestAckTimeout] and then invokes retry if the duration * exceeds. This is marked as timeout. @@ -167,9 +189,9 @@ internal abstract class CSRetryableCallback( } recordHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchTimeout.value, - eventType = CSEventTypesConstant.INSTANT, - eventBatchGuid = eventRequest.reqGuid, + eventName = CSHealthEventName.ClickStreamEventBatchTimeout.value, + eventType = CSEventTypesConstant.AGGREGATE, + eventBatchId = eventRequest.reqGuid, error = "SocketTimeout" ) if (shouldRetry()) { @@ -202,7 +224,9 @@ internal abstract class CSRetryableCallback( private fun shouldRetry(): Boolean { val shouldRetry = retryCount.get() < networkConfig.maxRetriesPerBatch - logger.debug { "CSRetryableCallback#shouldRetry : shouldRetry $shouldRetry" } + logger.debug { + "CSRetryableCallback#shouldRetry - shouldRetry $shouldRetry" + } return shouldRetry } @@ -210,14 +234,15 @@ internal abstract class CSRetryableCallback( * Logs the batch sent to the CT */ private fun logBatchSentEvent() { - logger.debug { "CSRetryableCallback#logBatchSentEvent" } + logger.debug { + "CSRetryableCallback#logBatchSentEvent" + } coroutineScope.launch(dispatcher) { recordHealthEvent( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamBatchSent.value, + eventName = CSHealthEventName.ClickStreamBatchSent.value, eventType = CSEventTypesConstant.AGGREGATE, - eventBatchGuid = eventRequest.reqGuid, - eventGuids = eventGuids, + eventBatchId = eventRequest.reqGuid, error = "" ) } @@ -227,12 +252,14 @@ internal abstract class CSRetryableCallback( * Send the Ack event and then invokes complete */ private suspend fun sendAckAndComplete() { - logger.debug { "CSRetryableCallback#sendAckAndComplete" } + logger.debug { + "CSRetryableCallback#sendAckAndComplete" + } recordHealthEvent( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamEventBatchSuccessAck.value, + eventName = CSHealthEventName.ClickStreamEventBatchAck.value, eventType = CSEventTypesConstant.AGGREGATE, - eventBatchGuid = eventRequest.reqGuid, + eventBatchId = eventRequest.reqGuid, error = "" ) onComplete() @@ -257,43 +284,51 @@ internal abstract class CSRetryableCallback( when (eventResponse.code.ordinal) { Code.MAX_CONNECTION_LIMIT_REACHED.ordinal -> { - logger.debug { "CSRetryableCallback#trackEventResponse - eventResponse MAX_CONNECTION_LIMIT_REACHED" } + logger.debug { + "CSRetryableCallback#trackEventResponse - eventResponse MAX_CONNECTION_LIMIT_REACHED" + } recordHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionFailure.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.MAX_CONNECTION_LIMIT_REACHED, - eventBatchGuid = eventRequestGuid + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.MAX_CONNECTION_LIMIT_REACHED, + eventBatchId = eventRequestGuid ) } Code.MAX_USER_LIMIT_REACHED.ordinal -> { - logger.debug { "CSRetryableCallback#trackEventResponse - eventResponse MAX_USER_LIMIT_REACHED" } + logger.debug { + "CSRetryableCallback#trackEventResponse - eventResponse MAX_USER_LIMIT_REACHED" + } recordHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionFailure.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.MAX_USER_LIMIT_REACHED, - eventBatchGuid = eventRequestGuid + eventName = CSHealthEventName.ClickStreamConnectionFailed.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.MAX_USER_LIMIT_REACHED, + eventBatchId = eventRequestGuid ) } Code.BAD_REQUEST.ordinal -> { - logger.debug { "CSRetryableCallback#trackEventResponse : eventResponse BAD_REQUEST" } + logger.debug { + "CSRetryableCallback#trackEventResponse - eventResponse BAD_REQUEST" + } recordHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamWriteToSocketFailed.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.PARSING_EXCEPTION, - eventBatchGuid = eventRequestGuid + eventName = CSHealthEventName.ClickStreamEventBatchErrorResponse.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.PARSING_EXCEPTION, + eventBatchId = eventRequestGuid ) } else -> { - logger.debug { "CSRetryableCallback#trackEventResponse : eventResponse ClickStreamEventBatchErrorResponse" } + logger.debug { + "CSRetryableCallback#trackEventResponse - eventResponse ClickStreamEventBatchErrorResponse" + } recordHealthEvent( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchErrorResponse.value, - eventType = CSEventTypesConstant.INSTANT, - error = CSErrorReasons.UNKNOWN, - eventBatchGuid = eventRequestGuid + eventName = CSHealthEventName.ClickStreamEventBatchErrorResponse.value, + eventType = CSEventTypesConstant.AGGREGATE, + error = CSErrorConstant.UNKNOWN, + eventBatchId = eventRequestGuid ) } } @@ -303,12 +338,11 @@ internal abstract class CSRetryableCallback( eventName: String, eventType: String, error: String, - eventBatchGuid: String, - eventGuids: String = "" + eventBatchId: String, ) { logger.debug { StringBuilder() - .append("CSRetryableCallback#recordHealthEvent : events ${eventRequest.eventsList}") + .append("CSRetryableCallback#recordHealthEvent") .apply { if (eventRequest.eventsCount > 0) { append("isHealthEvent : ${eventRequest.getEvents(0).isHealthEvent()}") @@ -317,17 +351,19 @@ internal abstract class CSRetryableCallback( } if (eventRequest.eventsCount > 0 && eventRequest.getEvents(0).isHealthEvent().not()) { - logger.debug { "CSRetryableCallback#recordHealthEvent : insertHealthEvent" } + logger.debug { + "CSRetryableCallback#recordHealthEvent - insertHealthEvent" + } - healthEventRepository.insertHealthEvent( - CSHealthEventDTO( + healthProcessor?.insertBatchEvent( + CSHealthEvent( eventName = eventName, eventType = eventType, - eventBatchGuid = eventBatchGuid, - eventGuid = eventGuids, + eventBatchGuid = eventBatchId, error = error, appVersion = info.appInfo.appVersion - ) + ), + eventRequest.eventsCount.toLong() ) } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSWorkManagerNetworkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSWorkManagerNetworkManager.kt deleted file mode 100644 index abd1bbad..00000000 --- a/clickstream/src/main/kotlin/clickstream/internal/networklayer/CSWorkManagerNetworkManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -package clickstream.internal.networklayer - -import android.annotation.SuppressLint -import clickstream.api.CSInfo -import clickstream.connection.CSSocketConnectionListener -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.logger.CSLogger -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch - -/** - * The NetworkManager is responsible for communicating with repository and the event scheduler - * when the app is in Background - * - * @param networkRepository - Handles the communication with the server - * @param dispatcher - CoroutineDispatcher on which the events are observed and processed - * @param logger - To create logs - * @param healthEventRepository - Used for logging health events - */ -@ExperimentalCoroutinesApi -internal class CSWorkManagerNetworkManager( - appLifeCycle: CSAppLifeCycle, - networkRepository: CSNetworkRepository, - private val dispatcher: CoroutineDispatcher, - private val logger: CSLogger, - healthEventRepository: CSHealthEventRepository, - info: CSInfo, - connectionListener: CSSocketConnectionListener -) : CSNetworkManager( - appLifeCycle, - networkRepository, - dispatcher, - logger, - healthEventRepository, - info, - connectionListener -) { - - private val coroutineExceptionHandler: CoroutineExceptionHandler by lazy { - CoroutineExceptionHandler { _, throwable -> - logger.error { - "================== CRASH IS HAPPENING ================== \n" + - "= In : CSBackgroundEventScheduler = \n" + - "= Due : ${throwable.message} = \n" + - "==================== END OF CRASH ====================== \n" - } - } - } - - init { - logger.debug { "CSWorkManagerNetworkManager#init" } - } - - override fun onStart() { - logger.debug { "CSWorkManagerNetworkManager#onStart" } - logger.debug { "CSWorkManagerNetworkManager#onStart - coroutineScope cancelled" } - - cancelJob() - } - - @SuppressLint("VisibleForTests") - override fun onStop() { - logger.debug { "CSWorkManagerNetworkManager#onStop" } - logger.debug { "CSWorkManagerNetworkManager#onStart - coroutineScope started and observeSocketState" } - - coroutineScope.launch(coroutineExceptionHandler) { - observeSocketConnectionState() - } - } -} diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSConnectivityOnLifecycle.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSConnectivityOnLifecycle.kt new file mode 100644 index 00000000..0ea4d103 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSConnectivityOnLifecycle.kt @@ -0,0 +1,51 @@ +package clickstream.internal.networklayer.socket + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import com.tinder.scarlet.Lifecycle +import com.tinder.scarlet.lifecycle.LifecycleRegistry + +internal class CSConnectivityOnLifecycle( + applicationContext: Context, + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry() +) : Lifecycle by lifecycleRegistry { + + init { + emitCurrentConnectivity(applicationContext) + subscribeToConnectivityChange(applicationContext) + } + + private fun emitCurrentConnectivity(applicationContext: Context) { + val connectivityManager = + applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + lifecycleRegistry.onNext(toLifecycleState(connectivityManager.isConnected())) + } + + private fun subscribeToConnectivityChange(applicationContext: Context) { + val intentFilter = IntentFilter() + .apply { addAction(ConnectivityManager.CONNECTIVITY_ACTION) } + applicationContext.registerReceiver(ConnectivityChangeBroadcastReceiver(), intentFilter) + } + + private fun ConnectivityManager.isConnected(): Boolean { + val activeNetworkInfo = activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting + } + + private fun toLifecycleState(isConnected: Boolean): Lifecycle.State = if (isConnected) { + Lifecycle.State.Started + } else { + Lifecycle.State.Stopped.AndAborted + } + + private inner class ConnectivityChangeBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val extras = intent.extras ?: return + val isConnected = !extras.getBoolean(ConnectivityManager.EXTRA_NO_CONNECTIVITY) + lifecycleRegistry.onNext(toLifecycleState(isConnected)) + } + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSSocketConnectionManager.kt b/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSSocketConnectionManager.kt new file mode 100644 index 00000000..1a0e0639 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/networklayer/socket/CSSocketConnectionManager.kt @@ -0,0 +1,37 @@ +package clickstream.internal.networklayer.socket + +import android.app.Application +import com.tinder.scarlet.Lifecycle +import com.tinder.scarlet.ShutdownReason +import com.tinder.scarlet.lifecycle.LifecycleRegistry + +private const val SHUTDOWN_REASON_CODE = 1001 + +/** + * Socket connection manager for clickstream. + * connect -> establish socket connection. + * disconnect -> disconnect socket connection. + */ +internal class CSSocketConnectionManager( + private val lifecycleRegistry: LifecycleRegistry, + application: Application, +) : Lifecycle by lifecycleRegistry.combineWith(CSConnectivityOnLifecycle(application)) { + + /** + * Connecting socket. + */ + fun connect() { + lifecycleRegistry.onNext(Lifecycle.State.Started) + } + + /** + * Disconnecting socket. + */ + fun disconnect() { + lifecycleRegistry.onNext( + Lifecycle.State.Stopped.WithReason( + ShutdownReason(SHUTDOWN_REASON_CODE, "Gracefully disconnected") + ) + ) + } +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/utils/CSGuIdGenerator.kt b/clickstream/src/main/kotlin/clickstream/internal/utils/CSGuIdGenerator.kt new file mode 100644 index 00000000..49d48df9 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/utils/CSGuIdGenerator.kt @@ -0,0 +1,24 @@ +package clickstream.internal.utils + +import java.util.UUID + +/** + * Generate a random ID every time the getId() is invoked. + */ +internal interface CSGuIdGenerator { + + /** + * Returns a random unique ID + */ + fun getId(): String +} + +/** + * Implementation of the [CSGuIdGenerator] + */ +internal class CSGuIdGeneratorImpl : CSGuIdGenerator { + + override fun getId(): String { + return UUID.randomUUID().toString() + } +} diff --git a/clickstream/src/main/kotlin/clickstream/internal/utils/CSTicker.kt b/clickstream/src/main/kotlin/clickstream/internal/utils/CSTicker.kt index c6064fd3..baac8283 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/utils/CSTicker.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/utils/CSTicker.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.isActive /** * Creates a flow that produces the first item after @@ -19,14 +18,14 @@ import kotlinx.coroutines.isActive * will be produced (it is equal to [delayMillis] by default) in milliseconds. */ @ExperimentalCoroutinesApi -internal suspend fun flowableTicker( +internal fun flowableTicker( delayMillis: Long, initialDelay: Long = delayMillis ): Flow = callbackFlow { delay(initialDelay) - while (this.isClosedForSend.not()) { + while (!this.isClosedForSend) { send(Unit) delay(delayMillis) } - awaitClose { } + awaitClose {} } \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/internal/utils/CSTimeStampGenerator.kt b/clickstream/src/main/kotlin/clickstream/internal/utils/CSTimeStampGenerator.kt new file mode 100644 index 00000000..89eedf86 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/internal/utils/CSTimeStampGenerator.kt @@ -0,0 +1,27 @@ +package clickstream.internal.utils + +import clickstream.config.timestamp.CSEventGeneratedTimestampListener + +/** + * Generate the current time stamp + */ +internal interface CSTimeStampGenerator { + /** + * Returns the current time stamp at he given instant + */ + fun getTimeStamp(): Long +} + +/** + * Implementation of the [CSTimeStampGenerator] + */ +internal class DefaultCSTimeStampGenerator( + private val timestampListener: CSEventGeneratedTimestampListener +) : CSTimeStampGenerator { + + override fun getTimeStamp(): Long { + return runCatching { + timestampListener.now() + }.getOrDefault(System.currentTimeMillis()) + } +} diff --git a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSBaseEventFlushWorkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSBaseEventFlushWorkManager.kt index 7705c06d..3f94a514 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSBaseEventFlushWorkManager.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSBaseEventFlushWorkManager.kt @@ -3,9 +3,9 @@ package clickstream.internal.workmanager import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.CoroutineWorker -import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import clickstream.internal.di.CSServiceLocator @@ -18,15 +18,15 @@ internal open class CSBaseEventFlushWorkManager( ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { - CSServiceLocator.getInstance().logger.debug { "CSBaseEventFlushWorkManager#doWork" } - + val serviceLocator = CSServiceLocator.getInstance() + val backgroundScheduler = serviceLocator.backgroundScheduler return try { - val serviceLocator = CSServiceLocator.getInstance() - val backgroundScheduler = serviceLocator.workManagerEventScheduler val success = backgroundScheduler.sendEvents() if (success) Result.success() else Result.failure() } catch (e: Exception) { Result.failure() + } finally { + backgroundScheduler.terminate() } } @@ -36,13 +36,15 @@ internal open class CSBaseEventFlushWorkManager( fun addObserveForWorkManagerStatus(context: Context, workerName: String) { fun getWorkInfosByTagLiveData(workerName: String) { - val logger = CSServiceLocator.getInstance().logger WorkManager.getInstance(context) .getWorkInfosByTagLiveData(workerName).observe( ProcessLifecycleOwner.get() - ) { state: MutableList -> + ) { state -> state.forEach { entry -> - logger.debug { "CSFlushScheduledService#addObserveForWorkManagerStatus - $workerName : ${entry.progress} ${entry.state}" } + Log.d( + "ClickStream", + "CSFlushScheduledService#addObserveForWorkManagerStatus - $workerName : ${entry.progress} ${entry.state}" + ) } } } diff --git a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushOneTimeWorkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushOneTimeWorkManager.kt index 1b5ceb47..a84e0df8 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushOneTimeWorkManager.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushOneTimeWorkManager.kt @@ -1,6 +1,7 @@ package clickstream.internal.workmanager import android.content.Context +import android.util.Log import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingWorkPolicy.KEEP @@ -28,7 +29,9 @@ internal class CSEventFlushOneTimeWorkManager private constructor( private const val CLICKSTREAM_TASK_TAG = "Clickstream_Event_Flushing_Task" fun enqueueWork(context: Context) { - CSServiceLocator.getInstance().logger.debug { "CSEventFlushOneTimeWorkManager#enqueueWork" } + if (CSServiceLocator.getInstance().logLevel >= INFO) { + Log.d("ClickStream", "CSEventFlushOneTimeWorkManager#enqueueWork") + } OneTimeWorkRequestBuilder() .setConstraints( @@ -44,7 +47,8 @@ internal class CSEventFlushOneTimeWorkManager private constructor( ) .build() .let { request -> - WorkManager.getInstance(context).enqueueUniqueWork(CLICKSTREAM_TASK_TAG, KEEP, request) + WorkManager.getInstance(context) + .enqueueUniqueWork(CLICKSTREAM_TASK_TAG, KEEP, request) } if (CSServiceLocator.getInstance().logLevel >= INFO) { diff --git a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushPeriodicWorkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushPeriodicWorkManager.kt index ac265194..def09738 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushPeriodicWorkManager.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSEventFlushPeriodicWorkManager.kt @@ -28,8 +28,6 @@ internal class CSEventFlushPeriodicWorkManager private constructor( private const val REPEATED_INTERVAL = 6L fun enqueueWork(context: Context) { - CSServiceLocator.getInstance().logger.debug { "CSEventFlushPeriodicWorkManager#enqueueWork" } - PeriodicWorkRequest.Builder( CSBaseEventFlushWorkManager::class.java, REPEATED_INTERVAL, diff --git a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSWorkManager.kt b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSWorkManager.kt index 56b5ae3b..cd7400bd 100644 --- a/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSWorkManager.kt +++ b/clickstream/src/main/kotlin/clickstream/internal/workmanager/CSWorkManager.kt @@ -13,38 +13,41 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi */ @ExperimentalCoroutinesApi internal class CSWorkManager( - appLifeCycle: CSAppLifeCycle, - internal val context: Context, + appLifeCycleObserver: CSAppLifeCycle, + private val context: Context, private val eventSchedulerConfig: CSEventSchedulerConfig, private val logger: CSLogger, private val remoteConfig: CSRemoteConfig -) : CSLifeCycleManager(appLifeCycle) { +) : CSLifeCycleManager(appLifeCycleObserver) { init { - logger.debug { "CSWorkManager#init" } + logger.debug { "$tag#init" } addObserver() } + override val tag: String + get() = "CSWorkManager" + override fun onStart() { logger.debug { - "CSWorkManager#onStart : " + - "backgroundTaskEnabled ${eventSchedulerConfig.backgroundTaskEnabled}, " + - "isForegroundEventFlushEnabled ${remoteConfig.isForegroundEventFlushEnabled}" + "$tag#onStart -" + + "backgroundTaskEnabled ${eventSchedulerConfig.backgroundTaskEnabled}, " + + "isForegroundEventFlushEnabled ${remoteConfig.isForegroundEventFlushEnabled}" } - + cancelBackgroundWork() if (remoteConfig.isForegroundEventFlushEnabled && eventSchedulerConfig.backgroundTaskEnabled) { setupFutureWork() } } override fun onStop() { - logger.debug { "CSWorkManager#onStop" } + logger.debug { "$tag#onStop" } executeOneTimeWork() } internal fun executeOneTimeWork() { - logger.debug { "CSWorkManager#enqueueImmediateService - backgroundTaskEnabled ${eventSchedulerConfig.backgroundTaskEnabled}" } + logger.debug { "$tag#enqueueImmediateService - backgroundTaskEnabled ${eventSchedulerConfig.backgroundTaskEnabled}" } if (eventSchedulerConfig.backgroundTaskEnabled) { CSEventFlushOneTimeWorkManager.enqueueWork(context) @@ -52,8 +55,12 @@ internal class CSWorkManager( } private fun setupFutureWork() { - logger.debug { "CSWorkManager#setupFutureWork" } + logger.debug { "$tag#setupFutureWork" } CSEventFlushPeriodicWorkManager.enqueueWork(context) } + + private fun cancelBackgroundWork() { + CSEventFlushOneTimeWorkManager.cancelWork(context) + } } diff --git a/clickstream/src/main/kotlin/clickstream/report/CSReportData.kt b/clickstream/src/main/kotlin/clickstream/report/CSReportData.kt new file mode 100644 index 00000000..09f7eda9 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/report/CSReportData.kt @@ -0,0 +1,38 @@ +package clickstream.report + +/** + * data class for representing report related data. + * */ +public sealed class CSReportData(public open val date: String) { + + /** + * Batch is acknowledged successfully by server. + * */ + public class CSSuccess(override val date: String, public val batchId: String) : + CSReportData(date) + + /** + * Failed to acknowledge batch by server. + * */ + public class CSFailure( + override val date: String, + public val batchId: String, + public val exception: Throwable + ) : CSReportData(date) + + /** + * Duplicate event sent by clickstream sdk. + * */ + public class CSDuplicateEvents( + override val date: String, + public val batchId: String, + public val guid: String + ) : + CSReportData(date) + + /** + * Text message. + * */ + public class CSMessage(override val date: String, public val message: String) : + CSReportData(date) +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/report/CSReportDataListener.kt b/clickstream/src/main/kotlin/clickstream/report/CSReportDataListener.kt new file mode 100644 index 00000000..49d50ed4 --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/report/CSReportDataListener.kt @@ -0,0 +1,12 @@ +package clickstream.report + +/** + * Interface to communicate report related data to client app. + * */ +public interface CSReportDataListener { + + /** + * Callback for report data as [CSReportData]. + * */ + public fun onNewData(tag: String, data: CSReportData) +} \ No newline at end of file diff --git a/clickstream/src/main/kotlin/clickstream/report/CSReportDataTracker.kt b/clickstream/src/main/kotlin/clickstream/report/CSReportDataTracker.kt new file mode 100644 index 00000000..231f44cf --- /dev/null +++ b/clickstream/src/main/kotlin/clickstream/report/CSReportDataTracker.kt @@ -0,0 +1,60 @@ +package clickstream.report + +import clickstream.internal.eventscheduler.CSEventData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Calendar +import kotlin.collections.HashSet + +/** + * Helper class that tracks report related data using [CSReportDataListener] + * + * */ +internal class CSReportDataTracker(private val csReportDataListener: CSReportDataListener) { + + private val uniqueIdSet = HashSet() + + fun trackMessage(tag: String, message: String) { + csReportDataListener.onNewData( + tag, CSReportData.CSMessage(date = getDate(), message) + ) + } + + suspend fun trackDupData(tag: String, eventData: List) = + withContext(Dispatchers.Default) { + for (event in eventData) { + if (uniqueIdSet.contains(event.eventGuid)) { + csReportDataListener.onNewData( + tag, + CSReportData.CSDuplicateEvents( + getDate(), + event.eventRequestGuid ?: "", + event.eventGuid, + ) + ) + } else { + uniqueIdSet.add(event.eventGuid) + } + } + } + + fun trackSuccess(tag: String, guid: String) { + csReportDataListener.onNewData( + tag, CSReportData.CSSuccess(getDate(), guid) + ) + } + + fun trackFailure(tag: String, guid: String, exception: Throwable) { + csReportDataListener.onNewData( + tag, CSReportData.CSFailure(getDate(), guid, exception) + ) + } + + private fun getDate(): String { + val formatter = SimpleDateFormat("dd/MM/yyyy hh:mm:ss.SSS") + val calendar = Calendar.getInstance() + calendar.timeInMillis = System.currentTimeMillis() + return formatter.format(calendar.time) + } +} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/ClickStreamConnectionTest.kt b/clickstream/src/test/kotlin/clickstream/ClickStreamConnectionTest.kt index fed603db..e17e2e51 100644 --- a/clickstream/src/test/kotlin/clickstream/ClickStreamConnectionTest.kt +++ b/clickstream/src/test/kotlin/clickstream/ClickStreamConnectionTest.kt @@ -4,7 +4,6 @@ import clickstream.internal.eventscheduler.CSEventData import clickstream.internal.networklayer.CSEventService import clickstream.internal.utils.CSFlowStreamAdapterFactory import clickstream.internal.utils.CSTimeStampMessageBuilder -import clickstream.model.CSEvent import clickstream.utils.TestFlowObserver import clickstream.utils.any import clickstream.utils.containingBytes @@ -97,7 +96,7 @@ public class ClickStreamConnectionTest { ) .build() ) - val (eventData, eventHealthData) = CSEventData.create(event) + val eventData = CSEventData.create(event) return transformToEventRequest(eventData = listOf(eventData)) } diff --git a/clickstream/src/test/kotlin/clickstream/ClickStreamFunctionalTest.kt b/clickstream/src/test/kotlin/clickstream/ClickStreamFunctionalTest.kt index 34392e97..391d0981 100644 --- a/clickstream/src/test/kotlin/clickstream/ClickStreamFunctionalTest.kt +++ b/clickstream/src/test/kotlin/clickstream/ClickStreamFunctionalTest.kt @@ -7,27 +7,28 @@ import clickstream.api.CSAppInfo import clickstream.api.CSInfo import clickstream.api.CSLocationInfo import clickstream.api.CSSessionInfo +import clickstream.config.CSConfig import clickstream.config.CSConfiguration -import clickstream.extension.protoName -import clickstream.fake.FakeCSAppLifeCycle -import clickstream.fake.FakeHealthGateway -import clickstream.fake.createCSConfig +import clickstream.config.CSEventProcessorConfig +import clickstream.config.CSEventSchedulerConfig +import clickstream.config.CSNetworkConfig import clickstream.fake.fakeUserInfo import clickstream.internal.DefaultCSDeviceInfo -import clickstream.model.CSEvent import clickstream.utils.CoroutineTestRule import com.gojek.clickstream.common.App import com.gojek.clickstream.common.Customer import com.gojek.clickstream.common.EventMeta +import com.gojek.clickstream.common.Merchant +import com.gojek.clickstream.common.MerchantUser +import com.gojek.clickstream.common.MerchantUserRole import com.gojek.clickstream.products.events.AdCardEvent import com.google.protobuf.Timestamp import kotlinx.coroutines.ExperimentalCoroutinesApi -import okhttp3.OkHttpClient.Builder import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -38,28 +39,28 @@ public class ClickStreamFunctionalTest { @get:Rule public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - private val fakeHealthGateway = FakeHealthGateway(mock(), mock(), mock(), mock()) private val app = ApplicationProvider.getApplicationContext() + private lateinit var sut: ClickStream + + @Before + public fun setup() { + ClickStream.release() + } @Test public fun `Given EventMeta When Customer property is filled Then final generated EventMeta should have Customer metadata`() { // Given val userInfo = fakeUserInfo() val csInfo = createCSInfo().copy(userInfo = userInfo) - val appLifecycle = FakeCSAppLifeCycle() ClickStream.initialize( CSConfiguration.Builder( context = app, info = csInfo, - config = createCSConfig(), - appLifeCycle = appLifecycle, - ) - .setDispatcher(coroutineRule.testDispatcher) - .setHealthGateway(fakeHealthGateway) - .build() + config = createCSConfig() + ).setDispatcher(coroutineRule.testDispatcher).build() ) - val sut = ClickStream.getInstance() + sut = ClickStream.getInstance() // When val event = generateCSCustomerEvent("12") @@ -74,27 +75,162 @@ public class ClickStreamFunctionalTest { assertTrue((event.message as AdCardEvent).meta.hasMerchant().not()) } - private fun generateCSCustomerEvent(guid: String): CSEvent { + @Test + public fun `Given EventMeta for BytesEvent When Customer property is filled Then final generated EventMeta should have Customer metadata`() { + // Given val userInfo = fakeUserInfo() + val csInfo = createCSInfo().copy(userInfo = userInfo) + ClickStream.initialize( + CSConfiguration.Builder( + context = app, + info = csInfo, + config = createCSConfig() + ).setDispatcher(coroutineRule.testDispatcher).build() + ) + + sut = ClickStream.getInstance() + + // When + val adCardEvent = buildAdCardEventForCustomer() + val bytesEvent = CSBytesEvent( + "", + Timestamp.getDefaultInstance(), + adCardEvent.protoName(), + adCardEvent.toByteArray() + ) + + sut.trackEvent(bytesEvent, true) + + val event = AdCardEvent.newBuilder().mergeFrom(bytesEvent.eventData).build() + // Then + assertTrue(event.protoName() == "AdCardEvent") + assertTrue((event as AdCardEvent).meta.customer.email == userInfo.email) + assertTrue(event.meta.customer.currentCountry == userInfo.currentCountry) + assertTrue(event.meta.customer.signedUpCountry == userInfo.signedUpCountry) + assertTrue(event.meta.customer.identity == userInfo.identity) + assertTrue(event.meta.hasMerchant().not()) + } + + @Test + public fun `Given EventMeta When Merchant property is filled Then final generated EventMeta should have Merchant metadata`() { + // Given + val csInfo = createCSInfo() + ClickStream.initialize( + CSConfiguration.Builder( + context = app, + info = csInfo, + config = createCSConfig() + ).setDispatcher(coroutineRule.testDispatcher).build() + ) + sut = ClickStream.getInstance() + + // When + val event = generateCSMerchantEvent("12") + sut.trackEvent(event, true) + + // Then + assertTrue(event.message.protoName() == "AdCardEvent") + assertTrue((event.message as AdCardEvent).meta.hasMerchant()) + assertTrue((event.message as AdCardEvent).meta.merchant.saudagarId == "1") + assertTrue((event.message as AdCardEvent).meta.merchant.user.role == MerchantUserRole.MERCHANT_USER_ROLE_ADMIN) + assertTrue((event.message as AdCardEvent).meta.merchant.user.signedUpCountry == "ID") + assertTrue((event.message as AdCardEvent).meta.merchant.user.phone == "085") + assertTrue((event.message as AdCardEvent).meta.merchant.user.identity == 12) + assertTrue((event.message as AdCardEvent).meta.merchant.user.email == "test@gmail.com") + assertTrue((event.message as AdCardEvent).meta.hasCustomer().not()) + } + + @Test + public fun `Given EventMeta for BytesEvent When Merchant property is filled Then final generated EventMeta should have Merchant metadata`() { + // Given + val csInfo = createCSInfo() + ClickStream.initialize( + CSConfiguration.Builder( + context = app, + info = csInfo, + config = createCSConfig() + ).setDispatcher(coroutineRule.testDispatcher).build() + ) + sut = ClickStream.getInstance() + + // When + val adCardEvent = buildAdCardEventForMerchant() + val bytesEvent = CSBytesEvent( + "", + Timestamp.getDefaultInstance(), + adCardEvent.protoName(), + adCardEvent.toByteArray() + ) + sut.trackEvent(bytesEvent, true) + + val event = AdCardEvent.newBuilder().mergeFrom(bytesEvent.eventData).build() + // Then + assertTrue(event.protoName() == "AdCardEvent") + assertTrue((event as AdCardEvent).meta.hasMerchant()) + assertTrue(event.meta.merchant.saudagarId == "1") + assertTrue(event.meta.merchant.user.role == MerchantUserRole.MERCHANT_USER_ROLE_ADMIN) + assertTrue(event.meta.merchant.user.signedUpCountry == "ID") + assertTrue(event.meta.merchant.user.phone == "085") + assertTrue(event.meta.merchant.user.identity == 12) + assertTrue(event.meta.merchant.user.email == "test@gmail.com") + assertTrue(event.meta.hasCustomer().not()) + } + + private fun generateCSMerchantEvent(guid: String): CSEvent { return CSEvent( guid = guid, timestamp = Timestamp.getDefaultInstance(), - message = AdCardEvent.newBuilder() - .setMeta( - EventMeta.newBuilder() - .setApp(App.newBuilder().setVersion("4.35.0")) - .setCustomer( - Customer.newBuilder() - .setCurrentCountry(userInfo.currentCountry) - .setEmail(userInfo.email) - .setIdentity(userInfo.identity) - .setSignedUpCountry(userInfo.signedUpCountry) + message = buildAdCardEventForMerchant() + ) + } + + private fun buildAdCardEventForMerchant() = AdCardEvent.newBuilder() + .setMeta( + EventMeta.newBuilder() + .setApp(App.newBuilder().setVersion("4.35.0")) + .setMerchant( + Merchant.newBuilder() + .setSaudagarId("1") + .setUser( + MerchantUser.newBuilder() + .setRole(MerchantUserRole.MERCHANT_USER_ROLE_ADMIN) + .setSignedUpCountry("ID") + .setPhone("085") + .setIdentity(12) + .setEmail("test@gmail.com") .build() ) .build() ) .build() ) + .build() + + private fun generateCSCustomerEvent(guid: String): CSEvent { + return CSEvent( + guid = guid, + timestamp = Timestamp.getDefaultInstance(), + message = buildAdCardEventForCustomer() + ) + } + + private fun buildAdCardEventForCustomer(): AdCardEvent { + val userInfo = fakeUserInfo() + return AdCardEvent.newBuilder() + .setMeta( + EventMeta.newBuilder() + .setApp(App.newBuilder().setVersion("4.35.0")) + .setCustomer( + Customer.newBuilder() + .setCurrentCountry(userInfo.currentCountry) + .setEmail(userInfo.email) + .setIdentity(userInfo.identity) + .setSignedUpCountry(userInfo.signedUpCountry) + .build() + ) + .build() + ) + .build() } private fun createCSInfo(): CSInfo { @@ -108,4 +244,15 @@ public class ClickStreamFunctionalTest { deviceInfo = DefaultCSDeviceInfo(), fakeUserInfo() ) } + + private fun createCSConfig(): CSConfig { + return CSConfig( + eventProcessorConfiguration = CSEventProcessorConfig( + realtimeEvents = emptyList(), + instantEvent = listOf("AdCardEvent") + ), + eventSchedulerConfig = CSEventSchedulerConfig.default(), + networkConfig = CSNetworkConfig.default("", mapOf()), + ) + } } diff --git a/clickstream/src/test/kotlin/clickstream/ClickStreamMerchantFunctionalTest.kt b/clickstream/src/test/kotlin/clickstream/ClickStreamMerchantFunctionalTest.kt deleted file mode 100644 index df16fa0c..00000000 --- a/clickstream/src/test/kotlin/clickstream/ClickStreamMerchantFunctionalTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package clickstream - -import android.app.Application -import android.os.Build.VERSION_CODES -import androidx.test.core.app.ApplicationProvider -import clickstream.api.CSAppInfo -import clickstream.api.CSInfo -import clickstream.api.CSLocationInfo -import clickstream.api.CSSessionInfo -import clickstream.config.CSConfiguration -import clickstream.extension.protoName -import clickstream.fake.FakeCSAppLifeCycle -import clickstream.fake.FakeHealthGateway -import clickstream.fake.createCSConfig -import clickstream.fake.fakeUserInfo -import clickstream.internal.DefaultCSDeviceInfo -import clickstream.model.CSEvent -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.common.App -import com.gojek.clickstream.common.EventMeta -import com.gojek.clickstream.common.Merchant -import com.gojek.clickstream.common.MerchantUser -import com.gojek.clickstream.common.MerchantUserRole -import com.gojek.clickstream.products.events.AdCardEvent -import com.google.protobuf.Timestamp -import kotlinx.coroutines.ExperimentalCoroutinesApi -import okhttp3.OkHttpClient.Builder -import org.junit.Assert.assertTrue -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, sdk = [VERSION_CODES.P]) -@Ignore -public class ClickStreamMerchantFunctionalTest { - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val fakeHealthGateway = FakeHealthGateway(mock(), mock(), mock(), mock()) - private val app = ApplicationProvider.getApplicationContext() - - @Test - public fun `Given EventMeta When Merchant property is filled Then final generated EventMeta should have Merchant metadata`() { - // Given - val csInfo = createCSInfo() - val appLifecycle = FakeCSAppLifeCycle() - ClickStream.initialize( - CSConfiguration.Builder( - context = app, - info = csInfo, - config = createCSConfig(), - appLifeCycle = appLifecycle - ) - .setDispatcher(coroutineRule.testDispatcher) - .setHealthGateway(fakeHealthGateway) - .build() - ) - val sut = ClickStream.getInstance() - - // When - val event = generateCSMerchantEvent("12") - sut.trackEvent(event, true) - - // Then - assertTrue(event.message.protoName() == "AdCardEvent") - assertTrue((event.message as AdCardEvent).meta.hasMerchant()) - assertTrue((event.message as AdCardEvent).meta.merchant.saudagarId == "1") - assertTrue((event.message as AdCardEvent).meta.merchant.user.role == MerchantUserRole.MERCHANT_USER_ROLE_ADMIN) - assertTrue((event.message as AdCardEvent).meta.merchant.user.signedUpCountry == "ID") - assertTrue((event.message as AdCardEvent).meta.merchant.user.phone == "085") - assertTrue((event.message as AdCardEvent).meta.merchant.user.identity == 12) - assertTrue((event.message as AdCardEvent).meta.merchant.user.email == "test@gmail.com") - assertTrue((event.message as AdCardEvent).meta.hasCustomer().not()) - } - - private fun generateCSMerchantEvent(guid: String): CSEvent { - return CSEvent( - guid = guid, - timestamp = Timestamp.getDefaultInstance(), - message = AdCardEvent.newBuilder() - .setMeta( - EventMeta.newBuilder() - .setApp(App.newBuilder().setVersion("4.35.0")) - .setMerchant( - Merchant.newBuilder() - .setSaudagarId("1") - .setUser( - MerchantUser.newBuilder() - .setRole(MerchantUserRole.MERCHANT_USER_ROLE_ADMIN) - .setSignedUpCountry("ID") - .setPhone("085") - .setIdentity(12) - .setEmail("test@gmail.com") - .build() - ) - .build() - ) - .build() - ) - .build() - ) - } - - private fun createCSInfo(): CSInfo { - return CSInfo( - appInfo = CSAppInfo(appVersion = "4.37.0"), - locationInfo = CSLocationInfo( - latitude = -6.1753924, - longitude = 106.8249641, - s2Ids = emptyMap() - ), sessionInfo = CSSessionInfo(sessionID = "1234"), - deviceInfo = DefaultCSDeviceInfo(), fakeUserInfo() - ) - } -} diff --git a/clickstream/src/test/kotlin/clickstream/extension/CSEventDataTest.kt b/clickstream/src/test/kotlin/clickstream/extension/CSEventDataTest.kt new file mode 100644 index 00000000..eb31b6c4 --- /dev/null +++ b/clickstream/src/test/kotlin/clickstream/extension/CSEventDataTest.kt @@ -0,0 +1,19 @@ +package clickstream.extension + +import clickstream.fake.defaultEventWrapperData +import clickstream.internal.eventscheduler.CSEventData +import org.junit.Test + +internal class CSEventDataTest { + + @Test + fun `Given event prefix check if event function returns Event with correct type`() { + + val csEventData = CSEventData.create(defaultEventWrapperData()) + val eventWithoutPrefix = csEventData.event() + val eventWithPrefix = csEventData.event("gobiz") + + assert(eventWithoutPrefix.type == "adcardevent") + assert(eventWithPrefix.type == "gobiz-adcardevent") + } +} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/extension/CSMessageExtTest.kt b/clickstream/src/test/kotlin/clickstream/extension/CSMessageExtTest.kt index 7071412f..21a37298 100644 --- a/clickstream/src/test/kotlin/clickstream/extension/CSMessageExtTest.kt +++ b/clickstream/src/test/kotlin/clickstream/extension/CSMessageExtTest.kt @@ -1,10 +1,13 @@ package clickstream.extension -import com.gojek.clickstream.products.common.* +import com.gojek.clickstream.products.common.Address +import com.gojek.clickstream.products.common.AppType +import com.gojek.clickstream.products.common.Outlet import com.gojek.clickstream.products.telemetry.Protocol import com.gojek.clickstream.products.telemetry.PubSubHealth import com.gojek.clickstream.products.telemetry.QOS import com.gojek.clickstream.products.telemetry.Topic +import clickstream.toFlatMap import org.junit.Test internal class CSMessageExtTest { @@ -27,7 +30,6 @@ internal class CSMessageExtTest { assert(addressMap["pillPosition"] == 5) assert(addressMap["changedAddressSourceOnUi"] == true) assert(addressMap["locationDetails"] == "location details") - } @Test @@ -42,7 +44,6 @@ internal class CSMessageExtTest { setTopic(topic) protocol = Protocol.PROTOCOL_MQTT appType = AppType.Consumer - }.build() pubSubHealth.toFlatMap().run { @@ -68,4 +69,4 @@ internal class CSMessageExtTest { } } } -} \ No newline at end of file +} diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppLifeCycle.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppLifeCycle.kt deleted file mode 100644 index 9a2d02a1..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppLifeCycle.kt +++ /dev/null @@ -1,13 +0,0 @@ -package clickstream.fake - -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.lifecycle.CSAppLifeCycleObserver - -internal class FakeCSAppLifeCycle : CSAppLifeCycle { - - val observers = mutableListOf() - - override fun addObserver(observer: CSAppLifeCycleObserver) { - observers.add(observer) - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppVersionSharedPref.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppVersionSharedPref.kt deleted file mode 100644 index 104c2942..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSAppVersionSharedPref.kt +++ /dev/null @@ -1,11 +0,0 @@ -package clickstream.fake - -import clickstream.util.CSAppVersionSharedPref - -internal class FakeCSAppVersionSharedPref( - private val value: Boolean -) : CSAppVersionSharedPref { - override suspend fun isAppVersionEqual(currentAppVersion: String): Boolean { - return value - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSConfig.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSConfig.kt deleted file mode 100644 index 03ce81c7..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSConfig.kt +++ /dev/null @@ -1,20 +0,0 @@ -package clickstream.fake - -import clickstream.config.CSConfig -import clickstream.config.CSEventProcessorConfig -import clickstream.config.CSEventSchedulerConfig -import clickstream.config.CSNetworkConfig -import clickstream.health.constant.CSTrackedVia -import clickstream.health.model.CSHealthEventConfig - -internal fun createCSConfig(): CSConfig { - return CSConfig( - eventProcessorConfiguration = CSEventProcessorConfig( - realtimeEvents = emptyList(), - instantEvent = listOf("AdCardEvent") - ), - eventSchedulerConfig = CSEventSchedulerConfig.default(), - networkConfig = CSNetworkConfig.default(createOkHttpClient()).copy(endPoint = ""), - healthEventConfig = CSHealthEventConfig.default(CSTrackedVia.Both) - ) -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSEventListener.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSEventListener.kt deleted file mode 100644 index 9fb7d6cd..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSEventListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package clickstream.fake - -import clickstream.listener.CSEventListener -import clickstream.listener.CSEventModel - -internal class FakeCSEventListener : CSEventListener { - - var isCalled = false - - override fun onCall(events: List) { - isCalled = true - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt index 320792e3..ea27763e 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventConfig.kt @@ -1,10 +1,10 @@ package clickstream.fake -import clickstream.health.constant.CSTrackedVia import clickstream.health.model.CSHealthEventConfig internal val fakeCSHealthEventConfig = CSHealthEventConfig( - minimumTrackedVersion = "4.37.0", - randomisingUserIdRemainders = listOf(123453, 5), - trackedVia = CSTrackedVia.Both + minTrackedVersion = "1.0.0", + randomUserIdRemainder = listOf(), + destination = listOf("CS", "CT"), + verbosityLevel = "min" ) \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventDTO.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventDTO.kt deleted file mode 100644 index 7fb03f72..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventDTO.kt +++ /dev/null @@ -1,44 +0,0 @@ -package clickstream.fake - -import clickstream.api.CSMetaProvider -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.model.CSHealthEventDTO - -internal fun fakeCSHealthEventDTOs( - csMetaProvider: CSMetaProvider -): List { - val list = mutableListOf() - - CSHealthEventDTO( - eventName = CSEventNamesConstant.Flushed.ClickStreamEventReceived.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = "1", - appVersion = csMetaProvider.app.version, - timeToConnection = 2, - error = "no error", - eventBatchGuid = "1" - ).let(list::add) - - CSHealthEventDTO( - eventName = CSEventNamesConstant.Flushed.ClickStreamEventReceived.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = "2", - appVersion = csMetaProvider.app.version, - timeToConnection = 3, - error = "no error", - eventBatchGuid = "2" - ).let(list::add) - - CSHealthEventDTO( - eventName = CSEventNamesConstant.Flushed.ClickStreamEventReceived.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = "3", - appVersion = csMetaProvider.app.version, - timeToConnection = 4, - error = "no error", - eventBatchGuid = "3" - ).let(list::add) - - return list -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventFactory.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventFactory.kt deleted file mode 100644 index 6ebc051a..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventFactory.kt +++ /dev/null @@ -1,10 +0,0 @@ -package clickstream.fake - -import clickstream.health.intermediate.CSHealthEventFactory -import com.gojek.clickstream.internal.Health - -internal class FakeCSHealthEventFactory : CSHealthEventFactory { - override suspend fun create(message: Health): Health { - return message - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventLoggerListener.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventLoggerListener.kt deleted file mode 100644 index 7f62428e..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventLoggerListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package clickstream.fake - -import clickstream.health.intermediate.CSHealthEventLoggerListener -import clickstream.health.model.CSHealthEvent - -internal class FakeCSHealthEventLoggerListener : CSHealthEventLoggerListener { - - val record = mutableMapOf() - - override fun logEvent(eventName: String, healthEvent: CSHealthEvent) { - record[eventName] = healthEvent - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventProcessor.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventProcessor.kt deleted file mode 100644 index aaf879a4..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventProcessor.kt +++ /dev/null @@ -1,45 +0,0 @@ -package clickstream.fake - -import clickstream.api.CSAppInfo -import clickstream.health.identity.DefaultCSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.internal.DefaultCSHealthEventFactory -import clickstream.health.internal.DefaultCSHealthEventProcessor -import clickstream.health.time.CSTimeStampGenerator -import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import kotlinx.coroutines.CoroutineDispatcher - -internal val fakeCSMetaProvider = FakeCSMetaProvider() -internal val fakeCSHealthEventDTOs = fakeCSHealthEventDTOs(fakeCSMetaProvider) -internal val fakeCSInfo = fakeCSInfo(fakeCSMetaProvider) -internal val fakeCSAppLifeCycle = FakeCSAppLifeCycle() -internal val fakeCSHealthEventRepository = FakeCSHealthEventRepository(fakeCSHealthEventDTOs) -internal val fakeCSHealthEventLoggerListener = FakeCSHealthEventLoggerListener() -internal val fakeCSHealthEventFactory = DefaultCSHealthEventFactory( - guIdGenerator = DefaultCSGuIdGenerator(), - timeStampGenerator = object : CSTimeStampGenerator { - override fun getTimeStamp(): Long { - return 1 - } - }, - metaProvider = fakeCSMetaProvider -) -internal val fakeCSAppVersionSharedPref = FakeCSAppVersionSharedPref(true) - -internal fun FakeCSHealthEventProcessor( - dispatcher: CoroutineDispatcher -): CSHealthEventProcessor { - return DefaultCSHealthEventProcessor( - appLifeCycleObserver = fakeCSAppLifeCycle, - healthEventRepository = fakeCSHealthEventRepository, - dispatcher = dispatcher, - healthEventConfig = fakeCSHealthEventConfig, - info = fakeCSInfo.copy(appInfo = CSAppInfo(fakeCSMetaProvider.app.version)), - logger = CSLogger(CSLogLevel.OFF), - healthEventLoggerListener = fakeCSHealthEventLoggerListener, - healthEventFactory = fakeCSHealthEventFactory, - appVersion = fakeCSMetaProvider.app.version, - appVersionPreference = fakeCSAppVersionSharedPref - ) -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventRepository.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventRepository.kt deleted file mode 100644 index ee5fce7c..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSHealthEventRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package clickstream.fake - -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO - -internal class FakeCSHealthEventRepository( - private val fakeHealthEvents: List -) : CSHealthEventRepository { - - private val stubbedHealthEvents = mutableListOf() - .apply { - addAll(fakeHealthEvents) - } - - override suspend fun insertHealthEvent(healthEvent: CSHealthEventDTO) { - stubbedHealthEvents.add(healthEvent) - } - - override suspend fun insertHealthEventList(healthEventList: List) { - stubbedHealthEvents.addAll(healthEventList) - } - - override suspend fun getInstantEvents(): List { - return stubbedHealthEvents - } - - override suspend fun getAggregateEvents(): List { - return stubbedHealthEvents - } - - override suspend fun deleteHealthEventsBySessionId(sessionId: String) { - val events = stubbedHealthEvents.filter { it.sessionId == sessionId } - stubbedHealthEvents.removeAll(events) - } - - override suspend fun deleteHealthEvents(events: List) { - stubbedHealthEvents.removeAll(events) - } -} diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSInfo.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSInfo.kt index a8b1f827..a94e08aa 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSInfo.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeCSInfo.kt @@ -1,80 +1,14 @@ package clickstream.fake -import clickstream.api.CSAppInfo import clickstream.api.CSDeviceInfo import clickstream.api.CSInfo -import clickstream.api.CSLocationInfo -import clickstream.api.CSMetaProvider -import clickstream.api.CSSessionInfo -import clickstream.api.CSUserInfo -import kotlinx.coroutines.runBlocking internal fun fakeCSInfo( - csMetaProvider: CSMetaProvider? = null, deviceInfo: CSDeviceInfo? = null ) = CSInfo( - deviceInfo = deviceInfo ?: buildDeviceInfo(csMetaProvider), - userInfo = buildUserInfo(csMetaProvider), - locationInfo = buildLocationInfo(csMetaProvider), - appInfo = buildAppInfo(csMetaProvider), - sessionInfo = buildSessionInfo(csMetaProvider) -) - -private fun buildDeviceInfo(csMetaProvider: CSMetaProvider?): CSDeviceInfo { - return if (csMetaProvider != null) { - object : CSDeviceInfo { - override fun getDeviceManufacturer(): String = csMetaProvider.device.deviceMake - override fun getDeviceModel(): String = csMetaProvider.device.deviceModel - override fun getSDKVersion(): String = csMetaProvider.device.operatingSystemVersion - override fun getOperatingSystem(): String = csMetaProvider.device.operatingSystem - override fun getDeviceHeight(): String = "1" - override fun getDeviceWidth(): String = "2" - } - } else { - fakeDeviceInfo() - } -} - - -private fun buildSessionInfo(csMetaProvider: CSMetaProvider?): CSSessionInfo { - return if (csMetaProvider != null) { - CSSessionInfo(sessionID = csMetaProvider.session.sessionId) - } else { - fakeCSSessionInfo - } -} - -private fun buildAppInfo(csMetaProvider: CSMetaProvider?): CSAppInfo { - return if (csMetaProvider != null) { - CSAppInfo(appVersion = csMetaProvider.app.version) - } else { - fakeAppInfo - } -} - -private fun buildLocationInfo(csMetaProvider: CSMetaProvider?): CSLocationInfo { - return if (csMetaProvider != null) { - runBlocking { - CSLocationInfo( - latitude = csMetaProvider.location().latitude, - longitude = csMetaProvider.location().longitude, - s2Ids = emptyMap() - ) - } - } else { - fakeLocationInfo - } -} - -private fun buildUserInfo(csMetaProvider: CSMetaProvider?): CSUserInfo { - return if (csMetaProvider != null) { - CSUserInfo( - currentCountry = csMetaProvider.customer.currentCountry, - signedUpCountry = csMetaProvider.customer.signedUpCountry, - identity = csMetaProvider.customer.identity, - email = csMetaProvider.customer.email - ) - } else { - fakeUserInfo() - } -} \ No newline at end of file + deviceInfo = deviceInfo ?: fakeDeviceInfo(), + userInfo = fakeUserInfo(), + locationInfo = fakeLocationInfo, + appInfo = fakeAppInfo, + sessionInfo = fakeCSSessionInfo +) \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeCSMetaProvider.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeCSMetaProvider.kt deleted file mode 100644 index d6421e89..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeCSMetaProvider.kt +++ /dev/null @@ -1,39 +0,0 @@ -package clickstream.fake - -import clickstream.api.CSMetaProvider -import com.gojek.clickstream.internal.HealthMeta - -internal class FakeCSMetaProvider : CSMetaProvider { - override suspend fun location(): HealthMeta.Location { - return HealthMeta.Location.newBuilder() - .setLatitude(-6.1753924) - .setLongitude(106.8249641) - .build() - } - - override val customer: HealthMeta.Customer - get() = HealthMeta.Customer.newBuilder() - .setCurrentCountry("ID") - .setEmail("test@gmail.com") - .setIdentity(12) - .setSignedUpCountry("ID") - .build() - - override val app: HealthMeta.App - get() = HealthMeta.App.newBuilder() - .setVersion("4.37.0") - .build() - - override val device: HealthMeta.Device - get() = HealthMeta.Device.newBuilder() - .setDeviceMake("Samsung") - .setDeviceModel("SM-900") - .setOperatingSystem("Android") - .setOperatingSystemVersion("10") - .build() - - override val session: HealthMeta.Session - get() = HealthMeta.Session.newBuilder() - .setSessionId("12345678910") - .build() -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeClickStream.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeClickStream.kt index 90994f5d..a737f680 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeClickStream.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeClickStream.kt @@ -1,7 +1,7 @@ package clickstream.fake +import clickstream.CSEvent import clickstream.ClickStream -import clickstream.model.CSEvent public class FakeClickStream( private val clickStream: ClickStream diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeEventBatchDao.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeEventBatchDao.kt index 93f2590c..912090cd 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeEventBatchDao.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeEventBatchDao.kt @@ -30,9 +30,8 @@ public class FakeEventBatchDao( return items } - override suspend fun loadOnGoingEvents(): List { - return items.filter { it.isOnGoing } - } + override suspend fun loadOnGoingEvents(): List = + items.filter { it.isOnGoing } override suspend fun insert(eventData: CSEventData) { items.add(eventData) @@ -46,13 +45,39 @@ public class FakeEventBatchDao( items.removeIf { it.eventRequestGuid == eventBatchGuId } } - override suspend fun loadEventByRequestId(guid: String): List { - return items.filter { it.eventRequestGuid == guid } - } - override suspend fun setOnGoingEvent(guid: String, ongoing: Boolean) { val newList = items.map { it.copy(isOnGoing = ongoing) }.toList() items.clear() items.addAll(newList) } + + override suspend fun loadEventByRequestId(guid: String): List { + return items.filter { it.eventRequestGuid == guid } ?: emptyList() + } + + override suspend fun updateAll(eventDataList: List) { + eventDataList.forEach { updatedEvent -> + val index = items.indexOfFirst { updatedEvent.eventGuid == it.eventGuid } + if (index != -1) { + items[index] = updatedEvent + } + } + } + + override suspend fun getUnprocessedEventsWithLimit(limit: Int): List { + val unProcessedEvents = items.filter { !it.isOnGoing } + return if (unProcessedEvents.size > limit) unProcessedEvents.subList(0, limit) else unProcessedEvents + } + + override suspend fun getAllUnprocessedEvents(): List { + return items.filter { !it.isOnGoing } + } + + override suspend fun getAllUnprocessedEventsCount(): Int { + return items.filter { !it.isOnGoing }.size + } + + override suspend fun getEventCount(): Int { + return items.size + } } diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeEventWrapperData.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeEventWrapperData.kt index 2cb66763..1abe0e46 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeEventWrapperData.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeEventWrapperData.kt @@ -1,7 +1,8 @@ package clickstream.fake +import clickstream.CSBytesEvent +import clickstream.CSEvent import clickstream.internal.utils.CSTimeStampMessageBuilder -import clickstream.model.CSEvent import com.gojek.clickstream.common.Customer import com.gojek.clickstream.common.Device import com.gojek.clickstream.common.Location @@ -16,12 +17,12 @@ import java.util.UUID * Generates a ClickStreamEventWrapper data * with default data every time invoked. */ -public fun defaultEventWrapperData(): CSEvent { +public fun defaultEventWrapperData(uuid: String = UUID.randomUUID().toString()): CSEvent { val event = AdCardEvent.newBuilder().apply { meta = meta.toBuilder().apply { - val objectID = UUID.randomUUID().toString() - eventGuid = objectID + eventGuid = uuid eventTimestamp = CSTimeStampMessageBuilder.build(System.currentTimeMillis()) + location = Location.getDefaultInstance() device = Device.getDefaultInstance() customer = Customer.getDefaultInstance() @@ -38,3 +39,27 @@ public fun defaultEventWrapperData(): CSEvent { message = event ) } + +public fun defaultBytesEventWrapperData(uuid: String = UUID.randomUUID().toString()): CSBytesEvent { + val event = AdCardEvent.newBuilder().apply { + meta = meta.toBuilder().apply { + eventGuid = uuid + eventTimestamp = CSTimeStampMessageBuilder.build(System.currentTimeMillis()) + + location = Location.getDefaultInstance() + device = Device.getDefaultInstance() + customer = Customer.getDefaultInstance() + session = Session.getDefaultInstance() + }.build() + type = AdCardType.Clicked + shuffleCard = ShuffleCard.getDefaultInstance() + serviceInfo = ServiceInfo.getDefaultInstance() + }.build() + + return CSBytesEvent( + guid = event.meta.eventGuid, + timestamp = event.eventTimestamp, + eventName = "AdCardEvent", + eventData = event.toByteArray() + ) +} diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeHealthGateway.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeHealthGateway.kt deleted file mode 100644 index ac46547b..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeHealthGateway.kt +++ /dev/null @@ -1,14 +0,0 @@ -package clickstream.fake - -import clickstream.health.CSHealthGateway -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventFactory -import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository - -internal class FakeHealthGateway( - override val eventHealthListener: CSEventHealthListener, - override val healthEventRepository: CSHealthEventRepository, - override val healthEventProcessor: CSHealthEventProcessor, - override val healthEventFactory: CSHealthEventFactory, -) : CSHealthGateway \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeInfo.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeInfo.kt index 58a75ea8..ba734cb0 100644 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeInfo.kt +++ b/clickstream/src/test/kotlin/clickstream/fake/FakeInfo.kt @@ -47,6 +47,7 @@ internal fun fakeSessionInfo( } internal fun fakeDeviceInfo(): CSDeviceInfo { + return object : CSDeviceInfo { override fun getDeviceManufacturer(): String = "Samsung" override fun getDeviceModel(): String = "IPhone X" diff --git a/clickstream/src/test/kotlin/clickstream/fake/FakeOkHttpClient.kt b/clickstream/src/test/kotlin/clickstream/fake/FakeOkHttpClient.kt deleted file mode 100644 index fc1f6e38..00000000 --- a/clickstream/src/test/kotlin/clickstream/fake/FakeOkHttpClient.kt +++ /dev/null @@ -1,11 +0,0 @@ -package clickstream.fake - -import java.util.concurrent.TimeUnit -import okhttp3.OkHttpClient - -internal fun createOkHttpClient(): OkHttpClient { - return OkHttpClient.Builder() - .writeTimeout(500, TimeUnit.MILLISECONDS) - .readTimeout(500, TimeUnit.MILLISECONDS) - .build() -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessorTest.kt b/clickstream/src/test/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessorTest.kt deleted file mode 100644 index 7edadf23..00000000 --- a/clickstream/src/test/kotlin/clickstream/health/internal/DefaultCSHealthEventProcessorTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -package clickstream.health.internal - -import clickstream.api.CSAppInfo -import clickstream.fake.FakeCSAppLifeCycle -import clickstream.fake.FakeCSAppVersionSharedPref -import clickstream.fake.FakeCSHealthEventLoggerListener -import clickstream.fake.FakeCSHealthEventRepository -import clickstream.fake.FakeCSMetaProvider -import clickstream.fake.fakeCSHealthEventConfig -import clickstream.fake.fakeCSHealthEventDTOs -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.identity.DefaultCSGuIdGenerator -import clickstream.health.time.CSTimeStampGenerator -import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import clickstream.utils.CoroutineTestRule -import java.util.regex.Pattern -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(MockitoJUnitRunner::class) -public class DefaultCSHealthEventProcessorTest { - - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val fakeCSMetaProvider = FakeCSMetaProvider() - private val fakeCSHealthEventDTOs = fakeCSHealthEventDTOs(fakeCSMetaProvider) - private val fakeCSInfo = fakeCSInfo(fakeCSMetaProvider) - private val fakeCSAppLifeCycle = FakeCSAppLifeCycle() - private val fakeCSHealthEventRepository = FakeCSHealthEventRepository(fakeCSHealthEventDTOs) - private val fakeCSHealthEventLoggerListener = FakeCSHealthEventLoggerListener() - private val fakeCSHealthEventFactory = DefaultCSHealthEventFactory( - guIdGenerator = DefaultCSGuIdGenerator(), - timeStampGenerator = object : CSTimeStampGenerator { - override fun getTimeStamp(): Long { - return 1 - } - }, - metaProvider = fakeCSMetaProvider - ) - private val uuidRegex = Pattern.compile("[a-f0-9]{8}(?:-[a-f0-9]{4}){4}[a-f0-9]{8}") - private val fakeCSAppVersionSharedPref = FakeCSAppVersionSharedPref(true) - private lateinit var sut: DefaultCSHealthEventProcessor - - @Before - public fun setup() { - sut = DefaultCSHealthEventProcessor( - appLifeCycleObserver = fakeCSAppLifeCycle, - healthEventRepository = fakeCSHealthEventRepository, - dispatcher = coroutineRule.testDispatcher, - healthEventConfig = fakeCSHealthEventConfig, - info = fakeCSInfo.copy(appInfo = CSAppInfo(fakeCSMetaProvider.app.version)), - logger = CSLogger(CSLogLevel.OFF), - healthEventLoggerListener = fakeCSHealthEventLoggerListener, - healthEventFactory = fakeCSHealthEventFactory, - appVersion = fakeCSMetaProvider.app.version, - appVersionPreference = fakeCSAppVersionSharedPref - ) - } - - @Test - public fun `verify getAggregate events`() { - coroutineRule.testDispatcher.runBlockingTest { - val events = sut.getAggregateEvents() - assertTrue(events.size == 1) - - events.forEachIndexed { index, event -> - assertTrue(event.eventName == CSEventNamesConstant.Flushed.ClickStreamEventReceived.value) - assertTrue(event.healthDetails.eventBatchGuidsList.containsAll(fakeCSHealthEventDTOs.map { it.eventBatchGuid })) - assertTrue(event.healthDetails.eventGuidsList.containsAll(fakeCSHealthEventDTOs.map { it.eventGuid })) - assertTrue(event.healthMeta.app.version == fakeCSHealthEventDTOs[index].appVersion) - assertTrue(event.healthMeta.customer.currentCountry == fakeCSInfo.userInfo.currentCountry) - assertTrue(event.healthMeta.customer.email == fakeCSInfo.userInfo.email) - assertTrue(event.healthMeta.customer.identity == fakeCSInfo.userInfo.identity) - assertTrue(event.healthMeta.customer.signedUpCountry == fakeCSInfo.userInfo.signedUpCountry) - assertTrue(event.healthMeta.device.deviceMake == fakeCSInfo.deviceInfo.getDeviceManufacturer()) - assertTrue(event.healthMeta.device.deviceModel == fakeCSInfo.deviceInfo.getDeviceModel()) - assertTrue(event.healthMeta.device.operatingSystem == fakeCSInfo.deviceInfo.getOperatingSystem()) - assertTrue(event.healthMeta.device.operatingSystemVersion == fakeCSInfo.deviceInfo.getSDKVersion()) - uuidRegex.matcher(event.healthMeta.eventGuid).matches() - assertTrue(event.healthMeta.location.latitude == fakeCSInfo.locationInfo.latitude) - assertTrue(event.healthMeta.location.longitude == fakeCSInfo.locationInfo.longitude) - assertTrue(event.healthMeta.session.sessionId == fakeCSInfo.sessionInfo.sessionID) - assertTrue(event.numberOfBatches == fakeCSHealthEventDTOs.map { it.eventBatchGuid }.size.toLong()) - assertTrue(event.numberOfEvents == fakeCSHealthEventDTOs.map { it.eventGuid }.size.toLong()) - assertFalse(event.traceDetails.hasErrorDetails()) - } - } - } - - @Test - public fun `verify getInstant events`() { - coroutineRule.testDispatcher.runBlockingTest { - val events = sut.getInstantEvents() - assertTrue(events.isNotEmpty()) - - events.forEachIndexed { index, event -> - assertTrue(event.eventName == CSEventNamesConstant.Flushed.ClickStreamEventReceived.value) - assertTrue(event.healthDetails.eventGuidsList.contains(fakeCSHealthEventDTOs[index].eventGuid)) - assertTrue(event.healthMeta.app.version == fakeCSHealthEventDTOs[index].appVersion) - assertTrue(event.healthMeta.customer.currentCountry == fakeCSInfo.userInfo.currentCountry) - assertTrue(event.healthMeta.customer.email == fakeCSInfo.userInfo.email) - assertTrue(event.healthMeta.customer.identity == fakeCSInfo.userInfo.identity) - assertTrue(event.healthMeta.customer.signedUpCountry == fakeCSInfo.userInfo.signedUpCountry) - assertTrue(event.healthMeta.device.deviceMake == fakeCSInfo.deviceInfo.getDeviceManufacturer()) - assertTrue(event.healthMeta.device.deviceModel == fakeCSInfo.deviceInfo.getDeviceModel()) - assertTrue(event.healthMeta.device.operatingSystem == fakeCSInfo.deviceInfo.getOperatingSystem()) - assertTrue(event.healthMeta.device.operatingSystemVersion == fakeCSInfo.deviceInfo.getSDKVersion()) - uuidRegex.matcher(event.healthMeta.eventGuid).matches() - assertTrue(event.healthMeta.location.latitude == fakeCSInfo.locationInfo.latitude) - assertTrue(event.healthMeta.location.longitude == fakeCSInfo.locationInfo.longitude) - assertTrue(event.healthMeta.session.sessionId == fakeCSInfo.sessionInfo.sessionID) - assertTrue(event.numberOfBatches == 0L) - assertTrue(event.numberOfEvents == 1L) - assertTrue(event.traceDetails.errorDetails.reason == fakeCSHealthEventDTOs[index].error) - assertTrue(event.traceDetails.timeToConnection == fakeCSHealthEventDTOs[index].timeToConnection.toString()) - } - } - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/OkHttpWebSocketEventObserver.kt b/clickstream/src/test/kotlin/clickstream/internal/OkHttpWebSocketEventObserver.kt deleted file mode 100644 index 9f69a01a..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/OkHttpWebSocketEventObserver.kt +++ /dev/null @@ -1,36 +0,0 @@ -package clickstream.internal - -import com.tinder.scarlet.Message -import com.tinder.scarlet.ShutdownReason -import com.tinder.scarlet.WebSocket -import io.reactivex.Flowable -import io.reactivex.processors.PublishProcessor -import okhttp3.Response -import okhttp3.WebSocketListener -import okio.ByteString - -internal class OkHttpWebSocketEventObserver : WebSocketListener() { - private val processor = PublishProcessor.create().toSerialized() - - fun observe(): Flowable = processor.onBackpressureBuffer() - - fun terminate() = processor.onComplete() - - override fun onOpen(webSocket: okhttp3.WebSocket, response: Response) = - processor.onNext(WebSocket.Event.OnConnectionOpened(webSocket)) - - override fun onMessage(webSocket: okhttp3.WebSocket, bytes: ByteString) = - processor.onNext(WebSocket.Event.OnMessageReceived(Message.Bytes(bytes.toByteArray()))) - - override fun onMessage(webSocket: okhttp3.WebSocket, text: String) = - processor.onNext(WebSocket.Event.OnMessageReceived(Message.Text(text))) - - override fun onClosing(webSocket: okhttp3.WebSocket, code: Int, reason: String) = - processor.onNext(WebSocket.Event.OnConnectionClosing(ShutdownReason(code, reason))) - - override fun onClosed(webSocket: okhttp3.WebSocket, code: Int, reason: String) = - processor.onNext(WebSocket.Event.OnConnectionClosed(ShutdownReason(code, reason))) - - override fun onFailure(webSocket: okhttp3.WebSocket, t: Throwable, response: Response?) = - processor.onNext(WebSocket.Event.OnConnectionFailed(t)) -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt b/clickstream/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt deleted file mode 100644 index 260d84bb..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/analytics/CSHealthEventProcessorTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package clickstream.internal.analytics - -import clickstream.api.CSInfo -import clickstream.fake.FakeCSAppVersionSharedPref -import clickstream.fake.fakeAppInfo -import clickstream.fake.fakeCSHealthEventConfig -import clickstream.fake.fakeCSInfo -import clickstream.fake.fakeUserInfo -import clickstream.health.constant.CSTrackedVia -import clickstream.health.intermediate.CSHealthEventFactory -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.internal.CSHealthEventEntity -import clickstream.health.internal.CSHealthEventEntity.Companion.mapToDtos -import clickstream.health.internal.DefaultCSHealthEventProcessor -import clickstream.internal.analytics.impl.NoOpCSHealthEventLogger -import clickstream.lifecycle.CSAppLifeCycle -import clickstream.logger.CSLogLevel.OFF -import clickstream.logger.CSLogger -import clickstream.utils.CoroutineTestRule -import com.nhaarman.mockitokotlin2.any -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -public class CSHealthEventProcessorTest { - - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val healthEventRepository = mock(CSHealthEventRepository::class.java) - private val csHealthEventFactory = mock(CSHealthEventFactory::class.java) - private val csAppVersionSharedPref = FakeCSAppVersionSharedPref(false) - private val appLifeCycle = mock() - - private lateinit var sut: DefaultCSHealthEventProcessor - - @Test - public fun `Given CSCustomerInfo and CSMerchantInfo When sendEvents Then verify events are successfully compute`() { - runBlocking { - sut = getEventProcessor( - fakeCSInfo().copy( - appInfo = fakeAppInfo.copy(appVersion = "4.38.0") - ) - ) - - val events = fakeCSHealthEvent().mapToDtos() - whenever(healthEventRepository.getInstantEvents()).thenReturn(events) - whenever(healthEventRepository.getAggregateEvents()).thenReturn(events) - - sut.onStop() - - verify(healthEventRepository).getInstantEvents() - verify(healthEventRepository, times(2)).getAggregateEvents() - } - } - - @Test - public fun `Given null CSCustomerInfo When sendEvents Then verify events are successfully compute`() { - runBlocking { - sut = getEventProcessor( - fakeCSInfo().copy( - appInfo = fakeAppInfo.copy(appVersion = "4.38.0"), - userInfo = fakeUserInfo() - ) - ) - - val events = fakeCSHealthEvent().mapToDtos() - whenever(healthEventRepository.getInstantEvents()).thenReturn(events) - whenever(healthEventRepository.getAggregateEvents()).thenReturn(events) - - sut.onStop() - - verify(healthEventRepository).getInstantEvents() - verify(healthEventRepository, times(2)).getAggregateEvents() - } - } - - @Test - public fun `verify aggregate events`() { - runBlocking { - sut = getEventProcessor( - fakeCSInfo().copy( - appInfo = fakeAppInfo.copy(appVersion = "4.38.0"), - userInfo = fakeUserInfo() - ) - ) - - whenever(healthEventRepository.getAggregateEvents()).thenReturn(fakeCSHealthEvent().mapToDtos()) - - assertTrue(sut.getAggregateEvents().isNotEmpty()) - } - } - - private fun getEventProcessor(csInfo: CSInfo) = DefaultCSHealthEventProcessor( - appLifeCycleObserver = appLifeCycle, - healthEventRepository = healthEventRepository, - dispatcher = coroutineRule.testDispatcher, - healthEventConfig = fakeCSHealthEventConfig.copy(trackedVia = CSTrackedVia.Both), - info = csInfo, - logger = CSLogger(OFF), - healthEventLoggerListener = NoOpCSHealthEventLogger(), - healthEventFactory = csHealthEventFactory, - appVersion = "4.37.0", - appVersionPreference = csAppVersionSharedPref - ) - - private fun fakeCSHealthEvent(): List { - return listOf( - CSHealthEventEntity( - healthEventID = 1, - eventName = "broken-1", - eventType = "instant", - timestamp = System.currentTimeMillis().toString(), - eventId = "1234", - eventBatchId = "456", - error = "", - sessionId = "13455", - count = 0, - networkType = "LTE", - startTime = System.currentTimeMillis(), - stopTime = System.currentTimeMillis() + 1_000, - bucketType = "", - batchSize = 1, - appVersion = "4.37.0" - ) - ) - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/CSEventProcessorTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/CSEventProcessorTest.kt new file mode 100644 index 00000000..13e865e2 --- /dev/null +++ b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/CSEventProcessorTest.kt @@ -0,0 +1,61 @@ +package clickstream.internal.eventprocessor + +import clickstream.CSEvent +import clickstream.config.CSEventProcessorConfig +import clickstream.fake.defaultEventWrapperData +import clickstream.internal.eventscheduler.CSEventScheduler +import clickstream.logger.CSLogger +import clickstream.toInternal +import com.gojek.clickstream.products.shuffle.ShuffleCard +import com.google.protobuf.Timestamp +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +public class CSEventProcessorTest { + + private val processorConfig = CSEventProcessorConfig( + realtimeEvents = listOf("ShuffleCard"), + instantEvent = listOf("AdCardEvent") + ) + private val dispatcher = TestCoroutineDispatcher() + private val eventScheduler = mock() + private val logger = mock() + + private lateinit var processor: CSEventProcessor + + @Before + public fun setup() { + processor = CSEventProcessor( + config = processorConfig, + eventScheduler = eventScheduler, + dispatcher = dispatcher, + logger = logger, + ) + } + + @Test + public fun `it should forward instant events to schedular`(): Unit = runBlockingTest { + val csEventInternal = defaultEventWrapperData("event-uuid").toInternal() + + processor.trackEvent(csEventInternal) + + verify(eventScheduler).sendInstantEvent(csEventInternal) + } + + @Test + public fun `it should forward non instant events to scheduler`(): Unit = runBlockingTest { + val shuffleCard = ShuffleCard.getDefaultInstance() + val csEventInternal = + CSEvent("event-uuid", Timestamp.getDefaultInstance(), shuffleCard).toInternal() + + processor.trackEvent(csEventInternal) + + verify(eventScheduler).scheduleEvent(csEventInternal) + } +} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSHealthEventFactoryTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSHealthEventFactoryTest.kt index a4ec596a..7dad6721 100644 --- a/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSHealthEventFactoryTest.kt +++ b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSHealthEventFactoryTest.kt @@ -1,18 +1,18 @@ package clickstream.internal.eventprocessor.impl -import clickstream.api.CSMetaProvider -import clickstream.extension.protoName -import clickstream.health.time.CSTimeStampGenerator -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.internal.DefaultCSHealthEventFactory +import clickstream.api.CSAppInfo +import clickstream.api.CSDeviceInfo +import clickstream.api.CSInfo +import clickstream.api.CSLocationInfo +import clickstream.api.CSSessionInfo +import clickstream.api.CSUserInfo +import clickstream.health.internal.CSGuIdGenerator +import clickstream.health.internal.factory.DefaultCSHealthEventFactory +import clickstream.health.time.CSHealthTimeStampGenerator +import clickstream.protoName import com.gojek.clickstream.internal.Health import com.gojek.clickstream.internal.HealthDetails import com.gojek.clickstream.internal.HealthMeta -import com.gojek.clickstream.internal.HealthMeta.App -import com.gojek.clickstream.internal.HealthMeta.Customer -import com.gojek.clickstream.internal.HealthMeta.Device -import com.gojek.clickstream.internal.HealthMeta.Location -import com.gojek.clickstream.internal.HealthMeta.Session import kotlinx.coroutines.runBlocking import org.junit.Assert.assertTrue import org.junit.Before @@ -26,13 +26,17 @@ import org.mockito.kotlin.whenever public class DefaultCSHealthEventFactoryTest { private val csGuIdGenerator = mock(CSGuIdGenerator::class.java) - private val csTimeStampGenerator = mock(CSTimeStampGenerator::class.java) - private val csMetaProvider = mock(CSMetaProvider::class.java) + private val csTimeStampGenerator = mock(CSHealthTimeStampGenerator::class.java) + private val csMetaProvider = mock(CSInfo::class.java) private lateinit var sut: DefaultCSHealthEventFactory @Before public fun setup() { - sut = DefaultCSHealthEventFactory(csGuIdGenerator, csTimeStampGenerator, csMetaProvider) + sut = DefaultCSHealthEventFactory( + csGuIdGenerator, + csTimeStampGenerator, + csInfo = csMetaProvider + ) } @Test @@ -44,11 +48,11 @@ public class DefaultCSHealthEventFactoryTest { val session = getHealthMetaSession() val device = getHealthMetaDevice() - whenever(csMetaProvider.app).thenReturn(app) - whenever(csMetaProvider.location()).thenReturn(location) - whenever(csMetaProvider.customer).thenReturn(customer) - whenever(csMetaProvider.session).thenReturn(session) - whenever(csMetaProvider.device).thenReturn(device) + whenever(csMetaProvider.appInfo).thenReturn(app) + whenever(csMetaProvider.locationInfo).thenReturn(location) + whenever(csMetaProvider.userInfo).thenReturn(customer) + whenever(csMetaProvider.sessionInfo).thenReturn(session) + whenever(csMetaProvider.deviceInfo).thenReturn(device) // When val health = getHealth().build() @@ -67,7 +71,6 @@ public class DefaultCSHealthEventFactoryTest { assertTrue(event.healthMeta.device.deviceMake == "Samsung") assertTrue(event.healthMeta.device.deviceModel == "SM-900") assertTrue(event.healthMeta.device.operatingSystem == "Android") - assertTrue(event.healthMeta.device.operatingSystemVersion == "10") assertTrue(event.healthMeta.eventGuid == "123456") assertTrue(event.healthMeta.location.latitude == -6.1753924) assertTrue(event.healthMeta.location.longitude == 106.8249641) @@ -87,30 +90,34 @@ public class DefaultCSHealthEventFactoryTest { .build() ) - private fun getHealthMetaDevice() = Device.newBuilder() - .setDeviceMake("Samsung") - .setDeviceModel("SM-900") - .setOperatingSystem("Android") - .setOperatingSystemVersion("10") - .build() - - private fun getHealthMetaSession() = Session.newBuilder() - .setSessionId("12345678910") - .build() - - private fun getHealthMetaCustomer() = Customer.newBuilder() - .setCurrentCountry("ID") - .setEmail("test@gmail.com") - .setIdentity(12) - .setSignedUpCountry("ID") - .build() - - private fun getHealthMetaLocation() = Location.newBuilder() - .setLatitude(-6.1753924) - .setLongitude(106.8249641) - .build() - - private fun getHealthMetaApp() = App.newBuilder() - .setVersion("4.37.0") - .build() + private fun getHealthMetaDevice() = object : CSDeviceInfo { + override fun getDeviceManufacturer() = "Samsung" + + override fun getDeviceModel() = "SM-900" + + override fun getSDKVersion() = "30" + + override fun getOperatingSystem() = "Android" + + override fun getDeviceHeight() = "300" + + override fun getDeviceWidth() = "400" + + } + + private fun getHealthMetaSession() = CSSessionInfo("12345678910") + + private fun getHealthMetaCustomer() = + CSUserInfo( + currentCountry = "ID", + email = "test@gmail.com", + identity = 12, + signedUpCountry = "ID" + ) + + private fun getHealthMetaLocation() = + CSLocationInfo(latitude = -6.1753924, longitude = 106.8249641, mapOf()) + + private fun getHealthMetaApp() = CSAppInfo(appVersion = "4.37.0") + } diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProviderTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProviderTest.kt index 082dfaa8..5f128387 100644 --- a/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProviderTest.kt +++ b/clickstream/src/test/kotlin/clickstream/internal/eventprocessor/impl/DefaultCSMetaProviderTest.kt @@ -1,12 +1,12 @@ package clickstream.internal.eventprocessor.impl import clickstream.api.CSDeviceInfo -import clickstream.api.CSMetaProvider import clickstream.fake.fakeAppInfo import clickstream.fake.fakeCSInfo import clickstream.fake.fakeCSSessionInfo import clickstream.fake.fakeLocationInfo import clickstream.fake.fakeUserInfo +import clickstream.internal.eventprocessor.CSMetaProvider import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals @@ -31,7 +31,7 @@ public class DefaultCSMetaProviderTest { @Before public fun setup() { - metaProvider = DefaultCSMetaProvider(fakeCSInfo(deviceInfo = deviceInfo)) + metaProvider = DefaultCSMetaProvider(fakeCSInfo(deviceInfo)) whenever(deviceInfo.getDeviceModel()).thenReturn(testDeviceModel) whenever(deviceInfo.getDeviceManufacturer()).thenReturn(testDeviceMake) whenever(deviceInfo.getOperatingSystem()).thenReturn(testOS) diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventSchedulerTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventSchedulerTest.kt deleted file mode 100644 index dc8646a5..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundEventSchedulerTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.extension.messageName -import clickstream.fake.FakeCSAppLifeCycle -import clickstream.fake.FakeCSEventListener -import clickstream.fake.FakeCSHealthEventProcessor -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.products.events.AdCardEvent -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.times -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.mockito.kotlin.whenever - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(MockitoJUnitRunner::class) -public class CSBackgroundEventSchedulerTest { - - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val eventRepository = mock() - private val networkManager = mock() - private val batteryStatusObserver = mock() - private val networkStatusObserver = mock() - private val healthEventRepository = mock() - private val logger = CSLogger(CSLogLevel.OFF) - private val guIdGenerator = mock() - private val timeStampGenerator = mock() - private val eventListeners = listOf(FakeCSEventListener()) - private val appLifeCycle = FakeCSAppLifeCycle() - private val healthEventProcessor = FakeCSHealthEventProcessor(coroutineRule.testDispatcher) - - private lateinit var sut: CSBackgroundEventScheduler - - @Before - public fun setup() { - sut = CSBackgroundEventScheduler( - appLifeCycle = appLifeCycle, - guIdGenerator = guIdGenerator, - timeStampGenerator = timeStampGenerator, - batteryStatusObserver = batteryStatusObserver, - networkStatusObserver = networkStatusObserver, - eventListeners = eventListeners, - networkManager = networkManager, - healthEventProcessor = healthEventProcessor, - info = fakeCSInfo, - eventRepository = eventRepository, - healthEventRepository = healthEventRepository, - logger = logger, - dispatcher = coroutineRule.testDispatcher - ) - } - - @Test - public fun `verify flushEvents`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() - ) - - val events = listOf(event) - whenever(eventRepository.getAllEvents()) - .thenReturn(events) - whenever(guIdGenerator.getId()) - .thenReturn("10") - whenever(timeStampGenerator.getTimeStamp()) - .thenReturn(1L) - - sut.onStop() - - verify(eventRepository).getAllEvents() - - val flushOnBackgroundDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamFlushOnBackground.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = events.joinToString { event -> event.eventGuid }, - appVersion = fakeCSInfo.appInfo.appVersion, - timeToConnection = networkManager.endConnectedTime - networkManager.startConnectingTime - ) - val batchCreatedDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamEventBatchCreated.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventBatchGuid = "10", - eventGuid = event.eventGuid, - appVersion = fakeCSInfo.appInfo.appVersion, - timeToConnection = networkManager.endConnectedTime - networkManager.startConnectingTime - ) - - verify(eventRepository, times(2)).insertEventDataList(any()) - - verify(healthEventRepository).insertHealthEvent(flushOnBackgroundDto) - verify(healthEventRepository).insertHealthEvent(batchCreatedDto) - - verifyNoMoreInteractions(healthEventRepository, eventRepository) - } - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundSchedulerTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundSchedulerTest.kt index 2cd651e6..321c2286 100644 --- a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundSchedulerTest.kt +++ b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSBackgroundSchedulerTest.kt @@ -1,268 +1,402 @@ package clickstream.internal.eventscheduler -import android.app.Application +import android.os.Build +import clickstream.config.CSEventSchedulerConfig import clickstream.config.CSRemoteConfig -import clickstream.connection.CSConnectionEvent -import clickstream.connection.CSSocketConnectionListener -import clickstream.extension.messageName -import clickstream.fake.FakeCSHealthEventProcessor -import clickstream.fake.FakeCSHealthEventRepository -import clickstream.fake.FakeCSMetaProvider -import clickstream.fake.createCSConfig -import clickstream.fake.fakeCSAppLifeCycle -import clickstream.fake.fakeCSHealthEventDTOs -import clickstream.fake.fakeCSHealthEventFactory -import clickstream.fake.fakeCSInfo -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSEventHealthListener +import clickstream.fake.FakeEventBatchDao +import clickstream.fake.defaultEventWrapperData +import clickstream.fake.fakeInfo import clickstream.health.intermediate.CSHealthEventProcessor -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSEventHealth -import clickstream.health.time.CSEventGeneratedTimestampListener -import clickstream.health.time.CSTimeStampGenerator +import clickstream.health.model.CSEventForHealth +import clickstream.health.model.CSHealthEventConfig +import clickstream.internal.db.CSBatchSizeSharedPref import clickstream.internal.di.CSServiceLocator -import clickstream.internal.di.impl.DefaultCServiceLocator +import clickstream.internal.eventscheduler.impl.DefaultCSEventRepository +import clickstream.internal.eventscheduler.impl.NoOpEventSchedulerErrorListener import clickstream.internal.networklayer.CSNetworkManager +import clickstream.internal.networklayer.socket.CSSocketConnectionManager +import clickstream.internal.utils.CSBatteryLevel import clickstream.internal.utils.CSBatteryStatusObserver +import clickstream.internal.utils.CSGuIdGenerator import clickstream.internal.utils.CSNetworkStatusObserver +import clickstream.internal.utils.CSTimeStampGenerator +import clickstream.lifecycle.CSAppLifeCycle import clickstream.lifecycle.CSBackgroundLifecycleManager import clickstream.logger.CSLogLevel import clickstream.logger.CSLogger -import clickstream.utils.CoroutineTestRule import com.gojek.clickstream.internal.Health -import com.gojek.clickstream.products.events.AdCardEvent -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify +import java.util.UUID import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest +import org.junit.After import org.junit.Before -import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.math.log -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(MockitoJUnitRunner::class) -@Ignore +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) public class CSBackgroundSchedulerTest { - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val context = mock() - private val eventRepository = mock() - - private val fakeCSMetaProvider = FakeCSMetaProvider() - private val fakeCSHealthEventDTOs = fakeCSHealthEventDTOs(fakeCSMetaProvider) - private val fakeCSHealthEventRepository = FakeCSHealthEventRepository(fakeCSHealthEventDTOs) - private val fakeCSHealthEventProcessor = FakeCSHealthEventProcessor(coroutineRule.testDispatcher) - private val logger = CSLogger(CSLogLevel.OFF) - - private val fakeServiceLocator = DefaultCServiceLocator( - context = context, - info = fakeCSInfo, - config = createCSConfig(), - eventGeneratedTimestampListener = object : CSEventGeneratedTimestampListener { - override fun now(): Long { - return 0L - } - }, - socketConnectionListener = object : CSSocketConnectionListener { - override fun onEventChanged(event: CSConnectionEvent) { - /*No Op*/ - } - }, - remoteConfig = object : CSRemoteConfig { - override val isForegroundEventFlushEnabled: Boolean - get() = false - }, - logLevel = CSLogLevel.OFF, - dispatcher = coroutineRule.testDispatcher, - eventHealthListener = object : CSEventHealthListener { - override fun onEventCreated(healthEvent: CSEventHealth) { - /*No Op*/ - } - }, - healthEventRepository = fakeCSHealthEventRepository, - healthEventProcessor = fakeCSHealthEventProcessor, - healthEventFactory = fakeCSHealthEventFactory, - appLifeCycle = fakeCSAppLifeCycle, - eventListener = emptyList() - ) + private val dispatcher = TestCoroutineDispatcher() + private val eventRepository = DefaultCSEventRepository(FakeEventBatchDao(dispatcher)) private val networkManager = mock() private val batteryStatusObserver = mock() + private val networkStatusObserver = mock() + private val healthEventProcessor = mock() private val backgroundLifecycleManager = mock() + private val logger = mock() private val guIdGenerator = mock() private val timeStampGenerator = mock() - private val networkStatusObserver = mock() - private val mockedCSHealthEventRepository = mock() - private val mockedCSHealthEventProcessor = mock() - - private lateinit var scheduler: CSWorkManagerEventScheduler + private val appLifeCycle = mock() + private val batchSizeRegulator = mock() + private val socketConnectionManager = mock() + private val remoteConfigMock = mock() + private val batchSizeSharedPrefMock = mock() + private lateinit var scheduler: CSBackgroundScheduler @Before public fun setup(): Unit = runBlockingTest { - CSServiceLocator.setServiceLocator(fakeServiceLocator) - - scheduler = CSWorkManagerEventScheduler( - appLifeCycle = fakeCSAppLifeCycle, + whenever( + healthEventProcessor.insertBatchEvent(any(), any>()) + ).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever( + healthEventProcessor.getHealthEventFlow( + any(), + any() + ) + ).thenReturn(flowOf(emptyList())) + scheduler = CSBackgroundScheduler( + appLifeCycleObserver = appLifeCycle, + networkManager = networkManager, + config = CSEventSchedulerConfig.default(), + batteryStatusObserver = batteryStatusObserver, + dispatcher = dispatcher, + healthProcessor = healthEventProcessor, + logger = logger, guIdGenerator = guIdGenerator, timeStampGenerator = timeStampGenerator, - batteryStatusObserver = batteryStatusObserver, + eventRepository = eventRepository, networkStatusObserver = networkStatusObserver, + info = fakeInfo(), eventListeners = emptyList(), - dispatcher = coroutineRule.testDispatcher, - healthEventProcessor = mockedCSHealthEventProcessor, - backgroundLifecycleManager = backgroundLifecycleManager, - info = fakeCSInfo, - eventRepository = eventRepository, - healthEventRepository = mockedCSHealthEventRepository, - logger = logger, - networkManager = networkManager + errorListener = NoOpEventSchedulerErrorListener(), + csReportDataTracker = null, + batchSizeRegulator = batchSizeRegulator, + csSocketConnectionManager = socketConnectionManager, + remoteConfig = remoteConfigMock, + batchSizeSharedPref = batchSizeSharedPrefMock, + csHealthGateway = mock() + ) + } + + @After + public fun tearDown() { + dispatcher.cancel() + verifyNoMoreInteractions( + networkManager, + batteryStatusObserver, + backgroundLifecycleManager, + logger, + guIdGenerator, + timeStampGenerator ) } @Test - public fun `Given no event exists When scheduler is called Then No event will be forwarded to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(emptyList()) - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(emptyList()) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(emptyList()) + public fun `Given no event exists When scheduler is called Then No event will be forwarded to network layer`(): Unit = + runBlockingTest { + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) scheduler.sendEvents() - verify(networkManager, never()).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, never()).getBatteryStatus() + verify(guIdGenerator, never()).getId() + verify(timeStampGenerator, never()).getTimeStamp() + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, never()).processEvent(any()) + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(logger, atLeastOnce()).debug { "" } + verify(healthEventProcessor, never()).insertBatchEvent( + any(), + any>() + ) } - } @Test - public fun `Given one event exists When scheduler is called Then One event will be forwarded to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() - ) - + public fun `Given one events exists When scheduler is called Then One event will be forwarded to network layer`(): Unit = + runBlockingTest { + val eventData = CSEventData.create(defaultEventWrapperData()) + + eventRepository.insertEventData(eventData) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(listOf(event)) - whenever(guIdGenerator.getId()).thenReturn("11") - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(emptyList()) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(emptyList()) scheduler.sendEvents() - verify(networkManager).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(1)).getBatteryStatus() + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(networkManager, times(1)).processEvent(any()) + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, atLeastOnce()).insertBatchEvent( + any(), + any>() + ) } - } @Test public fun `Given multiple events exists When scheduler is called Then Multiple event will be sent to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() - ) + runBlockingTest { + val eventData = CSEventData.create(defaultEventWrapperData()) + + eventRepository.insertEventData(eventData) + eventRepository.insertEventData(eventData) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(listOf(event, event)) - whenever(guIdGenerator.getId()).thenReturn("11") - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(emptyList()) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(emptyList()) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) scheduler.sendEvents() - verify(networkManager).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(1)).getBatteryStatus() + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager).processEvent(any()) + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(guIdGenerator).getId() + verify(timeStampGenerator).getTimeStamp() + verify(logger, atLeastOnce()).debug { "" } + verify(healthEventProcessor, atLeastOnce()).insertBatchEvent( + any(), + any>() + ) } } @Test - public fun `Given one health event exists When scheduler is called Then One event will be forwarded to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - val health = Health.newBuilder().build() + public fun `Given one health event exists When scheduler is called Then One event will be forwarded to network layer`(): Unit = + runBlockingTest { + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(healthEventProcessor.getHealthEventFlow(any(), eq(false))).thenReturn( + flowOf(listOf(Health.getDefaultInstance())) + ) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + + scheduler.sendEvents() + + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(1)).getBatteryStatus() + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(networkManager, times(1)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, never()).insertBatchEvent( + any(), + any>() + ) + } + @Test + public fun `Given one health event and one event exists When scheduler is called Then both will be forwarded to network layer`(): Unit = + runBlockingTest { + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(emptyList()) - whenever(guIdGenerator.getId()).thenReturn("11") - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(listOf(health)) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(listOf(health)) + + val eventData = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData) + whenever(healthEventProcessor.getHealthEventFlow(any(), eq(false))).thenReturn( + flowOf(listOf(Health.getDefaultInstance())) + ) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) scheduler.sendEvents() - verify(networkManager).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(2)).getBatteryStatus() + verify(guIdGenerator, times(2)).getId() + verify(timeStampGenerator, times(2)).getTimeStamp() + verify(networkManager, times(2)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, times(1)).insertBatchEvent( + any(), + any>() + ) } - } @Test - public fun `Given one health event and one event exists When scheduler is called Then both will be forwarded to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() + public fun `Given multiple health and app event exists When scheduler is called Then All will be forwarded to network layer`(): Unit = + runBlockingTest { + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + + val eventData = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData) + eventRepository.insertEventData(eventData) + whenever(healthEventProcessor.getHealthEventFlow(any(), eq(false))).thenReturn( + flowOf((0..3).map { Health.getDefaultInstance() }) ) - val health = Health.newBuilder().build() + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + scheduler.sendEvents() + + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(2)).getBatteryStatus() + verify(guIdGenerator, times(2)).getId() + verify(timeStampGenerator, times(2)).getTimeStamp() + verify(networkManager, times(2)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, times(1)).insertBatchEvent( + any(), + any>() + ) + } + + @Test + public fun `Given ignoreBattery flag is true and battery low When sendEvent then it should forward event`(): Unit = + runBlockingTest(dispatcher) { + whenever(remoteConfigMock.ignoreBatteryLvlOnFlush).thenReturn(true) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.LOW_BATTERY) whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(listOf(event)) - whenever(guIdGenerator.getId()).thenReturn("11") - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(listOf(health)) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(listOf(health)) + val eventData = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) scheduler.sendEvents() - verify(networkManager, times(2)).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, never()).getBatteryStatus() + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(networkManager, times(1)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, times(1)).insertBatchEvent( + any(), + any>() + ) } - } @Test - public fun `Given multiple health and app event exists When scheduler is called Then All will be forwarded to network layer`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() + public fun `Given ignoreBatteryLvlOnFlush is false and battery adequate When sendEvent then it should forward event`(): Unit = + runBlockingTest { + whenever(remoteConfigMock.ignoreBatteryLvlOnFlush).thenReturn(false) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + + val eventData = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + + scheduler.sendEvents() + + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, atLeastOnce()).getBatteryStatus() + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(networkManager, times(1)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, times(1)).insertBatchEvent( + any(), + any>() ) - val health = Health.newBuilder().build() + } + + @Test + public fun `Given multiple events & batching for flush enabled When scheduler is called Then Multiple event will be sent to network layer`() { + runBlockingTest { + val eventData1 = CSEventData.create(defaultEventWrapperData()) + val eventData2 = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData1) + eventRepository.insertEventData(eventData2) + + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) - whenever(networkManager.isSocketConnected()).thenReturn(true) - whenever(eventRepository.getAllEvents()).thenReturn(listOf(event, event)) - whenever(guIdGenerator.getId()).thenReturn("11") - whenever(mockedCSHealthEventProcessor.getInstantEvents()).thenReturn(listOf(health, health)) - whenever(mockedCSHealthEventProcessor.getAggregateEvents()).thenReturn(listOf(health, health)) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(remoteConfigMock.batchFlushedEvents).thenReturn(true) + whenever(batchSizeSharedPrefMock.getSavedBatchSize()).thenReturn(1) scheduler.sendEvents() - verify(networkManager, times(2)).processEvent(any(), any()) + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(2)).getBatteryStatus() + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, times(2)).processEvent(any()) + verify(guIdGenerator, times(2)).getId() + verify(timeStampGenerator, times(2)).getTimeStamp() + verify(logger, atLeastOnce()).debug { "" } + verify(healthEventProcessor, atLeastOnce()).insertBatchEvent( + any(), + any>() + ) } } -} + + @Test + public fun `Given health & event & health disabled When scheduler is called Then only event will be forwarded to network layer`(): Unit = + runBlockingTest { + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + + val eventData = CSEventData.create(defaultEventWrapperData()) + eventRepository.insertEventData(eventData) + whenever(healthEventProcessor.getHealthEventFlow(any(), eq(false))).thenReturn( + flowOf(emptyList()) + ) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + + scheduler.sendEvents() + + verify(backgroundLifecycleManager, never()).onStart() + verify(batteryStatusObserver, times(1)).getBatteryStatus() + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(networkManager, times(1)).processEvent(any()) + verify(logger, atLeastOnce()).debug { "" } + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(healthEventProcessor, atLeastOnce()).insertBatchEvent( + any(), + any>() + ) + } +} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerTest.kt new file mode 100644 index 00000000..cf6365fc --- /dev/null +++ b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSEventSchedulerTest.kt @@ -0,0 +1,293 @@ +package clickstream.internal.eventscheduler + +import android.os.Build +import clickstream.config.CSEventSchedulerConfig +import clickstream.config.CSRemoteConfig +import clickstream.fake.FakeEventBatchDao +import clickstream.fake.defaultBytesEventWrapperData +import clickstream.fake.defaultEventWrapperData +import clickstream.fake.fakeInfo +import clickstream.health.intermediate.CSHealthEventProcessor +import clickstream.internal.eventscheduler.impl.DefaultCSEventRepository +import clickstream.internal.eventscheduler.impl.NoOpEventSchedulerErrorListener +import clickstream.internal.networklayer.CSNetworkManager +import clickstream.internal.utils.CSBatteryLevel +import clickstream.internal.utils.CSBatteryStatusObserver +import clickstream.internal.utils.CSGuIdGenerator +import clickstream.internal.utils.CSNetworkStatusObserver +import clickstream.internal.utils.CSTimeStampGenerator +import clickstream.logger.CSLogger +import clickstream.toInternal +import java.util.UUID +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import clickstream.internal.networklayer.socket.CSSocketConnectionManager +import clickstream.lifecycle.CSAppLifeCycle + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [Build.VERSION_CODES.P]) +public class CSEventSchedulerTest { + + private val dispatcher = TestCoroutineDispatcher() + private val eventRepository = DefaultCSEventRepository(FakeEventBatchDao(dispatcher)) + private val config = CSEventSchedulerConfig( + eventsPerBatch = 2, + batchPeriod = 2000, + flushOnBackground = false, + connectionTerminationTimerWaitTimeInMillis = 5, + backgroundTaskEnabled = false, + workRequestDelayInHr = 1, + utf8ValidatorEnabled = true, + enableForegroundFlushing = false + ) + + private val networkManager = mock() + private val batteryStatusObserver = mock() + private val networkStatusObserver = mock() + private val healthEventRepository = mock() + private val logger = mock() + private val guIdGenerator = mock() + private val timeStampGenerator = mock() + private val appLifeCycle = mock() + private val batchSizeRegulator = mock() + private val connectionManger = mock() + private val remoteConfig = mock() + + private lateinit var scheduler: CSEventScheduler + + @Before + public fun setup() { + scheduler = CSEventScheduler( + appLifeCycleObserver = appLifeCycle, + networkManager = networkManager, + eventRepository = eventRepository, + timeStampGenerator = timeStampGenerator, + guIdGenerator = guIdGenerator, + logger = logger, + healthEventProcessor = healthEventRepository, + dispatcher = dispatcher, + batteryStatusObserver = batteryStatusObserver, + networkStatusObserver = networkStatusObserver, + config = config, + info = fakeInfo(), + eventListeners = emptyList(), + errorListener = NoOpEventSchedulerErrorListener(), + csReportDataTracker = null, + batchSizeRegulator = batchSizeRegulator, + socketConnectionManager = connectionManger, + remoteConfig = remoteConfig, + csHealthGateway = mock() + ) + scheduler.onStart() + } + + @After + public fun tearDown() { + scheduler.onStop() + dispatcher.cancel() + + verifyNoMoreInteractions( + networkManager, + guIdGenerator, + timeStampGenerator + ) + + } + + @Test + public fun `Given the scheduler & event should dispatch event at given interval & verify the results`(): Unit = + runBlockingTest { + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + + scheduler.scheduleEvent(defaultEventWrapperData().toInternal()) + dispatcher.advanceTimeBy(2000) + + verify(batchSizeRegulator, times(1)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, times(1)).processEvent(any()) + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + + @Test + public fun `Given the scheduler & bytes event should dispatch event at given interval & verify the results`(): Unit = + runBlockingTest { + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + + scheduler.scheduleEvent(defaultBytesEventWrapperData().toInternal()) + dispatcher.advanceTimeBy(2000) + + verify(batchSizeRegulator, times(1)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, times(1)).processEvent(any()) + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + + @Test + public fun `Given the scheduler & event list should dispatch event at given interval & verify the results`() { + runBlockingTest { + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + repeat(6) { + scheduler.scheduleEvent(defaultEventWrapperData().toInternal()) + } + + dispatcher.advanceTimeBy(6000) + + verify(batchSizeRegulator, times(6)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, times(3)).processEvent(any()) + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(guIdGenerator, times(3)).getId() + verify(timeStampGenerator, times(3)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + } + + @Test + public fun `Given the scheduler & bytes event list should dispatch event at given interval & verify the results`() { + runBlockingTest { + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(2) + repeat(6) { + scheduler.scheduleEvent(defaultBytesEventWrapperData().toInternal()) + } + + dispatcher.advanceTimeBy(6000) + + verify(batchSizeRegulator, times(6)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, times(3)).processEvent(any()) + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(guIdGenerator, times(3)).getId() + verify(timeStampGenerator, times(3)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + } + + @Test + public fun `Given the scheduler should dispatch event when list is empty & shouldn't invoke network manager`() { + runBlockingTest { + dispatcher.advanceTimeBy(2000) + + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, never()).processEvent(any()) + verifyZeroInteractions(networkManager) + verifyZeroInteractions(guIdGenerator) + verifyZeroInteractions(timeStampGenerator) + verify(batchSizeRegulator, times(1)).regulatedCountOfEventsPerBatch(any()) + } + } + + @Test + public fun `Given ignoreBatteryLvlCheck is true and not Flushing then it should dispatch event at given interval & verify the results`(): Unit = + runBlockingTest { + whenever(remoteConfig.ignoreBatteryLvlOnFlush).thenReturn(true) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + + scheduler.scheduleEvent(defaultEventWrapperData().toInternal()) + dispatcher.advanceTimeBy(2000) + + verify(batteryStatusObserver, atLeastOnce()).getBatteryStatus() + verify(batchSizeRegulator, times(1)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, times(1)).processEvent(any()) + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + + @Test + public fun `Given ignoreBatteryLvlCheck is false and not Flushing then it should dispatch event at given interval & verify the results`(): Unit = + runBlockingTest { + whenever(remoteConfig.ignoreBatteryLvlOnFlush).thenReturn(false) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.ADEQUATE_POWER) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + + scheduler.scheduleEvent(defaultEventWrapperData().toInternal()) + dispatcher.advanceTimeBy(2000) + + verify(batteryStatusObserver, atLeastOnce()).getBatteryStatus() + verify(batchSizeRegulator, times(1)).logEvent(any()) + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, atLeastOnce()).isSocketAvailable() + verify(networkManager, times(1)).processEvent(any()) + verify(guIdGenerator, times(1)).getId() + verify(timeStampGenerator, times(1)).getTimeStamp() + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } + + @Test + public fun `Given ignoreBatteryLvlCheck is false and not Flushing and low battery then it shouldn't invoke network manager`(): Unit = + runBlockingTest { + whenever(remoteConfig.ignoreBatteryLvlOnFlush).thenReturn(false) + whenever(guIdGenerator.getId()).thenReturn(UUID.randomUUID().toString()) + whenever(timeStampGenerator.getTimeStamp()).thenReturn(System.currentTimeMillis()) + whenever(batteryStatusObserver.getBatteryStatus()).thenReturn(CSBatteryLevel.LOW_BATTERY) + whenever(networkStatusObserver.isNetworkAvailable()).thenReturn(true) + whenever(networkManager.isSocketAvailable()).thenReturn(true) + whenever(batchSizeRegulator.regulatedCountOfEventsPerBatch(any())).thenReturn(1) + + scheduler.scheduleEvent(defaultEventWrapperData().toInternal()) + dispatcher.advanceTimeBy(2000) + dispatcher.advanceTimeBy(2000) + + verify(networkManager, times(1)).eventGuidFlow + verify(networkManager, never()).processEvent(any()) + verifyZeroInteractions(networkManager) + verifyZeroInteractions(guIdGenerator) + verifyZeroInteractions(timeStampGenerator) + verify(batchSizeRegulator, atLeastOnce()).regulatedCountOfEventsPerBatch(any()) + } +} diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSForegroundEventSchedulerTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSForegroundEventSchedulerTest.kt deleted file mode 100644 index 690443b2..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/CSForegroundEventSchedulerTest.kt +++ /dev/null @@ -1,228 +0,0 @@ -package clickstream.internal.eventscheduler - -import clickstream.config.CSEventSchedulerConfig -import clickstream.extension.messageName -import clickstream.fake.FakeCSAppLifeCycle -import clickstream.fake.FakeCSEventListener -import clickstream.fake.fakeInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.identity.CSGuIdGenerator -import clickstream.health.intermediate.CSEventHealthListener -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.networklayer.CSNetworkManager -import clickstream.internal.utils.CSBatteryLevel -import clickstream.internal.utils.CSBatteryStatusObserver -import clickstream.internal.utils.CSNetworkStatusObserver -import clickstream.internal.utils.CSResult -import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import clickstream.model.CSEvent -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.products.events.AdCardEvent -import com.google.protobuf.Timestamp -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -public class CSForegroundEventSchedulerTest { - - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val config = CSEventSchedulerConfig( - eventsPerBatch = 2, - batchPeriod = 2000, - flushOnBackground = false, - connectionTerminationTimerWaitTimeInMillis = 5, - backgroundTaskEnabled = false, - workRequestDelayInHr = 1, - utf8ValidatorEnabled = true - ) - - private val eventRepository = mock() - private val networkManager = mock() - private val batteryStatusObserver = mock() - private val networkStatusObserver = mock() - private val healthEventRepository = mock() - private val logger = CSLogger(CSLogLevel.OFF) - private val guIdGenerator = mock() - private val timeStampGenerator = mock() - private val eventHealthListener = mock() - private val eventListeners = listOf(FakeCSEventListener()) - private val appLifeCycle = FakeCSAppLifeCycle() - - private lateinit var sut: CSForegroundEventScheduler - - @Before - public fun setup() { - sut = CSForegroundEventScheduler( - appLifeCycle = appLifeCycle, - networkManager = networkManager, - eventRepository = eventRepository, - timeStampGenerator = timeStampGenerator, - guIdGenerator = guIdGenerator, - logger = logger, - healthEventRepository = healthEventRepository, - dispatcher = coroutineRule.testDispatcher, - batteryStatusObserver = batteryStatusObserver, - networkStatusObserver = networkStatusObserver, - config = config, - info = fakeInfo(), - eventHealthListener = eventHealthListener, - eventListeners = eventListeners - ) - } - - @Test - public fun `Given an AdCard Event When app state is active Then verify event should went to network manager`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEventData( - eventGuid = "1", - eventRequestGuid = "2", - eventTimeStamp = 3L, - isOnGoing = true, - messageAsBytes = adCardEvent.toByteArray(), - messageName = adCardEvent.messageName() - ) - - whenever(eventRepository.getEventDataList()) - .thenReturn(flowOf(listOf(event))) - whenever(eventRepository.getEventsOnGuId(any())) - .thenReturn(listOf(event)) - whenever(eventRepository.getOnGoingEvents()) - .thenReturn(listOf(event)) - whenever(networkManager.eventGuidFlow) - .thenReturn(flowOf(CSResult.Success("2"))) - whenever(networkStatusObserver.isNetworkAvailable()) - .thenReturn(true) - whenever(batteryStatusObserver.getBatteryStatus()) - .thenReturn(CSBatteryLevel.ADEQUATE_POWER) - whenever(networkManager.isSocketConnected()) - .thenReturn(true) - whenever(guIdGenerator.getId()) - .thenReturn("92") - whenever(timeStampGenerator.getTimeStamp()) - .thenReturn(922) - - sut.onStart() - - // simulate initial delay - coroutineRule.testDispatcher.advanceTimeBy(10) - sut.cancelJob() - - verify(networkManager).processEvent(any(), any()) - verify(networkManager).eventGuidFlow - verify(networkManager, times(2)).isSocketConnected() - - verify(eventRepository, times(2)).insertEventDataList(any()) - verify(eventRepository).getEventDataList() - verify(eventRepository).getOnGoingEvents() - verify(eventRepository).deleteEventDataByGuId("2") - verify(eventRepository).getEventsOnGuId(any()) - - verifyNoMoreInteractions(eventRepository) - } - } - - @Test - public fun `Given No Event When app state is active Then verify no event get proceses to network manager`() { - coroutineRule.testDispatcher.runBlockingTest { - whenever(eventRepository.getEventDataList()) - .thenReturn(flowOf(emptyList())) - whenever(eventRepository.getOnGoingEvents()) - .thenReturn(emptyList()) - whenever(networkManager.eventGuidFlow) - .thenReturn(emptyFlow()) - - sut.onStart() - - // simulate initial delay - coroutineRule.testDispatcher.advanceTimeBy(10) - sut.cancelJob() - - verify(networkManager, never()).processEvent(any(), any()) - verify(networkManager).eventGuidFlow - - verify(eventRepository, never()).insertEventDataList(any()) - verify(eventRepository).getEventDataList() - verify(eventRepository).getOnGoingEvents() - verify(eventRepository, never()).deleteEventDataByGuId("2") - - verifyNoMoreInteractions(eventRepository) - } - } - - @Test - public fun `verify scheduleEvent`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEvent( - guid = "1", - timestamp = Timestamp.getDefaultInstance(), - message = adCardEvent - ) - - sut.scheduleEvent(event) - - val cacheDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamEventCached.value, - eventType = CSEventTypesConstant.AGGREGATE, - eventGuid = event.guid, - appVersion = fakeInfo().appInfo.appVersion - ) - verify(eventHealthListener).onEventCreated(any()) - verify(eventRepository).insertEventData(any()) - verify(healthEventRepository).insertHealthEvent(cacheDto) - - eventListeners.forEach { - assertTrue(it.isCalled) - } - } - } - - @Test - public fun `verify sendInstantEvent`() { - coroutineRule.testDispatcher.runBlockingTest { - val adCardEvent = AdCardEvent.newBuilder().build() - val event = CSEvent( - guid = "1", - timestamp = Timestamp.getDefaultInstance(), - message = adCardEvent - ) - - whenever(timeStampGenerator.getTimeStamp()) - .thenReturn(1L) - whenever(guIdGenerator.getId()) - .thenReturn("1") - - sut.sendInstantEvent(event) - - verify(eventHealthListener).onEventCreated(any()) - verify(networkManager).processInstantEvent(any()) - - eventListeners.forEach { - assertTrue(it.isCalled) - } - } - } -} diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepositoryTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepositoryTest.kt index d33c8deb..785c8810 100644 --- a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepositoryTest.kt +++ b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/DefaultCSEventRepositoryTest.kt @@ -39,7 +39,7 @@ public class DefaultCSEventRepositoryTest { @Test public fun `given a data should successfully insert into db`() { runBlocking { - val (eventData, eventHealthData) = CSEventData.create(defaultEventWrapperData()) + val eventData = CSEventData.create(defaultEventWrapperData()) whenever(dao.insert(eventData)).then { println("adding item") dbItems.add(it.getArgument(0)) @@ -57,7 +57,7 @@ public class DefaultCSEventRepositoryTest { @Test public fun `given a list of data should successfully insert all into db`() { runBlocking { - val (eventData, eventHealthData) = CSEventData.create(defaultEventWrapperData()) + val eventData = CSEventData.create(defaultEventWrapperData()) val eventDatas = listOf(eventData) whenever(dao.insertAll(eventDatas)).then { dbItems.addAll(it.getArgument(0)) @@ -75,7 +75,7 @@ public class DefaultCSEventRepositoryTest { @Test public fun `Given an event id should successfully delete from db`() { runBlocking { - val (eventData, eventHealthData) = CSEventData.create(defaultEventWrapperData()) + val eventData = CSEventData.create(defaultEventWrapperData()) val eventBatchID = UUID.randomUUID().toString() val eventBatch = eventData.copy(eventRequestGuid = eventBatchID) dbItems.add(eventBatch) diff --git a/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategyTest.kt b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategyTest.kt new file mode 100644 index 00000000..6d690416 --- /dev/null +++ b/clickstream/src/test/kotlin/clickstream/internal/eventscheduler/impl/EventByteSizeBasedBatchStrategyTest.kt @@ -0,0 +1,45 @@ +package clickstream.internal.eventscheduler.impl + +import clickstream.CSBytesEvent +import clickstream.internal.db.CSBatchSizeSharedPref +import clickstream.internal.eventscheduler.DEFAULT_MIN_EVENT_COUNT +import clickstream.logger.CSLogger +import clickstream.toInternal +import com.google.protobuf.Timestamp +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.mock + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +public class EventByteSizeBasedBatchStrategyTest { + private val logger = mock() + private val csBatchSizeSharedPref = mock() + private lateinit var sut: EventByteSizeBasedBatchStrategy + + @Before + public fun setup() { + sut = EventByteSizeBasedBatchStrategy(logger, csBatchSizeSharedPref) + } + + @Test + public fun `it should return default minimum if no events are yet observed`(): Unit = runBlockingTest { + val countOfEventsPerBatch = sut.regulatedCountOfEventsPerBatch(4) + assertEquals(DEFAULT_MIN_EVENT_COUNT, countOfEventsPerBatch) + } + + @Test + public fun `it should return default minimum if expectedBatchSize is bigger than average calculated size`(): Unit = runBlockingTest { + val csEvent = CSBytesEvent("guid", Timestamp.getDefaultInstance(), "name", ByteArray(25000)) + + sut.logEvent(csEvent.toInternal()) + + val countOfEventsPerBatch = sut.regulatedCountOfEventsPerBatch(20000) + assertEquals(DEFAULT_MIN_EVENT_COUNT, countOfEventsPerBatch) + } +} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSConnectionDroppedTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSConnectionDroppedTest.kt deleted file mode 100644 index 2342d9ea..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSConnectionDroppedTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package clickstream.internal.networklayer - -import clickstream.internal.eventscheduler.CSEventData -import clickstream.internal.utils.CSFlowStreamAdapterFactory -import clickstream.internal.utils.CSTimeStampMessageBuilder -import clickstream.model.CSEvent -import clickstream.utils.TestFlowObserver -import clickstream.utils.any -import clickstream.utils.containingBytes -import clickstream.utils.flowTest -import clickstream.utils.newWebSocketFactory -import com.gojek.clickstream.common.App -import com.gojek.clickstream.common.EventMeta -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.de.common.EventResponse -import com.gojek.clickstream.products.events.AdCardEvent -import com.google.protobuf.Timestamp -import com.tinder.scarlet.Lifecycle -import com.tinder.scarlet.Scarlet -import com.tinder.scarlet.ShutdownReason -import com.tinder.scarlet.WebSocket -import com.tinder.scarlet.lifecycle.LifecycleRegistry -import com.tinder.scarlet.messageadapter.protobuf.ProtobufMessageAdapter -import com.tinder.scarlet.websocket.okhttp.newWebSocketFactory -import java.util.concurrent.TimeUnit -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import okhttp3.OkHttpClient -import okhttp3.mockwebserver.MockWebServer -import org.assertj.core.api.Assertions -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -internal class CSConnectionDroppedTest { - @get:Rule - public val mockWebServer: MockWebServer = MockWebServer() - private val serverUrlString by lazy { mockWebServer.url("/").toString() } - - private val serverLifecycleRegistry = LifecycleRegistry() - private lateinit var server: CSEventService - private lateinit var serverEventObserver: TestFlowObserver - - private val clientLifecycleRegistry = LifecycleRegistry() - private lateinit var client: CSEventService - private lateinit var clientEventObserver: TestFlowObserver - - @Test - public fun send_givenConnectionIsEstablished_shouldBeReceivedByTheServer() { - // Given - givenConnectionIsEstablished() - val testResponse: TestFlowObserver = server.observeResponse().flowTest() - - val eventRequest1: EventRequest = generatedEvent("1") - val eventRequest2: EventRequest = generatedEvent("2") - - // When - val event1 = client.sendEvent(eventRequest1) - val event2 = client.sendEvent(eventRequest2) - val event3 = client.sendEvent(eventRequest1) - val event4 = client.sendEvent(eventRequest2) - - // Then - Assertions.assertThat(event1).isTrue - Assertions.assertThat(event2).isTrue - Assertions.assertThat(event3).isTrue - Assertions.assertThat(event4).isTrue - - Assertions.assertThat(testResponse.values).allSatisfy { e -> - e is EventResponse - } - - serverLifecycleRegistry.onNext(Lifecycle.State.Stopped.WithReason(ShutdownReason.GRACEFUL)) - serverEventObserver.awaitValues( - any>(), - any().containingBytes(eventRequest1.toByteArray()), - any().containingBytes(eventRequest2.toByteArray()), - any().containingBytes(eventRequest1.toByteArray()), - any().containingBytes(eventRequest2.toByteArray()), - any(), - any() - ) - } - - private fun generatedEvent(guid: String): EventRequest { - val event = CSEvent( - guid = guid, - timestamp = Timestamp.getDefaultInstance(), - message = AdCardEvent.newBuilder() - .setMeta( - EventMeta.newBuilder() - .setApp(App.newBuilder().setVersion("4.35.0")) - .build() - ) - .build() - ) - val (eventData, eventHealthData) = CSEventData.create(event) - return transformToEventRequest(eventData = listOf(eventData)) - } - - private fun givenConnectionIsEstablished() { - createClientAndServer() - serverLifecycleRegistry.onNext(Lifecycle.State.Started) - clientLifecycleRegistry.onNext(Lifecycle.State.Started) - blockUntilConnectionIsEstablish() - } - - private fun createClientAndServer() { - server = createServer() - serverEventObserver = server.observeSocketState().flowTest() - client = createClient() - clientEventObserver = client.observeSocketState().flowTest() - } - - private fun createServer(): CSEventService { - val webSocketFactory = mockWebServer.newWebSocketFactory() - val scarlet = Scarlet.Builder() - .webSocketFactory(webSocketFactory) - .lifecycle(serverLifecycleRegistry) - .addStreamAdapterFactory(CSFlowStreamAdapterFactory()) - .addMessageAdapterFactory(ProtobufMessageAdapter.Factory()) - .build() - return scarlet.create() - } - - private fun createClient(): CSEventService { - val okHttpClient = OkHttpClient.Builder() - .writeTimeout(500, TimeUnit.MILLISECONDS) - .readTimeout(500, TimeUnit.MILLISECONDS) - .build() - val webSocketFactory = okHttpClient.newWebSocketFactory(serverUrlString) - val scarlet = Scarlet.Builder() - .webSocketFactory(webSocketFactory) - .lifecycle(clientLifecycleRegistry) - .addStreamAdapterFactory(CSFlowStreamAdapterFactory()) - .addMessageAdapterFactory(ProtobufMessageAdapter.Factory()) - .build() - return scarlet.create() - } - - private fun blockUntilConnectionIsEstablish() { - clientEventObserver.awaitValues( - any>() - ) - serverEventObserver.awaitValues( - any>() - ) - } - - private fun transformToEventRequest(eventData: List): EventRequest { - return EventRequest.newBuilder().apply { - reqGuid = "1011" - sentTime = CSTimeStampMessageBuilder.build(System.currentTimeMillis()) - addAllEvents(eventData.map { it.event() }) - }.build() - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsBatchTimeoutTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsBatchTimeoutTest.kt deleted file mode 100644 index dc55dac4..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsBatchTimeoutTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -package clickstream.internal.networklayer - -import clickstream.config.CSNetworkConfig -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.health.time.CSTimeStampGenerator -import clickstream.internal.utils.CSFlowStreamAdapterFactory -import clickstream.logger.CSLogLevel -import clickstream.logger.CSLogger -import clickstream.utils.CoroutineTestRule -import clickstream.utils.TestFlowObserver -import clickstream.utils.flowTest -import clickstream.utils.newWebSocketFactory -import com.gojek.clickstream.de.Event -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.products.events.AdCardEvent -import com.google.protobuf.Timestamp -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.tinder.scarlet.Lifecycle -import com.tinder.scarlet.Scarlet -import com.tinder.scarlet.WebSocket -import com.tinder.scarlet.lifecycle.LifecycleRegistry -import com.tinder.scarlet.messageadapter.protobuf.ProtobufMessageAdapter -import com.tinder.scarlet.websocket.okhttp.newWebSocketFactory -import java.util.concurrent.TimeUnit -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.runBlocking -import okhttp3.OkHttpClient -import okhttp3.mockwebserver.MockWebServer -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.verify - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -internal class CSHealthMetricsBatchTimeoutTest { - - @get:Rule - public val mockWebServer: MockWebServer = MockWebServer() - @get:Rule - public val coroutineRule: CoroutineTestRule = CoroutineTestRule() - - private val info = fakeCSInfo() - - private val serverUrlString by lazy { mockWebServer.url("/").toString() } - private val timestamp = mock() - private val health = mock() - - private val serverLifecycleRegistry = LifecycleRegistry() - private lateinit var server: CSEventService - private lateinit var serverEventObserver: TestFlowObserver - - private val clientLifecycleRegistry = LifecycleRegistry() - private lateinit var client: CSEventService - private lateinit var clientEventObserver: TestFlowObserver - - @Test - public fun verifyNoEventsAreNotCrashing() = runBlocking { - // Given - givenConnectionIsEstablished() - - // When - val eventRequest = EventRequest.newBuilder() - .setReqGuid("1234") - .setSentTime(Timestamp.getDefaultInstance()) - .addEvents( - Event.newBuilder() - .setEventBytes(AdCardEvent.newBuilder().build().toByteString()) - .setType("AdcardEvent") - .build() - ) - .build() - - // Then - object : CSRetryableCallback( - networkConfig = CSNetworkConfig.default(OkHttpClient()), - eventService = client, - eventRequest = eventRequest, - dispatcher = coroutineRule.testDispatcher, - timeStampGenerator = timestamp, - logger = CSLogger(CSLogLevel.OFF), - healthEventRepository = health, - info = info, - coroutineScope = coroutineRule.scope, - eventGuids = "1234" - ) { - override fun onSuccess(guid: String) { /*No Op*/ } - - override fun onFailure(throwable: Throwable, guid: String) { /*No Op*/ } - } - - coroutineRule.scope.advanceTimeBy(10_000) - - val batchTimeout = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamEventBatchTimeout.value, - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion, - eventBatchGuid = "1234", - error = "SocketTimeout" - ) - - val batchSent = CSHealthEventDTO( - eventName = CSEventNamesConstant.AggregatedAndFlushed.ClickStreamBatchSent.value, - eventType = CSEventTypesConstant.AGGREGATE, - appVersion = info.appInfo.appVersion, - eventBatchGuid = "1234", - eventGuid = "1234" - ) - - verify(health).insertHealthEvent(batchTimeout) - verify(health, times(2)).insertHealthEvent(batchSent) - } - - private fun givenConnectionIsEstablished() { - createClientAndServer() - serverLifecycleRegistry.onNext(Lifecycle.State.Started) - clientLifecycleRegistry.onNext(Lifecycle.State.Started) - blockUntilConnectionIsEstablish() - } - - private fun createClientAndServer() { - server = createServer() - serverEventObserver = server.observeSocketState().flowTest() - client = createClient() - clientEventObserver = client.observeSocketState().flowTest() - } - - private fun blockUntilConnectionIsEstablish() { - clientEventObserver.awaitValues( - clickstream.utils.any>() - ) - serverEventObserver.awaitValues( - clickstream.utils.any>() - ) - } - - private fun createServer(): CSEventService { - val webSocketFactory = mockWebServer.newWebSocketFactory() - val scarlet = Scarlet.Builder() - .webSocketFactory(webSocketFactory) - .lifecycle(serverLifecycleRegistry) - .addStreamAdapterFactory(CSFlowStreamAdapterFactory()) - .addMessageAdapterFactory(ProtobufMessageAdapter.Factory()) - .build() - return scarlet.create() - } - - private fun createClient(): CSEventService { - val okHttpClient = OkHttpClient.Builder() - .writeTimeout(500, TimeUnit.MILLISECONDS) - .readTimeout(500, TimeUnit.MILLISECONDS) - .build() - val webSocketFactory = okHttpClient.newWebSocketFactory(serverUrlString) - val scarlet = Scarlet.Builder() - .webSocketFactory(webSocketFactory) - .lifecycle(clientLifecycleRegistry) - .addStreamAdapterFactory(CSFlowStreamAdapterFactory()) - .addMessageAdapterFactory(ProtobufMessageAdapter.Factory()) - .build() - return scarlet.create() - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionClosedTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionClosedTest.kt deleted file mode 100644 index 076a0abc..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionClosedTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package clickstream.internal.networklayer - -import clickstream.connection.CSSocketConnectionListener -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.de.common.EventResponse -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.tinder.scarlet.ShutdownReason -import com.tinder.scarlet.WebSocket -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.runBlocking -import okhttp3.mockwebserver.MockWebServer -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -internal class CSHealthMetricsConnectionClosedTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - @get:Rule - val mockWebServer: MockWebServer = MockWebServer() - - private val connectionListener = mock() - private val healthEventRepository = mock() - private val server = FakeCSEventService() - private val info = fakeCSInfo() - - private val networkRepository: CSNetworkRepository by lazy { - CSNetworkRepositoryImpl( - networkConfig = mock(), - eventService = server, - dispatcher = coroutineRule.testDispatcher, - timeStampGenerator = mock(), - logger = mock(), - healthEventRepository = healthEventRepository, - info = info - ) - } - - private val networkManager: CSNetworkManager by lazy { - CSNetworkManager( - appLifeCycle = mock(), - networkRepository = networkRepository, - dispatcher = coroutineRule.testDispatcher, - logger = mock(), - healthEventRepository = healthEventRepository, - info = info, - connectionListener = connectionListener - ) - } - - @Test - fun `verify connection closed dto`() = runBlocking { - networkManager.observeSocketConnectionState() - - val dto = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionAttempt.value, - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion - ) - - verify(connectionListener, times(2)).onEventChanged(any()) - verify(healthEventRepository).insertHealthEvent(dto) - verify(healthEventRepository).insertHealthEvent( - dto.copy( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionDropped.value, - error = "Normal closure" - ) - ) - } - - private class FakeCSEventService : CSEventService { - override fun observeResponse(): Flow { - throw IllegalAccessException("broken") - } - - override fun observeSocketState(): Flow { - return flow { - emit(WebSocket.Event.OnConnectionClosed(ShutdownReason.GRACEFUL)) - } - } - - override fun sendEvent(streamBatchEvents: EventRequest): Boolean { - throw IllegalAccessException("broken") - } - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionFailedTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionFailedTest.kt deleted file mode 100644 index 1449f26a..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionFailedTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -package clickstream.internal.networklayer - -import clickstream.connection.CSSocketConnectionListener -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.de.common.EventResponse -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.tinder.scarlet.WebSocket -import java.net.SocketTimeoutException -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.runBlocking -import okhttp3.mockwebserver.MockWebServer -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -internal class CSHealthMetricsConnectionFailedTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - @get:Rule - val mockWebServer: MockWebServer = MockWebServer() - - private val connectionListener = mock() - private val healthEventRepository = mock() - private val server = FakeCSEventService() - private val info = fakeCSInfo() - - private val networkRepository: CSNetworkRepository by lazy { - CSNetworkRepositoryImpl( - networkConfig = mock(), - eventService = server, - dispatcher = coroutineRule.testDispatcher, - timeStampGenerator = mock(), - logger = mock(), - healthEventRepository = healthEventRepository, - info = info - ) - } - - private val networkManager: CSNetworkManager by lazy { - CSNetworkManager( - appLifeCycle = mock(), - networkRepository = networkRepository, - dispatcher = coroutineRule.testDispatcher, - logger = mock(), - healthEventRepository = healthEventRepository, - info = info, - connectionListener = connectionListener, - ) - } - - @Test - fun `verify connection failure dto`() = runBlocking { - networkManager.observeSocketConnectionState() - - val connAttemptDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionAttempt.value, - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion - ) - val connFailureDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionFailure.value, - error = "socket_timeout", - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion, - timeToConnection = networkManager.endConnectedTime - networkManager.startConnectingTime - ) - - verify(healthEventRepository).insertHealthEvent(connAttemptDto) - verify(healthEventRepository).insertHealthEvent(connFailureDto) - verify(connectionListener, times(2)).onEventChanged(any()) - } - - private class FakeCSEventService : CSEventService { - override fun observeResponse(): Flow { - throw IllegalAccessException("broken") - } - - override fun observeSocketState(): Flow { - return flow { - emit(WebSocket.Event.OnConnectionFailed(SocketTimeoutException("broken"))) - } - } - - override fun sendEvent(streamBatchEvents: EventRequest): Boolean { - throw IllegalAccessException("broken") - } - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionOpenedTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionOpenedTest.kt deleted file mode 100644 index a2b2e22e..00000000 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSHealthMetricsConnectionOpenedTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package clickstream.internal.networklayer - -import clickstream.connection.CSSocketConnectionListener -import clickstream.fake.fakeCSInfo -import clickstream.health.constant.CSEventNamesConstant -import clickstream.health.constant.CSEventTypesConstant -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.model.CSHealthEventDTO -import clickstream.utils.CoroutineTestRule -import com.gojek.clickstream.de.EventRequest -import com.gojek.clickstream.de.common.EventResponse -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.tinder.scarlet.WebSocket -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.runBlocking -import okhttp3.mockwebserver.MockWebServer -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -internal class CSHealthMetricsConnectionOpenedTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - @get:Rule - val mockWebServer: MockWebServer = MockWebServer() - - private val connectionListener = mock() - private val healthEventRepository = mock() - private val server = FakeCSEventService() - private val info = fakeCSInfo() - - private val networkRepository: CSNetworkRepository by lazy { - CSNetworkRepositoryImpl( - networkConfig = mock(), - eventService = server, - dispatcher = coroutineRule.testDispatcher, - timeStampGenerator = mock(), - logger = mock(), - healthEventRepository = healthEventRepository, - info = info - ) - } - - private val networkManager: CSNetworkManager by lazy { - CSNetworkManager( - appLifeCycle = mock(), - networkRepository = networkRepository, - dispatcher = coroutineRule.testDispatcher, - logger = mock(), - healthEventRepository = healthEventRepository, - info = info, - connectionListener = connectionListener - ) - } - - @Test - fun `verify connection opened dto`() = runBlocking { - networkManager.observeSocketConnectionState() - - val connAttemptDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionAttempt.value, - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion - ) - val connSuccessDto = CSHealthEventDTO( - eventName = CSEventNamesConstant.Instant.ClickStreamConnectionSuccess.value, - eventType = CSEventTypesConstant.INSTANT, - appVersion = info.appInfo.appVersion, - timeToConnection = networkManager.endConnectedTime - networkManager.startConnectingTime - ) - - verify(healthEventRepository).insertHealthEvent(connAttemptDto) - verify(healthEventRepository).insertHealthEvent(connSuccessDto) - verify(connectionListener, times(2)).onEventChanged(any()) - } - - private class FakeCSEventService : CSEventService { - override fun observeResponse(): Flow { - throw IllegalAccessException("broken") - } - - override fun observeSocketState(): Flow { - return flow { - emit(WebSocket.Event.OnConnectionOpened(Unit)) - } - } - - override fun sendEvent(streamBatchEvents: EventRequest): Boolean { - throw IllegalAccessException("broken") - } - } -} \ No newline at end of file diff --git a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSRetryableCallbackTest.kt b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSRetryableCallbackTest.kt index d45ed2e9..83026a98 100644 --- a/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSRetryableCallbackTest.kt +++ b/clickstream/src/test/kotlin/clickstream/internal/networklayer/CSRetryableCallbackTest.kt @@ -2,9 +2,9 @@ package clickstream.internal.networklayer import clickstream.config.CSNetworkConfig import clickstream.fake.fakeCSInfo -import clickstream.health.intermediate.CSHealthEventRepository -import clickstream.health.time.CSTimeStampGenerator +import clickstream.health.intermediate.CSHealthEventProcessor import clickstream.internal.utils.CSFlowStreamAdapterFactory +import clickstream.internal.utils.CSTimeStampGenerator import clickstream.logger.CSLogLevel.OFF import clickstream.logger.CSLogger import clickstream.utils.CoroutineTestRule @@ -36,12 +36,13 @@ public class CSRetryableCallbackTest { @get:Rule public val mockWebServer: MockWebServer = MockWebServer() + @get:Rule public val coroutineRule: CoroutineTestRule = CoroutineTestRule() private val serverUrlString by lazy { mockWebServer.url("/").toString() } private val timestamp = mock() - private val health = mock() + private val health = mock() private val serverLifecycleRegistry = LifecycleRegistry() private lateinit var server: CSEventService @@ -62,16 +63,15 @@ public class CSRetryableCallbackTest { // Then object : CSRetryableCallback( - networkConfig = CSNetworkConfig.default(OkHttpClient()), + networkConfig = CSNetworkConfig.default("", mapOf()), eventService = client, eventRequest = eventRequest, dispatcher = coroutineRule.testDispatcher, timeStampGenerator = timestamp, logger = CSLogger(OFF), - healthEventRepository = health, + healthProcessor = health, info = fakeCSInfo(), - coroutineScope = coroutineRule.scope, - eventGuids = "1234" + coroutineScope = coroutineRule.scope ) { override fun onSuccess(guid: String) { /*No Op*/ } diff --git a/scripts/versioning.gradle b/scripts/versioning.gradle index 7105e21d..f1ac3dec 100644 --- a/scripts/versioning.gradle +++ b/scripts/versioning.gradle @@ -1,3 +1,3 @@ ext { - gitVersionName = "2.0.0-alpha-1" + gitVersionName = "2.0.0-alpha-1-kk" } \ No newline at end of file