-
Notifications
You must be signed in to change notification settings - Fork 2.3k
datamage rtd contextual provider: initial release #14485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
patmmccann
merged 20 commits into
prebid:master
from
Ops-Mage:rtd/datamage-contextual-provider
Mar 20, 2026
+824
−1
Merged
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 035b2a6
remove test.html files
leiforion 67456d8
update example.html
leiforion 5ab88c8
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion af7634a
siplify architecture, adding back in linting, improve load
leiforion 5b319d4
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion 923ca7e
fixed caching issues and bot comments from original pr
leiforion 427757c
fix issues identified by bot
leiforion 0345761
replace dep GPT api calls to modern format
leiforion fe0b255
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion 0334499
update btoa to support non typical url encodings, strip common tracki…
leiforion 6b35e9a
remove misc local test files
leiforion 80f2d75
fix fetchPromise transient failur potential
leiforion f255dae
reduce key value size and unusued keys in payload to gam and ortb
leiforion d383867
update Rtd spec file naming
leiforion 3d12e1a
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion 4ce2874
update prebid js path in example
leiforion 3c0a2fd
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion f04b940
Merge branch 'master' into rtd/datamage-contextual-provider
leiforion 4c7cd78
include datamageRtdprovider in submodules json
leiforion File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
213 changes: 213 additions & 0 deletions
213
integrationExamples/realTimeData/datamageRtdProvider_example.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.