diff --git a/benchmarks/src/testSetup/monitor.ts b/benchmarks/src/testSetup/monitor.ts index c7cc23473..93ea0fccd 100644 --- a/benchmarks/src/testSetup/monitor.ts +++ b/benchmarks/src/testSetup/monitor.ts @@ -4,8 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -import { DefaultTimeProvider, RumActionType } from "@datadog/mobile-react-native"; -import { ErrorSource } from "@datadog/mobile-react-native/lib/typescript/rum/types"; +import { DefaultTimeProvider, ErrorSource, RumActionType } from "@datadog/mobile-react-native"; import type { DdRumType, ResourceKind } from "@datadog/mobile-react-native/lib/typescript/rum/types"; import type { GestureResponderEvent } from "react-native/types"; @@ -72,4 +71,4 @@ export const Monitor: Pick { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) - DdSdkReactNative.setAttributes({campaign: "ad-network"}) + DdSdkReactNative.addAttributes({campaign: "ad-network"}) }); } diff --git a/packages/codepush/__mocks__/react-native.ts b/packages/codepush/__mocks__/react-native.ts index 046ced2f6..0c8189840 100644 --- a/packages/codepush/__mocks__/react-native.ts +++ b/packages/codepush/__mocks__/react-native.ts @@ -18,9 +18,18 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 0e85e65ae..24e3f80c7 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -27,9 +27,18 @@ actualRN.NativeModules.DdSdk = { clearUserInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttribute: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, + removeAttribute: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + addAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, + removeAttributes: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt index 3d344b203..c8dba8e25 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogSDKWrapper.kt @@ -88,11 +88,24 @@ internal class DatadogSDKWrapper : DatadogWrapper { override fun clearUserInfo() { Datadog.clearUserInfo() } + + override fun addRumGlobalAttribute(key: String, value: Any?) { + this.getRumMonitor().addAttribute(key, value) + } + + override fun removeRumGlobalAttribute(key: String) { + this.getRumMonitor().removeAttribute(key) + } override fun addRumGlobalAttributes(attributes: Map) { - val rumMonitor = this.getRumMonitor() for (attribute in attributes) { - rumMonitor.addAttribute(attribute.key, attribute.value) + this.addRumGlobalAttribute(attribute.key, attribute.value) + } + } + + override fun removeRumGlobalAttributes(keys: Array) { + for (key in keys) { + this.removeRumGlobalAttribute(key) } } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt index 49d606b35..d6395b18b 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DatadogWrapper.kt @@ -91,6 +91,21 @@ interface DatadogWrapper { */ fun clearUserInfo() + + /** Adds a global attribute. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addRumGlobalAttribute(key: String, value: Any?) + + /** + * Removes a global attribute. + * + * @param key: Key that identifies the attribute. + */ + fun removeRumGlobalAttribute(key: String) + /** * Adds global attributes. * @@ -98,6 +113,13 @@ interface DatadogWrapper { */ fun addRumGlobalAttributes(attributes: Map) + /** + * Removes global attributes. + * + * @param keys Keys linked to the attributes to be removed + */ + fun removeRumGlobalAttributes(keys: Array) + /** * Sets tracking consent. */ 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..ffca896e3 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 @@ -14,6 +14,7 @@ import com.datadog.android.rum.configuration.VitalsUpdateFrequency import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import java.util.Locale import java.util.concurrent.TimeUnit @@ -66,11 +67,35 @@ class DdSdkImplementation( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM. + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + val attributeValue = value.toMap()["value"] + datadog.addRumGlobalAttribute(key, attributeValue) + GlobalState.addAttribute(key, attributeValue) + promise.resolve(null) + } + + /** + * Removes an attribute from the global context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + fun removeAttribute(key: String, promise: Promise) { + datadog.removeRumGlobalAttribute(key) + GlobalState.removeAttribute(key) + promise.resolve(null) + } + + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. - * @param attributes The global context attributes. + * @param attributes: The global context attributes. */ - fun setAttributes(attributes: ReadableMap, promise: Promise) { + fun addAttributes(attributes: ReadableMap, promise: Promise) { datadog.addRumGlobalAttributes(attributes.toHashMap()) for ((k,v) in attributes.toHashMap()) { GlobalState.addAttribute(k, v) @@ -78,6 +103,26 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + fun removeAttributes(keys: ReadableArray, promise: Promise) { + val keysArray = mutableListOf() + for (i in 0 until keys.size()) { + val key: String = keys.getString(i) + keysArray.add(key) + } + val keysStringArray = keysArray.toTypedArray() + + datadog.removeRumGlobalAttributes(keysStringArray) + for (key in keysStringArray) { + GlobalState.removeAttribute(key) + } + promise.resolve(null) + } + /** * Set the user information. * @param userInfo The user object (use builtin attributes: 'id', 'email', 'name', and any custom 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..a9d430081 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 @@ -12,13 +12,14 @@ import com.facebook.react.bridge.LifecycleEventListener 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 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) { @@ -40,13 +41,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + override fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + override fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - override fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + override fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + override fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** diff --git a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt index 0ebdd37fb..958ba521b 100644 --- a/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt +++ b/packages/core/android/src/oldarch/kotlin/com/datadog/reactnative/DdSdk.kt @@ -12,6 +12,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 /** The entry point to initialize Datadog's features. */ @@ -66,13 +67,43 @@ class DdSdk( } /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + @ReactMethod + fun addAttribute(key: String, value: ReadableMap, promise: Promise) { + implementation.addAttribute(key, value, promise) + } + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + @ReactMethod + fun removeAttribute(key: String, promise: Promise) { + implementation.removeAttribute(key, promise) + } + + /** + * Adds a set of attributes to the global context that is attached with all future Logs, Spans and RUM * events. * @param attributes The global context attributes. */ @ReactMethod - fun setAttributes(attributes: ReadableMap, promise: Promise) { - implementation.setAttributes(attributes, promise) + fun addAttributes(attributes: ReadableMap, promise: Promise) { + implementation.addAttributes(attributes, promise) + } + + /** + * Removes a set of attributes from the global context that is attached with all future Logs, Spans and RUM + * events. + * @param keys: They keys associated with the attributes to be removed. + */ + @ReactMethod + fun removeAttributes(keys: ReadableArray, promise: Promise) { + implementation.removeAttributes(keys, promise) } /** 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 4abf9b981..88f373e30 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 @@ -40,6 +40,7 @@ import com.datadog.tools.unit.setStaticValue import com.datadog.tools.unit.toReadableArray import com.datadog.tools.unit.toReadableJavaOnlyMap import com.datadog.tools.unit.toReadableMap +import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap @@ -78,7 +79,6 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder -import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -2966,28 +2966,96 @@ internal class DdSdkTest { } @Test - fun `𝕄 set RUM attributes 𝕎 setAttributes`( + fun `M set Rum attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + verify(mockDatadog).addRumGlobalAttribute(key, value) + } + + @Test + fun `M set GlobalState attribute W addAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // When + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + } + + @Test + fun `M remove Rum attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttribute(key) + } + + @Test + fun `M remove GlobalState attribute W removeAttribute`( + @StringForgery(type = StringForgeryType.NUMERICAL) key: String, + @StringForgery(type = StringForgeryType.ASCII) value: String + ) { + // Given + val attributeMap = JavaOnlyMap().apply { + putString("value", value) + } + testedBridgeSdk.addAttribute(key, attributeMap, mockPromise) + assertThat(GlobalState.globalAttributes).containsEntry(key, value) + + // When + testedBridgeSdk.removeAttribute(key, mockPromise) + + // Then + assertThat(GlobalState.globalAttributes).doesNotContainEntry(key, value) + } + + @Test + fun `𝕄 set RUM attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then verify(mockDatadog).addRumGlobalAttributes(customAttributes) } @Test - fun `𝕄 set GlobalState attributes 𝕎 setAttributes`( + fun `𝕄 set GlobalState attributes 𝕎 addAttributes`( @MapForgery( key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) ) customAttributes: Map ) { // When - testedBridgeSdk.setAttributes(customAttributes.toReadableMap(), mockPromise) + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) // Then customAttributes.forEach { (k, v) -> @@ -2995,6 +3063,46 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 remove RUM attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + verify(mockDatadog).removeRumGlobalAttributes(customAttributes.keys.toTypedArray()) + } + + @Test + fun `𝕄 remve GlobalState attributes 𝕎 removeAttributes`( + @MapForgery( + key = AdvancedForgery(string = [StringForgery(StringForgeryType.NUMERICAL)]), + value = AdvancedForgery(string = [StringForgery(StringForgeryType.ASCII)]) + ) customAttributes: Map + ) { + // Given + testedBridgeSdk.addAttributes(customAttributes.toReadableMap(), mockPromise) + verify(mockDatadog).addRumGlobalAttributes(customAttributes) + + // When + val keys = customAttributes.keys.toReadableArray() + testedBridgeSdk.removeAttributes(keys, mockPromise) + + // Then + customAttributes.forEach { (k, v) -> + assertThat(GlobalState.globalAttributes).doesNotContainEntry(k, v) + } + } + @Test fun `𝕄 build Granted consent 𝕎 buildTrackingConsent {granted}`(forge: Forge) { // When diff --git a/packages/core/ios/Sources/AnyEncodable.swift b/packages/core/ios/Sources/AnyEncodable.swift index 39821af87..7fac7bb3b 100644 --- a/packages/core/ios/Sources/AnyEncodable.swift +++ b/packages/core/ios/Sources/AnyEncodable.swift @@ -14,18 +14,25 @@ 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 +} + /// Casts `Any` value to `Encodable` by preserving its type information. private func castByPreservingTypeInformation(attributeValue: Any) -> Encodable? { switch attributeValue { diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 98a228f76..7129d7af1 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -30,11 +30,33 @@ + (void)initFromNative { [self initialize:configuration resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(setAttributes, withAttributes:(NSDictionary*)attributes +RCT_EXPORT_METHOD(addAttribute:(NSString*) key + withValue:(NSDictionary*) value + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self addAttribute:key value:value resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(removeAttribute:(NSString*) key + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttribute:key resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(addAttributes, withAttributes:(NSDictionary*)attributes withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { - [self setAttributes:attributes resolve:resolve reject:reject]; + [self addAttributes:attributes resolve:resolve reject:reject]; +} + +RCT_REMAP_METHOD(removeAttributes, withKeys:(NSArray *)keys + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self removeAttributes:keys resolve:resolve reject:reject]; } RCT_REMAP_METHOD(setUserInfo, withUserInfo:(NSDictionary*)userInfo @@ -135,8 +157,20 @@ - (void)initialize:(NSDictionary *)configuration resolve:(RCTPromiseResolveBlock [self.ddSdkImplementation initializeWithConfiguration:configuration resolve:resolve reject:reject]; } -- (void)setAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.ddSdkImplementation setAttributesWithAttributes:attributes resolve:resolve reject:reject]; +- (void)addAttribute:(NSString *)key value:(NSDictionary *)value resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributeWithKey:key value:value resolve:resolve reject:reject]; +} + +- (void)removeAttribute:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributeWithKey:key resolve:resolve reject:reject]; +} + +- (void)addAttributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation addAttributesWithAttributes:attributes resolve:resolve reject:reject]; +} + +- (void)removeAttributes:(NSArray *)keys resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation removeAttributesWithKeys:keys resolve:resolve reject:reject]; } - (void)setTrackingConsent:(NSString *)trackingConsent resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 9d057d158..16adb50f2 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -69,14 +69,43 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + + @objc + public func addAttribute(key: AttributeKey, value: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + if let attributeValue = value.object(forKey: "value") { + let castedValue = castValueToSwift(attributeValue) + RUMMonitorProvider().addAttribute(forKey: key, value: castedValue) + GlobalState.addAttribute(forKey: key, value: castedValue) + } + + resolve(nil) + } + + @objc + public func removeAttribute(key: AttributeKey, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttribute(forKey: key) + GlobalState.removeAttribute(key: key) + + resolve(nil) + } @objc - public func setAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func addAttributes(attributes: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { 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 removeAttributes(keys: [AttributeKey], resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + RUMMonitorProvider().removeAttributes(forKeys: keys) + for (key) in keys { + GlobalState.removeAttribute(key: key) + } resolve(nil) } diff --git a/packages/core/ios/Sources/GlobalState.swift b/packages/core/ios/Sources/GlobalState.swift index b932803a1..a758bf0ef 100644 --- a/packages/core/ios/Sources/GlobalState.swift +++ b/packages/core/ios/Sources/GlobalState.swift @@ -15,7 +15,7 @@ internal struct GlobalState { } internal static func removeAttribute(key: String) { - GlobalState.globalAttributes.removeValue(forKey: key) + GlobalState.globalAttributes[key] = nil } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index efbf57b96..adbb57da9 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,7 +651,7 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } - + func testClearUserInfo() throws { let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -704,12 +704,12 @@ class DdSdkTests: XCTestCase { } else { XCTFail("extra-info-4 is not of expected type or value") } - + bridge.clearUserInfo(resolve: mockResolve, reject: mockReject) - + ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() userInfo = try XCTUnwrap(ddContext.userInfo) - + XCTAssertEqual(userInfo.id, nil) XCTAssertEqual(userInfo.name, nil) XCTAssertEqual(userInfo.email, nil) @@ -719,7 +719,59 @@ class DdSdkTests: XCTestCase { XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) } - func testSettingAttributes() { + func testRemovingAttribute() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-1", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + + bridge.removeAttribute(key: "attribute-2", resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + + GlobalState.globalAttributes.removeAll() + } + + func testAddingAttributes() { let rumMonitorMock = MockRUMMonitor() let bridge = DdSdkImplementation( mainDispatchQueue: DispatchQueueMock(), @@ -734,7 +786,7 @@ class DdSdkTests: XCTestCase { reject: mockReject ) - bridge.setAttributes( + bridge.addAttributes( attributes: NSDictionary( dictionary: [ "attribute-1": 123, @@ -757,6 +809,66 @@ class DdSdkTests: XCTestCase { GlobalState.globalAttributes.removeAll() } + func testRemovingAttributes() { + let rumMonitorMock = MockRUMMonitor() + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { rumMonitorMock }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.addAttributes( + attributes: NSDictionary( + dictionary: [ + "attribute-1": 123, + "attribute-2": "abc", + "attribute-3": true, + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, 123) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, "abc") + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes( + keys: ["attribute-1", "attribute-2"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, true) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, true) + + bridge.removeAttributes(keys: ["attribute-3"], resolve: mockResolve, reject: mockReject) + + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(rumMonitorMock.addedAttributes["attribute-3"] as? Bool, nil) + + XCTAssertEqual(GlobalState.globalAttributes["attribute-1"] as? Int64, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-2"] as? String, nil) + XCTAssertEqual(GlobalState.globalAttributes["attribute-3"] as? Bool, nil) + + GlobalState.globalAttributes.removeAll() + + } + func testBuildLongTaskThreshold() { let configuration: DdSdkConfiguration = .mockAny(nativeLongTaskThresholdMs: 2500) diff --git a/packages/core/ios/Tests/MockRUMMonitor.swift b/packages/core/ios/Tests/MockRUMMonitor.swift index f0fa03364..3a882ed47 100644 --- a/packages/core/ios/Tests/MockRUMMonitor.swift +++ b/packages/core/ios/Tests/MockRUMMonitor.swift @@ -38,14 +38,20 @@ internal class MockRUMMonitor: RUMMonitorProtocol { addedAttributes[key] = value } - func removeAttribute(forKey key: DatadogInternal.AttributeKey) {} + func removeAttribute(forKey key: DatadogInternal.AttributeKey) { + addedAttributes.removeValue(forKey: key) + } func addAttributes(_ attributes: [DatadogInternal.AttributeKey : any DatadogInternal.AttributeValue]) { - // Not implemented + for (key, value) in attributes { + addAttribute(forKey: key, value: value) + } } func removeAttributes(forKeys keys: [DatadogInternal.AttributeKey]) { - // Not implemented + for key in keys { + removeAttribute(forKey: key) + } } var debug: Bool diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index 8e154c4cd..c49d13f48 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -36,7 +36,16 @@ module.exports = { clearUserInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), - setAttributes: jest + addAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttribute: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + addAttributes: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), + removeAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), setTrackingConsent: jest diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 1838542df..8360a695b 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -175,20 +175,61 @@ export class DdSdkReactNative { ); }; + /** + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + static addAttribute = async ( + key: string, + value: unknown + ): Promise => { + InternalLog.log( + `Adding attribute ${JSON.stringify(value)} for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttribute(key, { value }); + AttributesSingleton.getInstance().addAttribute(key, value); + }; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + static removeAttribute = async (key: string): Promise => { + InternalLog.log( + `Removing attribute for key ${key}`, + SdkVerbosity.DEBUG + ); + await DdSdk.removeAttribute(key); + AttributesSingleton.getInstance().removeAttribute(key); + }; + /** * Adds a set of attributes to the global context attached with all future Logs, Spans and RUM events. - * To remove an attribute, set it to `undefined` in a call to `setAttributes`. * @param attributes: The global context attributes. * @returns a Promise. */ - // eslint-disable-next-line @typescript-eslint/ban-types - static setAttributes = async (attributes: Attributes): Promise => { + static addAttributes = async (attributes: Attributes): Promise => { + InternalLog.log( + `Adding attributes ${JSON.stringify(attributes)}`, + SdkVerbosity.DEBUG + ); + await DdSdk.addAttributes(attributes); + AttributesSingleton.getInstance().addAttributes(attributes); + }; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + static removeAttributes = async (keys: string[]): Promise => { InternalLog.log( - `Setting attributes ${JSON.stringify(attributes)}`, + `Removing attributes for keys ${JSON.stringify(keys)}`, SdkVerbosity.DEBUG ); - await DdSdk.setAttributes(attributes); - AttributesSingleton.getInstance().setAttributes(attributes); + await DdSdk.removeAttributes(keys); + AttributesSingleton.getInstance().removeAttributes(keys); }; /** diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 57ff5d0ed..5e6f8c447 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -62,7 +62,7 @@ beforeEach(async () => { GlobalState.instance.isInitialized = false; DdSdkReactNative['wasAutoInstrumented'] = false; NativeModules.DdSdk.initialize.mockClear(); - NativeModules.DdSdk.setAttributes.mockClear(); + NativeModules.DdSdk.addAttributes.mockClear(); NativeModules.DdSdk.setTrackingConsent.mockClear(); NativeModules.DdSdk.onRUMSessionStarted.mockClear(); @@ -1045,24 +1045,80 @@ describe('DdSdkReactNative', () => { }); }); - describe('setAttributes', () => { - it('calls SDK method when setAttributes', async () => { + describe('addAttribute', () => { + it('calls SDK method when addAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + + // WHEN + + await DdSdkReactNative.addAttribute(key, value); + + // THEN + expect(DdSdk.addAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttribute).toHaveBeenCalledWith(key, { value }); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + value + ); + }); + }); + + describe('removeAttribute', () => { + it('calls SDK method when removeAttribute', async () => { + // GIVEN + const key = 'foo'; + const value = 'bar'; + await DdSdkReactNative.addAttribute(key, value); + + // WHEN + await DdSdkReactNative.removeAttribute(key); + + // THEN + expect(DdSdk.removeAttribute).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttribute).toHaveBeenCalledWith(key); + expect(AttributesSingleton.getInstance().getAttribute(key)).toEqual( + undefined + ); + }); + }); + + describe('addAttributes', () => { + it('calls SDK method when addAttributes', async () => { // GIVEN const attributes = { foo: 'bar' }; // WHEN - await DdSdkReactNative.setAttributes(attributes); + await DdSdkReactNative.addAttributes(attributes); // THEN - expect(DdSdk.setAttributes).toHaveBeenCalledTimes(1); - expect(DdSdk.setAttributes).toHaveBeenCalledWith(attributes); + expect(DdSdk.addAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.addAttributes).toHaveBeenCalledWith(attributes); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ foo: 'bar' }); }); }); + describe('removeAttributes', () => { + it('calls SDK method when removeAttributes', async () => { + // GIVEN + const attributes = { foo: 'bar', baz: 'quux' }; + await DdSdkReactNative.addAttributes(attributes); + + // WHEN + await DdSdkReactNative.removeAttributes(['foo', 'baz']); + + // THEN + expect(DdSdk.removeAttributes).toHaveBeenCalledTimes(1); + expect(DdSdk.removeAttributes).toHaveBeenCalledWith(['foo', 'baz']); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual( + {} + ); + }); + }); + describe('setUserInfo', () => { it('calls SDK method when setUserInfo, and sets the user in UserProvider', async () => { // GIVEN diff --git a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts index a51bb6c99..ac92c2d32 100644 --- a/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts +++ b/packages/core/src/sdk/AttributesSingleton/AttributesSingleton.ts @@ -9,13 +9,37 @@ import type { Attributes } from './types'; class AttributesProvider { private attributes: Attributes = {}; - setAttributes = (attributes: Attributes) => { + addAttribute = (key: string, value: unknown) => { + const newAttributes = { ...this.attributes }; + newAttributes[key] = value; + this.attributes = newAttributes; + }; + + removeAttribute = (key: string) => { + const updatedAttributes = { ...this.attributes }; + delete updatedAttributes[key]; + this.attributes = updatedAttributes; + }; + + addAttributes = (attributes: Attributes) => { this.attributes = { ...this.attributes, ...attributes }; }; + removeAttributes = (keys: string[]) => { + const updated = { ...this.attributes }; + for (const k of keys) { + delete updated[k]; + } + this.attributes = updated; + }; + + getAttribute = (key: string): unknown | undefined => { + return this.attributes[key]; + }; + getAttributes = (): Attributes => { return this.attributes; }; diff --git a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts index 23fbe5ad7..90d1133b4 100644 --- a/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts +++ b/packages/core/src/sdk/AttributesSingleton/__tests__/AttributesSingleton.test.ts @@ -7,9 +7,12 @@ import { AttributesSingleton } from '../AttributesSingleton'; describe('AttributesSingleton', () => { - it('adds, returns and resets the user info', () => { - // Adding first attributes - AttributesSingleton.getInstance().setAttributes({ + beforeEach(() => { + AttributesSingleton.reset(); + }); + + it('adds, returns and resets the attributes', () => { + AttributesSingleton.getInstance().addAttributes({ appType: 'student', extraInfo: { loggedIn: true @@ -23,11 +26,8 @@ describe('AttributesSingleton', () => { } }); - // Removing and adding new attributes - AttributesSingleton.getInstance().setAttributes({ - appType: undefined, - newAttribute: false - }); + AttributesSingleton.getInstance().removeAttribute('appType'); + AttributesSingleton.getInstance().addAttribute('newAttribute', false); expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ newAttribute: false, @@ -41,4 +41,48 @@ describe('AttributesSingleton', () => { expect(AttributesSingleton.getInstance().getAttributes()).toEqual({}); }); + + it('addAttribute sets a single key and getAttribute returns it', () => { + AttributesSingleton.getInstance().addAttribute('userId', '123'); + expect(AttributesSingleton.getInstance().getAttribute('userId')).toBe( + '123' + ); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + userId: '123' + }); + }); + + it('removeAttribute removes a single key and leaves others intact', () => { + AttributesSingleton.getInstance().addAttributes({ + a: 1, + b: 2 + }); + + AttributesSingleton.getInstance().removeAttribute('a'); + + expect( + AttributesSingleton.getInstance().getAttribute('a') + ).toBeUndefined(); + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + b: 2 + }); + }); + + it('removeAttributes removes multiple keys (missing keys are ignored)', () => { + AttributesSingleton.getInstance().addAttributes({ + keyToKeep: 'yes', + keyToRemove1: true, + keyToRemove2: false + }); + + AttributesSingleton.getInstance().removeAttributes([ + 'keyToRemove1', + 'keyToRemove2', + 'keyToIgnore' + ]); + + expect(AttributesSingleton.getInstance().getAttributes()).toEqual({ + keyToKeep: 'yes' + }); + }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index a2ce1120e..70401fe3c 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -26,10 +26,29 @@ export interface Spec extends TurboModule { initialize(configuration: Object): Promise; /** - * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. + * Adds a specific attribute to the global context attached with all future Logs, Spans and RUM. + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: Object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + + /** + * Adds the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: Object): Promise; + addAttributes(attributes: Object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Set the user information. diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bad19d429..c8d9821cc 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -83,11 +83,30 @@ export type DdSdkType = { */ initialize(configuration: DdSdkConfiguration): Promise; + /** + * Sets a specific attribute in the global context attached with all future Logs, Spans and RUM + * @param key: Key that identifies the attribute. + * @param value: Value linked to the attribute. + */ + addAttribute(key: string, value: object): Promise; + + /** + * Removes an attribute from the context attached with all future Logs, Spans and RUM events. + * @param key: They key associated with the attribute to be removed. + */ + removeAttribute(key: string): Promise; + /** * Sets the global context (set of attributes) attached with all future Logs, Spans and RUM events. * @param attributes: The global context attributes. */ - setAttributes(attributes: object): Promise; + addAttributes(attributes: object): Promise; + + /** + * Removes a set of attributes from the context attached with all future Logs, Spans and RUM events. + * @param keys: They keys associated with the attributes to be removed. + */ + removeAttributes(keys: string[]): Promise; /** * Sets the user information. diff --git a/packages/react-native-apollo-client/__mocks__/react-native.ts b/packages/react-native-apollo-client/__mocks__/react-native.ts index 046ced2f6..bbac607d3 100644 --- a/packages/react-native-apollo-client/__mocks__/react-native.ts +++ b/packages/react-native-apollo-client/__mocks__/react-native.ts @@ -18,9 +18,9 @@ actualRN.NativeModules.DdSdk = { initialize: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, - setAttributes: jest.fn().mockImplementation( + addAttributes: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) - ) as jest.MockedFunction, + ) as jest.MockedFunction, setTrackingConsent: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction,