From 7ee6dc2eb073f9ec3853034bcad12bbbfe3fbba3 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Tue, 10 Nov 2026 13:27:18 -0700 Subject: [PATCH] fix(config)!: BREAKING: default optableCacheTargeting to OPTABLE_RESOLVED targeting() wrote its public cache copy to "optable-cache:targeting", while buildRTD, pubProvidedId and identifyAndTokenize all default to "OPTABLE_RESOLVED". With default configs RTD never saw targeting results and tokenize-resolved UID2s were not refreshable from cache. OPTABLE_RESOLVED is already the convention used by wrapper integrations; make it the default. Move the default into DCN_DEFAULTS. The uid2-refresh test for the default key read DCN_DEFAULTS.optableCacheTargeting, which did not exist, so it was testing undefined; it now checks the real default. BREAKING CHANGE: the default public targeting cache key changes from "optable-cache:targeting" to "OPTABLE_RESOLVED". Integrations reading "optable-cache:targeting" directly must set optableCacheTargeting explicitly. targeting() now overwrites OPTABLE_RESOLVED, including EIDs merged there by identifyAndTokenize. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- lib/addons/uid2-refresh.md | 2 +- lib/addons/uid2-refresh.test.ts | 4 ++-- lib/addons/uid2-refresh.ts | 2 +- lib/config.test.ts | 2 +- lib/config.ts | 5 +++-- lib/sdk.test.ts | 4 ++-- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0307ea71..dffb0d24 100644 --- a/README.md +++ b/README.md @@ -168,8 +168,8 @@ When creating an instance of `OptableSDK`, you can pass an `InitConfig` object t - **`readOnly` (boolean, default: `false`)** When set to `true`, puts the SDK in a read-only mode, preventing any data modifications while still allowing API queries. -- **`optableCacheTargeting` (string, defaults: `optable-cache:targeting`)** - Local storage cache key used to store latest targeting response. +- **`optableCacheTargeting` (string, defaults: `OPTABLE_RESOLVED`)** + Local storage cache key used to store latest targeting response. The default is the key the Prebid RTD module (`buildRTD`), `pubProvidedId` and `identifyAndTokenize` use, so they share one cache; if you change it, pass the same key to those too. - **`ois` (boolean, default: `false`)** When set to `true`, participates in the [Optable Identity System](#optable-identity-system-ois): the SDK stores the derived OIS ID the DCN returns on the `X-Optable-OID` response header and replays it on subsequent requests, so the DCN recognizes the browser instead of deriving a new identity each visit. Requires a DCN node with OIS ID derivation enabled and device access consent, so it is a no-op otherwise. The `OPTABLE_OID` cookie identity is separate and needs no configuration. diff --git a/lib/addons/uid2-refresh.md b/lib/addons/uid2-refresh.md index 8bce7cba..0cf4666c 100644 --- a/lib/addons/uid2-refresh.md +++ b/lib/addons/uid2-refresh.md @@ -57,6 +57,6 @@ await refreshStaleUid2sFromCache(sdk.dcn); Reads every stored copy of the targeting cache and refreshes each distinct UID2 past its `refresh_from` through `refreshStaleUid2s`. Copies can hold different identities, so refreshes are keyed by refresh token: two copies holding the same token make one request, and two holding different ones are refreshed and written back separately. Use this on a page that served targeting from cache: only `mergeCache` produces a `staleUid2s` list, and it runs when a response is merged, so a page that never calls the edge has nothing to pass to `refreshStaleUid2s`. That is also the page where the cached token has had time to age. -Both copies are checked, the private `OPTABLE_TARGETING_` one and the public `optableCacheTargeting` one, because they can differ: `identifyAndTokenize` writes its merged cache only to its own `cacheKey` (`OPTABLE_RESOLVED` by default), which is also where prebid RTD and `pubProvidedId` read from. For a tokenize-resolved UID2 to be refreshable, that key and `optableCacheTargeting` have to name the same thing; the config default (`optable-cache:targeting`) does not. `applyUid2Refresh` then writes each outcome back to the copies holding that refresh token. +Both copies are checked, the private `OPTABLE_TARGETING_` one and the public `optableCacheTargeting` one, because they can differ: `identifyAndTokenize` writes its merged cache only to its own `cacheKey` (`OPTABLE_RESOLVED` by default), which is also where prebid RTD and `pubProvidedId` read from. For a tokenize-resolved UID2 to be refreshable, that key and `optableCacheTargeting` have to name the same thing; both default to `OPTABLE_RESOLVED`. `applyUid2Refresh` then writes each outcome back to the copies holding that refresh token. A missing cache, an unparseable copy, a cache without refresh material, and a token not yet due are all no-ops that make no request. diff --git a/lib/addons/uid2-refresh.test.ts b/lib/addons/uid2-refresh.test.ts index 5bcd06f9..c439c0da 100644 --- a/lib/addons/uid2-refresh.test.ts +++ b/lib/addons/uid2-refresh.test.ts @@ -490,7 +490,7 @@ describe("refreshStaleUid2sFromCache", () => { expect(calls).toBe(0); }); - it("does not read the tokenize cache under the default optableCacheTargeting", async () => { + it("reads the tokenize cache under the default optableCacheTargeting", async () => { localStorage.setItem( "OPTABLE_RESOLVED", JSON.stringify({ @@ -501,7 +501,7 @@ describe("refreshStaleUid2sFromCache", () => { await refreshStaleUid2sFromCache({ ...config, optableCacheTargeting: DCN_DEFAULTS.optableCacheTargeting }); - expect(calls).toBe(0); + expect(calls).toBe(1); }); it("applies each refresh only to the copy holding that token", async () => { diff --git a/lib/addons/uid2-refresh.ts b/lib/addons/uid2-refresh.ts index 46235678..eb6ed493 100644 --- a/lib/addons/uid2-refresh.ts +++ b/lib/addons/uid2-refresh.ts @@ -164,7 +164,7 @@ async function refreshStaleUid2s(config: ResolvedConfig, stale: StaleUid2[]): Pr */ async function refreshStaleUid2sFromCache(config: ResolvedConfig): Promise { // identifyAndTokenize writes only to its own cacheKey, so its copy is read - // here only when optableCacheTargeting names that same key. + // here only when optableCacheTargeting names that same key (true by default). const stale = new Map(); try { for (const cached of new LocalStorage(config).getTargetingCopies() as ResolvedCache[]) { diff --git a/lib/config.test.ts b/lib/config.test.ts index 63ccdb36..847d3c19 100644 --- a/lib/config.test.ts +++ b/lib/config.test.ts @@ -14,7 +14,7 @@ describe("getConfig", () => { experiments: [], sessionID: "", skipEnrichment: undefined, - optableCacheTargeting: "optable-cache:targeting", + optableCacheTargeting: "OPTABLE_RESOLVED", }); }); diff --git a/lib/config.ts b/lib/config.ts index 99c22f57..b2fe0fc2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -60,7 +60,7 @@ type InitConfig = { skipEnrichment?: boolean; // Targeting call on load initTargeting?: boolean; - // (Defaults to 'optable_cache_targeting') Cache Key used to store 'targeting' response + // (Defaults to 'OPTABLE_RESOLVED', the key prebid RTD and pubProvidedId read) Cache Key used to store 'targeting' response optableCacheTargeting?: string; // AB test configuration to define testing configurations abTests?: ABTestConfig[]; @@ -120,6 +120,7 @@ const DCN_DEFAULTS = { initPassport: true, readOnly: false, experiments: [], + optableCacheTargeting: "OPTABLE_RESOLVED", consent: { reg: null, deviceAccess: true, @@ -133,7 +134,7 @@ function getConfig(init: InitConfig): ResolvedConfig { const config: ResolvedConfig = { host: init.host, site: init.site, - optableCacheTargeting: init.optableCacheTargeting ?? "optable-cache:targeting", + optableCacheTargeting: init.optableCacheTargeting ?? DCN_DEFAULTS.optableCacheTargeting, cookies: init.cookies ?? DCN_DEFAULTS.cookies, initPassport: init.initPassport ?? DCN_DEFAULTS.initPassport, consent: DCN_DEFAULTS.consent, diff --git a/lib/sdk.test.ts b/lib/sdk.test.ts index 6e083595..34ef2941 100644 --- a/lib/sdk.test.ts +++ b/lib/sdk.test.ts @@ -250,7 +250,7 @@ describe("behavior testing of", () => { experiments: [], sessionID: "session", skipEnrichment: false, - optableCacheTargeting: "optable-cache:targeting", + optableCacheTargeting: "OPTABLE_RESOLVED", }); await sdk["init"]; expect(localStorage.setItem).toBeCalledTimes(0); @@ -292,7 +292,7 @@ describe("behavior testing of", () => { experiments: [], sessionID: "session", skipEnrichment: false, - optableCacheTargeting: "optable-cache:targeting", + optableCacheTargeting: "OPTABLE_RESOLVED", }); await sdk["init"]; expect(window.localStorage.setItem).toHaveBeenLastCalledWith(