diff --git a/detekt_custom.yml b/detekt_custom.yml index d4bc87ff59..ea884d1fc4 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -846,6 +846,7 @@ datadog: - "kotlin.collections.MutableCollection.forEach(kotlin.Function1)" - "kotlin.collections.MutableCollection.toList()" - "kotlin.collections.MutableIterator.hasNext()" + - "kotlin.collections.MutableList.add(com.datadog.android.api.InternalLogger.Target)" - "kotlin.collections.MutableList.add(com.datadog.android.core.internal.persistence.Batch)" - "kotlin.collections.MutableList.add(com.datadog.android.plugin.DatadogPlugin)" - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.domain.scope.RumScope)" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt index b148f58312..fd5970834b 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt @@ -230,7 +230,6 @@ internal class RumFeature constructor( return RumDataWriter( eventSerializer = MapperSerializer( RumEventMapper( - sdkCore, viewEventMapper = configuration.viewEventMapper, errorEventMapper = configuration.errorEventMapper, resourceEventMapper = configuration.resourceEventMapper, diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/RumDataWriter.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/RumDataWriter.kt index 6b9c914188..9aab9bc1f2 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/RumDataWriter.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/RumDataWriter.kt @@ -13,14 +13,7 @@ import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.persistence.Serializer import com.datadog.android.core.persistence.serializeToByteArray -import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.internal.domain.event.RumEventMeta -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.StorageEvent -import com.datadog.android.rum.model.ActionEvent -import com.datadog.android.rum.model.ErrorEvent -import com.datadog.android.rum.model.LongTaskEvent -import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.model.ViewEvent internal class RumDataWriter( @@ -70,36 +63,7 @@ internal class RumDataWriter( @WorkerThread internal fun onDataWritten(data: Any, rawData: ByteArray) { when (data) { - is ViewEvent -> persistViewEvent(rawData) - is ActionEvent -> notifyEventSent( - data.view.id, - StorageEvent.Action(data.action.frustration?.type?.size ?: 0) - ) - is ResourceEvent -> notifyEventSent(data.view.id, StorageEvent.Resource) - is ErrorEvent -> { - if (data.error.isCrash != true) { - notifyEventSent(data.view.id, StorageEvent.Error) - } - } - is LongTaskEvent -> { - if (data.longTask.isFrozenFrame == true) { - notifyEventSent(data.view.id, StorageEvent.FrozenFrame) - } else { - notifyEventSent(data.view.id, StorageEvent.LongTask) - } - } - } - } - - @WorkerThread - private fun persistViewEvent(data: ByteArray) { - sdkCore.writeLastViewEvent(data) - } - - private fun notifyEventSent(viewId: String, storageEvent: StorageEvent) { - val rumMonitor = GlobalRumMonitor.get(sdkCore) - if (rumMonitor is AdvancedRumMonitor) { - rumMonitor.eventSent(viewId, storageEvent) + is ViewEvent -> sdkCore.writeLastViewEvent(rawData) } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapper.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapper.kt index 9e09eff492..87fda4457d 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapper.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapper.kt @@ -7,12 +7,8 @@ package com.datadog.android.rum.internal.domain.event import com.datadog.android.api.InternalLogger -import com.datadog.android.api.SdkCore import com.datadog.android.event.EventMapper import com.datadog.android.event.NoOpEventMapper -import com.datadog.android.rum.GlobalRumMonitor -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.LongTaskEvent @@ -24,7 +20,6 @@ import com.datadog.android.telemetry.model.TelemetryErrorEvent import java.util.Locale internal data class RumEventMapper( - val sdkCore: SdkCore, val viewEventMapper: EventMapper = NoOpEventMapper(), val errorEventMapper: EventMapper = NoOpEventMapper(), val resourceEventMapper: EventMapper = NoOpEventMapper(), @@ -35,11 +30,7 @@ internal data class RumEventMapper( ) : EventMapper { override fun map(event: Any): Any? { - val mappedEvent = resolveEvent(event) - if (mappedEvent == null) { - notifyEventDropped(event) - } - return mappedEvent + return resolveEvent(event) } // region Internal @@ -113,28 +104,6 @@ internal data class RumEventMapper( } } - private fun notifyEventDropped(event: Any) { - val monitor = (GlobalRumMonitor.get(sdkCore) as? AdvancedRumMonitor) ?: return - when (event) { - is ActionEvent -> monitor.eventDropped( - event.view.id, - StorageEvent.Action(frustrationCount = event.action.frustration?.type?.size ?: 0) - ) - is ResourceEvent -> monitor.eventDropped(event.view.id, StorageEvent.Resource) - is ErrorEvent -> monitor.eventDropped(event.view.id, StorageEvent.Error) - is LongTaskEvent -> { - if (event.longTask.isFrozenFrame == true) { - monitor.eventDropped(event.view.id, StorageEvent.FrozenFrame) - } else { - monitor.eventDropped(event.view.id, StorageEvent.LongTask) - } - } - else -> { - // Nothing to do - } - } - } - // endregion companion object { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt index 417a0265d9..ced336ab15 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt @@ -7,7 +7,6 @@ package com.datadog.android.rum.internal.domain.scope import androidx.annotation.WorkerThread -import com.datadog.android.api.feature.Feature import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.rum.GlobalRumMonitor @@ -15,8 +14,10 @@ import com.datadog.android.rum.RumActionType import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.utils.hasUserData +import com.datadog.android.rum.utils.newRumEventWriteOperation import java.lang.ref.WeakReference import java.util.UUID import java.util.concurrent.TimeUnit @@ -220,90 +221,96 @@ internal class RumActionScope( } else { ActionEvent.ActionEventSessionType.SYNTHETICS } + val frustrations = mutableListOf() + if (trackFrustrations && eventErrorCount > 0 && actualType == RumActionType.TAP) { + frustrations.add(ActionEvent.Type.ERROR_TAP) + } - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - rumContext.viewId.orEmpty() - ) - val frustrations = mutableListOf() - if (trackFrustrations && eventErrorCount > 0 && actualType == RumActionType.TAP) { - frustrations.add(ActionEvent.Type.ERROR_TAP) - } + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + rumContext.viewId.orEmpty() + ) - val actionEvent = ActionEvent( - date = eventTimestamp, - action = ActionEvent.ActionEventAction( - type = actualType.toSchemaType(), - id = actionId, - target = ActionEvent.ActionEventActionTarget(eventName), - error = ActionEvent.Error(eventErrorCount), - crash = ActionEvent.Crash(eventCrashCount), - longTask = ActionEvent.LongTask(eventLongTaskCount), - resource = ActionEvent.Resource(eventResourceCount), - loadingTime = max(endNanos - startedNanos, 1L), - frustration = if (frustrations.isNotEmpty()) { - ActionEvent.Frustration(frustrations) - } else { - null - } - ), - view = ActionEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - application = ActionEvent.Application(rumContext.applicationId), - session = ActionEvent.ActionEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay - ), - synthetics = syntheticsAttribute, - source = ActionEvent.ActionEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - usr = if (user.hasUserData()) { - ActionEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) + ActionEvent( + date = eventTimestamp, + action = ActionEvent.ActionEventAction( + type = actualType.toSchemaType(), + id = actionId, + target = ActionEvent.ActionEventActionTarget(eventName), + error = ActionEvent.Error(eventErrorCount), + crash = ActionEvent.Crash(eventCrashCount), + longTask = ActionEvent.LongTask(eventLongTaskCount), + resource = ActionEvent.Resource(eventResourceCount), + loadingTime = max(endNanos - startedNanos, 1L), + frustration = if (frustrations.isNotEmpty()) { + ActionEvent.Frustration(frustrations) } else { null - }, - os = ActionEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion + } + ), + view = ActionEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + application = ActionEvent.Application(rumContext.applicationId), + session = ActionEvent.ActionEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay + ), + synthetics = syntheticsAttribute, + source = ActionEvent.ActionEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + usr = if (user.hasUserData()) { + ActionEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() + ) + } else { + null + }, + os = ActionEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ActionEvent.Device( + type = datadogContext.deviceInfo.deviceType.toActionSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ActionEvent.Context(additionalProperties = attributes), + dd = ActionEvent.Dd( + session = ActionEvent.DdSession( + plan = ActionEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition() ), - device = ActionEvent.Device( - type = datadogContext.deviceInfo.deviceType.toActionSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = ActionEvent.Context(additionalProperties = attributes), - dd = ActionEvent.Dd( - session = ActionEvent.DdSession( - plan = ActionEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition() - ), - configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate) - ), - connectivity = networkInfo.toActionConnectivity(), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, actionEvent) + configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate) + ), + connectivity = networkInfo.toActionConnectivity(), + service = datadogContext.service, + version = datadogContext.version + ) + } + .apply { + val storageEvent = StorageEvent.Action(frustrations.size) + onError { + it.eventDropped(rumContext.viewId.orEmpty(), storageEvent) + } + onSuccess { + it.eventSent(rumContext.viewId.orEmpty(), storageEvent) + } } + .submit() sent = true } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt index 928eeeae91..236d47d77a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt @@ -8,7 +8,6 @@ package com.datadog.android.rum.internal.domain.scope import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger -import com.datadog.android.api.feature.Feature import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver @@ -22,9 +21,11 @@ import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.event.ResourceTiming +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.utils.hasUserData +import com.datadog.android.rum.utils.newRumEventWriteOperation import java.net.MalformedURLException import java.net.URL import java.util.Locale @@ -203,90 +204,93 @@ internal class RumResourceScope( attributes.remove(RumAttributes.GRAPHQL_PAYLOAD) as? String?, attributes.remove(RumAttributes.GRAPHQL_VARIABLES) as? String? ) - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - rumContext.viewId.orEmpty() - ) - val duration = resolveResourceDuration(eventTime) - val resourceEvent = ResourceEvent( - date = eventTimestamp, - resource = ResourceEvent.Resource( - id = resourceId, - type = kind.toSchemaType(), - url = url, - duration = duration, - method = method.toResourceMethod(), - statusCode = statusCode, - size = size, - dns = finalTiming?.dns(), - connect = finalTiming?.connect(), - ssl = finalTiming?.ssl(), - firstByte = finalTiming?.firstByte(), - download = finalTiming?.download(), - provider = resolveResourceProvider(), - graphql = graphql - ), - action = rumContext.actionId?.let { ResourceEvent.Action(listOf(it)) }, - view = ResourceEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - usr = if (user.hasUserData()) { - ResourceEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) - } else { - null - }, - connectivity = networkInfo.toResourceConnectivity(), - application = ResourceEvent.Application(rumContext.applicationId), - session = ResourceEvent.ResourceEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay - ), - synthetics = syntheticsAttribute, - source = ResourceEvent.ResourceEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - os = ResourceEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = ResourceEvent.Device( - type = datadogContext.deviceInfo.deviceType.toResourceSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = ResourceEvent.Context(additionalProperties = attributes), - dd = ResourceEvent.Dd( - traceId = traceId, - spanId = spanId, - rulePsr = rulePsr, - session = ResourceEvent.DdSession( - plan = ResourceEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toResourceSessionPrecondition() - ), - configuration = ResourceEvent.Configuration(sessionSampleRate = sampleRate) + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + rumContext.viewId.orEmpty() + ) + val duration = resolveResourceDuration(eventTime) + ResourceEvent( + date = eventTimestamp, + resource = ResourceEvent.Resource( + id = resourceId, + type = kind.toSchemaType(), + url = url, + duration = duration, + method = method.toResourceMethod(), + statusCode = statusCode, + size = size, + dns = finalTiming?.dns(), + connect = finalTiming?.connect(), + ssl = finalTiming?.ssl(), + firstByte = finalTiming?.firstByte(), + download = finalTiming?.download(), + provider = resolveResourceProvider(), + graphql = graphql + ), + action = rumContext.actionId?.let { ResourceEvent.Action(listOf(it)) }, + view = ResourceEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + usr = if (user.hasUserData()) { + ResourceEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() + ) + } else { + null + }, + connectivity = networkInfo.toResourceConnectivity(), + application = ResourceEvent.Application(rumContext.applicationId), + session = ResourceEvent.ResourceEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay + ), + synthetics = syntheticsAttribute, + source = ResourceEvent.ResourceEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + os = ResourceEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ResourceEvent.Device( + type = datadogContext.deviceInfo.deviceType.toResourceSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ResourceEvent.Context(additionalProperties = attributes), + dd = ResourceEvent.Dd( + traceId = traceId, + spanId = spanId, + rulePsr = rulePsr, + session = ResourceEvent.DdSession( + plan = ResourceEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toResourceSessionPrecondition() ), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, resourceEvent) + configuration = ResourceEvent.Configuration(sessionSampleRate = sampleRate) + ), + service = datadogContext.service, + version = datadogContext.version + ) + } + .onError { + it.eventDropped(rumContext.viewId.orEmpty(), StorageEvent.Resource) + } + .onSuccess { + it.eventSent(rumContext.viewId.orEmpty(), StorageEvent.Resource) } + .submit() sent = true } @@ -345,84 +349,87 @@ internal class RumResourceScope( } else { ErrorEvent.ErrorEventSessionType.SYNTHETICS } - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - rumContext.viewId.orEmpty() - ) - val errorEvent = ErrorEvent( - date = eventTimestamp, - error = ErrorEvent.Error( - message = message, - source = source.toSchemaSource(), - stack = stackTrace, - isCrash = false, - resource = ErrorEvent.Resource( - url = url, - method = method.toErrorMethod(), - statusCode = statusCode ?: 0, - provider = resolveErrorProvider() - ), - type = errorType, - sourceType = ErrorEvent.SourceType.ANDROID - ), - action = rumContext.actionId?.let { ErrorEvent.Action(listOf(it)) }, - view = ErrorEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - usr = if (user.hasUserData()) { - ErrorEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) - } else { - null - }, - connectivity = networkInfo.toErrorConnectivity(), - application = ErrorEvent.Application(rumContext.applicationId), - session = ErrorEvent.ErrorEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay - ), - synthetics = syntheticsAttribute, - source = ErrorEvent.ErrorEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - os = ErrorEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = ErrorEvent.Device( - type = datadogContext.deviceInfo.deviceType.toErrorSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + rumContext.viewId.orEmpty() + ) + ErrorEvent( + date = eventTimestamp, + error = ErrorEvent.Error( + message = message, + source = source.toSchemaSource(), + stack = stackTrace, + isCrash = false, + resource = ErrorEvent.Resource( + url = url, + method = method.toErrorMethod(), + statusCode = statusCode ?: 0, + provider = resolveErrorProvider() ), - context = ErrorEvent.Context(additionalProperties = attributes), - dd = ErrorEvent.Dd( - session = ErrorEvent.DdSession( - plan = ErrorEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toErrorSessionPrecondition() - ), - configuration = ErrorEvent.Configuration(sessionSampleRate = sampleRate) + type = errorType, + sourceType = ErrorEvent.SourceType.ANDROID + ), + action = rumContext.actionId?.let { ErrorEvent.Action(listOf(it)) }, + view = ErrorEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + usr = if (user.hasUserData()) { + ErrorEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() + ) + } else { + null + }, + connectivity = networkInfo.toErrorConnectivity(), + application = ErrorEvent.Application(rumContext.applicationId), + session = ErrorEvent.ErrorEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay + ), + synthetics = syntheticsAttribute, + source = ErrorEvent.ErrorEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + os = ErrorEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ErrorEvent.Device( + type = datadogContext.deviceInfo.deviceType.toErrorSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ErrorEvent.Context(additionalProperties = attributes), + dd = ErrorEvent.Dd( + session = ErrorEvent.DdSession( + plan = ErrorEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toErrorSessionPrecondition() ), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, errorEvent) + configuration = ErrorEvent.Configuration(sessionSampleRate = sampleRate) + ), + service = datadogContext.service, + version = datadogContext.version + ) + } + .onError { + it.eventDropped(rumContext.viewId.orEmpty(), StorageEvent.Error) + } + .onSuccess { + it.eventSent(rumContext.viewId.orEmpty(), StorageEvent.Error) } + .submit() sent = true } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt index b0268f0407..8d0e8a80b2 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt @@ -22,6 +22,7 @@ import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.internal.vitals.VitalInfo import com.datadog.android.rum.internal.vitals.VitalListener import com.datadog.android.rum.internal.vitals.VitalMonitor @@ -30,6 +31,7 @@ import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.LongTaskEvent import com.datadog.android.rum.model.ViewEvent import com.datadog.android.rum.utils.hasUserData +import com.datadog.android.rum.utils.newRumEventWriteOperation import com.datadog.android.rum.utils.resolveViewUrl import java.lang.ref.Reference import java.lang.ref.WeakReference @@ -381,96 +383,100 @@ internal open class RumViewScope( event.message } - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - rumContext.viewId.orEmpty() + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + rumContext.viewId.orEmpty() + ) + val syntheticsAttribute = if ( + rumContext.syntheticsTestId.isNullOrBlank() || + rumContext.syntheticsResultId.isNullOrBlank() + ) { + null + } else { + ErrorEvent.Synthetics( + testId = rumContext.syntheticsTestId, + resultId = rumContext.syntheticsResultId ) - val syntheticsAttribute = if ( - rumContext.syntheticsTestId.isNullOrBlank() || - rumContext.syntheticsResultId.isNullOrBlank() - ) { - null - } else { - ErrorEvent.Synthetics( - testId = rumContext.syntheticsTestId, - resultId = rumContext.syntheticsResultId + } + val sessionType = if (syntheticsAttribute == null) { + ErrorEvent.ErrorEventSessionType.USER + } else { + ErrorEvent.ErrorEventSessionType.SYNTHETICS + } + ErrorEvent( + date = event.eventTime.timestamp + serverTimeOffsetInMs, + featureFlags = ErrorEvent.Context(featureFlags), + error = ErrorEvent.Error( + message = message, + source = event.source.toSchemaSource(), + stack = event.stacktrace ?: event.throwable?.loggableStackTrace(), + isCrash = isFatal, + type = errorType, + sourceType = event.sourceType.toSchemaSourceType() + ), + action = rumContext.actionId?.let { ErrorEvent.Action(listOf(it)) }, + view = ErrorEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + usr = if (user.hasUserData()) { + ErrorEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() ) - } - val sessionType = if (syntheticsAttribute == null) { - ErrorEvent.ErrorEventSessionType.USER } else { - ErrorEvent.ErrorEventSessionType.SYNTHETICS - } - val errorEvent = ErrorEvent( - date = event.eventTime.timestamp + serverTimeOffsetInMs, - featureFlags = ErrorEvent.Context(featureFlags), - error = ErrorEvent.Error( - message = message, - source = event.source.toSchemaSource(), - stack = event.stacktrace ?: event.throwable?.loggableStackTrace(), - isCrash = isFatal, - type = errorType, - sourceType = event.sourceType.toSchemaSourceType() - ), - action = rumContext.actionId?.let { ErrorEvent.Action(listOf(it)) }, - view = ErrorEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - usr = if (user.hasUserData()) { - ErrorEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) - } else { - null - }, - connectivity = datadogContext.networkInfo.toErrorConnectivity(), - application = ErrorEvent.Application(rumContext.applicationId), - session = ErrorEvent.ErrorEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay - ), - synthetics = syntheticsAttribute, - source = ErrorEvent.ErrorEventSource.tryFromSource( - source = datadogContext.source, - internalLogger = sdkCore.internalLogger - ), - os = ErrorEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = ErrorEvent.Device( - type = datadogContext.deviceInfo.deviceType.toErrorSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = ErrorEvent.Context(additionalProperties = updatedAttributes), - dd = ErrorEvent.Dd( - session = ErrorEvent.DdSession( - plan = ErrorEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toErrorSessionPrecondition() - ), - configuration = ErrorEvent.Configuration(sessionSampleRate = sampleRate) + null + }, + connectivity = datadogContext.networkInfo.toErrorConnectivity(), + application = ErrorEvent.Application(rumContext.applicationId), + session = ErrorEvent.ErrorEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay + ), + synthetics = syntheticsAttribute, + source = ErrorEvent.ErrorEventSource.tryFromSource( + source = datadogContext.source, + internalLogger = sdkCore.internalLogger + ), + os = ErrorEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ErrorEvent.Device( + type = datadogContext.deviceInfo.deviceType.toErrorSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ErrorEvent.Context(additionalProperties = updatedAttributes), + dd = ErrorEvent.Dd( + session = ErrorEvent.DdSession( + plan = ErrorEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toErrorSessionPrecondition() ), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, errorEvent) + configuration = ErrorEvent.Configuration(sessionSampleRate = sampleRate) + ), + service = datadogContext.service, + version = datadogContext.version + ) + } + .apply { + if (!isFatal) { + // if fatal, then we don't have time for the notification, app is crashing + onError { it.eventDropped(rumContext.viewId.orEmpty(), StorageEvent.Error) } + onSuccess { it.eventSent(rumContext.viewId.orEmpty(), StorageEvent.Error) } + } } + .submit() if (isFatal) { errorCount++ @@ -720,120 +726,117 @@ internal open class RumViewScope( val refreshRateInfo = lastFrameRateInfo val isSlowRendered = resolveRefreshRateInfo(refreshRateInfo) ?: false - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - val currentViewId = rumContext.viewId.orEmpty() - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - currentViewId - ) - val sessionReplayRecordsCount = featuresContextResolver.resolveViewRecordsCount( - datadogContext, - currentViewId + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> + val currentViewId = rumContext.viewId.orEmpty() + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + currentViewId + ) + val sessionReplayRecordsCount = featuresContextResolver.resolveViewRecordsCount( + datadogContext, + currentViewId + ) + val replayStats = ViewEvent.ReplayStats(recordsCount = sessionReplayRecordsCount) + val syntheticsAttribute = if ( + rumContext.syntheticsTestId.isNullOrBlank() || + rumContext.syntheticsResultId.isNullOrBlank() + ) { + null + } else { + ViewEvent.Synthetics( + testId = rumContext.syntheticsTestId, + resultId = rumContext.syntheticsResultId ) - val replayStats = ViewEvent.ReplayStats(recordsCount = sessionReplayRecordsCount) - val syntheticsAttribute = if ( - rumContext.syntheticsTestId.isNullOrBlank() || - rumContext.syntheticsResultId.isNullOrBlank() - ) { - null - } else { - ViewEvent.Synthetics( - testId = rumContext.syntheticsTestId, - resultId = rumContext.syntheticsResultId - ) - } - val sessionType = if (syntheticsAttribute == null) { - ViewEvent.ViewEventSessionType.USER - } else { - ViewEvent.ViewEventSessionType.SYNTHETICS - } + } + val sessionType = if (syntheticsAttribute == null) { + ViewEvent.ViewEventSessionType.USER + } else { + ViewEvent.ViewEventSessionType.SYNTHETICS + } - val viewEvent = ViewEvent( - date = eventTimestamp, - featureFlags = ViewEvent.Context(additionalProperties = featureFlags), - view = ViewEvent.View( - id = currentViewId, - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty(), - timeSpent = updatedDurationNs, - action = ViewEvent.Action(eventActionCount), - resource = ViewEvent.Resource(eventResourceCount), - error = ViewEvent.Error(eventErrorCount), - crash = ViewEvent.Crash(eventCrashCount), - longTask = ViewEvent.LongTask(eventLongTaskCount), - frozenFrame = ViewEvent.FrozenFrame(eventFrozenFramesCount), - customTimings = timings, - isActive = !viewComplete, - cpuTicksCount = eventCpuTicks, - cpuTicksPerSecond = if (updatedDurationNs >= ONE_SECOND_NS) { - eventCpuTicks?.let { (it * ONE_SECOND_NS) / updatedDurationNs } - } else { - null - }, - memoryAverage = memoryInfo?.meanValue, - memoryMax = memoryInfo?.maxValue, - refreshRateAverage = refreshRateInfo?.meanValue, - refreshRateMin = refreshRateInfo?.minValue, - isSlowRendered = isSlowRendered, - frustration = ViewEvent.Frustration(eventFrustrationCount.toLong()), - flutterBuildTime = eventFlutterBuildTime, - flutterRasterTime = eventFlutterRasterTime, - jsRefreshRate = eventJsRefreshRate - ), - usr = if (user.hasUserData()) { - ViewEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) + ViewEvent( + date = eventTimestamp, + featureFlags = ViewEvent.Context(additionalProperties = featureFlags), + view = ViewEvent.View( + id = currentViewId, + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty(), + timeSpent = updatedDurationNs, + action = ViewEvent.Action(eventActionCount), + resource = ViewEvent.Resource(eventResourceCount), + error = ViewEvent.Error(eventErrorCount), + crash = ViewEvent.Crash(eventCrashCount), + longTask = ViewEvent.LongTask(eventLongTaskCount), + frozenFrame = ViewEvent.FrozenFrame(eventFrozenFramesCount), + customTimings = timings, + isActive = !viewComplete, + cpuTicksCount = eventCpuTicks, + cpuTicksPerSecond = if (updatedDurationNs >= ONE_SECOND_NS) { + eventCpuTicks?.let { (it * ONE_SECOND_NS) / updatedDurationNs } } else { null }, - application = ViewEvent.Application(rumContext.applicationId), - session = ViewEvent.ViewEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay, - isActive = rumContext.isSessionActive - ), - synthetics = syntheticsAttribute, - source = ViewEvent.ViewEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - os = ViewEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = ViewEvent.Device( - type = datadogContext.deviceInfo.deviceType.toViewSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = ViewEvent.Context(additionalProperties = attributes), - dd = ViewEvent.Dd( - documentVersion = eventVersion, - session = ViewEvent.DdSession( - plan = ViewEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toViewSessionPrecondition() - ), - replayStats = replayStats, - configuration = ViewEvent.Configuration(sessionSampleRate = sampleRate) + memoryAverage = memoryInfo?.meanValue, + memoryMax = memoryInfo?.maxValue, + refreshRateAverage = refreshRateInfo?.meanValue, + refreshRateMin = refreshRateInfo?.minValue, + isSlowRendered = isSlowRendered, + frustration = ViewEvent.Frustration(eventFrustrationCount.toLong()), + flutterBuildTime = eventFlutterBuildTime, + flutterRasterTime = eventFlutterRasterTime, + jsRefreshRate = eventJsRefreshRate + ), + usr = if (user.hasUserData()) { + ViewEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() + ) + } else { + null + }, + application = ViewEvent.Application(rumContext.applicationId), + session = ViewEvent.ViewEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay, + isActive = rumContext.isSessionActive + ), + synthetics = syntheticsAttribute, + source = ViewEvent.ViewEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + os = ViewEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ViewEvent.Device( + type = datadogContext.deviceInfo.deviceType.toViewSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ViewEvent.Context(additionalProperties = attributes), + dd = ViewEvent.Dd( + documentVersion = eventVersion, + session = ViewEvent.DdSession( + plan = ViewEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toViewSessionPrecondition() ), - connectivity = datadogContext.networkInfo.toViewConnectivity(), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, viewEvent) - } + replayStats = replayStats, + configuration = ViewEvent.Configuration(sessionSampleRate = sampleRate) + ), + connectivity = datadogContext.networkInfo.toViewConnectivity(), + service = datadogContext.service, + version = datadogContext.version + ) + } + .submit() } private fun resolveViewDuration(event: RumRawEvent): Long { @@ -884,93 +887,95 @@ internal open class RumViewScope( val globalAttributes = GlobalRumMonitor.get(sdkCore).getAttributes().toMutableMap() - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> - val user = datadogContext.userInfo - val syntheticsAttribute = if ( - rumContext.syntheticsTestId.isNullOrBlank() || - rumContext.syntheticsResultId.isNullOrBlank() - ) { - null - } else { - ActionEvent.Synthetics( - testId = rumContext.syntheticsTestId, - resultId = rumContext.syntheticsResultId + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> + val user = datadogContext.userInfo + val syntheticsAttribute = if ( + rumContext.syntheticsTestId.isNullOrBlank() || + rumContext.syntheticsResultId.isNullOrBlank() + ) { + null + } else { + ActionEvent.Synthetics( + testId = rumContext.syntheticsTestId, + resultId = rumContext.syntheticsResultId + ) + } + val sessionType = if (syntheticsAttribute == null) { + ActionEvent.ActionEventSessionType.USER + } else { + ActionEvent.ActionEventSessionType.SYNTHETICS + } + + ActionEvent( + date = eventTimestamp, + action = ActionEvent.ActionEventAction( + type = ActionEvent.ActionEventActionType.APPLICATION_START, + id = UUID.randomUUID().toString(), + error = ActionEvent.Error(0), + crash = ActionEvent.Crash(0), + longTask = ActionEvent.LongTask(0), + resource = ActionEvent.Resource(0), + loadingTime = event.applicationStartupNanos + ), + view = ActionEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + usr = if (user.hasUserData()) { + ActionEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() ) - } - val sessionType = if (syntheticsAttribute == null) { - ActionEvent.ActionEventSessionType.USER } else { - ActionEvent.ActionEventSessionType.SYNTHETICS - } - - val actionEvent = ActionEvent( - date = eventTimestamp, - action = ActionEvent.ActionEventAction( - type = ActionEvent.ActionEventActionType.APPLICATION_START, - id = UUID.randomUUID().toString(), - error = ActionEvent.Error(0), - crash = ActionEvent.Crash(0), - longTask = ActionEvent.LongTask(0), - resource = ActionEvent.Resource(0), - loadingTime = event.applicationStartupNanos - ), - view = ActionEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - usr = if (user.hasUserData()) { - ActionEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) - } else { - null - }, - application = ActionEvent.Application(rumContext.applicationId), - session = ActionEvent.ActionEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = false - ), - synthetics = syntheticsAttribute, - source = ActionEvent.ActionEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - os = ActionEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = ActionEvent.Device( - type = datadogContext.deviceInfo.deviceType.toActionSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = ActionEvent.Context( - additionalProperties = globalAttributes - ), - dd = ActionEvent.Dd( - session = ActionEvent.DdSession( - plan = ActionEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition() - ), - configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate) + null + }, + application = ActionEvent.Application(rumContext.applicationId), + session = ActionEvent.ActionEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = false + ), + synthetics = syntheticsAttribute, + source = ActionEvent.ActionEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + os = ActionEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = ActionEvent.Device( + type = datadogContext.deviceInfo.deviceType.toActionSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = ActionEvent.Context( + additionalProperties = globalAttributes + ), + dd = ActionEvent.Dd( + session = ActionEvent.DdSession( + plan = ActionEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition() ), - connectivity = datadogContext.networkInfo.toActionConnectivity(), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, actionEvent) + configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate) + ), + connectivity = datadogContext.networkInfo.toActionConnectivity(), + service = datadogContext.service, + version = datadogContext.version + ) + } + .apply { + val storageEvent = StorageEvent.Action(0) + onError { it.eventDropped(rumContext.viewId.orEmpty(), storageEvent) } + onSuccess { it.eventSent(rumContext.viewId.orEmpty(), storageEvent) } } + .submit() } @Suppress("LongMethod") @@ -985,91 +990,94 @@ internal open class RumViewScope( ) val timestamp = event.eventTime.timestamp + serverTimeOffsetInMs val isFrozenFrame = event.durationNs > FROZEN_FRAME_THRESHOLD_NS - sdkCore.getFeature(Feature.RUM_FEATURE_NAME) - ?.withWriteContext { datadogContext, eventBatchWriter -> + sdkCore.newRumEventWriteOperation(writer) { datadogContext -> - val user = datadogContext.userInfo - val hasReplay = featuresContextResolver.resolveViewHasReplay( - datadogContext, - rumContext.viewId.orEmpty() + val user = datadogContext.userInfo + val hasReplay = featuresContextResolver.resolveViewHasReplay( + datadogContext, + rumContext.viewId.orEmpty() + ) + val syntheticsAttribute = if ( + rumContext.syntheticsTestId.isNullOrBlank() || + rumContext.syntheticsResultId.isNullOrBlank() + ) { + null + } else { + LongTaskEvent.Synthetics( + testId = rumContext.syntheticsTestId, + resultId = rumContext.syntheticsResultId ) - val syntheticsAttribute = if ( - rumContext.syntheticsTestId.isNullOrBlank() || - rumContext.syntheticsResultId.isNullOrBlank() - ) { - null - } else { - LongTaskEvent.Synthetics( - testId = rumContext.syntheticsTestId, - resultId = rumContext.syntheticsResultId + } + val sessionType = if (syntheticsAttribute == null) { + LongTaskEvent.LongTaskEventSessionType.USER + } else { + LongTaskEvent.LongTaskEventSessionType.SYNTHETICS + } + LongTaskEvent( + date = timestamp - TimeUnit.NANOSECONDS.toMillis(event.durationNs), + longTask = LongTaskEvent.LongTask( + duration = event.durationNs, + isFrozenFrame = isFrozenFrame + ), + action = rumContext.actionId?.let { LongTaskEvent.Action(listOf(it)) }, + view = LongTaskEvent.View( + id = rumContext.viewId.orEmpty(), + name = rumContext.viewName, + url = rumContext.viewUrl.orEmpty() + ), + usr = if (user.hasUserData()) { + LongTaskEvent.Usr( + id = user.id, + name = user.name, + email = user.email, + additionalProperties = user.additionalProperties.toMutableMap() ) - } - val sessionType = if (syntheticsAttribute == null) { - LongTaskEvent.LongTaskEventSessionType.USER } else { - LongTaskEvent.LongTaskEventSessionType.SYNTHETICS - } - val longTaskEvent = LongTaskEvent( - date = timestamp - TimeUnit.NANOSECONDS.toMillis(event.durationNs), - longTask = LongTaskEvent.LongTask( - duration = event.durationNs, - isFrozenFrame = isFrozenFrame - ), - action = rumContext.actionId?.let { LongTaskEvent.Action(listOf(it)) }, - view = LongTaskEvent.View( - id = rumContext.viewId.orEmpty(), - name = rumContext.viewName, - url = rumContext.viewUrl.orEmpty() - ), - usr = if (user.hasUserData()) { - LongTaskEvent.Usr( - id = user.id, - name = user.name, - email = user.email, - additionalProperties = user.additionalProperties.toMutableMap() - ) - } else { - null - }, - connectivity = datadogContext.networkInfo.toLongTaskConnectivity(), - application = LongTaskEvent.Application(rumContext.applicationId), - session = LongTaskEvent.LongTaskEventSession( - id = rumContext.sessionId, - type = sessionType, - hasReplay = hasReplay - ), - synthetics = syntheticsAttribute, - source = LongTaskEvent.LongTaskEventSource.tryFromSource( - datadogContext.source, - sdkCore.internalLogger - ), - os = LongTaskEvent.Os( - name = datadogContext.deviceInfo.osName, - version = datadogContext.deviceInfo.osVersion, - versionMajor = datadogContext.deviceInfo.osMajorVersion - ), - device = LongTaskEvent.Device( - type = datadogContext.deviceInfo.deviceType.toLongTaskSchemaType(), - name = datadogContext.deviceInfo.deviceName, - model = datadogContext.deviceInfo.deviceModel, - brand = datadogContext.deviceInfo.deviceBrand, - architecture = datadogContext.deviceInfo.architecture - ), - context = LongTaskEvent.Context(additionalProperties = updatedAttributes), - dd = LongTaskEvent.Dd( - session = LongTaskEvent.DdSession( - plan = LongTaskEvent.Plan.PLAN_1, - sessionPrecondition = rumContext.sessionStartReason.toLongTaskSessionPrecondition() - ), - configuration = LongTaskEvent.Configuration(sessionSampleRate = sampleRate) + null + }, + connectivity = datadogContext.networkInfo.toLongTaskConnectivity(), + application = LongTaskEvent.Application(rumContext.applicationId), + session = LongTaskEvent.LongTaskEventSession( + id = rumContext.sessionId, + type = sessionType, + hasReplay = hasReplay + ), + synthetics = syntheticsAttribute, + source = LongTaskEvent.LongTaskEventSource.tryFromSource( + datadogContext.source, + sdkCore.internalLogger + ), + os = LongTaskEvent.Os( + name = datadogContext.deviceInfo.osName, + version = datadogContext.deviceInfo.osVersion, + versionMajor = datadogContext.deviceInfo.osMajorVersion + ), + device = LongTaskEvent.Device( + type = datadogContext.deviceInfo.deviceType.toLongTaskSchemaType(), + name = datadogContext.deviceInfo.deviceName, + model = datadogContext.deviceInfo.deviceModel, + brand = datadogContext.deviceInfo.deviceBrand, + architecture = datadogContext.deviceInfo.architecture + ), + context = LongTaskEvent.Context(additionalProperties = updatedAttributes), + dd = LongTaskEvent.Dd( + session = LongTaskEvent.DdSession( + plan = LongTaskEvent.Plan.PLAN_1, + sessionPrecondition = rumContext.sessionStartReason.toLongTaskSessionPrecondition() ), - service = datadogContext.service, - version = datadogContext.version - ) - - @Suppress("ThreadSafety") // called in a worker thread context - writer.write(eventBatchWriter, longTaskEvent) + configuration = LongTaskEvent.Configuration(sessionSampleRate = sampleRate) + ), + service = datadogContext.service, + version = datadogContext.version + ) + } + .apply { + val storageEvent = + if (isFrozenFrame) StorageEvent.FrozenFrame else StorageEvent.LongTask + onError { it.eventDropped(rumContext.viewId.orEmpty(), storageEvent) } + onSuccess { it.eventSent(rumContext.viewId.orEmpty(), storageEvent) } } + .submit() pendingLongTaskCount++ if (isFrozenFrame) pendingFrozenFrameCount++ diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/utils/SdkCoreExt.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/utils/SdkCoreExt.kt new file mode 100644 index 0000000000..899cf3e7e0 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/utils/SdkCoreExt.kt @@ -0,0 +1,103 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor + +internal typealias EventOutcomeAction = (rumMonitor: AdvancedRumMonitor) -> Unit + +internal class WriteOperation( + private val sdkCore: FeatureSdkCore, + private val rumDataWriter: DataWriter, + private val eventSource: (DatadogContext) -> Any +) { + private val advancedRumMonitor = GlobalRumMonitor.get(sdkCore) as? AdvancedRumMonitor + private var onError: EventOutcomeAction = NO_OP_EVENT_OUTCOME_ACTION + private var onSuccess: EventOutcomeAction = NO_OP_EVENT_OUTCOME_ACTION + + /** + * Invoked if write operation failed. Invocation is done on the worker thread. + */ + fun onError(action: EventOutcomeAction): WriteOperation { + onError = action + return this + } + + /** + * Invoked if write operation failed. Invocation is done on the worker thread. + */ + fun onSuccess(action: EventOutcomeAction): WriteOperation { + onSuccess = action + return this + } + + fun submit() { + sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + ?.withWriteContext { datadogContext, eventBatchWriter -> + try { + val event = eventSource(datadogContext) + + @Suppress("ThreadSafety") // called in a worker thread context + val isSuccess = rumDataWriter.write(eventBatchWriter, event) + if (isSuccess) { + advancedRumMonitor?.let { + onSuccess(it) + } + } else { + notifyEventWriteFailure() + } + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + notifyEventWriteFailure(e) + } + } + } + + private fun notifyEventWriteFailure(exception: Exception? = null) { + val targets = mutableListOf(InternalLogger.Target.USER).apply { + // if no exception, no need to notify telemetry, probably we already handled failure + // internally and sent it to telemetry + if (exception != null) add(InternalLogger.Target.TELEMETRY) + } + sdkCore.internalLogger.log( + level = InternalLogger.Level.ERROR, + targets = targets, + messageBuilder = { WRITE_OPERATION_FAILED_ERROR }, + throwable = exception + ) + + advancedRumMonitor?.let { + if (onError == NO_OP_EVENT_OUTCOME_ACTION) { + sdkCore.internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + { NO_ERROR_CALLBACK_PROVIDED_WARNING } + ) + } + onError(it) + } + } + + internal companion object { + const val WRITE_OPERATION_FAILED_ERROR = "Write operation failed." + const val NO_ERROR_CALLBACK_PROVIDED_WARNING = + "Write operation failed, but no onError callback was provided." + val NO_OP_EVENT_OUTCOME_ACTION: EventOutcomeAction = {} + } +} + +internal fun FeatureSdkCore.newRumEventWriteOperation( + rumDataWriter: DataWriter, + eventSource: (DatadogContext) -> Any +): WriteOperation { + return WriteOperation(this, rumDataWriter, eventSource) +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/RumDataWriterTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/RumDataWriterTest.kt index d697eed3df..e4d15234f1 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/RumDataWriterTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/RumDataWriterTest.kt @@ -11,8 +11,6 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.RawBatchEvent import com.datadog.android.core.persistence.Serializer import com.datadog.android.rum.internal.domain.event.RumEventMeta -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.LongTaskEvent @@ -37,11 +35,8 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow -import org.mockito.kotlin.eq -import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -235,120 +230,6 @@ internal class RumDataWriterTest { verifyNoInteractions(mockInternalLogger) } - @Test - fun `𝕄 notify the RumMonitor 𝕎 onDataWritten() { ActionEvent }`( - @Forgery actionEvent: ActionEvent - ) { - // When - testedWriter.onDataWritten(actionEvent, fakeSerializedData) - - // Then - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventSent( - actionEvent.view.id, - StorageEvent.Action(frustrationCount = actionEvent.action.frustration?.type?.size ?: 0) - ) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - - @Test - fun `𝕄 notify the RumMonitor 𝕎 onDataWritten() { ResourceEvent }`( - @Forgery resourceEvent: ResourceEvent - ) { - // When - testedWriter.onDataWritten(resourceEvent, fakeSerializedData) - - // Then - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventSent( - resourceEvent.view.id, - StorageEvent.Resource - ) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - - @Test - fun `𝕄 notify the RumMonitor 𝕎 onDataWritten() { ErrorEvent isCrash=false }`( - @Forgery fakeEvent: ErrorEvent - ) { - // Given - val errorEvent = fakeEvent.copy(error = fakeEvent.error.copy(isCrash = false)) - - // When - testedWriter.onDataWritten(errorEvent, fakeSerializedData) - - // Then - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventSent( - fakeEvent.view.id, - StorageEvent.Error - ) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - - @Test - fun `𝕄 not notify the RumMonitor 𝕎 onDataWritten() { ErrorEvent isCrash=true }`( - @Forgery fakeEvent: ErrorEvent - ) { - // Given - val errorEvent = fakeEvent.copy(error = fakeEvent.error.copy(isCrash = true)) - - // When - testedWriter.onDataWritten(errorEvent, fakeSerializedData) - - // Then - verify( - rumMonitor.mockInstance as AdvancedRumMonitor, - never() - ).eventSent(eq(fakeEvent.view.id), any()) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - - @Test - fun `𝕄 notify the RumMonitor 𝕎 onDataWritten() { LongTaskEvent }`( - @Forgery fakeEvent: LongTaskEvent - ) { - // Given - val longTaskEvent = fakeEvent.copy( - longTask = LongTaskEvent.LongTask( - id = fakeEvent.longTask.id, - duration = fakeEvent.longTask.duration, - isFrozenFrame = false - ) - ) - - // When - testedWriter.onDataWritten(longTaskEvent, fakeSerializedData) - - // Then - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventSent( - longTaskEvent.view.id, - StorageEvent.LongTask - ) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - - @Test - fun `𝕄 notify the RumMonitor 𝕎 onDataWritten() { FrozenFrame Event }`( - @Forgery fakeEvent: LongTaskEvent - ) { - // Given - val frozenFrameEvent = fakeEvent.copy( - longTask = LongTaskEvent.LongTask( - id = fakeEvent.longTask.id, - duration = fakeEvent.longTask.duration, - isFrozenFrame = true - ) - ) - - // When - testedWriter.onDataWritten(frozenFrameEvent, fakeSerializedData) - - // Then - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventSent( - frozenFrameEvent.view.id, - StorageEvent.FrozenFrame - ) - verifyNoInteractions(rumMonitor.mockSdkCore) - } - // endregion companion object { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapperTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapperTest.kt index 2171b5ff21..f80866bf40 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapperTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventMapperTest.kt @@ -8,23 +8,17 @@ package com.datadog.android.rum.internal.domain.event import com.datadog.android.api.InternalLogger import com.datadog.android.event.EventMapper -import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor -import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.LongTaskEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.model.ViewEvent -import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.utils.forge.Configurator import com.datadog.android.rum.utils.forge.aRumEvent import com.datadog.android.rum.utils.verifyLog import com.datadog.android.telemetry.model.TelemetryConfigurationEvent import com.datadog.android.telemetry.model.TelemetryDebugEvent import com.datadog.android.telemetry.model.TelemetryErrorEvent -import com.datadog.tools.unit.annotations.TestConfigurationsProvider -import com.datadog.tools.unit.extensions.TestConfigurationExtension -import com.datadog.tools.unit.extensions.config.TestConfiguration import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -38,7 +32,6 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -47,8 +40,7 @@ import java.util.Locale @Extensions( ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class), - ExtendWith(TestConfigurationExtension::class) + ExtendWith(ForgeExtension::class) ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(Configurator::class) @@ -82,7 +74,6 @@ internal class RumEventMapperTest { whenever(mockViewEventMapper.map(any())).thenAnswer { it.arguments[0] } testedRumEventMapper = RumEventMapper( - sdkCore = rumMonitor.mockSdkCore, actionEventMapper = mockActionEventMapper, viewEventMapper = mockViewEventMapper, resourceEventMapper = mockResourceEventMapper, @@ -167,7 +158,6 @@ internal class RumEventMapperTest { fun `M return the original event W map { no internal mapper used }`(forge: Forge) { // GIVEN testedRumEventMapper = RumEventMapper( - sdkCore = rumMonitor.mockSdkCore, internalLogger = mockInternalLogger ) val fakeRumEvent = forge.aRumEvent() @@ -184,7 +174,6 @@ internal class RumEventMapperTest { fun `M return the original event W map { bundled event unknown }`() { // GIVEN testedRumEventMapper = RumEventMapper( - sdkCore = rumMonitor.mockSdkCore, internalLogger = mockInternalLogger ) val fakeRumEvent = Any() @@ -570,116 +559,4 @@ internal class RumEventMapperTest { ) } - - @Test - fun `𝕄 warn the RUM Monitor 𝕎 map() {action dropped}`( - @Forgery fakeRumEvent: ActionEvent - ) { - // Given - whenever(mockActionEventMapper.map(fakeRumEvent)) doReturn null - - // WHEN - val mappedRumEvent = testedRumEventMapper.map(fakeRumEvent) - - // THEN - assertThat(mappedRumEvent).isNull() - verify(rumMonitor.mockInstance as AdvancedRumMonitor) - .eventDropped( - fakeRumEvent.view.id, - StorageEvent.Action( - frustrationCount = fakeRumEvent.action.frustration?.type?.size ?: 0 - ) - ) - } - - @Test - fun `𝕄 warn the RUM Monitor 𝕎 map() {resource dropped}`( - @Forgery fakeRumEvent: ResourceEvent - ) { - // Given - whenever(mockResourceEventMapper.map(fakeRumEvent)) doReturn null - - // WHEN - val mappedRumEvent = testedRumEventMapper.map(fakeRumEvent) - - // THEN - assertThat(mappedRumEvent).isNull() - verify(rumMonitor.mockInstance as AdvancedRumMonitor) - .eventDropped(fakeRumEvent.view.id, StorageEvent.Resource) - } - - @Test - fun `𝕄 warn the RUM Monitor 𝕎 map() {error dropped}`( - @Forgery fakeRumEvent: ErrorEvent - ) { - // Given - val fakeNoCrashEvent = fakeRumEvent.copy( - error = fakeRumEvent.error.copy(isCrash = false) - ) - whenever(mockErrorEventMapper.map(fakeNoCrashEvent)) doReturn null - - // WHEN - val mappedRumEvent = testedRumEventMapper.map(fakeNoCrashEvent) - - // THEN - assertThat(mappedRumEvent).isNull() - verify(rumMonitor.mockInstance as AdvancedRumMonitor) - .eventDropped(fakeNoCrashEvent.view.id, StorageEvent.Error) - } - - @Test - fun `𝕄 warn the RUM Monitor 𝕎 map() {longTask dropped}`( - @Forgery longTaskEvent: LongTaskEvent - ) { - // Given - val fakeRumEvent = longTaskEvent.copy( - longTask = LongTaskEvent.LongTask( - id = longTaskEvent.longTask.id, - duration = longTaskEvent.longTask.duration, - isFrozenFrame = false - ) - ) - - // WHEN - val mappedRumEvent = testedRumEventMapper.map(fakeRumEvent) - - // THEN - assertThat(mappedRumEvent).isNull() - verify(rumMonitor.mockInstance as AdvancedRumMonitor) - .eventDropped(longTaskEvent.view.id, StorageEvent.LongTask) - } - - @Test - fun `𝕄 warn the RUM Monitor 𝕎 map() {frozenFrame dropped}`( - @Forgery longTaskEvent: LongTaskEvent - ) { - // Given - val fakeRumEvent = longTaskEvent.copy( - longTask = LongTaskEvent.LongTask( - id = longTaskEvent.longTask.id, - duration = longTaskEvent.longTask.duration, - isFrozenFrame = true - ) - ) - - // WHEN - val mappedRumEvent = testedRumEventMapper.map(fakeRumEvent) - - // THEN - assertThat(mappedRumEvent).isNull() - verify(rumMonitor.mockInstance as AdvancedRumMonitor).eventDropped( - longTaskEvent.view.id, - StorageEvent.FrozenFrame - ) - } - - companion object { - val rumMonitor = GlobalRumMonitorTestConfiguration() - - @TestConfigurationsProvider - @JvmStatic - fun getTestConfigurations(): List { - return listOf(rumMonitor) - } - } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt index 2f3b5cfb55..9fbe2402f5 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt @@ -21,6 +21,8 @@ import com.datadog.android.rum.assertj.ActionEventAssert.Companion.assertThat import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time +import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.utils.forge.Configurator @@ -28,6 +30,7 @@ import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import com.datadog.tools.unit.forge.aFilteredMap +import com.datadog.tools.unit.forge.anException import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery @@ -48,7 +51,9 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq +import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -161,6 +166,7 @@ internal class RumActionScopeTest { fakeParentContext.viewId.orEmpty() ) ).thenReturn(fakeHasReplay) + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn true testedScope = RumActionScope( mockParentScope, @@ -2566,6 +2572,46 @@ internal class RumActionScopeTest { assertThat(result2).isNull() } + @Test + fun `𝕄 notify about success 𝕎 handleEvent() { write succeeded }`() { + // When + testedScope.type = RumActionType.CUSTOM + val event = RumRawEvent.SendCustomActionNow() + testedScope.handleEvent(event, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(fakeParentContext.viewId.orEmpty(), StorageEvent.Action(0)) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { write failed }`() { + // When + testedScope.type = RumActionType.CUSTOM + val event = RumRawEvent.SendCustomActionNow() + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + testedScope.handleEvent(event, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Action(0)) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { write throws }`( + forge: Forge + ) { + // When + testedScope.type = RumActionType.CUSTOM + val event = RumRawEvent.SendCustomActionNow() + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doThrow forge.anException() + testedScope.handleEvent(event, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Action(0)) + } + // region Internal private fun mockEvent(): RumRawEvent { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt index 528dab10db..607c1cadeb 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt @@ -146,6 +146,7 @@ internal class RumContinuousActionScopeTest { val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) callback.invoke(fakeDatadogContext, mockEventBatchWriter) } + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn true testedScope = RumActionScope( mockParentScope, diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt index 7bf859b073..9e43c8471a 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt @@ -25,6 +25,8 @@ import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.event.ResourceTiming +import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.model.ErrorEvent import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.utils.asTimingsPayload @@ -35,6 +37,7 @@ import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import com.datadog.tools.unit.forge.aFilteredMap +import com.datadog.tools.unit.forge.anException import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery @@ -59,7 +62,9 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atMost import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq +import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -187,6 +192,7 @@ internal class RumResourceScopeTest { val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) callback.invoke(fakeDatadogContext, mockEventBatchWriter) } + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn true testedScope = RumResourceScope( mockParentScope, @@ -2655,6 +2661,162 @@ internal class RumResourceScopeTest { } } + // region write notification + + @Test + fun `𝕄 notify about success 𝕎 handleEvent() { resource write succeeded }`( + @Forgery kind: RumResourceKind, + @LongForgery(200, 600) statusCode: Long, + @LongForgery(0, 1024) size: Long, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + + // When + Thread.sleep(RESOURCE_DURATION_MS) + mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(fakeParentContext.viewId.orEmpty(), StorageEvent.Resource) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { resource write failed }`( + @Forgery kind: RumResourceKind, + @LongForgery(200, 600) statusCode: Long, + @LongForgery(0, 1024) size: Long, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + Thread.sleep(RESOURCE_DURATION_MS) + mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Resource) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { resource write throws }`( + @Forgery kind: RumResourceKind, + @LongForgery(200, 600) statusCode: Long, + @LongForgery(0, 1024) size: Long, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + Thread.sleep(RESOURCE_DURATION_MS) + mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Resource) + } + + @Test + fun `𝕄 notify about success 𝕎 handleEvent() { error write succeeded }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + + mockEvent = RumRawEvent.StopResourceWithError( + fakeKey, + null, + message, + source, + throwable, + attributes + ) + + // When + Thread.sleep(RESOURCE_DURATION_MS) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(fakeParentContext.viewId.orEmpty(), StorageEvent.Error) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { error write failed }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + mockEvent = RumRawEvent.StopResourceWithError( + fakeKey, + null, + message, + source, + throwable, + attributes + ) + + // When + Thread.sleep(RESOURCE_DURATION_MS) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Error) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent() { error write throws }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + forge: Forge + ) { + // Given + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + mockEvent = RumRawEvent.StopResourceWithError( + fakeKey, + null, + message, + source, + throwable, + attributes + ) + + // When + Thread.sleep(RESOURCE_DURATION_MS) + testedScope.handleEvent(mockEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(fakeParentContext.viewId.orEmpty(), StorageEvent.Error) + } + + // endregion + // region Internal private fun mockEvent(): RumRawEvent { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt index ae76ae270a..5ab2fcb041 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt @@ -30,6 +30,8 @@ import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time +import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor +import com.datadog.android.rum.internal.monitor.StorageEvent import com.datadog.android.rum.internal.vitals.VitalInfo import com.datadog.android.rum.internal.vitals.VitalListener import com.datadog.android.rum.internal.vitals.VitalMonitor @@ -45,6 +47,7 @@ import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import com.datadog.tools.unit.forge.aFilteredMap +import com.datadog.tools.unit.forge.anException import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery @@ -73,7 +76,9 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq +import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -252,6 +257,7 @@ internal class RumViewScopeTest { val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) callback.invoke(fakeDatadogContext, mockEventBatchWriter) } + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn true fakeReplayStats = ViewEvent.ReplayStats(recordsCount = fakeReplayRecordsCount) testedScope = RumViewScope( mockParentScope, @@ -7449,6 +7455,357 @@ internal class RumViewScopeTest { // endregion + // region write notification + + @Test + fun `𝕄 notify about success 𝕎 handleEvent(AddError+non-fatal) { write succeeded }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + false, + attributes + ) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddError+non-fatal) { write failed }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + false, + attributes + ) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddError+non-fatal) { write throws }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + false, + attributes + ) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 not notify about success 𝕎 handleEvent(AddError+fatal) { write succeeded }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + true, + attributes + ) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor, never()) + .eventSent(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 not notify about error 𝕎 handleEvent(AddError+fatal) { write failed }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + true, + attributes + ) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor, never()) + .eventDropped(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 not notify about error 𝕎 handleEvent(AddError+fatal) { write throws }`( + @StringForgery message: String, + @Forgery source: RumErrorSource, + @Forgery throwable: Throwable, + @StringForgery stacktrace: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + val attributes = forge.exhaustiveAttributes(excludedKeys = fakeAttributes.keys) + fakeEvent = RumRawEvent.AddError( + message, + source, + throwable, + stacktrace, + true, + attributes + ) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor, never()) + .eventDropped(testedScope.viewId, StorageEvent.Error) + } + + @Test + fun `𝕄 notify about success 𝕎 handleEvent(ApplicationStarted) { write succeeded }`( + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.ApplicationStarted( + eventTime = Time(), + applicationStartupNanos = forge.aPositiveLong() + ) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(testedScope.viewId, StorageEvent.Action(0)) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(ApplicationStarted) { write failed }`( + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.ApplicationStarted( + eventTime = Time(), + applicationStartupNanos = forge.aPositiveLong() + ) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.Action(0)) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(ApplicationStarted) { write throws }`( + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.ApplicationStarted( + eventTime = Time(), + applicationStartupNanos = forge.aPositiveLong() + ) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.Action(0)) + } + + @Test + fun `𝕄 notify about success 𝕎 handleEvent(AddLongTask) { write succeeded }`( + @LongForgery(250_000_000L, 700_000_000L) durationNs: Long, + @StringForgery target: String + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(testedScope.viewId, StorageEvent.LongTask) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddLongTask) { write failed }`( + @LongForgery(250_000_000L, 700_000_000L) durationNs: Long, + @StringForgery target: String + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.LongTask) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddLongTask) { write throws }`( + @LongForgery(250_000_000L, 700_000_000L) durationNs: Long, + @StringForgery target: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.LongTask) + } + + @Test + fun `𝕄 notify about success 𝕎 handleEvent(AddLongTask, is frozen frame) { write succeeded }`( + @LongForgery(700_000_000L, 10_000_000_000L) durationNs: Long, + @StringForgery target: String + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventSent(testedScope.viewId, StorageEvent.FrozenFrame) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddLongTask, is frozen frame) { write failed }`( + @LongForgery(700_000_000L, 10_000_000_000L) durationNs: Long, + @StringForgery target: String + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + whenever(mockWriter.write(eq(mockEventBatchWriter), isA())) doReturn false + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.FrozenFrame) + } + + @Test + fun `𝕄 notify about error 𝕎 handleEvent(AddLongTask, is frozen frame) { write throws }`( + @LongForgery(700_000_000L, 10_000_000_000L) durationNs: Long, + @StringForgery target: String, + forge: Forge + ) { + // Given + testedScope.activeActionScope = mockActionScope + fakeEvent = RumRawEvent.AddLongTask(durationNs, target) + whenever( + mockWriter.write(eq(mockEventBatchWriter), isA()) + ) doThrow forge.anException() + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verify(rumMonitor.mockInstance as AdvancedRumMonitor) + .eventDropped(testedScope.viewId, StorageEvent.FrozenFrame) + } + // region Misc @ParameterizedTest diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/SdkCoreExtTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/SdkCoreExtTest.kt new file mode 100644 index 0000000000..16fc2fc772 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/SdkCoreExtTest.kt @@ -0,0 +1,230 @@ +package com.datadog.android.rum.utils + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.storage.DataWriter +import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration +import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.tools.unit.annotations.TestConfigurationsProvider +import com.datadog.tools.unit.extensions.TestConfigurationExtension +import com.datadog.tools.unit.extensions.config.TestConfiguration +import com.datadog.tools.unit.forge.anException +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class SdkCoreExtTest { + + @Forgery + lateinit var fakeDatadogContext: DatadogContext + + @Mock + lateinit var mockRumFeatureScope: FeatureScope + + @Mock + lateinit var mockEventBatchWriter: EventBatchWriter + + @Mock + lateinit var mockWriter: DataWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + private val mockSdkCore + get() = rumMonitor.mockSdkCore + + @BeforeEach + fun `set up`() { + whenever(mockRumFeatureScope.withWriteContext(any(), any())) doAnswer { + val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1) + callback.invoke(fakeDatadogContext, mockEventBatchWriter) + } + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn true + } + + @Test + fun `𝕄 write data 𝕎 submit()`() { + // Given + val fakeEvent = Any() + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + fakeEvent + } + .submit() + + // Then + verify(mockWriter).write(mockEventBatchWriter, fakeEvent) + verifyNoInteractions(mockInternalLogger) + } + + @Test + fun `𝕄 call onSuccess 𝕎 submit() { write succeeded } `() { + // Given + val fakeEvent = Any() + var invoked = false + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + fakeEvent + } + .onSuccess { + invoked = true + } + .submit() + + // Then + verifyNoInteractions(mockInternalLogger) + assertThat(invoked) + .overridingErrorMessage("Expected to invoke onSuccess callback, but it wasn't.") + .isTrue + } + + @Test + fun `𝕄 call onError 𝕎 submit() { write was not successful }`() { + // Given + val fakeEvent = Any() + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doReturn false + var invoked = false + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + fakeEvent + } + .onError { + invoked = true + } + .submit() + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER), + message = WriteOperation.WRITE_OPERATION_FAILED_ERROR + ) + assertThat(invoked) + .overridingErrorMessage("Expected to invoke onError callback, but it wasn't.") + .isTrue + } + + @Test + fun `𝕄 call onError 𝕎 submit() { write throws }`( + forge: Forge + ) { + // Given + val fakeEvent = Any() + val fakeException = forge.anException() + whenever(mockWriter.write(eq(mockEventBatchWriter), any())) doThrow fakeException + var invoked = false + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + fakeEvent + } + .onError { + invoked = true + } + .submit() + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = WriteOperation.WRITE_OPERATION_FAILED_ERROR, + throwable = fakeException + ) + assertThat(invoked) + .overridingErrorMessage("Expected to invoke onError callback, but it wasn't.") + .isTrue + } + + @Test + fun `𝕄 call onError 𝕎 submit() { event creation throws }`( + forge: Forge + ) { + // Given + val fakeException = forge.anException() + var invoked = false + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + throw fakeException + } + .onError { + invoked = true + } + .submit() + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + targets = listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + message = WriteOperation.WRITE_OPERATION_FAILED_ERROR, + throwable = fakeException + ) + assertThat(invoked) + .overridingErrorMessage("Expected to invoke onError callback, but it wasn't.") + .isTrue + } + + @Test + fun `𝕄 notify no onError provided 𝕎 submit() { write failed }`( + forge: Forge + ) { + // Given + val fakeException = forge.anException() + + // When + mockSdkCore.newRumEventWriteOperation(mockWriter) { + throw fakeException + } + .submit() + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + message = WriteOperation.NO_ERROR_CALLBACK_PROVIDED_WARNING + ) + } + + companion object { + val rumMonitor = GlobalRumMonitorTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(rumMonitor) + } + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/RumEventMapperFactory.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/RumEventMapperFactory.kt index 4e1564a81b..9d84d59aeb 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/RumEventMapperFactory.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/forge/RumEventMapperFactory.kt @@ -15,7 +15,6 @@ internal class RumEventMapperFactory : ForgeryFactory { override fun getForgery(forge: Forge): RumEventMapper { return RumEventMapper( - sdkCore = mock(), viewEventMapper = mock(), actionEventMapper = mock(), resourceEventMapper = mock(),