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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,32 @@ The Optable Web SDK provides a method `installGPTSecureSignals` to pass user-def

Please refer to the list of approved Secure Signal [providers](https://support.google.com/admanager/answer/14750072). Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal.

To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first EID that passes the filter and carries a non-empty uid wins its source — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field).

The signals come from the last cached targeting response, so a first visit installs none until `targeting()` has resolved once.

With the npm package:

```javascript
Comment thread
etiennelatendresse-optable marked this conversation as resolved.
import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals";

const eids = sdk.targetingFromCache()?.ortb2?.user?.eids ?? [];
sdk.installGPTSecureSignals(...secureSignalsFromEids(eids, { sources: ["uidapi.com"] }));
```

With the SDK loaded from a script tag, the helper is available under `optable.utils`:

```html
<script async src="https://cdn.optable.co/web-sdk/latest/sdk.js"></script>
<script>
window.optable = window.optable || { cmd: [] };
optable.cmd.push(function () {
const eids = optable.instance.targetingFromCache()?.ortb2?.user?.eids ?? [];
optable.instance.installGPTSecureSignals(...optable.utils.secureSignalsFromEids(eids, { sources: ["uidapi.com"] }));
});
</script>
```

You can verify the signal was correctly passed to GAM by searching for its value cached in `localStorage` under the key `_GESPSK-<provider_name>`.

## Integrating Prebid
Expand Down
3 changes: 2 additions & 1 deletion browser/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Commands from "./commands";
import { resolveMultiNodeTargeting } from "../lib/core/resolvers/resolveMultiTargeting";
import { secureSignalsFromEids } from "../lib/core/secure-signals";

import OptableSDK, { InitConfig } from "../lib/sdk";
import OptablePrebidAnalytics from "../lib/addons/prebid/analytics";
Expand Down Expand Up @@ -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, secureSignalsFromEids };

if (window.optable.instance_config) {
window.optable.instance = new OptableSDK(window.optable.instance_config);
Expand Down
5 changes: 3 additions & 2 deletions lib/addons/gpt.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { ContextualTargetingKeyValuesOptions } from "../edge/contextual_segments";
import type { WitnessProperties } from "../edge/witness";
import type { SecureSignal } from "../core/secure-signals";
import { debugLog } from "../core/log";
import OptableSDK from "../sdk";

declare module "../sdk" {
export interface OptableSDK {
installGPTEventListeners: () => void;
installGPTSecureSignals: () => void;
installGPTSecureSignals: (...signals: SecureSignal[]) => void;
}
}

Expand Down Expand Up @@ -86,7 +87,7 @@ OptableSDK.prototype.installGPTEventListeners = function (eventSpec?: GptEventSp
/*
* Pass user-defined signals to GAM Secure Signals
*/
OptableSDK.prototype.installGPTSecureSignals = function (...signals: Array<{ provider: string; id: string }>) {
OptableSDK.prototype.installGPTSecureSignals = function (...signals: SecureSignal[]) {
window.googletag = window.googletag || { cmd: [] };
const gpt = window.googletag;

Expand Down
66 changes: 66 additions & 0 deletions lib/core/secure-signals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { secureSignalsFromEids } from "./secure-signals";

const EIDS = [
{ source: "uidapi.com", matcher: "m1", inserter: "optable", uids: [{ id: "uid2-a" }, { id: "uid2-b" }] },
{ source: "id5-sync.com", matcher: "m2", uids: [{ id: "id5-a" }] },
{ source: "liveramp.com", uids: [] },
];

describe("secureSignalsFromEids", () => {
it("emits one signal per source, taking the first non-empty uid", () => {
expect(secureSignalsFromEids(EIDS)).toEqual([
{ provider: "uidapi.com", id: "uid2-a" },
{ provider: "id5-sync.com", id: "id5-a" },
]);
});

it("keeps the first EID when a source appears twice", () => {
const dup = [
{ source: "pair.optable.co", uids: [{ id: "pair-1" }] },
{ source: "pair.optable.co", uids: [{ id: "pair-2" }] },
];
expect(secureSignalsFromEids(dup)).toEqual([{ provider: "pair.optable.co", id: "pair-1" }]);
});

it("falls through to a later EID of the same source when the first is empty or filtered out", () => {
expect(
secureSignalsFromEids([
{ source: "s.com", uids: [{ id: "" }] },
{ source: "s.com", uids: [{ id: "second" }] },
])
).toEqual([{ provider: "s.com", id: "second" }]);
expect(
secureSignalsFromEids(
[
{ source: "s.com", inserter: "other", uids: [{ id: "first" }] },
{ source: "s.com", inserter: "optable", uids: [{ id: "second" }] },
],
{ inserters: ["optable"] }
)
).toEqual([{ provider: "s.com", id: "second" }]);
});

it("filters by source, inserter and matcher", () => {
expect(secureSignalsFromEids(EIDS, { sources: ["id5-sync.com"] })).toEqual([
{ provider: "id5-sync.com", id: "id5-a" },
]);
expect(secureSignalsFromEids(EIDS, { inserters: ["optable"] }).map((s) => s.provider)).toEqual(["uidapi.com"]);
expect(secureSignalsFromEids(EIDS, { matchers: ["m2"] })).toEqual([{ provider: "id5-sync.com", id: "id5-a" }]);
});

it("requires every provided filter to match", () => {
expect(secureSignalsFromEids(EIDS, { sources: ["uidapi.com"], matchers: ["m2"] })).toEqual([]);
});

it("treats an empty filter list as no constraint", () => {
expect(secureSignalsFromEids(EIDS, { sources: [] })).toHaveLength(2);
});

it("skips uids without an id and tolerates empty input", () => {
expect(secureSignalsFromEids([{ source: "a.com", uids: [{ id: "" }, {}] }])).toEqual([]);
expect(secureSignalsFromEids([{ source: "a.com", uids: [{ id: "" }, { id: "real" }] }])).toEqual([
{ provider: "a.com", id: "real" },
]);
expect(secureSignalsFromEids([])).toEqual([]);
});
});
41 changes: 41 additions & 0 deletions lib/core/secure-signals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { EID, UID } from "iab-openrtb/v26";

// Converts cached EIDs into GPT secure-signal pairs for
// installGPTSecureSignals, optionally filtered by source, inserter or
// matcher. An empty or missing filter list means no constraint on that field.
//
// One signal per source: GPT keys secure signals by provider, so registering
// a provider twice drops all but one id. The first EID that passes the filter
// and carries a non-empty uid wins its source.

// Cached EIDs are parsed from storage, so ORTB's required fields may be
// absent at runtime. Derived from EID so new or renamed ORTB fields flow through.
type SecureSignalEid = Partial<Omit<EID, "uids">> & Pick<EID, "source"> & { uids?: Array<Pick<Partial<UID>, "id">> };

type SecureSignalsFilter = {
sources?: string[];
inserters?: string[];
matchers?: string[];
};

type SecureSignal = { provider: string; id: string };

export function secureSignalsFromEids(eids: SecureSignalEid[], filter: SecureSignalsFilter = {}): SecureSignal[] {
const allows = (list: string[] | undefined, value: string | undefined) => !list?.length || list.includes(value ?? "");

const bySource = new Map<string, SecureSignal>();
eids.forEach((eid) => {
if (bySource.has(eid.source)) return;
if (!allows(filter.sources, eid.source)) return;
if (!allows(filter.inserters, eid.inserter)) return;
if (!allows(filter.matchers, eid.matcher)) return;

const uid = eid.uids?.find((u) => u.id);
if (uid?.id) {
bySource.set(eid.source, { provider: eid.source, id: uid.id });
}
});
return [...bySource.values()];
}

export type { SecureSignalEid, SecureSignalsFilter, SecureSignal };
Comment thread
etiennelatendresse-optable marked this conversation as resolved.