Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e2812ea
Create datamage RTD integration
leiforion Feb 17, 2026
035b2a6
remove test.html files
leiforion Feb 18, 2026
67456d8
update example.html
leiforion Feb 18, 2026
5ab88c8
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Feb 18, 2026
af7634a
siplify architecture, adding back in linting, improve load
leiforion Feb 23, 2026
5b319d4
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Feb 23, 2026
923ca7e
fixed caching issues and bot comments from original pr
leiforion Feb 23, 2026
427757c
fix issues identified by bot
leiforion Feb 23, 2026
0345761
replace dep GPT api calls to modern format
leiforion Feb 27, 2026
fe0b255
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Feb 27, 2026
0334499
update btoa to support non typical url encodings, strip common tracki…
leiforion Feb 27, 2026
6b35e9a
remove misc local test files
leiforion Feb 27, 2026
80f2d75
fix fetchPromise transient failur potential
leiforion Feb 27, 2026
f255dae
reduce key value size and unusued keys in payload to gam and ortb
leiforion Feb 28, 2026
d383867
update Rtd spec file naming
leiforion Feb 28, 2026
3d12e1a
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Mar 9, 2026
4ce2874
update prebid js path in example
leiforion Mar 11, 2026
3c0a2fd
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Mar 11, 2026
f04b940
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion Mar 20, 2026
4c7cd78
include datamageRtdprovider in submodules json
leiforion Mar 20, 2026
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
213 changes: 213 additions & 0 deletions integrationExamples/realTimeData/datamageRtdProvider_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>OpsMage Prebid Test Page</title>

<!-- GPT -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous"></script>
<script>
window.googletag = window.googletag || { cmd: [] };
window.gptSlot = null;

googletag.cmd.push(function () {
googletag.pubads().disableInitialLoad();

// ✅ Save slot reference so we can refresh JUST this slot
window.gptSlot = googletag.defineSlot(
'/50536250/designcon',
[[300, 250]],
'div-gpt-ad-1771335974713-0'
).addService(googletag.pubads());

googletag.pubads().enableSingleRequest();
googletag.enableServices();

// ✅ Display once (no request yet because disableInitialLoad)
googletag.display('div-gpt-ad-1771335974713-0');
});

function dumpGptTargeting(label) {
googletag.cmd.push(function () {
const pubads = googletag.pubads();
const keys = pubads.getTargetingKeys();
const pubadsMap = {};
keys.forEach(k => { pubadsMap[k] = pubads.getTargeting(k); });

console.log(label, 'PUBADS targeting:', pubadsMap);

if (window.gptSlot && typeof window.gptSlot.getTargetingMap === 'function') {
console.log(label, 'SLOT targeting:', window.gptSlot.getTargetingMap());
}
});
}
</script>

<!-- Prebid -->
<script async src="build/dev/prebid.js"></script>

<script>
var pbjs = window.pbjs = window.pbjs || {};
pbjs.que = pbjs.que || [];

var PREBID_TIMEOUT = 1500;
var DATAMAGE_WAIT_MS = 1200; // bump a bit so dm arrives more often

var adUnits = [{
code: 'div-gpt-ad-1771335974713-0',
mediaTypes: { banner: { sizes: [[300, 250]] } },
bids: [{
bidder: 'appnexus',
params: { placementId: 1234567 }
}]
}];

function looksLikeQueryStringBlob(s) {
// Detect "om_x=y&om_a=b" style blobs (these cause %3D and %26 in the request)
return typeof s === 'string' && /(^|&)om_[^=]+=/.test(s);
}

function normalizeValues(values) {
if (values == null) return [];
const arr = Array.isArray(values) ? values : [values];
return arr
.map(v => (v == null ? '' : String(v)))
.map(v => v.trim())
.filter(v => v.length);
}

function applyDatamageToGpt(gptMap) {
if (!gptMap || !window.googletag) return;

googletag.cmd.push(function () {
const pubads = googletag.pubads();

Object.keys(gptMap).forEach(function (key) {
let values = gptMap[key];

// ✅ Guard: never allow a whole querystring to be set as a value
if (looksLikeQueryStringBlob(values)) {
console.warn('Refusing to set querystring blob as targeting value for', key, values);
return;
}

const outVals = normalizeValues(values);
if (!outVals.length) return;

pubads.setTargeting(key, outVals);
});

// Useful log
dumpGptTargeting('[after applyDatamageToGpt]');
});
}

function waitForDatamage(maxWaitMs) {
return new Promise(function (resolve) {
if (window.__DATAMAGE_GPT_TARGETING__) {
resolve(window.__DATAMAGE_GPT_TARGETING__);
return;
}

let done = false;
function finish(val) {
if (done) return;
done = true;
window.removeEventListener('datamage:gptTargeting', onEvt);
resolve(val || null);
}

function onEvt(e) {
finish(e && e.detail);
}

window.addEventListener('datamage:gptTargeting', onEvt);
setTimeout(function () { finish(null); }, maxWaitMs);
});
}

pbjs.que.push(function () {
pbjs.setConfig({
debug: true,

consentManagement: {
allowAuctionWithoutConsent: true,
gdpr: {
cmpApi: 'static',
timeout: 0,
consentData: {
getTCData: {
tcString: 'CPAa+2APAa+2AAOACBENC1CoAP_AAH_AAAAAAwwxgAAAAA',
gdprApplies: true,
purpose: { consents: { 1: true, 2: true, 3: true, 4: true, 5: true } },
vendor: { consents: { 1: true }, legitimateInterests: { 1: true } }
}
}
},
usp: { cmpApi: 'static', timeout: 0, consentData: { getUSPData: { uspString: '1---' } } }
},

allowActivities: {
accessDevice: { rules: [{ action: 'allow' }] },
enrichUfpd: { rules: [{ action: 'allow' }] },
enrichEids: { rules: [{ action: 'allow' }] },
transmitTid: { rules: [{ action: 'allow' }] }
},

realTimeData: {
auctionDelay: 500,
dataProviders: [{
name: "datamage",
params: {
api_key: '8328309832',
selector: 'article',
auction_timeout_ms: 0,
fetch_timeout_ms: 2500
}
}]
}
});

pbjs.addAdUnits(adUnits);

pbjs.requestBids({
bidsBackHandler: async function () {
// HB keys (if bids)
pbjs.setTargetingForGPTAsync();

// OpsMage keys (even if no bids)
var dm = await waitForDatamage(DATAMAGE_WAIT_MS);
console.log('DATAMAGE map:', dm);
if (dm) applyDatamageToGpt(dm);
// Dump before refresh (this is the truth source)
dumpGptTargeting('[before refresh]');

// ✅ Refresh only our slot (safer + easier to inspect)
googletag.cmd.push(function () {
if (window.gptSlot) {
googletag.pubads().refresh([window.gptSlot]);
} else {
// fallback
googletag.pubads().refresh();
}
});
},
timeout: PREBID_TIMEOUT
});
});
</script>
</head>

<body>
<h2>OpsMage Prebid Test Page</h2>
<div id="div-gpt-ad-1771335974713-0" style="width:300px; height:250px;"></div>
<article>The tech world is currently buzzing over the highly anticipated market debut of fakeDSP, a trailblazing
startup poised to redefine the landscape of digital signal processing. Leveraging proprietary neuromorphic
algorithms and quantum-ready architecture, fakeDSP promises to accelerate real-time data synthesis by speeds
previously thought impossible. With early analysts calling it a definitive disruptor in both the
telecommunications and audio-engineering sectors, the company’s entrance signifies a major leap forward in how
complex signals are analyzed and reconstructed in the AI era.</article>
</body>

</html>
79 changes: 79 additions & 0 deletions modules/datamageRrdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# DataMage RTD Submodule

DataMage provides contextual classification (IAB Categories, Sentiment, Brands, Locations, Public Figures, Restricted Categories, and related IDs) that can be used to enrich demand signals and Google Ad Manager targeting.

## What it does

DataMage supports two outcomes in a Prebid + GAM setup:

1) **Passes data to bidders (ORTB2 enrichment)**
- DataMage fetches classification for the current page/content.
- The results are inserted into the bid request using OpenRTB (ORTB2), so bidders can receive the contextual signal.

2) **Passes data to Google Ad Manager (direct GPT targeting)**
- DataMage publishes a targeting map on the page (`window.__DATAMAGE_GPT_TARGETING__`) and emits an event (`datamage:gptTargeting`).
- Your page then sets those key-values into GPT/GAM using `googletag.pubads().setTargeting(...)`.
- This works **even if there are no bids**, as long as GPT is refreshed after targeting is set.

## Keys provided

DataMage can provide the following keys (when available):

- `om_iab_cat_ids`, `om_iab_cats`
- `om_brand_ids`, `om_brands`
- `om_sentiment_ids`, `om_sentiment`
- `om_location_ids`, `om_locations`
- `om_public_figure_ids`, `om_public_figures`
- `om_restricted_cat_ids`, `om_restricted_cats`
- `om_ops_mage_data_id`
- `om_res_score_bucket`
- `om_res_score` (only when present)

> Publisher domain keys are not used.

## Integration

### 1) Build Prebid.js with DataMage
Include the module in your Prebid build:
```bash
gulp build --modules=datamageRtdProvider,...
```

### 2) Enable the RTD provider in Prebid config
Example:
```js
pbjs.setConfig({
realTimeData: {
auctionDelay: 500,
dataProviders: [{
name: "datamage",
params: {
api_key: "YOUR_API_KEY",
selector: "article",
auction_timeout_ms: 0,
fetch_timeout_ms: 2500
}
}]
}
});
```

### 3) GAM (GPT) setup requirements
To ensure DataMage key-values are included in the GAM request:

1. Call `googletag.pubads().disableInitialLoad()` before the ad request.
2. Define the slot and keep a reference to it.
3. Call `googletag.display()` once (no request yet because initial load is disabled).
4. Run `pbjs.requestBids(...)`.
5. After bids return:
- Call `pbjs.setTargetingForGPTAsync()` (for hb_* keys when bids exist).
- Wait for DataMage targeting (`window.__DATAMAGE_GPT_TARGETING__` or the `datamage:gptTargeting` event).
- Apply DataMage targeting via `googletag.pubads().setTargeting(...)`.
6. Call `googletag.pubads().refresh([slot])` to make the GAM request.

This sequence ensures:
- DataMage targeting reaches GAM
- ORTB2 enrichment reaches bidders
- DataMage targeting can still be applied even if there are no bids

Note: Datamage api URLs will cache for 5 minutes, so you may not see content return until the cache has cleared.
Loading