Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions lib/addons/prebid/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`.
Expand Down
61 changes: 61 additions & 0 deletions lib/addons/prebid/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
34 changes: 30 additions & 4 deletions lib/addons/prebid/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (e: any) => 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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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: {
Expand Down Expand Up @@ -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;
}),
Expand All @@ -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,
Expand Down
33 changes: 27 additions & 6 deletions lib/addons/prototypes/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this accept a param from wrappers so we don't have to constantly update this if new ones are on a page for certain tenants. Not blocking

const sourcesOf = (eids) => [...new Set(eids.map((e) => e.source).filter(Boolean))];
class OptablePrebidAnalytics {
constructor(optableInstance, config = {}) {
if (!optableInstance || typeof optableInstance.witness !== "function") {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -335,13 +347,19 @@ 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;

auction.bidderRequests.forEach((br) => {
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]).
Expand Down Expand Up @@ -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,
Expand Down