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
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,30 +426,46 @@ The response has the shape:
type ContextualSegmentsResponse = {
classifications: {
categories: { id: string; name: string; score: number; taxonomy: string }[];
keywords: { keyword: string; prominence: number }[];
};
};
```

`classifications` groups results by classification method; the DCN populates only the methods it has enabled. `categories` are taxonomy classifications (each carrying its own `taxonomy`); `keywords` are free-form terms extracted from the page. A category's `score` is a relevance score from 0 to 1, whereas a keyword's `prominence` is a per-page ordinal rank (1 = most prominent), not a comparable score.

Each call to `ctxSegments()` caches its response on the SDK instance (calling it again refreshes the cache). When `initContextual: true`, the SDK calls `ctxSegments()` for you during initialization, so the cache is populated automatically.

> **Note:** The requested URL must already have been classified by the DCN. If the DCN has no classification for the URL, the response will contain an empty `categories` array.
> **Note:** The requested URL must already have been classified by the DCN. If the DCN has no classification for the URL, the response will contain empty `categories` and `keywords` arrays.

#### Contextual targeting key-values

`ctxTargetingKeyValues(taxonomyKeys?)` reads the cached `ctxSegments()` response and builds a `Record<string, string[]>` of category ids grouped by taxonomy, ready to pass to an ad server such as Google Ad Manager via `googletag.pubads().setTargeting()`.

Without arguments, each taxonomy value is used as the key:
`ctxTargetingKeyValues(taxonomyKeys?, options?)` reads the cached `ctxSegments()` response and builds a `Record<string, string[]>`, ready to pass to an ad server such as Google Ad Manager via `googletag.pubads().setTargeting()`. It emits category ids grouped by taxonomy, plus, by default, the page's keywords under the key `ctx_kw`:

```javascript
sdk.ctxTargetingKeyValues();
// => { "iab_ct_3_1": ["53", "91", "58", "115", "90", "52"] }
// => {
// "iab_ct_3_1": ["53", "91", "58", "115", "90", "52"],
// "ctx_kw": ["advertising", "programmatic", "ad tech"]
// }
```

Pass a `taxonomyKeys` map to rename keys. Only taxonomies present in the map are emitted (filter + rename), which is useful when you only want to set keys you have configured in your ad server:
Pass a `taxonomyKeys` map to rename category keys. Only taxonomies present in the map are emitted (filter + rename), which is useful when you only want to set keys you have configured in your ad server:

```javascript
sdk.ctxTargetingKeyValues({ iab_ct_3_1: "foo" });
// => { "foo": ["53", "91", "58", "115", "90", "52"] }
// => { "foo": ["53", "91", "58", "115", "90", "52"], "ctx_kw": ["advertising", "programmatic", "ad tech"] }
```

Keyword values are ordered by `prominence` (most prominent first), capped to the top 10, and sanitized to GAM's value rules (lowercased, [reserved characters](https://support.google.com/admanager/answer/10020177) stripped, truncated to the 40-character value limit). Use the `options` argument to change the keyword key (`keywordKey`), change the cap (`maxKeywords`), or opt out of keyword key-values by passing an empty `keywordKey`:

```javascript
// Rename the keyword key and emit only the top 5 keywords:
sdk.ctxTargetingKeyValues({ iab_ct_3_1: "foo" }, { keywordKey: "kw", maxKeywords: 5 });
// => { "foo": ["53", ...], "kw": ["advertising", "programmatic", "ad tech", "marketing", "audience targeting"] }

// Opt out of keyword key-values entirely:
sdk.ctxTargetingKeyValues(undefined, { keywordKey: "" });
// => { "iab_ct_3_1": ["53", "91", "58", "115", "90", "52"] }
```

A typical Google Ad Manager activation uses a `loadGAM()` helper:
Expand Down
71 changes: 65 additions & 6 deletions demos/vanilla/nocookies/targeting/ctx_segments.html.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,58 @@
<div class="twelve column">
<h4>Example: contextual segments API</h4>
<p>
Shows how to call <code>ctxSegments()</code> to classify a page URL against one or more contextual
taxonomies (e.g. the
<a href="https://iabtechlab.com/standards/content-taxonomy/">IAB Content Taxonomy</a>) and inspect the
categories the DCN returns for it.
Shows how to call <code>ctxSegments()</code> to classify a page URL and inspect the classifications the DCN
returns for it: taxonomy categories (e.g. against the
<a href="https://iabtechlab.com/standards/content-taxonomy/">IAB Content Taxonomy</a>) and/or free-form
keywords, depending on which classifiers the DCN has enabled.
</p>
<pre><code>// Classify the URL of the current page (defaults to window.location.href):
optable.instance.ctxSegments();

// Or classify an explicit URL:
optable.instance.ctxSegments("https://optable.co/");</code></pre>
<p>The response is a <code>ContextualSegmentsResponse</code>:</p>
<pre><code>{
classifications: {
categories: [{ id, name, score, taxonomy }],
keywords: [{ keyword, prominence }],
},
}</code></pre>
<p>
The response is a <code>ContextualSegmentsResponse</code> of the form
<code>{ classifications: { categories: [{ id, name, score, taxonomy }] } }</code>.
The <code>classifications</code> object groups results by classification method, and the DCN includes only
the methods it has enabled. Two methods exist today:
</p>
<h6><code>categories</code>: taxonomy classifications</h6>
<table class="u-full-width">
<thead>
<tr><th>Field</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>id</code></td><td>Category id within its taxonomy (e.g. an IAB category id).</td></tr>
<tr><td><code>name</code></td><td>Human-readable category name.</td></tr>
<tr><td><code>score</code></td><td>Relevance score from 0 to 1.</td></tr>
<tr>
<td><code>taxonomy</code></td>
<td>Id of the taxonomy the category belongs to (e.g. <code>iab_ct_3_1</code>).</td>
</tr>
</tbody>
</table>
<h6><code>keywords</code>: free-form terms extracted from the page</h6>
<table class="u-full-width">
<thead>
<tr><th>Field</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>keyword</code></td><td>The extracted keyword text.</td></tr>
<tr>
<td><code>prominence</code></td>
<td>
Per-page ordinal rank (1 = most prominent), not a score, so prominences are not comparable across
pages.
</td>
</tr>
</tbody>
</table>
<p>
Alternatively, configure the SDK with <code>initContextual</code> set to a callback. The SDK will
automatically call <code>ctxSegments()</code> for the URL of the current page on initialization, and
Expand Down Expand Up @@ -161,6 +199,15 @@ optable.instance.ctxSegments("https://optable.co/");</code></pre>
runs:
</p>
<pre><code>loadGAM(optable.instance.ctxTargetingKeyValues());</code></pre>
<p>
By default the returned map has one key per taxonomy the DCN classified into (keyed by the raw taxonomy
value) plus the page's keywords under <code>ctx_kw</code>. For example,
<code>ctxTargetingKeyValues()</code> might return:
</p>
<pre><code>{
"iab_ct_3_1": ["53", "91", "58", "115", "90", "52"],
"ctx_kw": ["advertising", "programmatic", "ad tech"]
}</code></pre>
<p>
If you want <code>loadGAM()</code> to run as soon as the contextual segments arrive — without making a
second <code>ctxSegments()</code> call — pass a callback to <code>initContextual</code>. The SDK fires the
Expand Down Expand Up @@ -196,6 +243,18 @@ optable.instance.ctxSegments("https://optable.co/");</code></pre>
</p>
<pre><code>// Emit only the "iab_ct_3_1" taxonomy, under the GAM key "ctx_iab":
loadGAM(optable.instance.ctxTargetingKeyValues({ iab_ct_3_1: "ctx_iab" }));</code></pre>
<p>
Keyword classifications are also emitted, by default under the GAM key <code>ctx_kw</code>. The values are
the page's keywords ordered by <code>prominence</code> (most prominent first), capped to the top 10, and
sanitized to GAM's value rules (lowercased, reserved characters stripped, truncated to 40 characters). Pass
<code>keywordKey</code> to rename the key or <code>maxKeywords</code> to change the cap, or set
<code>keywordKey</code> to an empty string to opt out of keyword key-values entirely:
</p>
<pre><code>// Rename the keyword key and emit only the top 5 keywords:
loadGAM(optable.instance.ctxTargetingKeyValues({ iab_ct_3_1: "ctx_iab" }, { keywordKey: "kw", maxKeywords: 5 }));

// Opt out of keyword key-values:
loadGAM(optable.instance.ctxTargetingKeyValues(undefined, { keywordKey: "" }));</code></pre>
<div class="twelve column code-result" id="kv-result">—</div>
</div>
</div>
Expand Down
71 changes: 65 additions & 6 deletions demos/vanilla/targeting/ctx_segments.html.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,58 @@
<div class="twelve column">
<h4>Example: contextual segments API</h4>
<p>
Shows how to call <code>ctxSegments()</code> to classify a page URL against one or more contextual
taxonomies (e.g. the
<a href="https://iabtechlab.com/standards/content-taxonomy/">IAB Content Taxonomy</a>) and inspect the
categories the DCN returns for it.
Shows how to call <code>ctxSegments()</code> to classify a page URL and inspect the classifications the DCN
returns for it: taxonomy categories (e.g. against the
<a href="https://iabtechlab.com/standards/content-taxonomy/">IAB Content Taxonomy</a>) and/or free-form
keywords, depending on which classifiers the DCN has enabled.
</p>
<pre><code>// Classify the URL of the current page (defaults to window.location.href):
optable.instance.ctxSegments();

// Or classify an explicit URL:
optable.instance.ctxSegments("https://optable.co/");</code></pre>
<p>The response is a <code>ContextualSegmentsResponse</code>:</p>
<pre><code>{
classifications: {
categories: [{ id, name, score, taxonomy }],
keywords: [{ keyword, prominence }],
},
}</code></pre>
<p>
The response is a <code>ContextualSegmentsResponse</code> of the form
<code>{ classifications: { categories: [{ id, name, score, taxonomy }] } }</code>.
The <code>classifications</code> object groups results by classification method, and the DCN includes only
the methods it has enabled. Two methods exist today:
</p>
<h6><code>categories</code>: taxonomy classifications</h6>
<table class="u-full-width">
<thead>
<tr><th>Field</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>id</code></td><td>Category id within its taxonomy (e.g. an IAB category id).</td></tr>
<tr><td><code>name</code></td><td>Human-readable category name.</td></tr>
<tr><td><code>score</code></td><td>Relevance score from 0 to 1.</td></tr>
<tr>
<td><code>taxonomy</code></td>
<td>Id of the taxonomy the category belongs to (e.g. <code>iab_ct_3_1</code>).</td>
</tr>
</tbody>
</table>
<h6><code>keywords</code>: free-form terms extracted from the page</h6>
<table class="u-full-width">
<thead>
<tr><th>Field</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>keyword</code></td><td>The extracted keyword text.</td></tr>
<tr>
<td><code>prominence</code></td>
<td>
Per-page ordinal rank (1 = most prominent), not a score, so prominences are not comparable across
pages.
</td>
</tr>
</tbody>
</table>
<p>
Alternatively, configure the SDK with <code>initContextual</code> set to a callback. The SDK will
automatically call <code>ctxSegments()</code> for the URL of the current page on initialization, and
Expand Down Expand Up @@ -159,6 +197,15 @@ optable.instance.ctxSegments("https://optable.co/");</code></pre>
runs:
</p>
<pre><code>loadGAM(optable.instance.ctxTargetingKeyValues());</code></pre>
<p>
By default the returned map has one key per taxonomy the DCN classified into (keyed by the raw taxonomy
value) plus the page's keywords under <code>ctx_kw</code>. For example,
<code>ctxTargetingKeyValues()</code> might return:
</p>
<pre><code>{
"iab_ct_3_1": ["53", "91", "58", "115", "90", "52"],
"ctx_kw": ["advertising", "programmatic", "ad tech"]
}</code></pre>
<p>
If you want <code>loadGAM()</code> to run as soon as the contextual segments arrive — without making a
second <code>ctxSegments()</code> call — pass a callback to <code>initContextual</code>. The SDK fires the
Expand Down Expand Up @@ -193,6 +240,18 @@ optable.instance.ctxSegments("https://optable.co/");</code></pre>
</p>
<pre><code>// Emit only the "iab_ct_3_1" taxonomy, under the GAM key "ctx_iab":
loadGAM(optable.instance.ctxTargetingKeyValues({ iab_ct_3_1: "ctx_iab" }));</code></pre>
<p>
Keyword classifications are also emitted, by default under the GAM key <code>ctx_kw</code>. The values are
the page's keywords ordered by <code>prominence</code> (most prominent first), capped to the top 10, and
sanitized to GAM's value rules (lowercased, reserved characters stripped, truncated to 40 characters). Pass
<code>keywordKey</code> to rename the key or <code>maxKeywords</code> to change the cap, or set
<code>keywordKey</code> to an empty string to opt out of keyword key-values entirely:
</p>
<pre><code>// Rename the keyword key and emit only the top 5 keywords:
loadGAM(optable.instance.ctxTargetingKeyValues({ iab_ct_3_1: "ctx_iab" }, { keywordKey: "kw", maxKeywords: 5 }));

// Opt out of keyword key-values:
loadGAM(optable.instance.ctxTargetingKeyValues(undefined, { keywordKey: "" }));</code></pre>
<div class="twelve column code-result" id="kv-result">—</div>
</div>
</div>
Expand Down
119 changes: 119 additions & 0 deletions lib/edge/contextual_segments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { ContextualTargetingKeyValues } from "./contextual_segments";
import type { ContextualSegmentsResponse } from "./contextual_segments";

// Helper to build a response with the given categories/keywords. Accepts a
// partial so tests can model responses where the DCN omitted a key entirely.
function response(classifications: Partial<ContextualSegmentsResponse["classifications"]>): ContextualSegmentsResponse {
return { classifications: classifications as ContextualSegmentsResponse["classifications"] };
}

describe("ContextualTargetingKeyValues keyword emission", () => {
const withKeywords = response({
categories: [{ id: "53", name: "Business", score: 0.9, taxonomy: "iab_ct_3_1" }],
keywords: [
{ keyword: "startup", prominence: 2 },
{ keyword: "earnings", prominence: 1 },
{ keyword: "entrepreneur", prominence: 3 },
],
});

test("emits keywords by default under ctx_kw, sorted by prominence (1 = most prominent first)", () => {
expect(ContextualTargetingKeyValues(withKeywords)).toEqual({
iab_ct_3_1: ["53"],
ctx_kw: ["earnings", "startup", "entrepreneur"],
});
// Renaming taxonomy keys does not affect the default keyword key.
expect(ContextualTargetingKeyValues(withKeywords, { iab_ct_3_1: "ctx_iab" })).toEqual({
ctx_iab: ["53"],
ctx_kw: ["earnings", "startup", "entrepreneur"],
});
});

test("emits keywords under a caller-provided key when keywordKey is set", () => {
expect(ContextualTargetingKeyValues(withKeywords, undefined, { keywordKey: "kw" })).toEqual({
iab_ct_3_1: ["53"],
kw: ["earnings", "startup", "entrepreneur"],
});
});

test("opts out of keyword emission when keywordKey is an empty string", () => {
expect(ContextualTargetingKeyValues(withKeywords, { iab_ct_3_1: "ctx_iab" }, { keywordKey: "" })).toEqual({
ctx_iab: ["53"],
});
});

test("caps to maxKeywords by prominence", () => {
expect(ContextualTargetingKeyValues(withKeywords, undefined, { keywordKey: "ctx_kw", maxKeywords: 2 })).toEqual({
iab_ct_3_1: ["53"],
ctx_kw: ["earnings", "startup"],
});
});

test("defaults to the top 10 keywords when maxKeywords is not given", () => {
const many = response({
categories: [],
keywords: Array.from({ length: 15 }, (_, i) => ({ keyword: `kw${i}`, prominence: i + 1 })),
});
const result = ContextualTargetingKeyValues(many, undefined, { keywordKey: "ctx_kw" });
expect(result.ctx_kw).toHaveLength(10);
expect(result.ctx_kw).toEqual(["kw0", "kw1", "kw2", "kw3", "kw4", "kw5", "kw6", "kw7", "kw8", "kw9"]);
});

test("sanitizes values to GAM rules: lowercases, strips disallowed chars, truncates to 40, drops empties", () => {
const dirty = response({
categories: [],
keywords: [
{ keyword: "Q3 Earnings", prominence: 1 }, // uppercase + space (space allowed)
{ keyword: "R&D,budgets", prominence: 2 }, // & and , are disallowed
{ keyword: "!!!", prominence: 3 }, // becomes empty -> dropped
{ keyword: "a".repeat(50), prominence: 4 }, // too long -> truncated to 40
],
});
const result = ContextualTargetingKeyValues(dirty, undefined, { keywordKey: "ctx_kw" });
expect(result.ctx_kw).toEqual(["q3 earnings", "rdbudgets", "a".repeat(40)]);
});

test("dedupes case-insensitively, preserving prominence order", () => {
const dupes = response({
categories: [],
keywords: [
{ keyword: "B2B", prominence: 1 },
{ keyword: "b2b", prominence: 2 },
{ keyword: "pricing", prominence: 3 },
],
});
expect(ContextualTargetingKeyValues(dupes, undefined, { keywordKey: "ctx_kw" }).ctx_kw).toEqual(["b2b", "pricing"]);
});

test("omits the keyword key entirely when there are no usable keywords", () => {
expect(ContextualTargetingKeyValues(response({ categories: [] }), undefined, { keywordKey: "ctx_kw" })).toEqual({});
expect(
ContextualTargetingKeyValues(response({ categories: [], keywords: [] }), undefined, { keywordKey: "ctx_kw" })
).toEqual({});
expect(
ContextualTargetingKeyValues(
response({ categories: [], keywords: [{ keyword: "***", prominence: 1 }] }),
undefined,
{
keywordKey: "ctx_kw",
}
)
).toEqual({});
});

test("orders keywords with missing/invalid prominence last", () => {
const mixed = response({
categories: [],
keywords: [
{ keyword: "second", prominence: 5 },
{ keyword: "last", prominence: undefined as unknown as number },
{ keyword: "first", prominence: 1 },
],
});
expect(ContextualTargetingKeyValues(mixed, undefined, { keywordKey: "ctx_kw" }).ctx_kw).toEqual([
"first",
"second",
"last",
]);
});
});
Loading
Loading