diff --git a/modules.json b/modules.json
index 8ea023203e6..266ec93ee2a 100644
--- a/modules.json
+++ b/modules.json
@@ -18,6 +18,8 @@
"criteoBidAdapter",
"audienceNetworkBidAdapter",
"teadsBidAdapter",
+ "improvedigitalBidAdapter",
+ "improvedigitalClientBidAdapter",
"currency",
"schain",
"marfeelAnalyticsAdapter"
diff --git a/modules/improvedigitalClientBidAdapter.js b/modules/improvedigitalClientBidAdapter.js
new file mode 100644
index 00000000000..f27d0bb4b40
--- /dev/null
+++ b/modules/improvedigitalClientBidAdapter.js
@@ -0,0 +1,587 @@
+import * as utils from '../src/utils';
+import { registerBidder } from '../src/adapters/bidderFactory';
+import { config } from '../src/config';
+import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes';
+
+const BIDDER_CODE = 'improvedigital_client';
+
+export const spec = {
+ version: '6.0.0',
+ code: BIDDER_CODE,
+ aliases: ['id'],
+ supportedMediaTypes: [BANNER, NATIVE, VIDEO],
+
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {object} bid The bid to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function (bid) {
+ return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId)));
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
+ * @return ServerRequest Info describing the request to the server.
+ */
+ buildRequests: function (bidRequests, bidderRequest) {
+ let normalizedBids = bidRequests.map((bidRequest) => {
+ return getNormalizedBidRequest(bidRequest);
+ });
+
+ let idClient = new ImproveDigitalAdServerJSClient('hb');
+ let requestParameters = {
+ singleRequestMode: (config.getConfig('improvedigital.singleRequest') === true),
+ returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT,
+ libVersion: this.version
+ };
+
+ if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) {
+ requestParameters.gdpr = bidderRequest.gdprConsent.consentString;
+ }
+
+ if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) {
+ requestParameters.referrer = bidderRequest.refererInfo.referer;
+ }
+
+ requestParameters.schain = bidRequests[0].schain;
+
+ let requestObj = idClient.createRequest(
+ normalizedBids, // requestObject
+ requestParameters
+ );
+
+ if (requestObj.errors && requestObj.errors.length > 0) {
+ utils.logError('ID WARNING 0x01');
+ }
+
+ return requestObj.requests;
+ },
+
+ /**
+ * Unpack the response from the server into a list of bids.
+ *
+ * @param {*} serverResponse A successful response from the server.
+ * @return {Bid[]} An array of bids which were nested inside the server.
+ */
+ interpretResponse: function (serverResponse, bidRequest) {
+ const bids = [];
+ utils._each(serverResponse.body.bid, function (bidObject) {
+ if (!bidObject.price || bidObject.price === null ||
+ bidObject.hasOwnProperty('errorCode') ||
+ (!bidObject.adm && !bidObject.native)) {
+ return;
+ }
+
+ const bid = {};
+
+ if (bidObject.native) {
+ // Native
+ bid.native = getNormalizedNativeAd(bidObject.native);
+ // Expose raw oRTB response to the client to allow parsing assets not directly supported by Prebid
+ bid.ortbNative = bidObject.native;
+ if (bidObject.nurl) {
+ bid.native.impressionTrackers.unshift(bidObject.nurl);
+ }
+ bid.mediaType = NATIVE;
+ } else if (bidObject.ad_type && bidObject.ad_type === 'video') {
+ bid.vastXml = bidObject.adm;
+ bid.mediaType = VIDEO;
+ } else {
+ // Banner
+ let nurl = '';
+ if (bidObject.nurl && bidObject.nurl.length > 0) {
+ nurl = ``;
+ }
+ bid.ad = `${nurl}`;
+ bid.mediaType = BANNER;
+ }
+
+ // Common properties
+ bid.adId = bidObject.id;
+ bid.cpm = parseFloat(bidObject.price);
+ bid.creativeId = bidObject.crid;
+ bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD';
+
+ // Deal ID. Composite ads can have multiple line items and the ID of the first
+ // dealID line item will be used.
+ if (utils.isNumber(bidObject.lid) && bidObject.buying_type === 'deal_id') {
+ bid.dealId = bidObject.lid;
+ } else if (Array.isArray(bidObject.lid) &&
+ Array.isArray(bidObject.buying_type) &&
+ bidObject.lid.length === bidObject.buying_type.length) {
+ let isDeal = false;
+ bidObject.buying_type.forEach((bt, i) => {
+ if (isDeal) return;
+ if (bt === 'deal_id') {
+ isDeal = true;
+ bid.dealId = bidObject.lid[i];
+ }
+ });
+ }
+
+ bid.height = bidObject.h;
+ bid.netRevenue = bidObject.isNet ? bidObject.isNet : false;
+ bid.requestId = bidObject.id;
+ bid.ttl = 300;
+ bid.width = bidObject.w;
+
+ if (!bid.width || !bid.height) {
+ bid.width = 1;
+ bid.height = 1;
+ if (bidRequest.sizes) {
+ bid.width = bidRequest.sizes[0][0];
+ bid.height = bidRequest.sizes[0][1];
+ }
+ }
+
+ bids.push(bid);
+ });
+ return bids;
+ },
+
+ /**
+ * Register the user sync pixels which should be dropped after the auction.
+ *
+ * @param {SyncOptions} syncOptions Which user syncs are allowed?
+ * @param {ServerResponse[]} serverResponses List of server's responses.
+ * @return {UserSync[]} The user syncs which should be dropped.
+ */
+ getUserSyncs: function(syncOptions, serverResponses) {
+ if (syncOptions.pixelEnabled) {
+ const syncs = [];
+ serverResponses.forEach(response => {
+ response.body.bid.forEach(bidObject => {
+ if (utils.isArray(bidObject.sync)) {
+ bidObject.sync.forEach(syncElement => {
+ if (syncs.indexOf(syncElement) === -1) {
+ syncs.push(syncElement);
+ }
+ });
+ }
+ });
+ });
+ return syncs.map(sync => ({ type: 'image', url: sync }));
+ }
+ return [];
+ }
+};
+
+function getNormalizedBidRequest(bid) {
+ let adUnitId = utils.getBidIdParameter('adUnitCode', bid) || null;
+ let placementId = utils.getBidIdParameter('placementId', bid.params) || null;
+ let publisherId = null;
+ let placementKey = null;
+
+ if (placementId === null) {
+ publisherId = utils.getBidIdParameter('publisherId', bid.params) || null;
+ placementKey = utils.getBidIdParameter('placementKey', bid.params) || null;
+ }
+ const keyValues = utils.getBidIdParameter('keyValues', bid.params) || null;
+ const singleSizeFilter = utils.getBidIdParameter('size', bid.params) || null;
+ const bidId = utils.getBidIdParameter('bidId', bid);
+ const transactionId = utils.getBidIdParameter('transactionId', bid);
+ const currency = config.getConfig('currency.adServerCurrency');
+ const bidFloor = utils.getBidIdParameter('bidFloor', bid.params);
+ const bidFloorCur = utils.getBidIdParameter('bidFloorCur', bid.params);
+
+ let normalizedBidRequest = {};
+ const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video');
+ const context = utils.deepAccess(bid, 'mediaTypes.video.context');
+ if (bid.mediaType === 'video' || (videoMediaType && context !== 'outstream')) {
+ normalizedBidRequest.adTypes = [ VIDEO ];
+ }
+ if (placementId) {
+ normalizedBidRequest.placementId = placementId;
+ } else {
+ if (publisherId) {
+ normalizedBidRequest.publisherId = publisherId;
+ }
+ if (placementKey) {
+ normalizedBidRequest.placementKey = placementKey;
+ }
+ }
+
+ if (keyValues) {
+ normalizedBidRequest.keyValues = keyValues;
+ }
+
+ if (config.getConfig('improvedigital.usePrebidSizes') === true && bid.sizes && bid.sizes.length > 0) {
+ normalizedBidRequest.format = bid.sizes;
+ } else if (singleSizeFilter && singleSizeFilter.w && singleSizeFilter.h) {
+ normalizedBidRequest.size = {};
+ normalizedBidRequest.size.h = singleSizeFilter.h;
+ normalizedBidRequest.size.w = singleSizeFilter.w;
+ }
+
+ if (bidId) {
+ normalizedBidRequest.id = bidId;
+ }
+ if (adUnitId) {
+ normalizedBidRequest.adUnitId = adUnitId;
+ }
+ if (transactionId) {
+ normalizedBidRequest.transactionId = transactionId;
+ }
+ if (currency) {
+ normalizedBidRequest.currency = currency;
+ }
+ if (bidFloor) {
+ normalizedBidRequest.bidFloor = bidFloor;
+ normalizedBidRequest.bidFloorCur = bidFloorCur ? bidFloorCur.toUpperCase() : 'USD';
+ }
+ return normalizedBidRequest;
+}
+
+function getNormalizedNativeAd(rawNative) {
+ const native = {};
+ if (!rawNative || !utils.isArray(rawNative.assets)) {
+ return null;
+ }
+ // Assets
+ rawNative.assets.forEach(asset => {
+ if (asset.title) {
+ native.title = asset.title.text;
+ } else if (asset.data) {
+ switch (asset.data.type) {
+ case 1:
+ native.sponsoredBy = asset.data.value;
+ break;
+ case 2:
+ native.body = asset.data.value;
+ break;
+ case 3:
+ native.rating = asset.data.value;
+ break;
+ case 4:
+ native.likes = asset.data.value;
+ break;
+ case 5:
+ native.downloads = asset.data.value;
+ break;
+ case 6:
+ native.price = asset.data.value;
+ break;
+ case 7:
+ native.salePrice = asset.data.value;
+ break;
+ case 8:
+ native.phone = asset.data.value;
+ break;
+ case 9:
+ native.address = asset.data.value;
+ break;
+ case 10:
+ native.body2 = asset.data.value;
+ break;
+ case 11:
+ native.displayUrl = asset.data.value;
+ break;
+ case 12:
+ native.cta = asset.data.value;
+ break;
+ }
+ } else if (asset.img) {
+ switch (asset.img.type) {
+ case 2:
+ native.icon = {
+ url: asset.img.url,
+ width: asset.img.w,
+ height: asset.img.h
+ };
+ break;
+ case 3:
+ native.image = {
+ url: asset.img.url,
+ width: asset.img.w,
+ height: asset.img.h
+ };
+ break;
+ }
+ }
+ });
+ // Trackers
+ if (rawNative.eventtrackers) {
+ native.impressionTrackers = [];
+ rawNative.eventtrackers.forEach(tracker => {
+ // Only handle impression event. Viewability events are not supported yet.
+ if (tracker.event !== 1) return;
+ switch (tracker.method) {
+ case 1: // img
+ native.impressionTrackers.push(tracker.url);
+ break;
+ case 2: // js
+ // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used.
+ native.javascriptTrackers = ``;
+ break;
+ }
+ });
+ } else {
+ native.impressionTrackers = rawNative.imptrackers || [];
+ native.javascriptTrackers = rawNative.jstracker;
+ }
+ if (rawNative.link) {
+ native.clickUrl = rawNative.link.url;
+ native.clickTrackers = rawNative.link.clicktrackers;
+ }
+ if (rawNative.privacy) {
+ native.privacyLink = rawNative.privacy;
+ }
+ return native;
+}
+registerBidder(spec);
+
+export function ImproveDigitalAdServerJSClient(endPoint) {
+ this.CONSTANTS = {
+ AD_SERVER_BASE_URL: 'ice.360yield.com',
+ END_POINT: endPoint || 'hb',
+ AD_SERVER_URL_PARAM: 'jsonp=',
+ CLIENT_VERSION: 'JS-6.2.0',
+ MAX_URL_LENGTH: 2083,
+ ERROR_CODES: {
+ MISSING_PLACEMENT_PARAMS: 2,
+ LIB_VERSION_MISSING: 3
+ },
+ RETURN_OBJ_TYPE: {
+ DEFAULT: 0,
+ URL_PARAMS_SPLIT: 1
+ }
+ };
+
+ this.getErrorReturn = function(errorCode) {
+ return {
+ idMappings: {},
+ requests: {},
+ 'errorCode': errorCode
+ };
+ };
+
+ this.createRequest = function(requestObject, requestParameters, extraRequestParameters) {
+ if (!requestParameters.libVersion) {
+ return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING);
+ }
+
+ requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT;
+ requestParameters.adServerBaseUrl = 'https://' + (requestParameters.adServerBaseUrl || this.CONSTANTS.AD_SERVER_BASE_URL);
+
+ let impressionObjects = [];
+ let impressionObject;
+ if (utils.isArray(requestObject)) {
+ for (let counter = 0; counter < requestObject.length; counter++) {
+ impressionObject = this.createImpressionObject(requestObject[counter]);
+ impressionObjects.push(impressionObject);
+ }
+ } else {
+ impressionObject = this.createImpressionObject(requestObject);
+ impressionObjects.push(impressionObject);
+ }
+
+ let returnIdMappings = true;
+ if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT) {
+ returnIdMappings = false;
+ }
+
+ let returnObject = {};
+ returnObject.requests = [];
+ if (returnIdMappings) {
+ returnObject.idMappings = [];
+ }
+ let errors = null;
+
+ let baseUrl = `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`;
+
+ let bidRequestObject = {
+ bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters)
+ };
+ for (let counter = 0; counter < impressionObjects.length; counter++) {
+ impressionObject = impressionObjects[counter];
+
+ if (impressionObject.errorCode) {
+ errors = errors || [];
+ errors.push({
+ errorCode: impressionObject.errorCode,
+ adUnitId: impressionObject.adUnitId
+ });
+ } else {
+ if (returnIdMappings) {
+ returnObject.idMappings.push({
+ adUnitId: impressionObject.adUnitId,
+ id: impressionObject.impressionObject.id
+ });
+ }
+ bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || [];
+ bidRequestObject.bid_request.imp.push(impressionObject.impressionObject);
+
+ let writeLongRequest = false;
+ const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject));
+ if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) {
+ writeLongRequest = true;
+ if (bidRequestObject.bid_request.imp.length > 1) {
+ // Pop the current request and process it again in the next iteration
+ bidRequestObject.bid_request.imp.pop();
+ if (returnIdMappings) {
+ returnObject.idMappings.pop();
+ }
+ counter--;
+ }
+ }
+
+ if (writeLongRequest ||
+ !requestParameters.singleRequestMode ||
+ counter === impressionObjects.length - 1) {
+ returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject));
+ bidRequestObject = {
+ bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters)
+ };
+ }
+ }
+ }
+
+ if (errors) {
+ returnObject.errors = errors;
+ }
+
+ return returnObject;
+ };
+
+ this.formatRequest = function(requestParameters, bidRequestObject) {
+ switch (requestParameters.returnObjType) {
+ case this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT:
+ return {
+ method: 'GET',
+ url: `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}`,
+ data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${encodeURIComponent(JSON.stringify(bidRequestObject))}`
+ };
+ default:
+ const baseUrl = `${requestParameters.adServerBaseUrl}/` +
+ `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`;
+ return {
+ url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject))
+ }
+ }
+ };
+
+ this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) {
+ let impressionBidRequestObject = {};
+ impressionBidRequestObject.secure = 1;
+ if (requestParameters.requestId) {
+ impressionBidRequestObject.id = requestParameters.requestId;
+ } else {
+ impressionBidRequestObject.id = utils.getUniqueIdentifierStr();
+ }
+ if (requestParameters.domain) {
+ impressionBidRequestObject.domain = requestParameters.domain;
+ }
+ if (requestParameters.page) {
+ impressionBidRequestObject.page = requestParameters.page;
+ }
+ if (requestParameters.ref) {
+ impressionBidRequestObject.ref = requestParameters.ref;
+ }
+ if (requestParameters.callback) {
+ impressionBidRequestObject.callback = requestParameters.callback;
+ }
+ if (requestParameters.libVersion) {
+ impressionBidRequestObject.version = requestParameters.libVersion + '-' + this.CONSTANTS.CLIENT_VERSION;
+ }
+ if (requestParameters.referrer) {
+ impressionBidRequestObject.referrer = requestParameters.referrer;
+ }
+ if (requestParameters.gdpr || requestParameters.gdpr === 0) {
+ impressionBidRequestObject.gdpr = requestParameters.gdpr;
+ }
+ if (requestParameters.schain) {
+ impressionBidRequestObject.schain = requestParameters.schain;
+ }
+ if (extraRequestParameters) {
+ for (let prop in extraRequestParameters) {
+ impressionBidRequestObject[prop] = extraRequestParameters[prop];
+ }
+ }
+
+ return impressionBidRequestObject;
+ };
+
+ this.createImpressionObject = function(placementObject) {
+ let outputObject = {};
+ let impressionObject = {};
+ outputObject.impressionObject = impressionObject;
+
+ if (placementObject.id) {
+ impressionObject.id = placementObject.id;
+ } else {
+ impressionObject.id = utils.getUniqueIdentifierStr();
+ }
+ if (placementObject.adTypes) {
+ impressionObject.ad_types = placementObject.adTypes;
+ }
+ if (placementObject.adUnitId) {
+ outputObject.adUnitId = placementObject.adUnitId;
+ }
+ if (placementObject.currency) {
+ impressionObject.currency = placementObject.currency.toUpperCase();
+ }
+ if (placementObject.bidFloor) {
+ impressionObject.bidfloor = placementObject.bidFloor;
+ }
+ if (placementObject.bidFloorCur) {
+ impressionObject.bidfloorcur = placementObject.bidFloorCur.toUpperCase();
+ }
+ if (placementObject.placementId) {
+ impressionObject.pid = placementObject.placementId;
+ }
+ if (placementObject.publisherId) {
+ impressionObject.pubid = placementObject.publisherId;
+ }
+ if (placementObject.placementKey) {
+ impressionObject.pkey = placementObject.placementKey;
+ }
+ if (placementObject.transactionId) {
+ impressionObject.tid = placementObject.transactionId;
+ }
+ if (placementObject.keyValues) {
+ for (let key in placementObject.keyValues) {
+ for (let valueCounter = 0; valueCounter < placementObject.keyValues[key].length; valueCounter++) {
+ impressionObject.kvw = impressionObject.kvw || {};
+ impressionObject.kvw[key] = impressionObject.kvw[key] || [];
+ impressionObject.kvw[key].push(placementObject.keyValues[key][valueCounter]);
+ }
+ }
+ }
+
+ impressionObject.banner = {};
+ if (placementObject.size && placementObject.size.w && placementObject.size.h) {
+ impressionObject.banner.w = placementObject.size.w;
+ impressionObject.banner.h = placementObject.size.h;
+ }
+
+ // Set of desired creative sizes
+ // Input Format: array of pairs, i.e. [[300, 250], [250, 250]]
+ if (placementObject.format && utils.isArray(placementObject.format)) {
+ const format = placementObject.format
+ .filter(sizePair => sizePair.length === 2 &&
+ utils.isInteger(sizePair[0]) &&
+ utils.isInteger(sizePair[1]) &&
+ sizePair[0] >= 0 &&
+ sizePair[1] >= 0)
+ .map(sizePair => {
+ return { w: sizePair[0], h: sizePair[1] }
+ });
+ if (format.length > 0) {
+ impressionObject.banner.format = format;
+ }
+ }
+
+ if (!impressionObject.pid &&
+ !impressionObject.pubid &&
+ !impressionObject.pkey &&
+ !(impressionObject.banner && impressionObject.banner.w && impressionObject.banner.h)) {
+ outputObject.impressionObject = null;
+ outputObject.errorCode = this.CONSTANTS.ERROR_CODES.MISSING_PLACEMENT_PARAMS;
+ }
+ return outputObject;
+ };
+}