Skip to content

⚗️ Partial view updates (experimental)#4201

Draft
mormubis wants to merge 7 commits intomainfrom
adlrb/partial-view
Draft

⚗️ Partial view updates (experimental)#4201
mormubis wants to merge 7 commits intomainfrom
adlrb/partial-view

Conversation

@mormubis
Copy link
Contributor

Motivation

Implement the "RUM Event Format Limitation" RFC (partial view updates) to reduce bandwidth by sending only changed fields in subsequent view events. When the partial_view_updates experimental feature flag is enabled, the SDK sends the first event per view.id as a full view event, and all subsequent updates as view_update events containing only changed fields.

Changes

Phase 1 — Foundation Types & Feature Flag:

  • Added PARTIAL_VIEW_UPDATES to ExperimentalFeature enum
  • Added VIEW_UPDATE to RumEventType and RawRumViewUpdateEvent interface
  • Added VIEW_UPDATE to RawRumEvent union and modifiableFieldPathsByEvent

Phase 2 — Diff Engine:

  • Created viewDiff.ts with computeViewDiff() and createViewDiffTracker()
  • Implements all RFC update strategies: MERGE, REPLACE, APPEND, DELETE
  • 21 unit tests covering all strategies and edge cases

Phase 3 — Pipeline Integration:

  • Modified viewCollection.ts to conditionally emit view or view_update
  • Protected view_update from beforeSend dismissal (same as view)
  • Added VIEW_UPDATE to domain context type mapping and feature flag assembly hook
  • Resilient degradation: diff failures fall back to full view event
  • 8 new unit tests across viewCollection, assembly, and featureFlagContext

Phase 4 — E2E Testing:

  • 6 Playwright E2E scenarios validating the complete feature
  • Tests: event flow, document_version ordering, feature flag toggle, navigation lifecycle, required fields, view end

All changes are gated behind enableExperimentalFeatures: ['partial_view_updates']. When the flag is OFF, behavior is identical to the current SDK.

Test instructions

# Unit tests
yarn test:unit

# E2E tests (requires init first)
yarn test:e2e:init
yarn test:e2e -g "partial view"

# Enable in an app
datadogRum.init({
  enableExperimentalFeatures: ['partial_view_updates'],
  // ...other config
})

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

🤖 Generated with Claude Code

@github-actions
Copy link

github-actions bot commented Feb 18, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@cit-pr-commenter-54b7da
Copy link

cit-pr-commenter-54b7da bot commented Feb 18, 2026

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 169.55 KiB 172.23 KiB +2.68 KiB +1.58%
Rum Profiler 4.29 KiB 4.29 KiB 0 B 0.00%
Rum Recorder 24.71 KiB 24.71 KiB 0 B 0.00%
Logs 56.62 KiB 56.66 KiB +46 B +0.08%
Flagging 944 B 944 B 0 B 0.00%
Rum Slim 126.19 KiB 128.79 KiB +2.60 KiB +2.06%
Worker 23.63 KiB 23.63 KiB 0 B 0.00%
🚀 CPU Performance

Pending...

🧠 Memory Performance

Pending...

🔗 RealWorld

@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Feb 18, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 59.52%
Overall Coverage: 77.03% (-0.08%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 079b043 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

mormubis and others added 5 commits February 19, 2026 13:02
Implement Phases 1-2 of the partial view updates feature (behind
experimental feature flag). Add VIEW_UPDATE event type, RawRumViewUpdateEvent
interface, and a diff engine that computes minimal field-level diffs
between view states using MERGE, REPLACE, APPEND, and DELETE strategies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the diff engine into viewCollection to conditionally emit view_update
events when the PARTIAL_VIEW_UPDATES feature flag is enabled. First event
per view.id is always a full view; subsequent updates emit only changed
fields. Includes batch routing, beforeSend protection, feature flag context
support, domain context types, and comprehensive unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Playwright E2E scenarios validating the complete partial view updates
feature: view + view_update event flow, document_version ordering, feature
flag toggle, navigation lifecycle, required field presence, and view end
events. Skip view_update format validation until schema is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…a lands

Validation skips for view_update events will cause typecheck failures once
rum-events-format adds the view_update schema, serving as a reminder to
remove the skips and enable full validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix import ordering, unused imports, protected directory imports,
unsafe type assertions, and formatting across all changed files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mormubis and others added 2 commits February 19, 2026 16:24
Cast to the schema-generated RumEvent type so that comparing .type against
'view_update' is a TypeScript error (view_update is absent from the schema
union). The @ts-expect-error suppresses it now and will fail typecheck when
the schema lands — serving as a reminder to remove the skip.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The rum-events-format schema PR has landed, adding RumViewUpdateEvent to
the generated types. Remove the @ts-expect-error skips and runtime guards
that were blocking validation, and fix the TypeScript errors that surface
from the new schema:

- Remove view_update skip from formatValidation.ts and e2e validation.ts
- Update rumEvent.types.ts and profiling.ts with generated types
- Add view_update to developer extension event type color map
- Fix date/view.action optionality from ViewProperties shared schema type

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

if (rawRumEvent.type === 'view_update') {
return SKIPPED
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is view_update explicitly skipped here, but not view?

view_update gets an explicit guard because these events are emitted by SDK-internal timers and observers — user code never triggers them, so handlingStack is never set on their domain context. Without this guard, they'd go through getSourceUrl() only to hit computeStackTrace({ stack: undefined }) and return SKIPPED anyway. The explicit guard makes the intent clear and short-circuits pointless work.

view events don't need the same guard because they can have a meaningful handlingStack — specifically when a user explicitly calls datadogRum.startView(), which captures the call stack. In that case getSourceUrl() can resolve a real file URL and the view event legitimately receives source code context. When no handlingStack is present (the common case), it naturally returns SKIPPED via the contextByFile.get(url) miss at the end.

Copy link

Nice work on this — the diff engine design looks solid, and the REPLACE/MERGE/APPEND strategy categorization is clean. I've been prototyping a parallel implementation against our staging backend and wanted to share some observations.

High severity

1. No post-assembly strip (~500-650B wasted per view_update)

The diff in viewDiff.ts operates on the raw RawRumViewEvent before assembly. Assembly then adds these fields to every view_update identically to full view events:

  • usr, context, connectivity (~150-350B conditional)
  • _dd.configuration (~143B)
  • ddtags (~88B), service+version (~45B), source (~9B)
  • display.viewport (~27B), view.url+view.referrer (~44B)
  • _dd.sdk_name, _dd.format_version, session.type (~12B)
  • feature_flags when unchanged (~40-400B depending on flag count)

These fields have REPLACE semantics — they don't change between updates. In my prototype I added a second pass in startRumBatch.ts that stores the last assembled VIEW per view ID and strips unchanged REPLACE-semantics fields from subsequent view_updates (constructing a new object, not mutating). Steady-state savings: ~523B/VU base + ~56B per flag-set change. Without this, most of the bandwidth savings from the diff engine are negated by assembly overhead.

2. No periodic full VIEW refresh (no recovery from dropped events)

If any view_update is lost (network failure, batch timeout, intake hiccup), the backend's merged state drifts for the entire view lifetime with no self-healing. For SPAs where views can live for minutes or hours, this is a persistent silent corruption risk.

Suggestion: force a full view event every N updates or every T seconds (e.g., every 10 updates or 60s). Acts as a recovery checkpoint. The backend receives a complete snapshot and resets its merge state from that point.

3. No full VIEW on view end

When is_active goes false, the current diff sends a view_update containing only the changed fields from the last snapshot. If any earlier updates were lost, the final terminal state in the backend is incomplete.

Suggestion: always emit a full view event (not a diff) when is_active: false. This guarantees a complete final snapshot regardless of any prior losses.

Medium severity

4. _dd.page_states APPEND semantics are lossy on drop

If a view_update carrying new page_states entries is dropped, those foreground/background transitions are permanently unrecoverable — subsequent appends only send elements added after the last sent state. Given page_states is used for foreground time calculations and session replay stitching, this is a data quality risk.

One option: skip page_states entirely from view_update (let them be captured in periodic full VIEW refreshes as in point 2). Another option: always send the full page_states array on change (REPLACE semantics instead of APPEND), since the array is typically small.

5. feature_flags not covered by the diff

featureFlagContext.ts adds feature_flags to every view_update via assembly hooks — outside the diff engine's scope. They're always included even when unchanged. For customers with many flags this adds meaningful bytes per event. If stripping (point 1) is added, feature_flags would be handled there naturally.

Low severity

6. No snapshot cleanup on view end

diffTracker.reset() fires on new view.id, but completed views' snapshots persist until the tracker is overwritten. Minor memory concern for long-running SPAs with many navigations. Explicitly clearing on is_active: false would bound memory to active views only.


What looks good:

  • REPLACE for custom_timings (correct — whole object semantics)
  • batch.add() instead of upsert() for view_updates (each delta must be independently routable)
  • beforeSend protection (consistent with view)
  • Fallback to full view on diff failure
  • Empty diff = no event emitted
  • Deep clone in diffTracker (safe from mutation)
  • document_version always required in view_update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants