prebid/Prebid.js

View on GitHub
modules/smartxBidAdapter.js

Summary

Maintainability
F
6 days
Test Coverage
import {
  logError,
  deepAccess,
  isArray,
  getDNT,
  generateUUID,
  isEmpty,
  _each,
  logMessage,
  logWarn,
  isFn,
  isPlainObject,
  getBidIdParameter
} from '../src/utils.js';
import {
  Renderer
} from '../src/Renderer.js';
import {
  registerBidder
} from '../src/adapters/bidderFactory.js';
import {
  VIDEO
} from '../src/mediaTypes.js';

/**
 * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
 */

const BIDDER_CODE = 'smartx';
const URL = 'https://bid.sxp.smartclip.net/bid/1000';
const GVLID = 115;
export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  supportedMediaTypes: [VIDEO],
  /**
   * Determines whether or not the given bid request is valid.
   * From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid).
   *
   * @param {object} bid The bid to validate.
   * @return boolean True if this is a valid bid, and false otherwise.
   */
  isBidRequestValid: function (bid) {
    if (bid && typeof bid.params !== 'object') {
      logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.');
      return false;
    }
    if (!deepAccess(bid, 'mediaTypes.video')) {
      logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.');
      return false;
    }
    const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
    if (!playerSize || !isArray(playerSize)) {
      logError(BIDDER_CODE + ': mediaTypes.video.playerSize is not defined in the bidder settings.');
      return false;
    }
    if (!getBidIdParameter('tagId', bid.params)) {
      logError(BIDDER_CODE + ': tagId is not present in bidder params');
      return false;
    }
    if (!getBidIdParameter('publisherId', bid.params)) {
      logError(BIDDER_CODE + ': publisherId is not present in bidder params');
      return false;
    }
    if (!getBidIdParameter('siteId', bid.params)) {
      logError(BIDDER_CODE + ': siteId is not present in bidder params');
      return false;
    }
    if (deepAccess(bid, 'mediaTypes.video.context') === 'outstream') {
      if (!getBidIdParameter('outstream_options', bid.params)) {
        logError(BIDDER_CODE + ': outstream_options parameter is not defined');
        return false;
      }
      if (!getBidIdParameter('slot', bid.params.outstream_options)) {
        logError(BIDDER_CODE + ': slot parameter is not defined in outstream_options object in the configuration');
        return false;
      }
    }

    return true;
  },
  /**
   * Make a server request from the list of BidRequests.
   * from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test.
   *
   * @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) {
    const page = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation;
    const isPageSecure = !!page.match(/^https:/)

    const smartxRequests = bidRequests.map(function (bid) {
      const tagId = getBidIdParameter('tagId', bid.params);
      const publisherId = getBidIdParameter('publisherId', bid.params);
      const bidfloor = getBidFloor(bid) || 0;
      const bidfloorcur = getBidIdParameter('bidfloorcur', bid.params) || 'EUR';
      const siteId = getBidIdParameter('siteId', bid.params);
      const sitekey = getBidIdParameter('sitekey', bid.params);
      const domain = getBidIdParameter('domain', bid.params);
      const cat = getBidIdParameter('cat', bid.params) || [''];
      let pubcid = null;
      const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
      const contentWidth = playerSize[0][0];
      const contentHeight = playerSize[0][1];
      const secure = +(isPageSecure || (getBidIdParameter('secure', bid.params) ? 1 : 0));
      const ext = {
        sdk_name: 'Prebid 1+'
      };
      const mimes = getBidIdParameter('mimes', bid.params) || ['application/javascript', 'video/mp4', 'video/webm'];
      const linearity = getBidIdParameter('linearity', bid.params) || 1;
      const minduration = getBidIdParameter('minduration', bid.params) || 0;
      const maxduration = getBidIdParameter('maxduration', bid.params) || 500;
      const startdelay = getBidIdParameter('startdelay', bid.params) || 0;
      const minbitrate = getBidIdParameter('minbitrate', bid.params) || 0;
      const maxbitrate = getBidIdParameter('maxbitrate', bid.params) || 3500;
      const delivery = getBidIdParameter('delivery', bid.params) || [2];
      const pos = getBidIdParameter('pos', bid.params) || 1;
      const api = getBidIdParameter('api', bid.params) || [2];
      const protocols = getBidIdParameter('protocols', bid.params) || [2, 3, 5, 6];
      var contextcustom = deepAccess(bid, 'mediaTypes.video.context');
      var placement = 1;

      if (contextcustom === 'outstream') {
        placement = 3;
      }

      let smartxReq = [{
        id: bid.bidId,
        secure: secure,
        bidfloor: bidfloor,
        bidfloorcur: bidfloorcur,
        video: {
          w: contentWidth,
          h: contentHeight,
          mimes: mimes,
          linearity: linearity,
          minduration: minduration,
          maxduration: maxduration,
          startdelay: startdelay,
          protocols: protocols,
          minbitrate: minbitrate,
          maxbitrate: maxbitrate,
          delivery: delivery,
          pos: pos,
          placement: placement,
          api: api,
          ext: ext
        },
        tagid: tagId,
        ext: {
          'smart.bidpricetype': 1
        }
      }];

      if (bid.crumbs && bid.crumbs.pubcid) {
        pubcid = bid.crumbs.pubcid;
      }

      const language = navigator.language ? 'language' : 'userLanguage';

      const device = {
        h: screen.height,
        w: screen.width,
        dnt: getDNT() ? 1 : 0,
        language: navigator[language].split('-')[0],
        make: navigator.vendor ? navigator.vendor : '',
        ua: navigator.userAgent
      };

      const at = getBidIdParameter('at', bid.params) || 2;

      const cur = getBidIdParameter('cur', bid.params) || ['EUR'];

      const requestPayload = {
        id: generateUUID(),
        imp: smartxReq,
        site: {
          id: siteId,
          page: page,
          cat: cat,
          domain: domain,
          publisher: {
            id: publisherId
          },
          content: {
            ext: {
              prebid: {
                name: 'pbjs',
                version: '$prebid.version$'
              }
            }
          }
        },
        device: device,
        at: at,
        cur: cur,
        ext: {}
      };

      const userExt = {};

      // Add GDPR flag and consent string
      if (bidderRequest && bidderRequest.gdprConsent) {
        userExt.consent = bidderRequest.gdprConsent.consentString;
        if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') {
          requestPayload.regs = {
            ext: {
              gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)
            }
          };
        }
      }

      // Add sitekey if available
      if (sitekey) {
        requestPayload.site.content.ext.sitekey = sitekey;
      }

      // Add common id if available
      if (pubcid) {
        userExt.fpc = pubcid;
      }

      // Add schain object if available
      if (bid && bid.schain) {
        requestPayload['source'] = {
          ext: {
            schain: bid.schain
          }
        };
      }

      // Only add the user object if it's not empty
      if (!isEmpty(userExt)) {
        requestPayload.user = {
          ext: userExt
        };
      }

      // Add targeting
      if (getBidIdParameter('data', bid.params.user)) {
        var targetingarr = [];
        for (var i = 0; i < bid.params.user.data.length; i++) {
          var isemq = (bid.params.user.data[i].name) || 'empty';
          if (isemq !== 'empty') {
            var provider = bid.params.user.data[i].name;
            var targetingstring = (bid.params.user.data[i].segment[0].value) || 'empty';
            targetingarr.push({
              id: provider,
              name: provider,
              segment: {
                name: provider,
                value: targetingstring,
              }
            });
          }
        }

        requestPayload.user = {
          ext: userExt,
          data: targetingarr
        };
      }

      return {
        method: 'POST',
        url: URL,
        data: requestPayload,
        bidRequest: bidderRequest,
        options: {
          contentType: 'application/json',
          customHeaders: {
            'x-openrtb-version': '2.5'
          }
        }
      };
    });

    return smartxRequests;
  },
  /**
   * 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, bidderRequest) {
    const bidResponses = [];
    const serverResponseBody = serverResponse.body;
    if (serverResponseBody && isArray(serverResponseBody.seatbid)) {
      _each(serverResponseBody.seatbid, function (bids) {
        _each(bids.bid, function (smartxBid) {
          let currentBidRequest = {};
          for (let i in bidderRequest.bidRequest.bids) {
            if (smartxBid.impid == bidderRequest.bidRequest.bids[i].bidId) {
              currentBidRequest = bidderRequest.bidRequest.bids[i];
            }
          }
          /**
           * Make sure currency and price are the right ones
           */
          _each(currentBidRequest.params.pre_market_bids, function (pmb) {
            if (pmb.deal_id == smartxBid.id) {
              smartxBid.price = pmb.price;
              serverResponseBody.cur = pmb.currency;
            }
          });

          const bid = {
            requestId: currentBidRequest.bidId,
            currency: serverResponseBody.cur || 'USD',
            cpm: smartxBid.price,
            creativeId: smartxBid.crid || '',
            ttl: 360,
            netRevenue: true,
            vastContent: smartxBid.adm,
            vastXml: smartxBid.adm,
            mediaType: VIDEO,
            width: smartxBid.w,
            height: smartxBid.h
          };

          bid.meta = bid.meta || {};
          if (smartxBid && smartxBid.adomain && smartxBid.adomain.length > 0) {
            bid.meta.advertiserDomains = smartxBid.adomain;
          }

          const context = deepAccess(currentBidRequest, 'mediaTypes.video.context');

          if (context === 'outstream') {
            const playersize = deepAccess(currentBidRequest, 'mediaTypes.video.playerSize');
            const renderer = Renderer.install({
              id: 0,
              url: 'https://dco.smartclip.net/?plc=7777779',
              config: {
                adText: 'SmartX Outstream Video Ad via Prebid.js',
                player_width: playersize[0][0],
                player_height: playersize[0][1],
                content_page_url: deepAccess(bidderRequest, 'data.site.page'),
                ad_mute: +!!deepAccess(currentBidRequest, 'params.ad_mute'),
                hide_skin: +!!deepAccess(currentBidRequest, 'params.hide_skin'),
                outstream_options: deepAccess(currentBidRequest, 'params.outstream_options')
              }
            });
            try {
              renderer.setRender(createOutstreamConfig);
              renderer.setEventHandlers({
                impression: function impression() {
                  return logMessage('SmartX outstream video impression event');
                },
                loaded: function loaded() {
                  return logMessage('SmartX outstream video loaded event');
                },
                ended: function ended() {
                  return logMessage('SmartX outstream renderer video event');
                }
              });
            } catch (err) {
              logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err);
            }
            bid.renderer = renderer;
          }
          bidResponses.push(bid);
        })
      });
    }
    return bidResponses;
  }
}

function createOutstreamConfig(bid) {
  let confMinAdWidth = getBidIdParameter('minAdWidth', bid.renderer.config.outstream_options) || 290;
  let confMaxAdWidth = getBidIdParameter('maxAdWidth', bid.renderer.config.outstream_options) || 900;
  let confStartOpen = getBidIdParameter('startOpen', bid.renderer.config.outstream_options)
  let confEndingScreen = getBidIdParameter('endingScreen', bid.renderer.config.outstream_options)
  let confTitle = getBidIdParameter('title', bid.renderer.config.outstream_options);
  let confSkipOffset = getBidIdParameter('skipOffset', bid.renderer.config.outstream_options);
  let confDesiredBitrate = getBidIdParameter('desiredBitrate', bid.renderer.config.outstream_options);
  let confVisibilityThreshold = getBidIdParameter('visibilityThreshold', bid.renderer.config.outstream_options);
  let elementId = getBidIdParameter('slot', bid.renderer.config.outstream_options) || bid.adUnitCode;

  logMessage('[SMARTX][renderer] Handle SmartX outstream renderer');

  var playerConfig = {
    minAdWidth: confMinAdWidth,
    maxAdWidth: confMaxAdWidth,
    coreSetup: {},
    layoutSettings: {},
    onCappedCallback: function() {
      try {
        window.sc_smartIntxtNoad();
      } catch (f) {}
    },
  };

  if (confStartOpen == 'true') {
    playerConfig.startOpen = true;
  } else if (confStartOpen == 'false') {
    playerConfig.startOpen = false;
  }

  if (confEndingScreen == 'true') {
    playerConfig.endingScreen = true;
  } else if (confEndingScreen == 'false') {
    playerConfig.endingScreen = false;
  }

  if (confTitle || (typeof bid.renderer.config.outstream_options.title == 'string' && bid.renderer.config.outstream_options.title == '')) {
    playerConfig.layoutSettings.advertisingLabel = confTitle;
  }

  if (confSkipOffset) {
    playerConfig.coreSetup.skipOffset = confSkipOffset;
  }

  if (confDesiredBitrate) {
    playerConfig.coreSetup.desiredBitrate = confDesiredBitrate;
  }

  if (confVisibilityThreshold) {
    playerConfig.visibilityThreshold = confVisibilityThreshold;
  }

  playerConfig.adResponse = bid.vastContent;

  const divID = '[id="' + elementId + '"]';

  var playerListener = function callback(event) {
    switch (event) {
      case 'AdSlotStarted':
        try {
          window.sc_smartIntxtStart();
        } catch (f) {}
        break;

      case 'AdSlotComplete':
        try {
          window.sc_smartIntxtEnd();
        } catch (f) {}
        break;
    }
  };

  try {
    // eslint-disable-next-line
    outstreamplayer.connect(divID).setup(playerConfig, playerListener)
  } catch (e) {
    logError('[SMARTX][renderer] Error caught: ' + e);
  }
  return playerConfig;
}

/**
 * Get the floor price from bid.params for backward compatibility.
 * If not found, then check floor module.
 * @param bid A valid bid object
 * @returns {*|number} floor price
 */
function getBidFloor(bid) {
  let floor = getBidIdParameter('bidfloor', bid.params);
  let floorcur = getBidIdParameter('bidfloorcur', bid.params) || 'EUR';

  if (!floor && isFn(bid.getFloor)) {
    const floorObj = bid.getFloor({
      currency: floorcur,
      mediaType: '*',
      size: '*'
    });

    if (isPlainObject(floorObj) && !isNaN(floorObj.floor) && floorObj.currency === floorcur) {
      floor = floorObj.floor;
    }
  }

  return floor;
}

registerBidder(spec);