diff --git a/packages/rum/src/domain/record/eventIds.ts b/packages/rum/src/domain/record/eventIds.ts new file mode 100644 index 0000000000..df2f30f801 --- /dev/null +++ b/packages/rum/src/domain/record/eventIds.ts @@ -0,0 +1,17 @@ +export interface EventIds { + getIdForEvent(event: Event): number +} + +export function createEventIds(): EventIds { + const eventIds = new WeakMap() + let nextId = 1 + + return { + getIdForEvent(event: Event): number { + if (!eventIds.has(event)) { + eventIds.set(event, nextId++) + } + return eventIds.get(event)! + }, + } +} diff --git a/packages/rum/src/domain/record/index.ts b/packages/rum/src/domain/record/index.ts index c7e59efcc8..d5be936a21 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, SerializationContextStatus } from './serialization' +export { serializeNodeWithId, serializeDocument } from './serialization' export { createElementsScrollPositions } from './elementsScrollPositions' export type { ShadowRootsController } from './shadowRootsController' diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index 05dca7dc14..4d41c92dc7 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -21,10 +21,10 @@ import { createElementsScrollPositions } from './elementsScrollPositions' import type { ShadowRootsController } from './shadowRootsController' import { initShadowRootsController } from './shadowRootsController' import { startFullSnapshots } from './startFullSnapshots' -import { initRecordIds } from './recordIds' -import type { EmitRecordCallback, EmitStatsCallback } from './record.types' -import { createSerializationScope } from './serialization' +import { createEventIds } from './eventIds' import { createNodeIds } from './nodeIds' +import type { EmitRecordCallback, EmitStatsCallback } from './record.types' +import { createRecordingScope } from './recordingScope' export interface RecordOptions { emitRecord: EmitRecordCallback @@ -54,47 +54,36 @@ export function record(options: RecordOptions): RecordAPI { replayStats.addRecord(view.id) } - const elementsScrollPositions = createElementsScrollPositions() - const scope = createSerializationScope(createNodeIds()) - const shadowRootsController = initShadowRootsController( + const shadowRootsController = initShadowRootsController(processRecord, emitStats) + const scope = createRecordingScope( configuration, - scope, - processRecord, - emitStats, - elementsScrollPositions + createElementsScrollPositions(), + createEventIds(), + createNodeIds(), + shadowRootsController ) - const { stop: stopFullSnapshots } = startFullSnapshots( - elementsScrollPositions, - shadowRootsController, - lifeCycle, - configuration, - scope, - flushMutations, - processRecord, - emitStats - ) + const { stop: stopFullSnapshots } = startFullSnapshots(lifeCycle, processRecord, emitStats, flushMutations, scope) function flushMutations() { shadowRootsController.flush() mutationTracker.flush() } - const recordIds = initRecordIds() - const mutationTracker = trackMutation(processRecord, emitStats, configuration, scope, shadowRootsController, document) + const mutationTracker = trackMutation(document, processRecord, emitStats, scope) const trackers: Tracker[] = [ mutationTracker, - trackMove(configuration, scope, processRecord), - trackMouseInteraction(configuration, scope, processRecord, recordIds), - trackScroll(configuration, scope, processRecord, elementsScrollPositions, document), - trackViewportResize(configuration, processRecord), - trackInput(configuration, scope, processRecord), - trackMediaInteraction(configuration, scope, processRecord), - trackStyleSheet(scope, processRecord), - trackFocus(configuration, processRecord), - trackVisualViewportResize(configuration, processRecord), - trackFrustration(lifeCycle, processRecord, recordIds), - trackViewEnd(lifeCycle, flushMutations, processRecord), + trackMove(processRecord, scope), + trackMouseInteraction(processRecord, scope), + trackScroll(document, processRecord, scope), + trackViewportResize(processRecord, scope), + trackInput(document, processRecord, scope), + trackMediaInteraction(processRecord, scope), + trackStyleSheet(processRecord, scope), + trackFocus(processRecord, scope), + trackVisualViewportResize(processRecord, scope), + trackFrustration(lifeCycle, processRecord, scope), + trackViewEnd(lifeCycle, processRecord, flushMutations), ] return { diff --git a/packages/rum/src/domain/record/recordIds.ts b/packages/rum/src/domain/record/recordIds.ts deleted file mode 100644 index 6e42dfb688..0000000000 --- a/packages/rum/src/domain/record/recordIds.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type RecordIds = ReturnType - -export function initRecordIds() { - const recordIds = new WeakMap() - let nextId = 1 - - return { - getIdForEvent(event: Event): number { - if (!recordIds.has(event)) { - recordIds.set(event, nextId++) - } - return recordIds.get(event)! - }, - } -} diff --git a/packages/rum/src/domain/record/recordingScope.ts b/packages/rum/src/domain/record/recordingScope.ts new file mode 100644 index 0000000000..55d681a3da --- /dev/null +++ b/packages/rum/src/domain/record/recordingScope.ts @@ -0,0 +1,36 @@ +import type { RumConfiguration } from '@datadog/browser-rum-core' + +import type { ElementsScrollPositions } from './elementsScrollPositions' +import type { EventIds } from './eventIds' +import type { NodeIds } from './nodeIds' +import type { ShadowRootsController } from './shadowRootsController' + +/** + * State associated with a stream of session replay records. When a new stream of records + * starts (e.g. because recording has shut down and restarted), a new RecordingScope + * object must be created; this ensures that we don't generate records that reference ids + * or data which aren't present in the current stream. + */ +export interface RecordingScope { + configuration: RumConfiguration + elementsScrollPositions: ElementsScrollPositions + eventIds: EventIds + nodeIds: NodeIds + shadowRootsController: ShadowRootsController +} + +export function createRecordingScope( + configuration: RumConfiguration, + elementsScrollPositions: ElementsScrollPositions, + eventIds: EventIds, + nodeIds: NodeIds, + shadowRootsController: ShadowRootsController +): RecordingScope { + return { + configuration, + elementsScrollPositions, + eventIds, + nodeIds, + shadowRootsController, + } +} diff --git a/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts b/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts index 0341328e34..c80f74d4f3 100644 --- a/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts +++ b/packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts @@ -1,15 +1,8 @@ -import type { RumConfiguration } from '@datadog/browser-rum-core' import { NodePrivacyLevel, PRIVACY_ATTR_NAME } from '@datadog/browser-rum-core' -import { display, noop, objectValues } from '@datadog/browser-core' +import { display, objectValues } from '@datadog/browser-core' import type { SerializedNodeWithId } from '../../../types' -import { - serializeNodeWithId, - SerializationContextStatus, - createElementsScrollPositions, - createSerializationStats, -} from '..' -import { createNodeIds } from '../nodeIds' -import { createSerializationScope } from './serializationScope' +import { createSerializationTransactionForTesting } from '../test/serialization.specHelper' +import { serializeNodeWithId } from './serializeNode' export const makeHtmlDoc = (htmlContent: string, privacyTag: string) => { try { @@ -33,26 +26,11 @@ export const removeIdFieldsRecursivelyClone = (thing: Record): return thing } -const DEFAULT_SHADOW_ROOT_CONTROLLER = { - flush: noop, - stop: noop, - addShadowRoot: noop, - removeShadowRoot: noop, -} - export const generateLeanSerializedDoc = (htmlContent: string, privacyTag: string) => { + const transaction = createSerializationTransactionForTesting() const newDoc = makeHtmlDoc(htmlContent, privacyTag) const serializedDoc = removeIdFieldsRecursivelyClone( - serializeNodeWithId(newDoc, NodePrivacyLevel.ALLOW, { - serializationContext: { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, - elementsScrollPositions: createElementsScrollPositions(), - }, - configuration: {} as RumConfiguration, - scope: createSerializationScope(createNodeIds()), - })! as unknown as Record + serializeNodeWithId(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 a05bb71c4e..71169d1233 100644 --- a/packages/rum/src/domain/record/serialization/index.ts +++ b/packages/rum/src/domain/record/serialization/index.ts @@ -1,10 +1,8 @@ export { getElementInputValue } from './serializationUtils' -export { SerializationContextStatus } from './serialization.types' -export type { SerializationContext } from './serialization.types' export { serializeDocument } from './serializeDocument' export { serializeNodeWithId } from './serializeNode' export { serializeAttribute } from './serializeAttribute' -export type { SerializationScope } from './serializationScope' -export { createSerializationScope } from './serializationScope' export { createSerializationStats, updateSerializationStats, aggregateSerializationStats } from './serializationStats' export type { SerializationMetric, SerializationStats } from './serializationStats' +export { serializeInTransaction, SerializationKind } from './serializationTransaction' +export type { SerializationTransaction, SerializationTransactionCallback } from './serializationTransaction' diff --git a/packages/rum/src/domain/record/serialization/serialization.types.ts b/packages/rum/src/domain/record/serialization/serialization.types.ts index f92f645b6a..924dc61fc7 100644 --- a/packages/rum/src/domain/record/serialization/serialization.types.ts +++ b/packages/rum/src/domain/record/serialization/serialization.types.ts @@ -1,8 +1,4 @@ -import type { RumConfiguration, NodePrivacyLevel } from '@datadog/browser-rum-core' -import type { ElementsScrollPositions } from '../elementsScrollPositions' -import type { ShadowRootsController } from '../shadowRootsController' -import type { SerializationScope } from './serializationScope' -import type { SerializationStats } from './serializationStats' +import type { NodePrivacyLevel } from '@datadog/browser-rum-core' // Those values are the only one that can be used when inheriting privacy levels from parent to // children during serialization, since HIDDEN and IGNORE shouldn't serialize their children. This @@ -12,35 +8,3 @@ export type ParentNodePrivacyLevel = | typeof NodePrivacyLevel.MASK | typeof NodePrivacyLevel.MASK_USER_INPUT | typeof NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED - -export const enum SerializationContextStatus { - INITIAL_FULL_SNAPSHOT, - SUBSEQUENT_FULL_SNAPSHOT, - MUTATION, -} - -export type SerializationContext = - | { - status: SerializationContextStatus.MUTATION - serializationStats: SerializationStats - shadowRootsController: ShadowRootsController - } - | { - status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT - elementsScrollPositions: ElementsScrollPositions - serializationStats: SerializationStats - shadowRootsController: ShadowRootsController - } - | { - status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT - elementsScrollPositions: ElementsScrollPositions - serializationStats: SerializationStats - shadowRootsController: ShadowRootsController - } - -export interface SerializeOptions { - serializedNodeIds?: Set - serializationContext: SerializationContext - configuration: RumConfiguration - scope: SerializationScope -} diff --git a/packages/rum/src/domain/record/serialization/serializationScope.ts b/packages/rum/src/domain/record/serialization/serializationScope.ts deleted file mode 100644 index 94f197027d..0000000000 --- a/packages/rum/src/domain/record/serialization/serializationScope.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { NodeIds } from '../nodeIds' - -export interface SerializationScope { - nodeIds: NodeIds -} - -export function createSerializationScope(nodeIds: NodeIds): SerializationScope { - return { nodeIds } -} diff --git a/packages/rum/src/domain/record/serialization/serializationTransaction.ts b/packages/rum/src/domain/record/serialization/serializationTransaction.ts new file mode 100644 index 0000000000..4d6e500ddf --- /dev/null +++ b/packages/rum/src/domain/record/serialization/serializationTransaction.ts @@ -0,0 +1,81 @@ +import { elapsed, timeStampNow } from '@datadog/browser-core' + +import type { BrowserRecord } from '../../../types' +import type { NodeId } from '../nodeIds' +import type { EmitRecordCallback, EmitStatsCallback } from '../record.types' +import type { RecordingScope } from '../recordingScope' +import type { SerializationStats } from './serializationStats' +import { createSerializationStats, updateSerializationStats } from './serializationStats' + +export type SerializationTransactionCallback = (transaction: SerializationTransaction) => void + +export const enum SerializationKind { + INITIAL_FULL_SNAPSHOT, + SUBSEQUENT_FULL_SNAPSHOT, + INCREMENTAL_SNAPSHOT, +} + +/** + * A serialization transaction is used to build and emit a sequence of session replay + * records containing a serialized snapshot of the DOM. + */ +export interface SerializationTransaction { + /** Add a record to the transaction. It will be emitted when the transaction ends. */ + add(record: BrowserRecord): void + + /** + * Add a metric to the transaction's statistics. The aggregated statistics will be + * emitted when the transaction ends. + */ + addMetric(metric: keyof SerializationStats, value: number): void + + /** The kind of serialization being performed in this transaction. */ + kind: SerializationKind + + /** + * A set used to track nodes which have been serialized in the current transaction. If + * undefined, this feature is disabled; this is the default state in new transactions + * for performance reasons. Set the property to a non-undefined value if you need this + * capability. + */ + serializedNodeIds?: Set + + /** The recording scope in which this transaction is occurring. */ + scope: RecordingScope +} + +/** + * Perform serialization within a transaction. At the end of the transaction, the + * generated records and statistics will be emitted. + */ +export function serializeInTransaction( + kind: SerializationKind, + emitRecord: EmitRecordCallback, + emitStats: EmitStatsCallback, + scope: RecordingScope, + serialize: SerializationTransactionCallback +): void { + const records: BrowserRecord[] = [] + const stats = createSerializationStats() + + const transaction: SerializationTransaction = { + add: (record: BrowserRecord) => { + records.push(record) + }, + addMetric: (metric: keyof SerializationStats, value: number) => { + updateSerializationStats(stats, metric, value) + }, + kind, + scope, + } + + const start = timeStampNow() + serialize(transaction) + updateSerializationStats(stats, 'serializationDuration', elapsed(start, timeStampNow())) + + for (const record of records) { + emitRecord(record) + } + + emitStats(stats) +} diff --git a/packages/rum/src/domain/record/serialization/serializeAttributes.ts b/packages/rum/src/domain/record/serialization/serializeAttributes.ts index ecce4028ef..ac3fb96747 100644 --- a/packages/rum/src/domain/record/serialization/serializeAttributes.ts +++ b/packages/rum/src/domain/record/serialization/serializeAttributes.ts @@ -1,15 +1,14 @@ import { NodePrivacyLevel, shouldMaskNode } from '@datadog/browser-rum-core' import { isSafari } from '@datadog/browser-core' import { getElementInputValue, switchToAbsoluteUrl, getValidTagName } from './serializationUtils' -import type { SerializeOptions } from './serialization.types' -import { SerializationContextStatus } from './serialization.types' import { serializeAttribute } from './serializeAttribute' -import { updateSerializationStats } from './serializationStats' +import type { SerializationTransaction } from './serializationTransaction' +import { SerializationKind } from './serializationTransaction' export function serializeAttributes( element: Element, nodePrivacyLevel: NodePrivacyLevel, - options: SerializeOptions + transaction: SerializationTransaction ): Record { if (nodePrivacyLevel === NodePrivacyLevel.HIDDEN) { return {} @@ -21,7 +20,7 @@ export function serializeAttributes( for (let i = 0; i < element.attributes.length; i += 1) { const attribute = element.attributes.item(i)! const attributeName = attribute.name - const attributeValue = serializeAttribute(element, nodePrivacyLevel, attributeName, options.configuration) + const attributeValue = serializeAttribute(element, nodePrivacyLevel, attributeName, transaction.scope.configuration) if (attributeValue !== null) { safeAttrs[attributeName] = attributeValue } @@ -53,7 +52,7 @@ export function serializeAttributes( const stylesheet = Array.from(doc.styleSheets).find((s) => s.href === (element as HTMLLinkElement).href) const cssText = getCssRulesString(stylesheet) if (cssText && stylesheet) { - updateSerializationStats(options.serializationContext.serializationStats, 'cssText', cssText.length) + transaction.addMetric('cssText', cssText.length) safeAttrs._cssText = cssText } } @@ -62,7 +61,7 @@ export function serializeAttributes( if (tagName === 'style' && (element as HTMLStyleElement).sheet) { const cssText = getCssRulesString((element as HTMLStyleElement).sheet) if (cssText) { - updateSerializationStats(options.serializationContext.serializationStats, 'cssText', cssText.length) + transaction.addMetric('cssText', cssText.length) safeAttrs._cssText = cssText } } @@ -97,18 +96,17 @@ export function serializeAttributes( */ let scrollTop: number | undefined let scrollLeft: number | undefined - const serializationContext = options.serializationContext - switch (serializationContext.status) { - case SerializationContextStatus.INITIAL_FULL_SNAPSHOT: + switch (transaction.kind) { + case SerializationKind.INITIAL_FULL_SNAPSHOT: scrollTop = Math.round(element.scrollTop) scrollLeft = Math.round(element.scrollLeft) if (scrollTop || scrollLeft) { - serializationContext.elementsScrollPositions.set(element, { scrollTop, scrollLeft }) + transaction.scope.elementsScrollPositions.set(element, { scrollTop, scrollLeft }) } break - case SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT: - if (serializationContext.elementsScrollPositions.has(element)) { - ;({ scrollTop, scrollLeft } = serializationContext.elementsScrollPositions.get(element)!) + case SerializationKind.SUBSEQUENT_FULL_SNAPSHOT: + if (transaction.scope.elementsScrollPositions.has(element)) { + ;({ scrollTop, scrollLeft } = transaction.scope.elementsScrollPositions.get(element)!) } break } diff --git a/packages/rum/src/domain/record/serialization/serializeDocument.ts b/packages/rum/src/domain/record/serialization/serializeDocument.ts index a92704c2eb..d32ab214c5 100644 --- a/packages/rum/src/domain/record/serialization/serializeDocument.ts +++ b/packages/rum/src/domain/record/serialization/serializeDocument.ts @@ -1,29 +1,14 @@ -import { elapsed, timeStampNow } from '@datadog/browser-core' -import type { RumConfiguration } from '@datadog/browser-rum-core' -import type { SerializedNodeWithId } from '../../../types' -import type { SerializationContext } from './serialization.types' +import type { DocumentNode, SerializedNodeWithId } from '../../../types' import { serializeNodeWithId } from './serializeNode' -import type { SerializationScope } from './serializationScope' -import { updateSerializationStats } from './serializationStats' +import type { SerializationTransaction } from './serializationTransaction' export function serializeDocument( document: Document, - configuration: RumConfiguration, - scope: SerializationScope, - serializationContext: SerializationContext -): SerializedNodeWithId { - const serializationStart = timeStampNow() - const serializedNode = serializeNodeWithId(document, configuration.defaultPrivacyLevel, { - serializationContext, - configuration, - scope, - }) - updateSerializationStats( - serializationContext.serializationStats, - 'serializationDuration', - elapsed(serializationStart, timeStampNow()) - ) + transaction: SerializationTransaction +): DocumentNode & SerializedNodeWithId { + const defaultPrivacyLevel = transaction.scope.configuration.defaultPrivacyLevel + const serializedNode = serializeNodeWithId(document, defaultPrivacyLevel, transaction) // We are sure that Documents are never ignored, so this function never returns null - return serializedNode! + 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 26063ec235..90b7b9e00a 100644 --- a/packages/rum/src/domain/record/serialization/serializeNode.spec.ts +++ b/packages/rum/src/domain/record/serialization/serializeNode.spec.ts @@ -1,5 +1,4 @@ -import { noop } from '@datadog/browser-core' -import type { RumConfiguration, BrowserWindow } from '@datadog/browser-rum-core' +import type { BrowserWindow } from '@datadog/browser-rum-core' import { isAdoptedStyleSheetsSupported, registerCleanupTask } from '@datadog/browser-core/test' import { NodePrivacyLevel, @@ -11,13 +10,14 @@ import { PRIVACY_ATTR_VALUE_MASK_UNLESS_ALLOWLISTED, isAllowlisted, } from '@datadog/browser-rum-core' -import type { ElementNode, SerializedNodeWithId } from '../../../types' +import type { SerializedNodeWithId } from '../../../types' import { NodeType } from '../../../types' import { appendElement } from '../../../../../rum-core/test' -import type { ElementsScrollPositions } from '../elementsScrollPositions' -import { createElementsScrollPositions } from '../elementsScrollPositions' -import type { ShadowRootCallBack, ShadowRootsController } from '../shadowRootsController' -import { createNodeIds } from '../nodeIds' +import type { AddShadowRootCallBack } from '../shadowRootsController' +import { createSerializationTransactionForTesting } from '../test/serialization.specHelper' +import { createRecordingScopeForTesting } from '../test/recordingScope.specHelper' +import type { EmitRecordCallback, EmitStatsCallback } from '../record.types' +import type { RecordingScope } from '../recordingScope' import { HTML, generateLeanSerializedDoc, @@ -27,51 +27,31 @@ import { AST_MASK_UNLESS_ALLOWLISTED, AST_ALLOW, } from './htmlAst.specHelper' -import { serializeDocument } from './serializeDocument' -import type { SerializationContext, SerializeOptions } from './serialization.types' -import { SerializationContextStatus } from './serialization.types' import { serializeChildNodes, serializeDocumentNode, serializeNodeWithId } from './serializeNode' -import type { SerializationScope } from './serializationScope' -import { createSerializationScope } from './serializationScope' +import type { SerializationStats } from './serializationStats' import { createSerializationStats } from './serializationStats' - -const DEFAULT_CONFIGURATION = {} as RumConfiguration - -const DEFAULT_SHADOW_ROOT_CONTROLLER: ShadowRootsController = { - flush: noop, - stop: noop, - addShadowRoot: noop, - removeShadowRoot: noop, -} - -function getDefaultSerializationContext(): SerializationContext { - return { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, - elementsScrollPositions: createElementsScrollPositions(), - } -} +import type { SerializationTransaction } from './serializationTransaction' +import { serializeInTransaction, SerializationKind } from './serializationTransaction' describe('serializeNodeWithId', () => { - let addShadowRootSpy: jasmine.Spy - let scope: SerializationScope - - const getDefaultOptions = (): SerializeOptions => ({ - serializationContext: getDefaultSerializationContext(), - configuration: DEFAULT_CONFIGURATION, - scope, - }) + let addShadowRootSpy: jasmine.Spy + let emitRecordCallback: jasmine.Spy + let emitStatsCallback: jasmine.Spy + let scope: RecordingScope + let transaction: SerializationTransaction beforeEach(() => { - addShadowRootSpy = jasmine.createSpy() - scope = createSerializationScope(createNodeIds()) + addShadowRootSpy = jasmine.createSpy() + emitRecordCallback = jasmine.createSpy() + emitStatsCallback = jasmine.createSpy() + scope = createRecordingScopeForTesting({ addShadowRoot: addShadowRootSpy }) + transaction = createSerializationTransactionForTesting({ scope }) }) describe('document serialization', () => { it('serializes a document', () => { const document = new DOMParser().parseFromString('foo', 'text/html') - expect(serializeDocument(document, DEFAULT_CONFIGURATION, scope, getDefaultSerializationContext())).toEqual({ + expect(serializeNodeWithId(document, NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Document, childNodes: [ jasmine.objectContaining({ type: NodeType.DocumentType, name: 'html', publicId: '', systemId: '' }), @@ -84,17 +64,8 @@ describe('serializeNodeWithId', () => { }) describe('elements serialization', () => { - function serializeElement( - node: Element, - options: SerializeOptions | undefined = undefined - ): (ElementNode & { id: number }) | null { - return serializeNodeWithId(node, NodePrivacyLevel.ALLOW, options ?? getDefaultOptions()) as - | (ElementNode & { id: number }) - | null - } - it('serializes a div', () => { - expect(serializeElement(document.createElement('div'))).toEqual({ + expect(serializeNodeWithId(document.createElement('div'), NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: {}, @@ -108,7 +79,7 @@ describe('serializeNodeWithId', () => { const element = document.createElement('div') element.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN) - expect(serializeElement(element)).toEqual({ + expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)).toEqual({ type: NodeType.Element, tagName: 'div', attributes: { @@ -126,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(serializeElement(element)!.childNodes).toEqual([]) + expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([]) }) it('serializes attributes', () => { @@ -134,7 +105,7 @@ describe('serializeNodeWithId', () => { element.className = 'zog' element.style.width = '10px' - expect(serializeElement(element)!.attributes).toEqual({ + expect(serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ foo: 'bar', 'data-foo': 'data-bar', class: 'zog', @@ -144,66 +115,52 @@ describe('serializeNodeWithId', () => { describe('rr scroll attributes', () => { let element: HTMLElement - let elementsScrollPositions: ElementsScrollPositions beforeEach(() => { element = appendElement( '
' ) element.scrollBy(10, 20) - elementsScrollPositions = createElementsScrollPositions() }) it('should be retrieved from attributes during initial full snapshot', () => { - const serializedAttributes = serializeElement(element, { - ...getDefaultOptions(), - serializationContext: { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, - elementsScrollPositions, - }, - })!.attributes + const transaction = createSerializationTransactionForTesting({ + kind: SerializationKind.INITIAL_FULL_SNAPSHOT, + scope, + }) + const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) - expect(serializedAttributes).toEqual( + expect(node?.attributes).toEqual( jasmine.objectContaining({ rr_scrollLeft: 10, rr_scrollTop: 20, }) ) - expect(elementsScrollPositions.get(element)).toEqual({ scrollLeft: 10, scrollTop: 20 }) + expect(scope.elementsScrollPositions.get(element)).toEqual({ scrollLeft: 10, scrollTop: 20 }) }) it('should not be retrieved from attributes during subsequent full snapshot', () => { - const serializedAttributes = serializeElement(element, { - ...getDefaultOptions(), - serializationContext: { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, - elementsScrollPositions, - }, - })!.attributes + const transaction = createSerializationTransactionForTesting({ + kind: SerializationKind.SUBSEQUENT_FULL_SNAPSHOT, + scope, + }) + const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) - expect(serializedAttributes.rr_scrollLeft).toBeUndefined() - expect(serializedAttributes.rr_scrollTop).toBeUndefined() - expect(elementsScrollPositions.get(element)).toBeUndefined() + expect(node?.attributes.rr_scrollLeft).toBeUndefined() + expect(node?.attributes.rr_scrollTop).toBeUndefined() + expect(scope.elementsScrollPositions.get(element)).toBeUndefined() }) it('should be retrieved from elementsScrollPositions during subsequent full snapshot', () => { - elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) - - const serializedAttributes = serializeElement(element, { - ...getDefaultOptions(), - serializationContext: { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT, - elementsScrollPositions, - }, - })!.attributes + scope.elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) - expect(serializedAttributes).toEqual( + const transaction = createSerializationTransactionForTesting({ + kind: SerializationKind.SUBSEQUENT_FULL_SNAPSHOT, + scope, + }) + const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + + expect(node?.attributes).toEqual( jasmine.objectContaining({ rr_scrollLeft: 10, rr_scrollTop: 20, @@ -212,19 +169,16 @@ describe('serializeNodeWithId', () => { }) it('should not be retrieved during mutation', () => { - elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) - - const serializedAttributes = serializeElement(element, { - ...getDefaultOptions(), - serializationContext: { - serializationStats: createSerializationStats(), - shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, - status: SerializationContextStatus.MUTATION, - }, - })!.attributes + scope.elementsScrollPositions.set(element, { scrollLeft: 10, scrollTop: 20 }) - expect(serializedAttributes.rr_scrollLeft).toBeUndefined() - expect(serializedAttributes.rr_scrollTop).toBeUndefined() + const transaction = createSerializationTransactionForTesting({ + kind: SerializationKind.INCREMENTAL_SNAPSHOT, + scope, + }) + const node = serializeNodeWithId(element, NodePrivacyLevel.ALLOW, transaction) + + expect(node?.attributes.rr_scrollLeft).toBeUndefined() + expect(node?.attributes.rr_scrollTop).toBeUndefined() }) }) @@ -232,7 +186,7 @@ describe('serializeNodeWithId', () => { const head = document.createElement('head') head.innerHTML = ' foo ' - expect(serializeElement(head)!.childNodes).toEqual([ + expect(serializeNodeWithId(head, NodePrivacyLevel.ALLOW, transaction)?.childNodes).toEqual([ jasmine.objectContaining({ type: NodeType.Element, tagName: 'title', @@ -245,7 +199,7 @@ describe('serializeNodeWithId', () => { const input = document.createElement('input') input.value = 'toto' - expect(serializeElement(input)!).toEqual( + expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) @@ -256,7 +210,7 @@ describe('serializeNodeWithId', () => { const textarea = document.createElement('textarea') textarea.value = 'toto' - expect(serializeElement(textarea)!).toEqual( + expect(serializeNodeWithId(textarea, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'toto' }, }) @@ -273,7 +227,7 @@ describe('serializeNodeWithId', () => { select.appendChild(option2) select.options.selectedIndex = 1 - expect(serializeElement(select)!).toEqual( + expect(serializeNodeWithId(select, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { value: 'bar' }, childNodes: [ @@ -298,7 +252,7 @@ describe('serializeNodeWithId', () => { input.type = 'password' input.value = 'toto' - expect(serializeElement(input)!).toEqual(jasmine.objectContaining({})) + expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) }) it('does not serialize values set via attribute setter', () => { @@ -306,13 +260,13 @@ describe('serializeNodeWithId', () => { input.type = 'password' input.setAttribute('value', 'toto') - expect(serializeElement(input)!).toEqual(jasmine.objectContaining({})) + expect(serializeNodeWithId(input, NodePrivacyLevel.ALLOW, transaction)).toEqual(jasmine.objectContaining({})) }) it('serializes elements checked state', () => { const checkbox = document.createElement('input') checkbox.type = 'checkbox' - expect(serializeElement(checkbox)!).toEqual( + expect(serializeNodeWithId(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { type: 'checkbox', @@ -324,7 +278,7 @@ describe('serializeNodeWithId', () => { checkbox.checked = true - expect(serializeElement(checkbox)!).toEqual( + expect(serializeNodeWithId(checkbox, NodePrivacyLevel.ALLOW, transaction)).toEqual( jasmine.objectContaining({ attributes: { type: 'checkbox', @@ -338,7 +292,7 @@ describe('serializeNodeWithId', () => { it('serializes elements checked state', () => { const radio = document.createElement('input') radio.type = 'radio' - expect(serializeElement(radio)!.attributes).toEqual({ + expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ type: 'radio', value: 'on', checked: false, @@ -346,7 +300,7 @@ describe('serializeNodeWithId', () => { radio.checked = true - expect(serializeElement(radio)!.attributes).toEqual({ + expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({ type: 'radio', value: 'on', checked: true, @@ -356,7 +310,7 @@ describe('serializeNodeWithId', () => { it('serializes