prebid/Prebid.js

View on GitHub
modules/discoveryBidAdapter.js

Summary

Maintainability
F
1 wk
Test Coverage
import * as utils from '../src/utils.js';
import { getStorageManager } from '../src/storageManager.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, NATIVE } from '../src/mediaTypes.js';

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

const BIDDER_CODE = 'discovery';
const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn=';
const TIME_TO_LIVE = 500;
export const storage = getStorageManager({bidderCode: BIDDER_CODE});
let globals = {};
let itemMaps = {};
const MEDIATYPE = [BANNER, NATIVE];

/* ----- _ss_pp_id:start ------ */
const COOKIE_KEY_SSPPID = '_ss_pp_id';
export const COOKIE_KEY_MGUID = '__mguid_';
const COOKIE_KEY_PMGUID = '__pmguid_';
const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year
const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html';
export const THIRD_PARTY_COOKIE_ORIGIN = 'https://asset.popin.cc';

const UTM_KEY = '_ss_pp_utm';
let UTMValue = {};

const NATIVERET = {
  id: 'id',
  bidfloor: 0,
  // TODO Dynamic parameters
  native: {
    ver: '1.2',
    plcmtcnt: 1,
    assets: [
      {
        id: 1,
        required: 1,
        img: {
          type: 3,
          w: 300,
          wmin: 300,
          h: 174,
          hmin: 174,
        },
      },
      {
        id: 2,
        required: 1,
        title: {
          len: 75,
        },
      },
    ],
    plcmttype: 1,
    privacy: 1,
    eventtrackers: [
      {
        event: 1,
        methods: [1, 2],
      },
    ],
  },
  ext: {},
};

/**
 * get page title111
 * @returns {string}
 */

export function getPageTitle(win = window) {
  try {
    const ogTitle = win.top.document.querySelector('meta[property="og:title"]')
    return win.top.document.title || (ogTitle && ogTitle.content) || '';
  } catch (e) {
    const ogTitle = document.querySelector('meta[property="og:title"]')
    return document.title || (ogTitle && ogTitle.content) || '';
  }
}

/**
 * get page description
 * @returns {string}
 */
export function getPageDescription(win = window) {
  let element;

  try {
    element = win.top.document.querySelector('meta[name="description"]') ||
      win.top.document.querySelector('meta[property="og:description"]')
  } catch (e) {
    element = document.querySelector('meta[name="description"]') ||
      document.querySelector('meta[property="og:description"]')
  }

  return (element && element.content) || '';
}

/**
 * get page keywords
 * @returns {string}
 */
export function getPageKeywords(win = window) {
  let element;

  try {
    element = win.top.document.querySelector('meta[name="keywords"]');
  } catch (e) {
    element = document.querySelector('meta[name="keywords"]');
  }

  return (element && element.content) || '';
}

/**
 * get connection downlink
 * @returns {number}
 */
export function getConnectionDownLink(win = window) {
  const nav = win.navigator || {};
  return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined;
}

/**
 * get pmg uid
 * 获取并生成用户的id
 * @return {string}
 */
export const getPmgUID = () => {
  if (!storage.cookiesAreEnabled()) return;

  let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID);
  if (!pmgUid) {
    pmgUid = utils.generateUUID();
  }
  // Extend the expiration time of pmguid
  try {
    storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString());
  } catch (e) {}
  return pmgUid;
};

/* ----- _ss_pp_id:end ------ */

/**
 * get object key -> value
 * @param  {Object}    obj  对象
 * @param  {...string} keys 键名
 * @return {any}
 */
function getKv(obj, ...keys) {
  let o = obj;

  for (let key of keys) {
    if (o && o[key]) {
      o = o[key];
    } else {
      return '';
    }
  }
  return o;
}

/**
 * get device
 * @return {boolean}
 */
function getDevice() {
  let check = false;
  (function (a) {
    let reg1 = new RegExp(
      [
        '(android|bbd+|meego)',
        '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)',
        '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone',
        '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap',
        '|windows ce|xda|xiino|android|ipad|playbook|silk',
      ].join(''),
      'i'
    );
    let reg2 = new RegExp(
      [
        '1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)',
        '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )',
        '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell',
        '|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)',
        '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene',
        '|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c',
        '|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom',
        '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)',
        '|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)',
        '|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]',
        '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)',
        '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio',
        '|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms',
        '|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al',
        '|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)',
        '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|',
        'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)',
        '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-',
        '|your|zeto|zte-',
      ].join(''),
      'i'
    );
    if (reg1.test(a) || reg2.test(a.substr(0, 4))) {
      check = true;
    }
  })(navigator.userAgent || navigator.vendor || window.opera);
  return check;
}

/**
 * get BidFloor
 * @param {*} bid
 * @param {*} mediaType
 * @param {*} sizes
 * @returns
 */
function getBidFloor(bid) {
  if (!utils.isFn(bid.getFloor)) {
    return utils.deepAccess(bid, 'params.bidfloor', 0);
  }

  try {
    const bidFloor = bid.getFloor({
      currency: 'USD',
      mediaType: '*',
      size: '*',
    });
    return bidFloor.floor;
  } catch (_) {
    return 0;
  }
}

/**
 * get sizes for rtb
 * @param  {Array|Object} requestSizes
 * @return {Object}
 */
function transformSizes(requestSizes) {
  let sizes = [];
  let sizeObj = {};

  if (
    utils.isArray(requestSizes) &&
    requestSizes.length === 2 &&
    !utils.isArray(requestSizes[0])
  ) {
    sizeObj.width = parseInt(requestSizes[0], 10);
    sizeObj.height = parseInt(requestSizes[1], 10);
    sizes.push(sizeObj);
  } else if (typeof requestSizes === 'object') {
    for (let i = 0; i < requestSizes.length; i++) {
      let size = requestSizes[i];
      sizeObj = {};
      sizeObj.width = parseInt(size[0], 10);
      sizeObj.height = parseInt(size[1], 10);
      sizes.push(sizeObj);
    }
  }

  return sizes;
}

// Support sizes
const popInAdSize = [
  { w: 300, h: 250 },
  { w: 300, h: 600 },
  { w: 728, h: 90 },
  { w: 970, h: 250 },
  { w: 320, h: 50 },
  { w: 160, h: 600 },
  { w: 320, h: 180 },
  { w: 320, h: 100 },
  { w: 336, h: 280 },
];

/**
 * get screen size
 *
 * @returns {Array} eg: "['widthxheight']"
 */
function getScreenSize() {
  return utils.parseSizesInput([window.screen.width, window.screen.height]);
}

/**
 * @param {BidRequest} bidRequest
 * @param bidderRequest
 * @returns {string}
 */
function getReferrer(bidRequest = {}, bidderRequest = {}) {
  let pageUrl;
  if (bidRequest.params && bidRequest.params.referrer) {
    pageUrl = bidRequest.params.referrer;
  } else {
    pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page');
  }
  return pageUrl;
}

/**
 * get current time to UTC string
 * @returns utc string
 */
export function getCurrentTimeToUTCString() {
  const date = new Date();
  date.setTime(date.getTime() + COOKIE_RETENTION_TIME);
  return date.toUTCString();
}

/**
 * format imp ad test ext params
 *
 * @param validBidRequest sigleBidRequest
 * @param bidderRequest
 */
function addImpExtParams(bidRequest = {}, bidderRequest = {}) {
  const { deepAccess } = utils;
  const { params = {}, adUnitCode, bidId } = bidRequest;
  const ext = {
    bidId: bidId || '',
    adUnitCode: adUnitCode || '',
    token: params.token || '',
    siteId: params.siteId || '',
    zoneId: params.zoneId || '',
    publisher: params.publisher || '',
    p_pos: params.position || '',
    screenSize: getScreenSize(),
    referrer: getReferrer(bidRequest, bidderRequest),
    stack: deepAccess(bidRequest, 'refererInfo.stack', []),
    b_pos: deepAccess(bidRequest, 'mediaTypes.banner.pos', '', ''),
    ortbUser: deepAccess(bidRequest, 'ortb2.user', {}, {}),
    ortbSite: deepAccess(bidRequest, 'ortb2.site', {}, {}),
    tid: deepAccess(bidRequest, 'ortb2Imp.ext.tid', '', ''),
    browsiViewability: deepAccess(bidRequest, 'ortb2Imp.ext.data.browsi.browsiViewability', '', ''),
    adserverName: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name', '', ''),
    adslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot', '', ''),
    keywords: deepAccess(bidRequest, 'ortb2Imp.ext.data.keywords', '', ''),
    gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid', '', ''),
    pbadslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot', '', ''),
  };
  return ext;
}

/**
 * get aditem setting
 * @param {Array}  validBidRequests an an array of bids
 * @param {Object} bidderRequest  The master bidRequest object
 * @return {Object}
 */
function getItems(validBidRequests, bidderRequest) {
  let items = [];
  items = validBidRequests.map((req, i) => {
    let ret = {};
    // eslint-disable-next-line no-debugger
    let mediaTypes = getKv(req, 'mediaTypes');

    const bidFloor = getBidFloor(req);
    let id = '' + (i + 1);

    if (mediaTypes.native) {
      ret = { ...NATIVERET, ...{ id, bidFloor } };
    }
    // banner
    if (mediaTypes.banner) {
      let sizes = transformSizes(getKv(req, 'sizes'));
      let matchSize;

      for (let size of sizes) {
        matchSize = popInAdSize.find(
          (item) => size.width === item.w && size.height === item.h
        );
        if (matchSize) {
          break;
        }
      }
      if (!matchSize) {
        matchSize = sizes[0]
          ? { h: sizes[0].height || 0, w: sizes[0].width || 0 }
          : { h: 0, w: 0 };
      }
      ret = {
        id: id,
        bidfloor: bidFloor,
        banner: {
          h: matchSize.h,
          w: matchSize.w,
          pos: 1,
          format: sizes,
        },
        ext: {},
        tagid: req.params && req.params.tagid
      };
    }

    try {
      ret.ext = addImpExtParams(req, bidderRequest);
    } catch (e) {}

    itemMaps[id] = {
      req,
      ret,
    };
    return ret;
  });
  return items;
}

export const buildUTMTagData = (url) => {
  if (!storage.cookiesAreEnabled()) return;
  const urlParams = utils.parseUrl(url).search || {};
  const UTMParams = {};
  Object.keys(urlParams).forEach(key => {
    if (/^utm_/.test(key)) {
      UTMParams[key] = urlParams[key];
    }
  });
  UTMValue = JSON.parse(storage.getCookie(UTM_KEY) || '{}');
  Object.assign(UTMValue, UTMParams);
  storage.setCookie(UTM_KEY, JSON.stringify(UTMValue), getCurrentTimeToUTCString());
}

/**
 * get rtb qequest params
 *
 * @param {Array}  validBidRequests an an array of bids
 * @param {Object} bidderRequest  The master bidRequest object
 * @return {Object}
 */
function getParam(validBidRequests, bidderRequest) {
  const pubcid = utils.deepAccess(validBidRequests[0], 'crumbs.pubcid');
  const sharedid =
    utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') ||
    utils.deepAccess(validBidRequests[0], 'userId.pubcid');
  const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId;

  let isMobile = getDevice() ? 1 : 0;
  // input test status by Publisher. more frequently for test true req
  let isTest = validBidRequests[0].params.test || 0;
  let auctionId = getKv(bidderRequest, 'auctionId');
  let items = getItems(validBidRequests, bidderRequest);

  const timeout = bidderRequest.timeout || 2000;

  const domain =
    utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain;
  const location = utils.deepAccess(bidderRequest, 'refererInfo.referer');
  const page = utils.deepAccess(bidderRequest, 'refererInfo.page');
  const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref');
  const firstPartyData = bidderRequest.ortb2;
  const tpData = utils.deepAccess(bidderRequest, 'ortb2.user.data') || undefined;
  const topWindow = window.top;
  const title = getPageTitle();
  const desc = getPageDescription();
  const keywords = getPageKeywords();

  try {
    buildUTMTagData(page);
  } catch (error) { }

  if (items && items.length) {
    let c = {
      // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781
      id: 'pp_hbjs_' + auctionId,
      test: +isTest,
      at: 1,
      bcat: globals['bcat'],
      badv: globals['adv'],
      cur: ['USD'],
      device: {
        connectiontype: 0,
        js: 1,
        os: navigator.platform || '',
        ua: navigator.userAgent,
        language: /en/.test(navigator.language) ? 'en' : navigator.language,
      },
      ext: {
        eids,
        firstPartyData,
        ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined,
        pmguid: getPmgUID(),
        tpData,
        utm: storage.getCookie(UTM_KEY),
        page: {
          title: title ? title.slice(0, 100) : undefined,
          desc: desc ? desc.slice(0, 300) : undefined,
          keywords: keywords ? keywords.slice(0, 100) : undefined,
          hLen: topWindow.history?.length || undefined,
        },
        device: {
          nbw: getConnectionDownLink(),
          hc: topWindow.navigator?.hardwareConcurrency || undefined,
          dm: topWindow.navigator?.deviceMemory || undefined,
        }
      },
      user: {
        buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined,
        id: sharedid || pubcid,
      },
      tmax: timeout,
      site: {
        name: domain,
        domain: domain,
        page: page || location,
        ref: referer,
        mobile: isMobile,
        cat: [], // todo
        publisher: {
          id: globals['publisher'],
          // todo
          // name: xxx
        },
      },
      imp: items,
    };
    return c;
  } else {
    return null;
  }
}

export const spec = {
  code: BIDDER_CODE,
  supportedMediaTypes: MEDIATYPE,
  // aliases: ['ex'], // short code
  /**
   * Determines whether or not the given bid request is valid.
   *
   * @param {BidRequest} bid The bid params to validate.
   * @return boolean True if this is a valid bid, and false otherwise.
   */
  isBidRequestValid: function (bid) {
    if (bid.params.token) {
      globals['token'] = bid.params.token;
    }
    if (bid.params.publisher) {
      globals['publisher'] = bid.params.publisher;
    }
    if (bid.params.tagid) {
      globals['tagid'] = bid.params.tagid;
    }
    if (bid.params.bcat) {
      globals['bcat'] = Array.isArray(bid.params.bcat) ? bid.params.bcat : [];
    }
    if (bid.params.badv) {
      globals['badv'] = Array.isArray(bid.params.badv) ? bid.params.badv : [];
    }
    return true;
  },

  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {Array}  validBidRequests an an array of bids
   * @param {Object} bidderRequest  The master bidRequest object
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function (validBidRequests, bidderRequest) {
    if (!globals['token']) return;

    let payload = getParam(validBidRequests, bidderRequest);

    const payloadString = JSON.stringify(payload);
    return {
      method: 'POST',
      url: ENDPOINT_URL + globals['token'],
      data: payloadString,
    };
  },

  /**
   * Unpack the response from the server into a list of bids.
   * @param {ServerResponse} 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 = getKv(serverResponse, 'body', 'seatbid', 0, 'bid');
    const cur = getKv(serverResponse, 'body', 'cur');
    const bidResponses = [];
    for (let bid of bids) {
      let impid = getKv(bid, 'impid');
      if (itemMaps[impid]) {
        let bidId = getKv(itemMaps[impid], 'req', 'bidId');
        const mediaType = getKv(bid, 'w') ? 'banner' : 'native';
        let bidResponse = {
          requestId: bidId,
          cpm: getKv(bid, 'price'),
          creativeId: getKv(bid, 'cid'),
          mediaType,
          currency: cur,
          netRevenue: true,
          nurl: getKv(bid, 'nurl'),
          ttl: TIME_TO_LIVE,
          meta: {
            advertiserDomains: getKv(bid, 'adomain') || [],
          },
        };
        if (mediaType === 'native') {
          const adm = getKv(bid, 'adm');
          const admObj = JSON.parse(adm);
          var native = {};
          admObj.assets.forEach((asset) => {
            if (asset.title) {
              native.title = asset.title.text;
            } else if (asset.data) {
              native.data = asset.data.value;
            } else if (asset.img) {
              switch (asset.img.type) {
                case 1:
                  native.icon = {
                    url: asset.img.url,
                    width: asset.img.w,
                    height: asset.img.h,
                  };
                  break;
                default:
                  native.image = {
                    url: asset.img.url,
                    width: asset.img.w,
                    height: asset.img.h,
                  };
                  break;
              }
            }
          });
          if (admObj.link) {
            if (admObj.link.url) {
              native.clickUrl = admObj.link.url;
            }
          }
          if (Array.isArray(admObj.eventtrackers)) {
            native.impressionTrackers = [];
            admObj.eventtrackers.forEach((tracker) => {
              if (tracker.event !== 1) {
                return;
              }
              switch (tracker.method) {
                case 1:
                  native.impressionTrackers.push(tracker.url);
                  break;
                // case 2:
                //   native.javascriptTrackers = `<script src=\'${tracker.url}\'></script>`;
                //   break;
              }
            });
          }
          if (admObj.purl) {
            native.purl = admObj.purl;
          }
          bidResponse['native'] = native;
        } else {
          bidResponse['width'] = getKv(bid, 'w');
          bidResponse['height'] = getKv(bid, 'h');
          bidResponse['ad'] = getKv(bid, 'adm');
        }
        bidResponses.push(bidResponse);
      }
    }

    return bidResponses;
  },

  getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) {
    const origin = encodeURIComponent(location.origin || `https://${location.host}`);
    let syncParamUrl = `dm=${origin}`;

    if (gdprConsent && gdprConsent.consentString) {
      if (typeof gdprConsent.gdprApplies === 'boolean') {
        syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
      } else {
        syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`;
      }
    }
    if (uspConsent && uspConsent.consentString) {
      syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`;
    }

    if (syncOptions.iframeEnabled) {
      window.addEventListener('message', function handler(event) {
        if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) {
          return;
        }

        this.removeEventListener('message', handler);

        event.stopImmediatePropagation();

        const response = event.data;
        if (!response.optout && response.mguid) {
          storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString());
        }
      }, true);
      return [
        {
          type: 'iframe',
          url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}`
        }
      ];
    }
  },

  /**
   * Register bidder specific code, which will execute if bidder timed out after an auction
   * @param {data} Containing timeout specific data
   */
  onTimeout: function (data) {
    utils.logError('DiscoveryDSP adapter timed out for the auction.');
    // TODO send request timeout to serve, the interface is not ready
  },

  /**
   * Register bidder specific code, which  will execute if a bid from this bidder won the auction
   * @param {Bid} The bid that won the auction
   */
  onBidWon: function (bid) {
    if (bid['nurl']) {
      utils.triggerPixel(bid['nurl']);
    }
  },
};
registerBidder(spec);