Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ data class DdSdkConfiguration(
* @param initialResourceThreshold The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS).
* @param telemetrySampleRate The sample rate (between 0 and 100) of telemetry events.
* @param customEndpoint Custom RUM intake endpoint used to override the default Datadog intake.
* @param timeseries Experimental: configuration for memory and CPU timeseries collection.
*/
data class RumConfiguration(
val applicationId: String,
Expand All @@ -82,7 +83,22 @@ data class RumConfiguration(
val trackNonFatalAnrs: Boolean? = null,
val initialResourceThreshold: Double? = null,
val telemetrySampleRate: Double? = null,
val customEndpoint: String? = null
val customEndpoint: String? = null,
val timeseries: TimeseriesConfiguration? = null
)

/**
* Experimental: configuration for memory and CPU timeseries collection.
* Requires a native SDK build with timeseries support.
*
* @param enabled Enables collection of memory and CPU timeseries events.
* @param bufferSize The number of samples collected before a timeseries batch is flushed.
* @param intervalMs The sampling interval in milliseconds.
*/
data class TimeseriesConfiguration(
val enabled: Boolean,
val bufferSize: Double? = null,
val intervalMs: Double? = null
)

/**
Expand Down Expand Up @@ -157,7 +173,14 @@ internal data class JSONRumConfiguration(
val vitalsUpdateFrequency: String? = null,
val trackMemoryWarnings: Boolean? = null,
val telemetrySampleRate: Double? = null,
val customEndpoint: String? = null
val customEndpoint: String? = null,
val timeseries: JSONTimeseriesConfiguration? = null
)

internal data class JSONTimeseriesConfiguration(
val enabled: Boolean? = null,
val bufferSize: Double? = null,
val intervalMs: Double? = null
)

internal data class JSONLogsConfiguration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration {
trackNonFatalAnrs = rm.getBooleanOrNull("trackNonFatalAnrs"),
initialResourceThreshold = rm.getDoubleOrNull("initialResourceThreshold"),
telemetrySampleRate = rm.getDoubleOrNull("telemetrySampleRate"),
customEndpoint = rm.getString("customEndpoint")
customEndpoint = rm.getString("customEndpoint"),
timeseries = rm.getMap("timeseries")?.asTimeseriesConfiguration()
)
}

Expand Down Expand Up @@ -86,6 +87,14 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration {
}


internal fun ReadableMap.asTimeseriesConfiguration(): TimeseriesConfiguration {
return TimeseriesConfiguration(
enabled = getBooleanOrNull("enabled") ?: false,
bufferSize = getDoubleOrNull("bufferSize"),
intervalMs = getDoubleOrNull("intervalMs")
)
}

internal fun ReadableMap.asConfigurationForTelemetry(): ConfigurationForTelemetry {
return ConfigurationForTelemetry(
initializationType = getString("initializationType"),
Expand Down Expand Up @@ -187,7 +196,8 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration {
initialResourceThreshold = rum.initialResourceThreshold ?: DefaultConfiguration.initialResourceThreshold,
telemetrySampleRate = rum.telemetrySampleRate?.toDouble()
?: DefaultConfiguration.telemetrySampleRate,
customEndpoint = rum.customEndpoint
customEndpoint = rum.customEndpoint,
timeseries = rum.timeseries?.asTimeseriesConfiguration()
)
}

Expand Down Expand Up @@ -229,6 +239,14 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration {
}


internal fun JSONTimeseriesConfiguration.asTimeseriesConfiguration(): TimeseriesConfiguration {
return TimeseriesConfiguration(
enabled = this.enabled ?: false,
bufferSize = this.bufferSize,
intervalMs = this.intervalMs
)
}

internal fun JSONProxyConfiguration.asProxyConfig(): Pair<Proxy, ProxyAuthenticator?>? {
return buildProxyConfig(type, address, port, username, password)
}
Expand Down Expand Up @@ -331,6 +349,13 @@ internal fun DdSdkConfiguration.toReadableMap(): ReadableMap {
rum.initialResourceThreshold?.let { rumMap.putDouble("initialResourceThreshold", it) }
rum.telemetrySampleRate?.let { rumMap.putDouble("telemetrySampleRate", it) }
rum.customEndpoint?.let { rumMap.putString("customEndpoint", it) }
rum.timeseries?.let { timeseries ->
val timeseriesMap = WritableNativeMap()
timeseriesMap.putBoolean("enabled", timeseries.enabled)
timeseries.bufferSize?.let { timeseriesMap.putDouble("bufferSize", it) }
timeseries.intervalMs?.let { timeseriesMap.putDouble("intervalMs", it) }
rumMap.putMap("timeseries", timeseriesMap)
}

map.putMap("rumConfiguration", rumMap)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.datadog.android.event.EventMapper
import com.datadog.android.log.Logs
import com.datadog.android.log.LogsConfiguration
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.ExperimentalRumApi
import com.datadog.android.rum.Rum
import com.datadog.android.rum.RumConfiguration
import com.datadog.android._InternalProxy
Expand All @@ -27,6 +28,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency
import com.datadog.android.rum.metric.networksettled.TimeBasedInitialResourceIdentifier
import com.datadog.android.rum.model.ActionEvent
import com.datadog.android.rum.model.ResourceEvent
import com.datadog.android.rum.timeseries.TimeseriesConfiguration as NativeTimeseriesConfiguration
import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
import com.datadog.android.telemetry.model.TelemetryConfigurationEvent
import com.datadog.android.trace.Trace
Expand Down Expand Up @@ -254,11 +256,31 @@ class DdSdkNativeInitialization internal constructor(
configBuilder.setInitialResourceIdentifier(TimeBasedInitialResourceIdentifier(milliseconds))
}

configuration.rumConfiguration?.timeseries?.let { timeseries ->
setTimeseriesConfiguration(configBuilder, timeseries)
}

configBuilder.setSessionListener(DdSdkSessionStartedListener.getInstance())

return configBuilder.build()
}

@OptIn(ExperimentalRumApi::class)
private fun setTimeseriesConfiguration(
configBuilder: RumConfiguration.Builder,
timeseries: TimeseriesConfiguration
) {
if (!timeseries.enabled) {
return
}

val nativeConfigBuilder = NativeTimeseriesConfiguration.Builder()
timeseries.bufferSize?.let { nativeConfigBuilder.setBufferSize(it.toInt()) }
timeseries.intervalMs?.let { nativeConfigBuilder.setIntervalMs(it.toLong()) }

configBuilder.setTimeseriesConfiguration(nativeConfigBuilder.build())
}

private fun buildLogsConfiguration(configuration: DdSdkConfiguration): LogsConfiguration {
val configBuilder = LogsConfiguration.Builder()
configuration.logsConfiguration?.customEndpoint?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ internal class DdSdkNativeInitializationTest {
"[B3MULTI, TRACECONTEXT]"
)
assertThat(configuration.rumConfiguration?.initialResourceThreshold).isEqualTo(0.5)
assertThat(configuration.rumConfiguration?.timeseries?.enabled).isEqualTo(true)
assertThat(configuration.rumConfiguration?.timeseries?.bufferSize).isEqualTo(15.0)
}

@Test
Expand Down Expand Up @@ -192,6 +194,7 @@ internal class DdSdkNativeInitializationTest {
assertThat(configuration.rumConfiguration?.firstPartyHosts).isNull()
assertThat(configuration.logsConfiguration?.customEndpoint).isNull()
assertThat(configuration.traceConfiguration?.customEndpoint).isNull()
assertThat(configuration.rumConfiguration?.timeseries).isNull()
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2368,6 +2368,106 @@ internal class DdSdkTest {
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {timeseries enabled}`(
@Forgery configuration: DdSdkConfiguration
) {
// Given
val rumConfiguration = configuration.rumConfiguration?.copy(
timeseries = TimeseriesConfiguration(enabled = true, bufferSize = 15.0)
)
val bridgeConfiguration = configuration.copy(
rumConfiguration = rumConfiguration
)
val sdkConfigCaptor = argumentCaptor<Configuration>()
val rumConfigCaptor = argumentCaptor<RumConfiguration>()
val logsConfigCaptor = argumentCaptor<LogsConfiguration>()
val traceConfigCaptor = argumentCaptor<TraceConfiguration>()

val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java)
val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java)
val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java)

try {
rumMock.`when`<Unit> { Rum.enable(any(), any()) }.then { }
logsMock.`when`<Unit> { Logs.enable(any(), any()) }.then { }
traceMock.`when`<Unit> { Trace.enable(any(), any()) }.then { }

// When
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
inOrder(mockDatadog) {
verify(mockDatadog).initialize(
same(mockContext),
sdkConfigCaptor.capture(),
any()
)
rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) }
traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) }
logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) }
}
assertThat(rumConfigCaptor.firstValue)
.hasField("featureConfiguration") {
it.hasField("timeseriesConfiguration") { ts ->
ts.hasFieldEqualTo("bufferSize", 15)
}
}
} finally {
rumMock.close()
logsMock.close()
traceMock.close()
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {timeseries disabled by default}`(
@Forgery configuration: DdSdkConfiguration
) {
// Given
val rumConfiguration = configuration.rumConfiguration?.copy(timeseries = null)
val bridgeConfiguration = configuration.copy(
rumConfiguration = rumConfiguration
)
val sdkConfigCaptor = argumentCaptor<Configuration>()
val rumConfigCaptor = argumentCaptor<RumConfiguration>()
val logsConfigCaptor = argumentCaptor<LogsConfiguration>()
val traceConfigCaptor = argumentCaptor<TraceConfiguration>()

val rumMock = org.mockito.Mockito.mockStatic(Rum::class.java)
val traceMock = org.mockito.Mockito.mockStatic(Trace::class.java)
val logsMock = org.mockito.Mockito.mockStatic(Logs::class.java)

try {
rumMock.`when`<Unit> { Rum.enable(any(), any()) }.then { }
logsMock.`when`<Unit> { Logs.enable(any(), any()) }.then { }
traceMock.`when`<Unit> { Trace.enable(any(), any()) }.then { }

// When
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
inOrder(mockDatadog) {
verify(mockDatadog).initialize(
same(mockContext),
sdkConfigCaptor.capture(),
any()
)
rumMock.verify { Rum.enable(rumConfigCaptor.capture(), any()) }
traceMock.verify { Trace.enable(traceConfigCaptor.capture(), any()) }
logsMock.verify { Logs.enable(logsConfigCaptor.capture(), any()) }
}
assertThat(rumConfigCaptor.firstValue)
.hasField("featureConfiguration") {
it.hasFieldEqualTo("timeseriesConfiguration", null)
}
} finally {
rumMock.close()
logsMock.close()
traceMock.close()
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {never vitals frequency update}`(
@Forgery configuration: DdSdkConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency
import com.datadog.reactnative.ConfigurationForTelemetry
import com.datadog.reactnative.DdSdkConfiguration
import com.datadog.reactnative.ProxyAuthenticator
import com.datadog.reactnative.TimeseriesConfiguration
import com.facebook.react.bridge.ReadableMap
import java.net.Proxy

Expand Down Expand Up @@ -67,6 +68,7 @@ fun DdSdkConfiguration.toReadableJavaOnlyMap(): ReadableMap {
rumMap["initialResourceThreshold"] = rum?.initialResourceThreshold ?: 0.1
rumMap["telemetrySampleRate"] = rum?.telemetrySampleRate ?: 20.0
rum?.customEndpoint?.let { rumMap["customEndpoint"] = it }
rum?.timeseries?.let { rumMap["timeseries"] = it.toReadableMap() }
map["rumConfiguration"] = rumMap.toReadableMap()
}

Expand All @@ -93,6 +95,14 @@ fun DdSdkConfiguration.toReadableJavaOnlyMap(): ReadableMap {
return map.toReadableMap()
}

internal fun TimeseriesConfiguration.toReadableMap(): ReadableMap {
val map = mutableMapOf<String, Any?>()
map["enabled"] = enabled
bufferSize?.let { map["bufferSize"] = it }
intervalMs?.let { map["intervalMs"] = it }
return map.toReadableMap()
}

internal fun ConfigurationForTelemetry.toReadableJavaOnlyMap(): ReadableMap {
val map = mutableMapOf<String, Any?>()
initializationType?.let { map.put("initializationType", it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"resourceTraceSampleRate": 33,
"trackBackgroundEvents": true,
"trackFrustrations": false,
"timeseries": {
"enabled": true,
"bufferSize": 15
},
"firstPartyHosts": [
{
"match": "example.com",
Expand Down
21 changes: 21 additions & 0 deletions packages/core/datadog-configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,27 @@
"customEndpoint": {
"description": "Target a custom server for RUM.",
"type": "string"
},
"timeseries": {
"description": "Experimental: configuration for memory and CPU timeseries collection. Requires a native SDK build with timeseries support.",
"type": "object",
"properties": {
"enabled": {
"description": "Enables collection of memory and CPU timeseries events.",
"type": "boolean"
},
"bufferSize": {
"description": "The number of samples collected before a timeseries batch is flushed. iOS and Android only.",
"type": "integer"
},
"intervalMs": {
"description": "The sampling interval in milliseconds. Android only.",
"type": "integer"
}
},
"required": [
"enabled"
]
}
},
"required": [
Expand Down
11 changes: 10 additions & 1 deletion packages/core/ios/Sources/DdSdkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ public class DdSdkConfiguration: NSObject {
/// - trackMemoryWarnings: Whether memory warning events should be tracked.
/// - telemetrySampleRate: Sampling rate (0–100) for internal telemetry emitted via RUM.
/// - customEndpoint: A custom RUM intake endpoint to override the default Datadog intake.
/// - enableTimeseries: Experimental: enables collection of memory and CPU timeseries events.
/// Requires a native SDK build with timeseries support.
/// - timeseriesBatchSize: The number of samples collected before a timeseries batch is flushed.
public class RumConfiguration: NSObject {
public var applicationId: String = ""
public var trackFrustrations: Bool? = true
Expand All @@ -122,6 +125,8 @@ public class RumConfiguration: NSObject {
public var trackMemoryWarnings: Bool? = nil
public var telemetrySampleRate: Double? = nil
public var customEndpoint: String? = nil
public var enableTimeseries: Bool? = nil
public var timeseriesBatchSize: Double? = nil

init(
applicationId: String,
Expand All @@ -141,7 +146,9 @@ public class RumConfiguration: NSObject {
initialResourceThreshold: Double?,
trackMemoryWarnings: Bool?,
telemetrySampleRate: Double?,
customEndpoint: String?
customEndpoint: String?,
enableTimeseries: Bool? = nil,
timeseriesBatchSize: Double? = nil
) {
self.applicationId = applicationId
self.trackFrustrations = trackFrustrations
Expand All @@ -161,6 +168,8 @@ public class RumConfiguration: NSObject {
self.trackMemoryWarnings = trackMemoryWarnings
self.telemetrySampleRate = telemetrySampleRate
self.customEndpoint = customEndpoint
self.enableTimeseries = enableTimeseries
self.timeseriesBatchSize = timeseriesBatchSize
}

}
Expand Down
Loading