From 0ab07cb70ac32cdd92677e80dd2942e7770e2cb9 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Tue, 8 Nov 2022 10:18:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[RUMF-1409]=20Provide=20setUser=20a?= =?UTF-8?q?nd=20related=20functions=20for=20logs=20SDK=20(#1801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 shallow clone of user object * ✨ Add setUser and related functions to logs SDK * 📝 Updated user management documentation for logs SDK * 👌♻️ Mutualize checkUser --- packages/core/src/domain/user/index.ts | 2 + packages/core/src/domain/user/user.spec.ts | 35 +++++ packages/core/src/domain/user/user.ts | 32 ++++ packages/core/src/domain/user/user.types.ts | 6 + packages/core/src/index.ts | 1 + packages/logs/README.md | 102 ++++++++++++- packages/logs/src/boot/logsPublicApi.spec.ts | 145 ++++++++++++++++++- packages/logs/src/boot/logsPublicApi.ts | 26 +++- packages/logs/src/boot/startLogs.spec.ts | 1 + packages/logs/src/domain/assembly.spec.ts | 74 ++++++++++ packages/logs/src/domain/assembly.ts | 9 +- packages/logs/src/rawLogsEvent.types.ts | 3 +- packages/rum-core/src/boot/rumPublicApi.ts | 24 +-- packages/rum-core/src/domain/assembly.ts | 3 +- packages/rum-core/src/rawRumEvent.types.ts | 8 +- 15 files changed, 431 insertions(+), 40 deletions(-) create mode 100644 packages/core/src/domain/user/index.ts create mode 100644 packages/core/src/domain/user/user.spec.ts create mode 100644 packages/core/src/domain/user/user.ts create mode 100644 packages/core/src/domain/user/user.types.ts diff --git a/packages/core/src/domain/user/index.ts b/packages/core/src/domain/user/index.ts new file mode 100644 index 0000000000..d19ede1913 --- /dev/null +++ b/packages/core/src/domain/user/index.ts @@ -0,0 +1,2 @@ +export * from './user.types' +export * from './user' diff --git a/packages/core/src/domain/user/user.spec.ts b/packages/core/src/domain/user/user.spec.ts new file mode 100644 index 0000000000..9e9442a584 --- /dev/null +++ b/packages/core/src/domain/user/user.spec.ts @@ -0,0 +1,35 @@ +import { checkUser, sanitizeUser } from './user' +import type { User } from './user.types' + +describe('sanitize user function', () => { + it('should sanitize a user object', () => { + const obj = { id: 42, name: true, email: null } + const user = sanitizeUser(obj) + + expect(user).toEqual({ id: '42', name: 'true', email: 'null' }) + }) + + it('should not mutate the original data', () => { + const obj = { id: 42, name: 'test', email: null } + const user = sanitizeUser(obj) + + expect(user.id).toEqual('42') + expect(obj.id).toEqual(42) + }) +}) + +describe('check user function', () => { + it('should only accept valid user objects', () => { + const obj: any = { id: 42, name: true, email: null } // Valid, even though not sanitized + const user: User = { id: '42', name: 'John', email: 'john@doe.com' } + const undefUser: any = undefined + const nullUser: any = null + const invalidUser: any = 42 + + expect(checkUser(obj)).toBe(true) + expect(checkUser(user)).toBe(true) + expect(checkUser(undefUser)).toBe(false) + expect(checkUser(nullUser)).toBe(false) + expect(checkUser(invalidUser)).toBe(false) + }) +}) diff --git a/packages/core/src/domain/user/user.ts b/packages/core/src/domain/user/user.ts new file mode 100644 index 0000000000..d055f8e608 --- /dev/null +++ b/packages/core/src/domain/user/user.ts @@ -0,0 +1,32 @@ +import type { Context } from '../../tools/context' +import { display } from '../../tools/display' +import { assign, getType } from '../../tools/utils' +import type { User } from './user.types' + +/** + * Clone input data and ensure known user properties (id, name, email) + * are strings, as defined here: + * https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#user-related-attributes + */ +export function sanitizeUser(newUser: Context): Context { + // We shallow clone only to prevent mutation of user data. + const user = assign({}, newUser) + const keys = ['id', 'name', 'email'] + keys.forEach((key) => { + if (key in user) { + user[key] = String(user[key]) + } + }) + return user +} + +/** + * Simple check to ensure user is valid + */ +export function checkUser(newUser: User): boolean { + const isValid = getType(newUser) === 'object' + if (!isValid) { + display.error('Unsupported user:', newUser) + } + return isValid +} diff --git a/packages/core/src/domain/user/user.types.ts b/packages/core/src/domain/user/user.types.ts new file mode 100644 index 0000000000..e711b6ebc8 --- /dev/null +++ b/packages/core/src/domain/user/user.types.ts @@ -0,0 +1,6 @@ +export interface User { + id?: string | undefined + email?: string | undefined + name?: string | undefined + [key: string]: unknown +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7e74a1d419..44cb65c9ed 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -86,3 +86,4 @@ export { getSyntheticsTestId, getSyntheticsResultId, } from './domain/synthetics/syntheticsWorkerValues' +export { User, checkUser, sanitizeUser } from './domain/user' diff --git a/packages/logs/README.md b/packages/logs/README.md index 02f1fdb9d6..876b0bbc7c 100644 --- a/packages/logs/README.md +++ b/packages/logs/README.md @@ -94,7 +94,7 @@ To receive all logs and errors, load and configure the SDK at the beginning of t ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. ### TypeScript @@ -174,7 +174,7 @@ DD_LOGS.onReady(function () { window.DD_LOGS && DD_LOGS.logger.info('Button clicked', { name: 'buttonName', id: 123 }) ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. #### Results @@ -471,7 +471,7 @@ if (window.DD_LOGS) { } ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. ### Overwrite context @@ -570,7 +570,95 @@ window.DD_LOGS && DD_LOGS.clearGlobalContext() window.DD_LOGS && DD_LOGS.getGlobalContext() // => {} ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. + +#### User context + +The Datadog logs SDK provides convenient functions to associate a `User` with generated logs. + +- Set the user for all your loggers with the `setUser (newUser: User)` API. +- Add or modify a user property to all your loggers with the `setUserProperty (key: string, value: any)` API. +- Get the currently stored user with the `getUser ()` API. +- Remove a user property with the `removeUserProperty (key: string)` API. +- Clear all existing user properties with the `clearUser ()` API. + +**Note**: The user context is applied before the global context. Hence, every user property included in the global context will override the user context when generating logs. + +##### NPM + +For NPM, use: + +```javascript +import { datadogLogs } from '@datadog/browser-logs' + +datadogLogs.setUser({ id: '1234', name: 'John Doe', email: 'john@doe.com' }) +datadogLogs.setUserProperty('type', 'customer') +datadogLogs.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com', type: 'customer'} + +datadogLogs.removeUserProperty('type') +datadogLogs.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com'} + +datadogLogs.clearUser() +datadogLogs.getUser() // => {} +``` + +#### CDN async + +For CDN async, use: + +```javascript +DD_LOGS.onReady(function () { + DD_LOGS.setUser({ id: '1234', name: 'John Doe', email: 'john@doe.com' }) +}) + +DD_LOGS.onReady(function () { + DD_LOGS.setUserProperty('type', 'customer') +}) + +DD_LOGS.onReady(function () { + DD_LOGS.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com', type: 'customer'} +}) + +DD_LOGS.onReady(function () { + DD_LOGS.removeUserProperty('type') +}) + +DD_LOGS.onReady(function () { + DD_LOGS.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com'} +}) + +DD_LOGS.onReady(function () { + DD_LOGS.clearUser() +}) + +DD_LOGS.onReady(function () { + DD_LOGS.getUser() // => {} +}) +``` + +**Note:** Early API calls must be wrapped in the `DD_LOGS.onReady()` callback. This ensures the code only gets executed once the SDK is properly loaded. + +##### CDN sync + +For CDN sync, use: + +```javascript +window.DD_LOGS && DD_LOGS.setUser({ id: '1234', name: 'John Doe', email: 'john@doe.com' }) + +window.DD_LOGS && DD_LOGS.setUserProperty('type', 'customer') + +window.DD_LOGS && DD_LOGS.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com', type: 'customer'} + +window.DD_LOGS && DD_LOGS.removeUserProperty('type') + +window.DD_LOGS && DD_LOGS.getUser() // => {id: '1234', name: 'John Doe', email: 'john@doe.com'} + +window.DD_LOGS && DD_LOGS.clearUser() + +window.DD_LOGS && DD_LOGS.getUser() // => {} +``` + +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. #### Logger context @@ -617,7 +705,7 @@ window.DD_LOGS && DD_LOGS.setContext("{'env': 'staging'}") window.DD_LOGS && DD_LOGS.addContext('referrer', document.referrer) ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. ### Filter by status @@ -659,7 +747,7 @@ For CDN sync, use: window.DD_LOGS && DD_LOGS.logger.setLevel('') ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. ### Change the destination @@ -706,7 +794,7 @@ window.DD_LOGS && DD_LOGS.logger.setHandler('') window.DD_LOGS && DD_LOGS.logger.setHandler(['', '']) ``` -**Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. +**Note**: The `window.DD_LOGS` check prevents issues when a loading failure occurs with the SDK. ### Access internal context diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 86448d0b4b..0f941387d4 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -138,7 +138,7 @@ describe('logs entry', () => { }) it('should have the current date, view and global context', () => { - LOGS.addLoggerGlobalContext('foo', 'bar') + LOGS.setGlobalContextProperty('foo', 'bar') const getCommonContext = startLogs.calls.mostRecent().args[2] expect(getCommonContext()).toEqual({ @@ -147,6 +147,7 @@ describe('logs entry', () => { url: window.location.href, }, context: { foo: 'bar' }, + user: {}, }) }) }) @@ -325,5 +326,147 @@ describe('logs entry', () => { expect(LOGS.getInternalContext()?.session_id).toEqual(mockSessionId) }) }) + + describe('setUser', () => { + let logsPublicApi: LogsPublicApi + let displaySpy: jasmine.Spy<() => void> + + beforeEach(() => { + displaySpy = spyOn(display, 'error') + logsPublicApi = makeLogsPublicApi(startLogs) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) + }) + + it('should store user in common context', () => { + const user = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } } + logsPublicApi.setUser(user) + + const getCommonContext = startLogs.calls.mostRecent().args[2] + expect(getCommonContext().user).toEqual({ + email: 'qux', + foo: { bar: 'qux' }, + id: 'foo', + name: 'bar', + }) + }) + + it('should sanitize predefined properties', () => { + const user = { id: null, name: 2, email: { bar: 'qux' } } + logsPublicApi.setUser(user as any) + const getCommonContext = startLogs.calls.mostRecent().args[2] + expect(getCommonContext().user).toEqual({ + email: '[object Object]', + id: 'null', + name: '2', + }) + }) + + it('should clear a previously set user', () => { + const user = { id: 'foo', name: 'bar', email: 'qux' } + logsPublicApi.setUser(user) + logsPublicApi.clearUser() + + const getCommonContext = startLogs.calls.mostRecent().args[2] + expect(getCommonContext().user).toEqual({}) + }) + + it('should reject non object input', () => { + logsPublicApi.setUser(2 as any) + logsPublicApi.setUser(null as any) + logsPublicApi.setUser(undefined as any) + expect(displaySpy).toHaveBeenCalledTimes(3) + }) + }) + + describe('getUser', () => { + let logsPublicApi: LogsPublicApi + + beforeEach(() => { + logsPublicApi = makeLogsPublicApi(startLogs) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) + }) + + it('should return empty object if no user has been set', () => { + const userClone = logsPublicApi.getUser() + expect(userClone).toEqual({}) + }) + + it('should return a clone of the original object if set', () => { + const user = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } } + logsPublicApi.setUser(user) + const userClone = logsPublicApi.getUser() + const userClone2 = logsPublicApi.getUser() + + expect(userClone).not.toBe(user) + expect(userClone).not.toBe(userClone2) + expect(userClone).toEqual(user) + }) + }) + + describe('setUserProperty', () => { + const user = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } } + const addressAttribute = { city: 'Paris' } + let logsPublicApi: LogsPublicApi + + beforeEach(() => { + logsPublicApi = makeLogsPublicApi(startLogs) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) + }) + + it('should add attribute', () => { + logsPublicApi.setUser(user) + logsPublicApi.setUserProperty('address', addressAttribute) + const userClone = logsPublicApi.getUser() + + expect(userClone.address).toEqual(addressAttribute) + }) + + it('should not contain original reference to object', () => { + const userDetails: { [key: string]: any } = { name: 'john' } + logsPublicApi.setUser(user) + logsPublicApi.setUserProperty('userDetails', userDetails) + userDetails.DOB = '11/11/1999' + const userClone = logsPublicApi.getUser() + + expect(userClone.userDetails).not.toBe(userDetails) + }) + + it('should override attribute', () => { + logsPublicApi.setUser(user) + logsPublicApi.setUserProperty('foo', addressAttribute) + const userClone = logsPublicApi.getUser() + + expect(userClone).toEqual({ ...user, foo: addressAttribute }) + }) + + it('should sanitize properties', () => { + logsPublicApi.setUserProperty('id', 123) + logsPublicApi.setUserProperty('name', ['Adam', 'Smith']) + logsPublicApi.setUserProperty('email', { foo: 'bar' }) + const userClone = logsPublicApi.getUser() + + expect(userClone.id).toEqual('123') + expect(userClone.name).toEqual('Adam,Smith') + expect(userClone.email).toEqual('[object Object]') + }) + }) + + describe('removeUserProperty', () => { + let logsPublicApi: LogsPublicApi + + beforeEach(() => { + logsPublicApi = makeLogsPublicApi(startLogs) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) + }) + + it('should remove property', () => { + const user = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } } + + logsPublicApi.setUser(user) + logsPublicApi.removeUserProperty('foo') + const userClone = logsPublicApi.getUser() + expect(userClone.foo).toBeUndefined() + }) + }) }) }) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index b7a8739832..d1501b2868 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,4 +1,4 @@ -import type { InitConfiguration } from '@datadog/browser-core' +import type { Context, InitConfiguration, User } from '@datadog/browser-core' import { assign, BoundedBuffer, @@ -9,6 +9,8 @@ import { deepClone, canUseEventBridge, timeStampNow, + checkUser, + sanitizeUser, } from '@datadog/browser-core' import type { LogsInitConfiguration } from '../domain/configuration' import { validateAndBuildLogsConfiguration } from '../domain/configuration' @@ -33,6 +35,8 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { let isAlreadyInitialized = false const globalContextManager = createContextManager() + const userContextManager = createContextManager() + const customLoggers: { [name: string]: Logger | undefined } = {} let getInternalContextStrategy: StartLogsResult['getInternalContext'] = () => undefined @@ -56,7 +60,8 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { referrer: document.referrer, url: window.location.href, }, - context: globalContextManager.get(), + context: globalContextManager.getContext(), + user: userContextManager.getContext(), } } @@ -125,6 +130,23 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { getInitConfiguration: monitor(() => getInitConfigurationStrategy()), getInternalContext: monitor((startTime?: number | undefined) => getInternalContextStrategy(startTime)), + + setUser: monitor((newUser: User) => { + if (checkUser(newUser)) { + userContextManager.setContext(sanitizeUser(newUser as Context)) + } + }), + + getUser: monitor(userContextManager.getContext), + + setUserProperty: monitor((key, property) => { + const sanitizedProperty = sanitizeUser({ [key]: property })[key] + userContextManager.setContextProperty(key, sanitizedProperty) + }), + + removeUserProperty: monitor(userContextManager.removeContextProperty), + + clearUser: monitor(userContextManager.clearContext), }) function overrideInitConfigurationForBridge(initConfiguration: C): C { diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 2fd71307a3..dfd5b11eb2 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -33,6 +33,7 @@ const DEFAULT_MESSAGE = { status: StatusType.info, message: 'message' } const COMMON_CONTEXT = { view: { referrer: 'common_referrer', url: 'common_url' }, context: {}, + user: {}, } describe('logs', () => { diff --git a/packages/logs/src/domain/assembly.spec.ts b/packages/logs/src/domain/assembly.spec.ts index f1f77a2be1..dc11fd543e 100644 --- a/packages/logs/src/domain/assembly.spec.ts +++ b/packages/logs/src/domain/assembly.spec.ts @@ -25,6 +25,12 @@ const COMMON_CONTEXT: CommonContext = { url: 'url_from_common_context', }, context: { common_context_key: 'common_context_value' }, + user: {}, +} + +const COMMON_CONTEXT_WITH_USER: CommonContext = { + ...COMMON_CONTEXT, + user: { id: 'id', name: 'name', email: 'test@test.com' }, } describe('startLogsAssembly', () => { @@ -126,6 +132,7 @@ describe('startLogsAssembly', () => { url: 'url_from_saved_common_context', }, context: { foo: 'bar' }, + user: { email: 'test@test.com' }, } lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE, savedCommonContext }) @@ -270,6 +277,73 @@ describe('startLogsAssembly', () => { }) }) +describe('user management', () => { + const sessionManager: LogsSessionManager = { + findTrackedSession: () => (sessionIsTracked ? { id: SESSION_ID } : undefined), + } + + let sessionIsTracked: boolean + let lifeCycle: LifeCycle + let serverLogs: Array = [] + + const beforeSend: (event: LogsEvent) => void | boolean = noop + const mainLogger = new Logger(() => noop) + const configuration = { + ...validateAndBuildLogsConfiguration(initConfiguration)!, + beforeSend: (x: LogsEvent) => beforeSend(x), + } + + beforeEach(() => { + sessionIsTracked = true + lifeCycle = new LifeCycle() + lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (serverRumEvent) => serverLogs.push(serverRumEvent)) + }) + + afterEach(() => { + delete window.DD_RUM + serverLogs = [] + }) + + it('should not output usr key if user is not set', () => { + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, mainLogger, noop) + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) + expect(serverLogs[0].usr).toBeUndefined() + }) + + it('should include user data when user has been set', () => { + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT_WITH_USER, mainLogger, noop) + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) + expect(serverLogs[0].usr).toEqual({ + id: 'id', + name: 'name', + email: 'test@test.com', + }) + }) + + it('should prioritize global context over user context', () => { + const globalContextWithUser = { + ...COMMON_CONTEXT_WITH_USER, + context: { + ...COMMON_CONTEXT.context, + usr: { + id: 4242, + name: 'solution', + }, + }, + } + startLogsAssembly(sessionManager, configuration, lifeCycle, () => globalContextWithUser, mainLogger, noop) + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) + expect(serverLogs[0].usr).toEqual({ + id: 4242, + name: 'solution', + email: 'test@test.com', + }) + }) +}) + describe('logs limitation', () => { let clock: Clock const sessionManager = { diff --git a/packages/logs/src/domain/assembly.ts b/packages/logs/src/domain/assembly.ts index de68c8b981..ba9467a136 100644 --- a/packages/logs/src/domain/assembly.ts +++ b/packages/logs/src/domain/assembly.ts @@ -8,6 +8,7 @@ import { combine, createEventRateLimiter, getRelativeTime, + isEmptyObject, } from '@datadog/browser-core' import type { CommonContext } from '../rawLogsEvent.types' import type { LogsConfiguration } from './configuration' @@ -44,7 +45,13 @@ export function startLogsAssembly( const commonContext = savedCommonContext || getCommonContext() const log = combine( - { service: configuration.service, session_id: session.id, view: commonContext.view }, + { + service: configuration.service, + session_id: session.id, + // Insert user first to allow overrides from global context + usr: !isEmptyObject(commonContext.user) ? commonContext.user : undefined, + view: commonContext.view, + }, commonContext.context, getRUMInternalContext(startTime), rawLogsEvent, diff --git a/packages/logs/src/rawLogsEvent.types.ts b/packages/logs/src/rawLogsEvent.types.ts index 1d268d2532..c41e5f7e0a 100644 --- a/packages/logs/src/rawLogsEvent.types.ts +++ b/packages/logs/src/rawLogsEvent.types.ts @@ -1,4 +1,4 @@ -import type { Context, ErrorSource, TimeStamp } from '@datadog/browser-core' +import type { Context, ErrorSource, TimeStamp, User } from '@datadog/browser-core' import type { StatusType } from './domain/logger' export type RawLogsEvent = @@ -65,4 +65,5 @@ export interface CommonContext { url: string } context: Context + user: User } diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index f54dddd426..af94101d32 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -1,4 +1,4 @@ -import type { Context, InitConfiguration, TimeStamp, RelativeTime } from '@datadog/browser-core' +import type { Context, InitConfiguration, TimeStamp, RelativeTime, User } from '@datadog/browser-core' import { willSyntheticsInjectRum, assign, @@ -15,11 +15,13 @@ import { createHandlingStack, canUseEventBridge, areCookiesAuthorized, + checkUser, + sanitizeUser, } from '@datadog/browser-core' import type { LifeCycle } from '../domain/lifeCycle' import type { ViewContexts } from '../domain/contexts/viewContexts' import type { RumSessionManager } from '../domain/rumSessionManager' -import type { User, ReplayStats } from '../rawRumEvent.types' +import type { ReplayStats } from '../rawRumEvent.types' import { ActionType } from '../rawRumEvent.types' import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration' import { validateAndBuildRumConfiguration } from '../domain/configuration' @@ -222,9 +224,7 @@ export function makeRumPublicApi( }), setUser: monitor((newUser: User) => { - if (typeof newUser !== 'object' || !newUser) { - display.error('Unsupported user:', newUser) - } else { + if (checkUser(newUser)) { userContextManager.setContext(sanitizeUser(newUser as Context)) } }), @@ -249,20 +249,6 @@ export function makeRumPublicApi( }) return rumPublicApi - function sanitizeUser(newUser: Context) { - const shallowClonedUser = assign(newUser, {}) - if ('id' in shallowClonedUser) { - shallowClonedUser.id = String(shallowClonedUser.id) - } - if ('name' in shallowClonedUser) { - shallowClonedUser.name = String(shallowClonedUser.name) - } - if ('email' in shallowClonedUser) { - shallowClonedUser.email = String(shallowClonedUser.email) - } - return shallowClonedUser - } - function canHandleSession(initConfiguration: RumInitConfiguration): boolean { if (!areCookiesAuthorized(buildCookieOptions(initConfiguration))) { display.warn('Cookies are not authorized, we will not send any data.') diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index eca99152f1..d6ba4d89ec 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -1,4 +1,4 @@ -import type { Context, RawError, EventRateLimiter } from '@datadog/browser-core' +import type { Context, RawError, EventRateLimiter, User } from '@datadog/browser-core' import { combine, isEmptyObject, @@ -17,7 +17,6 @@ import type { RawRumLongTaskEvent, RawRumResourceEvent, RumContext, - User, } from '../rawRumEvent.types' import { RumEventType } from '../rawRumEvent.types' import type { RumEvent } from '../rumEvent.types' diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 0700a93ee4..2134ed6b3f 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -7,6 +7,7 @@ import type { ServerDuration, TimeStamp, RawErrorCause, + User, } from '@datadog/browser-core' import type { RumSessionPlan } from './domain/rumSessionManager' @@ -240,13 +241,6 @@ export interface RumContext { } } -export interface User { - id?: string | undefined - email?: string | undefined - name?: string | undefined - [key: string]: unknown -} - export interface CommonContext { user: User context: Context