From 584da00b95cb9257f852584ed97cfa03251fa5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Tue, 22 Sep 2026 15:48:44 +0200 Subject: [PATCH 1/3] RUM-17173 Add timeseries configuration support for React Native --- example/ios/Podfile.lock | 104 +++++++++--------- .../datadog/reactnative/DdSdkConfiguration.kt | 23 +++- .../reactnative/DdSdkConfigurationExt.kt | 27 ++++- .../reactnative/DdSdkNativeInitialization.kt | 29 +++++ .../DdSdkNativeInitializationTest.kt | 4 + .../com/datadog/reactnative/DdSdkTest.kt | 101 +++++++++++++++++ .../tools/unit/DdSdkConfigurationExt.kt | 8 ++ .../input/complete-configuration.json | 5 + .../core/datadog-configuration.schema.json | 18 +++ .../core/ios/Sources/DdSdkConfiguration.swift | 12 +- .../Sources/DdSdkNativeInitialization.swift | 20 +++- .../ios/Sources/RNDdSdkConfiguration.swift | 16 ++- .../DdSdkNativeInitializationTests.swift | 4 + packages/core/ios/Tests/DdSdkTests.swift | 31 +++++- .../Fixtures/complete-configuration.json | 5 + .../DdSdkReactNativeConfiguration.test.ts | 3 + .../core/src/config/FileBasedConfiguration.ts | 4 +- .../src/config/FileBasedConfiguration.type.ts | 2 + .../__tests__/FileBasedConfiguration.test.ts | 7 ++ .../configuration-all-fields.json | 7 +- .../src/config/features/RumConfiguration.ts | 6 + .../config/features/RumConfiguration.type.ts | 8 +- .../config/features/RumConfigurationNative.ts | 3 + .../config/types/TimeseriesConfiguration.ts | 22 ++++ packages/core/src/config/types/index.ts | 4 + .../__tests__/initialization.test.tsx | 1 + 26 files changed, 409 insertions(+), 65 deletions(-) create mode 100644 packages/core/src/config/types/TimeseriesConfiguration.ts diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4bfc123e9..f9129a437 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1972,9 +1972,9 @@ SPEC CHECKSUMS: DatadogInternal: e5a4652ee266986e29c6b8491aff7f78dc3b731d DatadogLogs: 52792add5827042f619771a23656e2bfb6b0f4e7 DatadogRUM: b78d608e013f39c5679ed31ad97dad323c6113e8 - DatadogSDKReactNative: d65150a6f37b4ca8f4d98909898a9c4632a7cbd6 - DatadogSDKReactNativeSessionReplay: 53f8448c51ea23aeb47fe3d67e7c383b91d06acf - DatadogSDKReactNativeWebView: 25586892c073fe6f95928dc556e6b623abe513f8 + DatadogSDKReactNative: 9350819a6032a51df0c913870bb999fd47f37c8d + DatadogSDKReactNativeSessionReplay: d48308b873e952fd5f0924fee28fc4020b6d2650 + DatadogSDKReactNativeWebView: 208e2661ed497aa43c5538999d9aabbed01effb3 DatadogSessionReplay: 932b2077b1a5367de1450479d72093621b9fd618 DatadogTrace: 5ef84e254e2e1d337bc52e0cdf16396ca102aeb1 DatadogWebViewTracking: e0470157f55a83680a549d79ba2985e0d17465db @@ -1993,63 +1993,63 @@ SPEC CHECKSUMS: RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 - React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 + React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 - react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 + React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 + react-native-crash-tester: 48bde9d6f5256c61ef2e0c52dfc74256b26e55eb + react-native-safe-area-context: e134b241010ebe2aacdcea013565963d13826faa + react-native-webview: 2ea635bc43fd8a4b89de61133e8cc0607084e9f8 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2 + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 - RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 - RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada - RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 + RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f + RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f + RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt index 50a3ae377..d3198a2a3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt @@ -65,6 +65,8 @@ 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. + * A non-null value enables timeseries collection. */ data class RumConfiguration( val applicationId: String, @@ -82,7 +84,19 @@ 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 collectTypes The types of timeseries data to collect ("cpu", "memory"). + * Defaults to collecting all supported types when omitted. + */ +data class TimeseriesConfiguration( + val collectTypes: List? = null ) /** @@ -157,7 +171,12 @@ 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 unstable_timeseries: JSONTimeseriesConfiguration? = null +) + +internal data class JSONTimeseriesConfiguration( + val collectTypes: List? = null ) internal data class JSONLogsConfiguration( diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt index 0bf90624c..f933aa16b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt @@ -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("unstable_timeseries")?.asTimeseriesConfiguration() ) } @@ -86,6 +87,12 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration { } +internal fun ReadableMap.asTimeseriesConfiguration(): TimeseriesConfiguration { + return TimeseriesConfiguration( + collectTypes = getArray("collectTypes")?.toArrayList()?.filterIsInstance() + ) +} + internal fun ReadableMap.asConfigurationForTelemetry(): ConfigurationForTelemetry { return ConfigurationForTelemetry( initializationType = getString("initializationType"), @@ -187,7 +194,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.unstable_timeseries?.asTimeseriesConfiguration() ) } @@ -229,6 +237,12 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration { } +internal fun JSONTimeseriesConfiguration.asTimeseriesConfiguration(): TimeseriesConfiguration { + return TimeseriesConfiguration( + collectTypes = this.collectTypes + ) +} + internal fun JSONProxyConfiguration.asProxyConfig(): Pair? { return buildProxyConfig(type, address, port, username, password) } @@ -331,6 +345,15 @@ 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() + timeseries.collectTypes?.let { types -> + val typesArray = WritableNativeArray() + types.forEach { typesArray.pushString(it) } + timeseriesMap.putArray("collectTypes", typesArray) + } + rumMap.putMap("unstable_timeseries", timeseriesMap) + } map.putMap("rumConfiguration", rumMap) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index a2add91bf..69fb6052b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -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 @@ -27,6 +28,8 @@ 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.timeseries.TimeseriesType as NativeTimeseriesType import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent import com.datadog.android.trace.Trace @@ -253,11 +256,37 @@ 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 + ) { + val nativeConfigBuilder = NativeTimeseriesConfiguration.Builder() + val collectTypes = timeseries.collectTypes?.mapNotNull { it.asTimeseriesType() } + if (collectTypes != null) { + nativeConfigBuilder.collectOnly(*collectTypes.toTypedArray()) + } + + configBuilder.setTimeseriesConfiguration(nativeConfigBuilder.build()) + } + + private fun String.asTimeseriesType(): NativeTimeseriesType? { + return when (lowercase(Locale.US)) { + "cpu" -> NativeTimeseriesType.CPU + "memory" -> NativeTimeseriesType.MEMORY + else -> null + } + } + private fun buildLogsConfiguration(configuration: DdSdkConfiguration): LogsConfiguration { val configBuilder = LogsConfiguration.Builder() configuration.logsConfiguration?.customEndpoint?.let { diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt index 4e9cb24c7..81bd926c2 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkNativeInitializationTest.kt @@ -144,6 +144,9 @@ internal class DdSdkNativeInitializationTest { "[B3MULTI, TRACECONTEXT]" ) assertThat(configuration.rumConfiguration?.initialResourceThreshold).isEqualTo(0.5) + assertThat(configuration.rumConfiguration?.timeseries?.collectTypes).isEqualTo( + listOf("cpu") + ) } @Test @@ -192,6 +195,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 diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt index b29bb4857..dc49bbb76 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkTest.kt @@ -30,6 +30,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.TimeseriesType import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent import com.datadog.android.trace.Trace @@ -2368,6 +2369,106 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 initialize native SDK 𝕎 initialize() {timeseries enabled}`( + @Forgery configuration: DdSdkConfiguration + ) { + // Given + val rumConfiguration = configuration.rumConfiguration?.copy( + timeseries = TimeseriesConfiguration(collectTypes = listOf("cpu")) + ) + val bridgeConfiguration = configuration.copy( + rumConfiguration = rumConfiguration + ) + val sdkConfigCaptor = argumentCaptor() + val rumConfigCaptor = argumentCaptor() + val logsConfigCaptor = argumentCaptor() + val traceConfigCaptor = argumentCaptor() + + 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` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { 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("enabledTypes", setOf(TimeseriesType.CPU)) + } + } + } 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() + val rumConfigCaptor = argumentCaptor() + val logsConfigCaptor = argumentCaptor() + val traceConfigCaptor = argumentCaptor() + + 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` { Rum.enable(any(), any()) }.then { } + logsMock.`when` { Logs.enable(any(), any()) }.then { } + traceMock.`when` { 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 diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt index 4fc6c96d6..5726fee1c 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt @@ -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 @@ -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["unstable_timeseries"] = it.toReadableMap() } map["rumConfiguration"] = rumMap.toReadableMap() } @@ -93,6 +95,12 @@ fun DdSdkConfiguration.toReadableJavaOnlyMap(): ReadableMap { return map.toReadableMap() } +internal fun TimeseriesConfiguration.toReadableMap(): ReadableMap { + val map = mutableMapOf() + collectTypes?.let { map["collectTypes"] = it.toReadableArray() } + return map.toReadableMap() +} + internal fun ConfigurationForTelemetry.toReadableJavaOnlyMap(): ReadableMap { val map = mutableMapOf() initializationType?.let { map.put("initializationType", it) } diff --git a/packages/core/android/src/test/resources/input/complete-configuration.json b/packages/core/android/src/test/resources/input/complete-configuration.json index 29bf0dd27..7ede39d02 100644 --- a/packages/core/android/src/test/resources/input/complete-configuration.json +++ b/packages/core/android/src/test/resources/input/complete-configuration.json @@ -30,6 +30,11 @@ "resourceTraceSampleRate": 33, "trackBackgroundEvents": true, "trackFrustrations": false, + "unstable_timeseries": { + "collectTypes": [ + "cpu" + ] + }, "firstPartyHosts": [ { "match": "example.com", diff --git a/packages/core/datadog-configuration.schema.json b/packages/core/datadog-configuration.schema.json index 992e72f3e..869ec43c7 100644 --- a/packages/core/datadog-configuration.schema.json +++ b/packages/core/datadog-configuration.schema.json @@ -266,6 +266,24 @@ "customEndpoint": { "description": "Target a custom server for RUM.", "type": "string" + }, + "unstable_timeseries": { + "description": "Experimental: configuration for memory and CPU timeseries collection. Requires a native SDK build with timeseries support. Presence of this object enables timeseries collection.", + "type": "object", + "properties": { + "collectTypes": { + "description": "The types of timeseries data to collect. Defaults to collecting all supported types when omitted.", + "type": "array", + "items": { + "description": "Type of timeseries data to collect.", + "type": "string", + "enum": [ + "cpu", + "memory" + ] + } + } + } } }, "required": [ diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index fd8e8f3e1..326fa4d78 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -103,6 +103,10 @@ 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. +/// - timeseriesCollectTypes: The types of timeseries data to collect ("cpu", "memory"). +/// Nil collects all supported types. public class RumConfiguration: NSObject { public var applicationId: String = "" public var trackFrustrations: Bool? = true @@ -122,6 +126,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 timeseriesCollectTypes: [String]? = nil init( applicationId: String, @@ -141,7 +147,9 @@ public class RumConfiguration: NSObject { initialResourceThreshold: Double?, trackMemoryWarnings: Bool?, telemetrySampleRate: Double?, - customEndpoint: String? + customEndpoint: String?, + enableTimeseries: Bool? = nil, + timeseriesCollectTypes: [String]? = nil ) { self.applicationId = applicationId self.trackFrustrations = trackFrustrations @@ -161,6 +169,8 @@ public class RumConfiguration: NSObject { self.trackMemoryWarnings = trackMemoryWarnings self.telemetrySampleRate = telemetrySampleRate self.customEndpoint = customEndpoint + self.enableTimeseries = enableTimeseries + self.timeseriesCollectTypes = timeseriesCollectTypes } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index c46ddd145..8c32828ed 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -8,7 +8,7 @@ import DatadogCore import DatadogCrashReporting import DatadogInternal import DatadogLogs -import DatadogRUM +@_spi(Experimental) import DatadogRUM import DatadogTrace import Foundation import React @@ -198,7 +198,7 @@ public class DdSdkNativeInitialization: NSObject { threshold: initialThreshold) } - return RUM.Configuration( + var configuration = RUM.Configuration( applicationID: rumConfig.applicationId, sessionSampleRate: Float( rumConfig.sessionSampleRate ?? DefaultConfiguration.sessionSamplingRate), @@ -238,6 +238,22 @@ public class DdSdkNativeInitialization: NSObject { telemetrySampleRate: Float( rumConfig.telemetrySampleRate ?? DefaultConfiguration.telemetrySampleRate) ) + + if rumConfig.enableTimeseries ?? false { + let collectTypes: [TimeseriesType]? = rumConfig.timeseriesCollectTypes?.compactMap { + switch $0.lowercased() { + case "cpu": + return .cpu + case "memory": + return .memory + default: + return nil + } + } + configuration.timeseries = Timeseries(collectTypes: collectTypes) + } + + return configuration } func buildLogsConfiguration(configuration: DdSdkConfiguration) -> Logs.Configuration { diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index bd84625b3..963ee2464 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -71,6 +71,10 @@ extension NSDictionary { let telemetrySampleRate = rumDict["telemetrySampleRate"] as? Double let customEndpoint = rumDict["customEndpoint"] as? String + let timeseriesDict = rumDict["unstable_timeseries"] as? NSDictionary + let enableTimeseries = timeseriesDict != nil + let timeseriesCollectTypes = timeseriesDict?["collectTypes"] as? [String] + rumConfiguration = RumConfiguration( applicationId: applicationId, trackFrustrations: trackFrustrations ?? DefaultConfiguration.trackFrustrations, @@ -98,7 +102,9 @@ extension NSDictionary { ?? DefaultConfiguration.trackMemoryWarnings, telemetrySampleRate: telemetrySampleRate ?? DefaultConfiguration.telemetrySampleRate, - customEndpoint: customEndpoint + customEndpoint: customEndpoint, + enableTimeseries: enableTimeseries, + timeseriesCollectTypes: timeseriesCollectTypes ) } else { rumConfiguration = nil @@ -357,6 +363,10 @@ extension Dictionary where Key == String, Value == AnyObject { let firstPartyHosts = firstPartyHostsArray?.asFirstPartyHosts() ?? DefaultConfiguration.firstPartyHosts + let timeseriesDict = rum["unstable_timeseries"] as? [String: Any?] + let enableTimeseries = timeseriesDict != nil + let timeseriesCollectTypes = timeseriesDict?["collectTypes"] as? [String] + rumConfiguration = RumConfiguration( applicationId: applicationId, trackFrustrations: rum["trackFrustrations"] as? Bool @@ -387,7 +397,9 @@ extension Dictionary where Key == String, Value == AnyObject { ?? DefaultConfiguration.trackMemoryWarnings, telemetrySampleRate: (rum["telemetrySampleRate"] as? Double) ?? DefaultConfiguration.telemetrySampleRate, - customEndpoint: rum["customEndpoint"] as? String + customEndpoint: rum["customEndpoint"] as? String, + enableTimeseries: enableTimeseries, + timeseriesCollectTypes: timeseriesCollectTypes ) } else { rumConfiguration = nil diff --git a/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift b/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift index cb6a6c500..49038200b 100644 --- a/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift +++ b/packages/core/ios/Tests/DdSdkNativeInitializationTests.swift @@ -66,6 +66,8 @@ class DdSdkNativeInitializationTests: XCTestCase { let expectedFirstPartyHosts: [String: Set]? = ["example.com": [.b3multi, .tracecontext]] XCTAssertEqual(configuration?.rumConfiguration?.firstPartyHosts, expectedFirstPartyHosts) XCTAssertEqual(configuration?.rumConfiguration?.initialResourceThreshold, 0.5) + XCTAssertEqual(configuration?.rumConfiguration?.enableTimeseries, true) + XCTAssertEqual(configuration?.rumConfiguration?.timeseriesCollectTypes, ["cpu"]) } func testReturnsConfigurationWithMinimalData() { @@ -104,6 +106,8 @@ class DdSdkNativeInitializationTests: XCTestCase { let expectedFirstPartyHosts: [String: Set]? = [:] XCTAssertEqual(configuration?.rumConfiguration?.firstPartyHosts, expectedFirstPartyHosts) XCTAssertEqual(configuration?.rumConfiguration?.initialResourceThreshold, nil) + XCTAssertEqual(configuration?.rumConfiguration?.enableTimeseries, false) + XCTAssertNil(configuration?.rumConfiguration?.timeseriesCollectTypes) } func testPrintsMessageWithIncorrectFile() { diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index bc13c4f98..b53dc8d5d 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -1049,6 +1049,32 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(ddConfig.vitalsUpdateFrequency, nil) } + func testBuildConfigurationEnabledTimeseries() { + let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration() + rumConfiguration.enableTimeseries = true + rumConfiguration.timeseriesCollectTypes = ["cpu"] + let configuration: DdSdkConfiguration = .mockAny(rumConfiguration: rumConfiguration) + + let ddConfig = DdSdkNativeInitialization().buildRumConfiguration( + configuration: configuration + ) + + XCTAssertEqual(ddConfig.timeseries?.collectTypes, [.cpu]) + } + + func testBuildConfigurationDisabledTimeseriesByDefault() { + let rumConfiguration: RumConfiguration = makeDefaultRumConfiguration() + rumConfiguration.enableTimeseries = nil + rumConfiguration.timeseriesCollectTypes = nil + let configuration: DdSdkConfiguration = .mockAny(rumConfiguration: rumConfiguration) + + let ddConfig = DdSdkNativeInitialization().buildRumConfiguration( + configuration: configuration + ) + + XCTAssertNil(ddConfig.timeseries) + } + func testBuildConfigurationAverageUploadFrequency() { let configuration: DdSdkConfiguration = .mockAny(uploadFrequency: "AVERAGE") @@ -1781,7 +1807,9 @@ func makeDefaultRumConfiguration() -> RumConfiguration { initialResourceThreshold: nil, trackMemoryWarnings: true, telemetrySampleRate: 45.0, - customEndpoint: nil + customEndpoint: nil, + enableTimeseries: nil, + timeseriesCollectTypes: nil ) } @@ -1908,6 +1936,7 @@ extension NSDictionary { rumConfig["nativeInteractionTracking"] = rumConfiguration?["nativeInteractionTracking"] rumConfig["customEndpoint"] = rumConfiguration?["customEndpoint"] rumConfig["trackFrustrations"] = rumConfiguration?["trackFrustrations"] + rumConfig["unstable_timeseries"] = rumConfiguration?["unstable_timeseries"] logsConfig["bundleLogsWithRum"] = logsConfiguration?["bundleLogsWithRum"] logsConfig["bundleLogsWithTraces"] = logsConfiguration?["bundleLogsWithTraces"] diff --git a/packages/core/ios/Tests/Fixtures/complete-configuration.json b/packages/core/ios/Tests/Fixtures/complete-configuration.json index d2648a289..ba835ed4c 100644 --- a/packages/core/ios/Tests/Fixtures/complete-configuration.json +++ b/packages/core/ios/Tests/Fixtures/complete-configuration.json @@ -30,6 +30,11 @@ "resourceTraceSampleRate": 33, "trackBackgroundEvents": true, "trackFrustrations": false, + "unstable_timeseries": { + "collectTypes": [ + "cpu" + ] + }, "firstPartyHosts": [ { "match": "example.com", diff --git a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts index 7f8a6e231..af9897f07 100644 --- a/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts +++ b/packages/core/src/__tests__/DdSdkReactNativeConfiguration.test.ts @@ -80,6 +80,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackNonFatalAnrs": undefined, "trackResources": false, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": true, "vitalsUpdateFrequency": "AVERAGE", }, @@ -225,6 +226,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackNonFatalAnrs": true, "trackResources": true, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": true, "vitalsUpdateFrequency": "AVERAGE", }, @@ -329,6 +331,7 @@ describe('DdSdkReactNativeConfiguration', () => { "trackNonFatalAnrs": false, "trackResources": false, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": false, "vitalsUpdateFrequency": "AVERAGE", }, diff --git a/packages/core/src/config/FileBasedConfiguration.ts b/packages/core/src/config/FileBasedConfiguration.ts index cd89c7c07..6d2c569a3 100644 --- a/packages/core/src/config/FileBasedConfiguration.ts +++ b/packages/core/src/config/FileBasedConfiguration.ts @@ -208,7 +208,9 @@ export const getJSONConfiguration = ( telemetrySampleRate: configuration.rumConfiguration.telemetrySampleRate, nativeInteractionTracking: - configuration.rumConfiguration.nativeInteractionTracking + configuration.rumConfiguration.nativeInteractionTracking, + unstable_timeseries: + configuration.rumConfiguration.unstable_timeseries } }), ...(configuration.traceConfiguration !== undefined && { diff --git a/packages/core/src/config/FileBasedConfiguration.type.ts b/packages/core/src/config/FileBasedConfiguration.type.ts index b248fe2f4..ea1bf7dc0 100644 --- a/packages/core/src/config/FileBasedConfiguration.type.ts +++ b/packages/core/src/config/FileBasedConfiguration.type.ts @@ -11,6 +11,7 @@ import type { BatchSize, ProxyConfiguration, SdkVerbosity, + TimeseriesConfiguration, TrackingConsent, UploadFrequency, VitalsUpdateFrequency @@ -55,6 +56,7 @@ export interface JsonConfiguration extends CoreConfigurationOptions { trackWatchdogTerminations?: boolean; vitalsUpdateFrequency?: VitalsUpdateFrequency; telemetrySampleRate?: number; + unstable_timeseries?: TimeseriesConfiguration; }; traceConfiguration?: { customEndpoint?: string; diff --git a/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts b/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts index 1b801d8c5..f5959c3a9 100644 --- a/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts +++ b/packages/core/src/config/__tests__/FileBasedConfiguration.test.ts @@ -74,6 +74,11 @@ describe('FileBasedConfiguration', () => { "trackNonFatalAnrs": true, "trackResources": true, "trackWatchdogTerminations": true, + "unstable_timeseries": { + "collectTypes": [ + "cpu", + ], + }, "useAccessibilityLabel": false, "vitalsUpdateFrequency": "NEVER", }, @@ -187,6 +192,7 @@ describe('FileBasedConfiguration', () => { "trackNonFatalAnrs": undefined, "trackResources": true, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": false, "vitalsUpdateFrequency": "AVERAGE", }, @@ -252,6 +258,7 @@ describe('FileBasedConfiguration', () => { "trackNonFatalAnrs": undefined, "trackResources": false, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": true, "vitalsUpdateFrequency": "AVERAGE", }, diff --git a/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json b/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json index 6a4f7623a..56d10c2b6 100644 --- a/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json +++ b/packages/core/src/config/__tests__/__fixtures__/configuration-all-fields.json @@ -33,7 +33,12 @@ "trackBackgroundEvents": true, "trackMemoryWarnings": false, "trackWatchdogTerminations": true, - "vitalsUpdateFrequency": "NEVER" + "vitalsUpdateFrequency": "NEVER", + "unstable_timeseries": { + "collectTypes": [ + "cpu" + ] + } }, "traceConfiguration": { "customEndpoint": "https://example.com/trace" diff --git a/packages/core/src/config/features/RumConfiguration.ts b/packages/core/src/config/features/RumConfiguration.ts index 9d6e6b965..84a4c26ce 100644 --- a/packages/core/src/config/features/RumConfiguration.ts +++ b/packages/core/src/config/features/RumConfiguration.ts @@ -7,6 +7,7 @@ import type { ActionEventMapper } from '../../rum/eventMappers/actionEventMapper import type { ErrorEventMapper } from '../../rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from '../../rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from '../../rum/types'; +import type { TimeseriesConfiguration } from '../types'; import { VitalsUpdateFrequency } from '../types'; import type { @@ -40,6 +41,7 @@ const DEFAULTS = { trackNonFatalAnrs: undefined, trackResources: false, trackWatchdogTerminations: false, + unstable_timeseries: undefined, useAccessibilityLabel: true, vitalsUpdateFrequency: VitalsUpdateFrequency.AVERAGE }; @@ -120,6 +122,10 @@ export class RumConfiguration implements RumConfigurationType { public trackWatchdogTerminations: boolean = DEFAULTS.trackWatchdogTerminations; + // Experimental Timeseries Configuration + public unstable_timeseries?: TimeseriesConfiguration = + DEFAULTS.unstable_timeseries; + // Use Accessibility Label enabled for interaction tracking public useAccessibilityLabel: boolean = DEFAULTS.useAccessibilityLabel; diff --git a/packages/core/src/config/features/RumConfiguration.type.ts b/packages/core/src/config/features/RumConfiguration.type.ts index 40509aaa6..4d42afae0 100644 --- a/packages/core/src/config/features/RumConfiguration.type.ts +++ b/packages/core/src/config/features/RumConfiguration.type.ts @@ -7,7 +7,7 @@ import type { ActionEventMapper } from '../../rum/eventMappers/actionEventMapper import type { ErrorEventMapper } from '../../rum/eventMappers/errorEventMapper'; import type { ResourceEventMapper } from '../../rum/eventMappers/resourceEventMapper'; import type { FirstPartyHost } from '../../rum/types'; -import type { VitalsUpdateFrequency } from '../types'; +import type { TimeseriesConfiguration, VitalsUpdateFrequency } from '../types'; /** * Required RUM configuration values. @@ -163,6 +163,12 @@ export interface RumConfigurationOptions { * Preferred frequency for collecting mobile vitals. */ vitalsUpdateFrequency?: VitalsUpdateFrequency; + + /** + * Experimental: configuration for memory and CPU timeseries collection. + * Requires a native SDK build with timeseries support. + */ + unstable_timeseries?: TimeseriesConfiguration; } /** diff --git a/packages/core/src/config/features/RumConfigurationNative.ts b/packages/core/src/config/features/RumConfigurationNative.ts index 155f086e9..c86d8579d 100644 --- a/packages/core/src/config/features/RumConfigurationNative.ts +++ b/packages/core/src/config/features/RumConfigurationNative.ts @@ -3,6 +3,8 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ +import type { TimeseriesConfiguration } from '../types'; + export type RumNativeConfiguration = { readonly applicationId: string; readonly trackFrustrations: boolean; @@ -23,4 +25,5 @@ export type RumNativeConfiguration = { readonly telemetrySampleRate: number; readonly customEndpoint: string; readonly firstPartyHosts: { match: string; propagatorTypes: string[] }[]; + readonly unstable_timeseries: TimeseriesConfiguration | undefined; }; diff --git a/packages/core/src/config/types/TimeseriesConfiguration.ts b/packages/core/src/config/types/TimeseriesConfiguration.ts new file mode 100644 index 000000000..cec5a926e --- /dev/null +++ b/packages/core/src/config/types/TimeseriesConfiguration.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Experimental: the type of timeseries data to collect. + */ +export type TimeseriesType = 'cpu' | 'memory'; + +/** + * Experimental: configuration for memory and CPU timeseries collection. + * Requires a native SDK build with timeseries support. + */ +export interface TimeseriesConfiguration { + /** + * The types of timeseries data to collect. + * Defaults to collecting all supported types when omitted. + */ + collectTypes?: TimeseriesType[]; +} diff --git a/packages/core/src/config/types/index.ts b/packages/core/src/config/types/index.ts index 6df83e28c..2dcdbe243 100644 --- a/packages/core/src/config/types/index.ts +++ b/packages/core/src/config/types/index.ts @@ -8,6 +8,10 @@ export { BatchProcessingLevel } from './BatchProcessingLevel'; export { BatchSize } from './BatchSize'; export { ProxyConfiguration, ProxyType } from './ProxyConfiguration'; export { SdkVerbosity } from './SdkVerbosity'; +export type { + TimeseriesConfiguration, + TimeseriesType +} from './TimeseriesConfiguration'; export { TrackingConsent } from './TrackingConsent'; export { UploadFrequency } from './UploadFrequency'; export { VitalsUpdateFrequency } from './VitalsUpdateFrequency'; diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index 224e49f0a..e33ff4058 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -111,6 +111,7 @@ describe('DatadogProvider', () => { "trackNonFatalAnrs": undefined, "trackResources": false, "trackWatchdogTerminations": false, + "unstable_timeseries": undefined, "useAccessibilityLabel": true, "vitalsUpdateFrequency": "AVERAGE", }, From d7904fabefcca12695bee362c095c74db3e63efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Mon, 28 Sep 2026 10:48:38 +0200 Subject: [PATCH 2/3] RUM-17173 Align timeseries collectTypes with new native Set-based API --- .../reactnative/DdSdkNativeInitialization.kt | 11 ++++---- .../Sources/DdSdkNativeInitialization.swift | 26 ++++++++++++------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 69fb6052b..1f0d71454 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -270,13 +270,14 @@ class DdSdkNativeInitialization internal constructor( configBuilder: RumConfiguration.Builder, timeseries: TimeseriesConfiguration ) { - val nativeConfigBuilder = NativeTimeseriesConfiguration.Builder() - val collectTypes = timeseries.collectTypes?.mapNotNull { it.asTimeseriesType() } - if (collectTypes != null) { - nativeConfigBuilder.collectOnly(*collectTypes.toTypedArray()) + val collectTypes = timeseries.collectTypes?.mapNotNull { it.asTimeseriesType() }?.toSet() + val nativeConfig = if (collectTypes != null) { + NativeTimeseriesConfiguration(collectTypes) + } else { + NativeTimeseriesConfiguration.DEFAULT } - configBuilder.setTimeseriesConfiguration(nativeConfigBuilder.build()) + configBuilder.setTimeseriesConfiguration(nativeConfig) } private fun String.asTimeseriesType(): NativeTimeseriesType? { diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 8c32828ed..f02d2b1f8 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -240,17 +240,23 @@ public class DdSdkNativeInitialization: NSObject { ) if rumConfig.enableTimeseries ?? false { - let collectTypes: [TimeseriesType]? = rumConfig.timeseriesCollectTypes?.compactMap { - switch $0.lowercased() { - case "cpu": - return .cpu - case "memory": - return .memory - default: - return nil - } + if let timeseriesCollectTypes = rumConfig.timeseriesCollectTypes { + let collectTypes: Set = Set( + timeseriesCollectTypes.compactMap { + switch $0.lowercased() { + case "cpu": + return .cpu + case "memory": + return .memory + default: + return nil + } + } + ) + configuration.timeseries = Timeseries(collectTypes: collectTypes) + } else { + configuration.timeseries = .default } - configuration.timeseries = Timeseries(collectTypes: collectTypes) } return configuration From d061be13b0bcd59efe4eefef81ec44a9fa6878e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Mon, 28 Sep 2026 12:19:59 +0200 Subject: [PATCH 3/3] RUM-17173 Make timeseries collectTypes mandatory to mirror native API --- .../com/datadog/reactnative/DdSdkConfiguration.kt | 11 ++++++++--- .../com/datadog/reactnative/DdSdkConfigurationExt.kt | 3 ++- .../datadog/reactnative/DdSdkNativeInitialization.kt | 9 ++------- packages/core/ios/Sources/DdSdkConfiguration.swift | 3 ++- .../core/src/config/types/TimeseriesConfiguration.ts | 11 ++++++++--- packages/core/src/config/types/index.ts | 1 + 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt index d3198a2a3..79ba3dfd3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt @@ -88,15 +88,20 @@ data class RumConfiguration( val timeseries: TimeseriesConfiguration? = null ) +/** + * Experimental: the default set of timeseries types to collect (cpu and memory). + */ +internal val DEFAULT_TIMESERIES_COLLECT_TYPES = listOf("cpu", "memory") + /** * Experimental: configuration for memory and CPU timeseries collection. * Requires a native SDK build with timeseries support. * - * @param collectTypes The types of timeseries data to collect ("cpu", "memory"). - * Defaults to collecting all supported types when omitted. + * @param collectTypes The types of timeseries data to collect ("cpu", "memory"). Required. + * Use [DEFAULT_TIMESERIES_COLLECT_TYPES] to collect the default set. */ data class TimeseriesConfiguration( - val collectTypes: List? = null + val collectTypes: List ) /** diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt index f933aa16b..a3e17fed2 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt @@ -90,6 +90,7 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration { internal fun ReadableMap.asTimeseriesConfiguration(): TimeseriesConfiguration { return TimeseriesConfiguration( collectTypes = getArray("collectTypes")?.toArrayList()?.filterIsInstance() + ?: DEFAULT_TIMESERIES_COLLECT_TYPES ) } @@ -239,7 +240,7 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration { internal fun JSONTimeseriesConfiguration.asTimeseriesConfiguration(): TimeseriesConfiguration { return TimeseriesConfiguration( - collectTypes = this.collectTypes + collectTypes = this.collectTypes ?: DEFAULT_TIMESERIES_COLLECT_TYPES ) } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 1f0d71454..cae963fb3 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -270,14 +270,9 @@ class DdSdkNativeInitialization internal constructor( configBuilder: RumConfiguration.Builder, timeseries: TimeseriesConfiguration ) { - val collectTypes = timeseries.collectTypes?.mapNotNull { it.asTimeseriesType() }?.toSet() - val nativeConfig = if (collectTypes != null) { - NativeTimeseriesConfiguration(collectTypes) - } else { - NativeTimeseriesConfiguration.DEFAULT - } + val collectTypes = timeseries.collectTypes.mapNotNull { it.asTimeseriesType() }.toSet() - configBuilder.setTimeseriesConfiguration(nativeConfig) + configBuilder.setTimeseriesConfiguration(NativeTimeseriesConfiguration(collectTypes)) } private fun String.asTimeseriesType(): NativeTimeseriesType? { diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index 326fa4d78..bd18137f8 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -106,7 +106,8 @@ public class DdSdkConfiguration: NSObject { /// - enableTimeseries: Experimental: enables collection of memory and CPU timeseries events. /// Requires a native SDK build with timeseries support. /// - timeseriesCollectTypes: The types of timeseries data to collect ("cpu", "memory"). -/// Nil collects all supported types. +/// The native SDK requires an explicit set of types; if nil, this SDK +/// passes its default set (cpu and memory) on your behalf. public class RumConfiguration: NSObject { public var applicationId: String = "" public var trackFrustrations: Bool? = true diff --git a/packages/core/src/config/types/TimeseriesConfiguration.ts b/packages/core/src/config/types/TimeseriesConfiguration.ts index cec5a926e..a81b1d62e 100644 --- a/packages/core/src/config/types/TimeseriesConfiguration.ts +++ b/packages/core/src/config/types/TimeseriesConfiguration.ts @@ -15,8 +15,13 @@ export type TimeseriesType = 'cpu' | 'memory'; */ export interface TimeseriesConfiguration { /** - * The types of timeseries data to collect. - * Defaults to collecting all supported types when omitted. + * The types of timeseries data to collect. Required. + * Use `defaultTimeseriesTypes` to collect the default set (cpu and memory). */ - collectTypes?: TimeseriesType[]; + collectTypes: TimeseriesType[]; } + +/** + * Experimental: the default set of timeseries types to collect (cpu and memory). + */ +export const defaultTimeseriesTypes: TimeseriesType[] = ['cpu', 'memory']; diff --git a/packages/core/src/config/types/index.ts b/packages/core/src/config/types/index.ts index 2dcdbe243..0f2984d3d 100644 --- a/packages/core/src/config/types/index.ts +++ b/packages/core/src/config/types/index.ts @@ -12,6 +12,7 @@ export type { TimeseriesConfiguration, TimeseriesType } from './TimeseriesConfiguration'; +export { defaultTimeseriesTypes } from './TimeseriesConfiguration'; export { TrackingConsent } from './TrackingConsent'; export { UploadFrequency } from './UploadFrequency'; export { VitalsUpdateFrequency } from './VitalsUpdateFrequency';