diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index d0ee92f..cd457c4 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -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, @@ -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 () => { @@ -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); diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index ceb4838..329e47f 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -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. @@ -172,11 +175,6 @@ class OptablePrebidAnalytics { return { disabled: true, eventName, properties }; } - if (!this.shouldSample()) { - 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);