prebid/Prebid.js

View on GitHub
modules/adlooxAnalyticsAdapter.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * This module provides [Adloox]{@link https://www.adloox.com/} Analytics
 * The module will inject Adloox's verification JS tag alongside slot at bidWin
 * @module modules/adlooxAnalyticsAdapter
 */

import adapterManager from '../src/adapterManager.js';
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import {loadExternalScript} from '../src/adloader.js';
import {auctionManager} from '../src/auctionManager.js';
import {AUCTION_COMPLETED} from '../src/auction.js';
import {EVENTS} from '../src/constants.js';
import {find} from '../src/polyfill.js';
import {getRefererInfo} from '../src/refererDetection.js';
import {
  deepAccess,
  getUniqueIdentifierStr,
  insertElement,
  isFn,
  isNumber,
  isPlainObject,
  isStr,
  logError,
  logInfo,
  logMessage,
  logWarn,
  mergeDeep,
  parseUrl
} from '../src/utils.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

const MODULE = 'adlooxAnalyticsAdapter';

const URL_JS = 'https://j.adlooxtracking.com/ads/js/tfav_adl_%%clientid%%.js';

const ADLOOX_VENDOR_ID = 93;

const ADLOOX_MEDIATYPE = {
  DISPLAY: 2,
  VIDEO: 6
};

const MACRO = {};
MACRO['client'] = function(b, c) {
  return c.client;
};
MACRO['clientid'] = function(b, c) {
  return c.clientid;
};
MACRO['tagid'] = function(b, c) {
  return c.tagid;
};
MACRO['platformid'] = function(b, c) {
  return c.platformid;
};
MACRO['targetelt'] = function(b, c) {
  return c.toselector(b);
};
MACRO['creatype'] = function(b, c) {
  return b.mediaType == 'video' ? ADLOOX_MEDIATYPE.VIDEO : ADLOOX_MEDIATYPE.DISPLAY;
};
MACRO['pageurl'] = function(b, c) {
  const refererInfo = getRefererInfo();
  return (refererInfo.page || '').substr(0, 300).split(/[?#]/)[0];
};
MACRO['gpid'] = function(b, c) {
  const adUnit = find(auctionManager.getAdUnits(), a => b.adUnitCode === a.code);
  return deepAccess(adUnit, 'ortb2Imp.ext.gpid') || deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot') || getGptSlotInfoForAdUnitCode(b.adUnitCode).gptSlot || b.adUnitCode;
};
MACRO['pbAdSlot'] = MACRO['pbadslot'] = MACRO['gpid']; // legacy

const PARAMS_DEFAULT = {
  'id1': function(b) { return b.adUnitCode },
  'id2': '%%gpid%%',
  'id3': function(b) { return b.bidder },
  'id4': function(b) { return b.adId },
  'id5': function(b) { return b.dealId },
  'id6': function(b) { return b.creativeId },
  'id7': function(b) { return b.size },
  'id11': '$ADLOOX_WEBSITE'
};

const NOOP = function() {};

let analyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), {
  track({ eventType, args }) {
    if (!analyticsAdapter[`handle_${eventType}`]) return;

    logInfo(MODULE, 'track', eventType, args);

    analyticsAdapter[`handle_${eventType}`](args);
  }
});

analyticsAdapter.context = null;

analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics;
analyticsAdapter.enableAnalytics = function(config) {
  analyticsAdapter.context = null;

  logInfo(MODULE, 'config', config);

  if (!isPlainObject(config.options)) {
    logError(MODULE, 'missing options');
    return;
  }
  if (!(config.options.js === undefined || isStr(config.options.js))) {
    logError(MODULE, 'invalid js options value');
    return;
  }
  if (!(config.options.toselector === undefined || isFn(config.options.toselector))) {
    logError(MODULE, 'invalid toselector options value');
    return;
  }
  if (!isStr(config.options.client)) {
    logError(MODULE, 'invalid client options value');
    return;
  }
  if (!isNumber(config.options.clientid)) {
    logError(MODULE, 'invalid clientid options value');
    return;
  }
  if (!isNumber(config.options.tagid)) {
    logError(MODULE, 'invalid tagid options value');
    return;
  }
  if (!isNumber(config.options.platformid)) {
    logError(MODULE, 'invalid platformid options value');
    return;
  }
  if (!(config.options.params === undefined || isPlainObject(config.options.params))) {
    logError(MODULE, 'invalid params options value');
    return;
  }

  analyticsAdapter.context = {
    js: config.options.js || URL_JS,
    toselector: config.options.toselector || function(bid) {
      let code = getGptSlotInfoForAdUnitCode(bid.adUnitCode).divId || bid.adUnitCode;
      // https://mathiasbynens.be/notes/css-escapes
      try {
        code = CSS.escape(code);
      } catch (_) {
        code = code.replace(/^\d/, '\\3$& ');
      }
      return `#${code}`
    },
    client: config.options.client,
    clientid: config.options.clientid,
    tagid: config.options.tagid,
    platformid: config.options.platformid,
    params: []
  };

  config.options.params = mergeDeep({}, PARAMS_DEFAULT, config.options.params || {});
  Object
    .keys(config.options.params)
    .forEach(k => {
      if (!Array.isArray(config.options.params[k])) {
        config.options.params[k] = [ config.options.params[k] ];
      }
      config.options.params[k].forEach(v => analyticsAdapter.context.params.push([ k, v ]));
    });

  Object.keys(COMMAND_QUEUE).forEach(commandProcess);

  analyticsAdapter.originEnableAnalytics(config);
}

analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics;
analyticsAdapter.disableAnalytics = function() {
  analyticsAdapter.context = null;

  analyticsAdapter.originDisableAnalytics();
}

analyticsAdapter.url = function(url, args, bid) {
  // utils.formatQS outputs PHP encoded querystrings... (╯°□°)╯ ┻━┻
  function a2qs(a) {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
    function fixedEncodeURIComponent(str) {
      return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
        return '%' + c.charCodeAt(0).toString(16);
      });
    }

    const args = [];
    let n = a.length;
    while (n-- > 0) {
      if (!(a[n][1] === undefined || a[n][1] === null || a[n][1] === false)) {
        args.unshift(fixedEncodeURIComponent(a[n][0]) + (a[n][1] !== true ? ('=' + fixedEncodeURIComponent(a[n][1])) : ''));
      }
    }

    return args.join('&');
  }

  const macros = (str) => {
    return str.replace(/%%([a-z]+)%%/gi, (match, p1) => MACRO[p1] ? MACRO[p1](bid, analyticsAdapter.context) : match);
  };

  url = macros(url);
  args = args || [];

  let n = args.length;
  while (n-- > 0) {
    if (isFn(args[n][1])) {
      try {
        args[n][1] = args[n][1](bid);
      } catch (_) {
        logError(MODULE, 'macro', args[n][0], _.message);
        args[n][1] = `ERROR: ${_.message}`;
      }
    }
    if (isStr(args[n][1])) {
      args[n][1] = macros(args[n][1]);
    }
  }

  return url + a2qs(args);
}

analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = function(auctionDetails) {
  if (!(auctionDetails.auctionStatus == AUCTION_COMPLETED && auctionDetails.bidsReceived.length > 0)) return;
  analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = NOOP;

  logMessage(MODULE, 'preloading verification JS');

  const uri = parseUrl(analyticsAdapter.url(`${analyticsAdapter.context.js}#`));

  const link = document.createElement('link');
  link.setAttribute('href', `${uri.protocol}://${uri.host}${uri.pathname}`);
  link.setAttribute('rel', 'preload');
  link.setAttribute('as', 'script');
  insertElement(link);
}

analyticsAdapter[`handle_${EVENTS.BID_WON}`] = function(bid) {
  if (deepAccess(bid, 'ext.adloox.video.adserver')) {
    logMessage(MODULE, `measuring '${bid.mediaType}' ad unit code '${bid.adUnitCode}' via Ad Server module`);
    return;
  }

  const sl = analyticsAdapter.context.toselector(bid);
  let el;
  try {
    el = document.querySelector(sl);
  } catch (_) { }
  if (!el) {
    logWarn(MODULE, `unable to find ad unit code '${bid.adUnitCode}' slot using selector '${sl}' (use options.toselector to change), ignoring`);
    return;
  }

  logMessage(MODULE, `measuring '${bid.mediaType}' unit at '${bid.adUnitCode}'`);

  const params = analyticsAdapter.context.params.concat([
    [ 'tagid', '%%tagid%%' ],
    [ 'platform', '%%platformid%%' ],
    [ 'fwtype', 4 ],
    [ 'targetelt', '%%targetelt%%' ],
    [ 'creatype', '%%creatype%%' ]
  ]);

  loadExternalScript(analyticsAdapter.url(`${analyticsAdapter.context.js}#`, params, bid), 'adloox');
}

adapterManager.registerAnalyticsAdapter({
  adapter: analyticsAdapter,
  code: 'adloox',
  gvlid: ADLOOX_VENDOR_ID
});

export default analyticsAdapter;

// src/events.js does not support custom events or handle races... (╯°□°)╯ ┻━┻
const COMMAND_QUEUE = {};
export const COMMAND = {
  CONFIG: 'config',
  TOSELECTOR: 'toselector',
  URL: 'url',
  TRACK: 'track'
};
export function command(cmd, data, callback0) {
  const cid = getUniqueIdentifierStr();
  const callback = function() {
    delete COMMAND_QUEUE[cid];
    if (callback0) callback0.apply(null, arguments);
  };
  COMMAND_QUEUE[cid] = { cmd, data, callback };
  if (analyticsAdapter.context) commandProcess(cid);
}
function commandProcess(cid) {
  const { cmd, data, callback } = COMMAND_QUEUE[cid];

  logInfo(MODULE, 'command', cmd, data);

  switch (cmd) {
    case COMMAND.CONFIG:
      const response = {
        client: analyticsAdapter.context.client,
        clientid: analyticsAdapter.context.clientid,
        tagid: analyticsAdapter.context.tagid,
        platformid: analyticsAdapter.context.platformid
      };
      callback(response);
      break;
    case COMMAND.TOSELECTOR:
      callback(analyticsAdapter.context.toselector(data.bid));
      break;
    case COMMAND.URL:
      if (data.ids) data.args = data.args.concat(analyticsAdapter.context.params.filter(p => /^id([1-9]|10)$/.test(p[0]))); // not >10
      callback(analyticsAdapter.url(data.url, data.args, data.bid));
      break;
    case COMMAND.TRACK:
      analyticsAdapter.track(data);
      callback(); // drain queue
      break;
    default:
      logWarn(MODULE, 'command unknown', cmd);
      // do not callback as arguments are unknown and to aid debugging
  }
}