From 426e48f95d945ff89677fe9ef3ee96b74a1535f7 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Tue, 14 Jul 2026 14:42:28 -0400 Subject: [PATCH 1/3] Add BotDetection functions --- lib/addons/botDetection.test.ts | 67 +++++++++++++++++++++++++++++++++ lib/addons/botDetection.ts | 48 +++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 lib/addons/botDetection.test.ts create mode 100644 lib/addons/botDetection.ts diff --git a/lib/addons/botDetection.test.ts b/lib/addons/botDetection.test.ts new file mode 100644 index 0000000..5adc7f9 --- /dev/null +++ b/lib/addons/botDetection.test.ts @@ -0,0 +1,67 @@ +import { enableBotDetection, isBot } from "./botDetection"; + +const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; + +describe("isBot", () => { + it.each([ + "Googlebot/2.1 (+http://www.google.com/bot.html)", + "Mozilla/5.0 (compatible; bingbot/2.0)", + "some-crawler/1.0", + "spider", + "Mozilla/5.0 (X11; Linux x86_64) HeadlessChrome/120.0.0.0", + "PhantomJS/2.1.1", + "curl/8.4.0", + "python-requests/2.31.0", + "axios/1.6.0", + "PostmanRuntime/7.36.0", + "GoogleOther", + ])("returns true for bot user agent %j", (ua) => { + expect(isBot(ua)).toBe(true); + }); + + it.each([ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/121.0", + ])("returns false for real browser user agent %j", (ua) => { + expect(isBot(ua)).toBe(false); + }); + + it("is case insensitive", () => { + expect(isBot("GOOGLEBOT")).toBe(true); + expect(isBot("CuRl/1.0")).toBe(true); + }); + + it("returns false for an empty user agent", () => { + expect(isBot("")).toBe(false); + }); + + it("falls back to navigator.userAgent when no argument is provided", () => { + const spy = jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Googlebot/2.1"); + expect(isBot()).toBe(true); + spy.mockRestore(); + }); +}); + +describe("enableBotDetection", () => { + beforeEach(() => { + sessionStorage.clear(); + jest.restoreAllMocks(); + }); + + it("marks targeting as done and returns true for a bot", () => { + jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Googlebot/2.1"); + expect(enableBotDetection()).toBe(true); + expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBe("1"); + }); + + it("is a no-op and returns false for a real user", () => { + jest + .spyOn(navigator, "userAgent", "get") + .mockReturnValue( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ); + expect(enableBotDetection()).toBe(false); + expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBeNull(); + }); +}); diff --git a/lib/addons/botDetection.ts b/lib/addons/botDetection.ts new file mode 100644 index 0000000..61b32e2 --- /dev/null +++ b/lib/addons/botDetection.ts @@ -0,0 +1,48 @@ +const BOT_PATTERN = new RegExp( + [ + "bot", + "crawler", + "spider", + "scraper", + "headless", + "phantomjs", + "selenium", + "webdriver", + "curl", + "wget", + "python", + "java", + "perl", + "ruby", + "go-http-client", + "okhttp", + "axios", + "fetch", + "postman", + "insomnia", + "googleother", + "google-extended", + "google-inspectiontool", + ].join("|"), + "i" +); + +const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; + +/** True if the current user agent looks like a known bot/crawler. */ +export function isBot(userAgent: string = navigator.userAgent): boolean { + return BOT_PATTERN.test(userAgent || ""); +} + +/** + * Skip targeting for bots by marking targeting as already done, + * so RTD short-circuits and returns null. No-op for real users. + * Returns whether the request was identified as a bot. + */ +export function enableBotDetection(): boolean { + if (isBot()) { + sessionStorage.setItem(TARGETING_DONE_KEY, "1"); + return true; + } + return false; +} From dcdc87ff191ef8a187d052d3e1bfa8a8f5a55787 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Wed, 15 Jul 2026 12:43:18 -0400 Subject: [PATCH 2/3] imove --- lib/addons/botDetection.ts | 14 -------------- lib/edge/targeting.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/addons/botDetection.ts b/lib/addons/botDetection.ts index 61b32e2..c908f0a 100644 --- a/lib/addons/botDetection.ts +++ b/lib/addons/botDetection.ts @@ -27,22 +27,8 @@ const BOT_PATTERN = new RegExp( "i" ); -const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; /** True if the current user agent looks like a known bot/crawler. */ export function isBot(userAgent: string = navigator.userAgent): boolean { return BOT_PATTERN.test(userAgent || ""); } - -/** - * Skip targeting for bots by marking targeting as already done, - * so RTD short-circuits and returns null. No-op for real users. - * Returns whether the request was identified as a bot. - */ -export function enableBotDetection(): boolean { - if (isBot()) { - sessionStorage.setItem(TARGETING_DONE_KEY, "1"); - return true; - } - return false; -} diff --git a/lib/edge/targeting.ts b/lib/edge/targeting.ts index 29cc10f..4f773a6 100644 --- a/lib/edge/targeting.ts +++ b/lib/edge/targeting.ts @@ -1,6 +1,7 @@ import type { ResolvedConfig, ABTestConfig, MatcherOverride } from "../config"; import { fetch } from "../core/network"; import { LocalStorage } from "../core/storage"; +import { isBot } from "../addons/botDetection"; import * as ortb2 from "iab-openrtb/v26"; import * as adcom from "iab-adcom"; import { sendTargetingUpdateEvent } from "../core/events/cache-refresh"; @@ -40,6 +41,8 @@ type TargetingResponse = { split_test_assignment?: string; }; +const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; + // Determine which A/B test (if any) should be used for this request function determineABTest(abTests?: ABTestConfig[]): ABTestConfig | null { if (!abTests || abTests.length === 0) { @@ -117,6 +120,20 @@ function TargetingClearCache(config: ResolvedConfig) { ls.clearTargeting(); } +/** + * Skip targeting for bots by marking targeting as already done, + * so RTD short-circuits and returns null. No-op for real users. + * Returns whether the request was identified as a bot. + */ +export function SkipTargetingForBots(): boolean { + if (isBot()) { + sessionStorage.setItem(TARGETING_DONE_KEY, "1"); + return true; + } + return false; +} + + // Prebid.js supports setting ortb2 object for compatible bidder adapters. // // We return the contents to be merged in ortb2 and passed to From 432f60b689ab40049115194227a34cb0a72dddf0 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Wed, 15 Jul 2026 12:52:58 -0400 Subject: [PATCH 3/3] comments --- lib/addons/botDetection.test.ts | 27 +-------------------------- lib/addons/botDetection.ts | 1 - lib/edge/targeting.test.js | 27 ++++++++++++++++++++++++++- lib/edge/targeting.ts | 11 +++++++---- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/addons/botDetection.test.ts b/lib/addons/botDetection.test.ts index 5adc7f9..51bd784 100644 --- a/lib/addons/botDetection.test.ts +++ b/lib/addons/botDetection.test.ts @@ -1,6 +1,4 @@ -import { enableBotDetection, isBot } from "./botDetection"; - -const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; +import { isBot } from "./botDetection"; describe("isBot", () => { it.each([ @@ -42,26 +40,3 @@ describe("isBot", () => { spy.mockRestore(); }); }); - -describe("enableBotDetection", () => { - beforeEach(() => { - sessionStorage.clear(); - jest.restoreAllMocks(); - }); - - it("marks targeting as done and returns true for a bot", () => { - jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Googlebot/2.1"); - expect(enableBotDetection()).toBe(true); - expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBe("1"); - }); - - it("is a no-op and returns false for a real user", () => { - jest - .spyOn(navigator, "userAgent", "get") - .mockReturnValue( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - ); - expect(enableBotDetection()).toBe(false); - expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBeNull(); - }); -}); diff --git a/lib/addons/botDetection.ts b/lib/addons/botDetection.ts index c908f0a..0fcc86a 100644 --- a/lib/addons/botDetection.ts +++ b/lib/addons/botDetection.ts @@ -27,7 +27,6 @@ const BOT_PATTERN = new RegExp( "i" ); - /** True if the current user agent looks like a known bot/crawler. */ export function isBot(userAgent: string = navigator.userAgent): boolean { return BOT_PATTERN.test(userAgent || ""); diff --git a/lib/edge/targeting.test.js b/lib/edge/targeting.test.js index 2c82961..8d6356b 100644 --- a/lib/edge/targeting.test.js +++ b/lib/edge/targeting.test.js @@ -1,4 +1,4 @@ -import { PrebidORTB2, TargetingKeyValues, Targeting } from "./targeting"; +import { PrebidORTB2, TargetingKeyValues, Targeting, SkipTargetingForBots } from "./targeting"; describe("PrebidORTB2", () => { test("returns empty array on empty input", () => { @@ -45,6 +45,31 @@ describe("TargetingKeyValues", () => { }); }); +describe("SkipTargetingForBots", () => { + const TARGETING_DONE_KEY = "OPTABLE_TARGETING_DONE"; + + beforeEach(() => { + sessionStorage.clear(); + jest.restoreAllMocks(); + }); + + it("marks targeting as done and returns true for a bot", () => { + jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Googlebot/2.1"); + expect(SkipTargetingForBots()).toBe(true); + expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBe("1"); + }); + + it("is a no-op and returns false for a real user", () => { + jest + .spyOn(navigator, "userAgent", "get") + .mockReturnValue( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ); + expect(SkipTargetingForBots()).toBe(false); + expect(sessionStorage.getItem(TARGETING_DONE_KEY)).toBeNull(); + }); +}); + describe("determineABTest", () => { // Mock Math.random to control the random bucket selection let originalMathRandom; diff --git a/lib/edge/targeting.ts b/lib/edge/targeting.ts index 4f773a6..9fe95ee 100644 --- a/lib/edge/targeting.ts +++ b/lib/edge/targeting.ts @@ -126,14 +126,17 @@ function TargetingClearCache(config: ResolvedConfig) { * Returns whether the request was identified as a bot. */ export function SkipTargetingForBots(): boolean { - if (isBot()) { - sessionStorage.setItem(TARGETING_DONE_KEY, "1"); - return true; + try { + if (typeof isBot === "function" && isBot()) { + sessionStorage.setItem(TARGETING_DONE_KEY, "1"); + return true; + } + } catch { + // isBot is unavailable or threw; fall through and treat as a real user. } return false; } - // Prebid.js supports setting ortb2 object for compatible bidder adapters. // // We return the contents to be merged in ortb2 and passed to