diff --git a/integrationExamples/gpt/azerionRtdProvider_example.html b/integrationExamples/gpt/azerionRtdProvider_example.html new file mode 100644 index 00000000000..978e9c8646e --- /dev/null +++ b/integrationExamples/gpt/azerionRtdProvider_example.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + +

Azerion RTD

+ +
+ +
+ + Segments: +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index 61d8c843d47..dcd3699d339 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -63,6 +63,7 @@ "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", + "azerionRtdProvider", "blueconicRtdProvider", "brandmetricsRtdProvider", "browsiRtdProvider", diff --git a/modules/azerionRtdProvider.js b/modules/azerionRtdProvider.js new file mode 100644 index 00000000000..8dda0aa8fb8 --- /dev/null +++ b/modules/azerionRtdProvider.js @@ -0,0 +1,129 @@ +/** + * This module adds the Azerion provider to the real time data module of prebid. + * + * The {@link module:modules/realTimeData} module is required + * @module modules/azerionRtdProvider + * @requires module:modules/realTimeData + */ +import { submodule } from '../src/hook.js'; +import { mergeDeep } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const SUBREAL_TIME_MODULE = 'azerion'; +export const STORAGE_KEY = 'ht-pa-v1-a'; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, moduleName: SUBREAL_TIME_MODULE +}); + +/** + * Get script url to load + * + * @param {Object} config + * + * @return {String} + */ +function getScriptURL(config) { + const VERSION = 'v1'; + const key = config.params?.key; + const publisherPath = key ? `${key}/` : ''; + return `https://pa.hyth.io/js/${VERSION}/${publisherPath}htpa.min.js`; +} + +/** + * Attach script tag to DOM + * + * @param {Object} config + * + * @return {void} + */ +export function attachScript(config) { + const script = getScriptURL(config); + loadExternalScript(script, SUBREAL_TIME_MODULE, () => { + if (typeof window.azerionPublisherAudiences === 'function') { + window.azerionPublisherAudiences(config.params?.process || {}); + } + }); +} + +/** + * Fetch audiences info from localStorage. + * + * @return {Array} Audience ids. + */ +export function getAudiences() { + try { + const data = storage.getDataFromLocalStorage(STORAGE_KEY); + return JSON.parse(data).map(({id}) => id); + } catch (_) { + return []; + } +} + +/** + * Pass audience data to configured bidders, using ORTB2 + * + * @param {Object} reqBidsConfigObj + * @param {Object} config + * @param {Array} audiences + * + * @return {void} + */ +export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) { + const defaultBidders = ['improvedigital']; + const bidders = config.params?.bidders || defaultBidders; + bidders.forEach((bidderCode) => mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, {[bidderCode]: { + user: { + data: [ + { name: 'azerion', ext: { segtax: 4 }, segment: audiences.map(id => ({id})) } + ] + } + }})); +} + +/** + * Module initialisation. + * + * @param {Object} config + * @param {Object} userConsent + * + * @return {boolean} + */ +function init(config, userConsent) { + attachScript(config); + return true; +} + +/** + * Real-time user audiences retrieval + * + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {Object} config + * @param {Object} userConsent + * + * @return {void} + */ +export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const audiences = getAudiences(); + if (audiences.length > 0) { + setAudiencesToBidders(reqBidsConfigObj, config, audiences); + } + callback(); +} + +/** @type {RtdSubmodule} */ +export const azerionSubmodule = { + name: SUBREAL_TIME_MODULE, + init: init, + getBidRequestData: getBidRequestData, +}; + +submodule(REAL_TIME_MODULE, azerionSubmodule); diff --git a/modules/azerionRtdProvider.md b/modules/azerionRtdProvider.md new file mode 100644 index 00000000000..5d074f6a96a --- /dev/null +++ b/modules/azerionRtdProvider.md @@ -0,0 +1,94 @@ +--- +layout: page_v2 +title: azerion RTD Provider +display_name: Azerion RTD Provider +description: Real Time audience generator. +page_type: module +module_type: rtd +module_code: azerionRtdProvider +enable_download: true +vendor_specific: true +sidebarType: 1 +--- + +# Azerion RTD Provider + +Azerion RTD is designed to help publishers find its users interests +while providing these interests to buyers in the bid request. All this without +exposing data to thir-party services. + +{:.no_toc} + +- TOC + {:toc} + +## Integration + +Compile the Azerion RTD module (`azerionRtdProvider`) into your Prebid build, +along with the parent RTD Module (`rtdModule`): + +```bash +gulp build --modules=rtdModule,azerionRtdProvider,appnexusBidAdapter,improvedigitalBidAdapter +``` + +Set configuration via `pbjs.setConfig`. + +```js +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: 'azerion', + waitForIt: true, + params: { + publisherId: 'publisherId', + bidders: ['appnexus', 'improvedigital'], + process: {} + } + } + ] + } + ... +} +``` + +### Parameter Description + +{: .table .table-bordered .table-striped } +| Name | Type | Description | Notes | +| :--- | :------- | :------------------ | :--------------- | +| name | `String` | RTD sub module name | Always "azerion" | +| waitForIt | `Boolean` | Required to ensure that the auction is delayed for the module to respond. | Optional. Defaults to false but recommended to true. | +| params.key | `String` | Publisher partner specific key | Optional | +| params.bidders | `Array` | Bidders with which to share segment information | Optional. Defaults to "improvedigital". | +| params.process | `Object` | Configuration for the publisher audiences script. | Optional. Defaults to `{}`. | + +### Configuration `process` Description + +{: .table .table-bordered .table-striped } +| Name | Type | Description | Notes | +| :------------- | :-------- | :----------------------------------------------- | :-------------------------- | +| process.optout | `Boolean` | Disables the process of audiences for the users. | Optional. Defaults to false | + +## Testing + +To view an example: + +```bash +gulp serve-fast --modules=rtdModule,azerionRtdProvider,appnexusBidAdapter,improvedigitalBidAdapter +``` + +Access [http://localhost:9999/integrationExamples/gpt/azerionRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionRtdProvider_example.html) +in your browser. + +Run the unit tests: + +```bash +npm test -- --file "test/spec/modules/azerionRtdProvider_spec.js" +``` + +## Support + +If you require further assistance please contact [support@azerion.com](mailto:support@azerion.com). diff --git a/src/adloader.js b/src/adloader.js index 5309f3a3d42..b756a230362 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -20,6 +20,7 @@ const _approvedLoadExternalJSList = [ 'hadron', 'medianet', 'improvedigital', + 'azerion', 'aaxBlockmeter', 'confiant', 'arcspan', diff --git a/test/spec/modules/azerionRtdProvider_spec.js b/test/spec/modules/azerionRtdProvider_spec.js new file mode 100644 index 00000000000..b5d94569913 --- /dev/null +++ b/test/spec/modules/azerionRtdProvider_spec.js @@ -0,0 +1,152 @@ +import {config} from 'src/config.js'; +import * as azerionRTD from 'modules/azerionRtdProvider.js'; +import {loadExternalScript} from '../../../src/adloader.js'; + +describe('Azerion RTD submodule', function () { + const STORAGE_KEY = 'ht-pa-v1-a'; + const USER_AUDIENCES = [{id: '1', visits: 123}, {id: '2', visits: 456}]; + + const key = 'publisher123'; + const bidders = ['appnexus', 'improvedigital']; + const process = {key: 'value'}; + const dataProvider = { name: 'azerion', waitForIt: true }; + + let reqBidsConfigObj; + let storageStub; + + beforeEach(function () { + config.resetConfig(); + reqBidsConfigObj = {ortb2Fragments: {bidder: {}}}; + window.azerionPublisherAudiences = sinon.spy(); + storageStub = sinon.stub(azerionRTD.storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + delete window.azerionPublisherAudiences; + storageStub.restore(); + }); + + describe('initialisation', function () { + let returned; + + beforeEach(function() { + returned = azerionRTD.azerionSubmodule.init(dataProvider); + }); + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should load external script', function() { + expect(loadExternalScript.called).to.be.true; + }); + + it('should load external script with default versioned url', function() { + const expected = 'https://pa.hyth.io/js/v1/htpa.min.js'; + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + + it('should call azerionPublisherAudiencesStub with empty configuration', function() { + expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal({}); + }); + + describe('with key', function() { + beforeEach(function() { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + returned = azerionRTD.azerionSubmodule.init({...dataProvider, params: {key}}); + }) + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should load external script with publisher id url', function() { + const expected = `https://pa.hyth.io/js/v1/${key}/htpa.min.js`; + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + }); + + describe('with process configuration', function() { + beforeEach(function() { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + returned = azerionRTD.azerionSubmodule.init({...dataProvider, params: {process}}); + }) + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should call azerionPublisherAudiencesStub with process configuration', function() { + expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal(process); + }); + }); + }); + + describe('gets audiences', function () { + let callbackStub; + + beforeEach(function() { + callbackStub = sinon.mock(); + }) + + describe('with empty storage', function() { + beforeEach(function() { + azerionRTD.azerionSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider); + }); + + it('does not run apply audiences to bidders', function() { + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); + }); + + it('calls callback anyway', function() { + expect(callbackStub.called).to.be.true; + }); + }); + + describe('with populate storage', function() { + beforeEach(function() { + storageStub.withArgs(STORAGE_KEY).returns(JSON.stringify(USER_AUDIENCES)); + azerionRTD.azerionSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider); + }); + + it('does apply audiences to bidder', function() { + const segments = reqBidsConfigObj.ortb2Fragments.bidder['improvedigital'].user.data[0].segment; + expect(segments).to.deep.equal([{id: '1'}, {id: '2'}]); + }); + + it('calls callback always', function() { + expect(callbackStub.called).to.be.true; + }); + }) + }); + + describe('sets audiences in bidder', function () { + const audiences = USER_AUDIENCES.map(({id}) => id); + const expected = { + 'user': { + 'data': [ + { + 'ext': { 'segtax': 4 }, + 'name': 'azerion', + 'segment': [ { 'id': '1' }, { 'id': '2' } ], + }, + ], + }, + }; + + it('for improvedigital by default', function () { + azerionRTD.setAudiencesToBidders(reqBidsConfigObj, dataProvider, audiences); + expect(reqBidsConfigObj.ortb2Fragments.bidder['improvedigital']).to.deep.equal(expected); + }); + + bidders.forEach((bidder) => { + it(`for ${bidder}`, function () { + const config = {...dataProvider, params: { bidders }}; + azerionRTD.setAudiencesToBidders(reqBidsConfigObj, config, audiences); + expect(reqBidsConfigObj.ortb2Fragments.bidder[bidder]).to.deep.equal(expected); + }); + }); + }); +});