11import type { Observable , RawError , PageMayExitEvent , Encoder } from '@datadog/browser-core'
22import {
3+ combine ,
34 createBatch ,
45 createFlushController ,
56 createHttpRequest ,
@@ -12,115 +13,58 @@ import type { RumConfiguration } from '../domain/configuration'
1213import type { LifeCycle } from '../domain/lifeCycle'
1314import { LifeCycleEventType } from '../domain/lifeCycle'
1415import type { AssembledRumEvent } from '../rawRumEvent.types'
16+ import type { RumViewEvent } from '../rumEvent.types'
1517import { RumEventType } from '../rawRumEvent.types'
16- import { diffMerge , isEqual } from '../domain/view/viewDiff'
18+ import { diffMerge } from '../domain/view/viewDiff'
1719
1820export const PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL = 100
1921
20- // Top-level assembled fields that should be diffed with simple equality
21- const ASSEMBLED_TOP_LEVEL_FIELDS = [
22- 'service' ,
23- 'version' ,
24- 'source' ,
25- 'ddtags' ,
26- 'context' ,
27- 'connectivity' ,
28- 'usr' ,
29- 'device' ,
30- 'privacy' ,
31- ] as const
32-
3322export function computeAssembledViewDiff (
3423 current : AssembledRumEvent ,
3524 last : AssembledRumEvent
3625) : AssembledRumEvent | undefined {
3726 const currentObj = current as unknown as Record < string , unknown >
3827 const lastObj = last as unknown as Record < string , unknown >
3928
40- const result : Record < string , unknown > = {
41- type : RumEventType . VIEW_UPDATE ,
42- date : currentObj . date ,
43- application : currentObj . application ,
44- session : currentObj . session ,
45- }
46-
47- let hasChanges = false
29+ const diff = diffMerge ( currentObj , lastObj , {
30+ // context, connectivity, usr, device, privacy are objects — use REPLACE to avoid partial updates
31+ replaceKeys : new Set ( [ 'view.custom_timings' , 'context' , 'connectivity' , 'usr' , 'device' , 'privacy' ] ) ,
32+ appendKeys : new Set ( [ '_dd.page_states' ] ) ,
33+ // Ignore always-required fields — they are added back via combine regardless of changes
34+ ignoreKeys : new Set ( [
35+ 'date' ,
36+ 'type' ,
37+ 'application' ,
38+ 'session' ,
39+ 'view.id' ,
40+ 'view.url' ,
41+ '_dd.document_version' ,
42+ '_dd.format_version' ,
43+ ] ) ,
44+ } )
4845
49- // --- view.* diff (MERGE strategy, nested-aware) ---
50- const currentView = currentObj . view as Record < string , unknown >
51- const lastView = lastObj . view as Record < string , unknown >
52- // view.id and view.url are always required by the schema (_common-schema.json) for backend routing
53- const viewResult : Record < string , unknown > = { id : currentView . id , url : currentView . url }
54-
55- // Note: diffMerge emits null for keys deleted between events. In practice, view fields only
56- // appear (e.g. first_byte, lcp, cls become available as data arrives) and never disappear
57- // within the same view — so the null-for-deleted-keys path is unreachable for view data.
58- const viewDiff = diffMerge ( currentView , lastView , { replaceKeys : new Set ( [ 'custom_timings' ] ) } )
59- if ( viewDiff ) {
60- delete viewDiff . id // already in required fields
61- delete viewDiff . url // already in required fields
62- Object . assign ( viewResult , viewDiff )
63- if ( Object . keys ( viewDiff ) . length > 0 ) {
64- hasChanges = true
65- }
46+ if ( ! diff ) {
47+ return undefined
6648 }
67- result . view = viewResult
6849
69- // --- _dd.* diff (MERGE strategy, page_states APPEND) ---
50+ const currentView = currentObj . view as Record < string , unknown >
7051 const currentDd = currentObj . _dd as Record < string , unknown >
71- const lastDd = lastObj . _dd as Record < string , unknown >
72- // _dd.document_version and _dd.format_version are always required by the schema for backend routing
73- const ddResult : Record < string , unknown > = {
74- document_version : currentDd . document_version ,
75- format_version : currentDd . format_version ,
76- }
7752
78- const ddDiff = diffMerge ( currentDd , lastDd , { appendKeys : new Set ( [ 'page_states' ] ) } )
79- if ( ddDiff ) {
80- delete ddDiff . document_version // already in required fields
81- delete ddDiff . format_version // already in required fields
82- Object . assign ( ddResult , ddDiff )
83- if ( Object . keys ( ddDiff ) . length > 0 ) {
84- hasChanges = true
85- }
86- }
87- result . _dd = ddResult
88-
89- // --- display.* diff (MERGE strategy) ---
90- const currentDisplay = currentObj . display as Record < string , unknown > | undefined
91- const lastDisplay = lastObj . display as Record < string , unknown > | undefined
92- if ( currentDisplay && lastDisplay ) {
93- const displayDiff = diffMerge ( currentDisplay , lastDisplay )
94- if ( displayDiff && Object . keys ( displayDiff ) . length > 0 ) {
95- result . display = displayDiff
96- hasChanges = true
97- }
98- } else if ( currentDisplay && ! lastDisplay ) {
99- result . display = currentDisplay
100- hasChanges = true
101- } else if ( ! currentDisplay && lastDisplay ) {
102- // In practice this branch is unreachable: display (scroll metrics) only appears
103- // once scroll is tracked and never goes away within the same view. Kept as a
104- // defensive fallback in case the invariant is violated in the future.
105- result . display = null
106- hasChanges = true
107- }
108-
109- // --- Top-level assembled fields (REPLACE strategy) ---
110- for ( const key of ASSEMBLED_TOP_LEVEL_FIELDS ) {
111- const currentVal = currentObj [ key ]
112- const lastVal = lastObj [ key ]
113- if ( ! isEqual ( currentVal , lastVal ) ) {
114- result [ key ] = currentVal
115- hasChanges = true
116- }
117- }
118-
119- if ( ! hasChanges ) {
120- return undefined
121- }
122-
123- return result as unknown as AssembledRumEvent
53+ // Merge always-required fields on top of the diff for backend routing
54+ return combine ( diff , {
55+ type : RumEventType . VIEW_UPDATE ,
56+ date : currentObj . date ,
57+ application : currentObj . application ,
58+ session : currentObj . session ,
59+ view : {
60+ id : currentView . id ,
61+ url : currentView . url ,
62+ } ,
63+ _dd : {
64+ document_version : currentDd . document_version ,
65+ format_version : currentDd . format_version ,
66+ } ,
67+ } ) as unknown as AssembledRumEvent
12468}
12569
12670export function startRumBatch (
@@ -172,7 +116,7 @@ export function startRumBatch(
172116 }
173117
174118 // View ended (is_active: false)
175- if ( ! ( serverRumEvent . view as any ) . is_active ) {
119+ if ( ! ( serverRumEvent as RumViewEvent ) . view . is_active ) {
176120 lastSentView = undefined
177121 viewUpdatesSinceCheckpoint = 0
178122 batch . upsert ( serverRumEvent , viewId )
@@ -195,14 +139,7 @@ export function startRumBatch(
195139 // They intentionally bypass RAW_RUM_EVENT_COLLECTED → assembly → RUM_EVENT_COLLECTED, which
196140 // means they skip beforeSend entirely. view_update is an internal bandwidth optimization —
197141 // not a customer-visible event type, and not modifiable via beforeSend.
198- if ( ! lastSentView ) {
199- // Safety fallback (should not happen in practice)
200- lastSentView = serverRumEvent
201- batch . upsert ( serverRumEvent , viewId )
202- return
203- }
204-
205- const diff = computeAssembledViewDiff ( serverRumEvent , lastSentView )
142+ const diff = computeAssembledViewDiff ( serverRumEvent , lastSentView ! )
206143 lastSentView = serverRumEvent
207144 if ( diff ) {
208145 sendToExtension ( 'rum' , diff )
0 commit comments