Skip to content
Closed
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
4 changes: 3 additions & 1 deletion lib/addons/prebid/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const analytics = initPrebidAnalytics({
### Custom payload fields

Set `window.optable.customAnalytics` to an async function returning an object; its
keys are merged into every auction payload:
keys are merged into every auction payload. It is called once per sampled auction, at
`auctionEnd`, so its result describes the page when that auction ended rather than when
the payload is sent:

```js
window.optable.customAnalytics = async () => ({ experiment: "floor-v2" });
Expand Down
20 changes: 20 additions & 0 deletions lib/addons/prebid/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,26 @@ describe("OptablePrebidAnalytics", () => {
analytics = new OptablePrebidAnalytics(mockOptableInstance);
});

it("should capture customAnalytics at auctionEnd, not when the payload is sent", async () => {
jest.useFakeTimers();
let origin = "game.example.com";
(window as any).optable = { customAnalytics: jest.fn(async () => ({ optableOrigin: origin })) };
analytics = new OptablePrebidAnalytics(mockOptableInstance, { analytics: true });

analytics.trackAuctionEnd({ auctionId: "auction-custom-timing", bidderRequests: [] });
origin = "later.example.com";
await jest.runAllTimersAsync();

expect(window.optable.customAnalytics).toHaveBeenCalledTimes(1);
expect(mockOptableInstance.witness).toHaveBeenCalledWith(
"optable.prebid.auction",
expect.objectContaining({ optableOrigin: "game.example.com" })
);

jest.useRealTimers();
delete (window as any).optable;
});

it("should handle bidsReceived and update bid status", async () => {
const event = {
auctionId: "auction-with-bids",
Expand Down
32 changes: 24 additions & 8 deletions lib/addons/prebid/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface AuctionItem {
bidWonEvents: any[];
timeoutBids: any[];
sampled: boolean;
customData: Promise<Record<string, any>> | undefined;
}

class OptablePrebidAnalytics {
Expand Down Expand Up @@ -102,7 +103,7 @@ class OptablePrebidAnalytics {
clearTimeout(auction.auctionEndTimeoutId);
this.auctions.delete(auctionId);

this.toWitness(auction.auctionEnd, auction.bidWonEvents, auction.missed).then((payload) => {
this.toWitness(auction.auctionEnd, auction.bidWonEvents, auction.missed, auction.customData).then((payload) => {
payload["auctionEndAt"] = auction.createdAt.toISOString();
payload["bidWonAt"] =
auction.bidWonEvents.length > 0
Expand Down Expand Up @@ -273,6 +274,8 @@ class OptablePrebidAnalytics {
* Process a Prebid `auctionEnd` event: record the raw event and schedule a
* delayed Witness API call, giving `bidWon` events a window to arrive. The
* payload is built from the raw event by `toWitness` when that timer fires.
* `window.optable.customAnalytics` is called here rather than when the timer
* fires, so its fields describe the page at the time of this auction.
* @param event - The raw Prebid auctionEnd event object.
* @param missed - True when the event was previously emitted (missed replay).
* @returns void
Expand All @@ -281,6 +284,7 @@ class OptablePrebidAnalytics {
const { auctionId, bidderRequests = [] } = event;
const timeoutBids = this.pendingTimeoutBids.get(auctionId) || [];
const sampled = !!this.config.analytics && this.shouldSample();
const customData = sampled ? (window as any).optable?.customAnalytics?.() : undefined;

this.log(`Processing auction ${auctionId} with ${bidderRequests.length} bidder requests`);

Expand All @@ -296,7 +300,12 @@ class OptablePrebidAnalytics {
return;
}
const effectiveMissed = storedAuction.missed;
const payload = await this.toWitness(event, storedAuction.bidWonEvents, effectiveMissed);
const payload = await this.toWitness(
event,
storedAuction.bidWonEvents,
effectiveMissed,
storedAuction.customData
);
payload["auctionEndAt"] = createdAt.toISOString();
payload["bidWonAt"] =
storedAuction.bidWonEvents.length > 0
Expand All @@ -316,6 +325,7 @@ class OptablePrebidAnalytics {
bidWonEvents: [],
timeoutBids,
sampled,
customData,
});
this.pendingTimeoutBids.delete(auctionId);

Expand Down Expand Up @@ -375,9 +385,15 @@ class OptablePrebidAnalytics {
* @param auctionEndEvent - The `auctionEnd` event object from Prebid.js.
* @param bidWonEvents - Array of `bidWon` events accumulated during the auction window.
* @param missed - True when the original events were already emitted (replayed).
* @param customData - Result of `customAnalytics` captured at auctionEnd; called now when omitted.
* @returns A payload object compatible with the Witness API.
*/
async toWitness(auctionEndEvent: any, bidWonEvents: any[], missed = false): Promise<Record<string, any>> {
async toWitness(
auctionEndEvent: any,
bidWonEvents: any[],
missed = false,
customData?: Promise<Record<string, any>>
): Promise<Record<string, any>> {
const { auctionId, bidderRequests = [], bidsReceived = [], noBids = [] } = auctionEndEvent;
const timeoutBids = this.auctions.get(auctionId)?.timeoutBids || [];

Expand Down Expand Up @@ -510,11 +526,11 @@ class OptablePrebidAnalytics {
`Auction ${auctionId} processed: ${bidderRequests.length} requests, ${totalBids} total bids, ${bidsReceived.length} received, ${noBids.length} no-bids, ${timeoutBids.length} timeouts`
);

if ((window as any).optable?.customAnalytics) {
await (window as any).optable.customAnalytics().then((response: any) => {
this.log(`Adding custom data to payload ${JSON.stringify(response)}`);
Object.assign(witnessData, response);
});
const custom = customData ?? (window as any).optable?.customAnalytics?.();
if (custom) {
const response = await custom;
this.log(`Adding custom data to payload ${JSON.stringify(response)}`);
Object.assign(witnessData, response);
}

return witnessData;
Expand Down