diff --git a/developer-extension/tsconfig.json b/developer-extension/tsconfig.json index 5c0efe0ed0..47c3948608 100644 --- a/developer-extension/tsconfig.json +++ b/developer-extension/tsconfig.json @@ -6,7 +6,7 @@ "module": "ES6", "target": "esnext", "jsx": "react", - "lib": ["ES2019", "DOM"], + "lib": ["ES2020", "DOM"], "types": ["chrome", "react", "react-dom"] } } diff --git a/packages/core/src/domain/console/consoleObservable.ts b/packages/core/src/domain/console/consoleObservable.ts index e55b81e3d2..3a20794256 100644 --- a/packages/core/src/domain/console/consoleObservable.ts +++ b/packages/core/src/domain/console/consoleObservable.ts @@ -4,6 +4,8 @@ import { mergeObservables, Observable } from '../../tools/observable' import { find, jsonStringify } from '../../tools/utils' import { ConsoleApiName } from '../../tools/display' import { callMonitored } from '../../tools/monitor' +import { sanitize } from '../../tools/sanitize' +import { isExperimentalFeatureEnabled } from '../configuration' export interface ConsoleLog { message: string @@ -68,10 +70,10 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: function formatConsoleParameters(param: unknown) { if (typeof param === 'string') { - return param + return isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(param) : param } if (param instanceof Error) { return formatErrorMessage(computeStackTrace(param)) } - return jsonStringify(param, undefined, 2) + return jsonStringify(isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(param) : param, undefined, 2) } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b6bdb359b8..c32a1d9dbe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -56,6 +56,7 @@ export * from './tools/display' export * from './tools/urlPolyfill' export * from './tools/timeUtils' export * from './tools/utils' +export * from './tools/sanitize' export * from './tools/createEventRateLimiter' export * from './tools/browserDetection' export { sendToExtension } from './tools/sendToExtension' diff --git a/packages/core/src/tools/contextManager.ts b/packages/core/src/tools/contextManager.ts index 3755025dc6..907977821a 100644 --- a/packages/core/src/tools/contextManager.ts +++ b/packages/core/src/tools/contextManager.ts @@ -1,5 +1,7 @@ +import { isExperimentalFeatureEnabled } from '../domain/configuration' import { computeBytesCount, deepClone, jsonStringify } from './utils' import type { Context, ContextValue } from './context' +import { sanitize } from './sanitize' export type ContextManager = ReturnType @@ -38,12 +40,12 @@ export function createContextManager(computeBytesCountImpl = computeBytesCount) getContext: () => deepClone(context), setContext: (newContext: Context) => { - context = deepClone(newContext) + context = isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(newContext) : deepClone(newContext) bytesCountCache = undefined }, setContextProperty: (key: string, property: any) => { - context[key] = deepClone(property) + context[key] = isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(property) : deepClone(property) bytesCountCache = undefined }, diff --git a/packages/core/src/tools/error.ts b/packages/core/src/tools/error.ts index 888c2febba..54c276d524 100644 --- a/packages/core/src/tools/error.ts +++ b/packages/core/src/tools/error.ts @@ -1,6 +1,8 @@ +import { isExperimentalFeatureEnabled } from '../domain/configuration' import type { StackTrace } from '../domain/tracekit' import { computeStackTrace } from '../domain/tracekit' import { callMonitored } from './monitor' +import { sanitize } from './sanitize' import type { ClocksState } from './timeUtils' import { jsonStringify, noop } from './utils' @@ -68,13 +70,14 @@ export function computeRawError({ handling, }: RawErrorParams): RawError { if (!stackTrace || (stackTrace.message === undefined && !(originalError instanceof Error))) { + const sanitizedError = isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(originalError) : originalError return { startClocks, source, handling, - originalError, - message: `${nonErrorPrefix} ${jsonStringify(originalError)!}`, - stack: NO_ERROR_STACK_PRESENT_MESSAGE, + originalError: sanitizedError, + message: `${nonErrorPrefix} ${jsonStringify(sanitizedError)!}`, + stack: 'No stack, consider using an instance of Error', handlingStack, type: stackTrace && stackTrace.name, } diff --git a/packages/core/src/tools/limitModification.spec.ts b/packages/core/src/tools/limitModification.spec.ts index de5661c34d..d01e30b527 100644 --- a/packages/core/src/tools/limitModification.spec.ts +++ b/packages/core/src/tools/limitModification.spec.ts @@ -1,3 +1,4 @@ +import { resetExperimentalFeatures, updateExperimentalFeatures } from '../domain/configuration' import type { Context } from './context' import { limitModification } from './limitModification' @@ -155,4 +156,17 @@ describe('limitModification', () => { foo: { bar: 'qux' }, }) }) + + it('should call sanitize on newly provided values', () => { + updateExperimentalFeatures(['sanitize_inputs']) + const object: Context = { bar: { baz: 42 } } + + const modifier = (candidate: any) => { + candidate.bar.self = candidate.bar + } + + limitModification(object, ['bar'], modifier) + expect(() => JSON.stringify(object)).not.toThrowError() + resetExperimentalFeatures() + }) }) diff --git a/packages/core/src/tools/limitModification.ts b/packages/core/src/tools/limitModification.ts index 2b328cfe72..b16a4129de 100644 --- a/packages/core/src/tools/limitModification.ts +++ b/packages/core/src/tools/limitModification.ts @@ -1,4 +1,6 @@ +import { isExperimentalFeatureEnabled } from '../domain/configuration' import type { Context } from './context' +import { sanitize } from './sanitize' import { deepClone, getType } from './utils' /** @@ -18,7 +20,7 @@ export function limitModification( const originalType = getType(originalValue) const newType = getType(newValue) if (newType === originalType) { - set(object, path, newValue) + set(object, path, isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(newValue) : newValue) } else if (originalType === 'object' && (newType === 'undefined' || newType === 'null')) { set(object, path, {}) } diff --git a/packages/core/src/tools/sanitize.spec.ts b/packages/core/src/tools/sanitize.spec.ts new file mode 100644 index 0000000000..59a3e761b3 --- /dev/null +++ b/packages/core/src/tools/sanitize.spec.ts @@ -0,0 +1,244 @@ +import { isIE } from './browserDetection' +import { display } from './display' +import { sanitize } from './sanitize' + +describe('sanitize', () => { + it('should deep clone an object', () => { + const obj = { a: 1, b: { c: 42 } } + const clone = sanitize(obj) + + expect(clone).toEqual(obj) + expect(clone).not.toBe(obj) + }) + + it('should survive an undefined input', () => { + const obj = undefined + expect(sanitize(obj)).toBe(undefined) + }) + + describe('simple types handling', () => { + it('should handle numbers', () => { + expect(sanitize(42)).toBe(42) + }) + + it('should handle strings', () => { + expect(sanitize('test')).toBe('test') + }) + + it('should handle functions', () => { + function testFunction() { + return true + } + if (isIE()) { + // IE does not provide access to function name + expect(sanitize(testFunction)).toBe('[Function] unknown') + } else { + expect(sanitize(testFunction)).toBe('[Function] testFunction') + } + }) + + it('should handle bigint', () => { + const bigIntFunction: (val: number) => any = (window as any).BigInt + if (typeof bigIntFunction === 'function') { + const bigint = bigIntFunction(2) + expect(sanitize(bigint)).toEqual('[BigInt] 2') + } else { + pending('BigInt is not supported on this browser') + } + }) + + it('shoud handle symbols', () => { + const symbolFunction: (description: string) => any = (window as any).Symbol + if (typeof symbolFunction === 'function') { + const symbol = symbolFunction('description') + expect(sanitize(symbol)).toEqual('[Symbol] description') + } else { + pending('Symbol is not supported on this browser') + } + }) + }) + + describe('objects handling', () => { + it('should serialize a Date as a string', () => { + const date = new Date('2022-12-12') + expect(sanitize(date)).toBe('2022-12-12T00:00:00.000Z') + }) + + it('should not traverse instrumented DOM nodes', () => { + const node = document.createElement('div') + ;(node as any).__hiddenProp = { value: 42 } + + expect(sanitize(node)).toBe('[HTMLDivElement]') + }) + + it('should serialize events', () => { + let event: CustomEvent + if (isIE()) { + event = document.createEvent('CustomEvent') + event.initCustomEvent('MyEvent', false, false, {}) + } else { + event = new CustomEvent('MyEvent') + } + + expect(sanitize(event)).toEqual({ + isTrusted: false, + }) + }) + + it('should serialize objects like maps as a string', () => { + const map = new Map([ + ['a', 13], + ['b', 37], + ]) + if (isIE()) { + // IE does not distinguish maps, weakmaps, sets... from generic objects + expect(sanitize(map)).toEqual({}) + } else { + expect(sanitize(map)).toBe('[Map]') + } + }) + + it('should survive when toStringTag throws', () => { + if (isIE()) { + pending('IE does not support Symbols') + } + + class CannotSerialize { + get [Symbol.toStringTag]() { + throw Error('Cannot serialize') + } + } + const cannotSerialize = new CannotSerialize() + + expect(sanitize(cannotSerialize)).toEqual('[Unserializable]') + }) + }) + + describe('arrays handling', () => { + // JSON.stringify ignores properties on arrays - We replicate the behavior + it('should ignore non-numerical properties on arrays', () => { + const arr = [1, 2, 3, 4] + ;(arr as any)['test'] = 'test' + + expect(sanitize(arr)).toEqual([1, 2, 3, 4]) + }) + }) + + describe('circular references handling', () => { + it('should remove circular references', () => { + const obj: any = { a: 42 } + obj.self = obj + + expect(sanitize(obj)).toEqual({ a: 42, self: '[Reference seen at $]' }) + }) + + it('should remove deep circular references', () => { + const obj: any = {} + obj.toto = { inner: obj } + + expect(sanitize(obj)).toEqual({ toto: { inner: '[Reference seen at $]' } }) + }) + + it('should remove circular references between two branches in a tree', () => { + const a: any = {} + const b: any = {} + a.link = b + b.link = a + const obj = { a, b } + + expect(sanitize(obj)).toEqual({ a: { link: '[Reference seen at $.b]' }, b: { link: '[Reference seen at $.a]' } }) + }) + + it('should replace already visited objects with a json path', () => { + const inner = [1] + const obj = { a: inner, b: inner } + + expect(sanitize(obj)).toEqual({ a: [1], b: '[Reference seen at $.a]' }) + }) + + it('should create an understandable path for visited objects in arrays', () => { + const inner = { a: 42 } + const arr = [inner, inner] + + expect(sanitize(arr)).toEqual([{ a: 42 }, '[Reference seen at $.0]']) + }) + }) + + describe('toJson functions handling', () => { + it('should use toJSON functions if available on root object', () => { + const toJSON = jasmine.createSpy('toJSON', () => 'Specific').and.callThrough() + const obj = { a: 1, b: 2, toJSON } + + expect(sanitize(obj)).toEqual('Specific') + expect(toJSON).toHaveBeenCalledTimes(1) + }) + + it('should use toJSON functions if available on nested objects', () => { + const toJSON = jasmine.createSpy('toJSON', () => ({ d: 4 })).and.callThrough() + const obj = { a: 1, b: 2, c: { a: 3, toJSON } } + + expect(sanitize(obj)).toEqual({ a: 1, b: 2, c: { d: 4 } }) + expect(toJSON).toHaveBeenCalledTimes(1) + }) + + it('should switch to the proper container type after applying toJSON', () => { + const obj = { a: 42, toJSON: () => [42] } + expect(sanitize(obj)).toEqual([42]) + }) + + it('should not use toJSON methods added to arrays and objects prototypes', () => { + const toJSONArray = jasmine.createSpy('toJSONArray', () => 'Array').and.callThrough() + const toJSONObject = jasmine.createSpy('toJSONObject', () => 'Object').and.callThrough() + ;(Array.prototype as any).toJSON = toJSONArray + ;(Object.prototype as any).toJSON = toJSONObject + + const arr = [{ a: 1, b: 2 }] + expect(sanitize(arr)).toEqual([{ a: 1, b: 2 }]) + expect(toJSONArray).toHaveBeenCalledTimes(0) + expect(toJSONObject).toHaveBeenCalledTimes(0) + delete (Array.prototype as any).toJSON + delete (Object.prototype as any).toJSON + }) + + it('should survive a faulty toJSON', () => { + const faulty = () => { + throw new Error('') + } + const obj = { b: 42, toJSON: faulty } + + // Since toJSON throws, sanitize falls back to serialize property by property + if (isIE()) { + // IE does not provide access to function name + expect(sanitize(obj)).toEqual({ b: 42, toJSON: '[Function] unknown' }) + } else { + expect(sanitize(obj)).toEqual({ b: 42, toJSON: '[Function] faulty' }) + } + }) + }) + + describe('maxSize verification', () => { + it('should return nothing if a simple type is over max size ', () => { + const displaySpy = spyOn(display, 'warn') + const str = 'A not so long string...' + + expect(sanitize(str, 5)).toBe(undefined) + expect(displaySpy).toHaveBeenCalled() + }) + + it('should stop cloning if an object container type reaches max size', () => { + const displaySpy = spyOn(display, 'warn') + const obj = { a: 'abc', b: 'def', c: 'ghi' } // Length of 31 after JSON.stringify + const sanitized = sanitize(obj, 21) + expect(sanitized).toEqual({ a: 'abc', b: 'def' }) // Length of 21 after JSON.stringify + expect(displaySpy).toHaveBeenCalled() + }) + + it('should stop cloning if an array container type reaches max size', () => { + const displaySpy = spyOn(display, 'warn') + const obj = [1, 2, 3, 4] // Length of 9 after JSON.stringify + const sanitized = sanitize(obj, 5) + expect(sanitized).toEqual([1, 2]) // Length of 5 after JSON.stringify + expect(displaySpy).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/core/src/tools/sanitize.ts b/packages/core/src/tools/sanitize.ts new file mode 100644 index 0000000000..2af0399f8d --- /dev/null +++ b/packages/core/src/tools/sanitize.ts @@ -0,0 +1,242 @@ +import type { Context, ContextArray, ContextValue } from './context' +import { display } from './display' +import type { ObjectWithToJsonMethod } from './utils' +import { detachToJsonMethod, ONE_KIBI_BYTE } from './utils' + +// eslint-disable-next-line @typescript-eslint/ban-types +type PrimitivesAndFunctions = string | number | boolean | undefined | null | symbol | bigint | Function +type ExtendedContextValue = PrimitivesAndFunctions | object | ExtendedContext | ExtendedContextArray +type ExtendedContext = { [key: string]: ExtendedContextValue } +type ExtendedContextArray = ExtendedContextValue[] + +type ContainerElementToProcess = { + source: ExtendedContextArray | ExtendedContext + target: ContextArray | Context + path: string +} + +// The maximum size of a single event is 256KiB. By default, we ensure that user-provided data +// going through sanitize fits inside our events, while leaving room for other contexts, metadata, ... +const SANITIZE_DEFAULT_MAX_CHARACTER_COUNT = 220 * ONE_KIBI_BYTE + +// Symbol for the root element of the JSONPath used for visited objects +const JSON_PATH_ROOT_ELEMENT = '$' + +// When serializing (using JSON.stringify) a key of an object, { key: 42 } gets wrapped in quotes as "key". +// With the separator (:), we need to add 3 characters to the count. +const KEY_DECORATION_LENGTH = 3 + +/** + * Ensures user-provided data is 'safe' for the SDK + * - Deep clones data + * - Removes cyclic references + * - Transforms unserializable types to a string representation + * + * LIMITATIONS: + * - Size is in characters, not byte count (may differ according to character encoding) + * - Size does not take into account indentation that can be applied to JSON.stringify + * - Non-numerical properties of Arrays are ignored. Same behavior as JSON.stringify + * + * @param source User-provided data meant to be serialized using JSON.stringify + * @param maxCharacterCount Maximum number of characters allowed in serialized form + */ +export function sanitize(source: string, maxCharacterCount?: number): string | undefined +export function sanitize(source: Context, maxCharacterCount?: number): Context +export function sanitize(source: unknown, maxCharacterCount?: number): ContextValue +export function sanitize(source: unknown, maxCharacterCount = SANITIZE_DEFAULT_MAX_CHARACTER_COUNT) { + // Unbind any toJSON function we may have on [] or {} prototypes + const restoreObjectPrototypeToJson = detachToJsonMethod(Object.prototype) + const restoreArrayPrototypeToJson = detachToJsonMethod(Array.prototype) + + // Initial call to sanitizeProcessor - will populate containerQueue if source is an Array or a plain Object + const containerQueue: ContainerElementToProcess[] = [] + const visitedObjectsWithPath = new WeakMap() + const sanitizedData = sanitizeProcessor( + source as ExtendedContextValue, + JSON_PATH_ROOT_ELEMENT, + undefined, + containerQueue, + visitedObjectsWithPath + ) + let accumulatedCharacterCount = JSON.stringify(sanitizedData)?.length || 0 + if (accumulatedCharacterCount > maxCharacterCount) { + warnOverCharacterLimit(maxCharacterCount, 'discarded', source) + return undefined + } + + while (containerQueue.length > 0 && accumulatedCharacterCount < maxCharacterCount) { + const containerToProcess = containerQueue.shift()! + let separatorLength = 0 // 0 for the first element, 1 for subsequent elements + + // Arrays and Objects have to be handled distinctly to ensure + // we do not pick up non-numerical properties from Arrays + if (Array.isArray(containerToProcess.source)) { + for (let key = 0; key < containerToProcess.source.length; key++) { + const targetData = sanitizeProcessor( + containerToProcess.source[key], + containerToProcess.path, + key, + containerQueue, + visitedObjectsWithPath + ) + accumulatedCharacterCount += JSON.stringify(targetData).length + separatorLength + if (accumulatedCharacterCount > maxCharacterCount) { + warnOverCharacterLimit(maxCharacterCount, 'truncated', source) + break + } + separatorLength = 1 + ;(containerToProcess.target as ContextArray)[key] = targetData + } + } else { + for (const key in containerToProcess.source) { + if (Object.prototype.hasOwnProperty.call(containerToProcess.source, key)) { + const targetData = sanitizeProcessor( + containerToProcess.source[key], + containerToProcess.path, + key, + containerQueue, + visitedObjectsWithPath + ) + accumulatedCharacterCount += + JSON.stringify(targetData).length + separatorLength + key.length + KEY_DECORATION_LENGTH + if (accumulatedCharacterCount > maxCharacterCount) { + warnOverCharacterLimit(maxCharacterCount, 'truncated', source) + break + } + separatorLength = 1 + ;(containerToProcess.target as Context)[key] = targetData + } + } + } + } + + // Rebind detached toJSON functions + restoreObjectPrototypeToJson() + restoreArrayPrototypeToJson() + + return sanitizedData +} + +/** + * Internal function to factorize the process common to the + * initial call to sanitize, and iterations for Arrays and Objects + * + */ +function sanitizeProcessor( + source: ExtendedContextValue, + parentPath: string, + key: string | number | undefined, + queue: ContainerElementToProcess[], + visitedObjectsWithPath: WeakMap +) { + // Start by handling toJSON, as we want to sanitize its output + const sourceToSanitize = tryToApplyToJSON(source) + + if (!sourceToSanitize || typeof sourceToSanitize !== 'object') { + return sanitizePrimitivesAndFunctions(sourceToSanitize) + } + + const sanitizedSource = sanitizeObjects(sourceToSanitize) + if (sanitizedSource !== '[Object]' && sanitizedSource !== '[Array]') { + return sanitizedSource + } + + // Handle potential cyclic references + // We need to use source as sourceToSanitize could be a reference to a new object + // At this stage, we know the source is an object type + const sourceAsObject = source as object + if (visitedObjectsWithPath.has(sourceAsObject)) { + return `[Reference seen at ${visitedObjectsWithPath.get(sourceAsObject)!}]` + } + + // Add processed source to queue + const currentPath = key !== undefined ? `${parentPath}.${key}` : parentPath + const target = Array.isArray(sourceToSanitize) ? ([] as ContextArray) : ({} as Context) + visitedObjectsWithPath.set(sourceAsObject, currentPath) + queue.push({ source: sourceToSanitize as ExtendedContext | ExtendedContextArray, target, path: currentPath }) + + return target +} + +/** + * Handles sanitization of simple, non-object types + * + */ +function sanitizePrimitivesAndFunctions(value: PrimitivesAndFunctions) { + // BigInt cannot be serialized by JSON.stringify(), convert it to a string representation + if (typeof value === 'bigint') { + return `[BigInt] ${value.toString()}` + } + // Functions cannot be serialized by JSON.stringify(). Moreover, if a faulty toJSON is present, it needs to be converted + // so it won't prevent stringify from serializing later + if (typeof value === 'function') { + return `[Function] ${value.name || 'unknown'}` + } + // JSON.stringify() does not serialize symbols. + if (typeof value === 'symbol') { + // symbol.description is part of ES2019+ + type symbolWithDescription = symbol & { description: string } + return `[Symbol] ${(value as symbolWithDescription).description || value.toString()}` + } + + return value +} + +/** + * Handles sanitization of object types + * + * LIMITATIONS + * - If a class defines a toStringTag Symbol, it will fall in the catch-all method and prevent enumeration of properties. + * To avoid this, a toJSON method can be defined. + * - IE11 does not return a distinct type for objects such as Map, WeakMap, ... These objects will pass through and their + * properties enumerated if any. + * + */ +function sanitizeObjects(value: object) { + try { + // Handle events - Keep a simple implementation to avoid breaking changes + if (value instanceof Event) { + return { + isTrusted: value.isTrusted, + } + } + + // Handle all remaining object types in a generic way + const result = Object.prototype.toString.call(value) + const match = result.match(/\[object (.*)\]/) + if (match && match[1]) { + return `[${match[1]}]` + } + } catch { + // If the previous serialization attempts failed, and we cannot convert using + // Object.prototype.toString, declare the value unserializable + } + return '[Unserializable]' +} + +/** + * Checks if a toJSON function exists and tries to execute it + * + */ +function tryToApplyToJSON(value: ExtendedContextValue) { + const object = value as ObjectWithToJsonMethod + if (object && typeof object.toJSON === 'function') { + try { + return object.toJSON() as ExtendedContextValue + } catch { + // If toJSON fails, we continue by trying to serialize the value manually + } + } + + return value +} + +/** + * Helper function to display the warning when the accumulated character count is over the limit + */ +function warnOverCharacterLimit(maxCharacterCount: number, changeType: 'discarded' | 'truncated', source: unknown) { + display.warn( + `The data provided has been ${changeType} as it is over the limit of ${maxCharacterCount} characters:`, + source + ) +} diff --git a/packages/core/src/tools/utils.ts b/packages/core/src/tools/utils.ts index b6ccb8896d..df2d97b1af 100644 --- a/packages/core/src/tools/utils.ts +++ b/packages/core/src/tools/utils.ts @@ -153,10 +153,10 @@ export function jsonStringify( } } -interface ObjectWithToJsonMethod { - toJSON: unknown +export interface ObjectWithToJsonMethod { + toJSON?: () => unknown } -function detachToJsonMethod(value: object) { +export function detachToJsonMethod(value: object) { const object = value as ObjectWithToJsonMethod const objectToJson = object.toJSON if (objectToJson) { diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index 1ff3c1c1af..315b491b87 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,5 +1,6 @@ import type { Context, InitConfiguration, User } from '@datadog/browser-core' import { + isExperimentalFeatureEnabled, assign, BoundedBuffer, createContextManager, @@ -11,6 +12,7 @@ import { timeStampNow, checkUser, sanitizeUser, + sanitize, } from '@datadog/browser-core' import type { LogsInitConfiguration } from '../domain/configuration' import { validateAndBuildLogsConfiguration } from '../domain/configuration' @@ -118,10 +120,10 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { createLogger: monitor((name: string, conf: LoggerConfiguration = {}) => { customLoggers[name] = new Logger( (...params) => handleLogStrategy(...params), - name, + isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(name) : name, conf.handler, conf.level, - conf.context + isExperimentalFeatureEnabled('sanitize_inputs') ? (sanitize(conf.context) as object) : conf.context ) return customLoggers[name]! diff --git a/packages/logs/src/domain/logger.ts b/packages/logs/src/domain/logger.ts index 24471f4b52..d4cc43fbaa 100644 --- a/packages/logs/src/domain/logger.ts +++ b/packages/logs/src/domain/logger.ts @@ -1,5 +1,6 @@ import type { Context } from '@datadog/browser-core' import { + isExperimentalFeatureEnabled, clocksNow, computeRawError, ErrorHandling, @@ -11,6 +12,7 @@ import { createContextManager, ErrorSource, monitored, + sanitize, } from '@datadog/browser-core' import type { LogsEvent } from '../logsEvent.types' @@ -79,11 +81,18 @@ export class Logger { } } + const sanitizedMessageContext = ( + isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(messageContext) : deepClone(messageContext) + ) as Context + const context = errorContext - ? (combine({ error: errorContext }, messageContext) as Context) - : (deepClone(messageContext) as Context) + ? (combine({ error: errorContext }, sanitizedMessageContext) as Context) + : sanitizedMessageContext - this.handleLogStrategy({ message, context, status }, this) + this.handleLogStrategy( + { message: isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(message)! : message, context, status }, + this + ) } debug(message: string, messageContext?: object, error?: Error) { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index c0e75c72b2..9d21e103eb 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -1,5 +1,6 @@ import type { Context, InitConfiguration, TimeStamp, RelativeTime, User } from '@datadog/browser-core' import { + isExperimentalFeatureEnabled, willSyntheticsInjectRum, assign, BoundedBuffer, @@ -17,6 +18,7 @@ import { areCookiesAuthorized, checkUser, sanitizeUser, + sanitize, } from '@datadog/browser-core' import type { LifeCycle } from '../domain/lifeCycle' import type { ViewContexts } from '../domain/contexts/viewContexts' @@ -199,8 +201,8 @@ export function makeRumPublicApi( addAction: monitor((name: string, context?: object) => { addActionStrategy({ - name, - context: deepClone(context as Context), + name: isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(name)! : name, + context: (isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(context) : deepClone(context)) as Context, startClocks: clocksNow(), type: ActionType.CUSTOM, }) @@ -210,16 +212,21 @@ export function makeRumPublicApi( const handlingStack = createHandlingStack() callMonitored(() => { addErrorStrategy({ - error, + error, // Do not sanitize error here, it is needed unserialized by computeRawError() handlingStack, - context: deepClone(context as Context), + context: (isExperimentalFeatureEnabled('sanitize_inputs') + ? sanitize(context) + : deepClone(context)) as Context, startClocks: clocksNow(), }) }) }, addTiming: monitor((name: string, time?: number) => { - addTimingStrategy(name, time as RelativeTime | TimeStamp | undefined) + addTimingStrategy( + isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(name)! : name, + time as RelativeTime | TimeStamp | undefined + ) }), setUser: monitor((newUser: User) => { @@ -250,7 +257,10 @@ export function makeRumPublicApi( * This feature is currently in beta. For more information see the full [feature flag tracking guide](https://docs.datadoghq.com/real_user_monitoring/feature_flag_tracking/). */ addFeatureFlagEvaluation: monitor((key: string, value: any) => { - addFeatureFlagEvaluationStrategy(key, value) + addFeatureFlagEvaluationStrategy( + isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(key)! : key, + isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(value) : value + ) }), }) diff --git a/packages/rum-core/src/domain/contexts/featureFlagContext.ts b/packages/rum-core/src/domain/contexts/featureFlagContext.ts index 9869a2ec2f..27cf8575fa 100644 --- a/packages/rum-core/src/domain/contexts/featureFlagContext.ts +++ b/packages/rum-core/src/domain/contexts/featureFlagContext.ts @@ -2,7 +2,6 @@ import type { RelativeTime, ContextValue, Context } from '@datadog/browser-core' import { jsonStringify, computeBytesCount, - deepClone, noop, isExperimentalFeatureEnabled, SESSION_TIME_OUT_DELAY, @@ -69,7 +68,7 @@ export function startFeatureFlagContexts( addFeatureFlagEvaluation: (key: string, value: ContextValue) => { const currentContext = featureFlagContexts.find() if (currentContext) { - currentContext[key] = deepClone(value) + currentContext[key] = value bytesCountCache = undefined } },