Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/rum/src/domain/record/eventIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface EventIds {
getIdForEvent(event: Event): number
}

export function createEventIds(): EventIds {
const eventIds = new WeakMap<Event, number>()
let nextId = 1

return {
getIdForEvent(event: Event): number {
if (!eventIds.has(event)) {
eventIds.set(event, nextId++)
}
return eventIds.get(event)!
},
}
}
2 changes: 1 addition & 1 deletion packages/rum/src/domain/record/index.ts
Original file line number Diff line number Diff line change
@@ -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'
55 changes: 22 additions & 33 deletions packages/rum/src/domain/record/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 0 additions & 15 deletions packages/rum/src/domain/record/recordIds.ts

This file was deleted.

36 changes: 36 additions & 0 deletions packages/rum/src/domain/record/recordingScope.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -33,26 +26,11 @@ export const removeIdFieldsRecursivelyClone = (thing: Record<string, unknown>):
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<string, unknown>
serializeNodeWithId(newDoc, NodePrivacyLevel.ALLOW, transaction) as unknown as Record<string, unknown>
) as unknown as SerializedNodeWithId
return serializedDoc
}
Expand Down
6 changes: 2 additions & 4 deletions packages/rum/src/domain/record/serialization/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<number>
serializationContext: SerializationContext
configuration: RumConfiguration
scope: SerializationScope
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<NodeId>

/** 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)
}
Loading