From e16231668097a727c83e4b15029dc26e98091ea7 Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Wed, 21 Oct 2026 13:05:57 -0400 Subject: [PATCH 1/2] identifyAndTokenize: forward nonOverridingMatchers to the cache merge --- README.md | 2 ++ lib/core/identify-tokenize.test.ts | 17 +++++++++++++++++ lib/core/identify-tokenize.ts | 6 +++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90c8ae16..d7aead90 100644 --- a/README.md +++ b/README.md @@ -1227,6 +1227,8 @@ import { identifyAndTokenize } from "@optable/web-sdk/lib/dist/core/identify-tok window.optable.identifyAndTokenize = (id) => identifyAndTokenize(sdk, id, { isControlGroup: () => isControlGroup }); ``` +`options` also carries the cache write: `cacheKey` (default `OPTABLE_RESOLVED`), and `maxUidsPerEid` and `nonOverridingMatchers`, both forwarded to [`mergeCache`](lib/core/eid-cache.md). Pass `nonOverridingMatchers` when tokenize runs alongside a targeting call that skipped the first-party matcher, so a first-party EID does not displace a third-party one already held on the same source. + The returned `{ merged, staleUid2s }` (null when skipped) lets the caller chain UID2 refreshes on the merged cache. ## Passport and Visitor ID diff --git a/lib/core/identify-tokenize.test.ts b/lib/core/identify-tokenize.test.ts index 2c420477..3b3e97ca 100644 --- a/lib/core/identify-tokenize.test.ts +++ b/lib/core/identify-tokenize.test.ts @@ -153,6 +153,23 @@ describe("identifyAndTokenize - failure paths and options", () => { expect(eids[0].uids).toHaveLength(1); }); + it("keeps a held third-party EID when tokenize returns a nonOverridingMatchers EID on its source", async () => { + localStorage.setItem( + "OPTABLE_RESOLVED", + JSON.stringify({ + ortb2: { user: { eids: [{ source: "liveramp.com", matcher: "3p.example", uids: [{ id: "third-party" }] }] } }, + }) + ); + const { sdk, tokenize } = makeSdk(); + tokenize.mockResolvedValue({ + user: { eids: [{ source: "liveramp.com", matcher: "acme.com", uids: [{ id: "first-party" }] }] }, + } as any); + + await identifyAndTokenize(sdk, "abc123", { nonOverridingMatchers: ["acme.com"] }); + + expect(cachedEids().map((e: any) => e.uids[0].id)).toEqual(["third-party"]); + }); + it("returns the stale UID2s the merge found, for the refresh loop to chain on", async () => { localStorage.setItem( "OPTABLE_RESOLVED", diff --git a/lib/core/identify-tokenize.ts b/lib/core/identify-tokenize.ts index 88fe467e..78594a1a 100644 --- a/lib/core/identify-tokenize.ts +++ b/lib/core/identify-tokenize.ts @@ -19,6 +19,7 @@ type IdentifyAndTokenizeOptions = { isControlGroup?: () => boolean; cacheKey?: string; maxUidsPerEid?: number; + nonOverridingMatchers?: string[]; }; type IdentifyAndTokenizeResult = { @@ -125,7 +126,10 @@ export async function identifyAndTokenize( } // mergeCache takes cache format, and replaceCache is idempotent. - const result = mergeCache(replaceCache(asCache), cached, { maxUidsPerEid: options.maxUidsPerEid }); + const result = mergeCache(replaceCache(asCache), cached, { + maxUidsPerEid: options.maxUidsPerEid, + nonOverridingMatchers: options.nonOverridingMatchers, + }); localStorage.setItem(cacheKey, JSON.stringify(result.merged)); sendTargetingUpdateEvent(sdk.dcn, result.merged as TargetingResponse); From f6da4d2d9547222f4041f21fa29d79ebcb556375 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Fri, 23 Oct 2026 12:41:04 -0400 Subject: [PATCH 2/2] identifyAndTokenize: test uncontested nonOverridingMatchers write, add README example --- README.md | 6 +++++- lib/core/identify-tokenize.test.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7aead90..e60029f7 100644 --- a/README.md +++ b/README.md @@ -1227,7 +1227,11 @@ import { identifyAndTokenize } from "@optable/web-sdk/lib/dist/core/identify-tok window.optable.identifyAndTokenize = (id) => identifyAndTokenize(sdk, id, { isControlGroup: () => isControlGroup }); ``` -`options` also carries the cache write: `cacheKey` (default `OPTABLE_RESOLVED`), and `maxUidsPerEid` and `nonOverridingMatchers`, both forwarded to [`mergeCache`](lib/core/eid-cache.md). Pass `nonOverridingMatchers` when tokenize runs alongside a targeting call that skipped the first-party matcher, so a first-party EID does not displace a third-party one already held on the same source. +`options` also controls the cache write: `cacheKey` (default `OPTABLE_RESOLVED`), and `maxUidsPerEid` and `nonOverridingMatchers`, both forwarded to [`mergeCache`](lib/core/eid-cache.md). Pass `nonOverridingMatchers` when tokenize runs alongside a targeting call that skipped the first-party matcher, so a first-party EID does not displace a third-party one already held on the same source: + +```javascript +identifyAndTokenize(sdk, id, { isControlGroup: () => isControlGroup, nonOverridingMatchers: ["mysite.com"] }); +``` The returned `{ merged, staleUid2s }` (null when skipped) lets the caller chain UID2 refreshes on the merged cache. diff --git a/lib/core/identify-tokenize.test.ts b/lib/core/identify-tokenize.test.ts index 3b3e97ca..5364b906 100644 --- a/lib/core/identify-tokenize.test.ts +++ b/lib/core/identify-tokenize.test.ts @@ -170,6 +170,26 @@ describe("identifyAndTokenize - failure paths and options", () => { expect(cachedEids().map((e: any) => e.uids[0].id)).toEqual(["third-party"]); }); + it("still caches a nonOverridingMatchers EID on a source nothing holds", async () => { + localStorage.setItem( + "OPTABLE_RESOLVED", + JSON.stringify({ + ortb2: { user: { eids: [{ source: "id5-sync.com", matcher: "3p.example", uids: [{ id: "third-party" }] }] } }, + }) + ); + const { sdk, tokenize } = makeSdk(); + tokenize.mockResolvedValue({ + user: { eids: [{ source: "liveramp.com", matcher: "acme.com", uids: [{ id: "first-party" }] }] }, + } as any); + + await identifyAndTokenize(sdk, "abc123", { nonOverridingMatchers: ["acme.com"] }); + + expect(cachedEids().map((e: any) => [e.source, e.uids[0].id])).toEqual([ + ["id5-sync.com", "third-party"], + ["liveramp.com", "first-party"], + ]); + }); + it("returns the stale UID2s the merge found, for the refresh loop to chain on", async () => { localStorage.setItem( "OPTABLE_RESOLVED",