Skip to content
Merged
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
1 change: 0 additions & 1 deletion packages/browser-core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { objectHasValue } from './utils/objectUtils'
// eslint-disable-next-line no-restricted-syntax
export enum ExperimentalFeature {
TRACK_INTAKE_REQUESTS = 'track_intake_requests',
PARTIAL_VIEW_UPDATES = 'partial_view_updates',
}

const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ describe('serializeRumConfiguration', () => {
profilingSampleRate: 42,
propagateTraceBaggage: true,
trackResourceHeaders: true,
betaEnableViewUpdates: true,
}

type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
Expand Down Expand Up @@ -887,6 +888,7 @@ describe('serializeRumConfiguration', () => {
use_remote_configuration_proxy: true,
profiling_sample_rate: 42,
track_resource_headers: 'default_headers',
beta_enable_view_updates: true,
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ export interface RumInitConfiguration extends InitConfiguration {
* @category Data Collection
*/
allowedGraphQlUrls?: Array<MatchOption | GraphQlUrlOption> | undefined

/**
* Enable partial view updates, which reduces bandwidth by sending only changed
* fields instead of full view events on intermediate updates.
*
* @category Beta
*/
betaEnableViewUpdates?: boolean | undefined
}

export type HybridInitConfiguration = Omit<RumInitConfiguration, 'applicationId' | 'clientToken'>
Expand Down Expand Up @@ -386,6 +394,7 @@ export interface RumConfiguration extends Configuration {
sessionReplaySampleRate: number
startSessionReplayRecordingManually: boolean
trackUserInteractions: boolean
betaEnableViewUpdates: boolean
trackViewsManually: boolean
trackResources: boolean
trackResourceHeaders: MatchHeader[]
Expand Down Expand Up @@ -458,6 +467,7 @@ export function validateAndBuildRumConfiguration(
workerUrl: initConfiguration.workerUrl,
compressIntakeRequests: !!initConfiguration.compressIntakeRequests,
trackUserInteractions: !!(initConfiguration.trackUserInteractions ?? true),
betaEnableViewUpdates: !!initConfiguration.betaEnableViewUpdates,
trackViewsManually: !!initConfiguration.trackViewsManually,
trackResources: !!(initConfiguration.trackResources ?? true),
trackResourceHeaders: validateAndBuildTrackResourceHeaders(initConfiguration),
Expand Down Expand Up @@ -687,6 +697,7 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration) {
profiling_sample_rate: configuration.profilingSampleRate,
use_remote_configuration_proxy: !!configuration.remoteConfigurationProxy,
track_resource_headers: getTrackResourceHeadersTelemetryValue(configuration.trackResourceHeaders),
beta_enable_view_updates: configuration.betaEnableViewUpdates,
...baseSerializedConfiguration,
} satisfies RawTelemetryConfiguration
}
66 changes: 16 additions & 50 deletions packages/browser-rum-core/src/transport/startRumBatch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ExperimentalFeature, Observable, addExperimentalFeatures } from '@datadog/browser-core'
import { resetExperimentalFeatures } from '@datadog/browser-core/src/tools/experimentalFeatures'
import { Observable } from '@datadog/browser-core'
import type { FlushEvent } from '@datadog/browser-core/src/transport/flushController'
import { registerCleanupTask } from '@datadog/browser-core/test'

import type { AssembledRumEvent } from '../rawRumEvent.types'
import { RumEventType } from '../rawRumEvent.types'
import type { RumViewEvent } from '../rumEvent.types'
Expand Down Expand Up @@ -215,11 +214,6 @@ describe('computeAssembledViewDiff', () => {
})

describe('startRumBatch partial_view_updates routing', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL should be 100', () => {
expect(PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL).toBe(100)
})
Expand Down Expand Up @@ -288,13 +282,10 @@ function makeView(viewId: string, docVersion: number, overrides: Record<string,
// ---------------------------------------------------------------------------

describe('createBatchDispatcher', () => {
describe('feature flag OFF', () => {
describe('feature disabled', () => {
it('should upsert full VIEW events (legacy behaviour)', () => {
resetExperimentalFeatures()
registerCleanupTask(resetExperimentalFeatures)

const { batch, upsertSpy } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, false)

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

describe('non-view events', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('should always append non-view events', () => {
const { batch, addSpy, upsertSpy } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

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

describe('optimization 1 — VIEW already in batch', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('should upsert the latest full VIEW for every intermediate update', () => {
const { batch, upsertSpy } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

const v1 = makeView('view-1', 1)
const v2 = makeView('view-1', 2, {
Expand Down Expand Up @@ -363,7 +344,7 @@ describe('createBatchDispatcher', () => {

it('should not emit any view_update events while the VIEW is in the batch', () => {
const { batch, upsertSpy } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

dispatch(makeView('view-1', 1))
dispatch(makeView('view-1', 2))
Expand All @@ -375,14 +356,9 @@ describe('createBatchDispatcher', () => {
})

describe('optimization 2 — no VIEW in batch (post-flush)', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('should upsert an aggregate view_update after a flush', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

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

it('should aggregate multiple updates into a single view_update per batch', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

dispatch(makeView('view-1', 1))
flush()
Expand Down Expand Up @@ -449,7 +425,7 @@ describe('createBatchDispatcher', () => {

it('should compute the diff from batchBase, not from the previous intermediate update', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

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

it('should emit nothing if nothing changed since batchBase', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

const v1 = makeView('view-1', 1)
dispatch(v1)
Expand All @@ -513,14 +489,9 @@ describe('createBatchDispatcher', () => {
})

describe('checkpoint', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('should send a full VIEW after PARTIAL_VIEW_UPDATE_CHECKPOINT_INTERVAL intermediate updates', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

dispatch(makeView('view-1', 1))
flush()
Expand Down Expand Up @@ -553,14 +524,9 @@ describe('createBatchDispatcher', () => {
})

describe('view lifecycle', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.PARTIAL_VIEW_UPDATES])
registerCleanupTask(resetExperimentalFeatures)
})

it('should send a full VIEW when a new view starts', () => {
const { batch, upsertSpy } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

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

it('should send a full VIEW when the view ends', () => {
const { batch, upsertSpy, flush } = createMockBatch()
const { dispatch } = createBatchDispatcher(batch)
const { dispatch } = createBatchDispatcher(batch, true)

dispatch(makeView('view-1', 1))
flush()
Expand Down Expand Up @@ -603,7 +569,7 @@ describe('createBatchDispatcher', () => {

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

// view-1 starts and flushes
dispatch(makeView('view-1', 1))
Expand Down Expand Up @@ -655,7 +621,7 @@ describe('createBatchDispatcher', () => {

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

dispatch(makeView('view-1', 1))
flush()
Expand Down
15 changes: 5 additions & 10 deletions packages/browser-rum-core/src/transport/startRumBatch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import type { Observable, Encoder, Context } from '@datadog/browser-core'
import {
createBatch,
DeflateEncoderStreamId,
isExperimentalFeatureEnabled,
ExperimentalFeature,
sendToExtension,
} from '@datadog/browser-core'
import { createBatch, DeflateEncoderStreamId, sendToExtension } from '@datadog/browser-core'
import { combine } from '@datadog/js-core/util'
import { createEndpointBuilder, createReplicaEndpointBuilder } from '@datadog/js-core/transport'
import type { RumConfiguration } from '../domain/configuration'
Expand Down Expand Up @@ -68,7 +62,8 @@ export function computeAssembledViewDiff(current: RumViewEvent, last: RumViewEve
* aggregated into a single view_update diff against the last flushed state.
*/
export function createBatchDispatcher(
batch: Pick<ReturnType<typeof createBatch>, 'flushObservable' | 'isEmpty' | 'add' | 'upsert'>
batch: Pick<ReturnType<typeof createBatch>, 'flushObservable' | 'isEmpty' | 'add' | 'upsert'>,
enableViewUpdates: boolean
): { dispatch: (event: AssembledRumEvent) => void; stop: () => void } {
let lastSentView: RumViewEvent | undefined
// Base used to compute the aggregate diff for the current batch's view_update.
Expand Down Expand Up @@ -96,7 +91,7 @@ export function createBatchDispatcher(
return
}

if (!isExperimentalFeatureEnabled(ExperimentalFeature.PARTIAL_VIEW_UPDATES)) {
if (!enableViewUpdates) {
// Feature OFF: existing behavior — upsert full view
batch.upsert(serverRumEvent, serverRumEvent.view.id)
return
Expand Down Expand Up @@ -195,7 +190,7 @@ export function startRumBatch(
})
sessionExpireObservable.subscribe(() => batch.forceFlush('session_expire'))

const { dispatch, stop: stopDispatcher } = createBatchDispatcher(batch)
const { dispatch, stop: stopDispatcher } = createBatchDispatcher(batch, configuration.betaEnableViewUpdates)
Comment thread
mormubis marked this conversation as resolved.
lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, dispatch)

const stopBatch = batch.stop.bind(batch)
Expand Down
14 changes: 7 additions & 7 deletions test/e2e/scenario/rum/partialViewUpdates.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createTest, waitForRequests } from '../../lib/framework'
test.describe('partial view updates', () => {
createTest('should send a view_update event when the update arrives in a new batch')
.withRum({
enableExperimentalFeatures: ['partial_view_updates'],
betaEnableViewUpdates: true,
})
.run(async ({ intakeRegistry, flushEvents, page }) => {
// Flush the initial VIEW so it lands in its own batch. Any update that arrives
Expand Down Expand Up @@ -38,7 +38,7 @@ test.describe('partial view updates', () => {

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

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

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

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

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

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