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

+ +

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

+
-

web-sdk-demo-gam360/header-ad

+

web-sdk-demo-gam360/header-ad

No response

-

web-sdk-demo-gam360/box-ad

+

web-sdk-demo-gam360/box-ad

No response

-

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({}); }); });