prebid/Prebid.js

View on GitHub
modules/kueezBidAdapter.js

Summary

Maintainability
F
3 days
Test Coverage
import {
  logWarn,
  logInfo,
  isArray,
  isFn,
  deepAccess,
  isEmpty,
  contains,
  timestamp,
  triggerPixel,
  isInteger,
  getBidIdParameter
} from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { config } from '../src/config.js';

const BIDDER_ENDPOINT = 'https://hb.kueezssp.com/hb-kz-multi';
const BIDDER_TEST_ENDPOINT = 'https://hb.kueezssp.com/hb-multi-kz-test'
const BIDDER_CODE = 'kueez';
const MAIN_CURRENCY = 'USD';
const MEDIA_TYPES = [BANNER, VIDEO];
const TTL = 420;
const VERSION = '1.0.0';
const SUPPORTED_SYNC_METHODS = {
  IFRAME: 'iframe',
  PIXEL: 'pixel'
}

export const spec = {
  code: BIDDER_CODE,
  version: VERSION,
  supportedMediaTypes: MEDIA_TYPES,
  isBidRequestValid: function (bidRequest) {
    return validateParams(bidRequest);
  },
  buildRequests: function (validBidRequests, bidderRequest) {
    const [ sharedParams ] = validBidRequests;
    const testMode = sharedParams.params.testMode;
    const bidsToSend = prepareBids(validBidRequests, sharedParams, bidderRequest);

    return {
      method: 'POST',
      url: getBidderEndpoint(testMode),
      data: bidsToSend
    }
  },
  interpretResponse: function ({body}) {
    const bidResponses = body?.bids;

    if (!bidResponses || !bidResponses.length) {
      return [];
    }

    return parseBidResponses(bidResponses);
  },
  getUserSyncs: function (syncOptions, serverResponses) {
    const syncs = [];
    for (const response of serverResponses) {
      if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) {
        const pixels = response.body.params.userSyncPixels.map(pixel => {
          return {
            type: 'image',
            url: pixel
          }
        })
        syncs.push(...pixels)
      }
      if (syncOptions.iframeEnabled && response.body.params.userSyncURL) {
        syncs.push({
          type: 'iframe',
          url: response.body.params.userSyncURL
        });
      }
    }
    return syncs;
  },
  onBidWon: function (bid) {
    if (bid == null) {
      return;
    }

    logInfo('onBidWon:', bid);
    if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) {
      triggerPixel(bid.nurl);
    }
  }
};

registerBidder(spec);

/**
 * Get schain string value
 * @param schainObject {Object}
 * @returns {string}
 */
function getSupplyChain(schainObject) {
  if (isEmpty(schainObject)) {
    return '';
  }
  let scStr = `${schainObject.ver},${schainObject.complete}`;
  schainObject.nodes.forEach((node) => {
    scStr += '!';
    scStr += `${getEncodedValIfNotEmpty(node.asi)},`;
    scStr += `${getEncodedValIfNotEmpty(node.sid)},`;
    scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`;
    scStr += `${getEncodedValIfNotEmpty(node.rid)},`;
    scStr += `${getEncodedValIfNotEmpty(node.name)},`;
    scStr += `${getEncodedValIfNotEmpty(node.domain)}`;
  });
  return scStr;
}

/**
 * Get the encoded value
 * @param val {string}
 * @returns {string}
 */
function getEncodedValIfNotEmpty(val) {
  return !isEmpty(val) ? encodeURIComponent(val) : '';
}

/**
 * get device type
 * @returns {string}
 */
function getDeviceType() {
  const ua = navigator.userAgent;
  if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i
    .test(ua.toLowerCase())) {
    return '5';
  }
  if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i
    .test(ua.toLowerCase())) {
    return '4';
  }
  if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i
    .test(ua.toLowerCase())) {
    return '3';
  }
  return '1';
}

/**
 * Get floor price
 * @param bid {bid}
 * @param mediaType {string}
 * @returns {Number}
 */
function getFloorPrice(bid, mediaType) {
  let floor = 0;

  if (isFn(bid.getFloor)) {
    let floorResult = bid.getFloor({
      currency: MAIN_CURRENCY,
      mediaType: mediaType,
      size: '*'
    });
    floor = floorResult.currency === MAIN_CURRENCY && floorResult.floor ? floorResult.floor : 0;
  }

  return floor;
}

/**
 * Get the ad sizes array from the bid
 * @param bid {bid}
 * @param mediaType {string}
 * @returns {Array}
 */
function getSizesArray(bid, mediaType) {
  let sizes = []

  if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) {
    sizes = bid.mediaTypes[mediaType].sizes;
  } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) {
    sizes = bid.sizes;
  }

  return sizes;
}

/**
 * Get the preferred user-sync method
 * @param filterSettings {filterSettings}
 * @param bidderCode {string}
 * @returns {string}
 */
function getSyncMethod(filterSettings, bidderCode) {
  const iframeConfigs = ['all', 'iframe'];
  const pixelConfig = 'image';
  if (filterSettings && iframeConfigs.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) {
    return SUPPORTED_SYNC_METHODS.IFRAME;
  }
  if (!filterSettings || !filterSettings[pixelConfig] || isSyncMethodAllowed(filterSettings[pixelConfig], bidderCode)) {
    return SUPPORTED_SYNC_METHODS.PIXEL;
  }
}

/**
 * Check sync rule support
 * @param filterSetting {Object}
 * @param bidderCode {string}
 * @returns {boolean}
 */
function isSyncMethodAllowed(filterSetting, bidderCode) {
  if (!filterSetting) {
    return false;
  }
  const bidders = isArray(filterSetting.bidders) ? filterSetting.bidders : [bidderCode];
  return filterSetting.filter === 'include' && contains(bidders, bidderCode);
}

/**
 * Get the bidder endpoint
 * @param testMode {boolean}
 * @returns {string}
 */
function getBidderEndpoint(testMode) {
  return testMode ? BIDDER_TEST_ENDPOINT : BIDDER_ENDPOINT;
}

/**
 * Generates the bidder parameters
 * @param validBidRequests {Array}
 * @param bidderRequest {bidderRequest}
 * @returns {Array}
 */
function generateBidParams(validBidRequests, bidderRequest) {
  const bidsArray = [];

  if (validBidRequests.length) {
    validBidRequests.forEach(bid => {
      bidsArray.push(generateBidParameters(bid, bidderRequest));
    });
  }

  return bidsArray;
}

/**
 * Generate bid specific parameters
 * @param bid {bid}
 * @param bidderRequest {bidderRequest}
 * @returns {Object} bid specific params object
 */
function generateBidParameters(bid, bidderRequest) {
  const {params} = bid;
  const mediaType = isBanner(bid) ? BANNER : VIDEO;
  const sizesArray = getSizesArray(bid, mediaType);
  const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`);
  const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`);
  const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`);
  const paramsFloorPrice = isNaN(params.floorPrice) ? 0 : params.floorPrice;

  const bidObject = {
    adUnitCode: getBidIdParameter('adUnitCode', bid),
    bidId: getBidIdParameter('bidId', bid),
    loop: getBidIdParameter('bidderRequestsCount', bid),
    bidderRequestId: getBidIdParameter('bidderRequestId', bid),
    floorPrice: Math.max(getFloorPrice(bid, mediaType), paramsFloorPrice),
    mediaType,
    sizes: sizesArray,
    transactionId: bid.ortb2Imp?.ext?.tid || ''
  };

  if (pos) {
    bidObject.pos = pos;
  }

  if (gpid) {
    bidObject.gpid = gpid;
  }

  if (placementId) {
    bidObject.placementId = placementId;
  }

  if (mediaType === VIDEO) {
    populateVideoParams(bidObject, bid);
  }

  return bidObject;
}

/**
 * Checks if the media type is a banner
 * @param bid {bid}
 * @returns {boolean}
 */
function isBanner(bid) {
  return bid.mediaTypes && bid.mediaTypes.banner;
}

/**
 * Generate params that are common between all bids
 * @param sharedParams {sharedParams}
 * @param bidderRequest {bidderRequest}
 * @returns {object} the common params object
 */
function generateSharedParams(sharedParams, bidderRequest) {
  const {bidderCode} = bidderRequest;
  const {syncEnabled, filterSettings} = config.getConfig('userSync') || {};
  const domain = window.location.hostname;
  const generalBidParams = getBidIdParameter('params', sharedParams);
  const userIds = getBidIdParameter('userId', sharedParams);
  const ortb2Metadata = bidderRequest.ortb2 || {};
  const timeout = bidderRequest.timeout;

  const params = {
    adapter_version: VERSION,
    auction_start: timestamp(),
    device_type: getDeviceType(),
    dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0,
    publisher_id: generalBidParams.org,
    publisher_name: domain,
    session_id: getBidIdParameter('auctionId', sharedParams),
    site_domain: domain,
    tmax: timeout,
    ua: navigator.userAgent,
    wrapper_type: 'prebidjs',
    wrapper_vendor: '$$PREBID_GLOBAL$$',
    wrapper_version: '$prebid.version$'
  };

  if (syncEnabled) {
    const allowedSyncMethod = getSyncMethod(filterSettings, bidderCode);
    if (allowedSyncMethod) {
      params.cs_method = allowedSyncMethod;
    }
  }

  if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) {
    params.gdpr = bidderRequest.gdprConsent.gdprApplies;
    params.gdpr_consent = bidderRequest.gdprConsent.consentString;
  }

  if (bidderRequest.uspConsent) {
    params.us_privacy = bidderRequest.uspConsent;
  }

  if (generalBidParams.ifa) {
    params.ifa = generalBidParams.ifa;
  }

  if (ortb2Metadata.site) {
    params.site_metadata = JSON.stringify(ortb2Metadata.site);
  }

  if (ortb2Metadata.user) {
    params.user_metadata = JSON.stringify(ortb2Metadata.user);
  }

  if (bidderRequest && bidderRequest.refererInfo) {
    params.referrer = deepAccess(bidderRequest, 'refererInfo.ref');
    params.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href');
  }

  if (sharedParams.schain) {
    params.schain = getSupplyChain(sharedParams.schain);
  }

  if (userIds) {
    params.userIds = JSON.stringify(userIds);
  }

  return params;
}

/**
 * Validates the bidder params
 * @param bidRequest {bidRequest}
 * @returns {boolean}
 */
function validateParams(bidRequest) {
  let isValid = true;

  if (!bidRequest.params) {
    logWarn('Kueez adapter - missing params');
    isValid = false;
  }

  if (!bidRequest.params.org) {
    logWarn('Kueez adapter - org is a required param');
    isValid = false;
  }

  return isValid;
}

/**
 * Validates the bidder params
 * @param validBidRequests {Array}
 * @param sharedParams {sharedParams}
 * @param bidderRequest {bidderRequest}
 * @returns {Object}
 */
function prepareBids(validBidRequests, sharedParams, bidderRequest) {
  return {
    params: generateSharedParams(sharedParams, bidderRequest),
    bids: generateBidParams(validBidRequests, bidderRequest)
  }
}

function getPlaybackMethod(bid) {
  const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`);

  if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) {
    return playbackMethod[0];
  } else if (isInteger(playbackMethod)) {
    return playbackMethod;
  }
}

function populateVideoParams(params, bid) {
  const linearity = deepAccess(bid, `mediaTypes.video.linearity`);
  const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`);
  const minDuration = deepAccess(bid, `mediaTypes.video.minduration`);
  const placement = deepAccess(bid, `mediaTypes.video.placement`);
  const playbackMethod = getPlaybackMethod(bid);
  const skip = deepAccess(bid, `mediaTypes.video.skip`);

  if (linearity) {
    params.linearity = linearity;
  }

  if (maxDuration) {
    params.maxDuration = maxDuration;
  }

  if (minDuration) {
    params.minDuration = minDuration;
  }

  if (placement) {
    params.placement = placement;
  }

  if (playbackMethod) {
    params.playbackMethod = playbackMethod;
  }

  if (skip) {
    params.skip = skip;
  }
}

/**
 * Processes the bid responses
 * @param bids {Array}
 * @returns {Array}
 */
function parseBidResponses(bids) {
  return bids.map(bid => {
    const bidResponse = {
      cpm: bid.cpm,
      creativeId: bid.requestId,
      currency: bid.currency || MAIN_CURRENCY,
      height: bid.height,
      mediaType: bid.mediaType,
      meta: {
        mediaType: bid.mediaType
      },
      netRevenue: bid.netRevenue || true,
      nurl: bid.nurl,
      requestId: bid.requestId,
      ttl: bid.ttl || TTL,
      width: bid.width
    };

    if (bid.adomain && bid.adomain.length) {
      bidResponse.meta.advertiserDomains = bid.adomain;
    }

    if (bid.mediaType === VIDEO) {
      bidResponse.vastXml = bid.vastXml;
    } else if (bid.mediaType === BANNER) {
      bidResponse.ad = bid.ad;
    }

    return bidResponse;
  });
}