prebid/Prebid.js

View on GitHub
modules/adyoulikeBidAdapter.js

Summary

Maintainability
F
5 days
Test Coverage
import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import { config } from '../src/config.js';
import {find} from '../src/polyfill.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';

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

const VERSION = '1.0';
const BIDDER_CODE = 'adyoulike';
const DEFAULT_DC = 'hb-api';
const CURRENCY = 'USD';
const GVLID = 259;

const NATIVE_IMAGE = {
  image: {
    required: true
  },
  title: {
    required: true
  },
  sponsoredBy: {
    required: true
  },
  clickUrl: {
    required: true
  },
  body: {
    required: false
  },
  icon: {
    required: false
  },
  cta: {
    required: false
  }
};

export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  supportedMediaTypes: [BANNER, NATIVE, VIDEO],
  aliases: ['ayl'], // 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) {
    const sizes = getSize(getSizeArray(bid));
    const sizeValid = sizes.width > 0 && sizes.height > 0;

    // allows no size for native only
    return (bid.params && bid.params.placement &&
            (sizeValid || (bid.mediaTypes && bid.mediaTypes.native)));
  },
  /**
   * Make a server request from the list of BidRequests.
   *
   * @param {BidRequest} bidRequests is an array of AdUnits and bids
   * @param {BidderRequest} bidderRequest
   * @return ServerRequest Info describing the request to the server.
   */
  buildRequests: function (bidRequests, bidderRequest) {
    // convert Native ORTB definition to old-style prebid native definition
    bidRequests = convertOrtbRequestToProprietaryNative(bidRequests);
    let hasVideo = false;
    let eids;
    const payload = {
      Version: VERSION,
      Bids: bidRequests.reduce((accumulator, bidReq) => {
        let mediatype = getMediatype(bidReq);
        let sizesArray = getSizeArray(bidReq);
        let size = getSize(sizesArray);
        accumulator[bidReq.bidId] = {};
        accumulator[bidReq.bidId].PlacementID = bidReq.params.placement;
        accumulator[bidReq.bidId].TransactionID = bidReq.ortb2Imp?.ext?.tid;
        accumulator[bidReq.bidId].Width = size.width;
        accumulator[bidReq.bidId].Height = size.height;
        accumulator[bidReq.bidId].AvailableSizes = sizesArray.join(',');
        if (typeof bidReq.getFloor === 'function') {
          accumulator[bidReq.bidId].Pricing = getFloor(bidReq, size, mediatype);
        }
        if (bidReq.schain) {
          accumulator[bidReq.bidId].SChain = bidReq.schain;
        }
        if (!eids && bidReq.userIdAsEids && bidReq.userIdAsEids.length) {
          eids = bidReq.userIdAsEids;
        }
        if (mediatype === NATIVE) {
          let nativeReq = bidReq.mediaTypes.native;
          if (nativeReq.type === 'image') {
            nativeReq = Object.assign({}, NATIVE_IMAGE, nativeReq);
          }
          // click url is always mandatory even if not specified by publisher
          nativeReq.clickUrl = {
            required: true
          };
          accumulator[bidReq.bidId].Native = nativeReq;
        }
        if (mediatype === VIDEO) {
          hasVideo = true;
          accumulator[bidReq.bidId].Video = bidReq.mediaTypes.video;

          const size = bidReq.mediaTypes.video.playerSize;
          if (Array.isArray(size) && !Array.isArray(size[0])) {
            accumulator[bidReq.bidId].Video.playerSize = [size];
          }
        }
        return accumulator;
      }, {}),
      PageRefreshed: getPageRefreshed()
    };

    if (bidderRequest.gdprConsent) {
      payload.gdprConsent = {
        consentString: bidderRequest.gdprConsent.consentString,
        consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : null
      };
    }

    if (bidderRequest.uspConsent) {
      payload.uspConsent = bidderRequest.uspConsent;
    }

    if (bidderRequest.ortb2) {
      payload.ortb2 = bidderRequest.ortb2;
    }
    if (eids) {
      payload.eids = eids;
    }

    payload.pbjs_version = '$prebid.version$';

    const data = JSON.stringify(payload);
    const options = {
      withCredentials: true
    };

    return {
      method: 'POST',
      url: createEndpoint(bidRequests, bidderRequest, hasVideo),
      data,
      options
    };
  },
  /**
   * 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, request) {
    const bidResponses = [];
    var bidRequests = {};

    if (!serverResponse || !serverResponse.body) {
      return bidResponses;
    }

    try {
      bidRequests = JSON.parse(request.data).Bids;
    } catch (err) {
      // json error initial request can't be read
    }

    // For this adapter, serverResponse is a list
    serverResponse.body.forEach(response => {
      const bid = createBid(response, bidRequests);
      if (bid) {
        bidResponses.push(bid);
      }
    });
    return bidResponses;
  },

  /**
   * List user sync endpoints.
   * Legal information have to be added to the request.
   * Only iframe syncs are supported.
   *
   * @param {*} syncOptions Publisher prebid configuration.
   * @param {*} serverResponses A successful response from the server.
   * @return {syncs[]} An array of syncs that should be executed.
   */
  getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
    if (!syncOptions.iframeEnabled) {
      return [];
    }

    let params = '';

    // GDPR
    if (gdprConsent) {
      params += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0);
      params += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '');
    }

    // coppa compliance
    if (config.getConfig('coppa') === true) {
      params += '&coppa=1';
    }

    // CCPA
    if (uspConsent) {
      params += '&us_privacy=' + encodeURIComponent(uspConsent);
    }

    // GPP
    if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
      params += '&gpp=' + encodeURIComponent(gppConsent.gppString);
      params += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(','));
    }

    return [{
      type: 'iframe',
      url: `https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4${params}`
    }];
  }
}

/* Get hostname from bids */
function getHostname(bidderRequest) {
  let dcHostname = find(bidderRequest, bid => bid.params.DC);
  if (dcHostname) {
    return ('-' + dcHostname.params.DC);
  }
  return '';
}

/* Get mediatype from bidRequest */
function getMediatype(bidRequest) {
  if (deepAccess(bidRequest, 'mediaTypes.banner')) {
    return BANNER;
  }
  if (deepAccess(bidRequest, 'mediaTypes.video')) {
    return VIDEO;
  }
  if (deepAccess(bidRequest, 'mediaTypes.native')) {
    return NATIVE;
  }
}

/* Get Floor price information */
function getFloor(bidRequest, size, mediaType) {
  const bidFloors = bidRequest.getFloor({
    currency: CURRENCY,
    mediaType,
    size: [ size.width, size.height ]
  });

  if (!isNaN(bidFloors.floor) && (bidFloors.currency === CURRENCY)) {
    return bidFloors.floor;
  }
}

/* Get information on page refresh */
function getPageRefreshed() {
  try {
    if (performance && performance.navigation) {
      return performance.navigation.type === performance.navigation.TYPE_RELOAD;
    }
  } catch (e) { }
  return false;
}

/* Create endpoint url */
function createEndpoint(bidRequests, bidderRequest, hasVideo) {
  let host = getHostname(bidRequests);
  const endpoint = hasVideo ? '/hb-api/prebid-video/v1' : '/hb-api/prebid/v1';
  return buildUrl({
    protocol: 'https',
    host: `${DEFAULT_DC}${host}.omnitagjs.com`,
    pathname: endpoint,
    search: createEndpointQS(bidderRequest)
  });
}

/* Create endpoint query string */
function createEndpointQS(bidderRequest) {
  const qs = {};
  if (bidderRequest) {
    const ref = bidderRequest.refererInfo;
    if (ref) {
      if (ref.location) {
        // RefererUrl will be removed in a future version.
        qs.RefererUrl = encodeURIComponent(ref.location);
        if (!ref.reachedTop) {
          qs.SafeFrame = true;
        }
      }

      qs.PageUrl = encodeURIComponent(ref.topmostLocation);
      qs.PageReferrer = encodeURIComponent(ref.location);
    }

    // retreive info from ortb2 object if present (prebid7)
    const siteInfo = bidderRequest.ortb2?.site;
    if (siteInfo) {
      qs.PageUrl = encodeURIComponent(siteInfo.page || ref?.topmostLocation);
      qs.PageReferrer = encodeURIComponent(siteInfo.ref || ref?.location);
    }
  }

  const can = bidderRequest?.refererInfo?.canonicalUrl;
  if (can) {
    qs.CanonicalUrl = encodeURIComponent(can);
  }

  return qs;
}

function getSizeArray(bid) {
  let inputSize = bid.sizes || [];

  if (bid.mediaTypes && bid.mediaTypes.banner) {
    inputSize = bid.mediaTypes.banner.sizes || [];
  }

  // handle size in bid.params in formats: [w, h] and [[w,h]].
  if (bid.params && Array.isArray(bid.params.size)) {
    inputSize = bid.params.size;
    if (!Array.isArray(inputSize[0])) {
      inputSize = [inputSize]
    }
  }

  return parseSizesInput(inputSize);
}

/* Get parsed size from request size */
function getSize(sizesArray) {
  const parsed = {};
  // the main requested size is the first one
  const size = sizesArray[0];

  if (typeof size !== 'string') {
    return parsed;
  }

  const parsedSize = size.toUpperCase().split('X');
  const width = parseInt(parsedSize[0], 10);
  if (width) {
    parsed.width = width;
  }

  const height = parseInt(parsedSize[1], 10);
  if (height) {
    parsed.height = height;
  }

  return parsed;
}

function getInternalImgUrl(uid) {
  if (!uid) return '';
  return 'https://blobs.omnitagjs.com/blobs/' + uid.substr(16, 2) + '/' + uid.substr(16) + '/' + uid;
}

function getImageUrl(config, resource, width, height) {
  let url = '';
  if (resource && resource.Kind) {
    switch (resource.Kind) {
      case 'INTERNAL':
        url = getInternalImgUrl(resource.Data.Internal.BlobReference.Uid);

        break;

      case 'EXTERNAL':
        const dynPrefix = config.DynamicPrefix;
        let extUrl = resource.Data.External.Url;
        extUrl = extUrl.replace(/\[height\]/i, '' + height);
        extUrl = extUrl.replace(/\[width\]/i, '' + width);

        if (extUrl.indexOf(dynPrefix) >= 0) {
          const urlmatch = (/.*url=([^&]*)/gm).exec(extUrl);
          url = urlmatch ? urlmatch[1] : '';
          if (!url) {
            url = getInternalImgUrl((/.*key=([^&]*)/gm).exec(extUrl)[1]);
          }
        } else {
          url = extUrl;
        }

        break;
    }
  }

  return url;
}

function getTrackers(eventsArray, jsTrackers) {
  const result = [];

  if (!eventsArray) return result;

  eventsArray.map((item, index) => {
    if ((jsTrackers && item.Kind === 'JAVASCRIPT_URL') ||
        (!jsTrackers && item.Kind === 'PIXEL_URL')) {
      result.push(item.Url);
    }
  });
  return result;
}

function getNativeAssets(response, nativeConfig) {
  if (typeof response.Native === 'object') {
    return response.Native;
  }
  const native = {};

  var adJson = {};
  var textsJson = {};
  if (typeof response.Ad === 'string') {
    adJson = JSON.parse(response.Ad.match(/\/\*PREBID\*\/(.*)\/\*PREBID\*\//)[1]);
    textsJson = adJson.Content.Preview.Text;

    var impressionUrl = adJson.TrackingPrefix +
            '/pixel?event_kind=IMPRESSION&attempt=' + adJson.Attempt;
    var insertionUrl = adJson.TrackingPrefix +
            '/pixel?event_kind=INSERTION&attempt=' + adJson.Attempt;

    if (adJson.Campaign) {
      impressionUrl += '&campaign=' + adJson.Campaign;
      insertionUrl += '&campaign=' + adJson.Campaign;
    }

    native.clickUrl = adJson.TrackingPrefix + '/ar?event_kind=CLICK&attempt=' + adJson.Attempt +
      '&campaign=' + adJson.Campaign + '&url=' + encodeURIComponent(adJson.Content.Landing.Url);

    if (adJson.OnEvents) {
      native.clickTrackers = getTrackers(adJson.OnEvents['CLICK']);
      native.impressionTrackers = getTrackers(adJson.OnEvents['IMPRESSION']);
      native.javascriptTrackers = getTrackers(adJson.OnEvents['IMPRESSION'], true);
    } else {
      native.impressionTrackers = [];
    }

    native.impressionTrackers.push(impressionUrl, insertionUrl);
  }

  Object.keys(nativeConfig).map(function(key, index) {
    switch (key) {
      case 'title':
        native[key] = textsJson.TITLE;
        break;
      case 'body':
        native[key] = textsJson.DESCRIPTION;
        break;
      case 'cta':
        native[key] = textsJson.CALLTOACTION;
        break;
      case 'sponsoredBy':
        native[key] = adJson.Content.Preview.Sponsor.Name;
        break;
      case 'image':
        // main image requested size
        const imgSize = nativeConfig.image.sizes || [];
        if (!imgSize.length) {
          imgSize[0] = response.Width || 300;
          imgSize[1] = response.Height || 250;
        }

        const url = getImageUrl(adJson, deepAccess(adJson, 'Content.Preview.Thumbnail.Image'), imgSize[0], imgSize[1]);
        if (url) {
          native[key] = {
            url,
            width: imgSize[0],
            height: imgSize[1]
          };
        }

        break;
      case 'icon':
        // icon requested size
        const iconSize = nativeConfig.icon.sizes || [];
        if (!iconSize.length) {
          iconSize[0] = 50;
          iconSize[1] = 50;
        }

        const icurl = getImageUrl(adJson, deepAccess(adJson, 'Content.Preview.Sponsor.Logo.Resource'), iconSize[0], iconSize[1]);

        if (icurl) {
          native[key] = {
            url: icurl,
            width: iconSize[0],
            height: iconSize[1]
          };
        }
        break;
      case 'privacyIcon':
        native[key] = getImageUrl(adJson, deepAccess(adJson, 'Content.Preview.Credit.Logo.Resource'), 25, 25);
        break;
      case 'privacyLink':
        native[key] = deepAccess(adJson, 'Content.Preview.Credit.Url');
        break;
    }
  });

  return native;
}

/* Create bid from response */
function createBid(response, bidRequests) {
  if (!response || (!response.Ad && !response.Native && !response.Vast)) {
    return;
  }

  const request = bidRequests && bidRequests[response.BidID];

  // In case we don't retreive the size from the adserver, use the given one.
  if (request) {
    if (!response.Width || response.Width === '0') {
      response.Width = request.Width;
    }

    if (!response.Height || response.Height === '0') {
      response.Height = request.Height;
    }
  }

  const bid = {
    requestId: response.BidID,
    ttl: 3600,
    creativeId: response.CreativeID,
    cpm: response.Price,
    netRevenue: true,
    currency: CURRENCY,
    meta: response.Meta || { advertiserDomains: [] }
  };

  // retreive video response if present
  const vast64 = response.Vast;
  if (vast64) {
    bid.width = response.Width;
    bid.height = response.Height;
    bid.vastXml = window.atob(vast64);
    bid.mediaType = 'video';
  } else if (request.Native) {
    // format Native response if Native was requested
    bid.native = getNativeAssets(response, request.Native);
    bid.mediaType = 'native';
  } else {
    bid.width = response.Width;
    bid.height = response.Height;
    bid.ad = response.Ad;
  }

  return bid;
}

registerBidder(spec);