Skip to content

Persist browser exposure deduplication in IndexedDB - #188

Merged
leoromanovsky merged 10 commits into
mainfrom
leo/move-assignment-cache-to-indexeddb
Oct 8, 2026
Merged

leoromanovsky merged 10 commits into
mainfrom
leo/move-assignment-cache-to-indexeddb

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Feb 27, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Browser exposure deduplication still uses localStorage or Chrome storage. localStorage serializes and writes the cache during evaluation. Use IndexedDB, which already stores flag configurations, for asynchronous persistence.

Changes and Decisions

  • Keep synchronous exposure checks in one bounded memory cache. Batch pending entries and merge them with stored entries in one IndexedDB transaction.
  • Preserve the current cache scope, 50,000-entry limit, exposure identity, and createdAt invalidation behavior.
  • Store an invalidation counter when clearing a scope. Discard older pending writes and retries from other instances so they cannot restore cleared entries.
  • Handle initialization, write, and clear races without stopping flag evaluation. Storage failures retain in-memory deduplication. Discarded batches can produce repeat exposures.
  • Remove the hybrid wrapper and old storage backends. Share the existing IndexedDB opener; no database upgrade or new dependency is needed.
  • Do not migrate old exposure entries. Upgrades or unfinished writes can produce repeat exposures. Cross-tab logging is not exactly-once.

Validation

Ran the packed release-mode SDK in Chromium. Verified exposure deduplication across reloads and obfuscation salt changes, separate client-token scopes, and original flag keys in exposure payloads. All 10 browser smoke scenarios passed. Responses came from local fixtures; this was not a staging test.

Base automatically changed from leo/add-indexeddb-flags-persistence to main February 28, 2026 20:40
…tToken

Replace the localStorage-based assignment cache (exposure deduplication)
with IndexedDB, consistent with the flags cache. Storage is now scoped
by clientToken via buildStorageKeySuffix(), so different apps on the same
origin no longer share state.

- New IndexedDBAssignmentCache: same dd-flagging DB / configurations
  store, in-memory Map mirror for fast synchronous set(), fire-and-forget
  persistence to IndexedDB.
- Simplified factory: IndexedDB → memory-only (Chrome storage and
  localStorage dropped).
- Updated provider to pass clientToken instead of hardcoded key.
@leoromanovsky
leoromanovsky force-pushed the leo/move-assignment-cache-to-indexeddb branch from c40284b to 9998cf0 Compare March 1, 2026 14:02
@leoromanovsky
leoromanovsky requested a review from Copilot March 2, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR changes the browser SDK’s exposure/assignment deduplication cache persistence to use IndexedDB (shared with the flags cache) and scopes the persisted data by clientToken, preventing cross-app collisions on the same origin.

Changes:

  • Introduces IndexedDBAssignmentCache and updates assignmentCacheFactory to use IndexedDB (or fall back to memory-only).
  • Updates the OpenFeature provider to pass clientToken into the assignment cache factory.
  • Updates/adds tests to use fake-indexeddb and to reset IndexedDB between test runs; removes Chrome storage assignment cache code.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/browser/src/cache/indexeddb-assignment-cache.ts Adds new IndexedDB-backed assignment cache with an in-memory mirror and fire-and-forget persistence.
packages/browser/src/cache/assignment-cache-factory.ts Simplifies factory to choose between IndexedDB-backed hybrid cache and memory-only cache.
packages/browser/src/openfeature/provider.ts Switches provider to create the exposure cache using clientToken.
packages/browser/src/cache/helpers.ts Removes Chrome storage helpers; keeps localStorage + IndexedDB availability helpers.
packages/browser/src/cache/chrome-storage-async-map.ts Removes Chrome storage async map implementation.
packages/browser/src/cache/chrome-storage-assignment-cache.ts Removes Chrome storage assignment cache implementation.
packages/browser/test/openfeature/exposures.spec.ts Resets IndexedDB between tests instead of clearing localStorage; adds fake-indexeddb setup.
packages/browser/test/cache/indexeddb-assignment-cache.spec.ts Adds coverage for IndexedDBAssignmentCache behavior and token isolation.
packages/browser/test/cache/hybrid-assignment-cache.spec.ts Updates hybrid cache test to hydrate serving cache from persistent IndexedDB store.
packages/browser/test/cache/assignment-cache-factory.spec.ts Updates factory tests for IndexedDB availability and memory-only fallback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated
Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated
Comment thread packages/browser/src/cache/assignment-cache-factory.ts Outdated
Comment thread packages/browser/test/cache/assignment-cache-factory.spec.ts Outdated
Comment thread packages/browser/test/cache/hybrid-assignment-cache.spec.ts Outdated
Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated
Comment thread packages/browser/test/cache/assignment-cache-factory.spec.ts Outdated
Comment thread packages/browser/test/cache/hybrid-assignment-cache.spec.ts Outdated
Comment thread packages/browser/test/openfeature/exposures.spec.ts Outdated
Comment thread packages/browser/test/openfeature/exposures.spec.ts
- Delete orphaned localStorage and Chrome storage cache files and helpers.
- Extract shared openDB() and IndexedDB constants into indexeddb-store.ts.
- Batch persist() writes via queueMicrotask so rapid set() calls coalesce
  into a single IDB transaction per tick.
- Move structuredClone polyfill and fake-indexeddb setup to shared jest
  setup file.
- Replace inline require() with top-level import in exposures spec.
- Add cross-instance persistence test for the page-reload scenario.
@leoromanovsky
leoromanovsky marked this pull request as ready for review March 2, 2026 17:30
@leoromanovsky
leoromanovsky requested a review from a team as a code owner March 2, 2026 17:30
Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated

@greghuels greghuels left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nothing major, but one thing to note is that the local exposure deduplication cache might be wiped out after users upgrade. I don't think that's blocking though, and a migration from localStorage to indexeddb might be overkill.

Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated
@leoromanovsky

Copy link
Copy Markdown
Collaborator Author

Nothing major, but one thing to note is that the local exposure deduplication cache might be wiped out after users upgrade. I don't think that's blocking though, and a migration from localStorage to indexeddb might be overkill.

Yes we're starting fresh.

@leoromanovsky leoromanovsky changed the title Move assignment cache to IndexedDB, scoped by clientToken Persist browser exposure deduplication in IndexedDB Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

OpenFeature Browser Provider Bundle Sizes

Measured from the Vite production output after installing packed @datadog/flagging-core and @datadog/openfeature-browser tarballs.

Both scenarios initialize an OpenFeature provider, evaluate a boolean flag, change context, and evaluate again. Telemetry is disabled for DatadogProvider; no tracking hooks are registered for DatadogCoreProvider. DatadogProvider fetches precomputed assignments for each context; DatadogCoreProvider receives rules from fetchRulesConfiguration once and evaluates locally.

Sizes include OpenFeature and the same small scenario harness. Configuration responses are supplied by Playwright and are not bundled. These are complete scenario JS sizes, not configuration payload sizes or isolated provider/Protobuf costs; the difference between rows is not a decoder-only delta.

Scenario HTML JS Assets Raw JS Gzip JS Protobuf Markers
DatadogProvider (precomputed fetching) provider.html 19 82.9 KiB 31.4 KiB no
DatadogCoreProvider + fetchRulesConfiguration core-provider.html 14 149.3 KiB 46.8 KiB yes

Dependency checks passed: no Protobuf markers in default/precomputed scenarios; markers present in rules-based scenarios.

OpenFeature Browser Tracking Hook Bundle Sizes

Synthetic entrypoints import and call tracking hook factories from the packed @datadog/openfeature-browser/rules-based ESM package. Deltas are measured against the no-hook baseline from the same Vite production build.

Scenario HTML JS Assets Raw JS Raw Δ Gzip JS Gzip Δ
tracking hook baseline tracking-baseline.html 6 2.6 KiB 0 B 1.6 KiB 0 B
exposure logging hook tracking-exposure.html 14 56.9 KiB +54.3 KiB 22.1 KiB +20.5 KiB
evaluation logging hook tracking-evaluation.html 14 53.2 KiB +50.6 KiB 20.8 KiB +19.2 KiB
RUM tracking hook tracking-rum.html 10 5.1 KiB +2.5 KiB 3.0 KiB +1.4 KiB
all tracking hooks tracking-all.html 17 61.2 KiB +58.6 KiB 24.1 KiB +22.5 KiB

OpenFeature Node Server Bundle Sizes

Independent Webpack production builds of the installed core and Node SDK tarballs, targeting Node 18 with ESM (module export condition). Node built-ins remain external. OpenFeature, dd-trace, and configuration fixtures are not bundled.

Scenario Raw JS Gzip JS Protobuf Modules
Node provider only 36.3 KiB 11.5 KiB no
SSR helpers only (fetch client rules + serialize) 80.1 KiB 23.1 KiB yes
Node provider + SSR helpers 115.9 KiB 33.6 KiB yes

Adding SSR helpers to the provider adds 79.7 KiB raw / 22.1 KiB gzip in this build. This includes fetching, parsing, and serialization, not just the decoder. SSR helpers fetch rules for the browser, not for configuring that provider (which uses its existing JSON API).

These are bundler sizes, not npm install sizes, unbundled Node require/import costs, payload sizes, or latency. Gzip compares compressed artifacts. The browser scenarios include OpenFeature and a different harness, so their sizes are not directly comparable.

Dependency checks passed: no Protobuf runtime or generated schema modules in the provider-only bundle; present in both SSR bundles (checked using Webpack module statistics).

This report shows current PR artifact sizes only; it does not compare against the base branch.

Comment thread packages/browser/src/cache/indexeddb-assignment-cache.ts Outdated
…ency

🐛 fix(browser): preserve exposure cache updates across instances
@leoromanovsky
leoromanovsky merged commit e37fc72 into main Oct 8, 2026
4 checks passed
@leoromanovsky
leoromanovsky deleted the leo/move-assignment-cache-to-indexeddb branch October 8, 2026 18:29
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.

4 participants