diff --git a/integrationExamples/gpt/optableRtdProvider_example.html b/integrationExamples/gpt/optableRtdProvider_example.html
index 5e1c8a77cb9..1074b84996e 100644
--- a/integrationExamples/gpt/optableRtdProvider_example.html
+++ b/integrationExamples/gpt/optableRtdProvider_example.html
@@ -14,7 +14,7 @@
font-size: 1.5em;
line-height: 1.6;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
+ margin: 0 0 32px;
}
.container {
@@ -36,6 +36,27 @@
letter-spacing: -0.5px;
}
+ h2 {
+ font-size: 24px;
+ line-height: 1.35;
+ font-weight: 100;
+ letter-spacing: -0.5px;
+ }
+
+ p {
+ font-size: 15px;
+ line-height: 24px;
+ font-weight: 400;
+ /*letter-spacing: -0.5px;*/
+ }
+
+ ul > li {
+ font-size: 15px;
+ line-height: 24px;
+ font-weight: 400;
+ /*letter-spacing: -0.5px;*/
+ }
+
a {
color: #1eaedb;
text-decoration: underline;
@@ -47,8 +68,8 @@
}
}
- #enriched-optable-data {
- font-size: 12px;
+ pre {
+ font-size: 13px;
max-width: 100%;
overflow-x: scroll;
padding: 16px;
@@ -93,9 +114,9 @@
},
},
bids: [{
- bidder: 'appnexus',
+ bidder: 'pubmatic',
params: {
- placementId: 13232392,
+ publisherId: "156209",
}
}]
},
@@ -107,9 +128,9 @@
},
},
bids: [{
- bidder: 'appnexus',
+ bidder: 'pubmatic',
params: {
- placementId: 13232392,
+ publisherId: "156209",
}
}]
},
@@ -121,29 +142,23 @@
},
},
bids: [{
- bidder: 'appnexus',
+ bidder: 'pubmatic',
params: {
- placementId: 13232392,
+ publisherId: "156209",
}
}]
},
];
pbjs.setConfig({
- optableRtdConfig: { // optional, check the doc for explanation
- email: 'email-sha256-hash',
- phone: 'phone-sha256-hash',
- postal_code: 'postal_code',
- },
debug: true, // use only for testing, remove in production
realTimeData: {
- auctionDelay: 1000, // should be set lower in production use
+ auctionDelay: 50,
dataProviders: [
{
name: 'optable',
waitForIt: true,
params: {
- // bundleUrl: "https://prebidtest.solutions.cdn.optable.co/public-assets/prebidtest-sdk.js?hello=world",
// adserverTargeting: false,
// handleRtd: async (reqBidsConfigObj, optableExtraData, mergeFn) => {
// const optableBundle = /** @type {Object} */ (window.optable);
@@ -176,7 +191,10 @@
try {
window.optable.cmd.push(() => {
document.getElementById('enriched-optable').style.display = 'block';
- document.getElementById('enriched-optable-data').textContent = JSON.stringify(data.ortb2.user, null, 2);
+ const jsonContent = JSON.stringify(data.ortb2.user, null, 2);
+ if (jsonContent) {
+ document.getElementById('enriched-optable-data').textContent = jsonContent;
+ }
});
} catch (e) {
console.error('Exception while trying to display enriched data', e);
@@ -236,10 +254,54 @@
-
Optable RTD module example
+
Prebid.js Integration with OptableRTD
+
Overview
+
+ This demo demonstrates how to integrate Optable's targeting capabilities with Prebid.js using the
+ OptableRTD module.
+ The implementation assumes Google Ad Manager (GAM) as the primary ad server, integrated via
+ Google Publisher Tag (GPT).
+
+
Key Features
+
+ - Loads active visitor cohorts and passes them to Prebid.js via OptableRTD
+ - Automatically forwards matching cohorts to GAM for targeting
+ - Supports local cache updates for improved targeting accuracy
+ - Configurable GAM targeting override via localStorage
+
+
Implementation
+
+ The Prebid.js configuration requires minimal setup. Here's the essential configuration:
+
+
pbjs.mergeConfig({
+ debug: true, // use only for testing, remove in production
+ priceGranularity: "low",
+ userSync: {
+ iframeEnabled: true,
+ enabledBidders: ["pubmatic"]
+ },
+ realTimeData: {
+ auctionDelay: 50,
+ dataProviders: [{
+ name: 'optable',
+ waitForIt: true // Required to respect auctionDelay
+ }]
+ }
+});
+
Important Notes
+
+ -
+ Targeting Cache: For optimal targeting accuracy, call
targeting
+ on page load to update the local cache, as the RTD module only uses cached values.
+
+ -
+ GAM Targeting Override: Set
localStorage.disableGamTargeting = "true"
+ before page load to prevent the SDK from sending targeting data to GAM.
+
+
- web-sdk-demo-gam360/header-ad
+ web-sdk-demo-gam360/header-ad
- web-sdk-demo-gam360/box-ad
+ web-sdk-demo-gam360/box-ad
- web-sdk-demo-gam360/footer-ad
+ web-sdk-demo-gam360/footer-ad
No response
```
-In this case bundleUrl parameter is not needed and the script will await bundle loading before delegating to it.
-
### Configuration
-This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to 1000 ms and make sure `waitForIt` is set to `true` for the `Optable` RTD provider.
+This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to at least 50 ms and make sure `waitForIt` is set to `true` for the `Optable` RTD provider.
```javascript
pbjs.setConfig({
debug: true, // we recommend turning this on for testing as it adds more logging
realTimeData: {
- auctionDelay: 1000,
+ auctionDelay: 50,
dataProviders: [
{
name: 'optable',
waitForIt: true, // should be true, otherwise the auctionDelay will be ignored
params: {
- bundleUrl: '
',
adserverTargeting: '',
},
},
@@ -55,48 +52,13 @@ pbjs.setConfig({
});
```
-### Additional input to the module
-
-Optable bundle may use PPIDs (publisher provided IDs) from the `user.ext.eids` as input.
-
-In addition, other arbitrary keys can be used as input, f.e. the following:
-
-- `optableRtdConfig.email` - a SHA256-hashed user email
-- `optableRtdConfig.phone` - a SHA256-hashed [E.164 normalized phone](https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization) (meaning a phone number consisting of digits and leading plus sign without spaces or any additional characters, f.e. a US number would be: `+12345678999`)
-- `optableRtdConfig.postal_code` - a ZIP postal code string
-
-Each of these identifiers is completely optional and can be provided through `pbjs.mergeConfig(...)` like so:
-
-```javascript
-pbjs.mergeConfig({
- optableRtdConfig: {
- email: await sha256("test@example.com"),
- phone: await sha256("12345678999"),
- postal_code: "61054"
- }
-})
-```
-
-Where `sha256` function can be defined as:
-
-```javascript
-async function sha256(input) {
- return [...new Uint8Array(
- await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))
- )].map(b => b.toString(16).padStart(2, "0")).join("");
-}
-```
-
-To handle PPIDs and the above input - a custom `handleRtd` function may need to be provided.
-
### Parameters
| Name | Type | Description | Default | Notes |
|--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------|
| name | String | Real time data module name | Always `optable` | |
-| waitForIt | Boolean | Should be set `true` together with `auctionDelay: 1000` | `false` | |
+| waitForIt | Boolean | Should be set `true` together with `auctionDelay: 50` | `false` | |
| params | Object | | | |
-| params.bundleUrl | String | Optable bundle URL | `null` | Optional |
| params.adserverTargeting | Boolean | If set to `true`, targeting keywords will be passed to the ad server upon auction completion | `true` | Optional |
| params.handleRtd | Function | An optional function that uses Optable data to enrich `reqBidsConfigObj` with the real-time data. If not provided, the module will do a default call to Optable bundle. The function signature is `[async] (reqBidsConfigObj, optableExtraData, mergeFn) => {}` | `null` | Optional |
@@ -125,7 +87,7 @@ A `handleRtd` function implementation has access to its surrounding context incl
If you want to see an example of how the optable RTD module works, run the following command:
```bash
-gulp serve --modules=optableRtdProvider,consentManagementGpp,consentManagementTcf,appnexusBidAdapter
+gulp serve --modules=optableRtdProvider,pubmaticBidAdapter
```
and then open the following URL in your browser:
@@ -138,4 +100,4 @@ Open the browser console to see the logs.
Any suggestions or questions can be directed to [prebid@optable.co](mailto:prebid@optable.co).
-Alternatively please open a new [issue](https://github.com/prebid/prebid-server-java/issues/new) or [pull request](https://github.com/prebid/prebid-server-java/pulls) in this repository.
+Alternatively, please open a new [issue](https://github.com/prebid/Prebid.js/issues/new) or [pull request](https://github.com/prebid/Prebid.js/pulls) in this repository.
diff --git a/src/adloader.js b/src/adloader.js
index a170af11f68..5e5932f99ea 100644
--- a/src/adloader.js
+++ b/src/adloader.js
@@ -36,7 +36,6 @@ const _approvedLoadExternalJSList = [
'wurfl',
'nodalsAi',
'anonymised',
- 'optable',
// UserId Submodules
'justtag',
'tncId',
diff --git a/test/spec/modules/optableRtdProvider_spec.js b/test/spec/modules/optableRtdProvider_spec.js
index 7aa4be3c8b2..536766eb393 100644
--- a/test/spec/modules/optableRtdProvider_spec.js
+++ b/test/spec/modules/optableRtdProvider_spec.js
@@ -1,7 +1,6 @@
import {
parseConfig,
defaultHandleRtd,
- mergeOptableData,
getBidRequestData,
getTargetingData,
optableSubmodule,
@@ -12,42 +11,22 @@ describe('Optable RTD Submodule', function () {
it('parses valid config correctly', function () {
const config = {
params: {
- bundleUrl: 'https://cdn.optable.co/bundle.js',
adserverTargeting: true,
handleRtd: () => {}
}
};
expect(parseConfig(config)).to.deep.equal({
- bundleUrl: 'https://cdn.optable.co/bundle.js',
adserverTargeting: true,
handleRtd: config.params.handleRtd,
});
});
- it('trims bundleUrl if it contains extra spaces', function () {
- const config = {params: {bundleUrl: ' https://cdn.optable.co/bundle.js '}};
- expect(parseConfig(config).bundleUrl).to.equal('https://cdn.optable.co/bundle.js');
- });
-
- it('throws an error for invalid bundleUrl format', function () {
- expect(() => parseConfig({params: {bundleUrl: 'invalidURL'}})).to.throw();
- expect(() => parseConfig({params: {bundleUrl: 'www.invalid.com'}})).to.throw();
- });
-
- it('throws an error for non-HTTPS bundleUrl', function () {
- expect(() => parseConfig({params: {bundleUrl: 'http://cdn.optable.co/bundle.js'}})).to.throw();
- expect(() => parseConfig({params: {bundleUrl: '//cdn.optable.co/bundle.js'}})).to.throw();
- expect(() => parseConfig({params: {bundleUrl: '/bundle.js'}})).to.throw();
- });
-
it('defaults adserverTargeting to true if missing', function () {
- expect(parseConfig(
- {params: {bundleUrl: 'https://cdn.optable.co/bundle.js'}}
- ).adserverTargeting).to.be.true;
+ expect(parseConfig({ params: {} }).adserverTargeting).to.be.true;
});
it('throws an error if handleRtd is not a function', function () {
- expect(() => parseConfig({params: {handleRtd: 'notAFunction'}})).to.throw();
+ expect(() => parseConfig({ params: { handleRtd: 'notAFunction' } })).to.throw();
});
});
@@ -56,10 +35,10 @@ describe('Optable RTD Submodule', function () {
beforeEach(() => {
sandbox = sinon.createSandbox();
- reqBidsConfigObj = {ortb2Fragments: {global: {}}};
+ reqBidsConfigObj = { ortb2Fragments: { global: {} } };
mergeFn = sinon.spy();
window.optable = {
- instance: {
+ rtd: {
targeting: sandbox.stub(),
targetingFromCache: sandbox.stub(),
},
@@ -71,63 +50,38 @@ describe('Optable RTD Submodule', function () {
});
it('merges valid targeting data into the global ORTB2 object', async function () {
- const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}};
- window.optable.instance.targetingFromCache.returns(targetingData);
- window.optable.instance.targeting.resolves(targetingData);
+ const targetingData = { ortb2: { user: { ext: { optable: 'testData' } } } };
+ window.optable.rtd.targetingFromCache.returns(targetingData);
+ window.optable.rtd.targeting.resolves(targetingData);
await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn);
expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true;
});
it('does nothing if targeting data is missing the ortb2 property', async function () {
- window.optable.instance.targetingFromCache.returns({});
- window.optable.instance.targeting.resolves({});
+ window.optable.rtd.targetingFromCache.returns({});
+ window.optable.rtd.targeting.resolves({});
await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn);
expect(mergeFn.called).to.be.false;
});
it('uses targeting data from cache if available', async function () {
- const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}};
- window.optable.instance.targetingFromCache.returns(targetingData);
+ const targetingData = { ortb2: { user: { ext: { optable: 'testData' } } } };
+ window.optable.rtd.targetingFromCache.returns(targetingData);
await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn);
expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true;
});
- it('calls targeting function if no data is found in cache', async function () {
- const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}};
- window.optable.instance.targetingFromCache.returns(null);
- window.optable.instance.targeting.resolves(targetingData);
+ it("doesn't call targeting function if no data is found in cache", async function () {
+ const targetingData = { ortb2: { user: { ext: { optable: 'testData' } } } };
+ window.optable.rtd.targetingFromCache.returns(null);
+ window.optable.rtd.targeting.resolves(targetingData);
await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn);
- expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true;
- });
- });
-
- describe('mergeOptableData', function () {
- let sandbox, mergeFn, handleRtdFn, reqBidsConfigObj;
-
- beforeEach(() => {
- sandbox = sinon.createSandbox();
- mergeFn = sinon.spy();
- reqBidsConfigObj = {ortb2Fragments: {global: {}}};
- });
-
- afterEach(() => {
- sandbox.restore();
- });
-
- it('calls handleRtdFn synchronously if it is a regular function', async function () {
- handleRtdFn = sinon.spy();
- await mergeOptableData(handleRtdFn, reqBidsConfigObj, {}, mergeFn);
- expect(handleRtdFn.calledOnceWith(reqBidsConfigObj, {}, mergeFn)).to.be.true;
- });
-
- it('calls handleRtdFn asynchronously if it is an async function', async function () {
- handleRtdFn = sinon.stub().resolves();
- await mergeOptableData(handleRtdFn, reqBidsConfigObj, {}, mergeFn);
- expect(handleRtdFn.calledOnceWith(reqBidsConfigObj, {}, mergeFn)).to.be.true;
+ expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.false;
+ expect(window.optable.rtd.targeting.called).to.be.false;
});
});
@@ -136,12 +90,11 @@ describe('Optable RTD Submodule', function () {
beforeEach(() => {
sandbox = sinon.createSandbox();
- reqBidsConfigObj = {ortb2Fragments: {global: {}}};
+ reqBidsConfigObj = { ortb2Fragments: { global: {} } };
callback = sinon.spy();
- moduleConfig = {params: {bundleUrl: 'https://cdn.optable.co/bundle.js'}};
+ moduleConfig = { params: {} };
- sandbox.stub(window, 'optable').value({cmd: []});
- sandbox.stub(window.document, 'createElement');
+ sandbox.stub(window, 'optable').value({ cmd: [] });
sandbox.stub(window.document, 'head');
});
@@ -149,20 +102,12 @@ describe('Optable RTD Submodule', function () {
sandbox.restore();
});
- it('loads Optable JS bundle if bundleUrl is provided', function () {
- getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
- expect(window.document.createElement.called).to.be.true;
- });
-
- it('uses existing Optable instance if no bundleUrl is provided', function () {
- moduleConfig.params.bundleUrl = null;
+ it('uses existing Optable instance', function () {
getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
expect(window.optable.cmd.length).to.equal(1);
});
it('calls callback when assuming the bundle is present', function (done) {
- moduleConfig.params.bundleUrl = null;
-
getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
// Check that the function is queued
@@ -176,9 +121,10 @@ describe('Optable RTD Submodule', function () {
}, 50);
});
- it('mergeOptableData catches error and executes callback when something goes wrong', function (done) {
- moduleConfig.params.bundleUrl = null;
- moduleConfig.params.handleRtd = () => { throw new Error('Test error'); };
+ it('getBidRequestData catches error and executes callback handleRtd throws an error', function (done) {
+ moduleConfig.params.handleRtd = () => {
+ throw new Error('Test error');
+ };
getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
@@ -192,7 +138,6 @@ describe('Optable RTD Submodule', function () {
});
it('getBidRequestData catches error and executes callback when something goes wrong', function (done) {
- moduleConfig.params.bundleUrl = null;
moduleConfig.params.handleRtd = 'not a function';
getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
@@ -205,15 +150,11 @@ describe('Optable RTD Submodule', function () {
}, 50);
});
- it("doesn't fail when optable is not available", function (done) {
+ it("doesn't fail when optable is not available", function () {
delete window.optable;
getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {});
- expect(window?.optable?.cmd?.length).to.be.undefined;
- setTimeout(() => {
- expect(callback.calledOnce).to.be.true;
- done();
- }, 50);
+ expect(window.optable.cmd.length).to.equal(1);
});
});
@@ -222,8 +163,8 @@ describe('Optable RTD Submodule', function () {
beforeEach(() => {
sandbox = sinon.createSandbox();
- moduleConfig = {params: {adserverTargeting: true}};
- window.optable = {instance: {targetingKeyValuesFromCache: sandbox.stub().returns({key1: 'value1'})}};
+ moduleConfig = { params: { adserverTargeting: true } };
+ window.optable = { rtd: { targetingKeyValuesFromCache: sandbox.stub().returns({ key1: 'value1' }) } };
});
afterEach(() => {
@@ -232,11 +173,11 @@ describe('Optable RTD Submodule', function () {
it('returns correct targeting data when Optable data is available', function () {
const result = getTargetingData(['adUnit1'], moduleConfig, {}, {});
- expect(result).to.deep.equal({adUnit1: {key1: 'value1'}});
+ expect(result).to.deep.equal({ adUnit1: { key1: 'value1' } });
});
it('returns empty object when no Optable data is found', function () {
- window.optable.instance.targetingKeyValuesFromCache.returns({});
+ window.optable.rtd.targetingKeyValuesFromCache.returns({});
expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({});
});
@@ -246,10 +187,10 @@ describe('Optable RTD Submodule', function () {
});
it('returns empty object when provided keys contain no data', function () {
- window.optable.instance.targetingKeyValuesFromCache.returns({key1: []});
+ window.optable.rtd.targetingKeyValuesFromCache.returns({ key1: [] });
expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({});
- window.optable.instance.targetingKeyValuesFromCache.returns({key1: [], key2: [], key3: []});
+ window.optable.rtd.targetingKeyValuesFromCache.returns({ key1: [], key2: [], key3: [] });
expect(getTargetingData(['adUnit1'], moduleConfig, {}, {})).to.deep.equal({});
});
});