Skip to content

♻️ Rewrite session storage with CookieAccessor and native Web Locks#4252

Draft
thomas-lebeau wants to merge 25 commits intov7from
thomas.lebeau/v7-cookieStore-nativeLock
Draft

♻️ Rewrite session storage with CookieAccessor and native Web Locks#4252
thomas-lebeau wants to merge 25 commits intov7from
thomas.lebeau/v7-cookieStore-nativeLock

Conversation

@thomas-lebeau
Copy link
Collaborator

Motivation

The session management layer had accumulated complexity: custom cookie locking logic, duplicated session managers across packages (rum, logs), tightly coupled session store strategies, and synchronous cookie access patterns. This PR rewrites the session storage stack to be simpler, more robust, and aligned with v7 goals.

Changes

  • Replace the custom cookie lock mechanism with the native Web Locks API for cross-tab coordination
  • Introduce a CookieAccessor abstraction layer that decouples session logic from raw cookie I/O
  • Make SessionStoreStrategy fully async with event-driven change detection instead of polling
  • Move cookieObservable from rum-core to core for proper shared ownership
  • Consolidate rumSessionManager and logsSessionManager into a single core SessionManager
  • Merge TrackedSession into SessionContext, extract SessionReplayState and computeSessionReplayState
  • Handle bridge environments in computeSessionReplayState
  • Extract recording condition helpers in postStartStrategy
  • Make all sampling decisions deterministic
  • Consolidate session manager test mocks and specs into core and rum-core

Test instructions

  • yarn test:unit — all unit tests should pass
  • yarn test:e2e — session store and tracking consent scenarios updated

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

…b to core SessionManager

- Add findTrackedSession(sampleRate, startTime, options) to the SessionManager interface so sampling is computed on demand
- Add TrackedSession type with id, anonymousId, and isReplayForced fields
- Add startSessionManagerStub for event bridge scenarios
- Rebuild session context on state update to reflect forcedReplay changes
…ionReplayState.ts

- Move SessionType, SessionReplayState enums and computeSessionReplayState function out of rumSessionManager.ts
- computeSessionReplayState takes a TrackedSession and RumConfiguration, computing replay state on demand
…ogs package

- Delete logsSessionManager.ts, replacing LogsSessionManager with core SessionManager throughout
- Pass sampleRate to findTrackedSession at each call site
- Update logs mock to implement full SessionManager interface
…m-core package

- Delete rumSessionManager.ts, replacing RumSessionManager/RumSession with core SessionManager/TrackedSession
- Move session replay state tests to computeSessionReplayState unit tests
- Pass sampleRate to findTrackedSession at each call site
- Update rum-core mock to implement full SessionManager interface
- Re-export SessionReplayState and computeSessionReplayState from sessionReplayState.ts
…playState

- Replace RumSessionManager with SessionManager and RumSession with TrackedSession across recorder, profiler, and segment collection
- Use computeSessionReplayState for replay state checks in postStartStrategy and getSessionReplayLink
- Pass sampleRate to findTrackedSession at each call site
- Remove logsSessionManager.spec.ts and rumSessionManager.spec.ts (now redundant)
- Move session manager stub, deterministic sampling, and returnInactive tests into core sessionManager.spec.ts
- Extract computeSessionReplayState tests into sessionReplayState.spec.ts
- Extract repeated session manager callback into a named variable in both
  preStartLogs.ts and preStartRum.ts, cleaning up the if/else branching
- Collapse multi-line imports into single-line in profilerApi.ts and profiler.ts
- Expand long single-line import into multi-line in sessionManager.spec.ts
SessionManager now reads sessionSampleRate directly from the
configuration object, eliminating the need for every call site
to pass it explicitly. This simplifies the API and removes a
source of potential inconsistency.
- Remove separate TrackedSession interface and unify with SessionContext
- Make anonymousId and isReplayForced optional on SessionContext
- Update findTrackedSession return type to SessionContext
- Update all references across rum-core and rum packages
- Remove intermediate object wrapping findTrackedSession
- Pass sessionManager directly, consistent with other context starters
- Merge two guard conditions into a single check in findTrackedSession
- Use spread operator in stub's updateSessionState for generic state updates
- Add clarifying comments on session state mutation and TODO for review
- Replace package-specific mockLogsSessionManager and mockRumSessionManager
  with a single createSessionManagerMock in core/test
- Add createStartSessionManagerMock helper for mocking startSessionManager
- Update all spec files across logs, rum-core and rum to use the shared mock
- Extract `canStartRecording` and `shouldForceReplay` helper functions
  to improve readability of the `start()` function
- Remove redundant comment in sessionReplayState spec
- Return SAMPLED when bridge supports RECORDS capability, OFF otherwise
- Skip sample rate logic entirely in bridge environments
- Add unit tests for bridge capability scenarios
@github-actions
Copy link

github-actions bot commented Feb 27, 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 27, 2026

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 171.75 KiB 172.01 KiB +264 B +0.15%
Rum Profiler 4.67 KiB 4.67 KiB 0 B 0.00%
Rum Recorder 24.88 KiB 25.10 KiB +227 B +0.89%
Logs 56.47 KiB 57.21 KiB +752 B +1.30%
Flagging 944 B 944 B 0 B 0.00%
Rum Slim 127.36 KiB 127.77 KiB +416 B +0.32%
Worker 23.63 KiB 23.63 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base CPU Time (ms) Local CPU Time (ms) 𝚫%
RUM - add global context 0.0049 0.0043 -12.24%
RUM - add action 0.0185 0.0139 -24.86%
RUM - add error 0.0183 0.0155 -15.30%
RUM - add timing 0.004 0.0047 +17.50%
RUM - start view 0.0166 0.0166 0.00%
RUM - start/stop session replay recording 0.001 0.0007 -30.00%
Logs - log message 0.021 0.0185 -11.90%
🧠 Memory Performance
Action Name Base Memory Consumption Local Memory Consumption 𝚫
RUM - add global context 26.18 KiB 36.63 KiB +10.45 KiB
RUM - add action 111.61 KiB 301.19 KiB +189.58 KiB
RUM - add timing 27.33 KiB 41.71 KiB +14.38 KiB
RUM - add error 114.24 KiB 297.61 KiB +183.37 KiB
RUM - start/stop session replay recording 26.42 KiB 26.84 KiB +428 B
RUM - start view 504.75 KiB 660.02 KiB +155.27 KiB
Logs - log message 45.21 KiB 200.88 KiB +155.66 KiB

🔗 RealWorld

@datadog-datadog-prod-us1
Copy link

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

⚠️ Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

🧪 29 Tests failed

browser extensions › should not warn - edge case simulating NextJs with an extension that override `appendChild` from browser-extensions/browserExtensions.scenario.ts (Datadog) (Fix with Cursor)

    Error: expect(received).toHaveLength(expected)

    Expected length: 1
    Received length: 2
    Received array:  [{"_dd": {"configuration": {"profiling_sample_rate": 0, "session_replay_sample_rate": 100, "session_sample_rate": 100, "start_session_replay_recording_manually": false, "trace_sample_rate": 100}, "document_version": 2, "drift": 0, "format_version": 2, "page_states": [{"start": 0, "state": "active"}], "replay_stats": {"records_count": 4, "segments_count": 1, "segments_total_raw_size": 0}, "sdk_name": "rum"}, "application": {"id": "37fe52bf-b3d5-4ac7-ad9b-44882d479ec8"}, "connectivity": {"effective_type": "4g", "status": "connected"}, "date": 1772796169742, "ddtags": "sdk_version:dev", "device": {"locale": "en-US", "locales": ["en-US"], "time_zone": "UTC"}, "display": {"viewport": {"height": 720, "width": 1280}}, "privacy": {"replay_level": "allow"}, "session": {"has_replay": true, "id": "50737945-0265-41f8-a0a9-99f2492ae896", "sampled_for_replay": true, "type": "user"}, "source": "browser", "type": "view", "usr": {"anonymous_id": "48cd5e7e-1d33-428b-99a8-a30cc6c244ed"}, "view": {"action": {"count": 0}, "cumulative_layout_shift": 0, "dom_complete": 227200000, "dom_content_loaded": 209700000, "dom_interactive": 196800000, "error": {"count": 0}, "first_byte": 9400000, "frustration": {"count": 0}, "id": "ab1d69bc-b1a1-4ceb-b68d-222fc99b1358", "is_active": true, "load_event": 227300000, "loading_time": 296000000, "loading_type": "initial_load", "long_task": {"count": 1}, "performance": {"cls": {"score": 0}}, "referrer": "", "resource": {"count": 5}, "time_spent": 991000000, "url": "http://172.28.208.44:9265/"}}, {"_dd": {"configuration": {"profiling_sample_rate": 0, "session_replay_sample_rate": 100, "session_sample_rate": 100, "start_session_replay_recording_manually": false, "trace_sample_rate": 100}, "document_version": 3, "drift": 0, "format_version": 2, "page_states": [{"start": 0, "state": "active"}], "replay_stats": {"records_count": 5, "segments_count": 2, "segments_total_raw_size": 958}, "sdk_name": "rum"}, "application": {"id": "37fe52bf-b3d5-4ac7-ad9b-44882d479ec8"}, "connectivity": {"effective_type": "4g", "status": "connected"}, "date": 1772796169742, "ddtags": "sdk_version:dev", "device": {"locale": "en-US", "locales": ["en-US"], "time_zone": "UTC"}, "display": {"viewport": {"height": 720, "width": 1280}}, "privacy": {"replay_level": "allow"}, "session": {"has_replay": true, "id": "50737945-0265-41f8-a0a9-99f2492ae896", "is_active": false, "sampled_for_replay": true, "type": "user"}, "source": "browser", "type": "view", "usr": {"anonymous_id": "48cd5e7e-1d33-428b-99a8-a30cc6c244ed"}, "view": {"action": {"count": 0}, "cumulative_layout_shift": 0, "dom_complete": 227200000, "dom_content_loaded": 209700000, "dom_interactive": 196800000, "error": {"count": 0}, "first_byte": 9400000, "frustration": {"count": 0}, "id": "ab1d69bc-b1a1-4ceb-b68d-222fc99b1358", "is_active": false, "load_event": 227300000, "loading_time": 296000000, "loading_type": "initial_load", "long_task": {"count": 1}, "performance": {"cls": {"score": 0}}, "referrer": "", "resource": {"count": 5}, "time_spent": 1094000000, "url": "http://172.28.208.44:9265/"}}]

      110 |       await flushEvents()
      111 |
    > 112 |       expect(intakeRegistry.rumViewEvents).toHaveLength(1)
...
startSessionManager automatic session expiration should expand duration on activity from Chrome Headless 139.0.0.0 (Linux 0.0.0) (Datadog) (Fix with Cursor)
Error: Expected spy unknown to have been called.
    at <Jasmine>
    at UserContext.<anonymous> (/go/src/github.com/DataDog/browser-sdk/packages/core/src/domain/session/sessionManager.spec.ts:348:32 <- /tmp/_karma_webpack_302826/commons.js:322713:38)
startSessionManager automatic session expiration should expand session on visibility from Chrome Headless 139.0.0.0 (Linux 0.0.0) (Datadog) (Fix with Cursor)
Error: Expected spy unknown to have been called.
    at <Jasmine>
    at UserContext.<anonymous> (/go/src/github.com/DataDog/browser-sdk/packages/core/src/domain/session/sessionManager.spec.ts:372:32 <- /tmp/_karma_webpack_302826/commons.js:322732:38)
View all

ℹ️ Info

❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 68.18%
Overall Coverage: 76.72%

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 2dc8eb7 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@thomas-lebeau thomas-lebeau changed the base branch from main to thomas.lebeau/v7-simplify-session-managers February 27, 2026 09:54
…dSession

- Return full session object from findTrackedSession instead of a subset of properties
- Remove redundant session parameter from canStartRecording, derive recording eligibility from replayState alone
- Update tests to call setupRecorderApi in describe blocks closer to where configuration is needed
- Replace runtime mutation of sessionReplaySampleRate with LOW_HASH_UUID
  and HIGH_HASH_UUID to control whether a session gets replay, reflecting
  that configuration does not change across session renewals
- Reset sample decision cache in cleanup to avoid cross-test leakage
- Remove now-unnecessary local `configuration` variables
- Guard three test suites that rely on deterministic hash-based
  sampling (HIGH_HASH_UUID / sessionReplaySampleRate) with a BigInt
  availability check, marking them as pending in environments that
  lack support
- Remove sessionStoreOperations and its cookie-based lock mechanism
- Introduce sessionLock module using navigator.locks for concurrency
- Simplify sessionStore by inlining retrieve/persist logic within lock
- Remove isLockEnabled from SessionStoreStrategy interface
- Clean up related tests and fake session store strategy
The cookieObservable only depends on browser-core types and will be
needed by the session cookie strategy in core. Move it to core and
re-export from rum-core to keep existing imports working.
Introduce a CookieAccessor interface that abstracts cookie operations.
Uses the CookieStore API when available for async native cookie access,
and falls back to document.cookie with Promise wrappers.

Also add set() and delete() to the CookieStore type definition.
Make the SessionStoreStrategy interface async (methods return Promises)
to support the CookieStore API which is inherently asynchronous.

Key changes:
- SessionStoreStrategy methods return Promises
- sessionLock supports async callbacks with promise-chain fallback
  when navigator.locks is unavailable
- sessionStore lock callbacks are async, expire() runs inside lock
- Replace polling-based session watching with onExternalChange:
  cookie strategy uses CookieStore events or interval polling,
  localStorage strategy uses native storage events
- Cookie strategy uses CookieAccessor for read/write operations
- All test suites updated with flushLock() async drain pattern
…change

- Update `previousCookieValue` after each change notification so
  consecutive changes are correctly detected and the callback is not
  called on every interval tick when the value is stable
- Consolidate `onExternalChange` in sessionInCookie to always use
  `createCookieObservable` (CookieStore vs. polling is now handled
  internally by the observable)
- Deduplicate `CookieStoreWindow` interface by re-exporting it from
  `cookieObservable` and importing it where needed
- Replace `isEmptyObject` check with `isSessionInNotStartedState` for
  clearer intent when deciding whether to encode cookie options
@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/v7-cookieStore-nativeLock branch from 7fcd324 to 2dc8eb7 Compare March 6, 2026 11:17
Base automatically changed from thomas.lebeau/v7-simplify-session-managers to v7 March 9, 2026 10:10
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.

1 participant