From dbcce1e53fe7115da5dba9b4e8111bba0ee1c8fb Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Wed, 22 Jul 2026 14:05:26 -0600 Subject: [PATCH 1/3] fix(prebid-analytics): send optableTargetingDone as '1'/'0' string not a count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field was set to `oMatchersSet.size || oSourcesSet.size`, which evaluates to the matcher count (0, 1, 2, …). The analytics processor schema declares this field as STRING and routes it through: CASE WHEN … IN ('1', 'true') THEN 'enriched' WHEN … IN ('0', 'false') THEN 'notenriched' Spark's JSON reader coerces a JSON number to its string representation, so a count of 2 (e.g. uid2 + id5) arrives as "2" — matching neither branch and producing status='unknown' for every enriched auction with 2+ active matchers. The single-matcher case worked by coincidence ("1" ∈ ('1','true')). Fix: send the literal string '1' or '0'. The count/identity of active matchers is already carried by the optableMatchers array, which the processor stores in fact_impression_opportunity.matchers directly. Co-Authored-By: Claude Sonnet 4.6 --- lib/addons/prebid/analytics.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 6daefda..58bc576 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -597,7 +597,10 @@ class OptablePrebidAnalytics { adUnitCode, totalRequests: bidderRequests.length, optableSampling: this.config.samplingRate || 1, - optableTargetingDone: oMatchersSet.size || oSourcesSet.size, + // Processor schema declares optableTargetingDone as STRING and checks IN ('1','true'). + // Send '1'/'0' so Spark reads a predictable string regardless of matcher count. + // A raw count (e.g. 2) coerces to "2" which matches neither branch → status='unknown'. + optableTargetingDone: oMatchersSet.size > 0 || oSourcesSet.size > 0 ? '1' : '0', optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), bidWon: bidWonEvents.map((e) => ({ From 7e9468e91d1076df58782d5e07f771d260e714bd Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Wed, 22 Jul 2026 14:10:00 -0600 Subject: [PATCH 2/3] test(prebid-analytics): add optableTargetingDone string type assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression coverage for the fix that changed optableTargetingDone from a raw count (number) to '1'/'0' strings. Adds four cases: single matcher, two matchers (uid2+id5 — the silent regression where count=2 → "2" → status='unknown'), no EIDs, and sources-only targeting. Co-Authored-By: Claude Sonnet 4.6 --- lib/addons/prebid/analytics.test.ts | 109 ++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index 102787f..a88a979 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -258,6 +258,115 @@ describe("OptablePrebidAnalytics", () => { expect(result.optableMatchers).toEqual([]); expect(result.optableSources).toEqual([]); }); + + it("optableTargetingDone: '1' when a single Optable matcher is present", async () => { + const result = await analytics.toWitness( + { + auctionId: "auction-targeting-one-matcher", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { eids: [{ inserter: "optable.co", matcher: "uid2", source: "uid2.com" }] }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }, + [] + ); + + expect(result.optableTargetingDone).toBe("1"); + }); + + it("optableTargetingDone: '1' when two Optable matchers are present (uid2 + id5)", async () => { + // Regression: previously sent `oMatchersSet.size` (the number 2) which Spark + // coerced to "2" — matching neither IN ('1','true') nor IN ('0','false') → + // processor classified every such auction as status='unknown' instead of 'enriched'. + const result = await analytics.toWitness( + { + auctionId: "auction-targeting-two-matchers", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { + eids: [ + { inserter: "optable.co", matcher: "uid2", source: "uid2.com" }, + { inserter: "optable.co", matcher: "id5", source: "id5-sync.com" }, + ], + }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }, + [] + ); + + expect(result.optableTargetingDone).toBe("1"); + expect(result.optableMatchers).toEqual(["uid2", "id5"]); + }); + + it("optableTargetingDone: '0' when no Optable EIDs are present", async () => { + const result = await analytics.toWitness( + { + auctionId: "auction-targeting-no-eids", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { eids: [] }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }, + [] + ); + + expect(result.optableTargetingDone).toBe("0"); + }); + + it("optableTargetingDone: '1' when Optable sources are present but no named matcher", async () => { + const result = await analytics.toWitness( + { + auctionId: "auction-targeting-source-only", + bidderRequests: [ + { + bidderCode: "bidder1", + bidderRequestId: "req-1", + ortb2: { + site: { domain: "example.com" }, + user: { eids: [{ inserter: "optable.co", source: "liveramp.com" }] }, + }, + bids: [], + }, + ], + bidsReceived: [], + noBids: [], + timeoutBids: [], + }, + [] + ); + + expect(result.optableTargetingDone).toBe("1"); + }); }); describe("trackAuctionEnd", () => { From 28b93c28c03eaa602c8110d099ce45ce7048e584 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Wed, 22 Jul 2026 14:11:22 -0600 Subject: [PATCH 3/3] chore: apply prettier formatting to analytics.ts Co-Authored-By: Claude Sonnet 4.6 --- lib/addons/prebid/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 58bc576..6206653 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -600,7 +600,7 @@ class OptablePrebidAnalytics { // Processor schema declares optableTargetingDone as STRING and checks IN ('1','true'). // Send '1'/'0' so Spark reads a predictable string regardless of matcher count. // A raw count (e.g. 2) coerces to "2" which matches neither branch → status='unknown'. - optableTargetingDone: oMatchersSet.size > 0 || oSourcesSet.size > 0 ? '1' : '0', + optableTargetingDone: oMatchersSet.size > 0 || oSourcesSet.size > 0 ? "1" : "0", optableMatchers: Array.from(oMatchersSet), optableSources: Array.from(oSourcesSet), bidWon: bidWonEvents.map((e) => ({