Skip to content
Open
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions lib/addons/sourceCheck.md
Original file line number Diff line number Diff line change
@@ -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://<host>/config?o=<site>&t=<node>&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.
69 changes: 69 additions & 0 deletions lib/addons/sourceCheck.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
66 changes: 66 additions & 0 deletions lib/addons/sourceCheck.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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 };