From 26bfda93190c537834ff3d2c569dcc444f6f181c Mon Sep 17 00:00:00 2001 From: mosherBT Date: Fri, 9 Oct 2026 13:59:02 -0400 Subject: [PATCH 1/2] feat(analytics): report totalBids and top-level adUnitCode in V2 prebid analytics V2 declared both but never set them, so every payload sent adUnitCode "unknown" and no totalBids. Match V1: the first bid's ad unit, and a count of received bids with a cpm. --- lib/addons/prebid/analytics.md | 1 + lib/addons/prebid/analytics.test.ts | 58 ++++++++++++++++++++++++++++- lib/addons/prebid/analytics.ts | 3 ++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index 4023faa8..a791b8ad 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -130,5 +130,6 @@ 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`. diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 09f8c63a..042a5d1c 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -194,7 +194,7 @@ describe("OptablePrebidAnalytics", () => { expect(result).toMatchObject({ auctionId: "auction-123", - adUnitCode: "unknown", + adUnitCode: "ad-unit-1", totalRequests: 1, optableMatchers: ["matcher1"], optableSources: ["source1"], @@ -1051,6 +1051,62 @@ describe("OptablePrebidAnalytics", () => { expect(payload.bidderRequests[0].status).toBe("RECEIVED"); }); + const bidCountAuction = (bidsPerRequest: string[][], bidsReceived: any[]) => ({ + auctionId: "auction-bid-count", + bidderRequests: bidsPerRequest.map((adUnits, i) => ({ + bidderCode: `bidder${i + 1}`, + bidderRequestId: `req-${i + 1}`, + ortb2: { site: { domain: "example.com" }, user: { eids: [] } }, + bids: adUnits.map((adUnitCode, j) => ({ bidId: `bid-${i + 1}-${j + 1}`, adUnitCode })), + })), + bidsReceived, + noBids: [], + timeoutBids: [], + }); + + it("reports totalBids: 0 when no bids are received", async () => { + const payload = await analytics.toWitness(bidCountAuction([["ad-unit-1"], ["ad-unit-1"]], []), []); + expect(payload.totalBids).toBe(0); + }); + + it("reports totalBids: 1 for a single received bid", async () => { + const payload = await analytics.toWitness( + bidCountAuction([["ad-unit-1"], ["ad-unit-1"]], [{ requestId: "bid-2-1", cpm: 1.2 }]), + [] + ); + expect(payload.totalBids).toBe(1); + }); + + it("counts received bids with a cpm across bidder requests", async () => { + const payload = await analytics.toWitness( + bidCountAuction( + [ + ["ad-unit-1", "ad-unit-2"], + ["ad-unit-1", "ad-unit-2"], + ], + [ + { requestId: "bid-1-1", cpm: 1.2 }, + { requestId: "bid-1-2", cpm: 0 }, + { requestId: "bid-2-1", cpm: 0.8 }, + { requestId: "bid-2-2", cpm: null }, + { requestId: "bid-unrequested", cpm: 3 }, + ] + ), + [] + ); + expect(payload.totalBids).toBe(3); + }); + + it("sets the top-level adUnitCode to the first bid's ad unit", async () => { + const payload = await analytics.toWitness(bidCountAuction([["ad-unit-1", "ad-unit-2"], ["ad-unit-3"]], []), []); + expect(payload.adUnitCode).toBe("ad-unit-1"); + }); + + it("falls back to an 'unknown' adUnitCode when there are no bids", async () => { + const payload = await analytics.toWitness(bidCountAuction([[], []], []), []); + expect(payload.adUnitCode).toBe("unknown"); + }); + it("should extract splitTestAssignment from bidsReceived", async () => { const event = { auctionId: "auction-split-test", diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 97f1b86f..31dac074 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -447,8 +447,10 @@ class OptablePrebidAnalytics { requests.forEach((request: any) => { request.bids.forEach((bid: any) => { + if (adUnitCode === "unknown" && bid.adUnitCode) adUnitCode = bid.adUnitCode; const bidReceived = bidsReceivedMap.get(bid.bidId); if (bidReceived) { + if (bidReceived.cpm != null) totalBids += 1; bid.status = STATUS.RECEIVED; bid.cpm = bidReceived.cpm; bid.size = `${bidReceived.width}x${bidReceived.height}`; @@ -479,6 +481,7 @@ class OptablePrebidAnalytics { auctionId, adUnitCode, totalRequests: bidderRequests.length, + totalBids, optableSampling: this.config.samplingRate || 1, // Processor schema declares optableTargetingDone as STRING and checks IN ('1','true'). // Send '1'/'0' so Spark reads a predictable string regardless of matcher count. From bb86c402277e9a81115f3d8c99ec847aaaa35592 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Mon, 19 Oct 2026 11:45:31 -0400 Subject: [PATCH 2/2] comments --- lib/addons/prebid/analytics.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 042a5d1c..1989a1aa 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -1051,7 +1051,7 @@ describe("OptablePrebidAnalytics", () => { expect(payload.bidderRequests[0].status).toBe("RECEIVED"); }); - const bidCountAuction = (bidsPerRequest: string[][], bidsReceived: any[]) => ({ + const bidCountAuction = (bidsPerRequest: (string | undefined)[][], bidsReceived: any[]) => ({ auctionId: "auction-bid-count", bidderRequests: bidsPerRequest.map((adUnits, i) => ({ bidderCode: `bidder${i + 1}`, @@ -1102,6 +1102,11 @@ describe("OptablePrebidAnalytics", () => { expect(payload.adUnitCode).toBe("ad-unit-1"); }); + it("skips bids without an ad unit when setting the top-level adUnitCode", async () => { + const payload = await analytics.toWitness(bidCountAuction([[undefined], [undefined, "ad-unit-2"]], []), []); + expect(payload.adUnitCode).toBe("ad-unit-2"); + }); + it("falls back to an 'unknown' adUnitCode when there are no bids", async () => { const payload = await analytics.toWitness(bidCountAuction([[], []], []), []); expect(payload.adUnitCode).toBe("unknown");