diff --git a/app-nexus-network/CHANGES.md b/app-nexus-network/CHANGES.md new file mode 100644 index 00000000..e69de29b diff --git a/app-nexus-network/DOCUMENTATION.md b/app-nexus-network/DOCUMENTATION.md new file mode 100644 index 00000000..61b36fc8 --- /dev/null +++ b/app-nexus-network/DOCUMENTATION.md @@ -0,0 +1,69 @@ +# AppNexusNetwork +## General Compatibility +|Feature| | +|---|---| +| Consent | Yes | +| Native Ad Support | | +| SafeFrame Support | | +| PMP Support | | + +## Browser Compatibility +| Browser | | +|--- |---| +| Chrome | | +| Edge | | +| Firefox | | +| Internet Explorer 9 | | +| Internet Explorer 10 | | +| Internet Explorer 11 | | +| Safari | | +| Mobile Chrome | | +| Mobile Safari | | +| UC Browser | | +| Samsung Internet | | +| Opera | | + +## Adapter Information +| Info | | +|---|---| +| Partner Id | AppNexusNetworkHtb | +| Ad Server Responds in (Cents, Dollars, etc) | hundreth of a cent | +| Bid Type (Gross / Net) | Net | +| GAM Key (Open Market) | ix_apnx2_om | +| GAM Key (Private Market) | ix_apnxnet_pm | +| Ad Server URLs | secure.adnxs.com/jpt | +| Slot Mapping Style (Size / Multiple Sizes / Slot) | | +| Request Architecture (MRA / SRA) | MRA | + +## Currencies Supported + +## Bid Request Information +### Parameters +| Key | Required | Type | Description | +|---|---|---|---| +| | | | | + +### Example +```javascript + +``` + +## Bid Response Information +### Bid Example +```javascript + +``` +### Pass Example +```javascript + +``` + +## Configuration Information +### Configuration Keys +| Key | Required | Type | Description | +|---|---|---|---| +| | | | | +### Example +```javascript + +``` \ No newline at end of file diff --git a/app-nexus-network/app-nexus-network-htb-exports.js b/app-nexus-network/app-nexus-network-htb-exports.js new file mode 100644 index 00000000..5d3cef43 --- /dev/null +++ b/app-nexus-network/app-nexus-network-htb-exports.js @@ -0,0 +1,22 @@ +/** + * This file contains any necessary functions that need to be exposed to the outside world. + * Things like (render functions) will be exposed by adding them to the shellInterface variable, under the partners + * profile name. This function will then be accessible through the window.headertag.AppNexusNetworkHtb object. + * If necessary for backwards compatibility with old creatives, you can also add things directly to the + * window namespace here, but this is discouraged if it's not strictly needed. + */ + +//? if(FEATURES.GPT_LINE_ITEMS) { + shellInterface.AppNexusNetworkHtb = { + render: SpaceCamp.services.RenderService.renderDfpAd.bind(null, 'AppNexusNetworkHtb') +}; + +/* Existing creatives use window.pbjs.renderApnxAd */ +window.pbjs = window.pbjs || {}; +window.pbjs.renderApnxAd = SpaceCamp.services.RenderService.renderDfpAd.bind(null, 'AppNexusNetworkHtb'); +//? } + +if (__directInterface.Layers.PartnersLayer.Partners.AppNexusNetworkHtb) { + shellInterface.AppNexusNetworkHtb = shellInterface.AppNexusNetworkHtb || {}; + shellInterface.AppNexusNetworkHtb.adResponseCallback = __directInterface.Layers.PartnersLayer.Partners.AppNexusNetworkHtb.adResponseCallback; +} diff --git a/app-nexus-network/app-nexus-network-htb-system-tests.js b/app-nexus-network/app-nexus-network-htb-system-tests.js new file mode 100644 index 00000000..56591add --- /dev/null +++ b/app-nexus-network/app-nexus-network-htb-system-tests.js @@ -0,0 +1,112 @@ +'use strict'; + +function getPartnerId() { + return 'AppNexusNetworkHtb'; +} + +function getStatsId() { + return 'APNXNET'; +} + +function getCallbackType() { + return 'ID'; +} + +function getArchitecture() { + return 'SRA'; +} + +function getBidRequestRegex() { + return { + method: 'GET', + urlRegex: /.*secure\.adnxs\.com\/jpt*/ + }; +} + +function getConfig() { + return { + xSlots: { + 1: { + placementId: "15894224", + sizes: [[300, 250]] + }, + 2: { + placementId: "15901268", + sizes: [[300,250], [300, 600]] + } + }, + mapping: { + "Fake Unit 1 300x250": ["1"], + "Fake Unit 2 300x250 or 300x600": ["2"] + } + }; +} + +function validateBidRequest(request) { + var q = request.query; + expect(q.id).toBeDefined(); + expect(q.size).toBeDefined(); + expect(q.psa).toBeDefined(); + expect(q.callback).toBeDefined(); + expect(q.callback_uid).toBeDefined(); + expect(q.gdpr).toBeDefined(); + expect(q.gdpr_consent).toBeDefined(); + expect(q.referrer).toBeDefined(); +} + +function getValidResponse(request, creative) { + var q = request.query; + var adm = creative; + // console.log(adm); + + var response = { + result: { + cpm: 20000, + width: 300, + height: 250, + creative_id: 100232340, + media_type_id: 1, + media_subtype_id: 1, + ad: adm, + is_bin_price_applicable: false + }, + callback_uid: q.callback_uid + }; + var jsonResponse = JSON.stringify(response); + // console.log(jsonResponse); + return 'headertag.AppNexusNetworkHtb.adResponseCallback(' + jsonResponse + ')'; +} + +function validateTargeting(targetingMap) { + expect(targetingMap).toEqual(jasmine.objectContaining({ + ix_apnxnet_om: jasmine.arrayContaining(['300x250_200']), + ix_apnxnet_id: jasmine.arrayContaining([jasmine.any(String)]) + })); +} + +function getPassResponse(request) { + var q = request.query; + + var response = { + result: { + cpm: 0.0, + ad: "" + }, + callback_uid: q.callback_uid + } + return 'headertag.AppNexusNetworkHtb.adResponseCallback('+ JSON.stringify(response) +');'; +} + + +module.exports = { + getPartnerId: getPartnerId, + getStatsId: getStatsId, + getCallbackType: getCallbackType, + getArchitecture: getArchitecture, + getConfig: getConfig, + getBidRequestRegex: getBidRequestRegex, + validateBidRequest: validateBidRequest, + getValidResponse: getValidResponse, + getPassResponse: getPassResponse, + validateTargeting: validateTargeting, +}; diff --git a/app-nexus-network/app-nexus-network-htb-validator.js b/app-nexus-network/app-nexus-network-htb-validator.js new file mode 100644 index 00000000..1fad21ea --- /dev/null +++ b/app-nexus-network/app-nexus-network-htb-validator.js @@ -0,0 +1,80 @@ +/** + * @author: Partner + * @license: UNLICENSED + * + * @copyright: Copyright (c) 2017 by Index Exchange. All rights reserved. + * + * The information contained within this document is confidential, copyrighted + * and or a trade secret. No part of this document may be reproduced or + * distributed in any form or by any means, in whole or in part, without the + * prior written permission of Index Exchange. + */ + +'use strict'; + +//////////////////////////////////////////////////////////////////////////////// +// Dependencies //////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +var Inspector = require('../../../libs/external/schema-inspector.js'); + +//////////////////////////////////////////////////////////////////////////////// +// Main //////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +var partnerValidator = function (configs) { + var result = Inspector.validate({ + type: 'object', + properties: { + xSlots: { + type: 'object', + properties: { + '*': { + type: 'object', + properties: { + placementId: { + type: 'string', + minLength: 1 + }, + sizes: { + type: 'array', + minLength: 1, + items: { + type: 'array', + exactLength: 2, + items: { + type: 'integer' + } + } + }, + keywords: { + type: 'object', + optional: true, + properties: { + '*': { + type: 'array', + minLength: 1, + items: { + type: 'string' + } + } + } + } + } + } + } + }, + mapping: { + type: 'object' + } + } + }, configs); + + if (!result.valid) { + return result.format(); + } + + return null; +}; + +module.exports = partnerValidator; diff --git a/app-nexus-network/app-nexus-network-htb.js b/app-nexus-network/app-nexus-network-htb.js new file mode 100644 index 00000000..00ffaa36 --- /dev/null +++ b/app-nexus-network/app-nexus-network-htb.js @@ -0,0 +1,445 @@ +/** + * @author: Partner + * @license: UNLICENSED + * + * @copyright: Copyright (c) 2017 by Index Exchange. All rights reserved. + * + * The information contained within this document is confidential, copyrighted + * and or a trade secret. No part of this document may be reproduced or + * distributed in any form or by any means, in whole or in part, without the + * prior written permission of Index Exchange. + */ + +'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 Utilities = require('utilities.js'); +var Whoopsie = require('whoopsie.js'); +var EventsService; +var RenderService; +var ComplianceService; + +//? if (DEBUG) { +var ConfigValidators = require('config-validators.js'); +var Inspector = require('schema-inspector.js'); +var AppNexusNetworkValidator = require('app-nexus-network-htb-validator.js'); +var Scribe = require('scribe.js'); +//? } + +//////////////////////////////////////////////////////////////////////////////// +// Main //////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * AppNexusNetworkHtb Class for the creation of the Header Tag Bidder + * + * @class + */ +function AppNexusNetworkHtb(configs) { + /* ===================================== + * Data + * ---------------------------------- */ + + /* Private + * ---------------------------------- */ + + /** + * Reference to the partner base class. + * + * @private {object} + */ + var __baseClass; + + /** + * Profile for this partner. + * + * @private {object} + */ + var __profile; + + /** + * Base URL for the bidding end-point. + * + * @private {object} + */ + var __baseUrl; + + var __parseFuncPath; + + /* ===================================== + * Functions + * ---------------------------------- */ + + /* Utilities + * ---------------------------------- */ + + /** + * Generates the request URL to the endpoint for the xSlots in the given + * returnParcels. + * + * @param {Object[]} returnParcels Array of parcels. + * @return {Object} Request object. + */ + function __generateRequestObj(returnParcels) { + //? if (DEBUG){ + var results = Inspector.validate({ + type: 'array', + exactLength: 1, + items: { + type: 'object', + properties: { + htSlot: { + type: 'object' + }, + xSlotRef: { + type: 'object' + }, + xSlotName: { + type: 'string', + minLength: 1 + } + } + } + }, returnParcels); + if (!results.valid) { + throw Whoopsie('INVALID_ARGUMENT', results.format()); + } + //? } + + /* MRA partners receive only one parcel in the array. */ + var returnParcel = returnParcels[0]; + var callbackId = System.generateUniqueId(); + var queryObj = { + id: returnParcel.xSlotRef.placementId, + size: Size.arrayToString([returnParcel.xSlotRef.sizes[0]]), + callback: __parseFuncPath, + callback_uid: callbackId, //jshint ignore:line + psa: 0 + }; + /* Endpoint expects first size to be assigned to the "size" parameter, + * while the rest are added to "promo_sizes". + */ + if (returnParcel.xSlotRef.sizes.length > 1) { + queryObj.promo_sizes = Size.arrayToString(returnParcel.xSlotRef.sizes.slice(1)); //jshint ignore:line + } + + if (Utilities.isObject(returnParcel.xSlotRef.keywords) && !Utilities.isEmpty(returnParcel.xSlotRef.keywords)) { + var keywordsObj = returnParcel.xSlotRef.keywords; + Object.keys(keywordsObj).forEach(function(key) { + var newKey = 'kw_' + key; + var values = ''; + //read in the values from the array of strings for the key and store in a comma separated list + keywordsObj[key].forEach(function (val) { + values += val + ','; + }); + values = values.slice(0, -1); // drop the last comma + + // append to as queryObj.kw_key="value1,value2" + queryObj[newKey] = values; + }); + } + + var referrer = Browser.getPageUrl(); + if (referrer) { + queryObj.referrer = referrer; + } + + /* ------------------------ 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. + */ + + /* ------- Put GDPR consent code here if you are implementing GDPR ---------- */ + if(ComplianceService.isPrivacyEnabled()) { + var gdprStatus = ComplianceService.gdpr.getConsent(); + queryObj.gdpr = gdprStatus.applies ? 1 : 0; + queryObj.gdpr_consent = gdprStatus.consentString; + } + + return { + url: __baseUrl, + data: queryObj, + callbackId: callbackId + }; + } + + function adResponseCallback(adResponseData) { + __baseClass._adResponseStore[adResponseData.callback_uid] = adResponseData; //jshint ignore:line + } + /* -------------------------------------------------------------------------- */ + + /* Helpers + * ---------------------------------- */ + + /** + * This function will render the ad given. + * @param {Object} doc The document of the iframe where the ad will go. + * @param {string} adm The ad code that came with the original demand. + */ + function __render(doc, adm) { + System.documentWrite(doc, adm); + } + + /* Parse adResponse, put demand into outParcels. + * AppNexus response contains a single result object. + */ + function __parseResponse(sessionId, adResponse, returnParcels) { + //? if (DEBUG){ + var results = Inspector.validate({ + type: 'array', + exactLength: 1, + items: { + type: 'object', + properties: { + htSlot: { + type: 'object' + }, + xSlotRef: { + type: 'object' + }, + xSlotName: { + type: 'string', + minLength: 1 + } + } + } + }, returnParcels); + if (!results.valid) { + throw Whoopsie('INVALID_ARGUMENT', results.format()); + } + //? } + + var bidReceived = false; + + var returnParcel = returnParcels[0]; + + /* prepare the info to send to header stats */ + var headerStatsInfo = { + sessionId: sessionId, + statsId: __profile.statsId, + htSlotId: returnParcel.htSlot.getId(), + requestId: returnParcel.requestId, + xSlotNames: [returnParcel.xSlotName] + }; + + var adResult; + if (adResponse && adResponse.hasOwnProperty('result')) { + adResult = adResponse.result; + } + + var targetingCpm = ''; + + if (adResult && adResult.hasOwnProperty('ad') && !Utilities.isEmpty(adResult.ad)) { + if ((adResult.hasOwnProperty('cpm') && adResult.cpm > 0) || adResult.deal_id) { //jshint ignore:line + bidReceived = true; + var bidPrice = adResult.cpm; + var bidSize = [Number(adResult.width), Number(adResult.height)]; + var bidDealId = adResult.deal_id || ''; //jshint ignore:line + var bidCreative = ''; + + if (bidPrice !== undefined) { + //? if(FEATURES.GPT_LINE_ITEMS) { + targetingCpm = __baseClass._bidTransformers.targeting.apply(bidPrice); + //? } + } + + returnParcel.size = bidSize; + returnParcel.targetingType = 'slot'; + returnParcel.targeting = {}; + + //? if(FEATURES.GPT_LINE_ITEMS) { + var sizeKey = Size.arrayToString(bidSize); + if (bidDealId) { + returnParcel.targeting[__baseClass._configs.targetingKeys.pm] = [sizeKey + '_' + bidDealId]; //jshint ignore:line + } + + if (targetingCpm !== undefined && targetingCpm !== '') { + returnParcel.targeting[__baseClass._configs.targetingKeys.om] = [sizeKey + '_' + targetingCpm]; + } + + returnParcel.targeting[__baseClass._configs.targetingKeys.id] = [returnParcel.requestId]; + //? } + + //? if(FEATURES.RETURN_CREATIVE) { + returnParcel.adm = bidCreative; + //? } + + //? if(FEATURES.RETURN_PRICE) { + returnParcel.price = Number(__baseClass._bidTransformers.price.apply(bidPrice)); + //? } + + var pubKitAdId = SpaceCamp.services.RenderService.registerAd({ + sessionId: sessionId, + partnerId: __profile.partnerId, + adm: bidCreative, + requestId: returnParcel.requestId, + size: returnParcel.size, + price: targetingCpm, + dealId: bidDealId, + timeOfExpiry: __profile.features.demandExpiry.enabled ? (__profile.features.demandExpiry.value + System.now()) : 0 + }); + + //? if(FEATURES.INTERNAL_RENDER) { + returnParcel.targeting.pubKitAdId = pubKitAdId; + //? } + } + } + + if (!bidReceived) { + //? if (DEBUG) { + Scribe.info(__profile.partnerId + ' returned no demand for placement: ' + returnParcel.xSlotRef.placementId); + //? } + returnParcel.pass = true; + } + + if (__profile.enabledAnalytics.requestTime) { + var result = 'hs_slot_pass'; + if (bidReceived) { + result = 'hs_slot_bid'; + } + EventsService.emit(result, headerStatsInfo); + } + } + + /* ===================================== + * Constructors + * ---------------------------------- */ + + (function __constructor() { + EventsService = SpaceCamp.services.EventsService; + RenderService = SpaceCamp.services.RenderService; + ComplianceService = SpaceCamp.services.ComplianceService; + + __profile = { + partnerId: 'AppNexusNetworkHtb', + namespace: 'AppNexusNetworkHtb', + statsId: 'APNXNET', + version: '2.2.0', + targetingType: 'slot', + enabledAnalytics: { + requestTime: true + }, + features: { + demandExpiry: { + enabled: false, + value: 0 + }, + rateLimiting: { + enabled: false, + value: 0 + } + }, + targetingKeys: { + id: 'ix_apnxnet_id', + om: 'ix_apnxnet_om', + pm: 'ix_apnxnet_pm' + }, + bidUnitInCents: 0.01, + lineItemType: Constants.LineItemTypes.ID_AND_SIZE, + callbackType: Partner.CallbackTypes.ID, + architecture: Partner.Architectures.MRA, + requestType: Partner.RequestTypes.ANY + }; + + //? if (DEBUG) { + var PartnerSpecificValidator = AppNexusNetworkValidator; + + var results = ConfigValidators.partnerBaseConfig(configs) || PartnerSpecificValidator(configs); + + if (results) { + throw Whoopsie('INVALID_CONFIG', results); + } + //? } + + __baseUrl = Browser.getProtocol() + '//secure.adnxs.com/jpt'; + __parseFuncPath = SpaceCamp.NAMESPACE + '.' + __profile.namespace + '.adResponseCallback'; + + __baseClass = Partner(__profile, configs, null, { + parseResponse: __parseResponse, + generateRequestObj: __generateRequestObj, + adResponseCallback: adResponseCallback + }); + + /* If wrapper is already active, we might be instantiated late so need to add our callback + since the shell potentially missed its chance */ + if (window[SpaceCamp.NAMESPACE]) { + window[SpaceCamp.NAMESPACE][__profile.namespace] = window[SpaceCamp.NAMESPACE][__profile.namespace] || {}; + window[SpaceCamp.NAMESPACE][__profile.namespace].adResponseCallback = adResponseCallback; + } + })(); + + /* ===================================== + * Public Interface + * ---------------------------------- */ + + var derivedClass = { + /* Class Information + * ---------------------------------- */ + + //? if (DEBUG) { + __type__: 'AppNexusNetworkHtb', + //? } + + //? if (TEST) { + __baseClass: __baseClass, + //? } + + /* Data + * ---------------------------------- */ + + //? if (TEST) { + __profile: __profile, + __baseUrl: __baseUrl, + //? } + + /* Functions + * ---------------------------------- */ + + //? if (TEST) { + __render: __render, + __parseResponse: __parseResponse, + + adResponseCallback: adResponseCallback, + //? } + }; + + return Classify.derive(__baseClass, derivedClass); +} + +//////////////////////////////////////////////////////////////////////////////// +// Exports ///////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +module.exports = AppNexusNetworkHtb;