Skip to content

Commit 1e8224a

Browse files
authored
✨ Add betaEnableViewUpdates config option (#4833)
1 parent d0883f6 commit 1e8224a

6 files changed

Lines changed: 41 additions & 68 deletions

File tree

packages/browser-core/src/tools/experimentalFeatures.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { objectHasValue } from './utils/objectUtils'
1515
// eslint-disable-next-line no-restricted-syntax
1616
export enum ExperimentalFeature {
1717
TRACK_INTAKE_REQUESTS = 'track_intake_requests',
18-
PARTIAL_VIEW_UPDATES = 'partial_view_updates',
1918
}
2019

2120
const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()

packages/browser-rum-core/src/domain/configuration/configuration.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ describe('serializeRumConfiguration', () => {
832832
profilingSampleRate: 42,
833833
propagateTraceBaggage: true,
834834
trackResourceHeaders: true,
835+
betaEnableViewUpdates: true,
835836
}
836837

837838
type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
@@ -887,6 +888,7 @@ describe('serializeRumConfiguration', () => {
887888
use_remote_configuration_proxy: true,
888889
profiling_sample_rate: 42,
889890
track_resource_headers: 'default_headers',
891+
beta_enable_view_updates: true,
890892
})
891893
})
892894
})

packages/browser-rum-core/src/domain/configuration/configuration.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ export interface RumInitConfiguration extends InitConfiguration {
343343
* @category Data Collection
344344
*/
345345
allowedGraphQlUrls?: Array<MatchOption | GraphQlUrlOption> | undefined
346+
347+
/**
348+
* Enable partial view updates, which reduces bandwidth by sending only changed
349+
* fields instead of full view events on intermediate updates.
350+
*
351+
* @category Beta
352+
*/
353+
betaEnableViewUpdates?: boolean | undefined
346354
}
347355

348356
export type HybridInitConfiguration = Omit<RumInitConfiguration, 'applicationId' | 'clientToken'>
@@ -386,6 +394,7 @@ export interface RumConfiguration extends Configuration {
386394
sessionReplaySampleRate: number
387395
startSessionReplayRecordingManually: boolean
388396
trackUserInteractions: boolean
397+
betaEnableViewUpdates: boolean
389398
trackViewsManually: boolean
390399
trackResources: boolean
391400
trackResourceHeaders: MatchHeader[]
@@ -458,6 +467,7 @@ export function validateAndBuildRumConfiguration(
458467
workerUrl: initConfiguration.workerUrl,
459468
compressIntakeRequests: !!initConfiguration.compressIntakeRequests,
460469
trackUserInteractions: !!(initConfiguration.trackUserInteractions ?? true),
470+
betaEnableViewUpdates: !!initConfiguration.betaEnableViewUpdates,
461471
trackViewsManually: !!initConfiguration.trackViewsManually,
462472
trackResources: !!(initConfiguration.trackResources ?? true),
463473
trackResourceHeaders: validateAndBuildTrackResourceHeaders(initConfiguration),
@@ -687,6 +697,7 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
687697
profiling_sample_rate: configuration.profilingSampleRate,
688698
use_remote_configuration_proxy: !!configuration.remoteConfigurationProxy,
689699
track_resource_headers: getTrackResourceHeadersTelemetryValue(configuration.trackResourceHeaders),
700+
beta_enable_view_updates: configuration.betaEnableViewUpdates,
690701
...baseSerializedConfiguration,
691702
} satisfies RawTelemetryConfiguration
692703
}

packages/browser-rum-core/src/transport/startRumBatch.spec.ts

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { ExperimentalFeature, Observable, addExperimentalFeatures } from '@datadog/browser-core'
2-
import { resetExperimentalFeatures } from '@datadog/browser-core/src/tools/experimentalFeatures'
1+
import { Observable } from '@datadog/browser-core'
32
import type { FlushEvent } from '@datadog/browser-core/src/transport/flushController'
4-
import { registerCleanupTask } from '@datadog/browser-core/test'
3+
54
import type { AssembledRumEvent } from '../rawRumEvent.types'
65
import { RumEventType } from '../rawRumEvent.types'
76
import type { RumViewEvent } from '../rumEvent.types'
@@ -215,11 +214,6 @@ describe('computeAssembledViewDiff', () => {
215214
})
216215

217216
describe('startRumBatch partial_view_updates routing', () => {
218-
beforeEach(() => {
219-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
220-
registerCleanupTask(resetExperimentalFeatures)
221-
})
222-
223217
it('PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL should be 100', () => {
224218
expect(PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL).toBe(100)
225219
})
@@ -288,13 +282,10 @@ function makeView(viewId: string, docVersion: number, overrides: Record<string,
288282
// ---------------------------------------------------------------------------
289283

290284
describe('createBatchDispatcher', () => {
291-
describe('feature flag OFF', () => {
285+
describe('feature disabled', () => {
292286
it('should upsert full VIEW events (legacy behaviour)', () => {
293-
resetExperimentalFeatures()
294-
registerCleanupTask(resetExperimentalFeatures)
295-
296287
const { batch, upsertSpy } = createMockBatch()
297-
const { dispatch } = createBatchDispatcher(batch)
288+
const { dispatch } = createBatchDispatcher(batch, false)
298289

299290
const v1 = makeView('view-1', 1)
300291
const v2 = makeView('view-1', 2)
@@ -308,14 +299,9 @@ describe('createBatchDispatcher', () => {
308299
})
309300

310301
describe('non-view events', () => {
311-
beforeEach(() => {
312-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
313-
registerCleanupTask(resetExperimentalFeatures)
314-
})
315-
316302
it('should always append non-view events', () => {
317303
const { batch, addSpy, upsertSpy } = createMockBatch()
318-
const { dispatch } = createBatchDispatcher(batch)
304+
const { dispatch } = createBatchDispatcher(batch, true)
319305

320306
const action = { type: RumEventType.ACTION } as unknown as AssembledRumEvent
321307
dispatch(action)
@@ -326,14 +312,9 @@ describe('createBatchDispatcher', () => {
326312
})
327313

328314
describe('optimization 1 — VIEW already in batch', () => {
329-
beforeEach(() => {
330-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
331-
registerCleanupTask(resetExperimentalFeatures)
332-
})
333-
334315
it('should upsert the latest full VIEW for every intermediate update', () => {
335316
const { batch, upsertSpy } = createMockBatch()
336-
const { dispatch } = createBatchDispatcher(batch)
317+
const { dispatch } = createBatchDispatcher(batch, true)
337318

338319
const v1 = makeView('view-1', 1)
339320
const v2 = makeView('view-1', 2, {
@@ -363,7 +344,7 @@ describe('createBatchDispatcher', () => {
363344

364345
it('should not emit any view_update events while the VIEW is in the batch', () => {
365346
const { batch, upsertSpy } = createMockBatch()
366-
const { dispatch } = createBatchDispatcher(batch)
347+
const { dispatch } = createBatchDispatcher(batch, true)
367348

368349
dispatch(makeView('view-1', 1))
369350
dispatch(makeView('view-1', 2))
@@ -375,14 +356,9 @@ describe('createBatchDispatcher', () => {
375356
})
376357

377358
describe('optimization 2 — no VIEW in batch (post-flush)', () => {
378-
beforeEach(() => {
379-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
380-
registerCleanupTask(resetExperimentalFeatures)
381-
})
382-
383359
it('should upsert an aggregate view_update after a flush', () => {
384360
const { batch, upsertSpy, flush } = createMockBatch()
385-
const { dispatch } = createBatchDispatcher(batch)
361+
const { dispatch } = createBatchDispatcher(batch, true)
386362

387363
dispatch(makeView('view-1', 1))
388364
flush() // batchHasFullView resets; batchBase = v1
@@ -412,7 +388,7 @@ describe('createBatchDispatcher', () => {
412388

413389
it('should aggregate multiple updates into a single view_update per batch', () => {
414390
const { batch, upsertSpy, flush } = createMockBatch()
415-
const { dispatch } = createBatchDispatcher(batch)
391+
const { dispatch } = createBatchDispatcher(batch, true)
416392

417393
dispatch(makeView('view-1', 1))
418394
flush()
@@ -449,7 +425,7 @@ describe('createBatchDispatcher', () => {
449425

450426
it('should compute the diff from batchBase, not from the previous intermediate update', () => {
451427
const { batch, upsertSpy, flush } = createMockBatch()
452-
const { dispatch } = createBatchDispatcher(batch)
428+
const { dispatch } = createBatchDispatcher(batch, true)
453429

454430
// Initial view with action.count = 0
455431
dispatch(makeView('view-1', 1))
@@ -496,7 +472,7 @@ describe('createBatchDispatcher', () => {
496472

497473
it('should emit nothing if nothing changed since batchBase', () => {
498474
const { batch, upsertSpy, flush } = createMockBatch()
499-
const { dispatch } = createBatchDispatcher(batch)
475+
const { dispatch } = createBatchDispatcher(batch, true)
500476

501477
const v1 = makeView('view-1', 1)
502478
dispatch(v1)
@@ -513,14 +489,9 @@ describe('createBatchDispatcher', () => {
513489
})
514490

515491
describe('checkpoint', () => {
516-
beforeEach(() => {
517-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
518-
registerCleanupTask(resetExperimentalFeatures)
519-
})
520-
521492
it('should send a full VIEW after PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL intermediate updates', () => {
522493
const { batch, upsertSpy, flush } = createMockBatch()
523-
const { dispatch } = createBatchDispatcher(batch)
494+
const { dispatch } = createBatchDispatcher(batch, true)
524495

525496
dispatch(makeView('view-1', 1))
526497
flush()
@@ -553,14 +524,9 @@ describe('createBatchDispatcher', () => {
553524
})
554525

555526
describe('view lifecycle', () => {
556-
beforeEach(() => {
557-
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
558-
registerCleanupTask(resetExperimentalFeatures)
559-
})
560-
561527
it('should send a full VIEW when a new view starts', () => {
562528
const { batch, upsertSpy } = createMockBatch()
563-
const { dispatch } = createBatchDispatcher(batch)
529+
const { dispatch } = createBatchDispatcher(batch, true)
564530

565531
dispatch(makeView('view-1', 1))
566532
dispatch(makeView('view-2', 1)) // new view
@@ -574,7 +540,7 @@ describe('createBatchDispatcher', () => {
574540

575541
it('should send a full VIEW when the view ends', () => {
576542
const { batch, upsertSpy, flush } = createMockBatch()
577-
const { dispatch } = createBatchDispatcher(batch)
543+
const { dispatch } = createBatchDispatcher(batch, true)
578544

579545
dispatch(makeView('view-1', 1))
580546
flush()
@@ -603,7 +569,7 @@ describe('createBatchDispatcher', () => {
603569

604570
it('should upsert a stale view-end without resetting state for the current view', () => {
605571
const { batch, upsertSpy, flush } = createMockBatch()
606-
const { dispatch } = createBatchDispatcher(batch)
572+
const { dispatch } = createBatchDispatcher(batch, true)
607573

608574
// view-1 starts and flushes
609575
dispatch(makeView('view-1', 1))
@@ -655,7 +621,7 @@ describe('createBatchDispatcher', () => {
655621

656622
it('should reset to opt-1 after a new view starts following a flush', () => {
657623
const { batch, upsertSpy, flush } = createMockBatch()
658-
const { dispatch } = createBatchDispatcher(batch)
624+
const { dispatch } = createBatchDispatcher(batch, true)
659625

660626
dispatch(makeView('view-1', 1))
661627
flush()

packages/browser-rum-core/src/transport/startRumBatch.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import type { Observable, Encoder, Context } from '@datadog/browser-core'
2-
import {
3-
createBatch,
4-
DeflateEncoderStreamId,
5-
isExperimentalFeatureEnabled,
6-
ExperimentalFeature,
7-
sendToExtension,
8-
} from '@datadog/browser-core'
2+
import { createBatch, DeflateEncoderStreamId, sendToExtension } from '@datadog/browser-core'
93
import { combine } from '@datadog/js-core/util'
104
import { createEndpointBuilder, createReplicaEndpointBuilder } from '@datadog/js-core/transport'
115
import type { RumConfiguration } from '../domain/configuration'
@@ -68,7 +62,8 @@ export function computeAssembledViewDiff(current: RumViewEvent, last: RumViewEve
6862
* aggregated into a single view_update diff against the last flushed state.
6963
*/
7064
export function createBatchDispatcher(
71-
batch: Pick<ReturnType<typeof createBatch>, 'flushObservable' | 'isEmpty' | 'add' | 'upsert'>
65+
batch: Pick<ReturnType<typeof createBatch>, 'flushObservable' | 'isEmpty' | 'add' | 'upsert'>,
66+
enableViewUpdates: boolean
7267
): { dispatch: (event: AssembledRumEvent) => void; stop: () => void } {
7368
let lastSentView: RumViewEvent | undefined
7469
// Base used to compute the aggregate diff for the current batch's view_update.
@@ -96,7 +91,7 @@ export function createBatchDispatcher(
9691
return
9792
}
9893

99-
if (!isExperimentalFeatureEnabled(ExperimentalFeature.PARTIAL_VIEW_UPDATES)) {
94+
if (!enableViewUpdates) {
10095
// Feature OFF: existing behavior — upsert full view
10196
batch.upsert(serverRumEvent, serverRumEvent.view.id)
10297
return
@@ -195,7 +190,7 @@ export function startRumBatch(
195190
})
196191
sessionExpireObservable.subscribe(() => batch.forceFlush('session_expire'))
197192

198-
const { dispatch, stop: stopDispatcher } = createBatchDispatcher(batch)
193+
const { dispatch, stop: stopDispatcher } = createBatchDispatcher(batch, configuration.betaEnableViewUpdates)
199194
lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, dispatch)
200195

201196
const stopBatch = batch.stop.bind(batch)

test/e2e/scenario/rum/partialViewUpdates.scenario.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createTest, waitForRequests } from '../../lib/framework'
44
test.describe('partial view updates', () => {
55
createTest('should send a view_update event when the update arrives in a new batch')
66
.withRum({
7-
enableExperimentalFeatures: ['partial_view_updates'],
7+
betaEnableViewUpdates: true,
88
})
99
.run(async ({ intakeRegistry, flushEvents, page }) => {
1010
// Flush the initial VIEW so it lands in its own batch. Any update that arrives
@@ -38,7 +38,7 @@ test.describe('partial view updates', () => {
3838

3939
createTest('should upsert the full VIEW when a view update arrives in the same batch')
4040
.withRum({
41-
enableExperimentalFeatures: ['partial_view_updates'],
41+
betaEnableViewUpdates: true,
4242
})
4343
.run(async ({ intakeRegistry, flushEvents, page }) => {
4444
// Trigger a view update without flushing first — it lands in the same batch as the initial VIEW.
@@ -59,7 +59,7 @@ test.describe('partial view updates', () => {
5959

6060
createTest('should have strictly increasing document_version across view and view_update events')
6161
.withRum({
62-
enableExperimentalFeatures: ['partial_view_updates'],
62+
betaEnableViewUpdates: true,
6363
})
6464
.run(async ({ intakeRegistry, flushEvents, page }) => {
6565
// Flush the initial VIEW first so the subsequent update lands in a new batch (opt-2 path).
@@ -107,7 +107,7 @@ test.describe('partial view updates', () => {
107107

108108
createTest('should emit a new full view event after navigation')
109109
.withRum({
110-
enableExperimentalFeatures: ['partial_view_updates'],
110+
betaEnableViewUpdates: true,
111111
})
112112
.run(async ({ intakeRegistry, flushEvents, page }) => {
113113
await page.evaluate(() => history.pushState(null, '', '/new-page'))
@@ -125,7 +125,7 @@ test.describe('partial view updates', () => {
125125

126126
createTest('should include required fields in all view_update events')
127127
.withRum({
128-
enableExperimentalFeatures: ['partial_view_updates'],
128+
betaEnableViewUpdates: true,
129129
})
130130
.run(async ({ intakeRegistry, flushEvents, page }) => {
131131
// Flush the initial VIEW first so the update lands in a new batch (opt-2 path).
@@ -154,7 +154,7 @@ test.describe('partial view updates', () => {
154154

155155
createTest('should send a full VIEW event (not view_update) with is_active false when view ends')
156156
.withRum({
157-
enableExperimentalFeatures: ['partial_view_updates'],
157+
betaEnableViewUpdates: true,
158158
})
159159
.run(async ({ intakeRegistry, flushEvents, page }) => {
160160
await page.evaluate(() => history.pushState(null, '', '/other-page'))
@@ -176,7 +176,7 @@ test.describe('partial view updates', () => {
176176

177177
createTest('should emit a full view checkpoint event during a long-lived view')
178178
.withRum({
179-
enableExperimentalFeatures: ['partial_view_updates'],
179+
betaEnableViewUpdates: true,
180180
})
181181
.run(async ({ intakeRegistry, flushEvents, page }) => {
182182
// Flush the initial VIEW first so it arrives at the intake in its own batch.

0 commit comments

Comments
 (0)