From 9e9819ed936c80d40ceb2fa1f164f2f6f9f08e4a Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Mon, 10 Feb 2020 16:04:27 -0800 Subject: [PATCH 01/13] add release 2.1.6 adapter changes --- rubicon/CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rubicon/CHANGES.md b/rubicon/CHANGES.md index e1b417b5..ce112ca8 100644 --- a/rubicon/CHANGES.md +++ b/rubicon/CHANGES.md @@ -1,3 +1,9 @@ +# 2.1.6 + +- Adapter changes: + - schain support: reads the schain object to create a serialized obj for sending with `fastlane.json` as `rp_schain` + - CCPA support + # 2.1.5 - Adapter changes: From dc34f0909ba21dc46077427e3de9b5819b25ba79 Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Mon, 10 Feb 2020 17:40:51 -0800 Subject: [PATCH 02/13] add uspConsent --- rubicon/DOCUMENTATION.md | 22 +++++++++++++++++++--- rubicon/rubicon-htb.js | 21 +++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/rubicon/DOCUMENTATION.md b/rubicon/DOCUMENTATION.md index 1f149f74..e7367eff 100644 --- a/rubicon/DOCUMENTATION.md +++ b/rubicon/DOCUMENTATION.md @@ -41,11 +41,27 @@ ### Parameters | Key | Required | Type | Description | |---|---|---|---| +| gdpr | No | object | An object noting whether GDPR applies (`applies` field) and the consent string (`consent` field) | +| us_privacy | No | object | An object noting the USP consent string (`consent` field) | | | | | | ### Example -```javascript - +```json +{ + "url": "https://localhost:5838/public/debugger/adapter-debugger.html", + "referrer": "https://localhost:5838/public/debugger/adapter-debugger.html", + "gdpr": { + "applies": true, + "consent": "TEST_GDPR_CONSENT_STRING" + }, + "us_privacy": { + "consent": "TEST_USP_CONSENT_STRING" + }, + "bidRequests": [{ + "adUnitId": "10809467961050726", + "requestId": "_hK5vIC5I" + }] +} ``` ## Bid Response Information @@ -66,4 +82,4 @@ ### Example ```javascript -``` \ No newline at end of file +``` diff --git a/rubicon/rubicon-htb.js b/rubicon/rubicon-htb.js index 271ee136..1b043d6c 100644 --- a/rubicon/rubicon-htb.js +++ b/rubicon/rubicon-htb.js @@ -403,6 +403,7 @@ function RubiconModule(configs) { var referrer = Browser.getPageUrl(); var gdprConsent = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(); + var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); var privacyEnabled = ComplianceService.isPrivacyEnabled(); /* eslint-disable camelcase */ var queryObj = { @@ -425,13 +426,21 @@ function RubiconModule(configs) { } /* eslint-enable camelcase */ - if (gdprConsent && privacyEnabled && typeof gdprConsent === 'object') { - if (typeof gdprConsent.applies === 'boolean') { - queryObj.gdpr = Number(gdprConsent.applies); + if (privacyEnabled) { + if (gdprConsent && typeof gdprConsent === 'object') { + if (typeof gdprConsent.applies === 'boolean') { + queryObj.gdpr = Number(gdprConsent.applies); + } + /* eslint-disable camelcase */ + queryObj.gdpr_consent = gdprConsent.consentString; + /* eslint-enable camelcase */ + } + + if (uspConsent && typeof uspConsent === 'object' && uspConsent.hasOwnProperty('uspString')) { + /* eslint-disable camelcase */ + queryObj.us_privacy = encodeURIComponent(uspConsent.uspString); + /* eslint-enable camelcase */ } - /* eslint-disable camelcase */ - queryObj.gdpr_consent = gdprConsent.consentString; - /* eslint-enable camelcase */ } for (var pageInv in pageFirstPartyData.inventory) { From c8c790b3e6c3475bae0b768fd0d71411b5ba7291 Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Tue, 11 Feb 2020 04:22:58 -0800 Subject: [PATCH 03/13] add schain support --- rubicon/rubicon-htb.js | 61 +++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/rubicon/rubicon-htb.js b/rubicon/rubicon-htb.js index 1b043d6c..9f96a9da 100644 --- a/rubicon/rubicon-htb.js +++ b/rubicon/rubicon-htb.js @@ -303,6 +303,40 @@ function RubiconModule(configs) { return _dt; } + /** + * Properly sorts schain object params + * @param {Array} nodes + * @returns {String} + */ + function __serializeSupplyChainNodes(nodes) { + return nodes.map(function(node) { + return ['asi', 'sid', 'hp', 'rid', 'name', 'domain'].map(function(prop) { + return encodeURIComponent(node[prop] || ''); + }).join(','); + }).join('!'); + } + + /** + * Serializes schain params according to OpenRTB requirements + * @param {Object} schain + * @returns {String} + */ + function __serializeSupplyChain(schain) { + if (!__hasValidSupplyChainParams(schain)) return ''; + return schain.ver + ',' + schain.complete + '!' + __serializeSupplyChainNodes(schain.nodes); + } + + function __hasValidSupplyChainParams(schain) { + if (!schain.nodes) return false; + // if (!isValid) utils.logError('Rubicon: required schain params missing'); + return schain.nodes.reduce(function(status, node) { + if (!status) return status; + return ['asi', 'sid', 'hp'].every(function(field) { + return node[field]; + }); + }, true); + } + /** * Generates the request URL to the endpoint for the xSlots in the given * returnParcels. @@ -402,9 +436,6 @@ function RubiconModule(configs) { var rubiSizeIds = __mapSizesToRubiconSizeIds(parcel.xSlotRef.sizes); var referrer = Browser.getPageUrl(); - var gdprConsent = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(); - var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); - var privacyEnabled = ComplianceService.isPrivacyEnabled(); /* eslint-disable camelcase */ var queryObj = { account_id: configs.accountId, @@ -424,25 +455,30 @@ function RubiconModule(configs) { if (slotFirstPartyData.position) { queryObj.p_pos = slotFirstPartyData.position; } - /* eslint-enable camelcase */ - if (privacyEnabled) { + if (ComplianceService.isPrivacyEnabled()) { + var gdprConsent = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(); + var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); + if (gdprConsent && typeof gdprConsent === 'object') { if (typeof gdprConsent.applies === 'boolean') { queryObj.gdpr = Number(gdprConsent.applies); } - /* eslint-disable camelcase */ queryObj.gdpr_consent = gdprConsent.consentString; - /* eslint-enable camelcase */ } - if (uspConsent && typeof uspConsent === 'object' && uspConsent.hasOwnProperty('uspString')) { - /* eslint-disable camelcase */ + if (uspConsent && typeof uspConsent === 'object') { queryObj.us_privacy = encodeURIComponent(uspConsent.uspString); - /* eslint-enable camelcase */ } } + // if SupplyChain is supplied and contains all required fields + if (parcel.schain && __hasValidSupplyChainParams(parcel.schain)) { + queryObj.rp_schain = __serializeSupplyChain(parcel.schain); + } + + /* eslint-enable camelcase */ + for (var pageInv in pageFirstPartyData.inventory) { if (!pageFirstPartyData.inventory.hasOwnProperty(pageInv)) { continue; @@ -495,10 +531,9 @@ function RubiconModule(configs) { } if (rubiSizeIds.length > 1) { - /* eslint-disable camelcase */ + // eslint-disable-next-line camelcase queryObj.alt_size_ids = rubiSizeIds.slice(1) .join(','); - /* eslint-enable camelcase */ } return { @@ -758,7 +793,7 @@ function RubiconModule(configs) { partnerId: 'RubiconHtb', namespace: 'RubiconHtb', statsId: 'RUBI', - version: '2.1.5', + version: '2.1.6', targetingType: 'slot', enabledAnalytics: { requestTime: true From a1db01d4ea4baa3438d20f6f6d2e6b042a707f3e Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Thu, 13 Feb 2020 01:56:29 -0800 Subject: [PATCH 04/13] fix schain config and parcel location --- rubicon/rubicon-htb-system-tests.js | 23 ++++++++++++++++++++++- rubicon/rubicon-htb-validator.js | 9 +++++++++ rubicon/rubicon-htb.js | 10 +++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/rubicon/rubicon-htb-system-tests.js b/rubicon/rubicon-htb-system-tests.js index 6d17c881..f751de72 100644 --- a/rubicon/rubicon-htb-system-tests.js +++ b/rubicon/rubicon-htb-system-tests.js @@ -47,6 +47,11 @@ function validateBidRequestWithPrivacy(request) { expect(r.gdpr_consent).toBe('TEST_GDPR_CONSENT_STRING'); } +function validateBidRequestWithCCPA(request) { + var request = request.query; + +} + function getConfig() { return { accountId: '1234', @@ -54,7 +59,23 @@ function getConfig() { 1: { siteId: '112233', zoneId: '556677', - sizes: [[300, 250], [300, 600]] + sizes: [[300, 250], [300, 600]], + schain: { + validation: 'strict', + config: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'indirectseller.com', + sid: '00001', + hp: 1 + }, { + asi: 'indirectseller-2.com', + sid: '00002', + hp: 1 + }] + } + } } } }; diff --git a/rubicon/rubicon-htb-validator.js b/rubicon/rubicon-htb-validator.js index 9f8004c3..78c3d5f2 100644 --- a/rubicon/rubicon-htb-validator.js +++ b/rubicon/rubicon-htb-validator.js @@ -183,6 +183,15 @@ function partnerValidator(configs) { } } } + }, + schain: { + optional: true, + type: 'object', + properties: { + nodes: { + type: 'array' + } + } } } } diff --git a/rubicon/rubicon-htb.js b/rubicon/rubicon-htb.js index 9f96a9da..704fb81e 100644 --- a/rubicon/rubicon-htb.js +++ b/rubicon/rubicon-htb.js @@ -372,7 +372,6 @@ function RubiconModule(configs) { strict: true, properties: { keywords: { - optional: true, type: 'array', items: { type: 'string' @@ -403,6 +402,15 @@ function RubiconModule(configs) { position: { optional: true, type: 'string' + }, + schain: { + optional: true, + type: 'object', + properties: { + nodes: { + type: 'object' + } + } } } } From b2f0b7ec4c1d03afa16f1d65cd7f0241afd6cd4b Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Thu, 13 Feb 2020 14:13:34 -0800 Subject: [PATCH 05/13] formatting fixes, commenting updates --- rubicon/rubicon-htb-system-tests.js | 38 +++++++++++++---------------- rubicon/rubicon-htb-validator.js | 18 +++++++------- rubicon/rubicon-htb.js | 27 +++++++++----------- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/rubicon/rubicon-htb-system-tests.js b/rubicon/rubicon-htb-system-tests.js index f751de72..2725f7a9 100644 --- a/rubicon/rubicon-htb-system-tests.js +++ b/rubicon/rubicon-htb-system-tests.js @@ -37,6 +37,8 @@ function validateBidRequest(request) { expect(r.alt_size_ids).toBe('10'); expect(r.rf).toEqual(jasmine.anything()); + + expect(r.schain).toBeDefined(); } function validateBidRequestWithPrivacy(request) { @@ -45,11 +47,8 @@ function validateBidRequestWithPrivacy(request) { expect(r.gdpr).toBe('1'); expect(r.gdpr_consent).toBe('TEST_GDPR_CONSENT_STRING'); -} - -function validateBidRequestWithCCPA(request) { - var request = request.query; + expect(r.us_privacy).toBeDefined(); } function getConfig() { @@ -59,24 +58,21 @@ function getConfig() { 1: { siteId: '112233', zoneId: '556677', - sizes: [[300, 250], [300, 600]], - schain: { - validation: 'strict', - config: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'indirectseller.com', - sid: '00001', - hp: 1 - }, { - asi: 'indirectseller-2.com', - sid: '00002', - hp: 1 - }] - } - } + sizes: [[300, 250], [300, 600]] } + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'indirectseller.com', + sid: '00001', + hp: 1 + }, { + asi: 'indirectseller-2.com', + sid: '00002', + hp: 1 + }] } }; } diff --git a/rubicon/rubicon-htb-validator.js b/rubicon/rubicon-htb-validator.js index 78c3d5f2..966f82d1 100644 --- a/rubicon/rubicon-htb-validator.js +++ b/rubicon/rubicon-htb-validator.js @@ -183,15 +183,6 @@ function partnerValidator(configs) { } } } - }, - schain: { - optional: true, - type: 'object', - properties: { - nodes: { - type: 'array' - } - } } } } @@ -323,6 +314,15 @@ function partnerValidator(configs) { } } } + }, + schain: { + optional: true, + type: 'object', + properties: { + nodes: { + type: 'array' + } + } } } }, configs); diff --git a/rubicon/rubicon-htb.js b/rubicon/rubicon-htb.js index 704fb81e..c7bf7adb 100644 --- a/rubicon/rubicon-htb.js +++ b/rubicon/rubicon-htb.js @@ -308,6 +308,7 @@ function RubiconModule(configs) { * @param {Array} nodes * @returns {String} */ + function __serializeSupplyChainNodes(nodes) { return nodes.map(function(node) { return ['asi', 'sid', 'hp', 'rid', 'name', 'domain'].map(function(prop) { @@ -321,14 +322,20 @@ function RubiconModule(configs) { * @param {Object} schain * @returns {String} */ + function __serializeSupplyChain(schain) { if (!__hasValidSupplyChainParams(schain)) return ''; return schain.ver + ',' + schain.complete + '!' + __serializeSupplyChainNodes(schain.nodes); } + /** + * Validate schain params + * @param {Object} schain + * @returns {boolean} + */ + function __hasValidSupplyChainParams(schain) { if (!schain.nodes) return false; - // if (!isValid) utils.logError('Rubicon: required schain params missing'); return schain.nodes.reduce(function(status, node) { if (!status) return status; return ['asi', 'sid', 'hp'].every(function(field) { @@ -402,15 +409,6 @@ function RubiconModule(configs) { position: { optional: true, type: 'string' - }, - schain: { - optional: true, - type: 'object', - properties: { - nodes: { - type: 'object' - } - } } } } @@ -466,8 +464,6 @@ function RubiconModule(configs) { if (ComplianceService.isPrivacyEnabled()) { var gdprConsent = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(); - var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); - if (gdprConsent && typeof gdprConsent === 'object') { if (typeof gdprConsent.applies === 'boolean') { queryObj.gdpr = Number(gdprConsent.applies); @@ -475,14 +471,15 @@ function RubiconModule(configs) { queryObj.gdpr_consent = gdprConsent.consentString; } + var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); if (uspConsent && typeof uspConsent === 'object') { queryObj.us_privacy = encodeURIComponent(uspConsent.uspString); } } - // if SupplyChain is supplied and contains all required fields - if (parcel.schain && __hasValidSupplyChainParams(parcel.schain)) { - queryObj.rp_schain = __serializeSupplyChain(parcel.schain); + // Add schain if it exists and contains all required fields + if (configs.schain && __hasValidSupplyChainParams(configs.schain)) { + queryObj.rp_schain = __serializeSupplyChain(configs.schain); } /* eslint-enable camelcase */ From 2715e21fd4b8d4f6fbfdffa576558c7031f358e2 Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Thu, 13 Feb 2020 14:51:09 -0800 Subject: [PATCH 06/13] test fix --- rubicon/rubicon-htb-system-tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rubicon/rubicon-htb-system-tests.js b/rubicon/rubicon-htb-system-tests.js index 2725f7a9..26b04e2f 100644 --- a/rubicon/rubicon-htb-system-tests.js +++ b/rubicon/rubicon-htb-system-tests.js @@ -38,7 +38,7 @@ function validateBidRequest(request) { expect(r.rf).toEqual(jasmine.anything()); - expect(r.schain).toBeDefined(); + expect(r.rp_schain).toBeDefined(); } function validateBidRequestWithPrivacy(request) { From ccedba3f706299aca533abba55309bb006f407dd Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Thu, 13 Feb 2020 14:54:01 -0800 Subject: [PATCH 07/13] reverted doc update --- rubicon/DOCUMENTATION.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/rubicon/DOCUMENTATION.md b/rubicon/DOCUMENTATION.md index e7367eff..58cdfc70 100644 --- a/rubicon/DOCUMENTATION.md +++ b/rubicon/DOCUMENTATION.md @@ -41,27 +41,11 @@ ### Parameters | Key | Required | Type | Description | |---|---|---|---| -| gdpr | No | object | An object noting whether GDPR applies (`applies` field) and the consent string (`consent` field) | -| us_privacy | No | object | An object noting the USP consent string (`consent` field) | | | | | | ### Example -```json -{ - "url": "https://localhost:5838/public/debugger/adapter-debugger.html", - "referrer": "https://localhost:5838/public/debugger/adapter-debugger.html", - "gdpr": { - "applies": true, - "consent": "TEST_GDPR_CONSENT_STRING" - }, - "us_privacy": { - "consent": "TEST_USP_CONSENT_STRING" - }, - "bidRequests": [{ - "adUnitId": "10809467961050726", - "requestId": "_hK5vIC5I" - }] -} +```javascript + ``` ## Bid Response Information From 4815ee11b0551c02c8e0e8af6c21a5550447704f Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Thu, 13 Feb 2020 15:00:18 -0800 Subject: [PATCH 08/13] remove added line-break --- rubicon/DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rubicon/DOCUMENTATION.md b/rubicon/DOCUMENTATION.md index 58cdfc70..1f149f74 100644 --- a/rubicon/DOCUMENTATION.md +++ b/rubicon/DOCUMENTATION.md @@ -66,4 +66,4 @@ ### Example ```javascript -``` +``` \ No newline at end of file From 14f4927b2139d1561af32d34495a8769b8f16c32 Mon Sep 17 00:00:00 2001 From: Isaac Dettman Date: Mon, 24 Feb 2020 11:26:08 -0800 Subject: [PATCH 09/13] fix lint errors --- rubicon/rubicon-htb-system-tests.js | 21 +++++----- rubicon/rubicon-htb.js | 60 ++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/rubicon/rubicon-htb-system-tests.js b/rubicon/rubicon-htb-system-tests.js index 26b04e2f..78bf8cb4 100644 --- a/rubicon/rubicon-htb-system-tests.js +++ b/rubicon/rubicon-htb-system-tests.js @@ -64,15 +64,18 @@ function getConfig() { schain: { ver: '1.0', complete: 1, - nodes: [{ - asi: 'indirectseller.com', - sid: '00001', - hp: 1 - }, { - asi: 'indirectseller-2.com', - sid: '00002', - hp: 1 - }] + nodes: [ + { + asi: 'indirectseller.com', + sid: '00001', + hp: 1 + }, + { + asi: 'indirectseller-2.com', + sid: '00002', + hp: 1 + } + ] } }; } diff --git a/rubicon/rubicon-htb.js b/rubicon/rubicon-htb.js index c7bf7adb..17e7d7a9 100644 --- a/rubicon/rubicon-htb.js +++ b/rubicon/rubicon-htb.js @@ -310,22 +310,20 @@ function RubiconModule(configs) { */ function __serializeSupplyChainNodes(nodes) { - return nodes.map(function(node) { - return ['asi', 'sid', 'hp', 'rid', 'name', 'domain'].map(function(prop) { + return nodes.map(function (node) { + return [ + 'asi', + 'sid', + 'hp', + 'rid', + 'name', + 'domain' + ].map(function (prop) { return encodeURIComponent(node[prop] || ''); - }).join(','); - }).join('!'); - } - - /** - * Serializes schain params according to OpenRTB requirements - * @param {Object} schain - * @returns {String} - */ - - function __serializeSupplyChain(schain) { - if (!__hasValidSupplyChainParams(schain)) return ''; - return schain.ver + ',' + schain.complete + '!' + __serializeSupplyChainNodes(schain.nodes); + }) + .join(','); + }) + .join('!'); } /** @@ -335,15 +333,39 @@ function RubiconModule(configs) { */ function __hasValidSupplyChainParams(schain) { - if (!schain.nodes) return false; - return schain.nodes.reduce(function(status, node) { - if (!status) return status; - return ['asi', 'sid', 'hp'].every(function(field) { + if (!schain.nodes) { + return false; + } + + return schain.nodes.reduce(function (nodeStatus, node) { + if (!nodeStatus) { + return nodeStatus; + } + + return [ + 'asi', + 'sid', + 'hp' + ].every(function (field) { return node[field]; }); }, true); } + /** + * Serializes schain params according to OpenRTB requirements + * @param {Object} schain + * @returns {String} + */ + + function __serializeSupplyChain(schain) { + if (!__hasValidSupplyChainParams(schain)) { + return ''; + } + + return schain.ver + ',' + schain.complete + '!' + __serializeSupplyChainNodes(schain.nodes); + } + /** * Generates the request URL to the endpoint for the xSlots in the given * returnParcels. From f3c707627cec999eee756cce36b57f47c0c6b917 Mon Sep 17 00:00:00 2001 From: Daniel Cassidy Date: Thu, 10 Sep 2020 18:44:09 +0100 Subject: [PATCH 10/13] HTP-131 Consumable: CCPA/GDPR Consent Support (#187) * Import Consumable adapter from external repository. https://github.com/indexexchange/consumable-adapter/ * consumable: Use Utilities.isNumber in preference to typeof operator. * consumable: Add missing 'unitId' and 'unitName' parameters to bid request. * consumable: Implement CCPA compliance. * consumable: Implement GDPR compliance. * consumable: Expect GDPR compliance information to be present in the bid request. * consumable: Update documentation: Consumable supports consent now. * consumable: Correct documentation of bid requests. * consumable: Correct configuration documentation. * consumable: Update version number and changelog. --- consumable/CHANGES.md | 4 + consumable/DOCUMENTATION.md | 152 ++++++ consumable/consumable-htb-exports.js | 5 + consumable/consumable-htb-system-tests.js | 148 ++++++ consumable/consumable-htb-validator.js | 80 ++++ consumable/consumable-htb.js | 537 ++++++++++++++++++++++ 6 files changed, 926 insertions(+) create mode 100644 consumable/CHANGES.md create mode 100644 consumable/DOCUMENTATION.md create mode 100644 consumable/consumable-htb-exports.js create mode 100644 consumable/consumable-htb-system-tests.js create mode 100644 consumable/consumable-htb-validator.js create mode 100644 consumable/consumable-htb.js diff --git a/consumable/CHANGES.md b/consumable/CHANGES.md new file mode 100644 index 00000000..cc7c71fc --- /dev/null +++ b/consumable/CHANGES.md @@ -0,0 +1,4 @@ +# 2.1.0 +- Add support for CCPA and GDPR consent information. +- Add missing parameters to bid request. +- Improve documentation. diff --git a/consumable/DOCUMENTATION.md b/consumable/DOCUMENTATION.md new file mode 100644 index 00000000..f903725f --- /dev/null +++ b/consumable/DOCUMENTATION.md @@ -0,0 +1,152 @@ +# Consumable +## General Compatibility +|Feature| | +|---|---| +| Consent | Yes | +| Native Ad Support | Yes | +| SafeFrame Support | Yes | +| PMP Support | No | + +## Browser Compatibility +| Browser | | +|--- |---| +| Chrome | Yes | +| Edge | Yes | +| Firefox | Yes | +| Internet Explorer 9 | No | +| Internet Explorer 10 | Yes | +| Internet Explorer 11 | Yes | +| Safari | Yes | +| Mobile Chrome | Yes | +| Mobile Safari | Yes | +| UC Browser | No | +| Samsung Internet | No | +| Opera | Yes | + +## Adapter Information +| Info | | +|---|---| +| Partner Id | ConsumableHtb | +| Ad Server Responds in (Cents, Dollars, etc) | Dollars | +| Bid Type (Gross / Net) | Net | +| GAM Key (Open Market) | ix_cnsm_id | +| GAM Key (Private Market) | ix_cnsm_dealid | +| Ad Server URLs | serverbid.com | +| Slot Mapping Style (Size / Multiple Sizes / Slot) | Size | +| Request Architecture (MRA / SRA) | SRA | + +## Currencies Supported +USD $ + +## Bid Request Information +### Parameters +| Key | Required | Type | Description | +|---|---|---|---| +| `placements` | Yes | `Array\` | List of ad placements (slots) to bid on | +| `time` | Yes | `Integer` | Time and date of the request as milliseconds elapsed since January 1, 1970 | +| `user` | Yes | `Object` | Currently always an empty object | +| `url` | Yes | `String` | URL of the current page | +| `referrer` | No | `String` | URL of the referrer | +| `enableBotFiltering` | Yes | `Boolean` | Always `true` | +| `includePricingData` | Yes | `Boolean` | Always `true` | +| `parallel` | Yes | `Boolean` | Always `true` | + +### Placement +| Key | Required | Type | Description | +| --- | --- | --- | --- | +| `divName` | Yes | `String` | Name of the slot to bid on | +| `adTypes` | Yes | `Array\` | List of IDs that determines possible ad sizes | +| `siteId` | Yes | `String` | Fixed identifier supplied by Consumable | +| `networkId` | Yes | `String` | Fixed identifier supplied by Consumable | +| `zoneIds` | No | `Array\` | List of fixed identifiers supplied by Consumable +| `unitId` | No | `String` | Fixed identifier supplied by Consumable | +| `unitName` | No | `String` | Fixed identifier supplied by Consumable | + +### Example +```json +{ + "placements":[ + { + "divName": "28cbf8f87b7d5", + "adTypes": [5], + "siteId": "1035514", + "networkId": "9969", + "zoneIds": [188825], + "unitId": "4508", + "unitName": "cnsmbl-audio-320x50-slider" + } + ], + "time": 1538599207715, + "user": {}, + "url": "http://example.com/page/path", + "referrer": "http://example.com/another/page", + "enableBotFiltering": true, + "includePricingData": true, + "parallel": true +} +``` + + +## Bid Response Information +### Bid Example +```json +{ + "decisions":{ + "2e53ecd4a29b06":{ + "adId":-7176956978374674467, + "impressionUrl":"https://e.serverbid.com/i/?i=ARAAAAAAAAAAcP...", + "contents":[ + { + "body":"", + "data":null, + "type":"rtb" + } + ], + "height":250, + "width":300, + "pricing":{ + "clearPrice":0.3075, + "eCPM":0.0, + "price":0.0, + "rateType":2, + "revenue":0.0 + } + } + } +} +``` + +### Pass Example +```json +{ + "user": { + "key": "ad39231daeb043f2a9610414f08394b5" + }, + "decisions": { + "2e53ecd4a29b06": null + } +} +``` + +## Configuration Information +### Configuration Keys +| Key | Required | Type | Description | +|---|---|---|---| +| `networkId` | Yes | `String` | Network ID (supplied by Consumable) | +| `siteId` | Yes | `String` | Site ID (supplied by Consumable) | +| `zoneIds` | No | `Array\` | Zone IDs (supplied by Consumable) | +| `unitId` | Yes | `String` | Unit ID (supplied by Consumable) | +| `unitName` | Yes | `String` | Unit Name (supplied by Consumable) | +| `sizes` | Yes | `Array\<\[Integer, Integer\]\>` | Possible sizes of the ad in pixels (`\[width, height\]`) | + +### Example +``` +{ + "networkId": "9969", + "siteId": "1029010", + "zoneIds": [187327], + "unitId": "4508", + "unitName": "cnsmbl-audio-320x50-slider", + "sizes": [[320, 50]] +} +``` \ No newline at end of file diff --git a/consumable/consumable-htb-exports.js b/consumable/consumable-htb-exports.js new file mode 100644 index 00000000..40e364ee --- /dev/null +++ b/consumable/consumable-htb-exports.js @@ -0,0 +1,5 @@ +//? if (FEATURES.GPT_LINE_ITEMS) { +shellInterface.ConsumableHtb = { + render: SpaceCamp.services.RenderService.renderDfpAd.bind(null, 'ConsumableHtb') +}; +//? } diff --git a/consumable/consumable-htb-system-tests.js b/consumable/consumable-htb-system-tests.js new file mode 100644 index 00000000..1b1bfa96 --- /dev/null +++ b/consumable/consumable-htb-system-tests.js @@ -0,0 +1,148 @@ +'use strict'; + +function getPartnerId() { + return 'ConsumableHtb'; +} + +function getStatsId() { + return 'CNSM'; +} + +function getBidRequestRegex() { + return { + method: 'POST', + urlRegex: /^https?:\/\/e\.serverbid\.com\/api\// + }; +} + +function getCallbackType() { + return 'NONE'; +} + +function getArchitecture() { + return 'SRA'; +} + +function getConfig() { + return { + xSlots: { + 1: { + networkId: '9969', + siteId: '1029010', + zoneIds: [187327], + unitId: '4508', + unitName: 'cnsmbl-audio-320x50-slider', + sizes: [[320, 50]] + }, + 2: { + networkId: '9969', + siteId: '1029010', + zoneIds: [187327], + unitId: '4508', + unitName: 'cnsmbl-audio-320x50-slider', + sizes: [[320, 50]] + } + } + }; +} + +function validateBidRequest(request) { + expect(request.host).toBe('e.serverbid.com'); + expect(request.pathname).toBe('/api/v2'); + expect(request.query).toEqual({}); + + var data = JSON.parse(request.body); + expect(data.gdpr).toBeDefined(); + expect(data.gdpr.applies).toBeDefined(); + expect(data.gdpr.consent).toBeDefined(); +} + +function getValidResponse(request, creative) { + return JSON.stringify({ + user: { + key: 'ad39231daeb043f2a9610414f08394b5' + }, + decisions: { + 1: { + adId: 1234, + creativeId: 2345, + flightId: 3456, + campaignId: 4567, + clickUrl: 'http://example.org/click', + impressionUrl: 'http://example.org/impression', + contents: [ + { + type: 'html', + body: creative + } + ], + height: 50, + width: 320, + events: [], + pricing: { + price: 0, + clearPrice: 2, + revenue: 0.002, + rateType: 2, + eCPM: 0 + } + }, + 2: { + adId: 1234, + creativeId: 2345, + flightId: 3456, + campaignId: 4567, + clickUrl: 'http://example.org/click', + impressionUrl: 'http://example.org/impression', + contents: [ + { + type: 'html', + body: creative + } + ], + height: 50, + width: 320, + events: [], + pricing: { + price: 0, + clearPrice: 2, + revenue: 0.002, + rateType: 2, + eCPM: 0 + } + } + } + }); +} + +function validateTargeting(targetingMap) { + expect(targetingMap).toEqual(jasmine.objectContaining({ + ix_cnsm_id: jasmine.arrayWithExactContents([jasmine.any(String), jasmine.any(String)]), + ix_cnsm_cpm: jasmine.arrayWithExactContents(['320x50_200', '320x50_200']) + })); +} + +function getPassResponse() { + return JSON.stringify({ + user: { + key: 'ad39231daeb043f2a9610414f08394b5' + }, + decisions: { + 1: null, + 2: null + } + }); +} + +module.exports = { + getPartnerId: getPartnerId, + getStatsId: getStatsId, + getBidRequestRegex: getBidRequestRegex, + getCallbackType: getCallbackType, + getArchitecture: getArchitecture, + getConfig: getConfig, + validateBidRequest: validateBidRequest, + getValidResponse: getValidResponse, + validateTargeting: validateTargeting, + getPassResponse: getPassResponse +}; diff --git a/consumable/consumable-htb-validator.js b/consumable/consumable-htb-validator.js new file mode 100644 index 00000000..00242cd8 --- /dev/null +++ b/consumable/consumable-htb-validator.js @@ -0,0 +1,80 @@ +'use strict'; + +//////////////////////////////////////////////////////////////////////////////// +// Dependencies //////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +var Inspector = require('../../../libs/external/schema-inspector.js'); + +//////////////////////////////////////////////////////////////////////////////// +// Main //////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/* ============================================================================= + * STEP 0 | Config Validation + * ----------------------------------------------------------------------------- + * This file contains the necessary validation for the partner configuration. + * This validation will be performed on the partner specific configuration object + * that is passed into the wrapper. The wrapper uses an outside library called + * schema-insepctor to perform the validation. Information about it can be found here: + * https://atinux.fr/schema-inspector/. + */ +function partnerValidator(configs) { + var result = Inspector.validate({ + type: 'object', + properties: { + xSlots: { + type: 'object', + properties: { + '*': { + type: 'object', + properties: { + networkId: { + type: 'string', + minLength: 1 + }, + siteId: { + type: 'string', + minLength: 1 + }, + zoneIds: { + type: 'array', + optional: true, + items: { + type: 'number' + } + }, + unitId: { + type: 'string', + minLength: 1 + }, + unitName: { + type: 'string', + minLength: 1 + }, + sizes: { + type: 'array', + minLength: 1, + items: { + type: 'array', + exactLength: 2, + items: { + type: 'number' + } + } + } + } + } + } + } + } + }, configs); + + if (!result.valid) { + return result.format(); + } + + return null; +} + +module.exports = partnerValidator; diff --git a/consumable/consumable-htb.js b/consumable/consumable-htb.js new file mode 100644 index 00000000..d7e7d86b --- /dev/null +++ b/consumable/consumable-htb.js @@ -0,0 +1,537 @@ +'use strict'; + +//////////////////////////////////////////////////////////////////////////////// +// Dependencies //////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +var Browser = require('browser.js'); +var Classify = require('classify.js'); +var Constants = require('constants.js'); +var Partner = require('partner.js'); +var Size = require('size.js'); +var SpaceCamp = require('space-camp.js'); +var System = require('system.js'); +var Network = require('network.js'); +var Utilities = require('utilities.js'); + +var RenderService; +var ComplianceService; + +//? if (DEBUG) { +var ConfigValidators = require('config-validators.js'); +var PartnerSpecificValidator = require('consumable-htb-validator.js'); +var Scribe = require('scribe.js'); +var Whoopsie = require('whoopsie.js'); +//? } + +//////////////////////////////////////////////////////////////////////////////// +// Main //////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Partner module template + * + * @class + */ +function ConsumableHtb(configs) { + /* ===================================== + * Data + * ---------------------------------- */ + + /* Private + * ---------------------------------- */ + + /** + * Reference to the partner base class. + * + * @private {object} + */ + var __baseClass; + + /** + * Profile for this partner. + * + * @private {object} + */ + var __profile; + + /* ===================================== + * Functions + * ---------------------------------- */ + + /* Utilities + * ---------------------------------- */ + + var adTypesBySize = { + '120x90': 1, + '468x60': 3, + '728x90': 4, + '300x250': 5, + '160x600': 6, + '120x600': 7, + '300x100': 8, + '180x150': 9, + '336x280': 10, + '240x400': 11, + '234x60': 12, + '88x31': 13, + '120x60': 14, + '120x240': 15, + '125x125': 16, + '220x250': 17, + '250x250': 18, + '250x90': 19, + '0x0': 20, + '200x90': 21, + '300x50': 22, + '320x50': 23, + '320x480': 24, + '185x185': 25, + '620x45': 26, + '300x125': 27, + '800x250': 28, + '300x600': 43, + '970x90': 77, + '970x250': 123, + '970x66': 286, + '320x250': 331, + '700x500': 374, + '486x60': 429, + '300x1050': 934, + '320x100': 1578, + '728x250': 2730, + '970x280': 3230, + '320x267': 3301 + }; + + function sizeToAdType(size) { + return adTypesBySize[Size.arrayToString(size)]; + } + + /** + * Generates the request URL and query data to the endpoint for the xSlots + * in the given returnParcels. + * + * @param {object[]} returnParcels + * + * @return {object} + */ + function __generateRequestObj(returnParcels) { + /* ============================================================================= + * STEP 2 | Generate Request URL + * ----------------------------------------------------------------------------- + * + * Generate the URL to request demand from the partner endpoint using the provided + * returnParcels. The returnParcels is an array of objects each object containing + * an .xSlotRef which is a reference to the xSlot object from the partner configuration. + * Use this to retrieve the placements/xSlots you need to request for. + * + * If your partner is MRA, returnParcels will be an array of length one. If your + * partner is SRA, it will contain any number of entities. In any event, the full + * contents of the array should be able to fit into a single request and the + * return value of this function should similarly represent a single request to the + * endpoint. + * + * Return an object containing: + * queryUrl: the url for the request + * data: the query object containing a map of the query string paramaters + * + * callbackId: + * + * arbitrary id to match the request with the response in the callback function. If + * your endpoint supports passing in an arbitrary ID and returning it as part of the response + * please use the callbackType: Partner.CallbackTypes.ID and fill out the adResponseCallback. + * Also please provide this adResponseCallback to your bid request here so that the JSONP + * response calls it once it has completed. + * + * If your endpoint does not support passing in an ID, simply use + * Partner.CallbackTypes.CALLBACK_NAME and the wrapper will take care of handling request + * matching by generating unique callbacks for each request using the callbackId. + * + * If your endpoint is ajax only, please set the appropriate values in your profile for this, + * i.e. Partner.CallbackTypes.NONE and Partner.Requesttypes.AJAX. You also do not need to provide + * a callbackId in this case because there is no callback. + * + * The return object should look something like this: + * { + * url: 'http://bidserver.com/api/bids' // base request url for a GET/POST request + * data: { // query string object that will be attached to the base url + * slots: [ + * { + * placementId: 54321, + * sizes: [[300, 250]] + * },{ + * placementId: 12345, + * sizes: [[300, 600]] + * },{ + * placementId: 654321, + * sizes: [[728, 90]] + * } + * ], + * site: 'http://google.com' + * }, + * callbackId: '_23sd2ij4i1' //unique id used for pairing requests and responses + * } + */ + + var callbackId = System.generateUniqueId(); + + /* ------------------------ Get consent information ------------------------- + * If you want to implement GDPR consent in your adapter, use the function + * ComplianceService.gdpr.getConsent() which will return an object. + * + * Here is what the values in that object mean: + * - applies: the boolean value indicating if the request is subject to + * GDPR regulations + * - consentString: the consent string developed by GDPR Consent Working + * Group under the auspices of IAB Europe + * + * The return object should look something like this: + * { + * applies: true, + * consentString: "BOQ7WlgOQ7WlgABABwAAABJOACgACAAQABA" + * } + * + * You can also determine whether or not the publisher has enabled privacy + * features in their wrapper by querying ComplianceService.isPrivacyEnabled(). + * + * This function will return a boolean, which indicates whether the wrapper's + * privacy features are on (true) or off (false). If they are off, the values + * returned from gdpr.getConsent() are safe defaults and no attempt has been + * made by the wrapper to contact a Consent Management Platform. + */ + + var data = { + placements: returnParcels.map(function (parcel) { + return { + networkId: parcel.xSlotRef.networkId, + siteId: parcel.xSlotRef.siteId, + zoneIds: parcel.xSlotRef.zoneIds, + unitId: parcel.xSlotRef.unitId, + unitName: parcel.xSlotRef.unitName, + divName: parcel.xSlotName, + adTypes: parcel.xSlotRef.sizes + .map(sizeToAdType) + .filter(Utilities.isNumber) + }; + }), + time: System.now(), + user: {}, + url: Browser.getPageUrl(), + referrer: Browser.getReferrer(), + enableBotFiltering: true, + includePricingData: true, + parallel: true + }; + + var uspConsent = ComplianceService.usp && ComplianceService.usp.getConsent(); + if (uspConsent && uspConsent.uspString) { + data.ccpa = uspConsent.uspString; + } + + var gdprConsent = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(); + if (gdprConsent) { + data.gdpr = { + applies: gdprConsent.applies, + consent: gdprConsent.consentString + }; + } + + return { + url: Browser.getProtocol() + '//e.serverbid.com/api/v2', + data: data, + callbackId: callbackId, + networkParamOverrides: { + method: 'POST', + contentType: 'application/json' + } + }; + } + + /* Helpers + * ---------------------------------- */ + + /* ============================================================================= + * STEP 5 | Rendering Pixel + * ----------------------------------------------------------------------------- + * + */ + + /** + * This function will render the pixel given. + * @param {string} pixelUrl Tracking pixel img url. + */ + function __renderPixel(pixelUrl) { + if (pixelUrl) { + Network.img({ + url: pixelUrl, + method: 'GET' + }); + } + } + + /** + * Parses and extracts demand from adResponse according to the adapter and then attaches it + * to the corresponding bid's returnParcel in the correct format using targeting keys. + * + * @param {string} sessionId The sessionId, used for stats and other events. + * + * @param {any} adResponse This is the bid response as returned from the bid request, that was either + * passed to a JSONP callback or simply sent back via AJAX. + * + * @param {object[]} returnParcels The array of original parcels, SAME array that was passed to + * generateRequestObj to signal which slots need demand. In this funciton, the demand needs to be + * attached to each one of the objects for which the demand was originally requested for. + */ + function __parseResponse(sessionId, adResponse, returnParcels) { + /* ============================================================================= + * STEP 4 | Parse & store demand response + * ----------------------------------------------------------------------------- + * + * Fill the below variables with information about the bid from the partner, using + * the adResponse variable that contains your module adResponse. + */ + + /* This an array of all the bids in your response that will be iterated over below. Each of + * these will be mapped back to a returnParcel object using some criteria explained below. + * The following variables will also be parsed and attached to that returnParcel object as + * returned demand. + * + * Use the adResponse variable to extract your bid information and insert it into the + * bids array. Each element in the bids array should represent a single bid and should + * match up to a single element from the returnParcel array. + * + */ + + for (var j = 0; j < returnParcels.length; j++) { + var curReturnParcel = returnParcels[j]; + + var headerStatsInfo = {}; + var htSlotId = curReturnParcel.htSlot.getId(); + headerStatsInfo[htSlotId] = {}; + headerStatsInfo[htSlotId][curReturnParcel.requestId] = [curReturnParcel.xSlotName]; + + var decision = adResponse.decisions && adResponse.decisions[curReturnParcel.xSlotName]; + + if (!decision) { + /* No matching bid found so its a pass */ + if (__profile.enabledAnalytics.requestTime) { + __baseClass._emitStatsEvent(sessionId, 'hs_slot_pass', headerStatsInfo); + } + curReturnParcel.pass = true; + + continue; + } + + /* The bid price for the given slot */ + var bidPrice = (decision.pricing && Number(decision.pricing.clearPrice)) || 0; + + /* The size of the given slot */ + var bidSize = [decision.width, decision.height]; + + /* The creative/adm for the given slot that will be rendered if is the winner. + * Please make sure the URL is decoded and ready to be document.written. + */ + var wrappedCreative = (decision.contents && decision.contents[0] && decision.contents[0].body) || ''; + var cb = System.now(); + var bidCreative = '' + + '
' + + wrappedCreative + + '
' + + '
' + + ''; + + /* The dealId if applicable for this slot. */ + var bidDealId = ''; + + /* Explicitly pass */ + var bidIsPass = bidPrice <= 0; + + /* OPTIONAL: tracking pixel url to be fired AFTER rendering a winning creative. + * If firing a tracking pixel is not required or the pixel url is part of the adm, + * leave empty; + */ + var pixelUrl = decision.impressionUrl || ''; + + /* --------------------------------------------------------------------------------------- */ + + if (bidIsPass) { + //? if (DEBUG) { + Scribe.info(__profile.partnerId + ' returned pass for { id: ' + adResponse.id + ' }.'); + //? } + if (__profile.enabledAnalytics.requestTime) { + __baseClass._emitStatsEvent(sessionId, 'hs_slot_pass', headerStatsInfo); + } + curReturnParcel.pass = true; + + continue; + } + + if (__profile.enabledAnalytics.requestTime) { + __baseClass._emitStatsEvent(sessionId, 'hs_slot_bid', headerStatsInfo); + } + + curReturnParcel.size = bidSize; + curReturnParcel.targetingType = 'slot'; + curReturnParcel.targeting = {}; + + var targetingCpm = ''; + + //? if (FEATURES.GPT_LINE_ITEMS) { + targetingCpm = __baseClass._bidTransformers.targeting.apply(bidPrice); + var sizeKey = Size.arrayToString(curReturnParcel.size); + + if (bidDealId) { + curReturnParcel.targeting[__baseClass._configs.targetingKeys.pmid] = [sizeKey + '_' + bidDealId]; + curReturnParcel.targeting[__baseClass._configs.targetingKeys.pm] = [sizeKey + '_' + targetingCpm]; + } else { + curReturnParcel.targeting[__baseClass._configs.targetingKeys.om] = [sizeKey + '_' + targetingCpm]; + } + curReturnParcel.targeting[__baseClass._configs.targetingKeys.id] = [curReturnParcel.requestId]; + //? } + + //? if (FEATURES.RETURN_CREATIVE) { + curReturnParcel.adm = bidCreative; + if (pixelUrl) { + curReturnParcel.winNotice = __renderPixel.bind(null, pixelUrl); + } + //? } + + //? if (FEATURES.RETURN_PRICE) { + curReturnParcel.price = Number(__baseClass._bidTransformers.price.apply(bidPrice)); + //? } + + var expiry = 0; + if (__profile.features.demandExpiry.enabled) { + expiry = __profile.features.demandExpiry.value + System.now(); + } + + var pubKitAdId = RenderService.registerAd({ + sessionId: sessionId, + partnerId: __profile.partnerId, + adm: bidCreative, + requestId: curReturnParcel.requestId, + size: curReturnParcel.size, + price: targetingCpm, + dealId: bidDealId || null, + timeOfExpiry: expiry, + auxFn: __renderPixel, + auxArgs: [pixelUrl] + }); + + //? if (FEATURES.INTERNAL_RENDER) { + curReturnParcel.targeting.pubKitAdId = pubKitAdId; + //? } + } + } + + /* ===================================== + * Constructors + * ---------------------------------- */ + + (function __constructor() { + RenderService = SpaceCamp.services.RenderService; + ComplianceService = SpaceCamp.services.ComplianceService; + + /* ============================================================================= + * STEP 1 | Partner Configuration + * ----------------------------------------------------------------------------- + * + * Please fill out the below partner profile according to the steps in the README doc. + */ + + /* ---------- Please fill out this partner profile according to your module ------------ */ + __profile = { + partnerId: 'ConsumableHtb', + namespace: 'ConsumableHtb', + statsId: 'CNSM', + version: '2.1.0', + targetingType: 'slot', + enabledAnalytics: { + requestTime: true + }, + features: { + demandExpiry: { + enabled: false, + value: 0 + }, + rateLimiting: { + enabled: false, + value: 0 + } + }, + + /* Targeting keys for demand, should follow format ix_{statsId}_id */ + targetingKeys: { + id: 'ix_cnsm_id', + om: 'ix_cnsm_cpm', + pm: 'ix_cnsm_cpm', + pmid: 'ix_cnsm_dealid' + }, + + bidUnitInCents: 100, + lineItemType: Constants.LineItemTypes.ID_AND_SIZE, + callbackType: Partner.CallbackTypes.NONE, + architecture: Partner.Architectures.SRA, + requestType: Partner.RequestTypes.AJAX + }; + + /* --------------------------------------------------------------------------------------- */ + + //? if (DEBUG) { + var results = ConfigValidators.partnerBaseConfig(configs) || PartnerSpecificValidator(configs); + + if (results) { + throw Whoopsie('INVALID_CONFIG', results); + } + //? } + + __baseClass = Partner(__profile, configs, null, { + parseResponse: __parseResponse, + generateRequestObj: __generateRequestObj + }); + })(); + + /* ===================================== + * Public Interface + * ---------------------------------- */ + + var derivedClass = { + /* Class Information + * ---------------------------------- */ + + //? if (DEBUG) { + __type__: 'ConsumableHtb', + //? } + + //? if (TEST) { + __baseClass: __baseClass, + //? } + + /* Data + * ---------------------------------- */ + + //? if (TEST) { + profile: __profile, + //? } + + /* Functions + * ---------------------------------- */ + + //? if (TEST) { + parseResponse: __parseResponse, + generateRequestObj: __generateRequestObj + //? } + }; + + return Classify.derive(__baseClass, derivedClass); +} + +//////////////////////////////////////////////////////////////////////////////// +// Exports ///////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +module.exports = ConsumableHtb; From f99f23ef87605db5aa5909d0dd0c77e1f024f31f Mon Sep 17 00:00:00 2001 From: ix-certification Date: Tue, 6 Oct 2020 13:33:42 -0400 Subject: [PATCH 11/13] [HTP-0001] 33Across Adapter: Fix Validation (#206) (#209) * fix the site id validation * update changelog * update the adapter version Co-authored-by: terryc33x <64039851+terryc33x@users.noreply.github.com> --- thirty-three-across/CHANGES.md | 3 +++ thirty-three-across/thirty-three-across-htb-system-tests.js | 2 +- thirty-three-across/thirty-three-across-htb-validator.js | 2 +- thirty-three-across/thirty-three-across-htb.js | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/thirty-three-across/CHANGES.md b/thirty-three-across/CHANGES.md index f50599ec..6ded900e 100644 --- a/thirty-three-across/CHANGES.md +++ b/thirty-three-across/CHANGES.md @@ -1,2 +1,5 @@ +# 2.0.1 +- fix the siteID validation + # 2.0.0 - Initial adapter implementation diff --git a/thirty-three-across/thirty-three-across-htb-system-tests.js b/thirty-three-across/thirty-three-across-htb-system-tests.js index 2249bb4b..feb07eda 100644 --- a/thirty-three-across/thirty-three-across-htb-system-tests.js +++ b/thirty-three-across/thirty-three-across-htb-system-tests.js @@ -25,7 +25,7 @@ function getArchitecture() { function getConfig() { return { - siteId: 'acbdefgABCDEFG-1234567', + siteId: 'abcdef_ABCDEF-12345678', test: 1, xSlots: { 1: { diff --git a/thirty-three-across/thirty-three-across-htb-validator.js b/thirty-three-across/thirty-three-across-htb-validator.js index 3e9dc4fc..c263e466 100644 --- a/thirty-three-across/thirty-three-across-htb-validator.js +++ b/thirty-three-across/thirty-three-across-htb-validator.js @@ -25,7 +25,7 @@ function partnerValidator(configs) { properties: { siteId: { type: 'string', - pattern: /^[a-zA-Z0-9-]{22}$/ + pattern: /^[a-zA-Z0-9_-]{22}$/ }, test: { type: 'number', diff --git a/thirty-three-across/thirty-three-across-htb.js b/thirty-three-across/thirty-three-across-htb.js index a7682fc4..44ce5523 100644 --- a/thirty-three-across/thirty-three-across-htb.js +++ b/thirty-three-across/thirty-three-across-htb.js @@ -55,7 +55,7 @@ function ThirtyThreeAcrossHtb(configs) { var __profile; var _indexLibVersion = SpaceCamp.version; - var _adapterVersion = '2.0.0'; + var _adapterVersion = '2.0.1'; var SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; var DEFAULT_SYNC_ID = 'zzz000000000003zzz'; From 1fa1abf2f0c54d486926789d575fff1379ec43aa Mon Sep 17 00:00:00 2001 From: Bernhard Valenti Date: Mon, 16 Nov 2020 11:03:07 -0500 Subject: [PATCH 12/13] Kargo Adapter: send idl envelope and identityData --- kargo/CHANGES.md | 9 ++++----- kargo/DOCUMENTATION.md | 4 +++- kargo/kargo-htb-system-tests.js | 1 + kargo/kargo-htb.js | 36 ++++++++++++++++++++++++++++++++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/kargo/CHANGES.md b/kargo/CHANGES.md index ba72aaf1..5052c60f 100644 --- a/kargo/CHANGES.md +++ b/kargo/CHANGES.md @@ -1,11 +1,10 @@ -Version 2.3.0 -============= +# 2.4.0 +- Add Liveramp IDL Envelope support +# 2.3.0 - Add CCPA support -Version 2.0.0 -============= - +# 2.0.0 - Adding TDID support - Use localStorage as primary lookup for Kargo data, falling back to cookies as secondary - Adding session ID to the request to keep track of requests per instantiation diff --git a/kargo/DOCUMENTATION.md b/kargo/DOCUMENTATION.md index 615bc0ce..285ac02a 100644 --- a/kargo/DOCUMENTATION.md +++ b/kargo/DOCUMENTATION.md @@ -67,6 +67,8 @@ "kargoID": "", "clientID": "", "tdID": "", + "idlEnv": "", + "identityData": null, "crbIDs": {}, "optOut": false, "usp": "1YNN" @@ -184,4 +186,4 @@ } } } -``` \ No newline at end of file +``` diff --git a/kargo/kargo-htb-system-tests.js b/kargo/kargo-htb-system-tests.js index 5547f1c0..77c17f27 100644 --- a/kargo/kargo-htb-system-tests.js +++ b/kargo/kargo-htb-system-tests.js @@ -55,6 +55,7 @@ function validateBidRequest(request) { expect(r.userIDs.kargoID).toBeDefined(); expect(r.userIDs.optOut).toBeDefined(); expect(r.userIDs.tdID).toBeDefined(); + expect(r.userIDs.idlEnv).toBeDefined(); expect(r.userIDs.crbIDs).toBeDefined(); } diff --git a/kargo/kargo-htb.js b/kargo/kargo-htb.js index ed240f85..43d3c164 100644 --- a/kargo/kargo-htb.js +++ b/kargo/kargo-htb.js @@ -121,6 +121,38 @@ function KargoHtb(configs) { return unifiedID; } + function __getIDLEnvelope(returnParcels) { + var idlEnvelope = ''; + var uids = [] + if (returnParcels && + returnParcels.length && + returnParcels[0].identityData && + returnParcels[0].identityData.LiveRampIp && + returnParcels[0].identityData.LiveRampIp.data && + returnParcels[0].identityData.LiveRampIp.data.uids) { + uids = returnParcels[0].identityData.LiveRampIp.data.uids; + } else { + return idlEnvelope; + } + for (var i = 0; i < uids.length; i++) { + if (uids[i].ext && + uids[i].ext.rtiPartner && + uids[i].ext.rtiPartner === 'idl') { + idlEnvelope = uids[i].id; + break; + } + }; + return idlEnvelope; + } + + function __getIdentityData(returnParcels) { + if (returnParcels && returnParcels.length) { + return returnParcels[0].identityData; + } else { + return null; + } + } + function __getCrbFromCookie() { try { var crb = JSON.parse(decodeURIComponent(Browser.readCookie('krg_crb'))); @@ -173,6 +205,8 @@ function KargoHtb(configs) { kargoID: crb.userId || '', clientID: crb.clientId || '', tdID: __getTDID(returnParcels), + idlEnv: __getIDLEnvelope(returnParcels), + identityData: __getIdentityData(returnParcels), crbIDs: crb.syncIds || {}, optOut: crb.optOut || false, usp: privacyEnabled && uspConsentObj ? uspConsentObj.uspString : null @@ -584,7 +618,7 @@ function KargoHtb(configs) { // Unique partner identifier statsId: 'KARG', - version: '2.2.1', + version: '2.4.0', targetingType: 'slot', enabledAnalytics: { requestTime: true From 59f89edc8bd715a3683c261aca9a55ee78e6851d Mon Sep 17 00:00:00 2001 From: Bernhard Valenti Date: Wed, 3 Feb 2021 15:46:00 -0500 Subject: [PATCH 13/13] Kargo Adapter: add gdpr support --- kargo/CHANGES.md | 3 +++ kargo/DOCUMENTATION.md | 10 ++++++++-- kargo/kargo-htb-system-tests.js | 2 ++ kargo/kargo-htb.js | 16 ++++++++++++++-- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/kargo/CHANGES.md b/kargo/CHANGES.md index 5052c60f..6e53f1d2 100644 --- a/kargo/CHANGES.md +++ b/kargo/CHANGES.md @@ -1,3 +1,6 @@ +# 2.5.0 +- Add GDPR support + # 2.4.0 - Add Liveramp IDL Envelope support diff --git a/kargo/DOCUMENTATION.md b/kargo/DOCUMENTATION.md index 285ac02a..bedc8f3c 100644 --- a/kargo/DOCUMENTATION.md +++ b/kargo/DOCUMENTATION.md @@ -2,7 +2,7 @@ ## General Compatibility |Feature| | |---|---| -| Consent | No | +| Consent | Yes | | Native Ad Support | Yes | | SafeFrame Support | No | | PMP Support | Yes | @@ -71,7 +71,13 @@ "identityData": null, "crbIDs": {}, "optOut": false, - "usp": "1YNN" + "usp": "1YNN", + "gdpr": { + "consent": "", + "applies": false, + "version": 2, + "addtlConsent": "" + } }, "krux": { "userID": null, diff --git a/kargo/kargo-htb-system-tests.js b/kargo/kargo-htb-system-tests.js index 77c17f27..c79d2789 100644 --- a/kargo/kargo-htb-system-tests.js +++ b/kargo/kargo-htb-system-tests.js @@ -57,6 +57,8 @@ function validateBidRequest(request) { expect(r.userIDs.tdID).toBeDefined(); expect(r.userIDs.idlEnv).toBeDefined(); expect(r.userIDs.crbIDs).toBeDefined(); + expect(r.userIDs.usp).toBeDefined(); + expect(r.userIDs.gdpr).toBeDefined(); } function getValidResponse(request, creative) { diff --git a/kargo/kargo-htb.js b/kargo/kargo-htb.js index 43d3c164..398819bd 100644 --- a/kargo/kargo-htb.js +++ b/kargo/kargo-htb.js @@ -200,8 +200,9 @@ function KargoHtb(configs) { var crb = __getCrb(); var privacyEnabled = ComplianceService.isPrivacyEnabled(); var uspConsentObj = ComplianceService.usp && ComplianceService.usp.getConsent(); + var gdprConsentObj = ComplianceService.gdpr && ComplianceService.gdpr.getConsent(2); - return { + var userIds = { kargoID: crb.userId || '', clientID: crb.clientId || '', tdID: __getTDID(returnParcels), @@ -211,6 +212,17 @@ function KargoHtb(configs) { optOut: crb.optOut || false, usp: privacyEnabled && uspConsentObj ? uspConsentObj.uspString : null }; + + if (privacyEnabled && gdprConsentObj) { + userIds['gdpr'] = { + consent: gdprConsentObj.consentString || '', + applies: gdprConsentObj.applies ? true : false, + version: 2, + addtlConsent: gdprConsentObj.addtlConsent + } + } + + return userIds; } function __getKruxDmpData() { @@ -618,7 +630,7 @@ function KargoHtb(configs) { // Unique partner identifier statsId: 'KARG', - version: '2.4.0', + version: '2.5.0', targetingType: 'slot', enabledAnalytics: { requestTime: true