From 6fceea28b66281853725a23c9b65f43ba15d493f Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Wed, 20 May 2026 18:22:03 +0100 Subject: [PATCH 1/2] Allow addUserExtraInfo to be called without prior setUserInfo The JS-side guard in addUserExtraInfo required setUserInfo to be called first, which prevented setting custom user attributes without providing a user ID. This was inconsistent with the native iOS and Android SDKs, which allow addUserExtraInfo to be called independently. --- packages/core/src/DdSdkReactNative.tsx | 37 +++++++------------ .../src/__tests__/DdSdkReactNative.test.tsx | 24 ++++++++++-- .../UserInfoSingleton/UserInfoSingleton.ts | 17 ++++++++- .../core/src/sdk/UserInfoSingleton/types.ts | 2 +- packages/core/src/types.tsx | 4 +- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 881d42fbf..9c5987f56 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -41,6 +41,7 @@ import { BufferSingleton } from './sdk/DatadogProvider/Buffer/BufferSingleton'; import { NativeDdSdk } from './sdk/DdSdkInternal'; import { GlobalState } from './sdk/GlobalState/GlobalState'; import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton'; +import type { UserInfo } from './sdk/UserInfoSingleton/types'; import { adaptLongTaskThreshold } from './utils/longTasksUtils'; import { version as sdkVersion } from './version'; @@ -258,12 +259,17 @@ export class DdSdkReactNative { * @param extraInfo: Additional information. * @returns a Promise. */ - static setUserInfo = async (userInfo: { - id: string; - name?: string; - email?: string; - extraInfo?: Record; - }): Promise => { + static setUserInfo = async ( + userInfo: UserInfo & { id: string } + ): Promise => { + if (typeof userInfo.id !== 'string' || userInfo.id.length === 0) { + InternalLog.log( + 'setUserInfo requires a valid user ID. Please provide a non-empty string as the id field.', + SdkVerbosity.WARN + ); + return; + } + InternalLog.log( `Setting user ${JSON.stringify(userInfo)}`, SdkVerbosity.DEBUG @@ -296,25 +302,8 @@ export class DdSdkReactNative { SdkVerbosity.DEBUG ); - const userInfo = UserInfoSingleton.getInstance().getUserInfo(); - if (!userInfo) { - InternalLog.log( - 'Skipped adding User Extra Info: User Info is currently undefined. A user ID must be set before adding extra info. Please call setUserInfo() first.', - SdkVerbosity.WARN - ); - - return; - } - const updatedUserInfo = { - ...userInfo, - extraInfo: { - ...userInfo.extraInfo, - ...extraUserInfo - } - }; - await NativeDdSdk.addUserExtraInfo(extraUserInfo); - UserInfoSingleton.getInstance().setUserInfo(updatedUserInfo); + UserInfoSingleton.getInstance().addUserExtraInfo(extraUserInfo); }; /** diff --git a/packages/core/src/__tests__/DdSdkReactNative.test.tsx b/packages/core/src/__tests__/DdSdkReactNative.test.tsx index 2ec1d2233..4a3902d32 100644 --- a/packages/core/src/__tests__/DdSdkReactNative.test.tsx +++ b/packages/core/src/__tests__/DdSdkReactNative.test.tsx @@ -1298,6 +1298,24 @@ describe('DdSdkReactNative', () => { } }); }); + + it('calls SDK method when addUserExtraInfo without prior setUserInfo', async () => { + // GIVEN + const extraInfo = { testId: 'abc123' }; + + // WHEN + await DdSdkReactNative.addUserExtraInfo(extraInfo); + + // THEN + expect(NativeDdSdk.addUserExtraInfo).toHaveBeenCalledWith( + extraInfo + ); + expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual({ + extraInfo: { + testId: 'abc123' + } + }); + }); }); describe('clearUserInfo', () => { @@ -1320,9 +1338,9 @@ describe('DdSdkReactNative', () => { // THEN expect(NativeDdSdk.clearUserInfo).toHaveBeenCalledTimes(1); expect(NativeDdSdk.setUserInfo).toHaveBeenCalled(); - expect(UserInfoSingleton.getInstance().getUserInfo()).toEqual( - undefined - ); + expect( + UserInfoSingleton.getInstance().getUserInfo() + ).toBeUndefined(); }); }); diff --git a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts index 2408fdbf2..a33e494e9 100644 --- a/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts +++ b/packages/core/src/sdk/UserInfoSingleton/UserInfoSingleton.ts @@ -11,9 +11,22 @@ import type { UserInfo } from './types'; class UserInfoProvider { private userInfo: UserInfo | undefined = undefined; - setUserInfo = (userInfo: UserInfo) => { + setUserInfo = (userInfo: UserInfo & { id: string }) => { + if (typeof userInfo.id !== 'string' || userInfo.id.length === 0) { + return; + } this.userInfo = userInfo; - setCachedUserId(this.userInfo.id); + setCachedUserId(userInfo.id); + }; + + addUserExtraInfo = (extraInfo: Record) => { + this.userInfo = { + ...this.userInfo, + extraInfo: { + ...this.userInfo?.extraInfo, + ...extraInfo + } + }; }; getUserInfo = (): UserInfo | undefined => { diff --git a/packages/core/src/sdk/UserInfoSingleton/types.ts b/packages/core/src/sdk/UserInfoSingleton/types.ts index dd14eb150..0c4975b75 100644 --- a/packages/core/src/sdk/UserInfoSingleton/types.ts +++ b/packages/core/src/sdk/UserInfoSingleton/types.ts @@ -5,7 +5,7 @@ */ export type UserInfo = { - readonly id: string; + readonly id?: string; readonly name?: string; readonly email?: string; readonly extraInfo?: Record; diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 4d3fda013..1f4811113 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -53,7 +53,7 @@ export type DdSdkType = { * @param email: The user email. * @param extraInfo: Additional information. */ - setUserInfo(userInfo: UserInfo): Promise; + setUserInfo(userInfo: UserInfo & { id: string }): Promise; /** * Clears the user information. @@ -128,7 +128,7 @@ export type DdTraceType = { // Core export type UserInfo = { - id: string; + id?: string; name?: string; email?: string; extraInfo?: object; From 81d29b16c8a6b3c31bfd224dc5e8f6f7e2525ee4 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Thu, 21 May 2026 10:25:41 +0100 Subject: [PATCH 2/2] Clarify setUserInfo JSDoc: id is required, use addUserExtraInfo for custom attributes --- packages/core/src/DdSdkReactNative.tsx | 9 ++++----- packages/core/src/types.tsx | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index 9c5987f56..4bc448d2a 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -252,11 +252,10 @@ export class DdSdkReactNative { }; /** - * Sets the user information. - * @param id: A mandatory unique user identifier (relevant to your business domain). - * @param name: The user name or alias. - * @param email: The user email. - * @param extraInfo: Additional information. + * Sets the user information. Requires a user ID — setting user properties + * like name or email implies a known user, so an ID must be provided. + * To add custom attributes without setting a user ID, use {@link addUserExtraInfo} instead. + * @param userInfo: The user object (id is required, name, email and extraInfo are optional). * @returns a Promise. */ static setUserInfo = async ( diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 1f4811113..49dc64bc2 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -47,11 +47,10 @@ export type DdSdkType = { removeAttributes(keys: string[]): Promise; /** - * Sets the user information. - * @param id: A unique user identifier (relevant to your business domain) - * @param name: The user name or alias. - * @param email: The user email. - * @param extraInfo: Additional information. + * Sets the user information. Requires a user ID — setting user properties + * like name or email implies a known user, so an ID must be provided. + * To add custom attributes without setting a user ID, use {@link addUserExtraInfo} instead. + * @param userInfo: The user object (id is required, name, email and extraInfo are optional). */ setUserInfo(userInfo: UserInfo & { id: string }): Promise;