prebid/Prebid.js

View on GitHub
modules/openxBidAdapter.js

Summary

Maintainability
D
2 days
Test Coverage
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import * as utils from '../src/utils.js';
import {mergeDeep} from '../src/utils.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {ortbConverter} from '../libraries/ortbConverter/converter.js';
import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js';

const bidderConfig = 'hb_pb_ortb';
const bidderVersion = '2.0';
export const REQUEST_URL = 'https://rtb.openx.net/openrtbb/prebidjs';
export const SYNC_URL = 'https://u.openx.net/w/1.0/pd';
export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba';
export const spec = {
  code: 'openx',
  gvlid: 69,
  supportedMediaTypes: [BANNER, VIDEO],
  isBidRequestValid,
  buildRequests,
  interpretResponse,
  getUserSyncs,
  transformBidParams
};

registerBidder(spec);

const converter = ortbConverter({
  context: {
    netRevenue: true,
    ttl: 300
  },
  imp(buildImp, bidRequest, context) {
    const imp = buildImp(bidRequest, context);
    mergeDeep(imp, {
      tagid: bidRequest.params.unit,
      ext: {
        divid: bidRequest.adUnitCode
      }
    });
    if (bidRequest.params.customParams) {
      utils.deepSetValue(imp, 'ext.customParams', bidRequest.params.customParams);
    }
    if (bidRequest.params.customFloor && !imp.bidfloor) {
      imp.bidfloor = bidRequest.params.customFloor;
    }
    return imp;
  },
  request(buildRequest, imps, bidderRequest, context) {
    const req = buildRequest(imps, bidderRequest, context);
    mergeDeep(req, {
      at: 1,
      ext: {
        bc: `${bidderConfig}_${bidderVersion}`,
        pv: '$prebid.version$'
      }
    })
    const bid = context.bidRequests[0];
    if (bid.params.coppa) {
      utils.deepSetValue(req, 'regs.coppa', 1);
    }
    if (bid.params.doNotTrack) {
      utils.deepSetValue(req, 'device.dnt', 1);
    }
    if (bid.params.platform) {
      utils.deepSetValue(req, 'ext.platform', bid.params.platform);
    }
    if (bid.params.delDomain) {
      utils.deepSetValue(req, 'ext.delDomain', bid.params.delDomain);
    }
    if (bid.params.response_template_name) {
      utils.deepSetValue(req, 'ext.response_template_name', bid.params.response_template_name);
    }
    if (bid.params.test) {
      req.test = 1
    }
    return req;
  },
  bidResponse(buildBidResponse, bid, context) {
    const bidResponse = buildBidResponse(bid, context);
    if (bid.ext) {
      bidResponse.meta.networkId = bid.ext.dsp_id;
      bidResponse.meta.advertiserId = bid.ext.buyer_id;
      bidResponse.meta.brandId = bid.ext.brand_id;
    }
    const {ortbResponse} = context;
    if (ortbResponse.ext && ortbResponse.ext.paf) {
      bidResponse.meta.paf = Object.assign({}, ortbResponse.ext.paf);
      bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id');
    }
    return bidResponse;
  },
  response(buildResponse, bidResponses, ortbResponse, context) {
    // pass these from request to the responses for use in userSync
    const {ortbRequest} = context;
    if (ortbRequest.ext) {
      if (ortbRequest.ext.delDomain) {
        utils.deepSetValue(ortbResponse, 'ext.delDomain', ortbRequest.ext.delDomain);
      }
      if (ortbRequest.ext.platform) {
        utils.deepSetValue(ortbResponse, 'ext.platform', ortbRequest.ext.platform);
      }
    }
    const response = buildResponse(bidResponses, ortbResponse, context);
    // TODO: we may want to standardize this and move fledge logic to ortbConverter
    let fledgeAuctionConfigs = utils.deepAccess(ortbResponse, 'ext.fledge_auction_configs');
    if (fledgeAuctionConfigs) {
      fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => {
        return {
          bidId,
          config: mergeDeep(Object.assign({}, cfg), {
            auctionSignals: {
              ortb2Imp: context.impContext[bidId]?.imp,
            },
          }),
        }
      });
      return {
        bids: response.bids,
        fledgeAuctionConfigs,
      }
    } else {
      return response.bids
    }
  },
  overrides: {
    imp: {
      bidfloor(setBidFloor, imp, bidRequest, context) {
        // enforce floors should always be in USD
        // TODO: does it make sense that request.cur can be any currency, but request.imp[].bidfloorcur must be USD?
        const floor = {};
        setBidFloor(floor, bidRequest, {...context, currency: 'USD'});
        if (floor.bidfloorcur === 'USD') {
          Object.assign(imp, floor);
        }
      },
      video(orig, imp, bidRequest, context) {
        if (FEATURES.VIDEO) {
          // `orig` is the video imp processor, which looks at bidRequest.mediaTypes[VIDEO]
          // to populate imp.video
          // alter its input `bidRequest` to also pick up parameters from `bidRequest.params`
          let videoParams = bidRequest.mediaTypes[VIDEO];
          if (videoParams) {
            videoParams = Object.assign({}, videoParams, bidRequest.params.video);
            bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}}
          }
          orig(imp, bidRequest, context);
          if (imp.video && videoParams?.context === 'outstream') {
            imp.video.placement = imp.video.placement || 4;
          }
        }
      }
    }
  }
});

function transformBidParams(params, isOpenRtb) {
  return convertTypes({
    'unit': 'string',
    'customFloor': 'number'
  }, params);
}

function isBidRequestValid(bidRequest) {
  const hasDelDomainOrPlatform = bidRequest.params.delDomain ||
    bidRequest.params.platform;

  if (utils.deepAccess(bidRequest, 'mediaTypes.banner') &&
    hasDelDomainOrPlatform) {
    return !!bidRequest.params.unit ||
      utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0;
  }

  return !!(bidRequest.params.unit && hasDelDomainOrPlatform);
}

function buildRequests(bids, bidderRequest) {
  let videoBids = bids.filter(bid => isVideoBid(bid));
  let bannerBids = bids.filter(bid => isBannerBid(bid));
  let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : [];
  videoBids.forEach(bid => {
    requests.push(createRequest([bid], bidderRequest, VIDEO));
  });
  return requests;
}

function createRequest(bidRequests, bidderRequest, mediaType) {
  return {
    method: 'POST',
    url: config.getConfig('openxOrtbUrl') || REQUEST_URL,
    data: converter.toORTB({bidRequests, bidderRequest, context: {mediaType}})
  }
}

function isVideoBid(bid) {
  return utils.deepAccess(bid, 'mediaTypes.video');
}

function isBannerBid(bid) {
  return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid);
}

function interpretResponse(resp, req) {
  if (!resp.body) {
    resp.body = {nbr: 0};
  }
  return converter.fromORTB({request: req.data, response: resp.body});
}

/**
 * @param syncOptions
 * @param responses
 * @param gdprConsent
 * @param uspConsent
 * @return {{type: (string), url: (*|string)}[]}
 */
function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) {
  if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) {
    let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image';
    let queryParamStrings = [];
    let syncUrl = SYNC_URL;
    if (gdprConsent) {
      queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0));
      queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
    }
    if (uspConsent) {
      queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent));
    }
    if (responses.length > 0 && responses[0].body && responses[0].body.ext) {
      const ext = responses[0].body.ext;
      if (ext.delDomain) {
        syncUrl = `https://${ext.delDomain}/w/1.0/pd`
      } else if (ext.platform) {
        queryParamStrings.push('ph=' + ext.platform)
      }
    } else {
      queryParamStrings.push('ph=' + DEFAULT_PH)
    }
    return [{
      type: pixelType,
      url: `${syncUrl}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}`
    }];
  }
}