diff --git a/packages/rum/src/domain/record/index.ts b/packages/rum/src/domain/record/index.ts index d5be936a21..7d91bd2c97 100644 --- a/packages/rum/src/domain/record/index.ts +++ b/packages/rum/src/domain/record/index.ts @@ -1,6 +1,6 @@ export { record } from './record' export type { SerializationMetric, SerializationStats } from './serialization' export { createSerializationStats, aggregateSerializationStats } from './serialization' -export { serializeNodeWithId, serializeDocument } from './serialization' +export { serializeNode, serializeDocument } from './serialization' export { createElementsScrollPositions } from './elementsScrollPositions' export type { ShadowRootsController } from './shadowRootsController' diff --git a/packages/rum/src/domain/record/nodeIds.ts b/packages/rum/src/domain/record/nodeIds.ts index 2fb8b902f3..0b4361ba17 100644 --- a/packages/rum/src/domain/record/nodeIds.ts +++ b/packages/rum/src/domain/record/nodeIds.ts @@ -4,7 +4,7 @@ export type NodeWithSerializedNode = Node & { __brand: 'NodeWithSerializedNode' export type NodeId = number & { __brand: 'NodeId' } export const enum NodeIdConstants { - FIRST_ID = 1, + FIRST_ID = 0, } export interface NodeIds { diff --git a/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts b/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts index 2181031e45..1683bc4da5 100644 --- a/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts +++ b/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts @@ -2,7 +2,7 @@ import { NodePrivacyLevel, PRIVACY_ATTR_NAME } from '@datadog/browser-rum-core' import { display, objectValues } from '@datadog/browser-core' import type { SerializedNodeWithId } from '../../../types' import { createSerializationTransactionForTesting } from '../test/serialization.specHelper' -import { serializeNodeWithId } from './serializeNode' +import { serializeNode } from './serializeNode' export const makeHtmlDoc = (htmlContent: string, privacyTag: string) => { try { @@ -30,7 +30,7 @@ export const generateLeanSerializedDoc = (htmlContent: string, privacyTag: strin const transaction = createSerializationTransactionForTesting() const newDoc = makeHtmlDoc(htmlContent, privacyTag) const serializedDoc = removeIdFieldsRecursivelyClone( - serializeNodeWithId(newDoc, NodePrivacyLevel.ALLOW, transaction) as unknown as Record + serializeNode(newDoc, NodePrivacyLevel.ALLOW, transaction) as unknown as Record ) as unknown as SerializedNodeWithId return serializedDoc } diff --git a/packages/rum/src/domain/record/serialization/index.ts b/packages/rum/src/domain/record/serialization/index.ts index 71169d1233..b4bd8a349b 100644 --- a/packages/rum/src/domain/record/serialization/index.ts +++ b/packages/rum/src/domain/record/serialization/index.ts @@ -1,6 +1,6 @@ export { getElementInputValue } from './serializationUtils' export { serializeDocument } from './serializeDocument' -export { serializeNodeWithId } from './serializeNode' +export { serializeNode } from './serializeNode' export { serializeAttribute } from './serializeAttribute' export { createSerializationStats, updateSerializationStats, aggregateSerializationStats } from './serializationStats' export type { SerializationMetric, SerializationStats } from './serializationStats' diff --git a/packages/rum/src/domain/record/serialization/serializationTransaction.ts b/packages/rum/src/domain/record/serialization/serializationTransaction.ts index 4d6e500ddf..b12fff1476 100644 --- a/packages/rum/src/domain/record/serialization/serializationTransaction.ts +++ b/packages/rum/src/domain/record/serialization/serializationTransaction.ts @@ -29,6 +29,12 @@ export interface SerializationTransaction { */ addMetric(metric: keyof SerializationStats, value: number): void + /** + * Assign and return an id to the given node. If the node has previously been assigned + * an id, the existing id will be reused. + */ + assignId(node: Node): NodeId + /** The kind of serialization being performed in this transaction. */ kind: SerializationKind @@ -59,12 +65,19 @@ export function serializeInTransaction( const stats = createSerializationStats() const transaction: SerializationTransaction = { - add: (record: BrowserRecord) => { + add(record: BrowserRecord): void { records.push(record) }, - addMetric: (metric: keyof SerializationStats, value: number) => { + addMetric(metric: keyof SerializationStats, value: number): void { updateSerializationStats(stats, metric, value) }, + assignId(node: Node): NodeId { + const id = scope.nodeIds.assign(node) + if (transaction.serializedNodeIds) { + transaction.serializedNodeIds.add(id) + } + return id + }, kind, scope, } diff --git a/packages/rum/src/domain/record/serialization/serializeDocument.ts b/packages/rum/src/domain/record/serialization/serializeDocument.ts index d32ab214c5..4b8f19a81e 100644 --- a/packages/rum/src/domain/record/serialization/serializeDocument.ts +++ b/packages/rum/src/domain/record/serialization/serializeDocument.ts @@ -1,5 +1,5 @@ import type { DocumentNode, SerializedNodeWithId } from '../../../types' -import { serializeNodeWithId } from './serializeNode' +import { serializeNode } from './serializeNode' import type { SerializationTransaction } from './serializationTransaction' export function serializeDocument( @@ -7,7 +7,7 @@ export function serializeDocument( transaction: SerializationTransaction ): DocumentNode & SerializedNodeWithId { const defaultPrivacyLevel = transaction.scope.configuration.defaultPrivacyLevel - const serializedNode = serializeNodeWithId(document, defaultPrivacyLevel, transaction) + const serializedNode = serializeNode(document, defaultPrivacyLevel, transaction) // We are sure that Documents are never ignored, so this function never returns null return serializedNode as DocumentNode & SerializedNodeWithId diff --git a/packages/rum/src/domain/record/serialization/serializeNode.spec.ts b/packages/rum/src/domain/record/serialization/serializeNode.spec.ts index f18cf79062..d43b543704 100644 --- a/packages/rum/src/domain/record/serialization/serializeNode.spec.ts +++ b/packages/rum/src/domain/record/serialization/serializeNode.spec.ts @@ -27,13 +27,13 @@ import { AST_MASK_UNLESS_ALLOWLISTED, AST_ALLOW, } from './htmlAst.specHelper' -import { serializeChildNodes, serializeDocumentNode, serializeNodeWithId } from './serializeNode' +import { serializeChildNodes, serializeDocumentNode, serializeNode } from './serializeNode' import type { SerializationStats } from './serializationStats' import { createSerializationStats } from './serializationStats' import type { SerializationTransaction } from './serializationTransaction' import { serializeInTransaction, SerializationKind } from './serializationTransaction' -describe('serializeNodeWithId', () => { +describe('serializeNode', () => { let addShadowRootSpy: jasmine.Spy let emitRecordCallback: jasmine.Spy let emitStatsCallback: jasmine.Spy @@ -51,7 +51,7 @@ describe('serializeNodeWithId', () => { describe('document serialization', () => { it('serializes a document', () => { const document = new DOMParser().parseFromString('foo', 'text/html') - expect(serializeNodeWithId(document, NodePrivacyLevel.ALLOW, transaction)).toEqual({ + expect(serializeNode(document, NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Document, childNodes: [ jasmine.objectContaining({ type: NodeType.DocumentType, name: 'html', publicId: '', systemId: '' }), @@ -65,7 +65,7 @@ describe('serializeNodeWithId', () => { describe('elements serialization', () => { it('serializes a div', () => { - expect(serializeNodeWithId(document.createElement('div'), NodePrivacyLevel.ALLOW, transaction)).toEqual({ + expect(serializeNode(document.createElement('div'), NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: {}, @@ -79,7 +79,7 @@ describe('serializeNodeWithId', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) - expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)).toEqual({ + expect(serializeNode(element, NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: { @@ -97,7 +97,7 @@ describe('serializeNodeWithId', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) element.appendChild(document.createElement('hr')) - expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([]) + expect(serializeNode(element, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([]) }) it('serializes attributes', () => { @@ -105,7 +105,7 @@ describe('serializeNodeWithId', () => { element.className = 'zog' element.style.width = '10px' - expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ + expect(serializeNode(element, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ foo: 'bar', 'data-foo': 'data-bar', class: 'zog', @@ -128,7 +128,7 @@ describe('serializeNodeWithId', () => { kind: SerializationKind.INITIAL_FULL_SNAPSHOT, scope, }) - const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + const node = serializeNode(element, NodePrivacyLevel.ALLOW, transaction) expect(node?.attributes).toEqual( jasmine.objectContaining({ @@ -144,7 +144,7 @@ describe('serializeNodeWithId', () => { kind: SerializationKind.SUBSEQUENT_FULL_SNAPSHOT, scope, }) - const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + const node = serializeNode(element, NodePrivacyLevel.ALLOW, transaction) expect(node?.attributes.rr_scrollLeft).toBeUndefined() expect(node?.attributes.rr_scrollTop).toBeUndefined() @@ -158,7 +158,7 @@ describe('serializeNodeWithId', () => { kind: SerializationKind.SUBSEQUENT_FULL_SNAPSHOT, scope, }) - const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + const node = serializeNode(element, NodePrivacyLevel.ALLOW, transaction) expect(node?.attributes).toEqual( jasmine.objectContaining({ @@ -175,7 +175,7 @@ describe('serializeNodeWithId', () => { kind: SerializationKind.INCREMENTAL_SNAPSHOT, scope, }) - const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + const node = serializeNode(element, NodePrivacyLevel.ALLOW, transaction) expect(node?.attributes.rr_scrollLeft).toBeUndefined() expect(node?.attributes.rr_scrollTop).toBeUndefined() @@ -186,7 +186,7 @@ describe('serializeNodeWithId', () => { const head = document.createElement('head') head.innerHTML = ' foo ' - expect(serializeNodeWithId(head, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([ + expect(serializeNode(head, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([ jasmine.objectContaining({ type: NodeType.Element, tagName: 'title', @@ -199,7 +199,7 @@ describe('serializeNodeWithId', () => { const input = document.createElement('input') input.value = 'toto' - expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual( + expect(serializeNode(input, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) @@ -210,7 +210,7 @@ describe('serializeNodeWithId', () => { const textarea = document.createElement('textarea') textarea.value = 'toto' - expect(serializeNodeWithId(textarea, NodePrivacyLevel.ALLOW, transaction)).toEqual( + expect(serializeNode(textarea, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) @@ -227,7 +227,7 @@ describe('serializeNodeWithId', () => { select.appendChild(option2) select.options.selectedIndex = 1 - expect(serializeNodeWithId(select, NodePrivacyLevel.ALLOW, transaction)).toEqual( + expect(serializeNode(select, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'bar' }, childNodes: [ @@ -252,7 +252,7 @@ describe('serializeNodeWithId', () => { input.type = 'password' input.value = 'toto' - expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) + expect(serializeNode(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) }) it('does not serialize values set via attribute setter', () => { @@ -260,13 +260,13 @@ describe('serializeNodeWithId', () => { input.type = 'password' input.setAttribute('value', 'toto') - expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) + expect(serializeNode(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) }) it('serializes elements checked state', () => { const checkbox = document.createElement('input') checkbox.type = 'checkbox' - expect(serializeNodeWithId(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( + expect(serializeNode(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { type: 'checkbox', @@ -277,7 +277,7 @@ describe('serializeNodeWithId', () => { checkbox.checked = true - expect(serializeNodeWithId(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( + expect(serializeNode(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { type: 'checkbox', @@ -291,14 +291,14 @@ describe('serializeNodeWithId', () => { it('serializes elements checked state', () => { const radio = document.createElement('input') radio.type = 'radio' - expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ + expect(serializeNode(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ type: 'radio', value: 'on', }) radio.checked = true - expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ + expect(serializeNode(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ type: 'radio', value: 'on', checked: '', @@ -308,7 +308,7 @@ describe('serializeNodeWithId', () => { it('serializes