{"version":3,"file":"sanitize.mjs","names":[],"sources":["../../../src/tools/serialisation/sanitize.ts"],"sourcesContent":["import { display } from '../display'\nimport { ONE_KIBI_BYTE } from '../utils/byteUtils'\nimport type { Context, ContextArray, ContextValue } from './context'\nimport type { ObjectWithToJsonMethod } from './jsonStringify'\nimport { detachToJsonMethod } from './jsonStringify'\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\ntype PrimitivesAndFunctions = string | number | boolean | undefined | null | symbol | bigint | Function\ntype ExtendedContextValue = PrimitivesAndFunctions | ExtendedContext | ExtendedContextArray\ninterface ExtendedContext {\n  [key: string]: ExtendedContextValue\n}\ntype ExtendedContextArray = ExtendedContextValue[]\n\ninterface ContainerElementToProcess {\n  source: ExtendedContextArray | ExtendedContext\n  target: ContextArray | Context\n  path: string\n}\n\ninterface SanitizedEvent extends Context {\n  type: string\n  isTrusted: boolean\n  currentTarget: string | null | undefined\n  target: string | null | undefined\n}\n\n// The maximum size of a single event is 256KiB. By default, we ensure that user-provided data\n// going through sanitize fits inside our events, while leaving room for other contexts, metadata, ...\nexport const SANITIZE_DEFAULT_MAX_CHARACTER_COUNT = 220 * ONE_KIBI_BYTE\n\n// Symbol for the root element of the JSONPath used for visited objects\nconst JSON_PATH_ROOT_ELEMENT = '$'\n\n// When serializing (using JSON.stringify) a key of an object, { key: 42 } gets wrapped in quotes as \"key\".\n// With the separator (:), we need to add 3 characters to the count.\nconst KEY_DECORATION_LENGTH = 3\n\n/**\n * Ensures user-provided data is 'safe' for the SDK\n * - Deep clones data\n * - Removes cyclic references\n * - Transforms unserializable types to a string representation\n *\n * LIMITATIONS:\n * - Size is in characters, not byte count (may differ according to character encoding)\n * - Size does not take into account indentation that can be applied to JSON.stringify\n * - Non-numerical properties of Arrays are ignored. Same behavior as JSON.stringify\n *\n * @param source              - User-provided data meant to be serialized using JSON.stringify\n * @param maxCharacterCount   - Maximum number of characters allowed in serialized form\n */\nexport function sanitize(source: string, maxCharacterCount?: number): string | undefined\nexport function sanitize(source: Context, maxCharacterCount?: number): Context\nexport function sanitize(source: unknown, maxCharacterCount?: number): ContextValue\nexport function sanitize(source: unknown, maxCharacterCount = SANITIZE_DEFAULT_MAX_CHARACTER_COUNT) {\n  // Unbind any toJSON function we may have on [] or {} prototypes\n  const restoreObjectPrototypeToJson = detachToJsonMethod(Object.prototype)\n  const restoreArrayPrototypeToJson = detachToJsonMethod(Array.prototype)\n\n  try {\n    // Initial call to sanitizeProcessor - will populate containerQueue if source is an Array or a plain Object\n    const containerQueue: ContainerElementToProcess[] = []\n    const visitedObjectsWithPath = new WeakMap<object, string>()\n    const sanitizedData = sanitizeProcessor(\n      source as ExtendedContextValue,\n      JSON_PATH_ROOT_ELEMENT,\n      undefined,\n      containerQueue,\n      visitedObjectsWithPath\n    )\n    const serializedSanitizedData = JSON.stringify(sanitizedData)\n    let accumulatedCharacterCount = serializedSanitizedData ? serializedSanitizedData.length : 0\n\n    if (accumulatedCharacterCount > maxCharacterCount) {\n      warnOverCharacterLimit(maxCharacterCount, 'discarded', source)\n      return undefined\n    }\n\n    while (containerQueue.length > 0 && accumulatedCharacterCount < maxCharacterCount) {\n      const containerToProcess = containerQueue.shift()!\n      let separatorLength = 0 // 0 for the first element, 1 for subsequent elements\n\n      // Arrays and Objects have to be handled distinctly to ensure\n      // we do not pick up non-numerical properties from Arrays\n      if (Array.isArray(containerToProcess.source)) {\n        for (let key = 0; key < containerToProcess.source.length; key++) {\n          const targetData = sanitizeProcessor(\n            containerToProcess.source[key],\n            containerToProcess.path,\n            key,\n            containerQueue,\n            visitedObjectsWithPath\n          )\n\n          if (targetData !== undefined) {\n            accumulatedCharacterCount += JSON.stringify(targetData).length\n          } else {\n            // When an element of an Array (targetData) is undefined, it is serialized as null:\n            // JSON.stringify([undefined]) => '[null]' - This accounts for 4 characters\n            accumulatedCharacterCount += 4\n          }\n          accumulatedCharacterCount += separatorLength\n          separatorLength = 1\n          if (accumulatedCharacterCount > maxCharacterCount) {\n            warnOverCharacterLimit(maxCharacterCount, 'truncated', source)\n            break\n          }\n          ;(containerToProcess.target as ContextArray)[key] = targetData\n        }\n      } else {\n        for (const key in containerToProcess.source) {\n          if (Object.prototype.hasOwnProperty.call(containerToProcess.source, key)) {\n            const targetData = sanitizeProcessor(\n              containerToProcess.source[key],\n              containerToProcess.path,\n              key,\n              containerQueue,\n              visitedObjectsWithPath\n            )\n            // When a property of an object has an undefined value, it will be dropped during serialization:\n            // JSON.stringify({a:undefined}) => '{}'\n            if (targetData !== undefined) {\n              accumulatedCharacterCount +=\n                JSON.stringify(targetData).length + separatorLength + key.length + KEY_DECORATION_LENGTH\n              separatorLength = 1\n            }\n            if (accumulatedCharacterCount > maxCharacterCount) {\n              warnOverCharacterLimit(maxCharacterCount, 'truncated', source)\n              break\n            }\n            ;(containerToProcess.target as Context)[key] = targetData\n          }\n        }\n      }\n    }\n\n    return sanitizedData\n  } finally {\n    // Rebind detached toJSON functions\n    restoreObjectPrototypeToJson()\n    restoreArrayPrototypeToJson()\n  }\n}\n\n/**\n * Internal function to factorize the process common to the\n * initial call to sanitize, and iterations for Arrays and Objects\n *\n */\nfunction sanitizeProcessor(\n  source: ExtendedContextValue,\n  parentPath: string,\n  key: string | number | undefined,\n  queue: ContainerElementToProcess[],\n  visitedObjectsWithPath: WeakMap<object, string>\n) {\n  // Start by handling toJSON, as we want to sanitize its output\n  const sourceToSanitize = tryToApplyToJSON(source)\n\n  if (!sourceToSanitize || typeof sourceToSanitize !== 'object') {\n    return sanitizePrimitivesAndFunctions(sourceToSanitize)\n  }\n\n  const sanitizedSource = sanitizeObjects(sourceToSanitize)\n  if (sanitizedSource !== '[Object]' && sanitizedSource !== '[Array]' && sanitizedSource !== '[Error]') {\n    return sanitizedSource\n  }\n\n  // Handle potential cyclic references\n  // We need to use source as sourceToSanitize could be a reference to a new object\n  // At this stage, we know the source is an object type\n  const sourceAsObject = source as object\n  if (visitedObjectsWithPath.has(sourceAsObject)) {\n    return `[Reference seen at ${visitedObjectsWithPath.get(sourceAsObject)!}]`\n  }\n\n  // Add processed source to queue\n  const currentPath = key !== undefined ? `${parentPath}.${key}` : parentPath\n  const target = Array.isArray(sourceToSanitize) ? ([] as ContextArray) : ({} as Context)\n  visitedObjectsWithPath.set(sourceAsObject, currentPath)\n  queue.push({ source: sourceToSanitize, target, path: currentPath })\n\n  return target\n}\n\n/**\n * Handles sanitization of simple, non-object types\n *\n */\nfunction sanitizePrimitivesAndFunctions(value: PrimitivesAndFunctions) {\n  // BigInt cannot be serialized by JSON.stringify(), convert it to a string representation\n  if (typeof value === 'bigint') {\n    return `[BigInt] ${value.toString()}`\n  }\n  // Functions cannot be serialized by JSON.stringify(). Moreover, if a faulty toJSON is present, it needs to be converted\n  // so it won't prevent stringify from serializing later\n  if (typeof value === 'function') {\n    return `[Function] ${value.name || 'unknown'}`\n  }\n  // JSON.stringify() does not serialize symbols.\n  if (typeof value === 'symbol') {\n    // symbol.description is part of ES2019+\n    type symbolWithDescription = symbol & { description: string }\n    return `[Symbol] ${(value as symbolWithDescription).description || value.toString()}`\n  }\n\n  return value\n}\n\n/**\n * Handles sanitization of object types\n *\n * LIMITATIONS\n * - If a class defines a toStringTag Symbol, it will fall in the catch-all method and prevent enumeration of properties.\n * To avoid this, a toJSON method can be defined.\n */\nfunction sanitizeObjects(value: object): string | SanitizedEvent {\n  try {\n    if (value instanceof Event) {\n      return sanitizeEvent(value)\n    }\n\n    if (value instanceof RegExp) {\n      return `[RegExp] ${value.toString()}`\n    }\n\n    // Handle all remaining object types in a generic way\n    const result = Object.prototype.toString.call(value)\n    const match = result.match(/\\[object (.*)\\]/)\n    if (match?.[1]) {\n      return `[${match[1]}]`\n    }\n  } catch {\n    // If the previous serialization attempts failed, and we cannot convert using\n    // Object.prototype.toString, declare the value unserializable\n  }\n  return '[Unserializable]'\n}\n\nfunction sanitizeEvent(event: Event): SanitizedEvent {\n  return {\n    type: event.type,\n    isTrusted: event.isTrusted,\n    currentTarget: event.currentTarget ? (sanitizeObjects(event.currentTarget) as string) : null,\n    target: event.target ? (sanitizeObjects(event.target) as string) : null,\n  }\n}\n\n/**\n * Checks if a toJSON function exists and tries to execute it\n *\n */\nfunction tryToApplyToJSON(value: ExtendedContextValue) {\n  const object = value as ObjectWithToJsonMethod\n  if (object && typeof object.toJSON === 'function') {\n    try {\n      return object.toJSON() as ExtendedContextValue\n    } catch {\n      // If toJSON fails, we continue by trying to serialize the value manually\n    }\n  }\n\n  return value\n}\n\n/**\n * Helper function to display the warning when the accumulated character count is over the limit\n */\nfunction warnOverCharacterLimit(maxCharacterCount: number, changeType: 'discarded' | 'truncated', source: unknown) {\n  display.warn(\n    `The data provided has been ${changeType} as it is over the limit of ${maxCharacterCount} characters:`,\n    source\n  )\n}\n"],"mappings":"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"}