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
14 changes: 11 additions & 3 deletions packages/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,20 @@ Reapplying the same configuration preserves deduplication. Neither provider emit
`createdAt` is a configuration timestamp, not an experiment revision. This behavior does not depend on it changing on every request.

Exposure caches retain up to 50,000 entries per scope, matching the Node provider's limit.
The memory cache removes the least recently used entry. Persistent caches remove the oldest written entries.
An evicted entry can produce another exposure. Storage failures do not prevent flag evaluation.
Exposure checks use a bounded in-memory cache. Background IndexedDB transactions merge newly recorded exposures
with the stored entries, so instances sharing a scope preserve each other's writes without replaying their loaded snapshots.
Memory removes the least recently used entries; persistence removes the oldest written entries when it reaches its limit.
Evicted entries can produce another exposure.
Clearing a scope stores an empty cache with an incremented invalidation counter. Other instances reject pending or retried
writes with an older counter. On their next write, they discard stale memory and pending entries, then load the current cache.
This can produce repeat exposures. Other tabs' memory caches are not updated immediately, and concurrent tabs can still log
the same exposure. The cache reduces duplicates; it does not guarantee exactly-once logging across tabs.
If IndexedDB is unavailable or a storage operation fails, evaluation and in-memory deduplication continue.
An abrupt page exit can lose an unfinished write and permit a repeat exposure after reload.

For rules-based configurations, `DatadogCoreProvider` also includes the rules configuration identity. Changed rules allow new exposures without clearing application-managed hook state. Retrieval metadata (`fetchedAt` and `etag`) and the server's `createdAt` build timestamp do not change that identity. These fields remain available on the configuration and in its portable wire representation.

Both the standalone exposure hook and `DatadogProvider` scope persistent exposure caches by telemetry site, client token, proxy URL, environment, application, service, and source. Scope values are hashed into the storage namespace; raw tokens are not stored in cache keys. Recreating a hook with the same scope retains deduplication, while another destination can emit its own exposures. Older unscoped cache entries are not reused, so upgrading can produce a one-time repeat exposure. Function-valued telemetry proxies use memory-only deduplication because their destination cannot be inferred reliably from the callback's identity.
Both the standalone exposure hook and `DatadogProvider` scope persistent exposure caches by telemetry site, client token, proxy URL, environment, application, service, and source. Scope values are hashed into the storage namespace; raw tokens are not stored in cache keys. Recreating a hook with the same scope retains deduplication, while another destination can emit its own exposures. Existing localStorage and Chrome storage entries are not migrated or deleted, so upgrading can produce repeat exposures. Function-valued telemetry proxies use memory-only deduplication because their destination cannot be inferred reliably from the callback's identity.

To exclude one of these integrations, omit that hook factory from both the import list and `composeDatadogTrackingHooks()` call.

Expand Down
2 changes: 1 addition & 1 deletion packages/browser/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/setup-indexeddb.ts', '<rootDir>/src/test-setup.ts'],
transform: {
'^.+\\.ts$': [
'ts-jest',
Expand Down
28 changes: 5 additions & 23 deletions packages/browser/src/cache/assignment-cache-factory.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
import type { AssignmentCache } from '@datadog/flagging-core'
import ChromeStorageAssignmentCache from './chrome-storage-assignment-cache'
import { hasWindowLocalStorage } from './helpers'
import HybridAssignmentCache from './hybrid-assignment-cache'
import { LocalStorageAssignmentCache } from './local-storage-assignment-cache'
import { hasIndexedDB } from './helpers'
import { IndexedDBAssignmentCache } from './indexeddb-assignment-cache'
import SimpleAssignmentCache from './simple-assignment-cache'

export function assignmentCacheFactory({
forceMemoryOnly = false,
chromeStorage,
storageKeySuffix,
}: {
forceMemoryOnly?: boolean
storageKeySuffix: string
chromeStorage?: chrome.storage.StorageArea
}): AssignmentCache {
const simpleCache = new SimpleAssignmentCache()

if (forceMemoryOnly) {
return simpleCache
}

if (chromeStorage) {
const chromeStorageCache = new ChromeStorageAssignmentCache(chromeStorage, storageKeySuffix)
return new HybridAssignmentCache(simpleCache, chromeStorageCache)
} else {
if (hasWindowLocalStorage()) {
const localStorageCache = new LocalStorageAssignmentCache(storageKeySuffix)
return new HybridAssignmentCache(simpleCache, localStorageCache)
} else {
return simpleCache
}
}
return !forceMemoryOnly && hasIndexedDB()
? new IndexedDBAssignmentCache(storageKeySuffix)
: new SimpleAssignmentCache()
}
43 changes: 0 additions & 43 deletions packages/browser/src/cache/chrome-storage-assignment-cache.ts

This file was deleted.

90 changes: 0 additions & 90 deletions packages/browser/src/cache/chrome-storage-async-map.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/browser/src/cache/exposure-cache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getMD5Hash } from '@datadog/flagging-core'
import type { FlaggingTrackingConfiguration, FlaggingTrackingInitConfiguration } from '../domain/configuration'
import { assignmentCacheFactory } from './assignment-cache-factory'
import { chromeStorageIfAvailable } from './helpers'

export function createExposureCache(
options: Partial<FlaggingTrackingInitConfiguration>,
Expand All @@ -22,7 +21,6 @@ export function createExposureCache(
)
return assignmentCacheFactory({
forceMemoryOnly,
chromeStorage: chromeStorageIfAvailable(),
storageKeySuffix: `dd-of-browser-${scope}`,
})
}
22 changes: 0 additions & 22 deletions packages/browser/src/cache/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
export function hasChromeStorage(): boolean {
return typeof chrome !== 'undefined' && !!chrome.storage
}

export function chromeStorageIfAvailable(): chrome.storage.StorageArea | undefined {
return hasChromeStorage() ? chrome.storage.local : undefined
}

/** Returns whether `window.localStorage` is available */
export function hasWindowLocalStorage(): boolean {
try {
return typeof window !== 'undefined' && !!window.localStorage
} catch {
// Chrome throws an error if local storage is disabled, and you try to access it
return false
}
}

export function localStorageIfAvailable(): Storage | undefined {
return hasWindowLocalStorage() ? window.localStorage : undefined
}

/** Returns whether IndexedDB is available */
export function hasIndexedDB(): boolean {
try {
Expand Down
44 changes: 0 additions & 44 deletions packages/browser/src/cache/hybrid-assignment-cache.ts

This file was deleted.

Loading
Loading