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..1989a1aa 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,67 @@ describe("OptablePrebidAnalytics", () => { expect(payload.bidderRequests[0].status).toBe("RECEIVED"); }); + const bidCountAuction = (bidsPerRequest: (string | undefined)[][], 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("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"); + }); + 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.