Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. 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.

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 (`<name>.cloud.<region>.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

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.
Expand Down
94 changes: 94 additions & 0 deletions lib/addons/geotargeting.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 undefined node for AU", () => {
expect(getGeoConfig("acme", "AU")).toStrictEqual({
host: "acme.cloud.au.optable.co",
node: undefined,
});
});

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: undefined,
});
});

test("resolves GB and UK to the same EU cloud host", () => {
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", () => {
expect(getGeoConfig("acme", "FR")).toBeNull();
});

test("returns null for a missing geo", () => {
expect(getGeoConfig("acme", "")).toBeNull();
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",
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)).toStrictEqual({
host: "acme-auth.cloud.mx.optable.co",
node: undefined,
});
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"]);
});
});
72 changes: 72 additions & 0 deletions lib/addons/geotargeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 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];
export type GeoMap = Record<string, GeoMapEntry>;

export interface GeoConfig {
host: string;
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: EU_ENTRY,
UK: EU_ENTRY,
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 | 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;
}

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: undefined };
}
Loading