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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,21 @@ 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).

## Static mappings

The static mappings addon applies a wrapper's customer configuration defaults onto `window.optable`, so values a publisher sets on the page before the wrapper script loads always win. Plain-object config sections merge recursively; any other default only fills in a value the publisher left `null` or `undefined`.

```typescript
import { setStaticMappings } from "@optable/web-sdk/lib/dist/addons/staticMappings";

setStaticMappings({
defaultSite: "customer-sdk",
analytics: { tenant: "customer", sample: 0.1 },
});
```

For the merge rules and a full wrapper example, see the [static mappings addon README](lib/addons/staticMappings.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
40 changes: 40 additions & 0 deletions lib/addons/staticMappings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Static Mappings Addon

Applies a wrapper's customer configuration defaults onto `window.optable`, so values a publisher sets on the page before the wrapper script loads always win.

## Usage

The wrapper passes its customer defaults; the addon fills in whatever the publisher left unset:

```js
import { setStaticMappings } from "@optable/web-sdk/lib/dist/addons/staticMappings";

setStaticMappings({
defaultSite: "customer-sdk",
node: "customer-node",
analytics: {
tenant: "customer",
sample: 0.1,
pbjsObjectName: "pbjs",
},
withID5: true,
});

// Values that are unconditional or derived from other keys stay in the wrapper:
window.optable.wrapperVersion = SDK_WRAPPER_VERSION;
window.optable.site = window.optable.site ?? window.optable.defaultSite;
window.optable.analytics.pbjsObject =
window.optable.analytics.pbjsObject ?? window[window.optable.analytics.pbjsObjectName];
```

`window.optable` is created if the page did not define it.

## Merge rules

- When the existing value and the default are both plain objects, they merge recursively, so nested config like `analytics.*` defaults key by key.
- Everything else is a leaf: the default is assigned as-is, and only when the current value is `null` or `undefined`. An absent object default is therefore assigned by reference, not cloned.
- A publisher value that is not a plain object is never recursed into or replaced by an object default.

Object values that are not config sections — a prebid global, for instance — should be assigned caller-side (like `pbjsObject` above) rather than passed as defaults, so a publisher-supplied object is never merged with the default one.

A default therefore never overrides an explicit publisher value. A publisher setting `sample = 0` or an empty string keeps it, and `0`, `""` and `false` all survive.
65 changes: 65 additions & 0 deletions lib/addons/staticMappings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { setStaticMappings } from "./staticMappings";

const w = window as unknown as { optable?: any };

beforeEach(() => {
delete w.optable;
});

describe("setStaticMappings", () => {
it("creates w.optable and fills top-level and nested defaults", () => {
setStaticMappings({
site: "customer-sdk",
analytics: { tenant: "customer", sample: 0.1 },
});
expect(w.optable.site).toBe("customer-sdk");
expect(w.optable.analytics).toEqual({ tenant: "customer", sample: 0.1 });
});

it("leaves publisher-set values alone, including falsy ones", () => {
w.optable = {
site: "publisher-site",
withID5: false,
analytics: { sample: 0, tenant: "" },
};
setStaticMappings({
site: "customer-sdk",
withID5: true,
analytics: { sample: 0.1, tenant: "customer", pbjsObjectName: "pbjs" },
});
expect(w.optable.site).toBe("publisher-site");
expect(w.optable.withID5).toBe(false);
expect(w.optable.analytics.sample).toBe(0);
expect(w.optable.analytics.tenant).toBe("");
expect(w.optable.analytics.pbjsObjectName).toBe("pbjs");
});

it("replaces null values with defaults", () => {
w.optable = { node: null };
setStaticMappings({ node: "a" });
expect(w.optable.node).toBe("a");
});

it("treats arrays as leaves and assigns absent object defaults by reference", () => {
const pbjs = { que: [] };
w.optable = { prebidInstances: ["oajs"] };
setStaticMappings({
prebidInstances: ["pbjs"],
analytics: { pbjsObject: pbjs },
});
expect(w.optable.prebidInstances).toEqual(["oajs"]);
expect(w.optable.analytics.pbjsObject).toBe(pbjs);
});

it("does not recurse into a publisher value that is not a plain object", () => {
w.optable = { analytics: "off" };
setStaticMappings({ analytics: { tenant: "customer" } });
expect(w.optable.analytics).toBe("off");
});

it("replaces a non-object window.optable instead of throwing", () => {
w.optable = "clobbered";
setStaticMappings({ site: "customer-sdk" });
expect(w.optable).toEqual({ site: "customer-sdk" });
});
});
30 changes: 30 additions & 0 deletions lib/addons/staticMappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Applies a wrapper's config defaults onto window.optable, so publisher-set
// values always win. When both sides are plain objects they merge recursively,
// covering nested config like analytics.*. Anything else is assigned as-is,
// and only when the current value is null or undefined, so publisher overrides
// of 0, "" and false survive.
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
}

function applyDefaults(target: Record<string, unknown>, defaults: Record<string, unknown>): void {
for (const key of Object.keys(defaults)) {
const def = defaults[key];
const cur = target[key];
if (isPlainObject(def) && isPlainObject(cur)) {
applyDefaults(cur, def);
} else if (cur === null || cur === undefined) {
target[key] = def;
}
}
}

function setStaticMappings(defaults: Record<string, unknown>): void {
const w = window as unknown as { optable?: Record<string, unknown> };
if (typeof w.optable !== "object" || w.optable === null) {
w.optable = {};
}
applyDefaults(w.optable, defaults);
}

export { setStaticMappings };