Skip to content

Commit 9632043

Browse files
🎨 [PANA-4929] Add the concept of serialization transactions and reorganize state (#3982)
1 parent 06eee4c commit 9632043

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+926
-1019
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export interface EventIds {
2+
getIdForEvent(event: Event): number
3+
}
4+
5+
export function createEventIds(): EventIds {
6+
const eventIds = new WeakMap<Event, number>()
7+
let nextId = 1
8+
9+
return {
10+
getIdForEvent(event: Event): number {
11+
if (!eventIds.has(event)) {
12+
eventIds.set(event, nextId++)
13+
}
14+
return eventIds.get(event)!
15+
},
16+
}
17+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { record } from './record'
22
export type { SerializationMetric, SerializationStats } from './serialization'
33
export { createSerializationStats, aggregateSerializationStats } from './serialization'
4-
export { serializeNodeWithId, serializeDocument, SerializationContextStatus } from './serialization'
4+
export { serializeNodeWithId, serializeDocument } from './serialization'
55
export { createElementsScrollPositions } from './elementsScrollPositions'
66
export type { ShadowRootsController } from './shadowRootsController'

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

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import { createElementsScrollPositions } from './elementsScrollPositions'
2121
import type { ShadowRootsController } from './shadowRootsController'
2222
import { initShadowRootsController } from './shadowRootsController'
2323
import { startFullSnapshots } from './startFullSnapshots'
24-
import { initRecordIds } from './recordIds'
25-
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
26-
import { createSerializationScope } from './serialization'
24+
import { createEventIds } from './eventIds'
2725
import { createNodeIds } from './nodeIds'
26+
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
27+
import { createRecordingScope } from './recordingScope'
2828

2929
export interface RecordOptions {
3030
emitRecord: EmitRecordCallback
@@ -54,47 +54,36 @@ export function record(options: RecordOptions): RecordAPI {
5454
replayStats.addRecord(view.id)
5555
}
5656

57-
const elementsScrollPositions = createElementsScrollPositions()
58-
const scope = createSerializationScope(createNodeIds())
59-
const shadowRootsController = initShadowRootsController(
57+
const shadowRootsController = initShadowRootsController(processRecord, emitStats)
58+
const scope = createRecordingScope(
6059
configuration,
61-
scope,
62-
processRecord,
63-
emitStats,
64-
elementsScrollPositions
60+
createElementsScrollPositions(),
61+
createEventIds(),
62+
createNodeIds(),
63+
shadowRootsController
6564
)
6665

67-
const { stop: stopFullSnapshots } = startFullSnapshots(
68-
elementsScrollPositions,
69-
shadowRootsController,
70-
lifeCycle,
71-
configuration,
72-
scope,
73-
flushMutations,
74-
processRecord,
75-
emitStats
76-
)
66+
const { stop: stopFullSnapshots } = startFullSnapshots(lifeCycle, processRecord, emitStats, flushMutations, scope)
7767

7868
function flushMutations() {
7969
shadowRootsController.flush()
8070
mutationTracker.flush()
8171
}
8272

83-
const recordIds = initRecordIds()
84-
const mutationTracker = trackMutation(processRecord, emitStats, configuration, scope, shadowRootsController, document)
73+
const mutationTracker = trackMutation(document, processRecord, emitStats, scope)
8574
const trackers: Tracker[] = [
8675
mutationTracker,
87-
trackMove(configuration, scope, processRecord),
88-
trackMouseInteraction(configuration, scope, processRecord, recordIds),
89-
trackScroll(configuration, scope, processRecord, elementsScrollPositions, document),
90-
trackViewportResize(configuration, processRecord),
91-
trackInput(configuration, scope, processRecord),
92-
trackMediaInteraction(configuration, scope, processRecord),
93-
trackStyleSheet(scope, processRecord),
94-
trackFocus(configuration, processRecord),
95-
trackVisualViewportResize(configuration, processRecord),
96-
trackFrustration(lifeCycle, processRecord, recordIds),
97-
trackViewEnd(lifeCycle, flushMutations, processRecord),
76+
trackMove(processRecord, scope),
77+
trackMouseInteraction(processRecord, scope),
78+
trackScroll(document, processRecord, scope),
79+
trackViewportResize(processRecord, scope),
80+
trackInput(document, processRecord, scope),
81+
trackMediaInteraction(processRecord, scope),
82+
trackStyleSheet(processRecord, scope),
83+
trackFocus(processRecord, scope),
84+
trackVisualViewportResize(processRecord, scope),
85+
trackFrustration(lifeCycle, processRecord, scope),
86+
trackViewEnd(lifeCycle, processRecord, flushMutations),
9887
]
9988

10089
return {

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

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { RumConfiguration } from '@datadog/browser-rum-core'
2+
3+
import type { ElementsScrollPositions } from './elementsScrollPositions'
4+
import type { EventIds } from './eventIds'
5+
import type { NodeIds } from './nodeIds'
6+
import type { ShadowRootsController } from './shadowRootsController'
7+
8+
/**
9+
* State associated with a stream of session replay records. When a new stream of records
10+
* starts (e.g. because recording has shut down and restarted), a new RecordingScope
11+
* object must be created; this ensures that we don't generate records that reference ids
12+
* or data which aren't present in the current stream.
13+
*/
14+
export interface RecordingScope {
15+
configuration: RumConfiguration
16+
elementsScrollPositions: ElementsScrollPositions
17+
eventIds: EventIds
18+
nodeIds: NodeIds
19+
shadowRootsController: ShadowRootsController
20+
}
21+
22+
export function createRecordingScope(
23+
configuration: RumConfiguration,
24+
elementsScrollPositions: ElementsScrollPositions,
25+
eventIds: EventIds,
26+
nodeIds: NodeIds,
27+
shadowRootsController: ShadowRootsController
28+
): RecordingScope {
29+
return {
30+
configuration,
31+
elementsScrollPositions,
32+
eventIds,
33+
nodeIds,
34+
shadowRootsController,
35+
}
36+
}

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

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import type { RumConfiguration } from '@datadog/browser-rum-core'
21
import { NodePrivacyLevel, PRIVACY_ATTR_NAME } from '@datadog/browser-rum-core'
3-
import { display, noop, objectValues } from '@datadog/browser-core'
2+
import { display, objectValues } from '@datadog/browser-core'
43
import type { SerializedNodeWithId } from '../../../types'
5-
import {
6-
serializeNodeWithId,
7-
SerializationContextStatus,
8-
createElementsScrollPositions,
9-
createSerializationStats,
10-
} from '..'
11-
import { createNodeIds } from '../nodeIds'
12-
import { createSerializationScope } from './serializationScope'
4+
import { createSerializationTransactionForTesting } from '../test/serialization.specHelper'
5+
import { serializeNodeWithId } from './serializeNode'
136

147
export const makeHtmlDoc = (htmlContent: string, privacyTag: string) => {
158
try {
@@ -33,26 +26,11 @@ export const removeIdFieldsRecursivelyClone = (thing: Record<string, unknown>):
3326
return thing
3427
}
3528

36-
const DEFAULT_SHADOW_ROOT_CONTROLLER = {
37-
flush: noop,
38-
stop: noop,
39-
addShadowRoot: noop,
40-
removeShadowRoot: noop,
41-
}
42-
4329
export const generateLeanSerializedDoc = (htmlContent: string, privacyTag: string) => {
30+
const transaction = createSerializationTransactionForTesting()
4431
const newDoc = makeHtmlDoc(htmlContent, privacyTag)
4532
const serializedDoc = removeIdFieldsRecursivelyClone(
46-
serializeNodeWithId(newDoc, NodePrivacyLevel.ALLOW, {
47-
serializationContext: {
48-
serializationStats: createSerializationStats(),
49-
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
50-
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,
51-
elementsScrollPositions: createElementsScrollPositions(),
52-
},
53-
configuration: {} as RumConfiguration,
54-
scope: createSerializationScope(createNodeIds()),
55-
})! as unknown as Record<string, unknown>
33+
serializeNodeWithId(newDoc, NodePrivacyLevel.ALLOW, transaction) as unknown as Record<string, unknown>
5634
) as unknown as SerializedNodeWithId
5735
return serializedDoc
5836
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
export { getElementInputValue } from './serializationUtils'
2-
export { SerializationContextStatus } from './serialization.types'
3-
export type { SerializationContext } from './serialization.types'
42
export { serializeDocument } from './serializeDocument'
53
export { serializeNodeWithId } from './serializeNode'
64
export { serializeAttribute } from './serializeAttribute'
7-
export type { SerializationScope } from './serializationScope'
8-
export { createSerializationScope } from './serializationScope'
95
export { createSerializationStats, updateSerializationStats, aggregateSerializationStats } from './serializationStats'
106
export type { SerializationMetric, SerializationStats } from './serializationStats'
7+
export { serializeInTransaction, SerializationKind } from './serializationTransaction'
8+
export type { SerializationTransaction, SerializationTransactionCallback } from './serializationTransaction'
Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import type { RumConfiguration, NodePrivacyLevel } from '@datadog/browser-rum-core'
2-
import type { ElementsScrollPositions } from '../elementsScrollPositions'
3-
import type { ShadowRootsController } from '../shadowRootsController'
4-
import type { SerializationScope } from './serializationScope'
5-
import type { SerializationStats } from './serializationStats'
1+
import type { NodePrivacyLevel } from '@datadog/browser-rum-core'
62

73
// Those values are the only one that can be used when inheriting privacy levels from parent to
84
// children during serialization, since HIDDEN and IGNORE shouldn't serialize their children. This
@@ -12,35 +8,3 @@ export type ParentNodePrivacyLevel =
128
| typeof NodePrivacyLevel.MASK
139
| typeof NodePrivacyLevel.MASK_USER_INPUT
1410
| typeof NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED
15-
16-
export const enum SerializationContextStatus {
17-
INITIAL_FULL_SNAPSHOT,
18-
SUBSEQUENT_FULL_SNAPSHOT,
19-
MUTATION,
20-
}
21-
22-
export type SerializationContext =
23-
| {
24-
status: SerializationContextStatus.MUTATION
25-
serializationStats: SerializationStats
26-
shadowRootsController: ShadowRootsController
27-
}
28-
| {
29-
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT
30-
elementsScrollPositions: ElementsScrollPositions
31-
serializationStats: SerializationStats
32-
shadowRootsController: ShadowRootsController
33-
}
34-
| {
35-
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT
36-
elementsScrollPositions: ElementsScrollPositions
37-
serializationStats: SerializationStats
38-
shadowRootsController: ShadowRootsController
39-
}
40-
41-
export interface SerializeOptions {
42-
serializedNodeIds?: Set<number>
43-
serializationContext: SerializationContext
44-
configuration: RumConfiguration
45-
scope: SerializationScope
46-
}

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

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { elapsed, timeStampNow } from '@datadog/browser-core'
2+
3+
import type { BrowserRecord } from '../../../types'
4+
import type { NodeId } from '../nodeIds'
5+
import type { EmitRecordCallback, EmitStatsCallback } from '../record.types'
6+
import type { RecordingScope } from '../recordingScope'
7+
import type { SerializationStats } from './serializationStats'
8+
import { createSerializationStats, updateSerializationStats } from './serializationStats'
9+
10+
export type SerializationTransactionCallback = (transaction: SerializationTransaction) => void
11+
12+
export const enum SerializationKind {
13+
INITIAL_FULL_SNAPSHOT,
14+
SUBSEQUENT_FULL_SNAPSHOT,
15+
INCREMENTAL_SNAPSHOT,
16+
}
17+
18+
/**
19+
* A serialization transaction is used to build and emit a sequence of session replay
20+
* records containing a serialized snapshot of the DOM.
21+
*/
22+
export interface SerializationTransaction {
23+
/** Add a record to the transaction. It will be emitted when the transaction ends. */
24+
add(record: BrowserRecord): void
25+
26+
/**
27+
* Add a metric to the transaction's statistics. The aggregated statistics will be
28+
* emitted when the transaction ends.
29+
*/
30+
addMetric(metric: keyof SerializationStats, value: number): void
31+
32+
/** The kind of serialization being performed in this transaction. */
33+
kind: SerializationKind
34+
35+
/**
36+
* A set used to track nodes which have been serialized in the current transaction. If
37+
* undefined, this feature is disabled; this is the default state in new transactions
38+
* for performance reasons. Set the property to a non-undefined value if you need this
39+
* capability.
40+
*/
41+
serializedNodeIds?: Set<NodeId>
42+
43+
/** The recording scope in which this transaction is occurring. */
44+
scope: RecordingScope
45+
}
46+
47+
/**
48+
* Perform serialization within a transaction. At the end of the transaction, the
49+
* generated records and statistics will be emitted.
50+
*/
51+
export function serializeInTransaction(
52+
kind: SerializationKind,
53+
emitRecord: EmitRecordCallback,
54+
emitStats: EmitStatsCallback,
55+
scope: RecordingScope,
56+
serialize: SerializationTransactionCallback
57+
): void {
58+
const records: BrowserRecord[] = []
59+
const stats = createSerializationStats()
60+
61+
const transaction: SerializationTransaction = {
62+
add: (record: BrowserRecord) => {
63+
records.push(record)
64+
},
65+
addMetric: (metric: keyof SerializationStats, value: number) => {
66+
updateSerializationStats(stats, metric, value)
67+
},
68+
kind,
69+
scope,
70+
}
71+
72+
const start = timeStampNow()
73+
serialize(transaction)
74+
updateSerializationStats(stats, 'serializationDuration', elapsed(start, timeStampNow()))
75+
76+
for (const record of records) {
77+
emitRecord(record)
78+
}
79+
80+
emitStats(stats)
81+
}

0 commit comments

Comments
 (0)