diff --git a/integrationExamples/gpt/pubxaiRtdProvider_example.html b/integrationExamples/gpt/pubxaiRtdProvider_example.html new file mode 100644 index 00000000000..566cfe0c928 --- /dev/null +++ b/integrationExamples/gpt/pubxaiRtdProvider_example.html @@ -0,0 +1,113 @@ + + + Individual Ad Unit Refresh Example + + + + + + + + +

Individual Ad Unit Refresh Example

+
Div-1
+

+
+ +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index d2a13a57330..8f8e9e5dd92 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -88,6 +88,7 @@ "optimeraRtdProvider", "oxxionRtdProvider", "permutiveRtdProvider", + "pubxaiRtdProvider", "qortexRtdProvider", "reconciliationRtdProvider", "relevadRtdProvider", diff --git a/modules/pubxaiRtdProvider.js b/modules/pubxaiRtdProvider.js new file mode 100644 index 00000000000..c2594df2b1f --- /dev/null +++ b/modules/pubxaiRtdProvider.js @@ -0,0 +1,144 @@ +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; +import { submodule } from '../src/hook.js'; +import { deepAccess } from '../src/utils.js'; +import { createFloorsDataForAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'pubxai'; +window.__pubxFloorRulesPromise__ = null; +export const FloorsApiStatus = Object.freeze({ + IN_PROGRESS: 'IN_PROGRESS', + SUCCESS: 'SUCCESS', + ERROR: 'ERROR', +}); +export const FLOORS_EVENT_HANDLE = 'floorsApi'; +export const FLOORS_END_POINT = 'https://floor.pbxai.com/'; +export const FLOOR_PROVIDER = 'PubxFloorProvider'; + +export const getFloorsConfig = (provider, floorsResponse) => { + const floorsConfig = { + floors: { + enforcement: { floorDeals: true }, + data: floorsResponse, + }, + }; + const { floorMin, enforcement } = deepAccess(provider, 'params'); + if (floorMin) { + floorsConfig.floors.floorMin = floorMin; + } + if (enforcement) { + floorsConfig.floors.enforcement = enforcement; + } + return floorsConfig; +}; + +export const setFloorsConfig = (provider, data) => { + if (data) { + const floorsConfig = getFloorsConfig(provider, data); + config.setConfig(floorsConfig); + window.__pubxLoaded__ = true; + window.__pubxFloorsConfig__ = floorsConfig; + } else { + config.setConfig({ floors: window.__pubxPrevFloorsConfig__ }); + window.__pubxLoaded__ = false; + window.__pubxFloorsConfig__ = null; + } +}; + +export const setDefaultPriceFloors = (provider) => { + const { data } = deepAccess(provider, 'params'); + if (data !== undefined) { + data.floorProvider = FLOOR_PROVIDER; + setFloorsConfig(provider, data); + } +}; + +export const setPriceFloors = async (provider) => { + window.__pubxPrevFloorsConfig__ = config.getConfig('floors'); + setDefaultPriceFloors(provider); + return fetchFloorRules(provider) + .then((floorsResponse) => { + setFloorsConfig(provider, floorsResponse); + setFloorsApiStatus(FloorsApiStatus.SUCCESS); + }) + .catch((_) => { + setFloorsApiStatus(FloorsApiStatus.ERROR); + }); +}; + +export const setFloorsApiStatus = (status) => { + window.__pubxFloorsApiStatus__ = status; + window.dispatchEvent( + new CustomEvent(FLOORS_EVENT_HANDLE, { detail: { status } }) + ); +}; + +export const getUrl = (provider) => { + const { pubxId, endpoint } = deepAccess(provider, 'params'); + return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${ + window.location.href + }`; +}; + +export const fetchFloorRules = async (provider) => { + return new Promise((resolve, reject) => { + setFloorsApiStatus(FloorsApiStatus.IN_PROGRESS); + // When the api call exceeds auctionDelay, the api call doesn't fail but the auction starts. + ajax(getUrl(provider), { + success: (responseText, response) => { + try { + if (response && response.response) { + // "content-type" = 'text/plain; charset=utf-8' + const floorsResponse = JSON.parse(response.response); + resolve(floorsResponse); + } else { + resolve(null); + } + } catch (error) { + reject(error); + } + }, + error: (responseText, response) => { + reject(response); + }, + }); + }); +}; + +const init = (provider) => { + window.__pubxFloorRulesPromise__ = setPriceFloors(provider); + return true; +}; + +const getBidRequestData = (() => { + let floorsAttached = false; + return (reqBidsConfigObj, onDone) => { + if (!floorsAttached) { + createFloorsDataForAuction( + reqBidsConfigObj.adUnits, + reqBidsConfigObj.auctionId + ); + window.__pubxFloorRulesPromise__.then(() => { + createFloorsDataForAuction( + reqBidsConfigObj.adUnits, + reqBidsConfigObj.auctionId + ); + onDone(); + }); + floorsAttached = true; + } + }; +})(); + +export const pubxaiSubmodule = { + name: SUBMODULE_NAME, + init, + getBidRequestData, +}; + +export const beforeInit = () => { + submodule(MODULE_NAME, pubxaiSubmodule); +}; + +beforeInit(); diff --git a/modules/pubxaiRtdProvider.md b/modules/pubxaiRtdProvider.md new file mode 100644 index 00000000000..eb5f2ec16e4 --- /dev/null +++ b/modules/pubxaiRtdProvider.md @@ -0,0 +1,63 @@ +## Overview + +Module Name: pubX.ai RTD Provider +Module Type: RTD Adapter +Maintainer: phaneendra@pubx.ai + +## Description + +RTD module for prebid provided by pubX.ai. + +## Usage + +Make sure to have the following modules listed while building prebid : `priceFloors` +For example: + +```shell +gulp build --modules=priceFloors +``` + +Inorder to compile the RTD module into your Prebid build: + +```shell +gulp build --modules=rtdModule,pubxaiRtdProvider +``` + +Now to use the pubX.ai RTD module, add `realTimeData` with the below mentioned params to the Prebid config. + +```javascript +const AUCTION_DELAY = 100; +pbjs.setConfig({ + // rest of the config + ..., + realTimeData: { + auctionDelay: AUCTION_DELAY, + dataProviders: { + name: "pubxai", + waitForIt: true, + params: { + pubxId: ``, + endpoint: ``, // (optional) + floorMin: ``, // (optional) + enforcement: ``, // (optional) + data: `` // (optional) + } + } + } + // rest of the config + ..., +}); +``` + +## Parameters + +| Name | Type | Description | Default | +| :----------------- | :------ | :--------------------------------------------------------------- | :------------------------- | +| name | String | Real time data module name | Always `pubxai` | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.pubxId | String | Publisher Id | | +| params.endpoint | String | URL to get the floor data (optional) | `https://floor.pbxai.com/` | +| params.floorMin | Number | Mimimum CPM floor (optional) | `None` | +| params.enforcement | Object | Enforcement behavior within the Price Floors Module (optional) | `None` | +| params.data | Object | Default Floor data provided by pubx (optional) | `None` | diff --git a/test/spec/modules/pubxaiRtdProvider_spec.js b/test/spec/modules/pubxaiRtdProvider_spec.js new file mode 100644 index 00000000000..b645b830246 --- /dev/null +++ b/test/spec/modules/pubxaiRtdProvider_spec.js @@ -0,0 +1,397 @@ +import * as priceFloors from '../../../modules/priceFloors'; +import { + FLOORS_END_POINT, + FLOORS_EVENT_HANDLE, + FloorsApiStatus, + beforeInit, + fetchFloorRules, + getFloorsConfig, + getUrl, + pubxaiSubmodule, + setDefaultPriceFloors, + setFloorsApiStatus, + setFloorsConfig, + setPriceFloors, +} from '../../../modules/pubxaiRtdProvider'; +import { config } from '../../../src/config'; +import * as hook from '../../../src/hook.js'; +import { server } from '../../mocks/xhr.js'; + +const getConfig = () => ({ + params: { + useRtd: true, + endpoint: 'http://pubxai.com:3001/floors', + data: { + currency: 'EUR', + floorProvider: 'PubxFloorProvider', + modelVersion: 'gpt-mvm_AB_0.50_dt_0.75_dwt_0.95_dnt_0.25_fm_0.50', + schema: { fields: ['gptSlot', 'mediaType'] }, + values: { '*|banner': 0.02 }, + }, + }, +}); + +const getFloorsResponse = () => ({ + currency: 'USD', + floorProvider: 'PubxFloorProvider', + modelVersion: 'gpt-mvm_AB_0.50_dt_0.75_dwt_0.95_dnt_0.25_fm_0.50', + schema: { fields: ['gptSlot', 'mediaType'] }, + values: { '*|banner': 0.02 }, +}); + +const resetGlobals = () => { + window.__pubxLoaded__ = undefined; + window.__pubxPrevFloorsConfig__ = undefined; + window.__pubxFloorsConfig__ = undefined; + window.__pubxFloorsApiStatus__ = undefined; + window.__pubxFloorRulesPromise__ = null; +}; + +const fakeServer = ( + fakeResponse = '', + providerConfig = undefined, + statusCode = 200 +) => { + const fakeResponseHeaders = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }; + const request = server.requests[0]; + request.respond( + statusCode, + fakeResponseHeaders, + fakeResponse ? JSON.stringify(fakeResponse) : '' + ); + return request; +}; + +const stubConfig = () => { + const stub = sinon.stub(config, 'setConfig'); + return stub; +}; + +describe('pubxaiRtdProvider', () => { + describe('beforeInit', () => { + it('should register RTD submodule provider', function () { + let submoduleStub = sinon.stub(hook, 'submodule'); + beforeInit(); + assert(submoduleStub.calledOnceWith('realTimeData', pubxaiSubmodule)); + submoduleStub.restore(); + }); + }); + describe('submodule', () => { + describe('name', function () { + it('should be pubxai', function () { + expect(pubxaiSubmodule.name).to.equal('pubxai'); + }); + }); + }); + describe('init', () => { + let stub; + beforeEach(() => { + resetGlobals(); + stub = stubConfig(); + }); + afterEach(() => { + stub.restore(); + }); + it('standard case - returns true', () => { + const initResult = pubxaiSubmodule.init({ params: { useRtd: true } }); + expect(initResult).to.be.true; + }); + it('setPriceFloors called when `useRtd` is true in the provider config', () => { + pubxaiSubmodule.init(getConfig()); + expect(window.__pubxLoaded__).to.equal(true); + }); + }); + describe('getBidRequestData', () => { + const reqBidsConfigObj = { + adUnits: [{ code: 'ad-slot-code-0' }], + auctionId: 'auction-id-0', + }; + let stub; + beforeEach(() => { + window.__pubxFloorRulesPromise__ = Promise.resolve(); + stub = sinon.stub(priceFloors, 'createFloorsDataForAuction'); + }); + afterEach(() => { + resetGlobals(); + stub.restore(); + }); + it('createFloorsDataForAuction called once before and once after __pubxFloorRulesPromise__. Also getBidRequestData executed only once', async () => { + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + assert(priceFloors.createFloorsDataForAuction.calledOnce); + await window.__pubxFloorRulesPromise__; + assert(priceFloors.createFloorsDataForAuction.calledTwice); + assert( + priceFloors.createFloorsDataForAuction.alwaysCalledWith( + reqBidsConfigObj.adUnits, + reqBidsConfigObj.auctionId + ) + ); + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + await window.__pubxFloorRulesPromise__; + assert(priceFloors.createFloorsDataForAuction.calledTwice); + }); + }); + describe('fetchFloorRules', () => { + const providerConfig = getConfig(); + const floorsResponse = getFloorsResponse(); + it('success with floors response', (done) => { + const promise = fetchFloorRules(providerConfig); + fakeServer(floorsResponse); + promise.then((res) => { + expect(res).to.deep.equal(floorsResponse); + done(); + }); + }); + it('success with no floors response', (done) => { + const promise = fetchFloorRules(providerConfig); + fakeServer(undefined); + promise.then((res) => { + expect(res).to.deep.equal(null); + done(); + }); + }); + it('API call error', (done) => { + const promise = fetchFloorRules(providerConfig); + fakeServer(undefined, undefined, 404); + promise + .then((res) => { + expect(true).to.be.false; + }) + .catch((e) => { + expect(e).to.not.be.undefined; + }) + .finally(() => { + done(); + }); + }); + it('Wrong API response', (done) => { + const promise = fetchFloorRules(providerConfig); + fakeServer('floorsResponse'); + promise + .then((res) => { + expect(true).to.be.false; + }) + .catch((e) => { + expect(e).to.not.be.undefined; + }) + .finally(() => { + done(); + }); + }); + }); + describe('setPriceFloors', () => { + const providerConfig = getConfig(); + const floorsResponse = getFloorsResponse(); + let stub; + beforeEach(() => { + resetGlobals(); + stub = stubConfig(); + }); + afterEach(() => { + stub.restore(); + }); + it('with floors response', (done) => { + const floorsPromise = setPriceFloors(providerConfig); + fakeServer(floorsResponse); + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal( + getFloorsConfig(providerConfig, providerConfig.params.data) + ); + floorsPromise.then(() => { + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal( + getFloorsConfig(providerConfig, floorsResponse) + ); + done(); + }); + }); + it('without floors response', (done) => { + const floorsPromise = setPriceFloors(providerConfig); + fakeServer(undefined); + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal( + getFloorsConfig(providerConfig, providerConfig.params.data) + ); + floorsPromise.then(() => { + expect(window.__pubxLoaded__).to.be.false; + expect(window.__pubxFloorsConfig__).to.deep.equal(null); + done(); + }); + }); + it('default floors', (done) => { + const floorsPromise = setPriceFloors(providerConfig); + fakeServer(undefined, undefined, 404); + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal( + getFloorsConfig(providerConfig, providerConfig.params.data) + ); + floorsPromise + .then(() => { + expect(true).to.be.false; + }) + .catch((e) => { + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal( + getFloorsConfig(providerConfig, providerConfig.params.data) + ); + }) + .finally(() => { + done(); + }); + }); + }); + describe('setFloorsConfig', () => { + const providerConfig = getConfig(); + let stub; + beforeEach(() => { + resetGlobals(); + stub = stubConfig(); + }); + afterEach(function () { + stub.restore(); + }); + it('non-empty floorResponse', () => { + const floorsResponse = getFloorsResponse(); + setFloorsConfig(providerConfig, floorsResponse); + const floorsConfig = getFloorsConfig(providerConfig, floorsResponse); + assert(config.setConfig.calledOnceWith(floorsConfig)); + expect(window.__pubxLoaded__).to.be.true; + expect(window.__pubxFloorsConfig__).to.deep.equal(floorsConfig); + }); + it('empty floorResponse', () => { + const floorsResponse = null; + setFloorsConfig(providerConfig, floorsResponse); + assert(config.setConfig.calledOnceWith({ floors: undefined })); + expect(window.__pubxLoaded__).to.be.false; + expect(window.__pubxFloorsConfig__).to.be.null; + }); + }); + describe('getFloorsConfig', () => { + let providerConfig; + const floorsResponse = getFloorsResponse(); + beforeEach(() => { + providerConfig = getConfig(); + }); + it('no customizations in the provider config', () => { + const result = getFloorsConfig(providerConfig, floorsResponse); + expect(result).to.deep.equal({ + floors: { + enforcement: { floorDeals: true }, + data: floorsResponse, + }, + }); + }); + it('only floormin in the provider config', () => { + providerConfig.params.floorMin = 2; + expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({ + floors: { + enforcement: { floorDeals: true }, + floorMin: 2, + data: floorsResponse, + }, + }); + }); + it('only enforcement in the provider config', () => { + providerConfig.params.enforcement = { + bidAdjustment: true, + enforceJS: false, + }; + expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({ + floors: { + enforcement: { + bidAdjustment: true, + enforceJS: false, + }, + data: floorsResponse, + }, + }); + }); + it('both floorMin and enforcement in the provider config', () => { + providerConfig.params.floorMin = 2; + providerConfig.params.enforcement = { + bidAdjustment: true, + enforceJS: false, + }; + expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({ + floors: { + enforcement: { + bidAdjustment: true, + enforceJS: false, + }, + floorMin: 2, + data: floorsResponse, + }, + }); + }); + }); + describe('setDefaultPriceFloors', () => { + let stub; + beforeEach(() => { + resetGlobals(); + stub = stubConfig(); + }); + afterEach(function () { + stub.restore(); + }); + it('should set default floors config', () => { + const providerConfig = getConfig(); + setDefaultPriceFloors(providerConfig); + assert( + config.setConfig.calledOnceWith( + getFloorsConfig(providerConfig, providerConfig.params.data) + ) + ); + expect(window.__pubxLoaded__).to.be.true; + }); + }); + describe('setFloorsApiStatus', () => { + let stub; + beforeEach(() => { + resetGlobals(); + stub = sinon.stub(window, 'dispatchEvent'); + }); + afterEach(function () { + stub.restore(); + }); + it('set status', () => { + setFloorsApiStatus(FloorsApiStatus.SUCCESS); + expect(window.__pubxFloorsApiStatus__).to.equal(FloorsApiStatus.SUCCESS); + }); + it('dispatch event', () => { + setFloorsApiStatus(FloorsApiStatus.SUCCESS); + assert( + window.dispatchEvent.calledOnceWith( + new CustomEvent(FLOORS_EVENT_HANDLE, { + detail: { status: FloorsApiStatus.SUCCESS }, + }) + ) + ); + }); + }); + describe('getUrl', () => { + const provider = { + name: 'pubxai', + waitForIt: true, + params: { + pubxId: '12345', + }, + }; + it('floors end point', () => { + expect(FLOORS_END_POINT).to.equal('https://floor.pbxai.com/'); + }); + it('standard case', () => { + expect(getUrl(provider)).to.equal( + `https://floor.pbxai.com/?pubxId=12345&page=${window.location.href}` + ); + }); + it('custom url provided', () => { + provider.params.endpoint = 'https://custom.floor.com/'; + expect(getUrl(provider)).to.equal( + `https://custom.floor.com/?pubxId=12345&page=${window.location.href}` + ); + }); + }); +});