diff --git a/packages/rum/src/domain/record/observers/mutationObserver.ts b/packages/rum/src/domain/record/observers/mutationObserver.ts index c7f1a83799..b65066f4ae 100644 --- a/packages/rum/src/domain/record/observers/mutationObserver.ts +++ b/packages/rum/src/domain/record/observers/mutationObserver.ts @@ -14,6 +14,7 @@ import type { RemovedNodeMutation, TextMutation, } from '../../../types' +import type { NodePrivacyLevelCache } from '../privacy' import { getNodePrivacyLevel, getTextContent } from '../privacy' import type { NodeWithSerializedNode } from '../serialization' import { @@ -109,6 +110,8 @@ function processMutations( configuration: RumConfiguration, shadowRootsController: ShadowRootsController ) { + const nodePrivacyLevelCache: NodePrivacyLevelCache = new Map() + mutations .filter((mutation): mutation is RumChildListMutationRecord => mutation.type === 'childList') .forEach((mutation) => { @@ -125,7 +128,8 @@ function processMutations( (mutation): mutation is WithSerializedTarget => mutation.target.isConnected && nodeAndAncestorsHaveSerializedNode(mutation.target) && - getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel) !== NodePrivacyLevel.HIDDEN + getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel, nodePrivacyLevelCache) !== + NodePrivacyLevel.HIDDEN ) const { adds, removes, hasBeenSerialized } = processChildListMutations( @@ -133,7 +137,8 @@ function processMutations( (mutation): mutation is WithSerializedTarget => mutation.type === 'childList' ), configuration, - shadowRootsController + shadowRootsController, + nodePrivacyLevelCache ) const texts = processCharacterDataMutations( @@ -141,7 +146,8 @@ function processMutations( (mutation): mutation is WithSerializedTarget => mutation.type === 'characterData' && !hasBeenSerialized(mutation.target) ), - configuration + configuration, + nodePrivacyLevelCache ) const attributes = processAttributesMutations( @@ -149,7 +155,8 @@ function processMutations( (mutation): mutation is WithSerializedTarget => mutation.type === 'attributes' && !hasBeenSerialized(mutation.target) ), - configuration + configuration, + nodePrivacyLevelCache ) if (!texts.length && !attributes.length && !removes.length && !adds.length) { @@ -167,7 +174,8 @@ function processMutations( function processChildListMutations( mutations: Array>, configuration: RumConfiguration, - shadowRootsController: ShadowRootsController + shadowRootsController: ShadowRootsController, + nodePrivacyLevelCache: NodePrivacyLevelCache ) { // First, we iterate over mutations to collect: // @@ -217,7 +225,11 @@ function processChildListMutations( continue } - const parentNodePrivacyLevel = getNodePrivacyLevel(node.parentNode!, configuration.defaultPrivacyLevel) + const parentNodePrivacyLevel = getNodePrivacyLevel( + node.parentNode!, + configuration.defaultPrivacyLevel, + nodePrivacyLevelCache + ) if (parentNodePrivacyLevel === NodePrivacyLevel.HIDDEN || parentNodePrivacyLevel === NodePrivacyLevel.IGNORE) { continue } @@ -271,7 +283,8 @@ function processChildListMutations( function processCharacterDataMutations( mutations: Array>, - configuration: RumConfiguration + configuration: RumConfiguration, + nodePrivacyLevelCache: NodePrivacyLevelCache ) { const textMutations: TextMutation[] = [] @@ -294,7 +307,8 @@ function processCharacterDataMutations( const parentNodePrivacyLevel = getNodePrivacyLevel( getParentNode(mutation.target)!, - configuration.defaultPrivacyLevel + configuration.defaultPrivacyLevel, + nodePrivacyLevelCache ) if (parentNodePrivacyLevel === NodePrivacyLevel.HIDDEN || parentNodePrivacyLevel === NodePrivacyLevel.IGNORE) { continue @@ -312,7 +326,8 @@ function processCharacterDataMutations( function processAttributesMutations( mutations: Array>, - configuration: RumConfiguration + configuration: RumConfiguration, + nodePrivacyLevelCache: NodePrivacyLevelCache ) { const attributeMutations: AttributeMutation[] = [] @@ -338,7 +353,7 @@ function processAttributesMutations( if (uncensoredValue === mutation.oldValue) { continue } - const privacyLevel = getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel) + const privacyLevel = getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel, nodePrivacyLevelCache) const attributeValue = serializeAttribute(mutation.target, privacyLevel, mutation.attributeName!, configuration) let transformedValue: string | null diff --git a/packages/rum/src/domain/record/privacy.spec.ts b/packages/rum/src/domain/record/privacy.spec.ts index bd9f72cfa3..1d360deb5d 100644 --- a/packages/rum/src/domain/record/privacy.spec.ts +++ b/packages/rum/src/domain/record/privacy.spec.ts @@ -81,6 +81,46 @@ describe('getNodePrivacyLevel', () => { expect(getNodePrivacyLevel(node, NodePrivacyLevel.ALLOW)).toBe(NodePrivacyLevel.MASK) }) }) + + describe('cache', () => { + it('fills the cache', () => { + const ancestor = document.createElement('div') + const node = document.createElement('div') + ancestor.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_MASK) + ancestor.appendChild(node) + + const cache = new Map() + getNodePrivacyLevel(node, NodePrivacyLevel.ALLOW, cache) + + expect(cache.get(node)).toBe(NodePrivacyLevel.MASK) + }) + + it('uses the cache', () => { + const ancestor = document.createElement('div') + const node = document.createElement('div') + ancestor.appendChild(node) + + const cache = new Map() + cache.set(node, NodePrivacyLevel.MASK_USER_INPUT) + + expect(getNodePrivacyLevel(node, NodePrivacyLevel.ALLOW, cache)).toBe(NodePrivacyLevel.MASK_USER_INPUT) + }) + + it('does not recurse on ancestors if the node is already in the cache', () => { + const ancestor = document.createElement('div') + const node = document.createElement('div') + ancestor.appendChild(node) + + const parentNodeGetterSpy = spyOnProperty(node, 'parentNode').and.returnValue(ancestor) + + const cache = new Map() + cache.set(node, NodePrivacyLevel.MASK_USER_INPUT) + + getNodePrivacyLevel(node, NodePrivacyLevel.ALLOW, cache) + + expect(parentNodeGetterSpy).not.toHaveBeenCalled() + }) + }) }) describe('getNodeSelfPrivacyLevel', () => { diff --git a/packages/rum/src/domain/record/privacy.ts b/packages/rum/src/domain/record/privacy.ts index 8c87568eac..f2a9a12876 100644 --- a/packages/rum/src/domain/record/privacy.ts +++ b/packages/rum/src/domain/record/privacy.ts @@ -18,17 +18,32 @@ export const MAX_ATTRIBUTE_VALUE_CHAR_LENGTH = 100_000 const TEXT_MASKING_CHAR = 'x' +export type NodePrivacyLevelCache = Map + /** * Get node privacy level by iterating over its ancestors. When the direct parent privacy level is * know, it is best to use something like: * * derivePrivacyLevelGivenParent(getNodeSelfPrivacyLevel(node), parentNodePrivacyLevel) */ -export function getNodePrivacyLevel(node: Node, defaultPrivacyLevel: NodePrivacyLevel): NodePrivacyLevel { +export function getNodePrivacyLevel( + node: Node, + defaultPrivacyLevel: NodePrivacyLevel, + cache?: NodePrivacyLevelCache +): NodePrivacyLevel { + if (cache && cache.has(node)) { + return cache.get(node)! + } const parentNode = getParentNode(node) - const parentNodePrivacyLevel = parentNode ? getNodePrivacyLevel(parentNode, defaultPrivacyLevel) : defaultPrivacyLevel + const parentNodePrivacyLevel = parentNode + ? getNodePrivacyLevel(parentNode, defaultPrivacyLevel, cache) + : defaultPrivacyLevel const selfNodePrivacyLevel = getNodeSelfPrivacyLevel(node) - return reducePrivacyLevel(selfNodePrivacyLevel, parentNodePrivacyLevel) + const nodePrivacyLevel = reducePrivacyLevel(selfNodePrivacyLevel, parentNodePrivacyLevel) + if (cache) { + cache.set(node, nodePrivacyLevel) + } + return nodePrivacyLevel } /**