From 64c45a6f86364e06a4c2e368f90db8aad6d70ce5 Mon Sep 17 00:00:00 2001 From: mosherBT Date: Wed, 9 Sep 2026 12:16:00 -0400 Subject: [PATCH] sourceCheck: add addon verifying the site slug has a configured source PRODUCT-4150 --- README.md | 16 ++++++++ lib/addons/sourceCheck.md | 23 ++++++++++++ lib/addons/sourceCheck.test.ts | 69 ++++++++++++++++++++++++++++++++++ lib/addons/sourceCheck.ts | 66 ++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 lib/addons/sourceCheck.md create mode 100644 lib/addons/sourceCheck.test.ts create mode 100644 lib/addons/sourceCheck.ts diff --git a/README.md b/README.md index 49f26c7a..bc596da8 100644 --- a/README.md +++ b/README.md @@ -1285,6 +1285,22 @@ window.optable.cmd = new OptableCommands(window.optable.cmd || []); For the page-side stub and behaviour details, see the [command queue addon README](lib/addons/commands.md). +## Source check + +The source check addon verifies that a page's site slug has a matching source configured in the DCN before the SDK is constructed, falling back to a default source when it does not — so multi-site publishers keep default enrichment on domains that were never provisioned. + +```typescript +import { checkSourceExists } from "@optable/web-sdk/lib/dist/addons/sourceCheck"; + +window.optable.site = await checkSourceExists({ + site: window.optable.site, + defaultSite: window.optable.defaultSite, + node: "customer-node", +}); +``` + +The result is cached in `sessionStorage`, so the probe runs at most once per session. For the probe and fallback details, see the [source check addon README](lib/addons/sourceCheck.md). + ## Demo Pages The demo pages are working examples of both `identify` and `targeting` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) DCN. diff --git a/lib/addons/sourceCheck.md b/lib/addons/sourceCheck.md new file mode 100644 index 00000000..2e4f3f81 --- /dev/null +++ b/lib/addons/sourceCheck.md @@ -0,0 +1,23 @@ +# Source Check Addon + +Verifies that a page's site slug has a matching source configured in the DCN before the SDK is constructed, and falls back to a default source when it does not — so targeting still works with default enrichment on domains that were never provisioned, instead of failing silently. Useful for multi-site publishers. + +## Usage + +```js +import { checkSourceExists } from "@optable/web-sdk/lib/dist/addons/sourceCheck"; + +window.optable.site = await checkSourceExists({ + site: window.optable.site, + defaultSite: window.optable.defaultSite, + node: "customer-node", +}); +``` + +Returns the site to use: `site` when the source exists, `defaultSite` (or `"default-sdk"` when that is empty) when it does not. Call it before constructing the SDK and pass the result as the constructor's `site`. + +## Behaviour + +- Probes `https:///config?o=&t=&purpose=check-source-exists` (`host` defaults to `na.edge.optable.co`). Only a network-level failure — the edge rejecting the unknown origin — marks the source missing; any HTTP response counts as existing. +- The result is cached in `sessionStorage` (`optable_source_exists`), so the probe runs at most once per session. +- A browser with `sessionStorage` blocked degrades to probing every page load. diff --git a/lib/addons/sourceCheck.test.ts b/lib/addons/sourceCheck.test.ts new file mode 100644 index 00000000..70ade62e --- /dev/null +++ b/lib/addons/sourceCheck.test.ts @@ -0,0 +1,69 @@ +import { http, HttpResponse } from "msw"; +import { server } from "../test/server"; +import { checkSourceExists } from "./sourceCheck"; + +const CHECK_URL = "https://na.edge.optable.co/config"; + +beforeEach(() => { + sessionStorage.clear(); +}); + +describe("checkSourceExists", () => { + it("returns the site and caches the result when the source exists", async () => { + let url: URL | undefined; + server.use( + http.get(CHECK_URL, ({ request }) => { + url = new URL(request.url); + return HttpResponse.json({}); + }) + ); + + await expect(checkSourceExists({ site: "pub-site", defaultSite: "pub-sdk", node: "pub" })).resolves.toBe( + "pub-site" + ); + expect(url?.searchParams.get("o")).toBe("pub-site"); + expect(url?.searchParams.get("t")).toBe("pub"); + expect(url?.searchParams.get("purpose")).toBe("check-source-exists"); + expect(sessionStorage.getItem("optable_source_exists")).toBe("1"); + }); + + it("falls back to defaultSite and caches the miss on a network failure", async () => { + server.use(http.get(CHECK_URL, () => HttpResponse.error())); + + await expect(checkSourceExists({ site: "unknown-site", defaultSite: "pub-sdk" })).resolves.toBe("pub-sdk"); + expect(sessionStorage.getItem("optable_source_exists")).toBe("0"); + }); + + it("falls back to 'default-sdk' when no defaultSite is configured", async () => { + server.use(http.get(CHECK_URL, () => HttpResponse.error())); + + await expect(checkSourceExists({ site: "unknown-site", defaultSite: "" })).resolves.toBe("default-sdk"); + }); + + it("uses the cached miss without probing", async () => { + sessionStorage.setItem("optable_source_exists", "0"); + + await expect(checkSourceExists({ site: "pub-site", defaultSite: "pub-sdk" })).resolves.toBe("pub-sdk"); + }); + + it("uses the cached hit without probing", async () => { + sessionStorage.setItem("optable_source_exists", "1"); + + await expect(checkSourceExists({ site: "pub-site", defaultSite: "pub-sdk" })).resolves.toBe("pub-site"); + }); + + it("probes a caller-provided host", async () => { + let hit = false; + server.use( + http.get("https://eu.edge.optable.co/config", () => { + hit = true; + return HttpResponse.json({}); + }) + ); + + await expect( + checkSourceExists({ site: "pub-site", defaultSite: "pub-sdk", host: "eu.edge.optable.co" }) + ).resolves.toBe("pub-site"); + expect(hit).toBe(true); + }); +}); diff --git a/lib/addons/sourceCheck.ts b/lib/addons/sourceCheck.ts new file mode 100644 index 00000000..1fe66743 --- /dev/null +++ b/lib/addons/sourceCheck.ts @@ -0,0 +1,66 @@ +import { debugLog } from "../core/log"; + +const SOURCE_EXISTS_KEY = "optable_source_exists"; +const DEFAULT_CHECK_HOST = "na.edge.optable.co"; + +type SourceCheckOptions = { + // Site slug configured on the page, to be verified against the DCN. + site: string; + // Source to fall back to when `site` has no matching source. + defaultSite: string; + // DCN node the wrapper targets. + node?: string; + // Edge host to probe. + host?: string; +}; + +/** + * Verifies that `site` has a matching source configured in the DCN and + * returns the site to use: `site` when it exists, `defaultSite` when it does + * not. The result is cached in sessionStorage, so the probe runs at most once + * per session. Only a network-level failure (the edge rejecting the unknown + * origin) marks a source missing. + */ +async function checkSourceExists({ site, defaultSite, node, host }: SourceCheckOptions): Promise { + const fallback = defaultSite || "default-sdk"; + + let cached: string | null = null; + try { + cached = sessionStorage.getItem(SOURCE_EXISTS_KEY); + } catch { + // sessionStorage unavailable; probe every load. + } + + if (cached === "0") { + debugLog("info", `Site "${site}" not configured (cached), using ${fallback}`); + return fallback; + } + if (cached !== null) { + return site; + } + + const params = new URLSearchParams({ o: site, purpose: "check-source-exists" }); + if (node) { + params.set("t", node); + } + + try { + await fetch(`https://${host || DEFAULT_CHECK_HOST}/config?${params.toString()}`); + try { + sessionStorage.setItem(SOURCE_EXISTS_KEY, "1"); + } catch { + // sessionStorage unavailable + } + return site; + } catch { + debugLog("info", `Site "${site}" not configured, falling back to ${fallback}`); + try { + sessionStorage.setItem(SOURCE_EXISTS_KEY, "0"); + } catch { + // sessionStorage unavailable + } + return fallback; + } +} + +export { checkSourceExists };