Skip to content

Commit 59e0144

Browse files
🎨 [PANA-4976] Separate emission of replay records and serialization stats (#3978)
1 parent d432ec3 commit 59e0144

32 files changed

+339
-304
lines changed

‎packages/rum/src/boot/startRecording.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RawError, HttpRequest, DeflateEncoder, Telemetry } from '@datadog/browser-core'
2-
import { createHttpRequest, addTelemetryDebug, canUseEventBridge } from '@datadog/browser-core'
2+
import { createHttpRequest, addTelemetryDebug, canUseEventBridge, noop } from '@datadog/browser-core'
33
import type { LifeCycle, ViewHistory, RumConfiguration, RumSessionManager } from '@datadog/browser-rum-core'
44
import { LifeCycleEventType } from '@datadog/browser-rum-core'
55

@@ -30,7 +30,8 @@ export function startRecording(
3030
const replayRequest =
3131
httpRequest || createHttpRequest([configuration.sessionReplayEndpointBuilder], reportError, SEGMENT_BYTES_LIMIT)
3232

33-
let addRecord: (record: BrowserRecord, stats?: SerializationStats) => void
33+
let addRecord: (record: BrowserRecord) => void
34+
let addStats: (stats: SerializationStats) => void
3435

3536
if (!canUseEventBridge()) {
3637
const segmentCollection = startSegmentCollection(
@@ -42,16 +43,19 @@ export function startRecording(
4243
encoder
4344
)
4445
addRecord = segmentCollection.addRecord
46+
addStats = segmentCollection.addStats
4547
cleanupTasks.push(segmentCollection.stop)
4648

4749
const segmentTelemetry = startSegmentTelemetry(telemetry, replayRequest.observable)
4850
cleanupTasks.push(segmentTelemetry.stop)
4951
} else {
5052
;({ addRecord } = startRecordBridge(viewHistory))
53+
addStats = noop
5154
}
5255

5356
const { stop: stopRecording } = record({
54-
emit: addRecord,
57+
emitRecord: addRecord,
58+
emitStats: addStats,
5559
configuration,
5660
lifeCycle,
5761
viewHistory,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DefaultPrivacyLevel, findLast } from '@datadog/browser-core'
1+
import { DefaultPrivacyLevel, findLast, noop } from '@datadog/browser-core'
22
import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-core'
33
import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core'
44
import { createNewEvent, collectAsyncCalls, registerCleanupTask } from '@datadog/browser-core/test'
@@ -22,11 +22,12 @@ import { appendElement } from '../../../../rum-core/test'
2222
import { getReplayStats, resetReplayStats } from '../replayStats'
2323
import type { RecordAPI } from './record'
2424
import { record } from './record'
25+
import type { EmitRecordCallback } from './record.types'
2526

2627
describe('record', () => {
2728
let recordApi: RecordAPI
2829
let lifeCycle: LifeCycle
29-
let emitSpy: jasmine.Spy<(record: BrowserRecord) => void>
30+
let emitSpy: jasmine.Spy<EmitRecordCallback>
3031
const FAKE_VIEW_ID = '123'
3132

3233
beforeEach(() => {
@@ -450,7 +451,8 @@ describe('record', () => {
450451
function startRecording() {
451452
lifeCycle = new LifeCycle()
452453
recordApi = record({
453-
emit: emitSpy,
454+
emitRecord: emitSpy,
455+
emitStats: noop,
454456
configuration: { defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW } as RumConfiguration,
455457
lifeCycle,
456458
viewHistory: {

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

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { sendToExtension } from '@datadog/browser-core'
22
import type { LifeCycle, RumConfiguration, ViewHistory } from '@datadog/browser-rum-core'
3-
import type { BrowserRecord } from '../../types'
43
import * as replayStats from '../replayStats'
4+
import type { BrowserRecord } from '../../types'
55
import type { Tracker } from './trackers'
66
import {
77
trackFocus,
@@ -22,12 +22,13 @@ import type { ShadowRootsController } from './shadowRootsController'
2222
import { initShadowRootsController } from './shadowRootsController'
2323
import { startFullSnapshots } from './startFullSnapshots'
2424
import { initRecordIds } from './recordIds'
25-
import type { SerializationStats } from './serialization'
25+
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
2626
import { createSerializationScope } from './serialization'
2727
import { createNodeIds } from './nodeIds'
2828

2929
export interface RecordOptions {
30-
emit?: (record: BrowserRecord, stats?: SerializationStats) => void
30+
emitRecord: EmitRecordCallback
31+
emitStats: EmitStatsCallback
3132
configuration: RumConfiguration
3233
lifeCycle: LifeCycle
3334
viewHistory: ViewHistory
@@ -40,14 +41,14 @@ export interface RecordAPI {
4041
}
4142

4243
export function record(options: RecordOptions): RecordAPI {
43-
const { emit, configuration, lifeCycle } = options
44+
const { emitRecord, emitStats, configuration, lifeCycle } = options
4445
// runtime checks for user options
45-
if (!emit) {
46-
throw new Error('emit function is required')
46+
if (!emitRecord || !emitStats) {
47+
throw new Error('emit functions are required')
4748
}
4849

49-
const emitAndComputeStats = (record: BrowserRecord, stats?: SerializationStats) => {
50-
emit(record, stats)
50+
const processRecord: EmitRecordCallback = (record: BrowserRecord) => {
51+
emitRecord(record)
5152
sendToExtension('record', { record })
5253
const view = options.viewHistory.findView()!
5354
replayStats.addRecord(view.id)
@@ -58,7 +59,8 @@ export function record(options: RecordOptions): RecordAPI {
5859
const shadowRootsController = initShadowRootsController(
5960
configuration,
6061
scope,
61-
emitAndComputeStats,
62+
processRecord,
63+
emitStats,
6264
elementsScrollPositions
6365
)
6466

@@ -69,7 +71,8 @@ export function record(options: RecordOptions): RecordAPI {
6971
configuration,
7072
scope,
7173
flushMutations,
72-
emitAndComputeStats
74+
processRecord,
75+
emitStats
7376
)
7477

7578
function flushMutations() {
@@ -78,23 +81,20 @@ export function record(options: RecordOptions): RecordAPI {
7881
}
7982

8083
const recordIds = initRecordIds()
81-
const mutationTracker = trackMutation(emitAndComputeStats, configuration, scope, shadowRootsController, document)
84+
const mutationTracker = trackMutation(processRecord, emitStats, configuration, scope, shadowRootsController, document)
8285
const trackers: Tracker[] = [
8386
mutationTracker,
84-
trackMove(configuration, scope, emitAndComputeStats),
85-
trackMouseInteraction(configuration, scope, emitAndComputeStats, recordIds),
86-
trackScroll(configuration, scope, emitAndComputeStats, elementsScrollPositions, document),
87-
trackViewportResize(configuration, emitAndComputeStats),
88-
trackInput(configuration, scope, emitAndComputeStats),
89-
trackMediaInteraction(configuration, scope, emitAndComputeStats),
90-
trackStyleSheet(scope, emitAndComputeStats),
91-
trackFocus(configuration, emitAndComputeStats),
92-
trackVisualViewportResize(configuration, emitAndComputeStats),
93-
trackFrustration(lifeCycle, emitAndComputeStats, recordIds),
94-
trackViewEnd(lifeCycle, (viewEndRecord) => {
95-
flushMutations()
96-
emitAndComputeStats(viewEndRecord)
97-
}),
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),
9898
]
9999

100100
return {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { BrowserRecord } from '../../types'
2+
import type { SerializationStats } from './serialization'
3+
4+
export type EmitRecordCallback<Record extends BrowserRecord = BrowserRecord> = (record: Record) => void
5+
export type EmitStatsCallback = (stats: SerializationStats) => void

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { RumConfiguration } from '@datadog/browser-rum-core'
2-
import type { BrowserIncrementalSnapshotRecord } from '../../types'
32
import { trackInput, trackMutation, trackScroll } from './trackers'
43
import type { ElementsScrollPositions } from './elementsScrollPositions'
4+
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
55
import type { SerializationScope } from './serialization'
66

77
interface ShadowRootController {
@@ -21,7 +21,8 @@ export interface ShadowRootsController {
2121
export const initShadowRootsController = (
2222
configuration: RumConfiguration,
2323
scope: SerializationScope,
24-
callback: (record: BrowserIncrementalSnapshotRecord) => void,
24+
emitRecord: EmitRecordCallback,
25+
emitStats: EmitStatsCallback,
2526
elementsScrollPositions: ElementsScrollPositions
2627
): ShadowRootsController => {
2728
const controllerByShadowRoot = new Map<ShadowRoot, ShadowRootController>()
@@ -31,11 +32,18 @@ export const initShadowRootsController = (
3132
if (controllerByShadowRoot.has(shadowRoot)) {
3233
return
3334
}
34-
const mutationTracker = trackMutation(callback, configuration, scope, shadowRootsController, shadowRoot)
35+
const mutationTracker = trackMutation(
36+
emitRecord,
37+
emitStats,
38+
configuration,
39+
scope,
40+
shadowRootsController,
41+
shadowRoot
42+
)
3543
// The change event does not bubble up across the shadow root, we have to listen on the shadow root
36-
const inputTracker = trackInput(configuration, scope, callback, shadowRoot)
44+
const inputTracker = trackInput(configuration, scope, emitRecord, shadowRoot)
3745
// The scroll event does not bubble up across the shadow root, we have to listen on the shadow root
38-
const scrollTracker = trackScroll(configuration, scope, callback, elementsScrollPositions, shadowRoot)
46+
const scrollTracker = trackScroll(configuration, scope, emitRecord, elementsScrollPositions, shadowRoot)
3947
controllerByShadowRoot.set(shadowRoot, {
4048
flush: () => mutationTracker.flush(),
4149
stop: () => {

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@ import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-co
22
import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core'
33
import type { TimeStamp } from '@datadog/browser-core'
44
import { noop } from '@datadog/browser-core'
5-
import { RecordType, type BrowserRecord } from '../../types'
5+
import { RecordType } from '../../types'
66
import { appendElement } from '../../../../rum-core/test'
77
import { startFullSnapshots } from './startFullSnapshots'
88
import { createElementsScrollPositions } from './elementsScrollPositions'
99
import type { ShadowRootsController } from './shadowRootsController'
10-
import { createSerializationScope, type SerializationStats } from './serialization'
10+
import { createSerializationScope } from './serialization'
1111
import { createNodeIds } from './nodeIds'
12+
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
1213

1314
describe('startFullSnapshots', () => {
1415
const viewStartClock = { relative: 1, timeStamp: 1 as TimeStamp }
1516
let lifeCycle: LifeCycle
16-
let emitCallback: jasmine.Spy<(record: BrowserRecord, stats?: SerializationStats) => void>
17+
let emitRecordCallback: jasmine.Spy<EmitRecordCallback>
18+
let emitStatsCallback: jasmine.Spy<EmitStatsCallback>
1719

1820
beforeEach(() => {
1921
lifeCycle = new LifeCycle()
20-
emitCallback = jasmine.createSpy()
22+
emitRecordCallback = jasmine.createSpy()
23+
emitStatsCallback = jasmine.createSpy()
2124
appendElement('<style>body { width: 100%; }</style>', document.head)
2225
startFullSnapshots(
2326
createElementsScrollPositions(),
@@ -26,39 +29,40 @@ describe('startFullSnapshots', () => {
2629
{} as RumConfiguration,
2730
createSerializationScope(createNodeIds()),
2831
noop,
29-
emitCallback
32+
emitRecordCallback,
33+
emitStatsCallback
3034
)
3135
})
3236

3337
it('takes a full snapshot when startFullSnapshots is called', () => {
34-
expect(emitCallback).toHaveBeenCalled()
38+
expect(emitRecordCallback).toHaveBeenCalled()
3539
})
3640

3741
it('takes a full snapshot when the view changes', () => {
38-
emitCallback.calls.reset()
42+
emitRecordCallback.calls.reset()
3943

4044
lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
4145
startClocks: viewStartClock,
4246
} as Partial<ViewCreatedEvent> as any)
4347

44-
expect(emitCallback).toHaveBeenCalled()
48+
expect(emitRecordCallback).toHaveBeenCalled()
4549
})
4650

4751
it('full snapshot related records should have the view change date', () => {
48-
emitCallback.calls.reset()
52+
emitRecordCallback.calls.reset()
4953

5054
lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
5155
startClocks: viewStartClock,
5256
} as Partial<ViewCreatedEvent> as any)
5357

54-
const records = emitCallback.calls.allArgs().map((args) => args[0])
58+
const records = emitRecordCallback.calls.allArgs().map((args) => args[0])
5559
expect(records[0].timestamp).toEqual(1)
5660
expect(records[1].timestamp).toEqual(1)
5761
expect(records[2].timestamp).toEqual(1)
5862
})
5963

6064
it('full snapshot records should contain Meta, Focus, FullSnapshot', () => {
61-
const records = emitCallback.calls.allArgs().map((args) => args[0])
65+
const records = emitRecordCallback.calls.allArgs().map((args) => args[0])
6266

6367
expect(records).toEqual(
6468
jasmine.arrayContaining([
@@ -97,7 +101,7 @@ describe('startFullSnapshots', () => {
97101
if (!window.visualViewport) {
98102
pending('visualViewport not supported')
99103
}
100-
const record = emitCallback.calls.mostRecent().args[0]
104+
const record = emitRecordCallback.calls.mostRecent().args[0]
101105

102106
expect(record).toEqual({
103107
data: jasmine.any(Object),
@@ -107,8 +111,7 @@ describe('startFullSnapshots', () => {
107111
})
108112

109113
it('full snapshot records should be emitted with serialization stats', () => {
110-
const fullSnapshotEmits = emitCallback.calls.allArgs().filter((args) => args[0].type === RecordType.FullSnapshot)
111-
expect(fullSnapshotEmits[0][1]).toEqual({
114+
expect(emitStatsCallback.calls.mostRecent().args[0]).toEqual({
112115
cssText: { count: 1, max: 21, sum: 21 },
113116
serializationDuration: jasmine.anything(),
114117
})

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { LifeCycleEventType, getScrollX, getScrollY, getViewportDimension } from
22
import type { RumConfiguration, LifeCycle } from '@datadog/browser-rum-core'
33
import { timeStampNow } from '@datadog/browser-core'
44
import type { TimeStamp } from '@datadog/browser-core'
5-
import type { BrowserRecord } from '../../types'
65
import { RecordType } from '../../types'
76
import type { ElementsScrollPositions } from './elementsScrollPositions'
87
import type { ShadowRootsController } from './shadowRootsController'
9-
import type { SerializationContext, SerializationScope, SerializationStats } from './serialization'
8+
import type { SerializationContext, SerializationScope } from './serialization'
109
import { createSerializationStats, SerializationContextStatus, serializeDocument } from './serialization'
1110
import { getVisualViewport } from './viewports'
11+
import type { EmitRecordCallback, EmitStatsCallback } from './record.types'
1212

1313
export function startFullSnapshots(
1414
elementsScrollPositions: ElementsScrollPositions,
@@ -17,11 +17,12 @@ export function startFullSnapshots(
1717
configuration: RumConfiguration,
1818
scope: SerializationScope,
1919
flushMutations: () => void,
20-
emit: (record: BrowserRecord, stats?: SerializationStats) => void
20+
emitRecord: EmitRecordCallback,
21+
emitStats: EmitStatsCallback
2122
) {
2223
const takeFullSnapshot = (timestamp: TimeStamp, status: SerializationContextStatus) => {
2324
const { width, height } = getViewportDimension()
24-
emit({
25+
emitRecord({
2526
data: {
2627
height,
2728
href: window.location.href,
@@ -30,7 +31,8 @@ export function startFullSnapshots(
3031
type: RecordType.Meta,
3132
timestamp,
3233
})
33-
emit({
34+
35+
emitRecord({
3436
data: {
3537
has_focus: document.hasFocus(),
3638
},
@@ -45,23 +47,21 @@ export function startFullSnapshots(
4547
serializationStats,
4648
shadowRootsController,
4749
}
48-
emit(
49-
{
50-
data: {
51-
node: serializeDocument(document, configuration, scope, serializationContext),
52-
initialOffset: {
53-
left: getScrollX(),
54-
top: getScrollY(),
55-
},
50+
emitRecord({
51+
data: {
52+
node: serializeDocument(document, configuration, scope, serializationContext),
53+
initialOffset: {
54+
left: getScrollX(),
55+
top: getScrollY(),
5656
},
57-
type: RecordType.FullSnapshot,
58-
timestamp,
5957
},
60-
serializationStats
61-
)
58+
type: RecordType.FullSnapshot,
59+
timestamp,
60+
})
61+
emitStats(serializationStats)
6262

6363
if (window.visualViewport) {
64-
emit({
64+
emitRecord({
6565
data: getVisualViewport(window.visualViewport),
6666
type: RecordType.VisualViewport,
6767
timestamp,

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ export { trackStyleSheet } from './trackStyleSheet'
77
export { trackFocus } from './trackFocus'
88
export { trackFrustration } from './trackFrustration'
99
export { trackViewEnd } from './trackViewEnd'
10-
export type { InputCallback } from './trackInput'
1110
export { trackInput } from './trackInput'
12-
export type { ScrollCallback } from './trackScroll'
13-
export type { MutationCallBack } from './trackMutation'
1411
export { trackMutation } from './trackMutation'
1512
export type { Tracker } from './tracker.types'

0 commit comments

Comments
 (0)