diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 284207c..a28749e 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -684,6 +684,119 @@ describe("OptablePrebidAnalytics", () => { expect(storedAuction).toBeDefined(); }); + it("should extract splitTestAssignment from bidsReceived", async () => { + const event = { + auctionId: "auction-split-test", + timeout: 3000, + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + eids: [ + { + inserter: "optable.co", + matcher: "matcher1", + source: "source1", + }, + ], + }, + }, + bids: [ + { + bidId: "bid-1", + adUnitCode: "ad-unit-1", + adUnitId: "ad-id-1", + transactionId: "trans-1", + src: "client", + }, + ], + }, + ], + bidsReceived: [ + { + requestId: "bid-1", + cpm: 1.5, + width: 300, + height: 250, + currency: "USD", + adUnitCode: "ad-unit-1", + ortb2Imp: { + ext: { + optable: { + splitTestAssignment: "treatment", + }, + }, + }, + }, + ], + noBids: [], + timeoutBids: [], + }; + + await analytics.trackAuctionEnd(event); + + // Get the stored auction and check the bid has splitTestAssignment + const storedAuction = analytics["auctions"].get("auction-split-test"); + expect(storedAuction).toBeDefined(); + + // The splitTestAssignment should be extracted and merged in toWitness + const payload = await analytics.toWitness(storedAuction.auctionEnd, null); + expect(payload.bidderRequests[0].bids[0].splitTestAssignment).toBe("treatment"); + }); + + it("should handle missing splitTestAssignment gracefully", async () => { + const event = { + auctionId: "auction-no-split-test", + timeout: 3000, + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + eids: [], + }, + }, + bids: [ + { + bidId: "bid-1", + adUnitCode: "ad-unit-1", + adUnitId: "ad-id-1", + transactionId: "trans-1", + src: "client", + }, + ], + }, + ], + bidsReceived: [ + { + requestId: "bid-1", + cpm: 1.5, + width: 300, + height: 250, + currency: "USD", + adUnitCode: "ad-unit-1", + // No ortb2Imp field + }, + ], + noBids: [], + timeoutBids: [], + }; + + await analytics.trackAuctionEnd(event); + + // Should not throw error and splitTestAssignment should be undefined + const storedAuction = analytics["auctions"].get("auction-no-split-test"); + expect(storedAuction).toBeDefined(); + + const payload = await analytics.toWitness(storedAuction.auctionEnd, null); + expect(payload.bidderRequests[0].bids[0].splitTestAssignment).toBeUndefined(); + }); + it("should handle noBids and update status", async () => { const event = { auctionId: "auction-no-bids", diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 22a8d2c..cb9e6c5 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -244,6 +244,7 @@ class OptablePrebidAnalytics { transactionId: string; src: string; floorData?: { floorMin: number }; + ortb2Imp?: { ext?: { optable?: { splitTestAssignment?: string } } }; }) => ({ bidId: b.bidId, bidderRequestId, @@ -252,6 +253,7 @@ class OptablePrebidAnalytics { transactionId: b.transactionId, src: b.src, floorMin: b.floorData?.floorMin, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, status: STATUS.REQUESTED, }) ), @@ -291,6 +293,7 @@ class OptablePrebidAnalytics { cpm: b.cpm, size: `${b.width}x${b.height}`, currency: b.currency, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, }); } else { // Create new bid object for this response @@ -305,6 +308,7 @@ class OptablePrebidAnalytics { size: `${b.width}x${b.height}`, currency: b.currency, status: STATUS.RECEIVED, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, }; br.bids.push(bidObj); bidIndex[bidId] = bidObj; @@ -349,7 +353,7 @@ class OptablePrebidAnalytics { this.sendToWitnessAPI("optable.prebid.auction", payload); }, this.config.bidWinTimeout); - // Store the processed auction + // Store the auction data this.auctions.set(auctionId, { auctionEnd: event, createdAt, missed, auctionEndTimeoutId }); // Clean up old auctions @@ -433,6 +437,7 @@ class OptablePrebidAnalytics { let totalBids = 0; let device = null; + // Process bidder requests const requests = bidderRequests.map((br: any) => { const { bidderCode, bidderRequestId, bids = [] } = br; const domain = br.ortb2.site?.domain ?? "unknown"; @@ -461,6 +466,7 @@ class OptablePrebidAnalytics { transactionId: string; src: string; floorData?: { floorMin: number }; + ortb2Imp?: { ext?: { optable?: { splitTestAssignment?: string } } }; }) => ({ bidId: b.bidId, bidderRequestId, @@ -469,12 +475,24 @@ class OptablePrebidAnalytics { transactionId: b.transactionId, src: b.src, floorMin: b.floorData?.floorMin, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, status: STATUS.REQUESTED, }) ), }; }); + // Merge splitTestAssignment from bidsReceived into the requests + const bidsReceivedMap = new Map(bidsReceived.map((b: any) => [b.requestId, b])); + requests.forEach((request: any) => { + request.bids.forEach((bid: any) => { + const bidReceived = bidsReceivedMap.get(bid.bidId); + if (bidReceived?.ortb2Imp?.ext?.optable?.splitTestAssignment) { + bid.splitTestAssignment = bidReceived.ortb2Imp.ext.optable.splitTestAssignment; + } + }); + }); + const witnessData: WitnessProperties = { bidderRequests: requests.map((br: any) => { br.optableMatchers.forEach((m: unknown) => oMatchersSet.add(m)); diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index 91d02dd..e4cfb5e 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -141,6 +141,7 @@ class OptablePrebidAnalytics { transactionId: b.transactionId, src: b.src, floorMin: b.floorData?.floorMin, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, status: STATUS.REQUESTED, })), }; @@ -178,6 +179,7 @@ class OptablePrebidAnalytics { cpm: b.cpm, size: `${b.width}x${b.height}`, currency: b.currency, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, }); } else { // Create new bid object for this response @@ -192,6 +194,7 @@ class OptablePrebidAnalytics { size: `${b.width}x${b.height}`, currency: b.currency, status: STATUS.RECEIVED, + splitTestAssignment: b.ortb2Imp?.ext?.optable?.splitTestAssignment, }; br.bids.push(bidObj); bidIndex[bidId] = bidObj; @@ -255,6 +258,7 @@ class OptablePrebidAnalytics { cpm: b.cpm, size: b.size, bidId: b.bidId, + splitTestAssignment: b.splitTestAssignment, }; }), };