prebid/Prebid.js

View on GitHub
modules/dxkultureBidAdapter.js

Summary

Maintainability
D
2 days
Test Coverage
import {
  logInfo,
  logWarn,
  logError,
  logMessage,
  deepAccess,
  deepSetValue,
  mergeDeep
} from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import { Renderer } from '../src/Renderer.js';
import {ortbConverter} from '../libraries/ortbConverter/converter.js'

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

const BIDDER_CODE = 'dxkulture';
const DEFAULT_BID_TTL = 300;
const DEFAULT_NET_REVENUE = true;
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_OUTSTREAM_RENDERER_URL = 'https://cdn.dxkulture.com/players/dxOutstreamPlayer.js';

const converter = ortbConverter({
  context: {
    netRevenue: DEFAULT_NET_REVENUE,
    ttl: DEFAULT_BID_TTL
  },
  imp(buildImp, bidRequest, context) {
    const imp = buildImp(bidRequest, context);

    if (!imp.bidfloor) {
      imp.bidfloor = bidRequest.params.bidfloor || 0;
      imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY;
    }
    return imp;
  },
  request(buildRequest, imps, bidderRequest, context) {
    const req = buildRequest(imps, bidderRequest, context);
    mergeDeep(req, {
      ext: {
        hb: 1,
        prebidver: '$prebid.version$',
        adapterver: '1.0.0',
      }
    })

    // Attaching GDPR Consent Params
    if (bidderRequest.gdprConsent) {
      deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
      deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0));
    }

    // CCPA
    if (bidderRequest.uspConsent) {
      deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent);
    }

    return req;
  },
  bidResponse(buildBidResponse, bid, context) {
    let resMediaType;
    const {bidRequest} = context;

    if (bid.adm?.trim().startsWith('<VAST')) {
      resMediaType = VIDEO;
    } else {
      resMediaType = BANNER;
    }

    context.mediaType = resMediaType;
    context.currency = DEFAULT_CURRENCY;

    if (resMediaType === VIDEO) {
      context.vastXml = bid.adm;
    }

    const bidResponse = buildBidResponse(bid, context);

    if (resMediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') {
      bidResponse.renderer = outstreamRenderer(bidResponse);
    }

    return bidResponse;
  }
});

export const spec = {
  code: BIDDER_CODE,
  VERSION: '1.0.0',
  supportedMediaTypes: [BANNER, VIDEO],
  ENDPOINT: 'https://ads.dxkulture.com/pbjs',

  /**
   * 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) {
    return (
      _validateParams(bid) &&
      _validateBanner(bid) &&
      _validateVideo(bid)
    );
  },

  buildRequests: function (validBidRequests, bidderRequest) {
    let contextMediaType = BANNER;

    if (hasVideoMediaType(validBidRequests)) {
      contextMediaType = VIDEO;
    }

    const data = converter.toORTB({ bidRequests: validBidRequests, bidderRequest, context: {contextMediaType} });

    let publisherId = validBidRequests[0].params.publisherId;
    let placementId = validBidRequests[0].params.placementId;

    if (validBidRequests[0].params.e2etest) {
      logMessage('dxkulture: E2E test mode enabled');
      publisherId = 'e2etest'
    }
    let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId;

    if (placementId) {
      baseEndpoint += '&placementId=' + placementId
    }

    return {
      method: 'POST',
      url: baseEndpoint,
      data: data
    };
  },

  interpretResponse: function (serverResponse, bidRequest) {
    const bids = converter.fromORTB({response: serverResponse.body, request: bidRequest.data}).bids;
    return bids;
  },

  getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) {
    logInfo('dxkulture.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses);
    let syncs = [];

    if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
      return syncs;
    }

    serverResponses.forEach(resp => {
      const userSync = deepAccess(resp, 'body.ext.usersync');
      if (userSync) {
        let syncDetails = [];
        Object.keys(userSync).forEach(key => {
          const value = userSync[key];
          if (value.syncs && value.syncs.length) {
            syncDetails = syncDetails.concat(value.syncs);
          }
        });
        syncDetails.forEach(syncDetails => {
          let queryParamStrings = [];
          let syncUrl = syncDetails.url;

          if (syncDetails.type === 'iframe') {
            if (gdprConsent) {
              queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0));
              queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
            }
            if (uspConsent) {
              queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent));
            }
            syncUrl = `${syncDetails.url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}`
          }

          syncs.push({
            type: syncDetails.type === 'iframe' ? 'iframe' : 'image',
            url: syncUrl
          });
        });

        if (syncOptions.iframeEnabled) {
          syncs = syncs.filter(s => s.type == 'iframe');
        } else if (syncOptions.pixelEnabled) {
          syncs = syncs.filter(s => s.type == 'image');
        }
      }
    });
    logInfo('dxkulture.getUserSyncs result=%o', syncs);
    return syncs;
  },

};

function outstreamRenderer(bid) {
  const rendererConfig = {
    width: bid.width,
    height: bid.height,
    vastTimeout: 5000,
    maxAllowedVastTagRedirects: 3,
    allowVpaid: false,
    autoPlay: true,
    preload: true,
    mute: false
  }

  const renderer = Renderer.install({
    id: bid.adId,
    url: DEFAULT_OUTSTREAM_RENDERER_URL,
    config: rendererConfig,
    loaded: false,
    targetId: bid.adUnitCode,
    adUnitCode: bid.adUnitCode
  });

  try {
    renderer.setRender(function (bid) {
      bid.renderer.push(() => {
        const { id, config } = bid.renderer;
        window.dxOutstreamPlayer(bid, id, config);
      });
    });
  } catch (err) {
    logWarn('dxkulture: Prebid Error calling setRender on renderer', err);
  }

  return renderer;
}

/* =======================================
 * Util Functions
 *======================================= */

function hasBannerMediaType(bidRequest) {
  return !!deepAccess(bidRequest, 'mediaTypes.banner');
}

function hasVideoMediaType(bidRequest) {
  return !!deepAccess(bidRequest, 'mediaTypes.video');
}

function _validateParams(bidRequest) {
  if (!bidRequest.params) {
    return false;
  }

  if (bidRequest.params.e2etest) {
    return true;
  }

  if (!bidRequest.params.publisherId) {
    logError('dxkulture: Validation failed: publisherId not declared');
    return false;
  }

  if (!bidRequest.params.placementId) {
    logError('dxkulture: Validation failed: placementId not declared');
    return false;
  }

  const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest);
  if (!mediaTypesExists) {
    return false;
  }

  return true;
}

/**
 * Validates banner bid request. If it is not banner media type returns true.
 * @param {BidRequest} bidRequest bid to validate
 * @return boolean, true if valid, otherwise false
 */
function _validateBanner(bidRequest) {
  // If there's no banner no need to validate
  if (!hasBannerMediaType(bidRequest)) {
    return true;
  }
  const banner = deepAccess(bidRequest, 'mediaTypes.banner');
  if (!Array.isArray(banner.sizes)) {
    return false;
  }

  return true;
}

/**
 * Validates video bid request. If it is not video media type returns true.
 * @param {BidRequest} bidRequest, bid to validate
 * @return boolean, true if valid, otherwise false
 */
function _validateVideo(bidRequest) {
  // If there's no video no need to validate
  if (!hasVideoMediaType(bidRequest)) {
    return true;
  }

  const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {});
  const videoBidderParams = deepAccess(bidRequest, 'params.video', {});
  const params = deepAccess(bidRequest, 'params', {});

  if (params && params.e2etest) {
    return true;
  }

  const videoParams = {
    ...videoPlacement,
    ...videoBidderParams // Bidder Specific overrides
  };

  if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) {
    logError('dxkulture: Validation failed: mimes are invalid');
    return false;
  }

  if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) {
    logError('dxkulture: Validation failed: protocols are invalid');
    return false;
  }

  if (!videoParams.context) {
    logError('dxkulture: Validation failed: context id not declared');
    return false;
  }

  if (videoParams.context !== 'instream') {
    logError('dxkulture: Validation failed: only context instream is supported ');
    return false;
  }

  if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) {
    logError('dxkulture: Validation failed: player size not declared or is not in format [[w,h]]');
    return false;
  }

  return true;
}

registerBidder(spec);