prebid/Prebid.js

View on GitHub
modules/roxotAnalyticsAdapter.js

Summary

Maintainability
C
1 day
Test Coverage
import {deepClone, getParameterByName, logError, logInfo} from '../src/utils.js';
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import { EVENTS } from '../src/constants.js';
import adapterManager from '../src/adapterManager.js';
import {includes} from '../src/polyfill.js';
import {ajaxBuilder} from '../src/ajax.js';
import {getStorageManager} from '../src/storageManager.js';
import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js';

const MODULE_CODE = 'roxot';

const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE});

let ajax = ajaxBuilder(0);

const DEFAULT_EVENT_URL = 'pa.rxthdr.com/v3';
const DEFAULT_SERVER_CONFIG_URL = 'pa.rxthdr.com/v3';
const analyticsType = 'endpoint';

const {
  AUCTION_INIT,
  AUCTION_END,
  BID_REQUESTED,
  BID_ADJUSTMENT,
  BIDDER_DONE,
  BID_WON
} = EVENTS;

const AUCTION_STATUS = {
  'RUNNING': 'running',
  'FINISHED': 'finished'
};
const BIDDER_STATUS = {
  'REQUESTED': 'requested',
  'BID': 'bid',
  'NO_BID': 'noBid',
  'TIMEOUT': 'timeout'
};
const ROXOT_EVENTS = {
  'AUCTION': 'a',
  'IMPRESSION': 'i',
  'BID_AFTER_TIMEOUT': 'bat'
};

let initOptions = {};

let localStoragePrefix = 'roxot_analytics_';

let utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
let utmTtlKey = 'utm_ttl';
let utmTtl = 60 * 60 * 1000;

let isNewKey = 'is_new_flag';
let isNewTtl = 60 * 60 * 1000;

let auctionCache = {};
let auctionTtl = 60 * 60 * 1000;

let sendEventCache = [];
let sendEventTimeoutId = null;
let sendEventTimeoutTime = 1000;

function detectDevice() {
  if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) {
    return 'tablet';
  }
  if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) {
    return 'mobile';
  }
  return 'desktop';
}

function checkIsNewFlag() {
  let key = buildLocalStorageKey(isNewKey);
  let lastUpdate = Number(storage.getDataFromLocalStorage(key));
  storage.setDataInLocalStorage(key, Date.now());
  return Date.now() - lastUpdate > isNewTtl;
}

function updateUtmTimeout() {
  storage.setDataInLocalStorage(buildLocalStorageKey(utmTtlKey), Date.now());
}

function isUtmTimeoutExpired() {
  let utmTimestamp = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTtlKey));
  return (Date.now() - utmTimestamp) > utmTtl;
}

function buildLocalStorageKey(key) {
  return localStoragePrefix.concat(key);
}

function isSupportedAdUnit(adUnit) {
  if (!initOptions.adUnits.length) {
    return true;
  }

  return includes(initOptions.adUnits, adUnit);
}

function deleteOldAuctions() {
  for (let auctionId in auctionCache) {
    let auction = auctionCache[auctionId];
    if (Date.now() - auction.start > auctionTtl) {
      delete auctionCache[auctionId];
    }
  }
}

function buildAuctionEntity(args) {
  return {
    'id': args.auctionId,
    'start': args.timestamp,
    'timeout': args.timeout,
    'adUnits': {}
  };
}

function extractAdUnitCode(args) {
  return args.adUnitCode.toLowerCase();
}

function extractBidder(args) {
  return args.bidder.toLowerCase();
}

function buildAdUnitAuctionEntity(auction, bidRequest) {
  return {
    'adUnit': extractAdUnitCode(bidRequest),
    'start': auction.start,
    'timeout': auction.timeout,
    'finish': 0,
    'status': AUCTION_STATUS.RUNNING,
    'bidders': {}
  };
}

function buildBidderRequest(auction, bidRequest) {
  return {
    'bidder': extractBidder(bidRequest),
    'isAfterTimeout': auction.status === AUCTION_STATUS.FINISHED ? 1 : 0,
    'start': bidRequest.startTime || Date.now(),
    'finish': 0,
    'status': BIDDER_STATUS.REQUESTED,
    'cpm': -1,
    'size': {
      'width': 0,
      'height': 0
    },
    'mediaType': '-',
    'source': bidRequest.source || 'client'
  };
}

function buildBidAfterTimeout(adUnitAuction, args) {
  return {
    'auction': deepClone(adUnitAuction),
    'adUnit': extractAdUnitCode(args),
    'bidder': extractBidder(args),
    'cpm': args.cpm,
    'size': {
      'width': args.width || 0,
      'height': args.height || 0
    },
    'mediaType': args.mediaType || '-',
    'start': args.requestTimestamp,
    'finish': args.responseTimestamp,
  };
}

function buildImpression(adUnitAuction, args) {
  return {
    'isNew': checkIsNewFlag() ? 1 : 0,
    'auction': deepClone(adUnitAuction),
    'adUnit': extractAdUnitCode(args),
    'bidder': extractBidder(args),
    'cpm': args.cpm,
    'size': {
      'width': args.width,
      'height': args.height
    },
    'mediaType': args.mediaType,
    'source': args.source || 'client'
  };
}

function handleAuctionInit(args) {
  auctionCache[args.auctionId] = buildAuctionEntity(args);
  deleteOldAuctions();
}

function handleBidRequested(args) {
  let auction = auctionCache[args.auctionId];
  args.bids.forEach(function (bidRequest) {
    let adUnitCode = extractAdUnitCode(bidRequest);
    let bidder = extractBidder(bidRequest);
    if (!isSupportedAdUnit(adUnitCode)) {
      return;
    }
    auction['adUnits'][adUnitCode] = auction['adUnits'][adUnitCode] || buildAdUnitAuctionEntity(auction, bidRequest);
    let adUnitAuction = auction['adUnits'][adUnitCode];
    adUnitAuction['bidders'][bidder] = adUnitAuction['bidders'][bidder] || buildBidderRequest(auction, bidRequest);
  });
}

function handleBidAdjustment(args) {
  let adUnitCode = extractAdUnitCode(args);
  let bidder = extractBidder(args);
  if (!isSupportedAdUnit(adUnitCode)) {
    return;
  }

  let adUnitAuction = auctionCache[args.auctionId]['adUnits'][adUnitCode];
  if (adUnitAuction.status === AUCTION_STATUS.FINISHED) {
    handleBidAfterTimeout(adUnitAuction, args);
    return;
  }

  let bidderRequest = adUnitAuction['bidders'][bidder];
  if (bidderRequest.cpm < args.cpm) {
    bidderRequest.cpm = args.cpm;
    bidderRequest.finish = args.responseTimestamp;
    bidderRequest.status = args.cpm === 0 ? BIDDER_STATUS.NO_BID : BIDDER_STATUS.BID;
    bidderRequest.size.width = args.width || 0;
    bidderRequest.size.height = args.height || 0;
    bidderRequest.mediaType = args.mediaType || '-';
    bidderRequest.source = args.source || 'client';
  }
}

function handleBidAfterTimeout(adUnitAuction, args) {
  let bidder = extractBidder(args);
  let bidderRequest = adUnitAuction['bidders'][bidder];
  let bidAfterTimeout = buildBidAfterTimeout(adUnitAuction, args);

  if (bidAfterTimeout.cpm > bidderRequest.cpm) {
    bidderRequest.cpm = bidAfterTimeout.cpm;
    bidderRequest.isAfterTimeout = 1;
    bidderRequest.finish = bidAfterTimeout.finish;
    bidderRequest.size = bidAfterTimeout.size;
    bidderRequest.mediaType = bidAfterTimeout.mediaType;
    bidderRequest.status = bidAfterTimeout.cpm === 0 ? BIDDER_STATUS.NO_BID : BIDDER_STATUS.BID;
  }

  registerEvent(ROXOT_EVENTS.BID_AFTER_TIMEOUT, 'Bid After Timeout', bidAfterTimeout);
}

function handleBidderDone(args) {
  let auction = auctionCache[args.auctionId];

  args.bids.forEach(function (bidDone) {
    let adUnitCode = extractAdUnitCode(bidDone);
    let bidder = extractBidder(bidDone);
    if (!isSupportedAdUnit(adUnitCode)) {
      return;
    }

    let adUnitAuction = auction['adUnits'][adUnitCode];
    if (adUnitAuction.status === AUCTION_STATUS.FINISHED) {
      return;
    }
    let bidderRequest = adUnitAuction['bidders'][bidder];
    if (bidderRequest.status !== BIDDER_STATUS.REQUESTED) {
      return;
    }

    bidderRequest.finish = Date.now();
    bidderRequest.status = BIDDER_STATUS.NO_BID;
    bidderRequest.cpm = 0;
  });
}

function handleAuctionEnd(args) {
  let auction = auctionCache[args.auctionId];
  if (!Object.keys(auction.adUnits).length) {
    delete auctionCache[args.auctionId];
  }

  let finish = Date.now();
  auction.finish = finish;
  for (let adUnit in auction.adUnits) {
    let adUnitAuction = auction.adUnits[adUnit];
    adUnitAuction.finish = finish;
    adUnitAuction.status = AUCTION_STATUS.FINISHED;

    for (let bidder in adUnitAuction.bidders) {
      let bidderRequest = adUnitAuction.bidders[bidder];
      if (bidderRequest.status !== BIDDER_STATUS.REQUESTED) {
        continue;
      }

      bidderRequest.status = BIDDER_STATUS.TIMEOUT;
    }
  }

  registerEvent(ROXOT_EVENTS.AUCTION, 'Auction', auction);
}

function handleBidWon(args) {
  let adUnitCode = extractAdUnitCode(args);
  if (!isSupportedAdUnit(adUnitCode)) {
    return;
  }
  let adUnitAuction = auctionCache[args.auctionId]['adUnits'][adUnitCode];
  let impression = buildImpression(adUnitAuction, args);
  registerEvent(ROXOT_EVENTS.IMPRESSION, 'Bid won', impression);
}

function handleOtherEvents(eventType, args) {
  registerEvent(eventType, eventType, args);
}

let roxotAdapter = Object.assign(adapter({url: DEFAULT_EVENT_URL, analyticsType}), {
  track({eventType, args}) {
    switch (eventType) {
      case AUCTION_INIT:
        handleAuctionInit(args);
        break;
      case BID_REQUESTED:
        handleBidRequested(args);
        break;
      case BID_ADJUSTMENT:
        handleBidAdjustment(args);
        break;
      case BIDDER_DONE:
        handleBidderDone(args);
        break;
      case AUCTION_END:
        handleAuctionEnd(args);
        break;
      case BID_WON:
        handleBidWon(args);
        break;
      default:
        handleOtherEvents(eventType, args);
        break;
    }
  },

});

roxotAdapter.originEnableAnalytics = roxotAdapter.enableAnalytics;

roxotAdapter.enableAnalytics = function (config) {
  if (this.initConfig(config)) {
    _logInfo('Analytics adapter enabled', initOptions);
    roxotAdapter.originEnableAnalytics(config);
  }
};

roxotAdapter.buildUtmTagData = function () {
  let utmTagData = {};
  let utmTagsDetected = false;
  utmTags.forEach(function (utmTagKey) {
    let utmTagValue = getParameterByName(utmTagKey);
    if (utmTagValue !== '') {
      utmTagsDetected = true;
    }
    utmTagData[utmTagKey] = utmTagValue;
  });
  utmTags.forEach(function (utmTagKey) {
    if (utmTagsDetected) {
      storage.setDataInLocalStorage(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]);
      updateUtmTimeout();
    } else {
      if (!isUtmTimeoutExpired()) {
        utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) : '';
        updateUtmTimeout();
      }
    }
  });
  return utmTagData;
};

roxotAdapter.initConfig = function (config) {
  let isCorrectConfig = true;
  initOptions = {};
  initOptions.options = deepClone(config.options);

  initOptions.publisherId = initOptions.options.publisherId || (initOptions.options.publisherIds[0]) || null;
  if (!initOptions.publisherId) {
    _logError('"options.publisherId" is empty');
    isCorrectConfig = false;
  }

  initOptions.adUnits = initOptions.options.adUnits || [];
  initOptions.adUnits = initOptions.adUnits.map(value => value.toLowerCase());
  initOptions.server = initOptions.options.server || DEFAULT_EVENT_URL;
  initOptions.configServer = initOptions.options.configServer || (initOptions.options.server || DEFAULT_SERVER_CONFIG_URL);
  initOptions.utmTagData = this.buildUtmTagData();
  initOptions.host = initOptions.options.host || window.location.hostname;
  initOptions.device = detectDevice();

  loadServerConfig();
  return isCorrectConfig;
};

roxotAdapter.getOptions = function () {
  return initOptions;
};

function registerEvent(eventType, eventName, data) {
  let eventData = {
    eventType: eventType,
    eventName: eventName,
    data: data
  };

  sendEventCache.push(eventData);

  _logInfo('Register event', eventData);

  (typeof initOptions.serverConfig === 'undefined') ? checkEventAfterTimeout() : checkSendEvent();
}

function checkSendEvent() {
  if (sendEventTimeoutId) {
    clearTimeout(sendEventTimeoutId);
    sendEventTimeoutId = null;
  }

  if (typeof initOptions.serverConfig === 'undefined') {
    checkEventAfterTimeout();
    return;
  }

  while (sendEventCache.length) {
    let event = sendEventCache.shift();
    let isNeedSend = initOptions.serverConfig[event.eventType] || 0;
    if (Number(isNeedSend) === 0) {
      _logInfo('Skip event ' + event.eventName, event);
      continue;
    }
    sendEvent(event.eventType, event.eventName, event.data);
  }
}

function checkEventAfterTimeout() {
  if (sendEventTimeoutId) {
    return;
  }

  sendEventTimeoutId = setTimeout(checkSendEvent, sendEventTimeoutTime);
}

function sendEvent(eventType, eventName, data) {
  let url = 'https://' + initOptions.server + '/' + eventType + '?publisherId=' + initOptions.publisherId + '&host=' + initOptions.host;
  let eventData = {
    'event': eventType,
    'eventName': eventName,
    'options': initOptions,
    'data': data
  };

  ajax(
    url,
    function () {
      _logInfo(eventName + ' sent', eventData);
    },
    JSON.stringify(eventData),
    {
      contentType: 'text/plain',
      method: 'POST',
      withCredentials: true
    }
  );
}

function loadServerConfig() {
  let url = 'https://' + initOptions.configServer + '/c' + '?publisherId=' + initOptions.publisherId + '&host=' + initOptions.host;
  ajax(
    url,
    {
      'success': function (data) {
        initOptions.serverConfig = JSON.parse(data);
      },
      'error': function () {
        initOptions.serverConfig = {};
        initOptions.serverConfig[ROXOT_EVENTS.AUCTION] = 1;
        initOptions.serverConfig[ROXOT_EVENTS.IMPRESSION] = 1;
        initOptions.serverConfig[ROXOT_EVENTS.BID_AFTER_TIMEOUT] = 1;
        initOptions.serverConfig['isError'] = 1;
      }
    },
    null,
    {
      contentType: 'text/json',
      method: 'GET',
      withCredentials: true
    }
  );
}

function _logInfo(message, meta) {
  logInfo(buildLogMessage(message), meta);
}

function _logError(message) {
  logError(buildLogMessage(message));
}

function buildLogMessage(message) {
  return 'Roxot Prebid Analytics: ' + message;
}

adapterManager.registerAnalyticsAdapter({
  adapter: roxotAdapter,
  code: MODULE_CODE,
});

export default roxotAdapter;