From b6a8e7e6834612e5b1556aa26d22b4472996e2b7 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Thu, 13 Aug 2026 14:34:04 -0600 Subject: [PATCH] feat(debug): optableDebug flag forces sampling=1, production group, and bypasses bot detection Co-Authored-By: Claude Sonnet 4.6 --- lib/addons/abTestAssignment.test.ts | 18 ++++++++++++++++++ lib/addons/abTestAssignment.ts | 5 +++-- lib/addons/prebid/analytics.test.ts | 10 ++++++++++ lib/addons/prebid/analytics.ts | 2 ++ lib/edge/targeting.ts | 2 ++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/addons/abTestAssignment.test.ts b/lib/addons/abTestAssignment.test.ts index dd6d0f2..dfeb672 100644 --- a/lib/addons/abTestAssignment.test.ts +++ b/lib/addons/abTestAssignment.test.ts @@ -83,6 +83,24 @@ describe("setupAB - override via flags", () => { expect(result.variant.id).toBe("production"); expect(result.isControl).toBe(false); }); + + it("forces treatment when optableDebug flag is set", () => { + sessionStorage.setItem("optableDebug", "1"); + resetFlags(); + jest.spyOn(Math, "random").mockReturnValue(0.97); // would normally land in control + const result = setupAB({ variants: [{ id: "production" }, { id: "test", trafficPercentage: 5 }] }); + expect(result.variant.id).toBe("production"); + expect(result.isControl).toBe(false); + }); + + it("optableControlGroup=1 takes priority over optableDebug", () => { + sessionStorage.setItem("optableDebug", "1"); + sessionStorage.setItem("optableControlGroup", "1"); + resetFlags(); + const result = setupAB({ variants: [{ id: "production" }, { id: "test", trafficPercentage: 5 }] }); + expect(result.variant.id).toBe("test"); + expect(result.isControl).toBe(true); + }); }); describe("setupAB - custom variant ids", () => { diff --git a/lib/addons/abTestAssignment.ts b/lib/addons/abTestAssignment.ts index 8ec63c8..f6a239c 100644 --- a/lib/addons/abTestAssignment.ts +++ b/lib/addons/abTestAssignment.ts @@ -60,10 +60,11 @@ export function setupAB(config: SetupABConfig): ABTestSetupResult { // Priority 1 — QA/debug override via URL param or sessionStorage flag. // ?optableControlGroup=1 forces the control variant; =0 forces treatment. // This lets QA verify both branches without clearing localStorage. - const controlGroupFlag = getFlags().optableControlGroup; + const flags = getFlags(); + const controlGroupFlag = flags.optableControlGroup; if (controlGroupFlag === "1") { selected = filled.find((v) => v.id === controlId) ?? { id: controlId, trafficPercentage: 0 }; - } else if (controlGroupFlag === "0") { + } else if (controlGroupFlag === "0" || flags.optableDebug) { selected = filled.find((v) => v.id === treatmentId) ?? { id: treatmentId, trafficPercentage: 0 }; } diff --git a/lib/addons/prebid/analytics.test.ts b/lib/addons/prebid/analytics.test.ts index cd457c4..3383eb4 100644 --- a/lib/addons/prebid/analytics.test.ts +++ b/lib/addons/prebid/analytics.test.ts @@ -1,5 +1,6 @@ import OptablePrebidAnalytics, { initPrebidAnalytics } from "./analytics"; import type OptableSDK from "../../sdk"; +import { resetFlags } from "../../core/flags"; // Mock the SDK_WRAPPER_VERSION global declare global { @@ -31,6 +32,8 @@ describe("OptablePrebidAnalytics", () => { document.removeEventListener("visibilitychange", (analytics as any).handleVisibilityChange); } jest.clearAllMocks(); + sessionStorage.clear(); + resetFlags(); }); describe("Class instantiation", () => { @@ -147,6 +150,13 @@ describe("OptablePrebidAnalytics", () => { mockRandom.mockRestore(); }); + + it("should return true when optableDebug flag is set, even with samplingRate 0", () => { + sessionStorage.setItem("optableDebug", "1"); + resetFlags(); + analytics = new OptablePrebidAnalytics(mockOptableInstance, { samplingRate: 0 }); + expect(analytics.shouldSample()).toBe(true); + }); }); describe("toWitness", () => { diff --git a/lib/addons/prebid/analytics.ts b/lib/addons/prebid/analytics.ts index 329e47f..79afd31 100644 --- a/lib/addons/prebid/analytics.ts +++ b/lib/addons/prebid/analytics.ts @@ -3,6 +3,7 @@ import type { WitnessProperties } from "../../edge/witness"; import type OptableSDK from "../../sdk"; import { buildRequest } from "../../core/network"; +import { getFlags } from "../../core/flags"; import * as Bowser from "bowser"; @@ -137,6 +138,7 @@ class OptablePrebidAnalytics { * @returns true if the event should be sampled and analytics calls may proceed. */ shouldSample(): boolean { + if (getFlags().optableDebug) return true; if (this.config.samplingRate! <= 0) return false; if (this.config.samplingRate! >= 1) return true; diff --git a/lib/edge/targeting.ts b/lib/edge/targeting.ts index b763dce..2f4179d 100644 --- a/lib/edge/targeting.ts +++ b/lib/edge/targeting.ts @@ -3,6 +3,7 @@ import { determineABTest } from "./abTest"; import { fetch } from "../core/network"; import { LocalStorage } from "../core/storage"; import { isBot } from "../addons/botDetection"; +import { getFlags } from "../core/flags"; import * as ortb2 from "iab-openrtb/v26"; import * as adcom from "iab-adcom"; import { sendTargetingUpdateEvent } from "../core/events/cache-refresh"; @@ -102,6 +103,7 @@ function TargetingClearCache(config: ResolvedConfig) { * Returns whether the request was identified as a bot. */ export function SkipTargetingForBots(): boolean { + if (getFlags().optableDebug) return false; try { if (typeof isBot === "function" && isBot()) { sessionStorage.setItem(TARGETING_DONE_KEY, "1");