prebid/Prebid.js

View on GitHub
modules/conversantAnalyticsAdapter.js

Summary

Maintainability
D
2 days
Test Coverage
import {ajax} from '../src/ajax.js';
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import { EVENTS } from '../src/constants.js';
import {getGlobal} from '../src/prebidGlobal.js';
import adapterManager from '../src/adapterManager.js';
import {logInfo, logWarn, logError, logMessage, deepAccess, isInteger} from '../src/utils.js';
import {getRefererInfo} from '../src/refererDetection.js';

// Maintainer: mediapsr@epsilon.com

const { AUCTION_END, AD_RENDER_FAILED, BID_TIMEOUT, BID_WON, BIDDER_ERROR } = EVENTS;
// STALE_RENDER, TCF2_ENFORCEMENT would need to add extra calls for these as they likely occur after AUCTION_END?
const GVLID = 24;
const ANALYTICS_TYPE = 'endpoint';

// for local testing set domain to 127.0.0.1:8290
const DOMAIN = 'https://web.hb.ad.cpe.dotomi.com/';
const ANALYTICS_URL = DOMAIN + 'cvx/event/prebidanalytics';
const ERROR_URL = DOMAIN + 'cvx/event/prebidanalyticerrors';
const ANALYTICS_CODE = 'conversant';
const ANALYTICS_ALIASES = [ANALYTICS_CODE, 'epsilon', 'cnvr'];

export const CNVR_CONSTANTS = {
  LOG_PREFIX: 'Conversant analytics adapter: ',
  ERROR_MISSING_DATA_PREFIX: 'Parsing method failed because of missing data: ',
  // Maximum time to keep an item in the cache before it gets purged
  MAX_MILLISECONDS_IN_CACHE: 30000,
  // How often cache cleanup will run
  CACHE_CLEANUP_TIME_IN_MILLIS: 30000,
  // Should be float from 0-1, 0 is turned off, 1 is sample every instance
  DEFAULT_SAMPLE_RATE: 1,

  // BID STATUS CODES
  WIN: 10,
  BID: 20,
  NO_BID: 30,
  TIMEOUT: 40,
  RENDER_FAILED: 50
};

// Saves passed in options from the bid adapter
const initOptions = {};

// Simple flag to help handle any tear down needed on disable
let conversantAnalyticsEnabled = false;

export const cnvrHelper = {
  // Turns on sampling for an instance of prebid analytics.
  doSample: true,
  doSendErrorData: false,

  /**
   * Used to hold data for RENDER FAILED events so we can send a payload back that will match our original auction data.
   * Contains the following key/value data:
   * <adId> => {
   *     'bidderCode': <bidderCode>,
   *     'adUnitCode': <adUnitCode>,
   *     'auctionId': <auctionId>,
   *     'timeReceived': Date.now()  //For cache cleaning
   * }
   */
  adIdLookup: {},

  /**
   * Time out events happen before AUCTION END so we can save them in a cache and report them at the same time as the
   * AUCTION END event.  Has the following data and key is based off of auctionId, adUnitCode, bidderCode from
   * keyStr = getLookupKey(auctionId, adUnitCode, bidderCode);
   * <keyStr> => {
   *     timeReceived: Date.now() //so cache can be purged in case it doesn't get cleaned out at auctionEnd
   * }
   */
  timeoutCache: {},

  /**
   * Lookup of auction IDs to auction start timestamps
   */
  auctionIdTimestampCache: {},

  /**
   * Capture any bidder errors and bundle them with AUCTION_END
   */
  bidderErrorCache: {}
};

/**
 * Cleanup timer for the adIdLookup and timeoutCache caches. If all works properly then the caches are self-cleaning
 * but in case something goes sideways we poll periodically to cleanup old values to prevent a memory leak
 */
let cacheCleanupInterval;

let conversantAnalytics = Object.assign(
  adapter({URL: ANALYTICS_URL, ANALYTICS_TYPE}),
  {
    track({eventType, args}) {
      try {
        if (cnvrHelper.doSample) {
          logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' track(): ' + eventType, args);
          switch (eventType) {
            case AUCTION_END:
              onAuctionEnd(args);
              break;
            case AD_RENDER_FAILED:
              onAdRenderFailed(args);
              break;
            case BID_WON:
              onBidWon(args);
              break;
            case BID_TIMEOUT:
              onBidTimeout(args);
              break;
            case BIDDER_ERROR:
              onBidderError(args)
          } // END switch
        } else {
          logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' - ' + eventType + ': skipped due to sampling');
        }// END IF(cnvrHelper.doSample)
      } catch (e) {
        // e = {stack:"...",message:"..."}
        logError(CNVR_CONSTANTS.LOG_PREFIX + 'Caught error in handling ' + eventType + ' event: ' + e.message);
        cnvrHelper.sendErrorData(eventType, e);
      }
    } // END track()
  }
);

// ================================================== EVENT HANDLERS ===================================================

/**
 * Handler for BIDDER_ERROR events, tries to capture as much data, save it in cache which is then picked up by
 * AUCTION_END event and included in that payload. Was not able to see an easy way to get adUnitCode in this event
 * so not including it for now.
 * https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error
 * Trigger when the HTTP response status code is not between 200-299 and not equal to 304.
 {
    error: XMLHttpRequest,  https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
    bidderRequest: {  https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error
        {
            auctionId: "b06c5141-fe8f-4cdf-9d7d-54415490a917",
            auctionStart: 1579746300522,
            bidderCode: "myBidderCode",
            bidderRequestId: "15246a574e859f",
            bids: [{...}],
            gdprConsent: {consentString: "BOtmiBKOtmiBKABABAENAFAAAAACeAAA", vendorData: {...}, gdprApplies: true},
            refererInfo: {
                canonicalUrl: null,
                page: "http://mypage.org?pbjs_debug=true",
                domain: "mypage.org",
                ref: null,
                numIframes: 0,
                reachedTop: true,
                isAmp: false,
                stack: ["http://mypage.org?pbjs_debug=true"]
            }
        }
    }
}
 */
function onBidderError(args) {
  if (!cnvrHelper.doSendErrorData) {
    logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping bidder error parsing due to config disabling error logging, bidder error status = ' + args.error.status + ', Message = ' + args.error.statusText);
    return;
  }

  let error = args.error;
  let bidRequest = args.bidderRequest;
  let auctionId = bidRequest.auctionId;
  let bidderCode = bidRequest.bidderCode;
  logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onBidderError(): error received from bidder ' + bidderCode + '. Status = ' + error.status + ', Message = ' + error.statusText);
  let errorObj = {
    status: error.status,
    message: error.statusText,
    bidderCode: bidderCode,
    url: cnvrHelper.getPageUrl(),
  };
  if (cnvrHelper.bidderErrorCache[auctionId]) {
    cnvrHelper.bidderErrorCache[auctionId]['errors'].push(errorObj);
  } else {
    cnvrHelper.bidderErrorCache[auctionId] = {
      errors: [errorObj],
      timeReceived: Date.now()
    };
  }
}

/**
 * We get the list of timeouts before the endAution, cache them temporarily in a global cache and the endAuction event
 * will pick them up.  Uses getLookupKey() to create the key to the entry from auctionId, adUnitCode and bidderCode.
 * Saves a single value of timeReceived so we can do cache purging periodically.
 *
 * Current assumption is that the timeout will always be an array even if it is just one object in the array.
 * @param args  [{
    "bidId": "80882409358b8a8",
    "bidder": "conversant",
    "adUnitCode": "MedRect",
    "auctionId": "afbd6e0b-e45b-46ab-87bf-c0bac0cb8881"
  }, {
    "bidId": "9da4c107a6f24c8",
    "bidder": "conversant",
    "adUnitCode": "Leaderboard",
    "auctionId": "afbd6e0b-e45b-46ab-87bf-c0bac0cb8881"
  }
 ]
 */
function onBidTimeout(args) {
  args.forEach(timedOutBid => {
    const timeoutCacheKey = cnvrHelper.getLookupKey(timedOutBid.auctionId, timedOutBid.adUnitCode, timedOutBid.bidder);
    cnvrHelper.timeoutCache[timeoutCacheKey] = {
      timeReceived: Date.now()
    }
  });
}

/**
 * Bid won occurs after auctionEnd so we need to send this separately. We also save an entry in the adIdLookup cache
 * so that if the render fails we can match up important data so we can send a valid RENDER FAILED event back.
 * @param args bidWon args
 */
function onBidWon(args) {
  const bidderCode = args.bidderCode;
  const adUnitCode = args.adUnitCode;
  const auctionId = args.auctionId;
  let timestamp = args.requestTimestamp ? args.requestTimestamp : Date.now();

  // Make sure we have all the data we need
  if (!bidderCode || !adUnitCode || !auctionId) {
    let errorReason = 'auction id';
    if (!bidderCode) {
      errorReason = 'bidder code';
    } else if (!adUnitCode) {
      errorReason = 'ad unit code'
    }
    throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason);
  }

  if (cnvrHelper.auctionIdTimestampCache[auctionId]) {
    timestamp = cnvrHelper.auctionIdTimestampCache[auctionId].timeReceived; // Don't delete, could be multiple winners/auction, allow cleanup to handle
  }

  const bidWonPayload = cnvrHelper.createPayload('bid_won', auctionId, timestamp);

  const adUnitPayload = cnvrHelper.createAdUnit();
  bidWonPayload.adUnits[adUnitCode] = adUnitPayload;

  const bidPayload = cnvrHelper.createBid(CNVR_CONSTANTS.WIN, args.timeToRespond);
  bidPayload.adSize = cnvrHelper.createAdSize(args.width, args.height);
  bidPayload.cpm = args.cpm;
  bidPayload.originalCpm = args.originalCpm;
  bidPayload.currency = args.currency;
  bidPayload.mediaType = args.mediaType;
  adUnitPayload.bids[bidderCode] = [bidPayload];

  if (!cnvrHelper.adIdLookup[args.adId]) {
    cnvrHelper.adIdLookup[args.adId] = {
      'bidderCode': bidderCode,
      'adUnitCode': adUnitCode,
      'auctionId': auctionId,
      'timeReceived': Date.now() // For cache cleaning
    };
  }

  sendData(bidWonPayload);
}

/**
 * RENDER FAILED occurs after AUCTION END and BID WON, the payload does not have all the data we need so we use
 * adIdLookup to pull data from a BID WON event to populate our payload
 * @param args = {
 *  reason: <value>
 *  message: <value>
 *  adId: <value> --optional
 *  bid: {object?} --optional: unsure what this looks like but guessing it is {bidder: <value>, params: {object}}
 *    }
 */
function onAdRenderFailed(args) {
  const adId = args.adId;
  // Make sure we have all the data we need, adId is optional so it's not guaranteed, without that we can't match it up
  // to our adIdLookup data.
  if (!adId || !cnvrHelper.adIdLookup[adId]) {
    let errorMsg = 'ad id';
    if (adId) {
      errorMsg = 'no lookup data for ad id';
    }
    // Either no adId to match against a bidWon event, or no data saved from a bidWon event that matches the adId
    throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorMsg);
  }
  const adIdObj = cnvrHelper.adIdLookup[adId];
  const adUnitCode = adIdObj['adUnitCode'];
  const bidderCode = adIdObj['bidderCode'];
  const auctionId = adIdObj['auctionId'];
  delete cnvrHelper.adIdLookup[adId]; // cleanup our cache

  if (!bidderCode || !adUnitCode || !auctionId) {
    let errorReason = 'auction id';
    if (!bidderCode) {
      errorReason = 'bidder code';
    } else if (!adUnitCode) {
      errorReason = 'ad unit code'
    }
    throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason);
  }

  let timestamp = Date.now();
  if (cnvrHelper.auctionIdTimestampCache[auctionId]) {
    timestamp = cnvrHelper.auctionIdTimestampCache[auctionId].timeReceived; // Don't delete, could be multiple winners/auction, allow cleanup to handle
  }

  const renderFailedPayload = cnvrHelper.createPayload('render_failed', auctionId, timestamp);
  const adUnitPayload = cnvrHelper.createAdUnit();
  adUnitPayload.bids[bidderCode] = [cnvrHelper.createBid(CNVR_CONSTANTS.RENDER_FAILED, 0)];
  adUnitPayload.bids[bidderCode][0].message = 'REASON: ' + args.reason + '. MESSAGE: ' + args.message;
  renderFailedPayload.adUnits[adUnitCode] = adUnitPayload;
  sendData(renderFailedPayload);
}

/**
 * AUCTION END contains bid and no bid info and all of the auction info we need. This sends the bulk of the information
 * about the auction back to the servers.  It will also check the timeoutCache for any matching bids, if any are found
 * then they will be removed from the cache and send back with this payload.
 * @param args AUCTION END payload, fairly large data structure, main objects are 'adUnits[]', 'bidderRequests[]',
 * 'noBids[]', 'bidsReceived[]'... 'winningBids[]' seems to be always blank.
 */
function onAuctionEnd(args) {
  const auctionId = args.auctionId;
  if (!auctionId) {
    throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'auction id');
  }

  const auctionTimestamp = args.timestamp ? args.timestamp : Date.now();
  cnvrHelper.auctionIdTimestampCache[auctionId] = { timeReceived: auctionTimestamp };

  const auctionEndPayload = cnvrHelper.createPayload('auction_end', auctionId, auctionTimestamp);
  // Get bid request information from adUnits
  if (!Array.isArray(args.adUnits)) {
    throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'no adUnits in event args');
  }

  // Write out any bid errors
  if (cnvrHelper.bidderErrorCache[auctionId]) {
    auctionEndPayload.bidderErrors = cnvrHelper.bidderErrorCache[auctionId].errors;
    delete cnvrHelper.bidderErrorCache[auctionId];
  }

  args.adUnits.forEach(adUnit => {
    const cnvrAdUnit = cnvrHelper.createAdUnit();
    // Initialize bids with bidderCode
    adUnit.bids.forEach(bid => {
      cnvrAdUnit.bids[bid.bidder] = []; // support multiple bids from a bidder for different sizes/media types //cnvrHelper.initializeBidDefaults();

      // Check for cached timeout responses
      const timeoutKey = cnvrHelper.getLookupKey(auctionId, adUnit.code, bid.bidder);
      if (cnvrHelper.timeoutCache[timeoutKey]) {
        cnvrAdUnit.bids[bid.bidder].push(cnvrHelper.createBid(CNVR_CONSTANTS.TIMEOUT, args.timeout));
        delete cnvrHelper.timeoutCache[timeoutKey];
      }
    });

    // Ad media types for the ad slot
    if (cnvrHelper.keyExistsAndIsObject(adUnit, 'mediaTypes')) {
      Object.entries(adUnit.mediaTypes).forEach(([mediaTypeName]) => {
        cnvrAdUnit.mediaTypes.push(mediaTypeName);
      });
    }

    // Ad sizes listed under the size key
    if (Array.isArray(adUnit.sizes) && adUnit.sizes.length >= 1) {
      adUnit.sizes.forEach(size => {
        if (!Array.isArray(size) || size.length !== 2) {
          logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unknown object while retrieving adUnit sizes.', adUnit);
          return; // skips to next item
        }
        cnvrAdUnit.sizes.push(cnvrHelper.createAdSize(size[0], size[1]));
      });
    }

    // If the Ad Slot is not unique then ad sizes and media types merge them together
    if (auctionEndPayload.adUnits[adUnit.code]) {
      // Merge ad sizes
      Array.prototype.push.apply(auctionEndPayload.adUnits[adUnit.code].sizes, cnvrAdUnit.sizes);
      // Merge mediaTypes
      Array.prototype.push.apply(auctionEndPayload.adUnits[adUnit.code].mediaTypes, cnvrAdUnit.mediaTypes);
    } else {
      auctionEndPayload.adUnits[adUnit.code] = cnvrAdUnit;
    }
  });

  if (Array.isArray(args.noBids)) {
    args.noBids.forEach(noBid => {
      const bidPayloadArray = deepAccess(auctionEndPayload, 'adUnits.' + noBid.adUnitCode + '.bids.' + noBid.bidder);

      if (bidPayloadArray) {
        bidPayloadArray.push(cnvrHelper.createBid(CNVR_CONSTANTS.NO_BID, 0)); // no time to respond info for this, would have to capture event and save it there
      } else {
        logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unable to locate bid object via adUnitCode/bidderCode in payload for noBid reply in END_AUCTION', Object.assign({}, noBid));
      }
    });
  } else {
    logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): noBids not defined in arguments.');
  }

  // Get bid data from bids sent
  if (Array.isArray(args.bidsReceived)) {
    args.bidsReceived.forEach(bid => {
      const bidPayloadArray = deepAccess(auctionEndPayload, 'adUnits.' + bid.adUnitCode + '.bids.' + bid.bidderCode);
      if (bidPayloadArray) {
        const bidPayload = cnvrHelper.createBid(CNVR_CONSTANTS.BID, bid.timeToRespond);
        bidPayload.originalCpm = bid.originalCpm;
        bidPayload.cpm = bid.cpm;
        bidPayload.currency = bid.currency;
        bidPayload.mediaType = bid.mediaType;
        bidPayload.adSize = {
          'w': bid.width,
          'h': bid.height
        };
        bidPayloadArray.push(bidPayload);
      } else {
        logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unable to locate bid object via adUnitCode/bidderCode in payload for bid reply in END_AUCTION', Object.assign({}, bid));
      }
    });
  } else {
    logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): bidsReceived not defined in arguments.');
  }
  // We need to remove any duplicate ad sizes from merging ad-slots or overlap in different media types and also
  // media-types from merged ad-slots in twin bids.
  Object.keys(auctionEndPayload.adUnits).forEach(function(adCode) {
    auctionEndPayload.adUnits[adCode].sizes = cnvrHelper.deduplicateArray(auctionEndPayload.adUnits[adCode].sizes);
    auctionEndPayload.adUnits[adCode].mediaTypes = cnvrHelper.deduplicateArray(auctionEndPayload.adUnits[adCode].mediaTypes);
  });

  sendData(auctionEndPayload);
}

// =============================================== START OF HELPERS ===================================================

/**
 * Helper to verify a key exists and is a data type of Object (not a function, or array)
 * @param parent The parent that we want to check the key for
 * @param key The key which we want to check
 * @returns {boolean} True if it's an object and exists, false otherwise (null, array, primitive, function)
 */
cnvrHelper.keyExistsAndIsObject = function (parent, key) {
  if (!parent.hasOwnProperty(key)) {
    return false;
  }
  return typeof parent[key] === 'object' &&
    !Array.isArray(parent[key]) &&
    parent[key] !== null;
}

/**
 * De-duplicate an array that could contain primitives or objects/associative arrays.
 * A temporary array is used to store a string representation of each object that we look at.  If an object matches
 * one found in the temp array then it is ignored.
 * @param array An array
 * @returns {*} A de-duplicated array.
 */
cnvrHelper.deduplicateArray = function(array) {
  if (!array || !Array.isArray(array)) {
    return array;
  }

  const tmpArray = [];
  return array.filter(function (tmpObj) {
    if (tmpArray.indexOf(JSON.stringify(tmpObj)) < 0) {
      tmpArray.push(JSON.stringify(tmpObj));
      return tmpObj;
    }
  });
};

/**
 * Generic method to look at each key/value pair of a cache object and looks at the 'timeReceived' key, if more than
 * the max wait time has passed then just delete the key.
 * @param cacheObj one of our cache objects [adIdLookup or timeoutCache]
 * @param currTime the current timestamp at the start of the most recent timer execution.
 */
cnvrHelper.cleanCache = function(cacheObj, currTime) {
  Object.keys(cacheObj).forEach(key => {
    const timeInCache = currTime - cacheObj[key].timeReceived;
    if (timeInCache >= CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE) {
      delete cacheObj[key];
    }
  });
};

/**
 * Helper to create an object lookup key for our timeoutCache
 * @param auctionId id of the auction
 * @param adUnitCode ad unit code
 * @param bidderCode bidder code
 * @returns string concatenation of all the params into a string key for timeoutCache
 */
cnvrHelper.getLookupKey = function(auctionId, adUnitCode, bidderCode) {
  return auctionId + '-' + adUnitCode + '-' + bidderCode;
};

/**
 * Creates our root payload object that gets sent back to the server
 * @param payloadType string type of payload (AUCTION_END, BID_WON, RENDER_FAILED)
 * @param auctionId id for the auction
 * @param timestamp timestamp in milliseconds of auction start time.
 * @returns
 *  {{
 *    requestType: *,
 *    adUnits: {},
 *    auction: {
 *      auctionId: *,
 *      preBidVersion: *,
 *      sid: *}
 * }}  Basic structure of our object that we return to the server.
 */
cnvrHelper.createPayload = function(payloadType, auctionId, timestamp) {
  return {
    requestType: payloadType,
    globalSampleRate: initOptions.global_sample_rate,
    cnvrSampleRate: initOptions.cnvr_sample_rate,
    auction: {
      auctionId: auctionId,
      preBidVersion: getGlobal().version,
      sid: initOptions.site_id,
      auctionTimestamp: timestamp
    },
    adUnits: {},
    bidderErrors: []
  };
};

/**
 * Helper to create an adSize object, if the value passed in is not an int then set it to -1
 * @param width in pixels (must be an int)
 * @param height in peixl (must be an int)
 * @returns {{w: *, h: *}} a fully valid adSize object
 */
cnvrHelper.createAdSize = function(width, height) {
  if (!isInteger(width)) {
    width = -1;
  }
  if (!isInteger(height)) {
    height = -1;
  }
  return {
    'w': width,
    'h': height
  };
};

/**
 * Helper to create the basic structure of our adUnit payload
 * @returns {{sizes: [], bids: {}}} Basic adUnit payload structure as follows
 */
cnvrHelper.createAdUnit = function() {
  return {
    sizes: [],
    mediaTypes: [],
    bids: {}
  };
};

/**
 * Helper to create a basic bid payload object.
 */
cnvrHelper.createBid = function (eventCode, timeToRespond) {
  return {
    'eventCodes': [eventCode],
    'timeToRespond': timeToRespond
  };
};

/**
 * Helper to get the sampling rates from an object and validate the result.
 * @param parentObj Parent object that has the sampling property
 * @param propNm Name of the sampling property
 * @param defaultSampleRate A default value to apply if there is a problem
 * @returns {number} returns a float number from 0 (always off) to 1 (always on)
 */
cnvrHelper.getSampleRate = function(parentObj, propNm, defaultSampleRate) {
  let sampleRate = defaultSampleRate;
  if (parentObj && typeof parentObj[propNm] !== 'undefined') {
    sampleRate = parseFloat(parentObj[propNm]);
    if (Number.isNaN(sampleRate) || sampleRate > 1) {
      sampleRate = defaultSampleRate;
    } else if (sampleRate < 0) {
      sampleRate = 0;
    }
  }
  return sampleRate;
}

/**
 * Helper to encapsulate logic for getting best known page url. Small but helpful in debugging/testing and if we ever want
 * to add more logic to this.
 *
 * From getRefererInfo(): page = the best candidate for the current page URL: `canonicalUrl`, falling back to `location`
 * @returns {*} Best guess at top URL based on logic from RefererInfo.
 */
cnvrHelper.getPageUrl = function() {
  return getRefererInfo().page;
}

/**
 * Packages up an error that occured in analytics handling and sends it back to our servers for logging
 * @param eventType = original event that was fired
 * @param exception = {stack:"...",message:"..."}, exception that was triggered
 */
cnvrHelper.sendErrorData = function(eventType, exception) {
  if (!cnvrHelper.doSendErrorData) {
    logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping sending error data due to config disabling error logging, error thrown = ' + exception);
    return;
  }

  let error = {
    event: eventType,
    siteId: initOptions.site_id,
    message: exception.message,
    stack: exception.stack,
    prebidVersion: '$$REPO_AND_VERSION$$', // testing val sample: prebid_prebid_7.27.0-pre'
    userAgent: navigator.userAgent,
    url: cnvrHelper.getPageUrl()
  };

  // eslint-disable-next-line no-undef
  ajax(ERROR_URL, function () {}, JSON.stringify(error), {contentType: 'text/plain'});
}

/**
 * Helper function to send data back to server.  Need to make sure we don't trigger a CORS preflight by not adding
 * extra header params.
 * @param payload our JSON payload from either AUCTION END, BID WIN, RENDER FAILED
 */
function sendData(payload) {
  ajax(ANALYTICS_URL, function () {}, JSON.stringify(payload), {contentType: 'text/plain'});
}

// =============================== BOILERPLATE FOR PRE-BID ANALYTICS SETUP  ============================================
// save the base class function
conversantAnalytics.originEnableAnalytics = conversantAnalytics.enableAnalytics;
conversantAnalytics.originDisableAnalytics = conversantAnalytics.disableAnalytics;

// override enableAnalytics so we can get access to the config passed in from the page
conversantAnalytics.enableAnalytics = function (config) {
  if (!config || !config.options || !config.options.site_id) {
    logError(CNVR_CONSTANTS.LOG_PREFIX + 'siteId is required.');
    return;
  }

  cacheCleanupInterval = setInterval(
    function() {
      const currTime = Date.now();
      cnvrHelper.cleanCache(cnvrHelper.adIdLookup, currTime);
      cnvrHelper.cleanCache(cnvrHelper.timeoutCache, currTime);
      cnvrHelper.cleanCache(cnvrHelper.auctionIdTimestampCache, currTime);
      cnvrHelper.cleanCache(cnvrHelper.bidderErrorCache, currTime);
    },
    CNVR_CONSTANTS.CACHE_CLEANUP_TIME_IN_MILLIS
  );

  Object.assign(initOptions, config.options);

  initOptions.global_sample_rate = cnvrHelper.getSampleRate(initOptions, 'sampling', 1);
  initOptions.cnvr_sample_rate = cnvrHelper.getSampleRate(initOptions, 'cnvr_sampling', CNVR_CONSTANTS.DEFAULT_SAMPLE_RATE);

  logInfo(CNVR_CONSTANTS.LOG_PREFIX + 'Conversant sample rate set to ' + initOptions.cnvr_sample_rate);
  logInfo(CNVR_CONSTANTS.LOG_PREFIX + 'Global sample rate set to ' + initOptions.global_sample_rate);
  // Math.random() pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1)
  cnvrHelper.doSample = Math.random() < initOptions.cnvr_sample_rate;

  if (initOptions.send_error_data !== undefined && initOptions.send_error_data !== null) {
    cnvrHelper.doSendErrorData = !!initOptions.send_error_data; // Forces data into boolean type
  }

  conversantAnalyticsEnabled = true;
  conversantAnalytics.originEnableAnalytics(config); // call the base class function
};

/**
 * Cleanup code for any timers and caches.
 */
conversantAnalytics.disableAnalytics = function () {
  if (!conversantAnalyticsEnabled) {
    return;
  }

  // Cleanup our caches and disable our timer
  clearInterval(cacheCleanupInterval);
  cnvrHelper.timeoutCache = {};
  cnvrHelper.adIdLookup = {};
  cnvrHelper.auctionIdTimestampCache = {};
  cnvrHelper.bidderErrorCache = {};

  conversantAnalyticsEnabled = false;
  conversantAnalytics.originDisableAnalytics();
};
ANALYTICS_ALIASES.forEach(alias => {
  adapterManager.registerAnalyticsAdapter({
    adapter: conversantAnalytics,
    code: alias,
    gvlid: GVLID
  });
});

export default conversantAnalytics;