diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index c9ce6712..a1ee221c 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -129,10 +129,21 @@ 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`, -`adUnitCode` (first bid's ad unit), `totalBids` (received bids with a `cpm`), -`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`, `adUnitCode` (first bid's ad unit), `totalBids` (received +bids with a `cpm`), `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) | The first payload of each page also carries `page`: `{ optableRTDInstalled, optableRTDConfigured, rtdDelay, udDelay }`. diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index e0e32de5..30a2031f 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -489,6 +489,67 @@ describe("OptablePrebidAnalytics", () => { 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([]); + }); + it("keeps an Optable EID when a later non-Optable EID shares its source", async () => { const result = await analytics.toWitness( { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index f0d7dcd2..5a1b7614 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; @@ -405,6 +417,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; @@ -422,9 +437,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; @@ -435,8 +452,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: { @@ -496,6 +516,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; }), @@ -511,6 +534,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, diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index 3cff8043..53c10706 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -5,6 +5,15 @@ 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", + transunion: (e) => e.source === "neustar.biz", +}; +const sourcesOf = (eids) => [...new Set(eids.map((e) => e.source).filter(Boolean))]; class OptablePrebidAnalytics { constructor(optableInstance, config = {}) { if (!optableInstance || typeof optableInstance.witness !== "function") { @@ -198,11 +207,11 @@ 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)); + const tuSources = sourcesOf(eids.filter(COMPETITOR_EIDS.transunion)); return { bidderCode, @@ -211,8 +220,11 @@ class OptablePrebidAnalytics { hasOEids: optableEIDS.length > 0, optableMatchers, optableSources, - hasLiEids: liveintentEIDS.length > 0, + hasLiEids: liSources.length > 0, liSources, + iiqSources, + gcSources, + tuSources, status: STATUS.REQUESTED, bids: bids.map((b) => ({ bidId: b.bidId, @@ -335,6 +347,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; let totalBids = 0; @@ -342,6 +357,9 @@ 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)); + br.tuSources.forEach((s) => tSourcesSet.add(s)); }); const auctionSources = Array.from(oSourcesSet); // Per-bidder delivery as a bitmask over auctionSources (bit i = auctionSources[i]). @@ -379,6 +397,9 @@ class OptablePrebidAnalytics { optableMatchers: Array.from(oMatchersSet), optableSources: auctionSources, liveintentEIDs: Array.from(lSourcesSet), + intentiqEIDs: Array.from(iSourcesSet), + growthcodeEIDs: Array.from(gSourcesSet), + transunionEIDs: Array.from(tSourcesSet), missed: missed && oSourcesSet.size === 0, url: `${window.location.hostname}${window.location.pathname}`, tenant: this.config.tenant,