From 15a539537c38ac25c9c0562839a761b2c25c71e3 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 19 Sep 2025 17:37:31 +0200 Subject: [PATCH] Expose clearUserInfo API --- packages/core/__mocks__/react-native.ts | 3 + .../datadog/reactnative/DatadogSDKWrapper.kt | 4 ++ .../com/datadog/reactnative/DatadogWrapper.kt | 5 ++ .../reactnative/DdSdkImplementation.kt | 10 ++- .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../kotlin/com/datadog/reactnative/DdSdk.kt | 8 +++ .../com/datadog/reactnative/DdSdkTest.kt | 11 +++ packages/core/ios/Sources/DdSdk.mm | 12 +++- .../ios/Sources/DdSdkImplementation.swift | 8 ++- packages/core/ios/Tests/DdSdkTests.swift | 67 +++++++++++++++++++ packages/core/jest/mock.js | 3 + packages/core/src/DdSdkReactNative.tsx | 10 +++ .../src/__tests__/DdSdkReactNative.test.tsx | 26 +++++++ .../UserInfoSingleton/UserInfoSingleton.ts | 4 ++ .../__tests__/UserInfoSingleton.test.ts | 55 ++++++++++++--- packages/core/src/specs/NativeDdSdk.ts | 5 ++ packages/core/src/types.tsx | 5 ++ 17 files changed, 230 insertions(+), 14 deletions(-) diff --git a/packages/core/__mocks__/react-native.ts b/packages/core/__mocks__/react-native.ts index 260fe68a7..0e85e65ae 100644 --- a/packages/core/__mocks__/react-native.ts +++ b/packages/core/__mocks__/react-native.ts @@ -24,6 +24,9 @@ actualRN.NativeModules.DdSdk = { addUserExtraInfo: jest.fn().mockImplementation( () => new Promise(resolve => resolve()) ) as jest.MockedFunction, + clearUserInfo: jest.fn().mockImplementation( + () => new Promise(resolve => resolve()) + ) as jest.MockedFunction, setAttributes: 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 141b995da..3d344b203 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 @@ -85,6 +85,10 @@ internal class DatadogSDKWrapper : DatadogWrapper { Datadog.addUserProperties(extraInfo) } + override fun clearUserInfo() { + Datadog.clearUserInfo() + } + override fun addRumGlobalAttributes(attributes: Map) { val rumMonitor = this.getRumMonitor() for (attribute in attributes) { 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 3ae3e6266..49d606b35 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 @@ -86,6 +86,11 @@ interface DatadogWrapper { extraInfo: Map ) + /** + * Clears the user information. + */ + fun clearUserInfo() + /** * Adds global attributes. * 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 54769fb8b..467d37335 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 @@ -102,7 +102,7 @@ class DdSdkImplementation( } /** - * Sets the user information. + * Sets the user extra information. * @param userExtraInfo: The additional information. (To set the id, name or email please user setUserInfo). */ fun addUserExtraInfo( @@ -114,6 +114,14 @@ class DdSdkImplementation( promise.resolve(null) } + /** + * Clears the user information. + */ + fun clearUserInfo(promise: Promise) { + datadog.clearUserInfo() + promise.resolve(null) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', 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 cfafffffe..4e4668a3e 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 @@ -67,6 +67,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + override fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', 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 97acb2ebf..0ebdd37fb 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 @@ -93,6 +93,14 @@ class DdSdk( implementation.addUserExtraInfo(extraInfo, promise) } + /** + * Clears the user information. + */ + @ReactMethod + fun clearUserInfo(promise: Promise) { + implementation.clearUserInfo(promise) + } + /** * Set the tracking consent regarding the data collection. * @param trackingConsent Consent, which can take one of the following values: 'pending', 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 9b0014b7e..4abf9b981 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 @@ -2954,6 +2954,17 @@ internal class DdSdkTest { } } + @Test + fun `𝕄 clear user info 𝕎 clearUserInfo()`() { + // When + testedBridgeSdk.clearUserInfo(mockPromise) + + // Then + argumentCaptor> { + verify(mockDatadog).clearUserInfo() + } + } + @Test fun `𝕄 set RUM attributes 𝕎 setAttributes`( @MapForgery( diff --git a/packages/core/ios/Sources/DdSdk.mm b/packages/core/ios/Sources/DdSdk.mm index 3ead770ac..98a228f76 100644 --- a/packages/core/ios/Sources/DdSdk.mm +++ b/packages/core/ios/Sources/DdSdk.mm @@ -51,6 +51,12 @@ + (void)initFromNative { [self addUserExtraInfo:extraInfo resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(clearUserInfo:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) +{ + [self clearUserInfo:resolve reject:reject]; +} + RCT_REMAP_METHOD(setTrackingConsent, withTrackingConsent:(NSString*)trackingConsent withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -81,7 +87,7 @@ + (void)initFromNative { [self consumeWebviewEvent:message resolve:resolve reject:reject]; } -RCT_REMAP_METHOD(clearAllData, withResolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearAllData:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { [self clearAllData:resolve reject:reject]; @@ -141,6 +147,10 @@ - (void)setUserInfo:(NSDictionary *)userInfo resolve:(RCTPromiseResolveBlock)res [self.ddSdkImplementation setUserInfoWithUserInfo:userInfo resolve:resolve reject:reject]; } +- (void)clearUserInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [self.ddSdkImplementation clearUserInfoWithResolve:resolve reject:reject]; +} + -(void)addUserExtraInfo:(NSDictionary *)extraInfo resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSdkImplementation addUserExtraInfoWithExtraInfo:extraInfo resolve:resolve reject:reject]; } diff --git a/packages/core/ios/Sources/DdSdkImplementation.swift b/packages/core/ios/Sources/DdSdkImplementation.swift index 5a4bbf9a5..9d057d158 100644 --- a/packages/core/ios/Sources/DdSdkImplementation.swift +++ b/packages/core/ios/Sources/DdSdkImplementation.swift @@ -102,7 +102,7 @@ public class DdSdkImplementation: NSObject { resolve(nil) } - + @objc public func addUserExtraInfo(extraInfo: NSDictionary, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { let castedExtraInfo = castAttributesToSwift(extraInfo) @@ -111,6 +111,12 @@ public class DdSdkImplementation: NSObject { resolve(nil) } + @objc + public func clearUserInfo(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + Datadog.clearUserInfo() + resolve(nil) + } + @objc public func setTrackingConsent(trackingConsent: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { Datadog.set(trackingConsent: (trackingConsent as NSString?).asTrackingConsent()) diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 555ce4549..efbf57b96 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -651,6 +651,73 @@ class DdSdkTests: XCTestCase { XCTFail("extra-info-4 is not of expected type or value") } } + + func testClearUserInfo() throws { + let bridge = DdSdkImplementation( + mainDispatchQueue: DispatchQueueMock(), + jsDispatchQueue: DispatchQueueMock(), + jsRefreshRateMonitor: JSRefreshRateMonitor(), + RUMMonitorProvider: { MockRUMMonitor() }, + RUMMonitorInternalProvider: { nil } + ) + bridge.initialize( + configuration: .mockAny(), + resolve: mockResolve, + reject: mockReject + ) + + bridge.setUserInfo( + userInfo: NSDictionary( + dictionary: [ + "id": "id_123", + "name": "John Doe", + "email": "john@doe.com", + "extraInfo": [ + "extra-info-1": 123, + "extra-info-2": "abc", + "extra-info-3": true, + "extra-info-4": [ + "nested-extra-info-1": 456 + ], + ], + ] + ), + resolve: mockResolve, + reject: mockReject + ) + + var ddContext = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + var userInfo = try XCTUnwrap(ddContext.userInfo) + + XCTAssertEqual(userInfo.id, "id_123") + XCTAssertEqual(userInfo.name, "John Doe") + XCTAssertEqual(userInfo.email, "john@doe.com") + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, 123) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, "abc") + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, true) + + if let extraInfo4Encodable = userInfo.extraInfo["extra-info-4"] + as? DatadogSDKReactNative.AnyEncodable, + let extraInfo4Dict = extraInfo4Encodable.value as? [String: Int] + { + XCTAssertEqual(extraInfo4Dict, ["nested-extra-info-1": 456]) + } 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) + XCTAssertEqual(userInfo.extraInfo["extra-info-1"] as? Int64, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-2"] as? String, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-3"] as? Bool, nil) + XCTAssertEqual(userInfo.extraInfo["extra-info-4"] as? [String: Int], nil) + } func testSettingAttributes() { let rumMonitorMock = MockRUMMonitor() diff --git a/packages/core/jest/mock.js b/packages/core/jest/mock.js index a8161295a..8e154c4cd 100644 --- a/packages/core/jest/mock.js +++ b/packages/core/jest/mock.js @@ -33,6 +33,9 @@ module.exports = { addUserExtraInfo: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), + clearUserInfo: jest + .fn() + .mockImplementation(() => new Promise(resolve => resolve())), setAttributes: jest .fn() .mockImplementation(() => new Promise(resolve => resolve())), diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 668ae09f3..1838542df 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -214,6 +214,16 @@ export class DdSdkReactNative { UserInfoSingleton.getInstance().setUserInfo(userInfo); }; + /** + * Clears the user information. + * @returns a Promise. + */ + static clearUserInfo = async (): Promise => { + InternalLog.log('Clearing user info', SdkVerbosity.DEBUG); + await DdSdk.clearUserInfo(); + UserInfoSingleton.getInstance().clearUserInfo(); + }; + /** * Set the user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo). diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index f9405aa51..57ff5d0ed 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -1112,6 +1112,32 @@ describe('DdSdkReactNative', () => { }); }); + describe('clearUserInfo', () => { + it('calls SDK method when clearUserInfo, and clears the user in UserProvider', async () => { + // GIVEN + const userInfo = { + id: 'id', + name: 'name', + email: 'email', + extraInfo: { + foo: 'bar' + } + }; + + await DdSdkReactNative.setUserInfo(userInfo); + + // WHEN + await DdSdkReactNative.clearUserInfo(); + + // THEN + expect(DdSdk.clearUserInfo).toHaveBeenCalledTimes(1); + expect(DdSdk.setUserInfo).toHaveBeenCalled(); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( + undefined + ); + }); + }); + describe('setTrackingConsent', () => { it('calls SDK method when setTrackingConsent', async () => { // GIVEN diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 26392d794..3ce23614b 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -16,6 +16,10 @@ class UserInfoProvider { getUserInfo = (): UserInfo | undefined => { return this.userInfo; }; + + clearUserInfo = () => { + this.userInfo = undefined; + }; } export class UserInfoSingleton { diff --git a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts index 1f7ae84e7..f8e7276d6 100644 --- a/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts +++ b/packages/core/src/sdk/UserInfoSingleton/__tests__/UserInfoSingleton.test.ts @@ -7,27 +7,60 @@ import { UserInfoSingleton } from '../UserInfoSingleton'; describe('UserInfoSingleton', () => { - it('sets, returns and resets the user info', () => { + beforeEach(() => { + UserInfoSingleton.reset(); + }); + + it('returns undefined by default', () => { + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('stores and returns user info after setUserInfo', () => { + const info = { + id: 'test', + email: 'user@mail.com', + extraInfo: { loggedIn: true } + }; + + UserInfoSingleton.getInstance().setUserInfo(info); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual(info); + }); + + it('clears user info with clearUserInfo', () => { UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + UserInfoSingleton.getInstance().clearUserInfo(); + + expect(UserInfoSingleton.getInstance().getUserInfo()).toBeUndefined(); + }); + + it('reset() replaces the provider and clears stored user info', () => { + const instanceBefore = UserInfoSingleton.getInstance(); + + UserInfoSingleton.getInstance().setUserInfo({ id: 'test', email: 'user@mail.com', - extraInfo: { - loggedIn: true - } + extraInfo: { loggedIn: true } }); UserInfoSingleton.reset(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( - undefined - ); + const instanceAfter = UserInfoSingleton.getInstance(); + + expect(instanceAfter).not.toBe(instanceBefore); + + expect(instanceAfter.getUserInfo()).toBeUndefined(); + }); + + it('getInstance returns the same provider between calls (singleton behavior)', () => { + const a = UserInfoSingleton.getInstance(); + const b = UserInfoSingleton.getInstance(); + + expect(a).toBe(b); }); }); diff --git a/packages/core/src/specs/NativeDdSdk.ts b/packages/core/src/specs/NativeDdSdk.ts index bbf2572ee..a2ce1120e 100644 --- a/packages/core/src/specs/NativeDdSdk.ts +++ b/packages/core/src/specs/NativeDdSdk.ts @@ -37,6 +37,11 @@ export interface Spec extends TurboModule { */ setUserInfo(user: Object): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add custom attributes to the current user information * @param extraInfo: The extraInfo object containing additionall custom attributes diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index fe1a5895c..bad19d429 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -98,6 +98,11 @@ export type DdSdkType = { */ setUserInfo(userInfo: UserInfo): Promise; + /** + * Clears the user information. + */ + clearUserInfo(): Promise; + /** * Add additional user information. * @param extraUserInfo: The additional information. (To set the id, name or email please user setUserInfo).