From ad7cfd01ed4e3b6ab988f888a11dc48bf0438b06 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Wed, 14 Oct 2026 20:59:06 -0400 Subject: [PATCH 1/2] fix(analytics): stop dropping Optable EIDs in the V2 source dedupe The dedupe kept the last EID per source, so a later non-Optable EID with the same source hid an Optable one and optableSources, optableMatchers and optableTargetingDone under-reported. The dedupe only fed the Optable filter, whose outputs are already distinct, so filter the full EID list like V1. --- lib/addons/prebid/analytics.test.ts | 37 ++++++++++++++++++++++++++++- lib/addons/prebid/analytics.ts | 8 +++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 64bba2c6..78f7d4ff 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -409,7 +409,7 @@ describe("OptablePrebidAnalytics", () => { expect(result.liveintentEIDs).toEqual([]); }); - it("reports LiveIntent and Optable EIDs side by side, before the source dedupe", async () => { + it("reports LiveIntent and Optable EIDs side by side", async () => { const result = await analytics.toWitness( liveintentAuction([ { @@ -431,6 +431,41 @@ describe("OptablePrebidAnalytics", () => { expect(result.optableSources).toEqual(["uidapi.com", "id5-sync.com"]); expect(result.liveintentEIDs).toEqual(["uidapi.com"]); }); + + it("keeps an Optable EID when a later non-Optable EID shares its source", async () => { + const result = await analytics.toWitness( + { + auctionId: "auction-shared-source", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + ext: { eids: [{ inserter: "optable.co", matcher: "uid2", source: "uidapi.com" }] }, + eids: [{ source: "uidapi.com", uids: [{ id: "li", ext: { provider: "liveintent.com" } }] }], + }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }, + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ + optableTargetingDone: true, + optableMatchers: ["uid2"], + optableSources: ["uidapi.com"], + }); + expect(result.optableMatchers).toEqual(["uid2"]); + expect(result.optableSources).toEqual(["uidapi.com"]); + expect(result.optableTargetingDone).toBe("1"); + }); }); describe("trackAuctionEnd", () => { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 490e3bd1..8f2e7547 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -392,17 +392,15 @@ class OptablePrebidAnalytics { const requests = bidderRequests.map((br: any) => { const { bidderCode, bidderRequestId, bids = [] } = br; const domain = br.ortb2.site?.domain ?? "unknown"; - const allEids = [...(br.ortb2.user?.ext?.eids ?? []), ...(br.ortb2.user?.eids ?? [])]; - // Deduplicate EIDs by source - const eids = Array.from(new Map(allEids.map((eid: any) => [eid.source, eid])).values()); + const eids = [...(br.ortb2.user?.ext?.eids ?? []), ...(br.ortb2.user?.eids ?? [])]; // Optable EIDs const optableEIDS = eids.filter((e: { inserter: string }) => e.inserter === "optable.co"); const optableMatchers = [...new Set(optableEIDS.map((e: any) => e.matcher).filter(Boolean))]; const optableSources = [...new Set(optableEIDS.map((e: any) => e.source).filter(Boolean))]; - // LiveIntent EIDs, matched before the dedupe so a same-source EID can't hide them - const liveintentEIDS = allEids.filter((e: any) => e.uids?.some((u: any) => u.ext?.provider === "liveintent.com")); + // LiveIntent EIDs + const liveintentEIDS = eids.filter((e: any) => e.uids?.some((u: any) => u.ext?.provider === "liveintent.com")); const liSources = [...new Set(liveintentEIDS.map((e: any) => e.source).filter(Boolean))]; device = br.ortb2.device; From 84f839dc1c40e09fa196bec4f5fa6c6f01203f34 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Wed, 14 Oct 2026 21:06:54 -0400 Subject: [PATCH 2/2] fix(analytics): dedupe V2 EIDs by source and inserter Dropping the dedupe broke its intended job of collapsing the same Optable EID copied into both user.ext.eids and user.eids. Keying on source and inserter keeps that while stopping another inserter's same-source EID from hiding Optable's. --- lib/addons/prebid/analytics.test.ts | 2 +- lib/addons/prebid/analytics.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 78f7d4ff..09f8c63a 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -409,7 +409,7 @@ describe("OptablePrebidAnalytics", () => { expect(result.liveintentEIDs).toEqual([]); }); - it("reports LiveIntent and Optable EIDs side by side", async () => { + it("reports LiveIntent and Optable EIDs side by side, before the source dedupe", async () => { const result = await analytics.toWitness( liveintentAuction([ { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 8f2e7547..97f1b86f 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -392,15 +392,17 @@ class OptablePrebidAnalytics { const requests = bidderRequests.map((br: any) => { const { bidderCode, bidderRequestId, bids = [] } = br; const domain = br.ortb2.site?.domain ?? "unknown"; - const eids = [...(br.ortb2.user?.ext?.eids ?? []), ...(br.ortb2.user?.eids ?? [])]; + const allEids = [...(br.ortb2.user?.ext?.eids ?? []), ...(br.ortb2.user?.eids ?? [])]; + // Deduplicate EIDs by source and inserter, so another inserter's EID can't hide Optable's + const eids = Array.from(new Map(allEids.map((eid: any) => [`${eid.source}|${eid.inserter}`, eid])).values()); // Optable EIDs const optableEIDS = eids.filter((e: { inserter: string }) => e.inserter === "optable.co"); const optableMatchers = [...new Set(optableEIDS.map((e: any) => e.matcher).filter(Boolean))]; const optableSources = [...new Set(optableEIDS.map((e: any) => e.source).filter(Boolean))]; - // LiveIntent EIDs - const liveintentEIDS = eids.filter((e: any) => e.uids?.some((u: any) => u.ext?.provider === "liveintent.com")); + // LiveIntent EIDs, matched before the dedupe so a same-source EID can't hide them + const liveintentEIDS = allEids.filter((e: any) => e.uids?.some((u: any) => u.ext?.provider === "liveintent.com")); const liSources = [...new Set(liveintentEIDS.map((e: any) => e.source).filter(Boolean))]; device = br.ortb2.device;