From 0ace9610b33ac9a3bf0d41bf5c04e5b4b4e8cfdf Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Sat, 9 May 2026 12:31:31 -0400 Subject: [PATCH 1/2] feat(analytics): track full optable RTD provider config in auction witness --- lib/addons/prototypes/analytics.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index d89c288..7b0c87f 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -57,6 +57,8 @@ class OptablePrebidAnalytics { } setHooks(pbjs) { + this.optableProvider = pbjs?.getConfig?.()?.realTimeData?.dataProviders?.find((p) => p.name === "optable") ?? null; + this.log("Processing missed auctionEnd"); pbjs.getEvents().forEach((event) => { if (event.eventType === "auctionEnd") { @@ -279,6 +281,7 @@ class OptablePrebidAnalytics { tenant: this.config.tenant, optableWrapperVersion: SDK_WRAPPER_VERSION, // eslint-disable-line no-undef prebidjsVersion: this.prebidInstance?.version || "unknown", + optableProvider: this.optableProvider ?? null, sessionDepth: sessionStorage?.optableSessionDepth || 1, pageAuctionsCount: window.optable?.pageAuctionsCount || 1, }; From c0cc80c517b726299d11788fee9623763610a1a9 Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Sat, 9 May 2026 16:26:44 -0400 Subject: [PATCH 2/2] feat(ab-test): add selectABTest addon for split test selection and persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts A/B test selection logic from wrapper bundles into a reusable addon: - calculateTrafficPercentages: fills missing trafficPercentage values evenly - determineABTest: weighted random bucket selection (mirrors lib/edge/targeting.ts) - selectABTest: full selection flow — forced overrides via sessionStorage (optableInclude, optableResolve, optableExclude), excludes marked tests, falls back to random, persists result to OPTABLE_SPLIT_TEST OptablePrebidAnalytics accepts config.selectedTest (result of selectABTest) and includes selectedABTest in every auction_processed witness payload. Wrappers can replace calculateTrafficPercentages + determineABTest + splitTestSelection with a single selectABTest() call. --- lib/addons/prototypes/ab-test.js | 78 ++++++++++++++++++++++++++++++ lib/addons/prototypes/analytics.js | 3 ++ 2 files changed, 81 insertions(+) create mode 100644 lib/addons/prototypes/ab-test.js diff --git a/lib/addons/prototypes/ab-test.js b/lib/addons/prototypes/ab-test.js new file mode 100644 index 0000000..def19c6 --- /dev/null +++ b/lib/addons/prototypes/ab-test.js @@ -0,0 +1,78 @@ +/* eslint-disable no-param-reassign */ + +// Fills missing trafficPercentage values by distributing remaining % evenly across +// tests that don't specify one. +export function calculateTrafficPercentages(abTests) { + const allocated = abTests + .filter((t) => t.trafficPercentage !== undefined) + .reduce((sum, t) => sum + t.trafficPercentage, 0); + const without = abTests.filter((t) => t.trafficPercentage === undefined); + if (without.length > 0) { + const even = (100 - allocated) / without.length; + without.forEach((t) => { + t.trafficPercentage = even; + }); + } + return abTests; +} + +// Weighted random bucket selection across tests with assigned trafficPercentage values. +// Mirrors determineABTest in lib/edge/targeting.ts. +export function determineABTest(abTests) { + if (!abTests || !abTests.length) return null; + const total = abTests.reduce((s, t) => s + t.trafficPercentage, 0); + if (total > 100) return null; + const bucket = Math.floor(Math.random() * 100); + let cumulative = 0; + for (const test of abTests) { + cumulative += test.trafficPercentage; + if (bucket < cumulative) return test; + } + return null; +} + +// Select an A/B test, applying sessionStorage forced-include/exclude overrides before +// falling back to weighted random selection. Persists the result to OPTABLE_SPLIT_TEST. +// +// Override keys (value "1" or "true" to activate): +// optableInclude — force this test +// optableResolve — force this test (alias) +// optableExclude — exclude this test from random selection +// optableInclude=false/"0" — also excludes this test +export function selectABTest(abTests) { + if (!abTests || !abTests.length) return null; + + const tests = calculateTrafficPercentages([...abTests]); + const trueVals = ["1", "true"]; + const falseVals = ["0", "false"]; + + // Forced inclusion wins — first matching test is selected immediately. + for (const test of tests) { + const key = test.id.toUpperCase(); + const forced = + trueVals.includes(sessionStorage.getItem(`optableInclude${key}`)) || + trueVals.includes(sessionStorage.getItem(`optableResolve${key}`)); + if (forced) { + const selected = { ...test, trafficPercentage: 100 }; + sessionStorage.setItem("OPTABLE_SPLIT_TEST", JSON.stringify(selected)); + return selected; + } + } + + // Filter out excluded tests before random selection. + const eligible = tests.filter((test) => { + const key = test.id.toUpperCase(); + return ( + !trueVals.includes(sessionStorage.getItem(`optableExclude${key}`)) && + !falseVals.includes(sessionStorage.getItem(`optableInclude${key}`)) + ); + }); + + const selected = determineABTest(eligible.length ? eligible : tests); + if (selected) { + const pinned = { ...selected, trafficPercentage: 100 }; + sessionStorage.setItem("OPTABLE_SPLIT_TEST", JSON.stringify(pinned)); + return pinned; + } + return null; +} diff --git a/lib/addons/prototypes/analytics.js b/lib/addons/prototypes/analytics.js index 7b0c87f..ad28dcc 100644 --- a/lib/addons/prototypes/analytics.js +++ b/lib/addons/prototypes/analytics.js @@ -19,6 +19,8 @@ class OptablePrebidAnalytics { this.optableInstance = optableInstance; this.isInitialized = true; + this.selectedTest = config.selectedTest ?? null; + // Store auction data this.auctions = {}; this.maxAuctionDataSize = 20; @@ -282,6 +284,7 @@ class OptablePrebidAnalytics { optableWrapperVersion: SDK_WRAPPER_VERSION, // eslint-disable-line no-undef prebidjsVersion: this.prebidInstance?.version || "unknown", optableProvider: this.optableProvider ?? null, + selectedABTest: this.selectedTest?.id ?? null, sessionDepth: sessionStorage?.optableSessionDepth || 1, pageAuctionsCount: window.optable?.pageAuctionsCount || 1, };