From f33187c8f8ab27d16e8b300123625e9d0e5a792c Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:27:15 -0400 Subject: [PATCH 1/3] analytics: report IntentIQ and GrowthCode EID presence, catch LiveIntent's main ID Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/addons/prototypes/analytics.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index fbdf4f1e..ababa956 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -5,6 +5,14 @@ const STATUS = { NO_BID: "NO_BID", TIMEOUT: "TIMEOUT", }; +// Competitor EIDs, matched on the tags each vendor leaves in the bid request. +// Publisher wrappers can strip inserter, so each rule also checks matcher or source. +const COMPETITOR_EIDS = { + liveintent: (e) => e.source === "liveintent.com" || e.uids?.some((u) => u.ext?.provider === "liveintent.com"), + intentiq: (e) => e.matcher === "intentiq.com" || e.source === "intentiq.com", + growthcode: (e) => e.inserter === "growthcode.io" || e.matcher === "growthcode.io" || e.source === "growthcode.io", +}; +const sourcesOf = (eids) => [...new Set(eids.map((e) => e.source).filter(Boolean))]; class OptablePrebidAnalytics { constructor(optableInstance, config = {}) { if (!optableInstance || typeof optableInstance.witness !== "function") { @@ -121,11 +129,10 @@ class OptablePrebidAnalytics { const optableMatchers = [...new Set(optableEIDS.map((e) => e.matcher).filter(Boolean))]; const optableSources = [...new Set(optableEIDS.map((e) => e.source).filter(Boolean))]; - // LiveIntent EIDs - const liveintentEIDS = eids - .filter((e) => e.uids?.some((u) => u.ext?.provider === "liveintent.com")) - .map((e) => ({ ...e, uids: e.uids.filter((u) => u.ext?.provider === "liveintent.com") })); - const liSources = [...new Set(liveintentEIDS.map((e) => e.source).filter(Boolean))]; + // Competitor EIDs + const liSources = sourcesOf(eids.filter(COMPETITOR_EIDS.liveintent)); + const iiqSources = sourcesOf(eids.filter(COMPETITOR_EIDS.intentiq)); + const gcSources = sourcesOf(eids.filter(COMPETITOR_EIDS.growthcode)); return { bidderCode, @@ -134,8 +141,10 @@ class OptablePrebidAnalytics { hasOEids: optableEIDS.length > 0, optableMatchers, optableSources, - hasLiEids: liveintentEIDS.length > 0, + hasLiEids: liSources.length > 0, liSources, + iiqSources, + gcSources, status: STATUS.REQUESTED, bids: bids.map((b) => ({ bidId: b.bidId, @@ -244,6 +253,8 @@ class OptablePrebidAnalytics { const oMatchersSet = new Set(); const oSourcesSet = new Set(); const lSourcesSet = new Set(); + const iSourcesSet = new Set(); + const gSourcesSet = new Set(); let adUnitCode; let totalBids = 0; @@ -252,6 +263,8 @@ class OptablePrebidAnalytics { br.optableMatchers.forEach((m) => oMatchersSet.add(m)); br.optableSources.forEach((s) => oSourcesSet.add(s)); br.liSources.forEach((s) => lSourcesSet.add(s)); + br.iiqSources.forEach((s) => iSourcesSet.add(s)); + br.gcSources.forEach((s) => gSourcesSet.add(s)); return { bidderCode: br.bidderCode, bids: br.bids.map((b) => { @@ -274,6 +287,8 @@ class OptablePrebidAnalytics { optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), liveintentEIDs: Array.from(lSourcesSet), + intentiqEIDs: Array.from(iSourcesSet), + growthcodeEIDs: Array.from(gSourcesSet), missed, url: `${window.location.hostname}${window.location.pathname}`, tenant: this.config.tenant, From f0b18d3f5104845293741e94f865f05304e052b4 Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:05:38 -0400 Subject: [PATCH 2/3] analytics: report TransUnion Fabrick ID presence Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/addons/prototypes/analytics.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index ababa956..bd0043a6 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -11,6 +11,7 @@ const COMPETITOR_EIDS = { liveintent: (e) => e.source === "liveintent.com" || e.uids?.some((u) => u.ext?.provider === "liveintent.com"), intentiq: (e) => e.matcher === "intentiq.com" || e.source === "intentiq.com", growthcode: (e) => e.inserter === "growthcode.io" || e.matcher === "growthcode.io" || e.source === "growthcode.io", + transunion: (e) => e.source === "neustar.biz", }; const sourcesOf = (eids) => [...new Set(eids.map((e) => e.source).filter(Boolean))]; class OptablePrebidAnalytics { @@ -133,6 +134,7 @@ class OptablePrebidAnalytics { const liSources = sourcesOf(eids.filter(COMPETITOR_EIDS.liveintent)); const iiqSources = sourcesOf(eids.filter(COMPETITOR_EIDS.intentiq)); const gcSources = sourcesOf(eids.filter(COMPETITOR_EIDS.growthcode)); + const tuSources = sourcesOf(eids.filter(COMPETITOR_EIDS.transunion)); return { bidderCode, @@ -145,6 +147,7 @@ class OptablePrebidAnalytics { liSources, iiqSources, gcSources, + tuSources, status: STATUS.REQUESTED, bids: bids.map((b) => ({ bidId: b.bidId, @@ -255,6 +258,7 @@ class OptablePrebidAnalytics { const lSourcesSet = new Set(); const iSourcesSet = new Set(); const gSourcesSet = new Set(); + const tSourcesSet = new Set(); let adUnitCode; let totalBids = 0; @@ -265,6 +269,7 @@ class OptablePrebidAnalytics { br.liSources.forEach((s) => lSourcesSet.add(s)); br.iiqSources.forEach((s) => iSourcesSet.add(s)); br.gcSources.forEach((s) => gSourcesSet.add(s)); + br.tuSources.forEach((s) => tSourcesSet.add(s)); return { bidderCode: br.bidderCode, bids: br.bids.map((b) => { @@ -289,6 +294,7 @@ class OptablePrebidAnalytics { liveintentEIDs: Array.from(lSourcesSet), intentiqEIDs: Array.from(iSourcesSet), growthcodeEIDs: Array.from(gSourcesSet), + transunionEIDs: Array.from(tSourcesSet), missed, url: `${window.location.hostname}${window.location.pathname}`, tenant: this.config.tenant, From 3e2f351f876b4c2594373d2b473358dcfd1973c0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Thu, 15 Oct 2026 13:04:30 -0400 Subject: [PATCH 3/3] analytics: report competitor EIDs in V2 prebid analytics Port the COMPETITOR_EIDS rules from the V1 add-on to V2: per-bidder iiqSources, gcSources and tuSources, and auction-level intentiqEIDs, growthcodeEIDs and transunionEIDs. LiveIntent detection in V2 now also matches source === "liveintent.com", so its main ID is no longer missed. Rules run on the EIDs before the source dedupe, like the existing V2 LiveIntent check. --- lib/addons/prebid/analytics.md | 17 ++++++-- lib/addons/prebid/analytics.test.ts | 61 +++++++++++++++++++++++++++++ lib/addons/prebid/analytics.ts | 34 ++++++++++++++-- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index 4023faa8..afe497e9 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -129,6 +129,17 @@ 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`, `liveintentEIDs` -(with per-bidder `hasLiEids` and `liSources`), `bidWon`, `missed`, -`url`, `tenant`, `prebidjsVersion`, `sessionDepth`, `pageAuctionsCount`, -`originSlug`, and the parsed `userAgent`/`device`. +(with per-bidder `hasLiEids` and `liSources`), `intentiqEIDs`, `growthcodeEIDs`, +`transunionEIDs` (with per-bidder `iiqSources`, `gcSources`, `tuSources`), +`bidWon`, `missed`, `url`, `tenant`, `prebidjsVersion`, `sessionDepth`, +`pageAuctionsCount`, `originSlug`, and the parsed `userAgent`/`device`. + +Competitor EIDs are the EID sources each vendor delivered, matched on the tags it +leaves in the bid request (rules in `COMPETITOR_EIDS`, shared with the V1 add-on): + +| Vendor | Rule | +| ---------- | --------------------------------------------------------------------------- | +| LiveIntent | `source === "liveintent.com"` or `uids[].ext.provider === "liveintent.com"` | +| IntentIQ | `matcher` or `source` is `"intentiq.com"` | +| GrowthCode | `inserter`, `matcher` or `source` is `"growthcode.io"` | +| TransUnion | `source === "neustar.biz"` (Fabrick ID) | diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 64bba2c6..50ee9774 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -431,6 +431,67 @@ describe("OptablePrebidAnalytics", () => { expect(result.optableSources).toEqual(["uidapi.com", "id5-sync.com"]); expect(result.liveintentEIDs).toEqual(["uidapi.com"]); }); + + it("catches LiveIntent's main ID by source when it carries no ext.provider", async () => { + const result = await analytics.toWitness( + liveintentAuction([{ eids: [{ source: "liveintent.com", uids: [{ id: "li" }] }] }]), + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ hasLiEids: true, liSources: ["liveintent.com"] }); + expect(result.liveintentEIDs).toEqual(["liveintent.com"]); + }); + + it("reports IntentIQ, GrowthCode and TransUnion EIDs per bidder and at the top level", async () => { + const result = await analytics.toWitness( + liveintentAuction([ + { + eids: [ + { inserter: "freestar.com", matcher: "intentiq.com", source: "adsrvr.org", uids: [{ id: "a" }] }, + { source: "intentiq.com", uids: [{ id: "b" }] }, + { inserter: "growthcode.io", source: "acxiom.com", uids: [{ id: "c" }] }, + { source: "neustar.biz", uids: [{ id: "d" }] }, + ], + }, + { + eids: [ + { matcher: "growthcode.io", source: "id5-sync.com", uids: [{ id: "e" }] }, + { source: "growthcode.io", uids: [{ id: "f" }] }, + ], + }, + ]), + [] + ); + + expect(result.bidderRequests[0]).toMatchObject({ + iiqSources: ["adsrvr.org", "intentiq.com"], + gcSources: ["acxiom.com"], + tuSources: ["neustar.biz"], + hasLiEids: false, + }); + expect(result.bidderRequests[1]).toMatchObject({ + iiqSources: [], + gcSources: ["id5-sync.com", "growthcode.io"], + tuSources: [], + }); + expect(result.intentiqEIDs).toEqual(["adsrvr.org", "intentiq.com"]); + expect(result.growthcodeEIDs).toEqual(["acxiom.com", "id5-sync.com", "growthcode.io"]); + expect(result.transunionEIDs).toEqual(["neustar.biz"]); + }); + + it("does not count Optable's own EIDs as a competitor's", async () => { + const result = await analytics.toWitness( + liveintentAuction([ + { eids: [{ inserter: "optable.co", matcher: "uid2", source: "uidapi.com", uids: [{ id: "o" }] }] }, + ]), + [] + ); + + expect(result.liveintentEIDs).toEqual([]); + expect(result.intentiqEIDs).toEqual([]); + expect(result.growthcodeEIDs).toEqual([]); + expect(result.transunionEIDs).toEqual([]); + }); }); describe("trackAuctionEnd", () => { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 490e3bd1..24ffc8c3 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -8,6 +8,18 @@ import * as Bowser from "bowser"; declare const SDK_WRAPPER_VERSION: string; +// Competitor EIDs, matched on the tags each vendor leaves in the bid request. +// Publisher wrappers can strip inserter, so each rule also checks matcher or source. +// Same rules as the V1 add-on (lib/addons/prototypes/analytics.js). +const COMPETITOR_EIDS: Record boolean> = { + liveintent: (e) => + e.source === "liveintent.com" || e.uids?.some((u: any) => u.ext?.provider === "liveintent.com") === true, + intentiq: (e) => e.matcher === "intentiq.com" || e.source === "intentiq.com", + growthcode: (e) => e.inserter === "growthcode.io" || e.matcher === "growthcode.io" || e.source === "growthcode.io", + transunion: (e) => e.source === "neustar.biz", +}; +const sourcesOf = (eids: any[]): string[] => [...new Set(eids.map((e) => e.source).filter(Boolean))] as string[]; + declare global { interface Window { pbjs?: any; @@ -384,6 +396,9 @@ class OptablePrebidAnalytics { const oMatchersSet = new Set(); const oSourcesSet = new Set(); const lSourcesSet = new Set(); + const iSourcesSet = new Set(); + const gSourcesSet = new Set(); + const tSourcesSet = new Set(); let adUnitCode: string = "unknown"; let totalBids = 0; let device = null; @@ -401,9 +416,11 @@ 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))]; + // Competitor EIDs, matched before the dedupe so a same-source EID can't hide them + const liSources = sourcesOf(allEids.filter(COMPETITOR_EIDS.liveintent)); + const iiqSources = sourcesOf(allEids.filter(COMPETITOR_EIDS.intentiq)); + const gcSources = sourcesOf(allEids.filter(COMPETITOR_EIDS.growthcode)); + const tuSources = sourcesOf(allEids.filter(COMPETITOR_EIDS.transunion)); device = br.ortb2.device; @@ -414,8 +431,11 @@ class OptablePrebidAnalytics { optableTargetingDone: optableEIDS.length > 0, optableMatchers, optableSources, - hasLiEids: liveintentEIDS.length > 0, + hasLiEids: liSources.length > 0, liSources, + iiqSources, + gcSources, + tuSources, status: STATUS.REQUESTED, bids: bids.map( (b: { @@ -473,6 +493,9 @@ class OptablePrebidAnalytics { br.optableMatchers.forEach((m: unknown) => oMatchersSet.add(m)); br.optableSources.forEach((s: unknown) => oSourcesSet.add(s)); br.liSources.forEach((s: unknown) => lSourcesSet.add(s)); + br.iiqSources.forEach((s: unknown) => iSourcesSet.add(s)); + br.gcSources.forEach((s: unknown) => gSourcesSet.add(s)); + br.tuSources.forEach((s: unknown) => tSourcesSet.add(s)); return br; }), @@ -487,6 +510,9 @@ class OptablePrebidAnalytics { optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), liveintentEIDs: Array.from(lSourcesSet), + intentiqEIDs: Array.from(iSourcesSet), + growthcodeEIDs: Array.from(gSourcesSet), + transunionEIDs: Array.from(tSourcesSet), bidWon: bidWonEvents.map((e) => ({ bidderCode: e.bidderCode, adUnitCode: e.adUnitCode,