Skip to content

Commit 3ab990f

Browse files
🎨 [PANA-4372] Explicitly scope serialization state (#3887)
1 parent ad8c360 commit 3ab990f

30 files changed

+420
-204
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { NodeId, NodeIds } from './nodeIds'
2+
import { createNodeIds, NodeIdConstants } from './nodeIds'
3+
4+
describe('NodeIds', () => {
5+
let nodeIds: NodeIds
6+
7+
beforeEach(() => {
8+
nodeIds = createNodeIds()
9+
})
10+
11+
describe('assign', () => {
12+
it('assigns node ids in order', () => {
13+
for (let id = NodeIdConstants.FIRST_ID as NodeId; id < NodeIdConstants.FIRST_ID + 3; id++) {
14+
const node = document.createElement('div')
15+
expect(nodeIds.assign(node)).toBe(id)
16+
expect(nodeIds.assign(node)).toBe(id)
17+
}
18+
})
19+
20+
it('reuses any existing node id', () => {
21+
nodeIds.assign(document.createElement('div'))
22+
nodeIds.assign(document.createElement('div'))
23+
const node = document.createElement('div')
24+
const nodeId = nodeIds.assign(node)
25+
expect(nodeIds.assign(node)).toBe(nodeId)
26+
expect(nodeIds.get(node)).toBe(nodeId)
27+
})
28+
})
29+
30+
describe('get', () => {
31+
it('returns undefined for DOM Nodes that have not been assigned an id', () => {
32+
expect(nodeIds.get(document.createElement('div'))).toBe(undefined)
33+
})
34+
35+
it('returns the serialized Node id when available', () => {
36+
const node = document.createElement('div')
37+
nodeIds.assign(node)
38+
expect(nodeIds.get(node)).toBe(NodeIdConstants.FIRST_ID as NodeId)
39+
})
40+
})
41+
42+
describe('areAssignedForNodeAndAncestors', () => {
43+
it('returns false for DOM Nodes that have not been assigned an id', () => {
44+
expect(nodeIds.areAssignedForNodeAndAncestors(document.createElement('div'))).toBe(false)
45+
})
46+
47+
it('returns true for DOM Nodes that have been assigned an id', () => {
48+
const node = document.createElement('div')
49+
nodeIds.assign(node)
50+
expect(nodeIds.areAssignedForNodeAndAncestors(node)).toBe(true)
51+
})
52+
53+
it('returns false for DOM Nodes when an ancestor has not been assigned an id', () => {
54+
const node = document.createElement('div')
55+
nodeIds.assign(node)
56+
57+
const parent = document.createElement('div')
58+
parent.appendChild(node)
59+
nodeIds.assign(parent)
60+
61+
const grandparent = document.createElement('div')
62+
grandparent.appendChild(parent)
63+
64+
expect(nodeIds.areAssignedForNodeAndAncestors(node)).toBe(false)
65+
})
66+
67+
it('returns true for DOM Nodes when all ancestors have been assigned an id', () => {
68+
const node = document.createElement('div')
69+
nodeIds.assign(node)
70+
71+
const parent = document.createElement('div')
72+
parent.appendChild(node)
73+
nodeIds.assign(parent)
74+
75+
const grandparent = document.createElement('div')
76+
grandparent.appendChild(parent)
77+
nodeIds.assign(grandparent)
78+
79+
expect(nodeIds.areAssignedForNodeAndAncestors(node)).toBe(true)
80+
})
81+
82+
it('returns true for DOM Nodes in shadow subtrees', () => {
83+
const node = document.createElement('div')
84+
nodeIds.assign(node)
85+
86+
const parent = document.createElement('div')
87+
parent.appendChild(node)
88+
nodeIds.assign(parent)
89+
90+
const grandparent = document.createElement('div')
91+
const shadowRoot = grandparent.attachShadow({ mode: 'open' })
92+
shadowRoot.appendChild(parent)
93+
nodeIds.assign(grandparent)
94+
95+
expect(nodeIds.areAssignedForNodeAndAncestors(node)).toBe(true)
96+
})
97+
})
98+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getParentNode, isNodeShadowRoot } from '@datadog/browser-rum-core'
2+
3+
export type NodeWithSerializedNode = Node & { __brand: 'NodeWithSerializedNode' }
4+
export type NodeId = number & { __brand: 'NodeId' }
5+
6+
export const enum NodeIdConstants {
7+
FIRST_ID = 1,
8+
}
9+
10+
export interface NodeIds {
11+
assign(node: Node): NodeId
12+
get(node: Node): NodeId | undefined
13+
areAssignedForNodeAndAncestors(node: Node): node is NodeWithSerializedNode
14+
}
15+
16+
export function createNodeIds(): NodeIds {
17+
const nodeIds = new WeakMap<Node, NodeId>()
18+
let nextNodeId = NodeIdConstants.FIRST_ID
19+
20+
const get = (node: Node): NodeId | undefined => nodeIds.get(node)
21+
22+
return {
23+
assign: (node: Node): NodeId => {
24+
// Try to reuse any existing id.
25+
let nodeId = get(node)
26+
if (nodeId === undefined) {
27+
nodeId = nextNodeId++ as NodeId
28+
nodeIds.set(node, nodeId)
29+
}
30+
return nodeId
31+
},
32+
33+
get,
34+
35+
areAssignedForNodeAndAncestors: (node: Node): node is NodeWithSerializedNode => {
36+
let current: Node | null = node
37+
while (current) {
38+
if (get(current) === undefined && !isNodeShadowRoot(current)) {
39+
return false
40+
}
41+
current = getParentNode(current)
42+
}
43+
return true
44+
},
45+
}
46+
}

‎packages/rum/src/domain/record/record.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { initShadowRootsController } from './shadowRootsController'
2323
import { startFullSnapshots } from './startFullSnapshots'
2424
import { initRecordIds } from './recordIds'
2525
import type { SerializationStats } from './serialization'
26+
import { createSerializationScope } from './serialization'
27+
import { createNodeIds } from './nodeIds'
2628

2729
export interface RecordOptions {
2830
emit?: (record: BrowserRecord, stats?: SerializationStats) => void
@@ -52,14 +54,20 @@ export function record(options: RecordOptions): RecordAPI {
5254
}
5355

5456
const elementsScrollPositions = createElementsScrollPositions()
55-
56-
const shadowRootsController = initShadowRootsController(configuration, emitAndComputeStats, elementsScrollPositions)
57+
const scope = createSerializationScope(createNodeIds())
58+
const shadowRootsController = initShadowRootsController(
59+
configuration,
60+
scope,
61+
emitAndComputeStats,
62+
elementsScrollPositions
63+
)
5764

5865
const { stop: stopFullSnapshots } = startFullSnapshots(
5966
elementsScrollPositions,
6067
shadowRootsController,
6168
lifeCycle,
6269
configuration,
70+
scope,
6371
flushMutations,
6472
emitAndComputeStats
6573
)
@@ -70,16 +78,16 @@ export function record(options: RecordOptions): RecordAPI {
7078
}
7179

7280
const recordIds = initRecordIds()
73-
const mutationTracker = trackMutation(emitAndComputeStats, configuration, shadowRootsController, document)
81+
const mutationTracker = trackMutation(emitAndComputeStats, configuration, scope, shadowRootsController, document)
7482
const trackers: Tracker[] = [
7583
mutationTracker,
76-
trackMove(configuration, emitAndComputeStats),
77-
trackMouseInteraction(configuration, emitAndComputeStats, recordIds),
78-
trackScroll(configuration, emitAndComputeStats, elementsScrollPositions, document),
84+
trackMove(configuration, scope, emitAndComputeStats),
85+
trackMouseInteraction(configuration, scope, emitAndComputeStats, recordIds),
86+
trackScroll(configuration, scope, emitAndComputeStats, elementsScrollPositions, document),
7987
trackViewportResize(configuration, emitAndComputeStats),
80-
trackInput(configuration, emitAndComputeStats),
81-
trackMediaInteraction(configuration, emitAndComputeStats),
82-
trackStyleSheet(emitAndComputeStats),
88+
trackInput(configuration, scope, emitAndComputeStats),
89+
trackMediaInteraction(configuration, scope, emitAndComputeStats),
90+
trackStyleSheet(scope, emitAndComputeStats),
8391
trackFocus(configuration, emitAndComputeStats),
8492
trackVisualViewportResize(configuration, emitAndComputeStats),
8593
trackFrustration(lifeCycle, emitAndComputeStats, recordIds),

‎packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
createElementsScrollPositions,
99
createSerializationStats,
1010
} from '..'
11+
import { createNodeIds } from '../nodeIds'
12+
import { createSerializationScope } from './serializationScope'
1113

1214
export const makeHtmlDoc = (htmlContent: string, privacyTag: string) => {
1315
try {
@@ -50,6 +52,7 @@ export const generateLeanSerializedDoc = (htmlContent: string, privacyTag: strin
5052
elementsScrollPositions: createElementsScrollPositions(),
5153
},
5254
configuration: {} as RumConfiguration,
55+
scope: createSerializationScope(createNodeIds()),
5356
})! as unknown as Record<string, unknown>
5457
) as unknown as SerializedNodeWithId
5558
return serializedDoc
Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
export {
2-
getElementInputValue,
3-
getSerializedNodeId,
4-
hasSerializedNode,
5-
nodeAndAncestorsHaveSerializedNode,
6-
} from './serializationUtils'
7-
export type { NodeWithSerializedNode } from './serialization.types'
1+
export { getElementInputValue } from './serializationUtils'
82
export { SerializationContextStatus } from './serialization.types'
93
export type { SerializationContext } from './serialization.types'
104
export { serializeDocument } from './serializeDocument'
115
export { serializeNodeWithId } from './serializeNode'
126
export { serializeAttribute } from './serializeAttribute'
7+
export type { SerializationScope } from './serializationScope'
8+
export { createSerializationScope } from './serializationScope'
139
export { createSerializationStats, updateSerializationStats, aggregateSerializationStats } from './serializationStats'
1410
export type { SerializationMetric, SerializationStats } from './serializationStats'

‎packages/rum/src/domain/record/serialization/serialization.types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RumConfiguration, NodePrivacyLevel } from '@datadog/browser-rum-core'
22
import type { ElementsScrollPositions } from '../elementsScrollPositions'
33
import type { ShadowRootsController } from '../shadowRootsController'
4+
import type { SerializationScope } from './serializationScope'
45
import type { SerializationStats } from './serializationStats'
56

67
// Those values are the only one that can be used when inheriting privacy levels from parent to
@@ -42,6 +43,5 @@ export interface SerializeOptions {
4243
parentNodePrivacyLevel: ParentNodePrivacyLevel
4344
serializationContext: SerializationContext
4445
configuration: RumConfiguration
46+
scope: SerializationScope
4547
}
46-
47-
export type NodeWithSerializedNode = Node & { s: 'Node with serialized node' }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { NodeIds } from '../nodeIds'
2+
3+
export interface SerializationScope {
4+
nodeIds: NodeIds
5+
}
6+
7+
export function createSerializationScope(nodeIds: NodeIds): SerializationScope {
8+
return { nodeIds }
9+
}

‎packages/rum/src/domain/record/serialization/serializationUtils.spec.ts‎

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,5 @@
11
import { NodePrivacyLevel } from '@datadog/browser-rum-core'
2-
import {
3-
getSerializedNodeId,
4-
hasSerializedNode,
5-
setSerializedNodeId,
6-
getElementInputValue,
7-
switchToAbsoluteUrl,
8-
} from './serializationUtils'
9-
10-
describe('serialized Node storage in DOM Nodes', () => {
11-
describe('hasSerializedNode', () => {
12-
it('returns false for DOM Nodes that are not yet serialized', () => {
13-
expect(hasSerializedNode(document.createElement('div'))).toBe(false)
14-
})
15-
16-
it('returns true for DOM Nodes that have been serialized', () => {
17-
const node = document.createElement('div')
18-
setSerializedNodeId(node, 42)
19-
20-
expect(hasSerializedNode(node)).toBe(true)
21-
})
22-
})
23-
24-
describe('getSerializedNodeId', () => {
25-
it('returns undefined for DOM Nodes that are not yet serialized', () => {
26-
expect(getSerializedNodeId(document.createElement('div'))).toBe(undefined)
27-
})
28-
29-
it('returns the serialized Node id', () => {
30-
const node = document.createElement('div')
31-
setSerializedNodeId(node, 42)
32-
33-
expect(getSerializedNodeId(node)).toBe(42)
34-
})
35-
})
36-
})
2+
import { getElementInputValue, switchToAbsoluteUrl } from './serializationUtils'
373

384
describe('getElementInputValue', () => {
395
it('returns "undefined" for a non-input element', () => {

‎packages/rum/src/domain/record/serialization/serializationUtils.ts‎

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,6 @@
11
import { buildUrl } from '@datadog/browser-core'
2-
import { getParentNode, isNodeShadowRoot, CENSORED_STRING_MARK, shouldMaskNode } from '@datadog/browser-rum-core'
2+
import { CENSORED_STRING_MARK, shouldMaskNode } from '@datadog/browser-rum-core'
33
import type { NodePrivacyLevel } from '@datadog/browser-rum-core'
4-
import type { NodeWithSerializedNode } from './serialization.types'
5-
6-
const serializedNodeIds = new WeakMap<Node, number>()
7-
8-
export function hasSerializedNode(node: Node): node is NodeWithSerializedNode {
9-
return serializedNodeIds.has(node)
10-
}
11-
12-
export function nodeAndAncestorsHaveSerializedNode(node: Node): node is NodeWithSerializedNode {
13-
let current: Node | null = node
14-
while (current) {
15-
if (!hasSerializedNode(current) && !isNodeShadowRoot(current)) {
16-
return false
17-
}
18-
current = getParentNode(current)
19-
}
20-
return true
21-
}
22-
23-
export function getSerializedNodeId(node: NodeWithSerializedNode): number
24-
export function getSerializedNodeId(node: Node): number | undefined
25-
export function getSerializedNodeId(node: Node) {
26-
return serializedNodeIds.get(node)
27-
}
28-
29-
export function setSerializedNodeId(node: Node, serializeNodeId: number) {
30-
serializedNodeIds.set(node, serializeNodeId)
31-
}
324

335
/**
346
* Get the element "value" to be serialized as an attribute or an input update record. It respects

‎packages/rum/src/domain/record/serialization/serializeDocument.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ import type { RumConfiguration } from '@datadog/browser-rum-core'
33
import type { SerializedNodeWithId } from '../../../types'
44
import type { SerializationContext } from './serialization.types'
55
import { serializeNodeWithId } from './serializeNode'
6+
import type { SerializationScope } from './serializationScope'
67
import { updateSerializationStats } from './serializationStats'
78

89
export function serializeDocument(
910
document: Document,
1011
configuration: RumConfiguration,
12+
scope: SerializationScope,
1113
serializationContext: SerializationContext
1214
): SerializedNodeWithId {
1315
const serializationStart = timeStampNow()
1416
const serializedNode = serializeNodeWithId(document, {
1517
serializationContext,
1618
parentNodePrivacyLevel: configuration.defaultPrivacyLevel,
1719
configuration,
20+
scope,
1821
})
1922
updateSerializationStats(
2023
serializationContext.serializationStats,

0 commit comments

Comments
 (0)