Skip to content

Commit

Permalink
Add error event support to Analytics manager
Browse files Browse the repository at this point in the history
COAND-979
  • Loading branch information
araratthehero committed Oct 15, 2024
1 parent 1d92608 commit 06e868b
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 4 deletions.
8 changes: 8 additions & 0 deletions components-core/api/components-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,14 @@ public final class com/adyen/checkout/components/core/internal/data/model/Analyt
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/adyen/checkout/components/core/internal/data/model/AnalyticsTrackError$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/adyen/checkout/components/core/internal/data/model/AnalyticsTrackError;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lcom/adyen/checkout/components/core/internal/data/model/AnalyticsTrackError;
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/adyen/checkout/components/core/internal/data/model/AnalyticsTrackInfo$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lcom/adyen/checkout/components/core/internal/data/model/AnalyticsTrackInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.app.Application
import androidx.annotation.RestrictTo
import com.adyen.checkout.components.core.Amount
import com.adyen.checkout.components.core.internal.analytics.data.DefaultAnalyticsRepository
import com.adyen.checkout.components.core.internal.analytics.data.local.ErrorAnalyticsLocalDataStore
import com.adyen.checkout.components.core.internal.analytics.data.local.InfoAnalyticsLocalDataStore
import com.adyen.checkout.components.core.internal.analytics.data.local.LogAnalyticsLocalDataStore
import com.adyen.checkout.components.core.internal.analytics.data.remote.AnalyticsTrackRequestProvider
Expand Down Expand Up @@ -59,13 +60,15 @@ class AnalyticsManagerFactory {
analyticsRepository = DefaultAnalyticsRepository(
localInfoDataStore = InfoAnalyticsLocalDataStore(),
localLogDataStore = LogAnalyticsLocalDataStore(),
localErrorDataStore = ErrorAnalyticsLocalDataStore(),
remoteDataStore = DefaultAnalyticsRemoteDataStore(
analyticsService = AnalyticsService(
HttpClientFactory.getAnalyticsHttpClient(environment),
),
clientKey = clientKey,
infoSize = INFO_SIZE,
logSize = LOG_SIZE,
errorSize = ERROR_SIZE,
),
analyticsSetupProvider = DefaultAnalyticsSetupProvider(
application = application,
Expand All @@ -83,5 +86,6 @@ class AnalyticsManagerFactory {
companion object {
private const val INFO_SIZE = 50
private const val LOG_SIZE = 5
private const val ERROR_SIZE = 5
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.adyen.checkout.core.internal.util.adyenLog
internal class DefaultAnalyticsRepository(
private val localInfoDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Info>,
private val localLogDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Log>,
private val localErrorDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Error>,
private val remoteDataStore: AnalyticsRemoteDataStore,
private val analyticsSetupProvider: AnalyticsSetupProvider,
private val analyticsTrackRequestProvider: AnalyticsTrackRequestProvider,
Expand All @@ -33,6 +34,7 @@ internal class DefaultAnalyticsRepository(
when (event) {
is AnalyticsEvent.Info -> localInfoDataStore.storeEvent(event)
is AnalyticsEvent.Log -> localLogDataStore.storeEvent(event)
is AnalyticsEvent.Error -> localErrorDataStore.storeEvent(event)
}
}

Expand All @@ -41,17 +43,20 @@ internal class DefaultAnalyticsRepository(
) {
val infoEvents = localInfoDataStore.fetchEvents(remoteDataStore.infoSize)
val logEvents = localLogDataStore.fetchEvents(remoteDataStore.logSize)
val errorEvents = localErrorDataStore.fetchEvents(remoteDataStore.errorSize)

if (infoEvents.isEmpty() && logEvents.isEmpty()) return
if (infoEvents.isEmpty() && logEvents.isEmpty() && errorEvents.isEmpty()) return

val request = analyticsTrackRequestProvider(
infoList = infoEvents,
logList = logEvents,
errorList = errorEvents,
)
remoteDataStore.sendEvents(request, checkoutAttemptId)

localInfoDataStore.removeEvents(infoEvents)
localLogDataStore.removeEvents(logEvents)
localErrorDataStore.removeEvents(errorEvents)

adyenLog(AdyenLogLevel.DEBUG) { "Analytics events successfully sent" }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 15/10/2024.
*/

package com.adyen.checkout.components.core.internal.analytics.data.local

import com.adyen.checkout.components.core.internal.analytics.AnalyticsEvent
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.LinkedList

internal class ErrorAnalyticsLocalDataStore : AnalyticsLocalDataStore<AnalyticsEvent.Error> {

private val list = LinkedList<AnalyticsEvent.Error>()

private val mutex = Mutex()

override suspend fun storeEvent(event: AnalyticsEvent.Error) {
mutex.withLock {
list.add(event)
}
}

override suspend fun fetchEvents(size: Int) = mutex.withLock {
list.takeLast(size)
}

override suspend fun removeEvents(events: List<AnalyticsEvent.Error>) {
mutex.withLock {
list.removeAll(events.toSet())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal interface AnalyticsRemoteDataStore {

val infoSize: Int
val logSize: Int
val errorSize: Int

suspend fun fetchCheckoutAttemptId(request: AnalyticsSetupRequest): AnalyticsSetupResponse

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.adyen.checkout.components.core.internal.analytics.data.remote

import com.adyen.checkout.components.core.internal.analytics.AnalyticsEvent
import com.adyen.checkout.components.core.internal.analytics.AnalyticsPlatformParams
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackError
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackInfo
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackLog
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackRequest
Expand All @@ -19,12 +20,14 @@ internal class AnalyticsTrackRequestProvider {
operator fun invoke(
infoList: List<AnalyticsEvent.Info>,
logList: List<AnalyticsEvent.Log>,
errorList: List<AnalyticsEvent.Error>,
): AnalyticsTrackRequest {
return AnalyticsTrackRequest(
channel = AnalyticsPlatformParams.channel,
platform = AnalyticsPlatformParams.platform,
info = infoList.map { event -> event.mapToTrackEvent() },
logs = logList.map { event -> event.mapToTrackEvent() },
errors = errorList.map { event -> event.mapToErrorEvent() },
)
}

Expand Down Expand Up @@ -52,4 +55,13 @@ internal class AnalyticsTrackRequestProvider {
message = message,
result = result,
)

private fun AnalyticsEvent.Error.mapToErrorEvent() = AnalyticsTrackError(
id = id,
timestamp = timestamp,
component = component,
errorType = errorType?.value,
code = code,
message = message,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class DefaultAnalyticsRemoteDataStore(
private val clientKey: String,
override val infoSize: Int,
override val logSize: Int,
override val errorSize: Int,
) : AnalyticsRemoteDataStore {

override suspend fun fetchCheckoutAttemptId(request: AnalyticsSetupRequest): AnalyticsSetupResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2024 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ararat on 15/10/2024.
*/

package com.adyen.checkout.components.core.internal.data.model

import com.adyen.checkout.core.exception.ModelSerializationException
import com.adyen.checkout.core.internal.data.model.ModelObject
import com.adyen.checkout.core.internal.data.model.getLongOrNull
import com.adyen.checkout.core.internal.data.model.getStringOrNull
import kotlinx.parcelize.Parcelize
import org.json.JSONException
import org.json.JSONObject

@Parcelize
internal data class AnalyticsTrackError(
val id: String,
val timestamp: Long?,
val component: String?,
val errorType: String?,
val code: String?,
val message: String?,
) : ModelObject() {

companion object {
private const val ID = "id"
private const val TIMESTAMP = "timestamp"
private const val COMPONENT = "component"
private const val ERROR_TYPE = "errorType"
private const val CODE = "code"
private const val MESSAGE = "message"

@JvmField
val SERIALIZER: Serializer<AnalyticsTrackError> = object : Serializer<AnalyticsTrackError> {
override fun serialize(modelObject: AnalyticsTrackError): JSONObject {
return try {
JSONObject().apply {
put(ID, modelObject.id)
putOpt(TIMESTAMP, modelObject.timestamp)
putOpt(COMPONENT, modelObject.component)
putOpt(ERROR_TYPE, modelObject.errorType)
putOpt(CODE, modelObject.code)
putOpt(MESSAGE, modelObject.message)
}
} catch (e: JSONException) {
throw ModelSerializationException(AnalyticsTrackError::class.java, e)
}
}

override fun deserialize(jsonObject: JSONObject): AnalyticsTrackError {
return try {
with(jsonObject) {
AnalyticsTrackError(
id = getString(ID),
timestamp = getLongOrNull(TIMESTAMP),
component = getStringOrNull(COMPONENT),
errorType = getStringOrNull(ERROR_TYPE),
code = getStringOrNull(CODE),
message = getStringOrNull(MESSAGE),
)
}
} catch (e: JSONException) {
throw ModelSerializationException(AnalyticsTrackError::class.java, e)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ internal data class AnalyticsTrackRequest(
val platform: String?,
val info: List<AnalyticsTrackInfo>?,
val logs: List<AnalyticsTrackLog>?,
val errors: List<AnalyticsTrackError>?,
) : ModelObject() {
companion object {
private const val CHANNEL = "channel"
private const val PLATFORM = "platform"
private const val INFO = "info"
private const val LOGS = "logs"
private const val ERRORS = "errors"

@JvmField
val SERIALIZER: Serializer<AnalyticsTrackRequest> = object : Serializer<AnalyticsTrackRequest> {
Expand All @@ -39,6 +41,7 @@ internal data class AnalyticsTrackRequest(
putOpt(PLATFORM, modelObject.platform)
putOpt(INFO, ModelUtils.serializeOptList(modelObject.info, AnalyticsTrackInfo.SERIALIZER))
putOpt(LOGS, ModelUtils.serializeOptList(modelObject.logs, AnalyticsTrackLog.SERIALIZER))
putOpt(ERRORS, ModelUtils.serializeOptList(modelObject.errors, AnalyticsTrackError.SERIALIZER))
}
} catch (e: JSONException) {
throw ModelSerializationException(AnalyticsTrackRequest::class.java, e)
Expand All @@ -53,6 +56,7 @@ internal data class AnalyticsTrackRequest(
platform = getStringOrNull(PLATFORM),
info = deserializeOptList(getJSONArray(INFO), AnalyticsTrackInfo.SERIALIZER),
logs = deserializeOptList(getJSONArray(LOGS), AnalyticsTrackLog.SERIALIZER),
errors = deserializeOptList(getJSONArray(ERRORS), AnalyticsTrackError.SERIALIZER),
)
}
} catch (e: JSONException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.mockito.kotlin.whenever
internal class DefaultAnalyticsRepositoryTest(
@Mock private val localInfoDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Info>,
@Mock private val localLogDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Log>,
@Mock private val localErrorDataStore: AnalyticsLocalDataStore<AnalyticsEvent.Error>,
@Mock private val remoteDataStore: AnalyticsRemoteDataStore,
@Mock private val analyticsSetupProvider: AnalyticsSetupProvider,
@Mock private val analyticsTrackRequestProvider: AnalyticsTrackRequestProvider,
Expand All @@ -40,6 +41,7 @@ internal class DefaultAnalyticsRepositoryTest(
analyticsRepository = DefaultAnalyticsRepository(
localInfoDataStore = localInfoDataStore,
localLogDataStore = localLogDataStore,
localErrorDataStore = localErrorDataStore,
remoteDataStore = remoteDataStore,
analyticsSetupProvider = analyticsSetupProvider,
analyticsTrackRequestProvider = analyticsTrackRequestProvider,
Expand Down Expand Up @@ -69,6 +71,11 @@ internal class DefaultAnalyticsRepositoryTest(
analyticsRepository.storeEvent(logEvent)

verify(localLogDataStore).storeEvent(logEvent)

val errorEvent = AnalyticsEvent.Error(component = "test")
analyticsRepository.storeEvent(errorEvent)

verify(localErrorDataStore).storeEvent(errorEvent)
}

@Nested
Expand All @@ -79,6 +86,7 @@ internal class DefaultAnalyticsRepositoryTest(
fun `there are no events stored, then sending is canceled`() = runTest {
whenever(localInfoDataStore.fetchEvents(any())) doReturn emptyList()
whenever(localLogDataStore.fetchEvents(any())) doReturn emptyList()
whenever(localErrorDataStore.fetchEvents(any())) doReturn emptyList()

analyticsRepository.sendEvents("test")

Expand All @@ -90,21 +98,25 @@ internal class DefaultAnalyticsRepositoryTest(
fun `it is successful, then events are cleared from storage`() = runTest {
val infoEvents = listOf(AnalyticsEvent.Info(component = "test info"))
val logEvents = listOf(AnalyticsEvent.Log(component = "test log"))
val errorEvents = listOf(AnalyticsEvent.Error(component = "test error"))
whenever(localInfoDataStore.fetchEvents(any())) doReturn infoEvents
whenever(localLogDataStore.fetchEvents(any())) doReturn logEvents
whenever(analyticsTrackRequestProvider.invoke(any(), any())) doReturn mock()
whenever(localErrorDataStore.fetchEvents(any())) doReturn errorEvents
whenever(analyticsTrackRequestProvider.invoke(any(), any(), any())) doReturn mock()

analyticsRepository.sendEvents("test")

verify(localInfoDataStore).removeEvents(infoEvents)
verify(localLogDataStore).removeEvents(logEvents)
verify(localErrorDataStore).removeEvents(errorEvents)
}

@Test
fun `it fails, then events are not cleared from storage`() = runTest {
whenever(localInfoDataStore.fetchEvents(any())) doReturn listOf(mock())
whenever(localLogDataStore.fetchEvents(any())) doReturn listOf(mock())
whenever(analyticsTrackRequestProvider.invoke(any(), any())) doReturn mock()
whenever(localErrorDataStore.fetchEvents(any())) doReturn listOf(mock())
whenever(analyticsTrackRequestProvider.invoke(any(), any(), any())) doReturn mock()
whenever(remoteDataStore.sendEvents(any(), any())) doAnswer { error("test") }

runCatching {
Expand All @@ -113,6 +125,7 @@ internal class DefaultAnalyticsRepositoryTest(

verify(localInfoDataStore, never()).removeEvents(any())
verify(localLogDataStore, never()).removeEvents(any())
verify(localErrorDataStore, never()).removeEvents(any())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.adyen.checkout.components.core.internal.analytics.data.remote

import com.adyen.checkout.components.core.internal.analytics.AnalyticsEvent
import com.adyen.checkout.components.core.internal.analytics.DirectAnalyticsEventCreation
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackError
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackInfo
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackLog
import com.adyen.checkout.components.core.internal.data.model.AnalyticsTrackRequest
Expand Down Expand Up @@ -47,8 +48,18 @@ internal class AnalyticsTrackRequestProviderTest {
message = null,
),
)
val errorList = listOf(
AnalyticsEvent.Error(
id = "id",
timestamp = 12345L,
component = "dropin",
errorType = AnalyticsEvent.Error.Type.INTERNAL,
code = "100",
message = null
)
)

val result = analyticsTrackRequestProvider.invoke(infoList, logList)
val result = analyticsTrackRequestProvider.invoke(infoList, logList, errorList)

val expected = AnalyticsTrackRequest(
channel = "android",
Expand Down Expand Up @@ -80,6 +91,16 @@ internal class AnalyticsTrackRequestProviderTest {
message = null,
),
),
errors = listOf(
AnalyticsTrackError(
id = "id",
timestamp = 12345L,
component = "dropin",
errorType = "Internal",
code = "100",
message = null
)
)
)
assertEquals(expected, result)
}
Expand Down
Loading

0 comments on commit 06e868b

Please sign in to comment.