From d91d4926339681af9f55d649f455b0fbe5a1ad64 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Fri, 9 Oct 2026 13:30:42 -0400 Subject: [PATCH] feat(analytics): report LiveIntent EIDs in V2 prebid analytics V1 reports which LiveIntent-provided EIDs reached each bidder; V2 did not. Match on the full EID list before the dedupe by source, the same input V1 uses, so a same-source Optable EID can't hide a LiveIntent one. --- lib/addons/prebid/analytics.md | 3 +- lib/addons/prebid/analytics.test.ts | 69 +++++++++++++++++++++++++++++ lib/addons/prebid/analytics.ts | 9 ++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index 6a21c839..4023faa8 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -128,6 +128,7 @@ Returns the `OptablePrebidAnalytics` instance, or `null` when no Prebid instance Each sampled auction sends an `optable.prebid.auction` event with, among others: `bidderRequests` (with per-bid `status`, `cpm`, `size`, `splitTestAssignment`), -`optableMatchers`, `optableSources`, `optableTargetingDone`, `bidWon`, `missed`, +`optableMatchers`, `optableSources`, `optableTargetingDone`, `liveintentEIDs` +(with per-bidder `hasLiEids` and `liSources`), `bidWon`, `missed`, `url`, `tenant`, `prebidjsVersion`, `sessionDepth`, `pageAuctionsCount`, `originSlug`, and the parsed `userAgent`/`device`. diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index cd457c41..64bba2c6 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -362,6 +362,75 @@ describe("OptablePrebidAnalytics", () => { expect(result.optableTargetingDone).toBe("1"); }); + + const liveintentAuction = (bidderRequests: any[]) => ({ + auctionId: "auction-liveintent", + bidderRequests: bidderRequests.map((user, i) => ({ + bidderCode: `bidder${i + 1}`, + bidderRequestId: `req-${i + 1}`, + ortb2: { site: { domain: "example.com" }, user }, + bids: [], + })), + bidsReceived: [], + noBids: [], + timeoutBids: [], + }); + + it("reports LiveIntent EIDs per bidder and at the top level", async () => { + const result = await analytics.toWitness( + liveintentAuction([ + { + eids: [ + { source: "uidapi.com", uids: [{ id: "a", ext: { provider: "liveintent.com" } }] }, + { source: "openx.net", uids: [{ id: "b", ext: { provider: "liveintent.com" } }] }, + ], + }, + { eids: [{ source: "uidapi.com", uids: [{ id: "a", ext: { provider: "liveintent.com" } }] }] }, + ]), + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ hasLiEids: true, liSources: ["uidapi.com", "openx.net"] }); + expect(result.bidderRequests[1]).toMatchObject({ hasLiEids: true, liSources: ["uidapi.com"] }); + expect(result.liveintentEIDs).toEqual(["uidapi.com", "openx.net"]); + }); + + it("reports no LiveIntent EIDs when none carry the LiveIntent provider", async () => { + const result = await analytics.toWitness( + liveintentAuction([ + { eids: [{ source: "adsrvr.org", uids: [{ id: "a", ext: { provider: "example.com" } }] }] }, + {}, + ]), + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ hasLiEids: false, liSources: [] }); + expect(result.bidderRequests[1]).toMatchObject({ hasLiEids: false, liSources: [] }); + expect(result.liveintentEIDs).toEqual([]); + }); + + it("reports LiveIntent and Optable EIDs side by side, before the source dedupe", async () => { + const result = await analytics.toWitness( + liveintentAuction([ + { + ext: { eids: [{ source: "uidapi.com", uids: [{ id: "li", ext: { provider: "liveintent.com" } }] }] }, + eids: [ + { inserter: "optable.co", matcher: "uid2", source: "uidapi.com", uids: [{ id: "o" }] }, + { inserter: "optable.co", matcher: "id5", source: "id5-sync.com", uids: [{ id: "o2" }] }, + ], + }, + ]), + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ + optableSources: ["uidapi.com", "id5-sync.com"], + hasLiEids: true, + liSources: ["uidapi.com"], + }); + expect(result.optableSources).toEqual(["uidapi.com", "id5-sync.com"]); + expect(result.liveintentEIDs).toEqual(["uidapi.com"]); + }); }); describe("trackAuctionEnd", () => { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index bec0197c..490e3bd1 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -383,6 +383,7 @@ class OptablePrebidAnalytics { const oMatchersSet = new Set(); const oSourcesSet = new Set(); + const lSourcesSet = new Set(); let adUnitCode: string = "unknown"; let totalBids = 0; let device = null; @@ -400,6 +401,10 @@ class OptablePrebidAnalytics { 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")); + const liSources = [...new Set(liveintentEIDS.map((e: any) => e.source).filter(Boolean))]; + device = br.ortb2.device; return { @@ -409,6 +414,8 @@ class OptablePrebidAnalytics { optableTargetingDone: optableEIDS.length > 0, optableMatchers, optableSources, + hasLiEids: liveintentEIDS.length > 0, + liSources, status: STATUS.REQUESTED, bids: bids.map( (b: { @@ -465,6 +472,7 @@ class OptablePrebidAnalytics { bidderRequests: requests.map((br: any) => { br.optableMatchers.forEach((m: unknown) => oMatchersSet.add(m)); br.optableSources.forEach((s: unknown) => oSourcesSet.add(s)); + br.liSources.forEach((s: unknown) => lSourcesSet.add(s)); return br; }), @@ -478,6 +486,7 @@ class OptablePrebidAnalytics { optableTargetingDone: oMatchersSet.size > 0 || oSourcesSet.size > 0 ? "1" : "0", optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), + liveintentEIDs: Array.from(lSourcesSet), bidWon: bidWonEvents.map((e) => ({ bidderCode: e.bidderCode, adUnitCode: e.adUnitCode,