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
42 changes: 39 additions & 3 deletions lib/addons/prebid/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,8 @@ describe("OptablePrebidAnalytics", () => {
expect(mockOptableInstance.witness).not.toHaveBeenCalled();
});

it("should not send when sampling returns false", async () => {
it("should not sample - trackAuctionEnd already decided", async () => {
// Sampling here as well would make the effective rate rate^2.
analytics = new OptablePrebidAnalytics(mockOptableInstance, {
analytics: true,
samplingRate: 0,
Expand All @@ -701,11 +702,11 @@ describe("OptablePrebidAnalytics", () => {
const result = await analytics.sendToWitnessAPI("test.event", { prop: "value" });

expect(result).toEqual({
disabled: true,
disabled: false,
eventName: "test.event",
properties: { prop: "value" },
});
expect(mockOptableInstance.witness).not.toHaveBeenCalled();
expect(mockOptableInstance.witness).toHaveBeenCalledWith("test.event", { prop: "value" });
});

it("should send to witness API when enabled and sampled", async () => {
Expand All @@ -719,6 +720,41 @@ describe("OptablePrebidAnalytics", () => {
expect(mockOptableInstance.witness).toHaveBeenCalledWith("test.event", { prop: "value" });
});

it("should draw once per auction at a fractional rate", async () => {
// Regression: the rate was applied at trackAuctionEnd and again on send,
// so 0.1 behaved as 0.01. The second draw here fails the rate - if it
// still gated, the auction would never reach witness.
const randomSpy = jest.spyOn(Math, "random").mockReturnValueOnce(0.3).mockReturnValue(0.9);
jest.useFakeTimers();

analytics = new OptablePrebidAnalytics(mockOptableInstance, {
analytics: true,
samplingRate: 0.5,
});

await analytics.trackAuctionEnd({
auctionId: "auction-fractional",
timeout: 3000,
bidderRequests: [
{
bidderCode: "bidder1",
bidderRequestId: "req-1",
ortb2: { site: { domain: "example.com" }, user: { eids: [] } },
bids: [],
},
],
bidsReceived: [],
noBids: [],
});
await jest.runAllTimersAsync();

expect(mockOptableInstance.witness).toHaveBeenCalledTimes(1);
expect(randomSpy).toHaveBeenCalledTimes(1);

jest.useRealTimers();
randomSpy.mockRestore();
});

it("should handle errors from witness API", async () => {
const error = new Error("Witness API error");
mockOptableInstance.witness = jest.fn().mockRejectedValue(error);
Expand Down
10 changes: 4 additions & 6 deletions lib/addons/prebid/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ class OptablePrebidAnalytics {
}

/**
* Send an event to the Witness API when analytics are enabled and sampling passes.
* Send an event to the Witness API when analytics are enabled.
*
* Does not sample: `trackAuctionEnd` already decided that once per auction.
* Sampling again here would make the effective rate rate^2.
* @param eventName - The name of the event to send (e.g. "optable.prebid.auction").
* @param properties - An object of event properties to include in the payload.
* @returns A small result object indicating whether the call was disabled or sent.
Expand All @@ -172,11 +175,6 @@ class OptablePrebidAnalytics {
return { disabled: true, eventName, properties };
}

if (!this.shouldSample()) {
Comment thread
juanli16 marked this conversation as resolved.
this.log("Event not sampled - skipping Witness API call for:", eventName, properties);
return { disabled: true, eventName, properties };
}

try {
await this.optableInstance.witness(eventName, properties);
this.log("Sending to Witness API:", eventName, properties);
Expand Down
Loading