diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 0e85e65ae..68dff07d1 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -123,6 +123,18 @@ actualRN.NativeModules.DdRum = { addTiming: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + addViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeViewAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, addViewLoadingTime: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt index 35471374e..977f2bec0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdRumImplementation.kt @@ -13,6 +13,7 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale @@ -248,6 +249,49 @@ class DdRumImplementation(private val datadog: DatadogWrapper = DatadogSDKWrappe promise.resolve(null) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + val attributes = mutableMapOf() + attributes[key] = attributeValue + datadog.getRumMonitor().addViewAttributes(attributes) + promise.resolve(null) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + fun removeViewAttribute(key: String, promise: Promise) { + val keysToDelete: Collection = listOf(key) + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + datadog.getRumMonitor().addViewAttributes(attributes.toMap()) + promise.resolve(null) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + val keysToDelete = (0 until keys.size()) + .mapNotNull { keys.getString(it) } + datadog.getRumMonitor().removeViewAttributes(keysToDelete) + promise.resolve(null) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 467d37335..ce747c807 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -94,8 +94,6 @@ class DdSdkImplementation( if (id != null) { datadog.setUserInfo(id, name, email, extraInfo) - } else { - // TO DO - Log warning? } promise.resolve(null) diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt index ce8104685..6cb2b385b 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -9,6 +9,7 @@ package com.datadog.reactnative import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -201,6 +202,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + override fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + override fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + override fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + override fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt index 4e4668a3e..fbe985164 100644 --- a/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/newarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -18,7 +18,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule /** The entry point to initialize Datadog's features. */ class DdSdk( reactContext: ReactApplicationContext, - datadogWrapper: DatadogWrapper = DatadogSDKWrapper() + datadogWrapper: DatadogWrapper = DatadogSDKWrapper(), ddTelemetry: DdTelemetry = DdTelemetry() ) : NativeDdSdkSpec(reactContext) { diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt index 79742e854..a6c4965ea 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdRum.kt @@ -10,6 +10,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap /** @@ -192,6 +193,43 @@ class DdRum( implementation.addTiming(name, promise) } + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + @ReactMethod + fun addViewAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addViewAttribute(key, value, promise) + } + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + @ReactMethod + fun removeViewAttribute(key: String, promise: Promise) { + implementation.removeViewAttribute(key, promise) + } + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + @ReactMethod + fun addViewAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addViewAttributes(attributes, promise) + } + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + @ReactMethod + fun removeViewAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeViewAttributes(keys, promise) + } + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt index be1c57b3a..9619794df 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdRumTest.kt @@ -13,13 +13,16 @@ import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.tools.unit.forge.BaseConfigurator +import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableMap import com.facebook.react.bridge.Promise import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.AdvancedForgery import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.MapForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeConfiguration @@ -456,6 +459,61 @@ internal class DdRumTest { verify(mockRumMonitor).addTiming(timing) } + @Test + fun `M call addViewAttribute W addViewAttribute()`( + @StringForgery key: String, + @StringForgery value: String + ) { + var attributeMap = mutableMapOf() + attributeMap.put("value", value) + + var attributes = mutableMapOf() + attributes.put(key, value) + + // When + testedDdRum.addViewAttribute(key, attributeMap.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(attributes) + } + + @Test + fun `M call removeViewAttribute W removeViewAttribute()`(@StringForgery key: String) { + // When + testedDdRum.removeViewAttribute(key, mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(listOf(key)) + } + + @Test + fun `M call addViewAttributes W addViewAttributes()`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.addViewAttributes(customAttributes.toReadableMap(), mockPromise) + + // Then + verify(mockRumMonitor).addViewAttributes(customAttributes) + } + + @Test + fun `𝕄 call removeViewAttributes 𝕎 removeViewAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // When + testedDdRum.removeViewAttributes(customAttributes.keys.toReadableArray(), mockPromise) + + // Then + verify(mockRumMonitor).removeViewAttributes(customAttributes.keys.toList()) + } + @Test fun `M call addViewLoadingTime w addViewLoadingTime()`(@BoolForgery overwrite: Boolean) { // When diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt index 702cc2533..13f73d94a 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/MockRumMonitor.kt @@ -30,7 +30,13 @@ class MockRumMonitor : RumMonitor { override fun addAttribute(key: String, value: Any?) {} - override fun addViewAttributes(attributes: Map) {} + override fun removeAttribute(key: String) {} + + override fun clearAttributes() {} + + override fun getAttributes(): Map { + return mapOf() + } override fun addError( message: String, @@ -55,15 +61,10 @@ class MockRumMonitor : RumMonitor { @ExperimentalRumApi override fun addViewLoadingTime(overwrite: Boolean) {} - override fun clearAttributes() {} - - override fun getAttributes(): Map { - return mapOf() - } - override fun getCurrentSessionId(callback: (String?) -> Unit) {} - override fun removeAttribute(key: String) {} + override fun addViewAttributes(attributes: Map) {} + override fun removeViewAttributes(attributes: Collection) {} override fun startAction( diff --git a/packages/core/ios/Sources/AnyEncodable.swift b/packages/core/ios/Sources/AnyEncodable.swift index 39821af87..4263ef6d2 100644 --- a/packages/core/ios/Sources/AnyEncodable.swift +++ b/packages/core/ios/Sources/AnyEncodable.swift @@ -14,13 +14,20 @@ internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Enc var casted: [String: Encodable] = [:] attributes.forEach { key, value in - if let castedValue = castByPreservingTypeInformation(attributeValue: value) { - // If possible, cast attribute by preserving its type information - casted[key] = castedValue - } else { - // Otherwise, cast by preserving its encoded value (and loosing type information) - casted[key] = castByPreservingEncodedValue(attributeValue: value) - } + casted[key] = castValueToSwift(value) + } + + return casted +} + +internal func castValueToSwift(_ value: Any) -> Encodable { + var casted: Encodable + if let castedValue = castByPreservingTypeInformation(attributeValue: value) { + // If possible, cast attribute by preserving its type information + casted = castedValue + } else { + // Otherwise, cast by preserving its encoded value (and loosing type information) + casted = castByPreservingEncodedValue(attributeValue: value) } return casted diff --git a/packages/core/ios/Sources/DdRum.mm b/packages/core/ios/Sources/DdRum.mm index 5d831942a..f5c324ce8 100644 --- a/packages/core/ios/Sources/DdRum.mm +++ b/packages/core/ios/Sources/DdRum.mm @@ -107,6 +107,35 @@ @implementation DdRum [self addTiming:name resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(addViewAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttribute:key resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(addViewAttributes:(NSDictionary*) attributes + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addViewAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeViewAttributes:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeViewAttributes:keys resolve:resolve reject:reject]; +} + RCT_REMAP_METHOD(addViewLoadingTime, withOverwrite:(BOOL)overwrite withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -138,7 +167,7 @@ @implementation DdRum // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params +(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @@ -180,6 +209,22 @@ - (void)addTiming:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve rejec [self.ddRumImplementation addTimingWithName:name resolve:resolve reject:reject]; } +- (void)addViewAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeViewAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addViewAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation addViewAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeViewAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddRumImplementation removeViewAttributesWithKeys:keys resolve:resolve reject:reject]; +} + - (void)addViewLoadingTime:(BOOL)overwrite resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {\ [self.ddRumImplementation addViewLoadingTimeWithOverwrite:overwrite resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdRumImplementation.swift b/packages/core/ios/Sources/DdRumImplementation.swift index 9f8da4c7f..6fac21f82 100644 --- a/packages/core/ios/Sources/DdRumImplementation.swift +++ b/packages/core/ios/Sources/DdRumImplementation.swift @@ -181,6 +181,34 @@ public class DdRumImplementation: NSObject { resolve(nil) } + @objc + public func addViewAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedAttribute = castValueToSwift(attributeValue) + nativeRUM.addViewAttribute(forKey: key, value: castedAttribute) + } + resolve(nil) + } + + @objc + public func removeViewAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttribute(forKey: key) + resolve(nil) + } + + @objc + public func addViewAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + let castedAttributes = castAttributesToSwift(attributes) + nativeRUM.addViewAttributes(castedAttributes) + resolve(nil) + } + + @objc + public func removeViewAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + nativeRUM.removeViewAttributes(forKeys: keys) + resolve(nil) + } + @objc public func addViewLoadingTime(overwrite: Bool, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { nativeRUM.addViewLoadingTime(overwrite: overwrite) diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9d057d158..f657021a9 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -4,18 +4,19 @@ * Copyright 2016-Present Datadog, Inc. */ -import Foundation import DatadogCore -import DatadogRUM +import DatadogCrashReporting +import DatadogInternal import DatadogLogs +import DatadogRUM import DatadogTrace -import DatadogCrashReporting import DatadogWebViewTracking -import DatadogInternal +import Foundation import React func getDefaultAppVersion() -> String { - let bundleShortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let bundleShortVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String return bundleShortVersion ?? bundleVersion ?? "0.0.0" } @@ -29,7 +30,7 @@ public class DdSdkImplementation: NSObject { let RUMMonitorInternalProvider: () -> RUMMonitorInternalProtocol? var webviewMessageEmitter: InternalExtension.AbstractMessageEmitter? - private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1; + private let jsLongTaskThresholdInSeconds: TimeInterval = 0.1 @objc public convenience init(bridge: RCTBridge) { @@ -41,7 +42,7 @@ public class DdSdkImplementation: NSObject { RUMMonitorInternalProvider: { RUMMonitor.shared()._internal } ) } - + init( mainDispatchQueue: DispatchQueueType, jsDispatchQueue: DispatchQueueType, @@ -56,10 +57,13 @@ public class DdSdkImplementation: NSObject { self.RUMMonitorInternalProvider = RUMMonitorInternalProvider super.init() } - + // Using @escaping RCTPromiseResolveBlock type will result in an issue when compiling the Swift header file. @objc - public func initialize(configuration: NSDictionary, resolve:@escaping ((Any?) -> Void), reject:RCTPromiseRejectBlock) -> Void { + public func initialize( + configuration: NSDictionary, resolve: @escaping ((Any?) -> Void), + reject: RCTPromiseRejectBlock + ) { let sdkConfiguration = configuration.asDdSdkConfiguration() let nativeInitialization = DdSdkNativeInitialization() @@ -71,18 +75,22 @@ public class DdSdkImplementation: NSObject { } @objc - public func setAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setAttributes( + attributes: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedAttributes = castAttributesToSwift(attributes) for (key, value) in castedAttributes { RUMMonitorProvider().addAttribute(forKey: key, value: value) GlobalState.addAttribute(forKey: key, value: value) } - + resolve(nil) } @objc - public func setUserInfo(userInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setUserInfo( + userInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedUserInfo = castAttributesToSwift(userInfo) let id = castedUserInfo["id"] as? String let name = castedUserInfo["name"] as? String @@ -90,21 +98,22 @@ public class DdSdkImplementation: NSObject { var extraInfo: [AttributeKey: AttributeValue] = [:] if let extraInfoEncodable = castedUserInfo["extraInfo"] as? AnyEncodable, - let extraInfoDict = extraInfoEncodable.value as? [String: Any] { + let extraInfoDict = extraInfoEncodable.value as? [String: Any] + { extraInfo = castAttributesToSwift(extraInfoDict) } if let validId = id { Datadog.setUserInfo(id: validId, name: name, email: email, extraInfo: extraInfo) - } else { - // TO DO - log warning message? } resolve(nil) } - + @objc - public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addUserExtraInfo( + extraInfo: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedExtraInfo = castAttributesToSwift(extraInfo) Datadog.addUserExtraInfo(castedExtraInfo) @@ -112,59 +121,80 @@ public class DdSdkImplementation: NSObject { } @objc - public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearUserInfo(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearUserInfo() resolve(nil) } @objc - public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func setTrackingConsent( + trackingConsent: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) resolve(nil) } - - + @objc - public func sendTelemetryLog(message: NSString, attributes: NSDictionary, config: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func sendTelemetryLog( + message: NSString, attributes: NSDictionary, config: NSDictionary, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { let castedAttributes = castAttributesToSwift(attributes) let castedConfig = castAttributesToSwift(config) - DdTelemetry.sendTelemetryLog(message: message as String, attributes: castedAttributes, config: castedConfig) + DdTelemetry.sendTelemetryLog( + message: message as String, attributes: castedAttributes, config: castedConfig) resolve(nil) } @objc - public func telemetryDebug(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryDebug(id: "datadog_react_native:\(message)", message: message as String) + public func telemetryDebug( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryDebug( + id: "datadog_react_native:\(message)", message: message as String) resolve(nil) } - + @objc - public func telemetryError(message: NSString, stack: NSString, kind: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - DdTelemetry.telemetryError(id: "datadog_react_native:\(String(describing: kind)):\(message)", message: message as String, kind: kind as String, stack: stack as String) + public func telemetryError( + message: NSString, stack: NSString, kind: NSString, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + DdTelemetry.telemetryError( + id: "datadog_react_native:\(String(describing: kind)):\(message)", + message: message as String, kind: kind as String, stack: stack as String) resolve(nil) } @objc - public func consumeWebviewEvent(message: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - do{ + public func consumeWebviewEvent( + message: NSString, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + do { try DatadogSDKWrapper.shared.sendWebviewMessage(body: message) } catch { - DdTelemetry.telemetryError(id: "datadog_react_native:\(error.localizedDescription)", message: "The message being sent was:\(message)" as String, kind: "WebViewEventBridgeError" as String, stack: String(describing: error) as String) + DdTelemetry.telemetryError( + id: "datadog_react_native:\(error.localizedDescription)", + message: "The message being sent was:\(message)" as String, + kind: "WebViewEventBridgeError" as String, + stack: String(describing: error) as String) } resolve(nil) } - + @objc - public func clearAllData(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func clearAllData(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { Datadog.clearAllData() resolve(nil) } - func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) -> Void { + func overrideReactNativeTelemetry(rnConfiguration: DdSdkConfiguration) { DdTelemetry.overrideTelemetryConfiguration( - initializationType: rnConfiguration.configurationForTelemetry?.initializationType as? String, - reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion as? String, + initializationType: rnConfiguration.configurationForTelemetry?.initializationType + as? String, + reactNativeVersion: rnConfiguration.configurationForTelemetry?.reactNativeVersion + as? String, reactVersion: rnConfiguration.configurationForTelemetry?.reactVersion as? String, trackCrossPlatformLongTasks: rnConfiguration.longTaskThresholdMs != 0, trackErrors: rnConfiguration.configurationForTelemetry?.trackErrors, @@ -179,38 +209,45 @@ public class DdSdkImplementation: NSObject { func startJSRefreshRateMonitoring(sdkConfiguration: DdSdkConfiguration) { if let frameTimeCallback = buildFrameTimeCallback(sdkConfiguration: sdkConfiguration) { // Falling back to mainDispatchQueue if bridge is nil is only useful for tests - self.jsRefreshRateMonitor.startMonitoring(jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) + self.jsRefreshRateMonitor.startMonitoring( + jsQueue: jsDispatchQueue, frameTimeCallback: frameTimeCallback) } } - func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration)-> ((Double) -> ())? { + func buildFrameTimeCallback(sdkConfiguration: DdSdkConfiguration) -> ((Double) -> Void)? { let jsRefreshRateMonitoringEnabled = sdkConfiguration.vitalsUpdateFrequency != nil let jsLongTaskMonitoringEnabled = sdkConfiguration.longTaskThresholdMs != 0 - - if (!jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled) { + + if !jsRefreshRateMonitoringEnabled && !jsLongTaskMonitoringEnabled { return nil } func frameTimeCallback(frameTime: Double) { // These checks happen before dispatching because they are quick and less overhead than the dispatch itself. let shouldRecordFrameTime = jsRefreshRateMonitoringEnabled && frameTime > 0 - let shouldRecordLongTask = jsLongTaskMonitoringEnabled && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 + let shouldRecordLongTask = + jsLongTaskMonitoringEnabled + && frameTime > sdkConfiguration.longTaskThresholdMs / 1_000 guard shouldRecordFrameTime || shouldRecordLongTask, - let rumMonitorInternal = RUMMonitorInternalProvider() else { return } + let rumMonitorInternal = RUMMonitorInternalProvider() + else { return } // Record current timestamp, it may change slightly before event is created on background thread. let now = Date() // Leave JS thread ASAP to give as much time to JS engine work. sharedQueue.async { - if (shouldRecordFrameTime) { - rumMonitorInternal.updatePerformanceMetric(at: now, metric: .jsFrameTimeSeconds, value: frameTime, attributes: [:]) + if shouldRecordFrameTime { + rumMonitorInternal.updatePerformanceMetric( + at: now, metric: .jsFrameTimeSeconds, value: frameTime, attributes: [:]) } - if (shouldRecordLongTask) { - rumMonitorInternal.addLongTask(at: now, duration: frameTime, attributes: ["long_task.target": "javascript"]) + if shouldRecordLongTask { + rumMonitorInternal.addLongTask( + at: now, duration: frameTime, attributes: ["long_task.target": "javascript"] + ) } } } - + return frameTimeCallback } diff --git a/packages/core/ios/Tests/DdRumTests.swift b/packages/core/ios/Tests/DdRumTests.swift index 1102b7b6b..5f6adc016 100644 --- a/packages/core/ios/Tests/DdRumTests.swift +++ b/packages/core/ios/Tests/DdRumTests.swift @@ -253,6 +253,65 @@ internal class DdRumTests: XCTestCase { XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 0) } + func testAddViewAttribute() throws { + let viewAttributeKey = "attributeKey" + let viewAttributes = NSDictionary( + dictionary: [ + "value": 123, + ] + ) + + rum.addViewAttribute(key: viewAttributeKey, value: viewAttributes, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttribute(key: viewAttributeKey)) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 1) + XCTAssertEqual(lastAttributes["attributeKey"] as? Int64, 123) + } + + func testRemoveViewAttribute() throws { + let viewAttributeKey = "attributeKey" + + rum.removeViewAttribute(key: viewAttributeKey, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttribute(key: viewAttributeKey)) + } + + func testAddViewAttributes() throws { + let viewAttributes = NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ) + + rum.addViewAttributes(attributes: viewAttributes, resolve: mockResolve, reject: mockReject) + + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .addViewAttributes()) + XCTAssertEqual(mockNativeRUM.receivedAttributes.count, 1) + let lastAttributes = try XCTUnwrap(mockNativeRUM.receivedAttributes.last) + XCTAssertEqual(lastAttributes.count, 3) + XCTAssertEqual(lastAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(lastAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(lastAttributes["attribute-3"] as? Bool, true) + } + + + func testRemoveViewAttributes() throws { + let viewAttributeKeys = ["attributeKey1", "attributeKey2", "attributeKey3"] + + rum.removeViewAttributes(keys: viewAttributeKeys, resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(mockNativeRUM.calledMethods.count, 1) + XCTAssertEqual(mockNativeRUM.calledMethods.last, .removeViewAttributes(keys: viewAttributeKeys)) + } + func testAddViewLoadingTime() throws { rum.addViewLoadingTime(overwrite: true, resolve: mockResolve, reject: mockReject) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index f0fa03364..31e3290ba 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -10,22 +10,6 @@ @testable import DatadogSDKReactNative internal class MockRUMMonitor: RUMMonitorProtocol { - func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { - // not implemented - } - - func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // not implemented - } - - func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { - // not implemented - } - - func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // not implemented - } - func currentSessionID(completion: @escaping (String?) -> Void) { // not implemented } @@ -65,6 +49,10 @@ internal class MockRUMMonitor: RUMMonitorProtocol { case stopUserAction(type: RUMActionType, name: String?) case addUserAction(type: RUMActionType, name: String) case addTiming(name: String) + case addViewAttribute(key: String) + case removeViewAttribute(key: String) + case addViewAttributes(_: Int? = nil) // We need an attribute for the case to be Equatable + case removeViewAttributes(keys: [String]) case addViewLoadingTime(overwrite: Bool) case stopSession(_: Int? = nil) // We need an attribute for the case to be Equatable case addResourceMetrics(resourceKey: String, @@ -125,6 +113,24 @@ internal class MockRUMMonitor: RUMMonitorProtocol { func addTiming(name: String) { calledMethods.append(.addTiming(name: name)) } + func addViewAttribute(forKey key: DatadogInternal.AttributeKey, value: any DatadogInternal.AttributeValue) { + calledMethods.append(.addViewAttribute(key: key)) + receivedAttributes.append([key :value]) + } + + func removeViewAttribute(forKey key: DatadogInternal.AttributeKey) { + calledMethods.append(.removeViewAttribute(key: key)) + } + + func addViewAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { + calledMethods.append(.addViewAttributes()) + receivedAttributes.append(attributes) + } + + func removeViewAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { + calledMethods.append(.removeViewAttributes(keys: keys)) + } + func addViewLoadingTime(overwrite: Bool) { calledMethods.append(.addViewLoadingTime(overwrite: overwrite)) } diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 8e154c4cd..0bdb03c21 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -110,6 +110,18 @@ module.exports = { addTiming: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeViewAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), addViewLoadingTime: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index 908404fc8..e5deb8146 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -9,6 +9,7 @@ import { DdAttributes } from '../DdAttributes'; import { InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; import type { DdNativeRumType } from '../nativeModulesTypes'; +import type { Attributes } from '../sdk/AttributesSingleton/types'; import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall'; import { DdSdk } from '../sdk/DdSdk'; import { GlobalState } from '../sdk/GlobalState/GlobalState'; @@ -284,6 +285,50 @@ class DdRumWrapper implements DdRumType { return bufferVoidNativeCall(() => this.nativeRum.addTiming(name)); }; + addViewAttribute = (key: string, value: unknown): Promise => { + InternalLog.log( + `Adding view attribute “${key}" with value “${JSON.stringify( + value + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttribute(key, { value }) + ); + }; + + removeViewAttribute = (key: string): Promise => { + InternalLog.log( + `Removing view attribute “${key}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttribute(key) + ); + }; + + addViewAttributes = (attributes: Attributes): Promise => { + InternalLog.log( + `Adding view attributes "${JSON.stringify( + attributes + )}” to RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.addViewAttributes(attributes) + ); + }; + + removeViewAttributes = (keys: string[]): Promise => { + InternalLog.log( + `Removing view attributes “${keys}" from RUM View`, + SdkVerbosity.DEBUG + ); + return bufferVoidNativeCall(() => + this.nativeRum.removeViewAttributes(keys) + ); + }; + addViewLoadingTime = (overwrite: boolean): Promise => { InternalLog.log( overwrite diff --git a/packages/core/src/rum/__tests__/DdRum.test.ts b/packages/core/src/rum/__tests__/DdRum.test.ts index 7e5fc24de..41b873fe9 100644 --- a/packages/core/src/rum/__tests__/DdRum.test.ts +++ b/packages/core/src/rum/__tests__/DdRum.test.ts @@ -1091,6 +1091,98 @@ describe('DdRum', () => { }); }); + describe('DdRum.addTiming', () => { + it('calls the native SDK when setting a timing', async () => { + // GIVEN + const timingName = 'testTiming'; + + // WHEN + await DdRum.addTiming(timingName); + + // THEN + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledTimes(1); + expect(NativeModules.DdRum.addTiming).toHaveBeenCalledWith( + timingName + ); + }); + }); + + describe('DdRum.addViewAttribute', () => { + it('calls the native SDK when setting a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + const value = { test: 'attribute' }; + + // WHEN + + await DdRum.addViewAttribute(key, value); + + // THEN + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttribute + ).toHaveBeenCalledWith(key, { value }); + }); + }); + + describe('DdRum.removViewAttribute', () => { + it('calls the native SDK when removing a view attribute', async () => { + // GIVEN + const key = 'testAttribute'; + + // WHEN + await DdRum.removeViewAttribute(key); + + // THEN + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttribute + ).toHaveBeenCalledWith(key); + }); + }); + + describe('DdRum.addViewAttributes', () => { + it('calls the native SDK when setting view attributes', async () => { + // GIVEN + const attributes = { + test: 'attribute' + }; + + // WHEN + await DdRum.addViewAttributes(attributes); + + // THEN + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.addViewAttributes + ).toHaveBeenCalledWith(attributes); + }); + }); + + describe('DdRum.removViewAttributes', () => { + it('calls the native SDK when removing view attributes', async () => { + // GIVEN + const keysToDelete = ['test1', 'test2']; + + // WHEN + await DdRum.removeViewAttributes(keysToDelete); + + // THEN + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledTimes(1); + expect( + NativeModules.DdRum.removeViewAttributes + ).toHaveBeenCalledWith(keysToDelete); + }); + }); + describe('DdRum.addAction', () => { test('uses given context when context is valid', async () => { const context = { diff --git a/packages/core/src/rum/types.ts b/packages/core/src/rum/types.ts index fc8d07c02..3def7f0e6 100644 --- a/packages/core/src/rum/types.ts +++ b/packages/core/src/rum/types.ts @@ -4,6 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ +import type { Attributes } from '../sdk/AttributesSingleton/types'; import type { ErrorSource } from '../types'; import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext'; @@ -148,6 +149,31 @@ export type DdRumType = { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: unknown): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Attributes): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view. diff --git a/packages/core/src/specs/NativeDdRum.ts b/packages/core/src/specs/NativeDdRum.ts index f6f7b3daa..e31f5b925 100644 --- a/packages/core/src/specs/NativeDdRum.ts +++ b/packages/core/src/specs/NativeDdRum.ts @@ -136,6 +136,31 @@ export interface Spec extends TurboModule { */ addTiming(name: string): Promise; + /** + * Adds a custom attribute to the active RUM View. It will be propagated to all future RUM events associated with the active View. + * @param key: key for this view attribute. + * @param value: value for this attribute. + */ + addViewAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the active RUM View. + * @param key: key for the attribute to be removed from the view. + */ + removeViewAttribute(key: string): Promise; + + /** + * Adds multiple attributes to the active RUM View. They will be propagated to all future RUM events associated with the active View. + * @param attributes: key/value object containing all attributes to be added to the view. + */ + addViewAttributes(attributes: Object): Promise; + + /** + * Removes multiple attributes from the active RUM View. + * @param keys: keys for the attributes to be removed from the view. + */ + removeViewAttributes(keys: string[]): Promise; + /** * Adds the loading time of the view to the active view. * It is calculated as the difference between the current time and the start time of the view.