From c629d4c6e51e487ef227590d20a04f93c3ce37c1 Mon Sep 17 00:00:00 2001 From: Jordi Garcia Date: Fri, 23 Feb 2024 12:43:08 +0100 Subject: [PATCH 1/5] Azerion Edge RTD Module: Initial release ### Type of change [x] Feature: New RTD Submodule ### Description of change Adds new Azerion Edge RTD module. Maintainer: azerion.com Contact: @garciapuig @mserrate @gguridi --- .../gpt/azerionedgeRtdProvider_example.html | 91 +++++++++ modules/.submodules.json | 1 + modules/azerionedgeRtdProvider.js | 143 ++++++++++++++ modules/azerionedgeRtdProvider.md | 112 +++++++++++ src/adloader.js | 3 +- .../modules/azerionedgeRtdProvider_spec.js | 183 ++++++++++++++++++ 6 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 integrationExamples/gpt/azerionedgeRtdProvider_example.html create mode 100644 modules/azerionedgeRtdProvider.js create mode 100644 modules/azerionedgeRtdProvider.md create mode 100644 test/spec/modules/azerionedgeRtdProvider_spec.js diff --git a/integrationExamples/gpt/azerionedgeRtdProvider_example.html b/integrationExamples/gpt/azerionedgeRtdProvider_example.html new file mode 100644 index 00000000000..880fe5ed706 --- /dev/null +++ b/integrationExamples/gpt/azerionedgeRtdProvider_example.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + +

Azerion Edge RTD

+ +
+ +
+ + Segments: +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index 61d8c843d47..cfa98b5ab32 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -63,6 +63,7 @@ "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", + "azerionedgeRtdProvider", "blueconicRtdProvider", "brandmetricsRtdProvider", "browsiRtdProvider", diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js new file mode 100644 index 00000000000..a162ce074aa --- /dev/null +++ b/modules/azerionedgeRtdProvider.js @@ -0,0 +1,143 @@ +/** + * This module adds the Azerion provider to the real time data module of prebid. + * + * The {@link module:modules/realTimeData} module is required + * @module modules/azerionedgeRtdProvider + * @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('./rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const SUBREAL_TIME_MODULE = 'azerionedge'; +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://edge.hyth.io/js/${VERSION}/${publisherPath}azerion-edge.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: 'azerionedge', + 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 azerionedgeSubmodule = { + name: SUBREAL_TIME_MODULE, + init: init, + getBidRequestData: getBidRequestData, +}; + +submodule(REAL_TIME_MODULE, azerionedgeSubmodule); diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md new file mode 100644 index 00000000000..6e395ab6f66 --- /dev/null +++ b/modules/azerionedgeRtdProvider.md @@ -0,0 +1,112 @@ +--- +layout: page_v2 +title: azerion edge RTD Provider +display_name: Azerion Edge RTD Provider +description: Client-side contextual cookieless audiences. +page_type: module +module_type: rtd +module_code: azerionedgeRtdProvider +enable_download: true +vendor_specific: true +sidebarType: 1 +--- + +# Azerion Edge RTD Provider + +Client-side contextual cookieless audiences. + +Azerion Edge RTD module helps publishers to capture users' interest +audiences on their site, and attach these into the bid request. + +Maintainer: [azerion.com](https://www.azerion.com/) + +{:.no_toc} + +- TOC + {:toc} + +## Integration + +Compile the Azerion Edge RTD module (`azerionedgeRtdProvider`) into your Prebid build, +along with the parent RTD Module (`rtdModule`): + +```bash +gulp build --modules=rtdModule,azerionedgeRtdProvider,improvedigitalBidAdapter +``` + +Set configuration via `pbjs.setConfig`. + +```js +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: 'azerionedge', + waitForIt: true, + params: { + key: '', + bidders: ['improvedigital'], + process: {} + } + } + ] + } + ... +} +``` + +### Parameter Description + +{: .table .table-bordered .table-striped } +| Name | Type | Description | Notes | +| :--- | :------- | :------------------ | :--------------- | +| name | `String` | RTD sub module name | Always "azerionedge" | +| 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 Azerion Edge script. | Optional. Defaults to `{}`. | + +## Context + +As all data collection is on behalf of the publisher and based on the consent the publisher has +received from the user, this module does not require a TCF vendor configuration. Consent is +provided to the module when the user gives the relevant permissions on the publisher website. + +As Prebid.js utilizes TCF vendor consent for the RTD module to load, the module needs to be labeled +within the Vendor Exceptions. + +### Instructions + +If the Prebid GDPR enforcement is enabled, the module should be labeled +as exception, as shown below: + +```js +[ + { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ["azerionedge"] + }, + ... +] +``` + +## Testing + +To view an example: + +```bash +gulp serve-fast --modules=rtdModule,azerionedgeRtdProvider,improvedigitalBidAdapter +``` + +Access [http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html) +in your browser. + +Run the unit tests: + +```bash +npm test -- --file "test/spec/modules/azerionedgeRtdProvider_spec.js" +``` diff --git a/src/adloader.js b/src/adloader.js index 5309f3a3d42..c2da2646320 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -20,6 +20,7 @@ const _approvedLoadExternalJSList = [ 'hadron', 'medianet', 'improvedigital', + 'azerionedge', 'aaxBlockmeter', 'confiant', 'arcspan', @@ -33,7 +34,7 @@ const _approvedLoadExternalJSList = [ 'contxtful', 'id5', 'lucead', -] +]; /** * Loads external javascript. Can only be used if external JS is approved by Prebid. See https://github.com/prebid/prebid-js-external-js-template#policy diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js new file mode 100644 index 00000000000..f08aaebdf55 --- /dev/null +++ b/test/spec/modules/azerionedgeRtdProvider_spec.js @@ -0,0 +1,183 @@ +import { config } from 'src/config.js'; +import * as azerionedgeRTD from 'modules/azerionedgeRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; + +describe('Azerion Edge 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: 'azerionedge', waitForIt: true }; + + let reqBidsConfigObj; + let storageStub; + + beforeEach(function () { + config.resetConfig(); + reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + window.azerionPublisherAudiences = sinon.spy(); + storageStub = sinon.stub(azerionedgeRTD.storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + delete window.azerionPublisherAudiences; + storageStub.restore(); + }); + + describe('initialisation', function () { + let returned; + + beforeEach(function () { + returned = azerionedgeRTD.azerionedgeSubmodule.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://edge.hyth.io/js/v1/azerion-edge.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 = azerionedgeRTD.azerionedgeSubmodule.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://edge.hyth.io/js/v1/${key}/azerion-edge.min.js`; + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + }); + + describe('with process configuration', function () { + beforeEach(function () { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + returned = azerionedgeRTD.azerionedgeSubmodule.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 () { + azerionedgeRTD.azerionedgeSubmodule.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)); + azerionedgeRTD.azerionedgeSubmodule.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: 'azerionedge', + segment: [{ id: '1' }, { id: '2' }], + }, + ], + }, + }; + + it('for improvedigital by default', function () { + azerionedgeRTD.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 } }; + azerionedgeRTD.setAudiencesToBidders(reqBidsConfigObj, config, audiences); + expect(reqBidsConfigObj.ortb2Fragments.bidder[bidder]).to.deep.equal( + expected + ); + }); + }); + }); +}); From 09c1cbce4bf159b2571a2a8d65e57c0a89561406 Mon Sep 17 00:00:00 2001 From: Jordi Garcia Date: Tue, 27 Feb 2024 17:26:14 +0100 Subject: [PATCH 2/5] Azerion Edge RTD Module: Initial release. Typo --- modules/azerionedgeRtdProvider.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md index 6e395ab6f66..2849bef3f63 100644 --- a/modules/azerionedgeRtdProvider.md +++ b/modules/azerionedgeRtdProvider.md @@ -31,7 +31,7 @@ Compile the Azerion Edge RTD module (`azerionedgeRtdProvider`) into your Prebid along with the parent RTD Module (`rtdModule`): ```bash -gulp build --modules=rtdModule,azerionedgeRtdProvider,improvedigitalBidAdapter +gulp build --modules=rtdModule,azerionedgeRtdProvider ``` Set configuration via `pbjs.setConfig`. @@ -99,7 +99,7 @@ as exception, as shown below: To view an example: ```bash -gulp serve-fast --modules=rtdModule,azerionedgeRtdProvider,improvedigitalBidAdapter +gulp serve-fast --modules=rtdModule,azerionedgeRtdProvider ``` Access [http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html) From 732cbc03720c04d707042eafa13f81e6d230f1ef Mon Sep 17 00:00:00 2001 From: Jordi Garcia Date: Tue, 16 Apr 2024 12:23:59 +0200 Subject: [PATCH 3/5] AzerionEdge RTD Module: Documentation: Required parameters Type of change: Documentation/Feature Description of change: Specifying new required parameters on documentation. Updating examples. --- integrationExamples/gpt/azerionedgeRtdProvider_example.html | 6 +++--- modules/azerionedgeRtdProvider.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/integrationExamples/gpt/azerionedgeRtdProvider_example.html b/integrationExamples/gpt/azerionedgeRtdProvider_example.html index 880fe5ed706..e85ab705235 100644 --- a/integrationExamples/gpt/azerionedgeRtdProvider_example.html +++ b/integrationExamples/gpt/azerionedgeRtdProvider_example.html @@ -40,17 +40,17 @@ { name: "azerionedge", waitForIt: true, - params: { bidders: ["appnexus"] }, + params: { bidders: ["improvedigital"] }, }, ], }, }); - pbjs.setBidderConfig({ bidders: ["appnexus"], config: {} }); + pbjs.setBidderConfig({ bidders: ["improvedigital"], config: {} }); pbjs.addAdUnits([ { code: TEST_DIV, mediaTypes: { banner: { sizes: TEST_SIZES } }, - bids: [{ bidder: "appnexus", params: { placementId: 13144370 } }], + bids: [{ bidder: "improvedigital", params: { placementId: 13144370 } }], }, ]); pbjs.requestBids({ diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md index 2849bef3f63..6658907c480 100644 --- a/modules/azerionedgeRtdProvider.md +++ b/modules/azerionedgeRtdProvider.md @@ -1,6 +1,6 @@ --- layout: page_v2 -title: azerion edge RTD Provider +title: Azerion Edge RTD Provider display_name: Azerion Edge RTD Provider description: Client-side contextual cookieless audiences. page_type: module @@ -18,6 +18,8 @@ Client-side contextual cookieless audiences. Azerion Edge RTD module helps publishers to capture users' interest audiences on their site, and attach these into the bid request. +Please contact [edge@azerion.com](edge@azerion.com) for more information. + Maintainer: [azerion.com](https://www.azerion.com/) {:.no_toc} @@ -64,7 +66,7 @@ pbjs.setConfig( | :--- | :------- | :------------------ | :--------------- | | name | `String` | RTD sub module name | Always "azerionedge" | | 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.key | `String` | Publisher partner specific key | Mandatory. The key is required for the module to work. If you haven't received one, please reach [support@improvedigital.com](support@improvedigital.com) | | params.bidders | `Array` | Bidders with which to share segment information | Optional. Defaults to "improvedigital". | | params.process | `Object` | Configuration for the Azerion Edge script. | Optional. Defaults to `{}`. | From fb704f3d05d21f83d7a0460c06e809d070f18f06 Mon Sep 17 00:00:00 2001 From: Jordi Garcia Date: Wed, 12 Jun 2024 14:53:49 +0200 Subject: [PATCH 4/5] AzerionEdge RTD Module: Compatible with GDPR/USP Privacy Modules (#14) - Added GDPR validation. - We validate against ImproveDigital vendor ID consent and several purposes. - We don't load edge script, nor process the existing data, if consent wasn't given. - Adding support for USP consent. --- modules/azerionedgeRtdProvider.js | 82 ++++++++++++- modules/azerionedgeRtdProvider.md | 17 --- .../modules/azerionedgeRtdProvider_spec.js | 110 +++++++++++++++++- 3 files changed, 184 insertions(+), 25 deletions(-) diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js index a162ce074aa..bb0769cf15f 100644 --- a/modules/azerionedgeRtdProvider.js +++ b/modules/azerionedgeRtdProvider.js @@ -19,6 +19,9 @@ const REAL_TIME_MODULE = 'realTimeData'; const SUBREAL_TIME_MODULE = 'azerionedge'; export const STORAGE_KEY = 'ht-pa-v1-a'; +const IMPROVEDIGITAL_GVLID = '253'; +const PURPOSES = ['1', '3', '5', '7', '9']; + export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBREAL_TIME_MODULE, @@ -106,10 +109,79 @@ export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) { * @return {boolean} */ function init(config, userConsent) { - attachScript(config); + if (hasUserConsented(userConsent)) { + attachScript(config); + } return true; } +/** + * List the vendors consented coming from userConsent object. + * + * @param {Object} userConsent + * + * @return {Array} + */ +function getVendorsConsented(userConsent) { + const consents = userConsent?.gdpr?.vendorData?.vendor?.consents || {}; + return Object.entries(consents).reduce((acc, [vendorId, consented]) => { + return consented ? [...acc, vendorId] : acc; + }, []); +} + +/** + * List the purposes consented coming from userConsent object. + * + * @param {Object} userConsent + * + * @return {Array} + */ +export function getPurposesConsented(userConsent) { + const consents = userConsent?.gdpr?.vendorData?.purpose?.consents || {}; + return Object.entries(consents).reduce((acc, [purposeId, consented]) => { + return consented ? [...acc, purposeId] : acc; + }, []); +} + +/** + * Checks if GDPR gives us access through the userConsent object. + * + * @param {Object} userConsent + * + * @return {boolean} + */ +export function hasGDPRAccess(userConsent) { + const gdprApplies = userConsent?.gdpr?.gdprApplies; + const isVendorAllowed = getVendorsConsented(userConsent).includes(IMPROVEDIGITAL_GVLID); + const arePurposesAllowed = PURPOSES.every((purpose) => getPurposesConsented(userConsent).includes(purpose)); + return !gdprApplies || (isVendorAllowed && arePurposesAllowed); +} + +/** + * Checks if USP gives us access through the userConsent object. + * + * @param {Object} userConsent + * + * @return {boolean} + */ +export function hasUSPAccess(userConsent) { + const uspProvided = userConsent?.usp; + const hasProvidedUserNotice = uspProvided?.[1] !== 'N'; + const hasNotOptedOut = uspProvided?.[2] !== 'Y'; + return !uspProvided || (hasProvidedUserNotice && hasNotOptedOut); +} + +/** + * Checks if GDPR/USP gives us access through the userConsent object. + * + * @param {Object} userConsent + * + * @return {boolean} + */ +export function hasUserConsented(userConsent) { + return hasGDPRAccess(userConsent) && hasUSPAccess(userConsent); +} + /** * Real-time user audiences retrieval * @@ -126,9 +198,11 @@ export function getBidRequestData( config, userConsent ) { - const audiences = getAudiences(); - if (audiences.length > 0) { - setAudiencesToBidders(reqBidsConfigObj, config, audiences); + if (hasUserConsented(userConsent)) { + const audiences = getAudiences(); + if (audiences.length > 0) { + setAudiencesToBidders(reqBidsConfigObj, config, audiences); + } } callback(); } diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md index 6658907c480..e1bdf792647 100644 --- a/modules/azerionedgeRtdProvider.md +++ b/modules/azerionedgeRtdProvider.md @@ -79,23 +79,6 @@ provided to the module when the user gives the relevant permissions on the publi As Prebid.js utilizes TCF vendor consent for the RTD module to load, the module needs to be labeled within the Vendor Exceptions. -### Instructions - -If the Prebid GDPR enforcement is enabled, the module should be labeled -as exception, as shown below: - -```js -[ - { - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: ["azerionedge"] - }, - ... -] -``` - ## Testing To view an example: diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js index f08aaebdf55..c4e249e7327 100644 --- a/test/spec/modules/azerionedgeRtdProvider_spec.js +++ b/test/spec/modules/azerionedgeRtdProvider_spec.js @@ -13,6 +13,9 @@ describe('Azerion Edge RTD submodule', function () { const bidders = ['appnexus', 'improvedigital']; const process = { key: 'value' }; const dataProvider = { name: 'azerionedge', waitForIt: true }; + const tcfGDPRNotApplicable = { gdprApplies: false }; + const uspNotProvided = { usp: undefined }; + const ignoreConsent = {gdpr: tcfGDPRNotApplicable, usp: uspNotProvided}; let reqBidsConfigObj; let storageStub; @@ -33,7 +36,7 @@ describe('Azerion Edge RTD submodule', function () { let returned; beforeEach(function () { - returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider); + returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, ignoreConsent); }); it('should return true', function () { @@ -60,7 +63,7 @@ describe('Azerion Edge RTD submodule', function () { returned = azerionedgeRTD.azerionedgeSubmodule.init({ ...dataProvider, params: { key }, - }); + }, ignoreConsent); }); it('should return true', function () { @@ -80,7 +83,7 @@ describe('Azerion Edge RTD submodule', function () { returned = azerionedgeRTD.azerionedgeSubmodule.init({ ...dataProvider, params: { process }, - }); + }, ignoreConsent); }); it('should return true', function () { @@ -95,6 +98,105 @@ describe('Azerion Edge RTD submodule', function () { }); }); + describe('GDPR access', () => { + const vendorConsented = { '253': true } + const purposesConsented = {'1': true, '3': true, '5': true, '7': true, '9': true}; + const partialPurposesConsented = {'1': true, '3': true, '5': true, '7': true}; + const tcfConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented }, purpose: { consents: purposesConsented } } }; + const tcfVendorNotConsented = { gdprApplies: true, vendorData: { purpose: {consents: purposesConsented} } }; + const tcfPurposesNotConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented } } }; + const tcfPartialPurposesNotConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented }, purpose: { consents: partialPurposesConsented } } }; + + [ + ['not applicable', tcfGDPRNotApplicable, true], + ['tcf consented', tcfConsented, true], + ['tcf vendor not consented', tcfVendorNotConsented, false], + ['tcf purposes not consented', tcfPurposesNotConsented, false], + ['tcp partial purposes not consented', tcfPartialPurposesNotConsented, false], + ].forEach(([info, gdpr, expected]) => { + it(`for ${info} should return ${expected}`, () => { + expect(azerionedgeRTD.hasGDPRAccess({gdpr})).to.equal(expected); + }); + + it(`for ${info} should load=${expected} the external script`, () => { + azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, {gdpr, usp: uspNotProvided}); + expect(loadExternalScript.called).to.equal(expected); + }); + + describe('for bid request data', function () { + let callbackStub; + + beforeEach(function () { + callbackStub = sinon.mock(); + azerionedgeRTD.azerionedgeSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider, {gdpr, usp: uspNotProvided}); + }); + + it(`does call=${expected} the local storage looking for audiences`, function () { + expect(storageStub.called).to.equal(expected); + }); + + it('calls callback always', function () { + expect(callbackStub.called).to.be.true; + }); + }); + }); + }); + + describe('USP acccess', () => { + const uspMalformed = -1; + const uspNotApplicable = '1---'; + const uspUserNotifiedOptedOut = '1YY-'; + const uspUserNotifiedNotOptedOut = '1YN-'; + const uspUserNotifiedUnknownOptedOut = '1Y--'; + const uspUserNotNotifiedOptedOut = '1NY-'; + const uspUserNotNotifiedNotOptedOut = '1NN-'; + const uspUserNotNotifiedUnknownOptedOut = '1N--'; + const uspUserUnknownNotifiedOptedOut = '1-Y-'; + const uspUserUnknownNotifiedNotOptedOut = '1-N-'; + const uspUserUnknownNotifiedUnknownOptedOut = '1---'; + + [ + ['malformed', uspMalformed, true], + ['not applicable', uspNotApplicable, true], + ['not provided', uspNotProvided, true], + ['user notified and opted out', uspUserNotifiedOptedOut, false], + ['user notified and not opted out', uspUserNotifiedNotOptedOut, true], + ['user notified and unknown opted out', uspUserNotifiedUnknownOptedOut, true], + ['user not notified and opted out', uspUserNotNotifiedOptedOut, false], + ['user not notified and not opted out', uspUserNotNotifiedNotOptedOut, false], + ['user not notified and unknown opted out', uspUserNotNotifiedUnknownOptedOut, false], + ['user unknown notified and opted out', uspUserUnknownNotifiedOptedOut, false], + ['user unknown notified and not opted out', uspUserUnknownNotifiedNotOptedOut, true], + ['user unknown notified and unknown opted out', uspUserUnknownNotifiedUnknownOptedOut, true], + ].forEach(([info, usp, expected]) => { + it(`for ${info} should return ${expected}`, () => { + expect(azerionedgeRTD.hasUSPAccess({usp})).to.equal(expected); + }); + + it(`for ${info} should load=${expected} the external script`, () => { + azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, {gdpr: tcfGDPRNotApplicable, usp}); + expect(loadExternalScript.called).to.equal(expected); + }); + + describe('for bid request data', function () { + let callbackStub; + + beforeEach(function () { + callbackStub = sinon.mock(); + azerionedgeRTD.azerionedgeSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider, {gdpr: tcfGDPRNotApplicable, usp}); + }); + + it(`does call=${expected} the local storage looking for audiences`, function () { + expect(storageStub.called).to.equal(expected); + }); + + it('calls callback always', function () { + expect(callbackStub.called).to.be.true; + }); + }); + }); + }); + describe('gets audiences', function () { let callbackStub; @@ -111,7 +213,7 @@ describe('Azerion Edge RTD submodule', function () { ); }); - it('does not run apply audiences to bidders', function () { + it('does not apply audiences to bidders', function () { expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); }); From 322be6d7398ae781878e73e99918f5c883f7732f Mon Sep 17 00:00:00 2001 From: Jordi Garcia Date: Tue, 2 Jul 2024 16:51:48 +0200 Subject: [PATCH 5/5] AzerionEdgeRTDModule: Passing the consent to the script execution (#17) Adding GVL ID to the module configuration Passing the consent to the script execution instead of handling it in prebid (#16) --------- Co-authored-by: Gorka Guridi --- modules/azerionedgeRtdProvider.js | 92 ++--------- .../modules/azerionedgeRtdProvider_spec.js | 155 ++++-------------- 2 files changed, 48 insertions(+), 199 deletions(-) diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js index bb0769cf15f..852639972c2 100644 --- a/modules/azerionedgeRtdProvider.js +++ b/modules/azerionedgeRtdProvider.js @@ -20,7 +20,6 @@ const SUBREAL_TIME_MODULE = 'azerionedge'; export const STORAGE_KEY = 'ht-pa-v1-a'; const IMPROVEDIGITAL_GVLID = '253'; -const PURPOSES = ['1', '3', '5', '7', '9']; export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, @@ -45,14 +44,21 @@ function getScriptURL(config) { * Attach script tag to DOM * * @param {Object} config + * @param {Object} userConsent * * @return {void} */ -export function attachScript(config) { +export function attachScript(config, userConsent) { const script = getScriptURL(config); loadExternalScript(script, SUBREAL_TIME_MODULE, () => { if (typeof window.azerionPublisherAudiences === 'function') { - window.azerionPublisherAudiences(config.params?.process || {}); + const publisherConfig = config.params?.process || {}; + window.azerionPublisherAudiences({ + ...publisherConfig, + gdprApplies: userConsent?.gdpr?.gdprApplies, + gdprConsent: userConsent?.gdpr?.consentString, + uspConsent: userConsent?.usp, + }); } }); } @@ -109,79 +115,10 @@ export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) { * @return {boolean} */ function init(config, userConsent) { - if (hasUserConsented(userConsent)) { - attachScript(config); - } + attachScript(config, userConsent); return true; } -/** - * List the vendors consented coming from userConsent object. - * - * @param {Object} userConsent - * - * @return {Array} - */ -function getVendorsConsented(userConsent) { - const consents = userConsent?.gdpr?.vendorData?.vendor?.consents || {}; - return Object.entries(consents).reduce((acc, [vendorId, consented]) => { - return consented ? [...acc, vendorId] : acc; - }, []); -} - -/** - * List the purposes consented coming from userConsent object. - * - * @param {Object} userConsent - * - * @return {Array} - */ -export function getPurposesConsented(userConsent) { - const consents = userConsent?.gdpr?.vendorData?.purpose?.consents || {}; - return Object.entries(consents).reduce((acc, [purposeId, consented]) => { - return consented ? [...acc, purposeId] : acc; - }, []); -} - -/** - * Checks if GDPR gives us access through the userConsent object. - * - * @param {Object} userConsent - * - * @return {boolean} - */ -export function hasGDPRAccess(userConsent) { - const gdprApplies = userConsent?.gdpr?.gdprApplies; - const isVendorAllowed = getVendorsConsented(userConsent).includes(IMPROVEDIGITAL_GVLID); - const arePurposesAllowed = PURPOSES.every((purpose) => getPurposesConsented(userConsent).includes(purpose)); - return !gdprApplies || (isVendorAllowed && arePurposesAllowed); -} - -/** - * Checks if USP gives us access through the userConsent object. - * - * @param {Object} userConsent - * - * @return {boolean} - */ -export function hasUSPAccess(userConsent) { - const uspProvided = userConsent?.usp; - const hasProvidedUserNotice = uspProvided?.[1] !== 'N'; - const hasNotOptedOut = uspProvided?.[2] !== 'Y'; - return !uspProvided || (hasProvidedUserNotice && hasNotOptedOut); -} - -/** - * Checks if GDPR/USP gives us access through the userConsent object. - * - * @param {Object} userConsent - * - * @return {boolean} - */ -export function hasUserConsented(userConsent) { - return hasGDPRAccess(userConsent) && hasUSPAccess(userConsent); -} - /** * Real-time user audiences retrieval * @@ -198,11 +135,9 @@ export function getBidRequestData( config, userConsent ) { - if (hasUserConsented(userConsent)) { - const audiences = getAudiences(); - if (audiences.length > 0) { - setAudiencesToBidders(reqBidsConfigObj, config, audiences); - } + const audiences = getAudiences(); + if (audiences.length > 0) { + setAudiencesToBidders(reqBidsConfigObj, config, audiences); } callback(); } @@ -212,6 +147,7 @@ export const azerionedgeSubmodule = { name: SUBREAL_TIME_MODULE, init: init, getBidRequestData: getBidRequestData, + gvlid: IMPROVEDIGITAL_GVLID, }; submodule(REAL_TIME_MODULE, azerionedgeSubmodule); diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js index c4e249e7327..0eef82a2512 100644 --- a/test/spec/modules/azerionedgeRtdProvider_spec.js +++ b/test/spec/modules/azerionedgeRtdProvider_spec.js @@ -8,14 +8,17 @@ describe('Azerion Edge RTD submodule', function () { { id: '1', visits: 123 }, { id: '2', visits: 456 }, ]; - + const IMPROVEDIGITAL_GVLID = '253'; const key = 'publisher123'; const bidders = ['appnexus', 'improvedigital']; const process = { key: 'value' }; const dataProvider = { name: 'azerionedge', waitForIt: true }; - const tcfGDPRNotApplicable = { gdprApplies: false }; - const uspNotProvided = { usp: undefined }; - const ignoreConsent = {gdpr: tcfGDPRNotApplicable, usp: uspNotProvided}; + const userConsent = {gdpr: {gdprApplies: 'gdpr-applies', consentString: 'consent-string'}, usp: 'usp'}; + + const resetAll = () => { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + } let reqBidsConfigObj; let storageStub; @@ -36,7 +39,11 @@ describe('Azerion Edge RTD submodule', function () { let returned; beforeEach(function () { - returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, ignoreConsent); + returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, userConsent); + }); + + it('should have the correct gvlid', () => { + expect(azerionedgeRTD.azerionedgeSubmodule.gvlid).to.equal(IMPROVEDIGITAL_GVLID); }); it('should return true', function () { @@ -52,18 +59,21 @@ describe('Azerion Edge RTD submodule', function () { 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({}); + [ + ['gdprApplies', userConsent.gdpr.gdprApplies], + ['gdprConsent', userConsent.gdpr.consentString], + ['uspConsent', userConsent.usp], + ].forEach(([key, value]) => { + it(`should call azerionPublisherAudiencesStub with ${key}:${value}`, function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.include({[key]: value}); + }); }); describe('with key', function () { beforeEach(function () { - window.azerionPublisherAudiences.resetHistory(); - loadExternalScript.resetHistory(); - returned = azerionedgeRTD.azerionedgeSubmodule.init({ - ...dataProvider, - params: { key }, - }, ignoreConsent); + resetAll(); + const config = { ...dataProvider, params: { key } }; + returned = azerionedgeRTD.azerionedgeSubmodule.init(config, userConsent); }); it('should return true', function () { @@ -78,120 +88,23 @@ describe('Azerion Edge RTD submodule', function () { describe('with process configuration', function () { beforeEach(function () { - window.azerionPublisherAudiences.resetHistory(); - loadExternalScript.resetHistory(); - returned = azerionedgeRTD.azerionedgeSubmodule.init({ - ...dataProvider, - params: { process }, - }, ignoreConsent); + resetAll(); + const config = { ...dataProvider, params: { process } }; + returned = azerionedgeRTD.azerionedgeSubmodule.init(config, userConsent); }); 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('GDPR access', () => { - const vendorConsented = { '253': true } - const purposesConsented = {'1': true, '3': true, '5': true, '7': true, '9': true}; - const partialPurposesConsented = {'1': true, '3': true, '5': true, '7': true}; - const tcfConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented }, purpose: { consents: purposesConsented } } }; - const tcfVendorNotConsented = { gdprApplies: true, vendorData: { purpose: {consents: purposesConsented} } }; - const tcfPurposesNotConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented } } }; - const tcfPartialPurposesNotConsented = { gdprApplies: true, vendorData: { vendor: { consents: vendorConsented }, purpose: { consents: partialPurposesConsented } } }; - - [ - ['not applicable', tcfGDPRNotApplicable, true], - ['tcf consented', tcfConsented, true], - ['tcf vendor not consented', tcfVendorNotConsented, false], - ['tcf purposes not consented', tcfPurposesNotConsented, false], - ['tcp partial purposes not consented', tcfPartialPurposesNotConsented, false], - ].forEach(([info, gdpr, expected]) => { - it(`for ${info} should return ${expected}`, () => { - expect(azerionedgeRTD.hasGDPRAccess({gdpr})).to.equal(expected); - }); - - it(`for ${info} should load=${expected} the external script`, () => { - azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, {gdpr, usp: uspNotProvided}); - expect(loadExternalScript.called).to.equal(expected); - }); - - describe('for bid request data', function () { - let callbackStub; - - beforeEach(function () { - callbackStub = sinon.mock(); - azerionedgeRTD.azerionedgeSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider, {gdpr, usp: uspNotProvided}); - }); - - it(`does call=${expected} the local storage looking for audiences`, function () { - expect(storageStub.called).to.equal(expected); - }); - - it('calls callback always', function () { - expect(callbackStub.called).to.be.true; - }); - }); - }); - }); - - describe('USP acccess', () => { - const uspMalformed = -1; - const uspNotApplicable = '1---'; - const uspUserNotifiedOptedOut = '1YY-'; - const uspUserNotifiedNotOptedOut = '1YN-'; - const uspUserNotifiedUnknownOptedOut = '1Y--'; - const uspUserNotNotifiedOptedOut = '1NY-'; - const uspUserNotNotifiedNotOptedOut = '1NN-'; - const uspUserNotNotifiedUnknownOptedOut = '1N--'; - const uspUserUnknownNotifiedOptedOut = '1-Y-'; - const uspUserUnknownNotifiedNotOptedOut = '1-N-'; - const uspUserUnknownNotifiedUnknownOptedOut = '1---'; - - [ - ['malformed', uspMalformed, true], - ['not applicable', uspNotApplicable, true], - ['not provided', uspNotProvided, true], - ['user notified and opted out', uspUserNotifiedOptedOut, false], - ['user notified and not opted out', uspUserNotifiedNotOptedOut, true], - ['user notified and unknown opted out', uspUserNotifiedUnknownOptedOut, true], - ['user not notified and opted out', uspUserNotNotifiedOptedOut, false], - ['user not notified and not opted out', uspUserNotNotifiedNotOptedOut, false], - ['user not notified and unknown opted out', uspUserNotNotifiedUnknownOptedOut, false], - ['user unknown notified and opted out', uspUserUnknownNotifiedOptedOut, false], - ['user unknown notified and not opted out', uspUserUnknownNotifiedNotOptedOut, true], - ['user unknown notified and unknown opted out', uspUserUnknownNotifiedUnknownOptedOut, true], - ].forEach(([info, usp, expected]) => { - it(`for ${info} should return ${expected}`, () => { - expect(azerionedgeRTD.hasUSPAccess({usp})).to.equal(expected); - }); - - it(`for ${info} should load=${expected} the external script`, () => { - azerionedgeRTD.azerionedgeSubmodule.init(dataProvider, {gdpr: tcfGDPRNotApplicable, usp}); - expect(loadExternalScript.called).to.equal(expected); - }); - - describe('for bid request data', function () { - let callbackStub; - - beforeEach(function () { - callbackStub = sinon.mock(); - azerionedgeRTD.azerionedgeSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider, {gdpr: tcfGDPRNotApplicable, usp}); - }); - - it(`does call=${expected} the local storage looking for audiences`, function () { - expect(storageStub.called).to.equal(expected); - }); - - it('calls callback always', function () { - expect(callbackStub.called).to.be.true; + [ + ['gdprApplies', userConsent.gdpr.gdprApplies], + ['gdprConsent', userConsent.gdpr.consentString], + ['uspConsent', userConsent.usp], + ...Object.entries(process), + ].forEach(([key, value]) => { + it(`should call azerionPublisherAudiencesStub with ${key}:${value}`, function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.include({[key]: value}); }); }); });