diff --git a/deep-intent/CHANGES.md b/deep-intent/CHANGES.md new file mode 100644 index 00000000..0d82107c --- /dev/null +++ b/deep-intent/CHANGES.md @@ -0,0 +1,3 @@ +# 1.0.0 + +- Initial Version, implemented the adapter diff --git a/deep-intent/DOCUMENTATION.md b/deep-intent/DOCUMENTATION.md new file mode 100644 index 00000000..dd3390cd --- /dev/null +++ b/deep-intent/DOCUMENTATION.md @@ -0,0 +1,180 @@ +# DeepIntent +## General Compatibility +|Feature| | +|---|---| +| Consent | Y | +| Native Ad Support | N | +| SafeFrame Support | Y | +| PMP Support | N | + +## Browser Compatibility +| Browser | | +|--- |---| +| Chrome | Y | +| Edge | Y | +| Firefox | Y | +| Internet Explorer 9 | N | +| Internet Explorer 10 | Y | +| Internet Explorer 11 | Y | +| Safari | Y | +| Mobile Chrome | Y | +| Mobile Safari | Y | +| UC Browser | N | +| Samsung Internet | N | +| Opera | Y | + +## Adapter Information +| Info | | +|---|---| +| Partner Id | DeepIntentHtb | +| Ad Server Responds in (Cents, Dollars, etc) | Dollars | +| Bid Type (Gross / Net) | Net | +| GAM Key (Open Market) | ix_dee_om | +| GAM Key (Private Market) | ix_dee_om | +| Ad Server URLs | https://prebid.deepintent.com/prebid | +| Slot Mapping Style (Size / Multiple Sizes / Slot) | Slot | +| Request Architecture (MRA / SRA) | MRA | + +## Currencies Supported +USD + +## Bid Request Information +### Parameters +| Key | Required | Type | Description | +|---|---|---|---| +|id|Yes|string|Identifier| +|at|Yes|Integer|Default Value 1 (Hardcoded)| +|imp|Yes|Object|ortb request containing slot and impressions information| +|site|Yes|Object|Contains site page and domain information| +|device|Yes|Object|Contains device data: browser, width & height of device, DONOTTRACK flag and language| +|user|No|Object|Contains user information: userid, buyeruid, yob, gender, keywords, customdata, eids (Unified Id data), gdpr consent string| +|regs|No|Object|Contains information if GDPR status is applicable and ccpa consent| + +### Example +```javascript +{ + "id": "9LKgIp41", + "at": 1, + "imp": [ + { + "id": "htSlotDesktopAId", + "tagid": "/43743431/DMDemo", + "secure": 1, + "banner": { + "h": 600, + "w": 160, + "pos": 0 + }, + "displaymanager": "di_indexexchange", + "displaymanagerver": "1.0.0", + "ext": {} + }, + { + "id": "htSlotDesktopAId", + "tagid": "/43743431/DMDemo1", + "secure": 1, + "banner": { + "h": 90, + "w": 728, + "pos": 0 + }, + "displaymanager": "di_indexexchange", + "displaymanagerver": "1.0.0", + "ext": {} + } + ], + "site": { + "page": "http://test.com/test.html", + "domain": "test.com" + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36", + "js": 1, + "dnt": 1, + "h": 900, + "w": 1440, + "language": "en-US" + }, + "user": { + "id": "di_testuid", + "buyeruid": "di_testbuyeruid", + "yob": 2002, + "gender": "F", + "ext": { + "gdpr_consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0, + "is_privacy": "1NYN" + } + } +} +``` + +## Bid Response Information +### Bid Example +```javascript +{ + "id": "4E733404-CC2E-48A2-BC83-4DD5F38FE9BB", + "bidId": "0b08b09f-aaa1-4c14-b1c8-7debb1a7c1cd", + "seatbid": [ + { + "seat": "12345", + "bid": [ + { + "id": "4E733404-CC2E-48A2-BC83-4DD5F38FE9BB", + "impid": "htSlotDesktopAId", + "price": 2, + "adid": "10001", + "adm": "
here is your ad
", + "adomain": [ + "test.com" + ], + "cid": "16981", + "crid": "13665", + "h": 600, + "w": 160 + }, + { + "id": "4E733404-CC2E-48A2-BC83-4DD5F38FE9BC", + "impid": "htSlotDesktopAId", + "price": 2, + "adid": "10001", + "adm": "
here is your ad
", + "adomain": [ + "test.com" + ], + "cid": "16981", + "crid": "13665", + "h": 250, + "w": 300 + } + ] + } + ] +} +``` +### Pass Example +```javascript +HTTP status code - 204 No Content +``` + +## Configuration Information +### Configuration Keys +| Key | Required | Type | Description | +|---|---|---|---| +|pubId|Yes|string|Publisher ID| +|yob|No|string|Year of Birth| +|gender|No|string|Gender Information| +|version|No|string|version number| +### Example +```javascript +{ + pubId: '10001', + yob: '1990', + gender: 'F', + version: '1.0.0' +} +``` diff --git a/deep-intent/deep-intent-htb-exports.js b/deep-intent/deep-intent-htb-exports.js new file mode 100644 index 00000000..ab22cc89 --- /dev/null +++ b/deep-intent/deep-intent-htb-exports.js @@ -0,0 +1,10 @@ +//? if (FEATURES.GPT_LINE_ITEMS) { +shellInterface.DeepIntentHtb = { + render: SpaceCamp.services.RenderService.renderDfpAd.bind(null, 'DeepIntentHtb') +}; + + +shellInterface.DeepIntentModule = { + render: SpaceCamp.services.RenderService.renderDfpAd.bind(null, 'DeepIntentHtb') +}; +//? } diff --git a/deep-intent/deep-intent-htb-system-tests.js b/deep-intent/deep-intent-htb-system-tests.js new file mode 100644 index 00000000..a2fef10e --- /dev/null +++ b/deep-intent/deep-intent-htb-system-tests.js @@ -0,0 +1,202 @@ +'use strict'; + +function getPartnerId() { + return 'DeepIntentHtb'; +} + +function getStatsId() { + return 'DEE'; +} + +function getCallbackType() { + return 'NONE'; +} + +function getArchitecture() { + return 'SRA'; +} + +function getConfig() { + return { + tagId: '100013', + pos: 1, + user: { + id: 'di_testuid', + buyeruid: 'di_testbuyeruid', + yob: 2002, + gender: 'F' + }, + xSlots: { + 1: { + adUnitName: '/43743431/DMDemo', + + // Winning bid for [160x600] + sizes: [[160, 600], [300, 250]] + }, + 2: { + adUnitName: '/43743431/DMDemo1', + + // Winning bid for [300x250] + sizes: [ + [728, 90], + [800, 250], + [300, 250] + ] + } + } + }; +} + +function getBidRequestRegex() { + return { + method: 'POST', + urlRegex: /https:\/\/prebid\.deepintent\.com\/prebid/ + }; +} + +function validateBidRequest(request) { + // Expect(request.host).toEqual('deepintent.com'); + expect(request.host).toEqual('prebid.deepintent.com'); + var config = getConfig(); + var sizes1 = config.xSlots[1].sizes; + var sizes2 = config.xSlots[2].sizes; + + var body = request.body; + if (body) { + body = JSON.parse(body); + expect(body.id).toBeDefined(); + expect(body.imp.length).toEqual(2); + expect(body.imp[0].banner).toBeDefined(); + expect(body.imp[0].secure).toEqual(1); + expect(body.imp[0].banner.w).toEqual(sizes1[0][0]); + expect(body.imp[0].banner.h).toEqual(sizes1[0][1]); + expect(body.imp[0].tagid).toEqual(config.xSlots['1'].adUnitName); + + expect(body.imp[1].banner).toBeDefined(); + expect(body.imp[1].banner.w).toEqual(sizes2[0][0]); + expect(body.imp[1].banner.h).toEqual(sizes2[0][1]); + expect(body.imp[1].tagid).toEqual(config.xSlots['2'].adUnitName); + + expect(body.at).toEqual(1); + + expect(body.site).toBeDefined(); + expect(body.site.page).toBeDefined(); + expect(body.site.domain).toBeDefined(); + + expect(body.user.id).toBeDefined(); + expect(body.user.buyeruid).toBeDefined(); + expect(body.user.yob).toEqual(config.user.yob); + expect(body.user.gender).toEqual(config.user.gender); + + expect(body.device).toBeDefined(); + expect(body.device.ua).toBeDefined(); + expect(body.device.js).toBeDefined(); + expect(body.device.js).toEqual(1); + expect(body.device.dnt).toBeDefined(); + expect(body.device.h).toBeDefined(); + expect(body.device.w).toBeDefined(); + expect(body.device.language).toBeDefined(); + } +} + +function getValidResponse(request, creative) { + var body = JSON.parse(request.body); + var response = { + id: '4E733404-CC2E-48A2-BC83-4DD5F38FE9BB', + bidId: '0b08b09f-aaa1-4c14-b1c8-7debb1a7c1cd', + seatbid: [ + { + seat: '12345', + bid: [ + { + id: '4E733404-CC2E-48A2-BC83-4DD5F38FE9BB', + impid: body.imp[0].id, + price: 2, + adid: '10001', + adm: creative, + adomain: ['test.com'], + cid: '16981', + crid: '13665', + h: 600, + w: 160 + }, + { + id: '4E733404-CC2E-48A2-BC83-4DD5F38FE9BC', + impid: body.imp[1].id, + price: 2, + adid: '10001', + adm: creative, + adomain: ['test.com'], + cid: '16981', + crid: '13665', + h: 250, + w: 300 + } + ] + } + ] + }; + + return JSON.stringify(response); +} + +function validateTargeting(targetingMap) { + var isObjEmpty = Object.keys(targetingMap).length === 0 && targetingMap.constructor === Object; + if (!isObjEmpty) { + expect(targetingMap).toEqual(jasmine.objectContaining({ + ix_dee_om: jasmine.arrayContaining([jasmine.any(String), jasmine.any(String)]), + ix_dee_id: jasmine.arrayContaining([jasmine.any(String), jasmine.any(String)]) + })); + } +} + +function getPassResponse() { + return JSON.stringify({ bids: [] }); +} + +function validateBidRequestWithPrivacy(request) { + var r = JSON.parse(request.body); + + expect(r.regs.ext.gdpr).toEqual(1); + + expect(r.user).toEqual(jasmine.objectContaining({ + ext: { + gdpr_consent: 'TEST_GDPR_CONSENT_STRING' + } + })); +} + +function validateBidRequestWithAdSrvrOrg(request) { + var body = JSON.parse(request.body); + expect(body.user.eids[0].source).toEqual('adserver.org'); + expect(body.user.eids[0].uids).toEqual(jasmine.arrayContaining([ + { + id: 'TEST_ADSRVR_ORG_STRING', + ext: { + rtiPartner: 'TDID' + } + } + ])); +} + +function validateBidRequestWithUspapi(request) { + var r = JSON.parse(request.body); + + expect(r.regs.ext.us_privacy).toEqual('TEST_USPAPI_CONSENT_STRING'); +} + +module.exports = { + getPartnerId: getPartnerId, + getStatsId: getStatsId, + getCallbackType: getCallbackType, + getArchitecture: getArchitecture, + getConfig: getConfig, + getBidRequestRegex: getBidRequestRegex, + validateBidRequest: validateBidRequest, + getValidResponse: getValidResponse, + validateTargeting: validateTargeting, + getPassResponse: getPassResponse, + validateBidRequestWithAdSrvrOrg: validateBidRequestWithAdSrvrOrg, + validateBidRequestWithPrivacy: validateBidRequestWithPrivacy, + validateBidRequestWithUspapi: validateBidRequestWithUspapi +}; diff --git a/deep-intent/deep-intent-htb-validator.js b/deep-intent/deep-intent-htb-validator.js new file mode 100644 index 00000000..d36e8d2d --- /dev/null +++ b/deep-intent/deep-intent-htb-validator.js @@ -0,0 +1,73 @@ +'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: { + pubId: { + type: 'string' + }, + yob: { + type: 'string', + optional: true + }, + gender: { + type: 'string', + optional: true + }, + version: { + type: 'number', + optional: true + }, + xSlots: { + type: 'object', + properties: { + '*': { + type: 'object', + properties: { + sizes: { + type: 'array', + minLength: 1 + } + } + } + } + }, + mapping: { + '*': { + type: 'array' + } + } + } + }, + configs + ); + + if (!result.valid) { + return result.format(); + } + + return null; +} + +module.exports = partnerValidator; diff --git a/deep-intent/deep-intent-htb.js b/deep-intent/deep-intent-htb.js new file mode 100644 index 00000000..4d8813ab --- /dev/null +++ b/deep-intent/deep-intent-htb.js @@ -0,0 +1,730 @@ +'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 ComplianceService; +var RenderService; + +//? if (DEBUG) { +var ConfigValidators = require('config-validators.js'); +var PartnerSpecificValidator = require('deep-intent-htb-validator.js'); +var Scribe = require('scribe.js'); +var Whoopsie = require('whoopsie.js'); +//? } + +var undef; +var DI_M_V = '1.0.0'; + +//////////////////////////////////////////////////////////////////////////////// +// Main //////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Partner module template + * + * @class + */ +function DeepIntentHtb(configs) { + /* ===================================== + * Data + * ---------------------------------- */ + + /* Private + * ---------------------------------- */ + + /** + * Reference to the partner base class. + * + * @private {object} + */ + var __baseClass; + + /** + * Profile for this partner. + * + * @private {object} + */ + var __profile; + var __globalConfigs; + + /* ===================================== + * Functions + * ---------------------------------- */ + + /* Utilities + * ---------------------------------- */ + + function __getSiteObject() { + var retObj = { + page: Browser.topWindow.location.href, + domain: Browser.topWindow.location.hostname + }; + + return retObj; + } + + function __getDeviceObject() { + var dnt = Browser.topWindow.navigator.doNotTrack === 'yes' || Browser.topWindow.navigator.doNotTrack === '1' + || Browser.topWindow.navigator.msDoNotTrack === '1' ? 1 : 0; + + return { + ua: Browser.getUserAgent(), + js: 1, + dnt: dnt, + h: Browser.getScreenHeight(), + w: Browser.getScreenWidth(), + language: Browser.getLanguage() + }; + } + + function __getUserObject(userInputObj, idData) { + var user = {}; + if (!Utilities.isEmpty(userInputObj)) { + var id = userInputObj.id && typeof userInputObj.id === 'string' ? userInputObj.id : undef; + // eslint-disable-next-line max-len + var buyeruid = userInputObj.buyeruid && typeof userInputObj.buyeruid === 'string' ? userInputObj.buyeruid : undef; + var yob = userInputObj.yob && typeof userInputObj.yob === 'number' ? userInputObj.yob : null; + var gender = userInputObj.gender && typeof userInputObj.gender === 'string' ? userInputObj.gender : undef; + // eslint-disable-next-line max-len + var keywords = userInputObj.keywords && typeof userInputObj.keywords === 'string' ? userInputObj.keywords : undef; + // eslint-disable-next-line max-len + var customdata = userInputObj.customdata && typeof userInputObj.customdata === 'string' ? userInputObj.customdata : undef; + if (id) { + user.id = id; + } + + if (buyeruid) { + user.buyeruid = buyeruid; + } + + if (yob) { + user.yob = yob; + } + + if (gender) { + user.gender = gender; + } + + if (keywords) { + user.keywords = keywords; + } + + if (customdata) { + user.customdata = customdata; + } + + if (idData && idData.hasOwnProperty('AdserverOrgIp') && idData.AdserverOrgIp.hasOwnProperty('data')) { + user.eids = [idData.AdserverOrgIp.data]; + } + } + + return user; + } + + function __getBannerObj(bid) { + // Get Sizes from MediaTypes Object, Will always take first size, will be overrided by params for exact w,h + if (bid.xSlotRef.sizes) { + var sizes = bid.xSlotRef.sizes; + if (Utilities.isArray(sizes) && sizes.length > 0) { + return { + h: sizes[0][1], + w: sizes[0][0], + pos: bid && bid.params && bid.params.pos ? bid.params.pos : 0 + }; + } + } + + return null; + } + + function __getCustomParams(bid) { + if (bid.params && bid.params.custom) { + return { + deepintent: bid.params.custom + }; + } + + return {}; + } + + function __getImprObject(bid) { + if (!bid) { + return null; + } + + return { + id: bid.htSlot.getId(), + tagid: bid.xSlotRef.adUnitName || '', + secure: Browser.getProtocol(0, 1), + banner: __getBannerObj(bid), + displaymanager: 'di_indexexchange', + displaymanagerver: DI_M_V, + ext: __getCustomParams(bid) + }; + } + + /** + * 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 + * } + */ + + /* ---------------------- PUT CODE HERE ------------------------------------ */ + var callbackId = System.generateUniqueId(); + + /* Change this to your bidder endpoint. */ + var baseUrl = 'https://prebid.deepintent.com/ix'; + var impressions = []; + var idData = returnParcels[0] && returnParcels[0].identityData; + if (Utilities.isArray(returnParcels)) { + for (var kk = 0; kk < returnParcels.length; kk++) { + var impObj = __getImprObject(returnParcels[kk]); + if (impObj) { + impressions.push(impObj); + } + } + } + + var queryObj = { + id: System.generateUniqueId(), + + at: 1, + + imp: impressions, + + site: __getSiteObject(), + + device: __getDeviceObject() + + }; + + var user = __getUserObject(__globalConfigs.user, idData); + if (user && !Utilities.isEmpty(user)) { + queryObj.user = user; + } + + /* ------------------------ 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 isPrivacyEnabled = ComplianceService.isPrivacyEnabled(); + if (isPrivacyEnabled) { + var gdprStatus = ComplianceService.gdpr.getConsent(); + queryObj.regs = { + ext: { + gdpr: gdprStatus.applies ? 1 : 0 + } + }; + + queryObj.user.ext = { + // eslint-disable-next-line camelcase + gdpr_consent: gdprStatus.consentString + + }; + } + + /* + * Add CCPA information + */ + var uspConsent = ComplianceService.usp.getConsent(); + var extCopy = queryObj.regs && queryObj.regs.ext ? queryObj.regs.ext : {}; + // eslint-disable-next-line camelcase + queryObj.regs.ext = Utilities.mergeObjects(extCopy, { us_privacy: uspConsent.uspString }); + + return { + url: baseUrl, + data: queryObj, + callbackId: callbackId, + + /* Signals a POST request and the content type */ + networkParamOverrides: { + method: 'POST', + contentType: 'text/plain' + } + }; + } + + /* ============================================================================= + * STEP 3 | Response callback + * ----------------------------------------------------------------------------- + * + * This generator is only necessary if the partner's endpoint has the ability + * to return an arbitrary ID that is sent to it. It should retrieve that ID from + * the response and save the response to adResponseStore keyed by that ID. + * + * If the endpoint does not have an appropriate field for this, set the profile's + * callback type to CallbackTypes.CALLBACK_NAME and omit this function. + */ + function adResponseCallback(adResponse) { + /* Get callbackId from adResponse here */ + var callbackId = 0; + __baseClass._adResponseStore[callbackId] = adResponse; + } + + /* -------------------------------------------------------------------------- */ + + /* 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: decodeURIComponent(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. + * + */ + + /* ---------- Process adResponse and extract the bids into the bids array ------------ */ + + var bids = []; + if ( + adResponse + && adResponse.seatbid + && Utilities.isArray(adResponse.seatbid) + && adResponse.seatbid.length > 0 + ) { + for (var ii = 0; ii < adResponse.seatbid.length; ii++) { + bids = bids.concat(adResponse.seatbid[ii].bid); + } + } + + /* --------------------------------------------------------------------------------- */ + + 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 curBid; + var sizes; + var curReturnParcelLen = curReturnParcel.xSlotRef.sizes.length; + var bidMatchFound = false; + + if (!bids || !Utilities.isArray(bids) || bids.length === 0) { + if (__profile.enabledAnalytics.requestTime) { + __baseClass._emitStatsEvent( + sessionId, + 'hs_slot_pass', + headerStatsInfo + ); + } + curReturnParcel.pass = true; + + continue; + } + + for (var i = 0; i < bids.length; i++) { + bidMatchFound = false; + for (var index = 0; index < curReturnParcelLen; index++) { + sizes = curReturnParcel.xSlotRef.sizes[index]; + + /** + * This section maps internal returnParcels and demand returned from the bid request. + * In order to match them correctly, they must be matched via some criteria. This + * is usually some sort of placements or inventory codes. Please replace the someCriteria + * key to a key that represents the placement in the configuration and in the bid responses. + */ + if (bids[i].impid === curReturnParcel.htSlot.getId()) { + if ( + parseInt(bids[i].w, 10) === parseInt(sizes[0], 10) + && parseInt(bids[i].h, 10) === parseInt(sizes[1], 10) + ) { + curBid = bids[i]; + bids.splice(i, 1); + bidMatchFound = true; + + break; + } + } + } + + if (bidMatchFound) { + break; + } + } + + /* No matching bid found so its a pass */ + if (!curBid) { + if (__profile.enabledAnalytics.requestTime) { + __baseClass._emitStatsEvent( + sessionId, + 'hs_slot_pass', + headerStatsInfo + ); + } + curReturnParcel.pass = true; + + continue; + } + + /* ---------- Fill the bid variables with data from the bid response here. ------------ */ + + /* Using the above variable, curBid, extract various information about the bid and assign it to + * these local variables */ + + /* The bid price for the given slot */ + var bidPrice = curBid.price; + + /* The size of the given slot */ + var bidSize = [Number(curBid.w), Number(curBid.h)]; + + /* 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 bidCreative = curBid.adm; + + /* The dealId if applicable for this slot. */ + var bidDealId = curBid.dealid; + + /* 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 = ''; + + /* --------------------------------------------------------------------------------------- */ + + curBid = null; + 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() { + ComplianceService = SpaceCamp.services.ComplianceService; + RenderService = SpaceCamp.services.RenderService; + + /* ============================================================================= + * 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: 'DeepIntentHtb', + namespace: 'DeepIntentHtb', + statsId: 'DEE', + version: '2.0.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_dee_id', + om: 'ix_dee_om', + pm: 'ix_dee_om', + pmid: 'ix_dee_dealid' + }, + + /* The bid price unit (in cents) the endpoint returns, please refer to the readme for details */ + 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); + } + //? } + __globalConfigs = { + pubId: configs.publisherId, + + yob: configs.yob || undef, + gender: configs.gender || undef, + version: configs.version || undef, + user: configs.user || undef + }; + __baseClass = Partner(__profile, configs, null, { + parseResponse: __parseResponse, + generateRequestObj: __generateRequestObj, + adResponseCallback: adResponseCallback + }); + })(); + + /* ===================================== + * Public Interface + * ---------------------------------- */ + + var derivedClass = { + /* Class Information + * ---------------------------------- */ + + //? if (DEBUG) { + __type__: 'DeepIntentHtb', + //? } + + //? if (TEST) { + __baseClass: __baseClass, + //? } + + /* Data + * ---------------------------------- */ + + //? if (TEST) { + profile: __profile, + //? } + + /* Functions + * ---------------------------------- */ + + //? if (TEST) { + parseResponse: __parseResponse, + generateRequestObj: __generateRequestObj, + adResponseCallback: adResponseCallback + //? } + }; + + return Classify.derive(__baseClass, derivedClass); +} + +//////////////////////////////////////////////////////////////////////////////// +// Exports ///////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +module.exports = DeepIntentHtb;