From fda84a2ecbefa6e7b78973bb281ff4cbeb392eff Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Thu, 16 Jul 2026 12:37:03 -0400 Subject: [PATCH 1/3] Add geotargeting addon Generalizes the geo-based host/node resolution from the mediavine solution wrapper (getGeoConfig/geoMap) into a reusable addon: the tenant name is a parameter instead of being hardcoded, and the visitor geo is passed explicitly. Returns null for unsupported geos so callers can skip region-specific initialization. Co-Authored-By: Claude Fable 5 --- README.md | 23 +++++++++ lib/addons/geotargeting.test.js | 88 +++++++++++++++++++++++++++++++++ lib/addons/geotargeting.ts | 59 ++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 lib/addons/geotargeting.test.js create mode 100644 lib/addons/geotargeting.ts diff --git a/README.md b/README.md index 1d63858..cd06713 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ JavaScript SDK for integrating with an [Optable Data Connectivity Node (DCN)](ht - [Rules](#rules) - [Return Value](#return-value) - [Input Type](#input-type) +- [Geotargeting](#geotargeting) - [Demo Pages](#demo-pages) ## Installing @@ -1174,6 +1175,28 @@ type NodeTargetingRule = { }; ``` +## Geotargeting + +The geotargeting addon maps a visitor's geo (country code) to the Optable host and node that should serve them, so that a single SDK bundle can route traffic to region-specific DCNs. + +```typescript +import { getGeoConfig } from "@optable/web-sdk/lib/dist/addons/geotargeting"; + +const geoConfig = getGeoConfig("acme", visitorGeo); // e.g. { host: "na.edge.optable.co", node: "acme" } for "US" + +if (geoConfig) { + const sdk = new OptableSDK({ + host: geoConfig.host, + node: geoConfig.node, + site: "my-site", + }); +} +``` + +The first argument is the node name: the tenant name, optionally with an `-auth` suffix to select the auth variant of the node (e.g. `"acme-auth"`). `getGeoConfig` returns `null` when the geo is not supported, in which case region-specific initialization should be skipped. + +By default the following geos are supported: `US`, `CA`, `GB`, `UK` and `AU`. A custom `GeoMap` can be passed as a third argument to override the mapping; see `lib/addons/geotargeting.ts` for the entry format. + ## 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/geotargeting.test.js b/lib/addons/geotargeting.test.js new file mode 100644 index 0000000..cdc7752 --- /dev/null +++ b/lib/addons/geotargeting.test.js @@ -0,0 +1,88 @@ +import { getGeoConfig, DEFAULT_GEO_MAP } from "./geotargeting.ts"; + +describe("getGeoConfig", () => { + test("resolves the regional edge host and node for US", () => { + expect(getGeoConfig("acme", "US")).toEqual({ + host: "na.edge.optable.co", + node: "acme", + }); + }); + + test("resolves the auth node for US", () => { + expect(getGeoConfig("acme-auth", "US")).toEqual({ + host: "na.edge.optable.co", + node: "acme-auth", + }); + }); + + test("resolves the regional edge host and node for CA", () => { + expect(getGeoConfig("acme", "CA")).toEqual({ + host: "ca.edge.optable.co", + node: "acme-ca", + }); + }); + + test("resolves the auth node for CA", () => { + expect(getGeoConfig("acme-auth", "CA")).toEqual({ + host: "ca.edge.optable.co", + node: "acme-ca-auth", + }); + }); + + test("resolves a dedicated cloud host with null node for AU", () => { + expect(getGeoConfig("acme", "AU")).toEqual({ + host: "acme.cloud.au.optable.co", + node: null, + }); + }); + + test("resolves a dedicated auth cloud host with null node for AU", () => { + expect(getGeoConfig("acme-auth", "AU")).toEqual({ + host: "acme-auth.cloud.au.optable.co", + node: null, + }); + }); + + test("resolves GB and UK to the same EU cloud host", () => { + const expected = { host: "acme.cloud.eu.optable.co", node: null }; + expect(getGeoConfig("acme", "GB")).toEqual(expected); + expect(getGeoConfig("acme", "UK")).toEqual(expected); + }); + + test("returns null for an unsupported geo", () => { + expect(getGeoConfig("acme", "FR")).toBeNull(); + }); + + test("returns null for a missing geo", () => { + expect(getGeoConfig("acme", "")).toBeNull(); + expect(getGeoConfig("acme", undefined)).toBeNull(); + }); + + test("only treats a trailing -auth as the auth variant", () => { + expect(getGeoConfig("author-press", "US")).toEqual({ + host: "na.edge.optable.co", + node: "author-press", + }); + }); + + test("accepts a custom geo map", () => { + const geoMap = { + BR: ["-br.cloud", "-br-auth.cloud", "sa.edge.optable.co"], + MX: [".cloud.mx", "-auth.cloud.mx"], + }; + expect(getGeoConfig("acme", "BR", geoMap)).toEqual({ + host: "sa.edge.optable.co", + node: "acme-br", + }); + expect(getGeoConfig("acme-auth", "MX", geoMap)).toEqual({ + host: "acme-auth.cloud.mx.optable.co", + node: null, + }); + expect(getGeoConfig("acme", "US", geoMap)).toBeNull(); + }); + + test("exposes the default geo map", () => { + expect(DEFAULT_GEO_MAP.US).toEqual([".cloud", "-auth.cloud", "na.edge.optable.co"]); + expect(DEFAULT_GEO_MAP.CA).toEqual(["-ca.cloud", "-ca-auth.cloud", "ca.edge.optable.co"]); + }); +}); diff --git a/lib/addons/geotargeting.ts b/lib/addons/geotargeting.ts new file mode 100644 index 0000000..f0e4f18 --- /dev/null +++ b/lib/addons/geotargeting.ts @@ -0,0 +1,59 @@ +/* + * The geotargeting addon maps a visitor's geo (country code) to the Optable + * host and node that should serve them, so that a single SDK bundle can route + * traffic to region-specific DCNs. + * + * A GeoMap entry is a tuple of host fragments keyed by country code: + * [0] — host suffix for the standard (non-auth) node + * [1] — host suffix for the auth node + * [2] — optional regional edge host shared by multiple nodes + * + * When a regional edge host ([2]) is present, it is used as the host and the + * node name is derived from the tenant name plus the suffix with ".cloud" + * removed (e.g. "acme" + "-ca-auth" → "acme-ca-auth"). Otherwise the tenant + * runs on a dedicated cloud host built as `${name}${suffix}.optable.co` and + * the node is null (the host's default node is used). + */ + +export type GeoMapEntry = [string, string] | [string, string, string]; +export type GeoMap = Record; + +export interface GeoConfig { + host: string; + node: string | null; +} + +export const DEFAULT_GEO_MAP: GeoMap = { + AU: [".cloud.au", "-auth.cloud.au"], + CA: ["-ca.cloud", "-ca-auth.cloud", "ca.edge.optable.co"], + GB: [".cloud.eu", "-auth.cloud.eu"], + UK: [".cloud.eu", "-auth.cloud.eu"], + US: [".cloud", "-auth.cloud", "na.edge.optable.co"], +}; + +/* + * getGeoConfig() resolves the host and node for a node name in a given geo. + * + * nodeName is the tenant name, optionally with an "-auth" suffix selecting the + * auth variant of the node (e.g. "acme" or "acme-auth"). + * + * Returns null when the geo is missing or not present in the map, in which + * case the caller should skip region-specific initialization. + */ +export function getGeoConfig(nodeName: string, geo: string, geoMap: GeoMap = DEFAULT_GEO_MAP): GeoConfig | null { + const entry = geoMap[geo]; + if (!entry) { + return null; + } + + const auth = /-auth$/i.test(nodeName); + const name = auth ? nodeName.replace(/-auth$/i, "") : nodeName; + const suffix = auth ? entry[1] : entry[0]; + const edgeHost = entry[2]; + + if (edgeHost != null) { + return { host: edgeHost, node: name + suffix.replace(/\.cloud/, "") }; + } + + return { host: `${name}${suffix}.optable.co`, node: null }; +} From 6d59dc4543991a760d4c4c11f4e8897af859b461 Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Fri, 17 Jul 2026 10:32:38 -0400 Subject: [PATCH 2/3] Address geotargeting review findings - Reject geos inherited from Object.prototype (e.g. "constructor", "__proto__") which bypassed the unsupported-geo guard and produced a bogus "undefined.optable.co" host - Type GeoConfig.node as string | undefined instead of string | null so the result spreads directly into InitConfig under strict TS - Accept geo: string | undefined, matching the documented and tested missing-geo behavior - Share one EU entry between GB and UK so they cannot drift apart - Expose getGeoConfig to script-tag consumers via window.optable.utils - Document that DEFAULT_GEO_MAP reflects a specific provisioning shape and that differently provisioned tenants must pass their own GeoMap Co-Authored-By: Claude Fable 5 --- README.md | 6 ++++-- browser/sdk.ts | 3 ++- lib/addons/geotargeting.test.js | 28 +++++++++++++++++----------- lib/addons/geotargeting.ts | 29 +++++++++++++++++++++-------- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cd06713..3bcd244 100644 --- a/README.md +++ b/README.md @@ -1193,9 +1193,11 @@ if (geoConfig) { } ``` -The first argument is the node name: the tenant name, optionally with an `-auth` suffix to select the auth variant of the node (e.g. `"acme-auth"`). `getGeoConfig` returns `null` when the geo is not supported, in which case region-specific initialization should be skipped. +The first argument is the node name: the tenant name, optionally with an `-auth` suffix to select the auth variant of the node (e.g. `"acme-auth"`). `getGeoConfig` returns `null` when the geo is not supported, in which case region-specific initialization should be skipped. For geos served by a dedicated cloud host rather than a regional edge host, `node` is `undefined` and the host's default node is used. -By default the following geos are supported: `US`, `CA`, `GB`, `UK` and `AU`. A custom `GeoMap` can be passed as a third argument to override the mapping; see `lib/addons/geotargeting.ts` for the entry format. +When using a script tag, the helper is available as `window.optable.utils.getGeoConfig`. + +The default `GeoMap` supports the `US`, `CA`, `GB`, `UK` and `AU` geos and reflects a specific provisioning shape: regional edge nodes in US/CA and dedicated per-tenant cloud hosts (`.cloud..optable.co`) in AU and the EU. The dedicated hosts only exist for tenants provisioned that way — pass a custom `GeoMap` as the third argument when your topology differs; see `lib/addons/geotargeting.ts` for the entry format. ## Demo Pages diff --git a/browser/sdk.ts b/browser/sdk.ts index d56266e..17c3d27 100644 --- a/browser/sdk.ts +++ b/browser/sdk.ts @@ -1,5 +1,6 @@ import Commands from "./commands"; import { resolveMultiNodeTargeting } from "../lib/core/resolvers/resolveMultiTargeting"; +import { getGeoConfig } from "../lib/addons/geotargeting"; import OptableSDK, { InitConfig } from "../lib/sdk"; import OptablePrebidAnalytics from "../lib/addons/prebid/analytics"; @@ -28,7 +29,7 @@ window.optable = window.optable || {}; window.optable.SDK = OptableSDK; window.optable.OptablePrebidAnalytics = OptablePrebidAnalytics; window.optable.cmd = new Commands(window.optable.cmd || []); -window.optable.utils = { resolveMultiNodeTargeting }; +window.optable.utils = { resolveMultiNodeTargeting, getGeoConfig }; if (window.optable.instance_config) { window.optable.instance = new OptableSDK(window.optable.instance_config); diff --git a/lib/addons/geotargeting.test.js b/lib/addons/geotargeting.test.js index cdc7752..6eaaf95 100644 --- a/lib/addons/geotargeting.test.js +++ b/lib/addons/geotargeting.test.js @@ -29,24 +29,24 @@ describe("getGeoConfig", () => { }); }); - test("resolves a dedicated cloud host with null node for AU", () => { - expect(getGeoConfig("acme", "AU")).toEqual({ + test("resolves a dedicated cloud host with undefined node for AU", () => { + expect(getGeoConfig("acme", "AU")).toStrictEqual({ host: "acme.cloud.au.optable.co", - node: null, + node: undefined, }); }); - test("resolves a dedicated auth cloud host with null node for AU", () => { - expect(getGeoConfig("acme-auth", "AU")).toEqual({ + test("resolves a dedicated auth cloud host with undefined node for AU", () => { + expect(getGeoConfig("acme-auth", "AU")).toStrictEqual({ host: "acme-auth.cloud.au.optable.co", - node: null, + node: undefined, }); }); test("resolves GB and UK to the same EU cloud host", () => { - const expected = { host: "acme.cloud.eu.optable.co", node: null }; - expect(getGeoConfig("acme", "GB")).toEqual(expected); - expect(getGeoConfig("acme", "UK")).toEqual(expected); + const expected = { host: "acme.cloud.eu.optable.co", node: undefined }; + expect(getGeoConfig("acme", "GB")).toStrictEqual(expected); + expect(getGeoConfig("acme", "UK")).toStrictEqual(expected); }); test("returns null for an unsupported geo", () => { @@ -58,6 +58,12 @@ describe("getGeoConfig", () => { expect(getGeoConfig("acme", undefined)).toBeNull(); }); + test("returns null for geos inherited from Object.prototype", () => { + expect(getGeoConfig("acme", "constructor")).toBeNull(); + expect(getGeoConfig("acme", "__proto__")).toBeNull(); + expect(getGeoConfig("acme", "toString")).toBeNull(); + }); + test("only treats a trailing -auth as the auth variant", () => { expect(getGeoConfig("author-press", "US")).toEqual({ host: "na.edge.optable.co", @@ -74,9 +80,9 @@ describe("getGeoConfig", () => { host: "sa.edge.optable.co", node: "acme-br", }); - expect(getGeoConfig("acme-auth", "MX", geoMap)).toEqual({ + expect(getGeoConfig("acme-auth", "MX", geoMap)).toStrictEqual({ host: "acme-auth.cloud.mx.optable.co", - node: null, + node: undefined, }); expect(getGeoConfig("acme", "US", geoMap)).toBeNull(); }); diff --git a/lib/addons/geotargeting.ts b/lib/addons/geotargeting.ts index f0e4f18..636b430 100644 --- a/lib/addons/geotargeting.ts +++ b/lib/addons/geotargeting.ts @@ -12,7 +12,12 @@ * node name is derived from the tenant name plus the suffix with ".cloud" * removed (e.g. "acme" + "-ca-auth" → "acme-ca-auth"). Otherwise the tenant * runs on a dedicated cloud host built as `${name}${suffix}.optable.co` and - * the node is null (the host's default node is used). + * the node is undefined (the host's default node is used). + * + * DEFAULT_GEO_MAP reflects one specific provisioning shape: regional edge + * nodes in US/CA and dedicated per-tenant cloud hosts in AU and the EU. The + * dedicated hosts only exist for tenants provisioned that way — tenants with + * a different topology must pass their own GeoMap. */ export type GeoMapEntry = [string, string] | [string, string, string]; @@ -20,14 +25,16 @@ export type GeoMap = Record; export interface GeoConfig { host: string; - node: string | null; + node: string | undefined; } +const EU_ENTRY: GeoMapEntry = [".cloud.eu", "-auth.cloud.eu"]; + export const DEFAULT_GEO_MAP: GeoMap = { AU: [".cloud.au", "-auth.cloud.au"], CA: ["-ca.cloud", "-ca-auth.cloud", "ca.edge.optable.co"], - GB: [".cloud.eu", "-auth.cloud.eu"], - UK: [".cloud.eu", "-auth.cloud.eu"], + GB: EU_ENTRY, + UK: EU_ENTRY, US: [".cloud", "-auth.cloud", "na.edge.optable.co"], }; @@ -40,9 +47,15 @@ export const DEFAULT_GEO_MAP: GeoMap = { * Returns null when the geo is missing or not present in the map, in which * case the caller should skip region-specific initialization. */ -export function getGeoConfig(nodeName: string, geo: string, geoMap: GeoMap = DEFAULT_GEO_MAP): GeoConfig | null { - const entry = geoMap[geo]; - if (!entry) { +export function getGeoConfig( + nodeName: string, + geo: string | undefined, + geoMap: GeoMap = DEFAULT_GEO_MAP +): GeoConfig | null { + const entry = geo === undefined ? undefined : geoMap[geo]; + // Array.isArray also rejects inherited Object.prototype members picked up + // when an unexpected geo like "constructor" is looked up in the map + if (!Array.isArray(entry)) { return null; } @@ -55,5 +68,5 @@ export function getGeoConfig(nodeName: string, geo: string, geoMap: GeoMap = DEF return { host: edgeHost, node: name + suffix.replace(/\.cloud/, "") }; } - return { host: `${name}${suffix}.optable.co`, node: null }; + return { host: `${name}${suffix}.optable.co`, node: undefined }; } From 7861129f20a93aa59aa7a59d57fc8b213308f135 Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Fri, 17 Jul 2026 11:25:43 -0400 Subject: [PATCH 3/3] Keep geotargeting out of the browser bundle The addon is meant to be imported dynamically by bundles that need it, not shipped always-on via window.optable.utils. Co-Authored-By: Claude Fable 5 --- README.md | 2 -- browser/sdk.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 3bcd244..afbb994 100644 --- a/README.md +++ b/README.md @@ -1195,8 +1195,6 @@ if (geoConfig) { The first argument is the node name: the tenant name, optionally with an `-auth` suffix to select the auth variant of the node (e.g. `"acme-auth"`). `getGeoConfig` returns `null` when the geo is not supported, in which case region-specific initialization should be skipped. For geos served by a dedicated cloud host rather than a regional edge host, `node` is `undefined` and the host's default node is used. -When using a script tag, the helper is available as `window.optable.utils.getGeoConfig`. - The default `GeoMap` supports the `US`, `CA`, `GB`, `UK` and `AU` geos and reflects a specific provisioning shape: regional edge nodes in US/CA and dedicated per-tenant cloud hosts (`.cloud..optable.co`) in AU and the EU. The dedicated hosts only exist for tenants provisioned that way — pass a custom `GeoMap` as the third argument when your topology differs; see `lib/addons/geotargeting.ts` for the entry format. ## Demo Pages diff --git a/browser/sdk.ts b/browser/sdk.ts index 17c3d27..d56266e 100644 --- a/browser/sdk.ts +++ b/browser/sdk.ts @@ -1,6 +1,5 @@ import Commands from "./commands"; import { resolveMultiNodeTargeting } from "../lib/core/resolvers/resolveMultiTargeting"; -import { getGeoConfig } from "../lib/addons/geotargeting"; import OptableSDK, { InitConfig } from "../lib/sdk"; import OptablePrebidAnalytics from "../lib/addons/prebid/analytics"; @@ -29,7 +28,7 @@ window.optable = window.optable || {}; window.optable.SDK = OptableSDK; window.optable.OptablePrebidAnalytics = OptablePrebidAnalytics; window.optable.cmd = new Commands(window.optable.cmd || []); -window.optable.utils = { resolveMultiNodeTargeting, getGeoConfig }; +window.optable.utils = { resolveMultiNodeTargeting }; if (window.optable.instance_config) { window.optable.instance = new OptableSDK(window.optable.instance_config);