Skip to content

Commit

Permalink
RM-1844: Add Method Call Telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Mar 26, 2024
1 parent 1bd46ae commit 2eccdbe
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {

this.viewOnDrawInterceptor = ViewOnDrawInterceptor(
recordedDataQueueHandler = recordedDataQueueHandler,
logger = internalLogger,
SnapshotProducer(
DefaultImageWireframeHelper(
logger = internalLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ package com.datadog.android.sessionreplay.internal.recorder

import android.view.View
import android.view.ViewTreeObserver.OnDrawListener
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.recorder.listener.WindowsOnDrawListener
import java.util.WeakHashMap

internal class ViewOnDrawInterceptor(
private val recordedDataQueueHandler: RecordedDataQueueHandler,
private val logger: InternalLogger,
private val snapshotProducer: SnapshotProducer,
private val onDrawListenerProducer: (List<View>) -> OnDrawListener =
{ decorViews ->
WindowsOnDrawListener(
decorViews,
recordedDataQueueHandler,
snapshotProducer
snapshotProducer,
logger = logger
)
}
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import android.content.Context
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.MainThread
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.Debouncer
import com.datadog.android.sessionreplay.internal.recorder.SnapshotProducer
import com.datadog.android.sessionreplay.internal.recorder.telemetry.MethodCalledTelemetry.Companion.METHOD_CALL_OPERATION_NAME
import com.datadog.android.sessionreplay.internal.recorder.telemetry.TelemetryWrapper
import com.datadog.android.sessionreplay.internal.utils.MiscUtils
import java.lang.ref.WeakReference

Expand All @@ -22,7 +25,12 @@ internal class WindowsOnDrawListener(
private val recordedDataQueueHandler: RecordedDataQueueHandler,
private val snapshotProducer: SnapshotProducer,
private val debouncer: Debouncer = Debouncer(),
private val miscUtils: MiscUtils = MiscUtils
private val miscUtils: MiscUtils = MiscUtils,
private val logger: InternalLogger,
private var telemetryWrapper: TelemetryWrapper = TelemetryWrapper(
samplingRate = 5f,
logger = logger
)
) : ViewTreeObserver.OnDrawListener {

internal val weakReferencedDecorViews: List<WeakReference<View>>
Expand Down Expand Up @@ -58,11 +66,18 @@ internal class WindowsOnDrawListener(
RecordedDataQueueRefs(recordedDataQueueHandler)
recordedDataQueueRefs.recordedDataQueueItem = item

val methodCallTelemetry = telemetryWrapper.startMethodCalled(
callerClass = this.javaClass.name,
operationName = METHOD_CALL_OPERATION_NAME
)

val nodes = views
.mapNotNull {
snapshotProducer.produce(it, systemInformation, recordedDataQueueRefs)
}

methodCallTelemetry?.stopMethodCalled(isSuccessful = nodes.isNotEmpty())

if (nodes.isNotEmpty()) {
item.nodes = nodes
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.sessionreplay.internal.recorder.telemetry

import com.datadog.android.Datadog
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.InternalSdkCore

internal class MethodCalledTelemetry(
private val operationName: String,
private val callerClass: String,
private val logger: InternalLogger,
private val startTime: Long = System.nanoTime(),
private val internalSdkCore: InternalSdkCore? = Datadog.getInstance() as? InternalSdkCore
) {
internal fun stopMethodCalled(isSuccessful: Boolean) {
val executionTime = System.nanoTime() - startTime
val additionalProperties: MutableMap<String, Any> = mutableMapOf()

val deviceInfo = internalSdkCore?.getDatadogContext()?.deviceInfo

additionalProperties[EXECUTION_TIME] = executionTime
additionalProperties[OPERATION_NAME] = operationName
additionalProperties[CALLER_CLASS] = callerClass
additionalProperties[IS_SUCCESSFUL] = isSuccessful
additionalProperties[METRIC_TYPE] = METRIC_TYPE_VALUE
val deviceMap = mutableMapOf<String, Any>()
val osMap = mutableMapOf<String, Any>()

deviceInfo?.deviceModel?.let {
deviceMap[DEVICE_MODEL] = it
}

deviceInfo?.deviceBrand?.let {
deviceMap[DEVICE_BRAND] = it
}

deviceInfo?.architecture?.let {
deviceMap[DEVICE_ARCHITECTURE] = it
}

deviceInfo?.osName?.let {
osMap[OS_NAME] = it
}

deviceInfo?.osVersion?.let {
osMap[OS_VERSION] = it
}

deviceInfo?.deviceBuildId?.let {
osMap[OS_BUILD] = it
}

additionalProperties[DEVICE_KEY] = deviceMap
additionalProperties[OS_KEY] = osMap

logger.logMetric(
messageBuilder = { METRIC_NAME },
additionalProperties = additionalProperties
)
}

internal companion object {
// Basic Metric type key.
internal const val METRIC_TYPE = "metric_type"

// Title of the metric to be sent
internal const val METRIC_NAME = "[Mobile Metric] Method Called"

// Metric type value.
internal const val METRIC_TYPE_VALUE = "method called"

// The key for operation name.
internal const val OPERATION_NAME = "operation_name"

// The key for caller class.
internal const val CALLER_CLASS = "caller_class"

// The key for is successful.
internal const val IS_SUCCESSFUL = "is_successful"

// The key for execution time.
internal const val EXECUTION_TIME = "execution_time"

// The key for device object.
internal const val DEVICE_KEY = "device"

// The key for device model name.
internal const val DEVICE_MODEL = "model"

// The key for device brand.
internal const val DEVICE_BRAND = "brand"

// The key for CPU architecture.
internal const val DEVICE_ARCHITECTURE = "architecture"

// The key for operating system object.
internal const val OS_KEY = "os"

// The key for OS name.
internal const val OS_NAME = "name"

// The key for OS version.
internal const val OS_VERSION = "version"

// The key for OS build.
internal const val OS_BUILD = "build"

// The value for operation name
internal const val METHOD_CALL_OPERATION_NAME = "Capture Record"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.sessionreplay.internal.recorder.telemetry

import com.datadog.android.api.InternalLogger
import com.datadog.android.core.sampling.RateBasedSampler
import com.datadog.android.core.sampling.Sampler

internal class TelemetryWrapper(

// The sampling rate of the method call. Value between `0.0` and `100.0`,
// where `0.0` means NO event will be processed and `100.0` means ALL events will be processed.
// Note that this value is multiplicated by telemetry sampling (by default 20%) and
// metric events sampling (hardcoded to 15%). Making it effectively 3% sampling rate
// for sending events, when this value is set to `100`.
private val samplingRate: Float = 100.0f,

private val logger: InternalLogger,
private val sampler: Sampler = RateBasedSampler(samplingRate)
) {
internal fun startMethodCalled(
// Platform agnostic name of the operation.
operationName: String,

// The name of the class that calls the method.
callerClass: String
): MethodCalledTelemetry? {
return if (sampler.sample()) {
MethodCalledTelemetry(
operationName,
callerClass,
logger
)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package com.datadog.android.sessionreplay.internal.recorder

import android.view.View
import android.view.ViewTreeObserver
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.recorder.listener.WindowsOnDrawListener
Expand Down Expand Up @@ -37,22 +38,26 @@ import org.mockito.quality.Strictness
@ForgeConfiguration(ForgeConfigurator::class)
internal class ViewOnDrawInterceptorTest {

lateinit var testedInterceptor: ViewOnDrawInterceptor
private lateinit var testedInterceptor: ViewOnDrawInterceptor

@Mock
lateinit var mockRecordedDataQueueHandler: RecordedDataQueueHandler

@Mock
lateinit var mockSnapshotProducer: SnapshotProducer

lateinit var fakeDecorViews: List<View>
@Mock
lateinit var mockLogger: InternalLogger

private lateinit var fakeDecorViews: List<View>

@BeforeEach
fun `set up`(forge: Forge) {
fakeDecorViews = forge.aMockedDecorViewsList()
testedInterceptor = ViewOnDrawInterceptor(
mockRecordedDataQueueHandler,
mockSnapshotProducer
recordedDataQueueHandler = mockRecordedDataQueueHandler,
snapshotProducer = mockSnapshotProducer,
logger = mockLogger
)
}

Expand All @@ -74,8 +79,9 @@ internal class ViewOnDrawInterceptorTest {
// Given
val mockOnDrawListener = mock<ViewTreeObserver.OnDrawListener>()
testedInterceptor = ViewOnDrawInterceptor(
mockRecordedDataQueueHandler,
mockSnapshotProducer
recordedDataQueueHandler = mockRecordedDataQueueHandler,
snapshotProducer = mockSnapshotProducer,
logger = mockLogger
) { _ -> mockOnDrawListener }

// When
Expand Down
Loading

0 comments on commit 2eccdbe

Please sign in to comment.