From 258ec0f69ff0c532c3cf092a3d443b332d7fc073 Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:55:59 -0400 Subject: [PATCH 1/3] analytics: auction with Optable sources is not missed A replayed auction that already carries Optable EIDs read the cache before the hook attached, so it was not truly missed. missed and optableLoaded now reflect that. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/addons/prebid/analytics.test.ts | 45 +++++++++++++++++++++++++++++ lib/addons/prebid/analytics.ts | 6 ++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index cd457c41..fa5c7978 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -1132,6 +1132,51 @@ describe("OptablePrebidAnalytics", () => { analytics = new OptablePrebidAnalytics(mockOptableInstance); }); + it("should not mark a replayed auction as missed when Optable sources are present", async () => { + const auctionEndEvent = { + auctionId: "auction-replayed-with-cache", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + eids: [{ inserter: "optable.co", matcher: "matcher1", source: "source1" }], + }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + }; + + const result = await analytics.toWitness(auctionEndEvent, [], true); + + expect(result.missed).toBe(false); + }); + + it("should keep a replayed auction as missed when no Optable sources are present", async () => { + const auctionEndEvent = { + auctionId: "auction-replayed-no-cache", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { site: { domain: "example.com" } }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + }; + + const result = await analytics.toWitness(auctionEndEvent, [], true); + + expect(result.missed).toBe(true); + }); + it("should handle multiple bidder requests", async () => { const auctionEndEvent = { auctionId: "auction-multi", diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index bec0197c..96825ed8 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -108,7 +108,7 @@ class OptablePrebidAnalytics { auction.bidWonEvents.length > 0 ? new Date(Math.min(...auction.bidWonEvents.map((e: any) => e._receivedAt.getTime()))).toISOString() : null; - payload["optableLoaded"] = !auction.missed; + payload["optableLoaded"] = !payload.missed; navigator.sendBeacon( witnessUrl, @@ -302,7 +302,7 @@ class OptablePrebidAnalytics { storedAuction.bidWonEvents.length > 0 ? new Date(Math.min(...storedAuction.bidWonEvents.map((e: any) => e._receivedAt.getTime()))).toISOString() : null; - payload["optableLoaded"] = !effectiveMissed; + payload["optableLoaded"] = !payload.missed; this.sendToWitnessAPI("optable.prebid.auction", payload); this.auctions.delete(auctionId); }, this.config.bidWinTimeout); @@ -483,7 +483,7 @@ class OptablePrebidAnalytics { adUnitCode: e.adUnitCode, cpm: e.cpm, })), - missed, + missed: missed && oSourcesSet.size === 0, url: `${window.location.hostname}${window.location.pathname}`, tenant: this.optableInstance?.dcn?.node ?? "unknown", // eslint-disable-next-line no-undef From bbd40a147b1abe9451992eac86badfbc739c1422 Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:34:50 -0400 Subject: [PATCH 2/3] analytics: keep optableLoaded tied to hook timing A replayed auction with Optable sources read the cache, so it is not missed, but the SDK still was not hooked in when the auction started. optableLoaded goes back to reflecting hook timing only. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/addons/prebid/analytics.test.ts | 34 +++++++++++++++++++++++++++++ lib/addons/prebid/analytics.ts | 4 ++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index fa5c7978..a827c1cc 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -543,6 +543,40 @@ describe("OptablePrebidAnalytics", () => { ); }); + it("should report not missed but not loaded when a replayed auction carries Optable sources", async () => { + const auctionEndEvent = { + auctionId: "auction-replayed-cache-read", + timeout: 3000, + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + eids: [{ inserter: "optable.co", matcher: "matcher1", source: "source1" }], + }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }; + + await analytics.trackAuctionEnd(auctionEndEvent, true); + await jest.runAllTimersAsync(); + + expect(mockOptableInstance.witness).toHaveBeenCalledWith( + "optable.prebid.auction", + expect.objectContaining({ + missed: false, + optableLoaded: false, + }) + ); + }); + it("should accumulate multiple bidWon events and emit one witness with an array", async () => { const auctionId = "auction-multi-unit"; const auctionEndEvent = { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 96825ed8..cd4190b8 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -108,7 +108,7 @@ class OptablePrebidAnalytics { auction.bidWonEvents.length > 0 ? new Date(Math.min(...auction.bidWonEvents.map((e: any) => e._receivedAt.getTime()))).toISOString() : null; - payload["optableLoaded"] = !payload.missed; + payload["optableLoaded"] = !auction.missed; navigator.sendBeacon( witnessUrl, @@ -302,7 +302,7 @@ class OptablePrebidAnalytics { storedAuction.bidWonEvents.length > 0 ? new Date(Math.min(...storedAuction.bidWonEvents.map((e: any) => e._receivedAt.getTime()))).toISOString() : null; - payload["optableLoaded"] = !payload.missed; + payload["optableLoaded"] = !effectiveMissed; this.sendToWitnessAPI("optable.prebid.auction", payload); this.auctions.delete(auctionId); }, this.config.bidWinTimeout); From 38b2363876f04c7263ec22e90ceef1fc10a65fd6 Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:49:18 -0400 Subject: [PATCH 3/3] analytics: V1 auction with Optable sources is not missed Same fix as #356, applied to the V1 add-on that feeds bid_analytics. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/addons/prototypes/analytics.js | 2 +- .../prototypes/analytics.missed.test.js | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 lib/addons/prototypes/analytics.missed.test.js diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index fbdf4f1e..db300ae3 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -274,7 +274,7 @@ class OptablePrebidAnalytics { optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), liveintentEIDs: Array.from(lSourcesSet), - missed, + missed: missed && oSourcesSet.size === 0, url: `${window.location.hostname}${window.location.pathname}`, tenant: this.config.tenant, optableWrapperVersion: SDK_WRAPPER_VERSION, // eslint-disable-line no-undef diff --git a/lib/addons/prototypes/analytics.missed.test.js b/lib/addons/prototypes/analytics.missed.test.js new file mode 100644 index 00000000..6dd548dd --- /dev/null +++ b/lib/addons/prototypes/analytics.missed.test.js @@ -0,0 +1,41 @@ +import OptablePrebidAnalytics from "./analytics"; + +global.SDK_WRAPPER_VERSION = "1.0.0-test"; + +function bidderRequest(bidderCode, sources) { + return { + bidderCode, + bidderRequestId: `br-${bidderCode}`, + ortb2: { + user: { ext: { eids: sources.map((source) => ({ source, inserter: "optable.co", uids: [{ id: "x" }] })) } }, + }, + bids: [{ bidId: `bid-${bidderCode}`, adUnitCode: "slot-1" }], + }; +} + +async function run(sources, missed) { + const witness = jest.fn().mockResolvedValue(undefined); + const analytics = new OptablePrebidAnalytics({ witness }, { analytics: true, tenant: "t" }); + await analytics.trackAuctionEnd({ auctionId: "a-1", bidderRequests: [bidderRequest("a", sources)] }, missed); + return JSON.parse(witness.mock.calls[0][1].auction); +} + +beforeEach(() => { + window.optable = {}; +}); + +describe("missed", () => { + test("false when the hook attached before the auction", async () => { + expect((await run([], false)).missed).toBe(false); + }); + + test("false when the hook attached late but Optable sources are present", async () => { + const p = await run(["uidapi.com"], true); + expect(p.optableSources).toEqual(["uidapi.com"]); + expect(p.missed).toBe(false); + }); + + test("true when the hook attached late and no Optable sources are present", async () => { + expect((await run([], true)).missed).toBe(true); + }); +});