prebid/Prebid.js

View on GitHub
modules/adagioBidAdapter.js

Summary

Maintainability
F
2 wks
Test Coverage
import {find} from '../src/polyfill.js';
import {
  cleanObj,
  deepAccess,
  deepClone,
  generateUUID,
  getDNT,
  getUniqueIdentifierStr,
  getWindowSelf,
  getWindowTop,
  inIframe,
  isArray,
  isFn,
  isInteger,
  isNumber,
  isArrayOfNums,
  logError,
  logInfo,
  logWarn,
  mergeDeep,
  isStr,
} from '../src/utils.js';
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {loadExternalScript} from '../src/adloader.js';
import {verify} from 'criteo-direct-rsa-validate/build/verify.js';
import {getStorageManager} from '../src/storageManager.js';
import {getRefererInfo, parseDomain} from '../src/refererDetection.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import {Renderer} from '../src/Renderer.js';
import {OUTSTREAM} from '../src/video.js';
import { getGlobal } from '../src/prebidGlobal.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';
import { userSync } from '../src/userSync.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';

const BIDDER_CODE = 'adagio';
const LOG_PREFIX = 'Adagio:';
const FEATURES_VERSION = '1';
export const ENDPOINT = 'https://mp.4dex.io/prebid';
const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO];
const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js';
const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript';
const GVLID = 617;
export const storage = getStorageManager({bidderCode: BIDDER_CODE});

const BB_PUBLICATION = 'adagio';
const BB_RENDERER_DEFAULT = 'renderer';
export const BB_RENDERER_URL = `https://${BB_PUBLICATION}.bbvms.com/r/$RENDERER.js`;

const MAX_SESS_DURATION = 30 * 60 * 1000;
const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp';
const ADAGIO_PUBKEY_E = 65537;
const CURRENCY = 'USD';

// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP.
// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext'
// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf
export const ORTB_VIDEO_PARAMS = {
  'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'),
  'minduration': (value) => isInteger(value),
  'maxduration': (value) => isInteger(value),
  'protocols': (value) => isArrayOfNums(value),
  'w': (value) => isInteger(value),
  'h': (value) => isInteger(value),
  'startdelay': (value) => isInteger(value),
  'placement': (value) => isInteger(value),
  'linearity': (value) => isInteger(value),
  'skip': (value) => [1, 0].includes(value),
  'skipmin': (value) => isInteger(value),
  'skipafter': (value) => isInteger(value),
  'sequence': (value) => isInteger(value),
  'battr': (value) => isArrayOfNums(value),
  'maxextended': (value) => isInteger(value),
  'minbitrate': (value) => isInteger(value),
  'maxbitrate': (value) => isInteger(value),
  'boxingallowed': (value) => isInteger(value),
  'playbackmethod': (value) => isArrayOfNums(value),
  'playbackend': (value) => isInteger(value),
  'delivery': (value) => isArrayOfNums(value),
  'pos': (value) => isInteger(value),
  'api': (value) => isArrayOfNums(value)
};

let currentWindow;

export const GlobalExchange = (function() {
  let features;
  let exchangeData = {};

  return {
    clearFeatures: function() {
      features = undefined;
    },

    clearExchangeData: function() {
      exchangeData = {};
    },

    getOrSetGlobalFeatures: function () {
      if (!features) {
        features = {
          page_dimensions: getPageDimensions().toString(),
          viewport_dimensions: getViewPortDimensions().toString(),
          user_timestamp: getTimestampUTC().toString(),
          dom_loading: getDomLoadingDuration().toString(),
        }
      }
      return features;
    },

    prepareExchangeData(storageValue) {
      const adagioStorage = JSON.parse(storageValue, function(name, value) {
        if (name.charAt(0) !== '_' || name === '') {
          return value;
        }
      });
      let random = deepAccess(adagioStorage, 'session.rnd');
      let newSession = false;

      if (internal.isNewSession(adagioStorage)) {
        newSession = true;
        random = Math.random();
      }

      const data = {
        session: {
          new: newSession,
          rnd: random
        }
      }

      mergeDeep(exchangeData, adagioStorage, data);

      internal.enqueue({
        action: 'session',
        ts: Date.now(),
        data: exchangeData
      });
    },

    getExchangeData() {
      return exchangeData
    }
  };
})();

export function adagioScriptFromLocalStorageCb(ls) {
  try {
    if (!ls) {
      logWarn(`${LOG_PREFIX} script not found.`);
      return;
    }

    const hashRgx = /^(\/\/ hash: (.+)\n)(.+\n)$/;

    if (!hashRgx.test(ls)) {
      logWarn(`${LOG_PREFIX} no hash found.`);
      storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY);
    } else {
      const r = ls.match(hashRgx);
      const hash = r[2];
      const content = r[3];

      if (verify(content, hash, ADAGIO_PUBKEY, ADAGIO_PUBKEY_E)) {
        logInfo(`${LOG_PREFIX} start script.`);
        Function(ls)(); // eslint-disable-line no-new-func
      } else {
        logWarn(`${LOG_PREFIX} invalid script found.`);
        storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY);
      }
    }
  } catch (err) {
    logError(LOG_PREFIX, err);
  }
}

export function getAdagioScript() {
  storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => {
    internal.adagioScriptFromLocalStorageCb(ls);
  });

  storage.localStorageIsEnabled(isValid => {
    if (isValid) {
      loadExternalScript(ADAGIO_TAG_URL, BIDDER_CODE);
    } else {
      // Try-catch to avoid error when 3rd party cookies is disabled (e.g. in privacy mode)
      try {
        // ensure adagio removing for next time.
        // It's an antipattern regarding the TCF2 enforcement logic
        // but it's the only way to respect the user choice update.
        window.localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY);
        // Extra data from external script.
        // This key is removed only if localStorage is not accessible.
        window.localStorage.removeItem('adagio');
      } catch (e) {
        logInfo(`${LOG_PREFIX} unable to clear Adagio scripts from localstorage.`);
      }
    }
  });
}

function canAccessTopWindow() {
  try {
    if (getWindowTop().location.href) {
      return true;
    }
  } catch (error) {
    return false;
  }
}

function getCurrentWindow() {
  return currentWindow || getWindowSelf();
}

function isSafeFrameWindow() {
  const ws = getWindowSelf();
  return !!(ws.$sf && ws.$sf.ext);
}

function initAdagio() {
  if (canAccessTopWindow()) {
    currentWindow = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf();
  }

  const w = internal.getCurrentWindow();

  w.ADAGIO = w.ADAGIO || {};
  w.ADAGIO.adUnits = w.ADAGIO.adUnits || {};
  w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || [];
  w.ADAGIO.queue = w.ADAGIO.queue || [];
  w.ADAGIO.versions = w.ADAGIO.versions || {};
  w.ADAGIO.versions.pbjs = '$prebid.version$';
  w.ADAGIO.isSafeFrameWindow = isSafeFrameWindow();

  storage.getDataFromLocalStorage('adagio', (storageData) => {
    try {
      GlobalExchange.prepareExchangeData(storageData);
    } catch (e) {
      logError(LOG_PREFIX, e);
    }
  });

  getAdagioScript();
}

function enqueue(ob) {
  const w = internal.getCurrentWindow();

  w.ADAGIO = w.ADAGIO || {};
  w.ADAGIO.queue = w.ADAGIO.queue || [];
  w.ADAGIO.queue.push(ob);
};

function getPageviewId() {
  const w = internal.getCurrentWindow();

  w.ADAGIO = w.ADAGIO || {};
  w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID();

  return w.ADAGIO.pageviewId;
};

function getDevice() {
  const language = navigator.language ? 'language' : 'userLanguage';
  return {
    userAgent: navigator.userAgent,
    language: navigator[language],
    dnt: getDNT() ? 1 : 0,
    geo: {},
    js: 1
  };
};

function getSite(bidderRequest) {
  const { refererInfo } = bidderRequest;
  return {
    domain: parseDomain(refererInfo.topmostLocation) || '',
    page: refererInfo.topmostLocation || '',
    referrer: refererInfo.ref || getWindowSelf().document.referrer || '',
    top: refererInfo.reachedTop
  };
};

function getElementFromTopWindow(element, currentWindow) {
  try {
    if (getWindowTop() === currentWindow) {
      if (!element.getAttribute('id')) {
        element.setAttribute('id', `adg-${getUniqueIdentifierStr()}`);
      }
      return element;
    } else {
      const frame = currentWindow.frameElement;
      const frameClientRect = frame.getBoundingClientRect();
      const elementClientRect = element.getBoundingClientRect();

      if (frameClientRect.width !== elementClientRect.width || frameClientRect.height !== elementClientRect.height) {
        return false;
      }

      return getElementFromTopWindow(frame, currentWindow.parent);
    }
  } catch (err) {
    logWarn(`${LOG_PREFIX}`, err);
    return false;
  }
};

function autoDetectAdUnitElementIdFromGpt(adUnitCode) {
  const autoDetectedAdUnit = getGptSlotInfoForAdUnitCode(adUnitCode);

  if (autoDetectedAdUnit.divId) {
    return autoDetectedAdUnit.divId;
  }
};

function isRendererPreferredFromPublisher(bidRequest) {
  // renderer defined at adUnit level
  const adUnitRenderer = deepAccess(bidRequest, 'renderer');
  const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render);

  // renderer defined at adUnit.mediaTypes level
  const mediaTypeRenderer = deepAccess(bidRequest, 'mediaTypes.video.renderer');
  const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render);

  return !!(
    (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) ||
    (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true))
  );
}

/**
 *
 * @param {object} adagioStorage
 * @returns {boolean}
 */
function isNewSession(adagioStorage) {
  const now = Date.now();
  const { lastActivityTime, vwSmplg } = deepAccess(adagioStorage, 'session', {});
  return (
    !isNumber(lastActivityTime) ||
    !isNumber(vwSmplg) ||
    (now - lastActivityTime) > MAX_SESS_DURATION
  )
}

function setPlayerName(bidRequest) {
  const playerName = (internal.isRendererPreferredFromPublisher(bidRequest)) ? 'other' : 'adagio';

  if (playerName === 'other') {
    logWarn(`${LOG_PREFIX} renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.`);
  }

  return playerName;
}

export const internal = {
  enqueue,
  getPageviewId,
  getDevice,
  getSite,
  getElementFromTopWindow,
  getRefererInfo,
  adagioScriptFromLocalStorageCb,
  getCurrentWindow,
  canAccessTopWindow,
  isRendererPreferredFromPublisher,
  isNewSession
};

function _getGdprConsent(bidderRequest) {
  if (!deepAccess(bidderRequest, 'gdprConsent')) {
    return false;
  }

  const {
    apiVersion,
    gdprApplies,
    consentString,
    allowAuctionWithoutConsent
  } = bidderRequest.gdprConsent;

  return cleanObj({
    apiVersion,
    consentString,
    consentRequired: gdprApplies ? 1 : 0,
    allowAuctionWithoutConsent: allowAuctionWithoutConsent ? 1 : 0
  });
}

function _getCoppa() {
  return {
    required: config.getConfig('coppa') === true ? 1 : 0
  };
}

function _getUspConsent(bidderRequest) {
  return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false;
}

function _getGppConsent(bidderRequest) {
  let gpp = deepAccess(bidderRequest, 'gppConsent.gppString')
  let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections')

  if (!gpp || !gppSid) {
    gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '')
    gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', [])
  }
  return { gpp, gppSid }
}

function _getSchain(bidRequest) {
  return deepAccess(bidRequest, 'schain');
}

function _getEids(bidRequest) {
  if (deepAccess(bidRequest, 'userIdAsEids')) {
    return bidRequest.userIdAsEids;
  }
}

function _buildVideoBidRequest(bidRequest) {
  const videoAdUnitParams = deepAccess(bidRequest, 'mediaTypes.video', {});
  const videoBidderParams = deepAccess(bidRequest, 'params.video', {});
  const computedParams = {};

  // Special case for playerSize.
  // Eeach props will be overrided if they are defined in config.
  if (Array.isArray(videoAdUnitParams.playerSize)) {
    const tempSize = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize;
    computedParams.w = tempSize[0];
    computedParams.h = tempSize[1];
  }

  const videoParams = {
    ...computedParams,
    ...videoAdUnitParams,
    ...videoBidderParams
  };

  if (videoParams.context && videoParams.context === OUTSTREAM) {
    bidRequest.mediaTypes.video.playerName = setPlayerName(bidRequest);
  }

  // Only whitelisted OpenRTB options need to be validated.
  // Other options will still remain in the `mediaTypes.video` object
  // sent in the ad-request, but will be ignored by the SSP.
  Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => {
    if (videoParams.hasOwnProperty(paramName)) {
      if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) {
        bidRequest.mediaTypes.video[paramName] = videoParams[paramName];
      } else {
        delete bidRequest.mediaTypes.video[paramName];
        logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`);
      }
    }
  });
}

function _parseNativeBidResponse(bid) {
  if (!bid.admNative || !Array.isArray(bid.admNative.assets)) {
    logError(`${LOG_PREFIX} Invalid native response`);
    return;
  }

  const native = {}

  function addAssetDataValue(data) {
    const map = {
      1: 'sponsoredBy', // sponsored
      2: 'body', // desc
      3: 'rating',
      4: 'likes',
      5: 'downloads',
      6: 'price',
      7: 'salePrice',
      8: 'phone',
      9: 'address',
      10: 'body2', // desc2
      11: 'displayUrl',
      12: 'cta'
    }
    if (map.hasOwnProperty(data.type) && typeof data.value === 'string') {
      native[map[data.type]] = data.value;
    }
  }

  // assets
  bid.admNative.assets.forEach(asset => {
    if (asset.title) {
      native.title = asset.title.text
    } else if (asset.data) {
      addAssetDataValue(asset.data)
    } else if (asset.img) {
      switch (asset.img.type) {
        case 1:
          native.icon = {
            url: asset.img.url,
            width: asset.img.w,
            height: asset.img.h
          };
          break;
        default:
          native.image = {
            url: asset.img.url,
            width: asset.img.w,
            height: asset.img.h
          };
          break;
      }
    }
  });

  if (bid.admNative.link) {
    if (bid.admNative.link.url) {
      native.clickUrl = bid.admNative.link.url;
    }
    if (Array.isArray(bid.admNative.link.clicktrackers)) {
      native.clickTrackers = bid.admNative.link.clicktrackers
    }
  }

  if (Array.isArray(bid.admNative.eventtrackers)) {
    native.impressionTrackers = [];
    bid.admNative.eventtrackers.forEach(tracker => {
      // Only Impression events are supported. Prebid does not support Viewability events yet.
      if (tracker.event !== 1) {
        return;
      }

      // methods:
      // 1: image
      // 2: js
      // note: javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used.
      switch (tracker.method) {
        case 1:
          native.impressionTrackers.push(tracker.url);
          break;
        case 2:
          const script = `<script async src=\"${tracker.url}\"></script>`;
          if (!native.javascriptTrackers) {
            native.javascriptTrackers = script;
          } else {
            native.javascriptTrackers += `\n${script}`;
          }
          break;
      }
    });
  } else {
    native.impressionTrackers = Array.isArray(bid.admNative.imptrackers) ? bid.admNative.imptrackers : [];
    if (bid.admNative.jstracker) {
      native.javascriptTrackers = bid.admNative.jstracker;
    }
  }

  if (bid.admNative.privacy) {
    native.privacyLink = bid.admNative.privacy;
  }

  if (bid.admNative.ext) {
    native.ext = {}

    if (bid.admNative.ext.bvw) {
      native.ext.adagio_bvw = bid.admNative.ext.bvw;
    }
  }

  bid.native = native
}

// bidRequest param must be the `bidRequest` object with the original `auctionId` value.
function _getFloors(bidRequest) {
  if (!isFn(bidRequest.getFloor)) {
    return false;
  }

  const floors = [];

  const getAndPush = (mediaType, size) => {
    const info = bidRequest.getFloor({
      currency: CURRENCY,
      mediaType,
      size
    });

    floors.push(cleanObj({
      mt: mediaType,
      s: isArray(size) ? `${size[0]}x${size[1]}` : undefined,
      f: (!isNaN(info.floor) && info.currency === CURRENCY) ? info.floor : undefined
    }));
  }

  Object.keys(bidRequest.mediaTypes).forEach(mediaType => {
    if (SUPPORTED_MEDIA_TYPES.indexOf(mediaType) !== -1) {
      const sizeProp = mediaType === VIDEO ? 'playerSize' : 'sizes';

      if (bidRequest.mediaTypes[mediaType][sizeProp] && bidRequest.mediaTypes[mediaType][sizeProp].length) {
        if (isArray(bidRequest.mediaTypes[mediaType][sizeProp][0])) {
          bidRequest.mediaTypes[mediaType][sizeProp].forEach(size => {
            getAndPush(mediaType, [size[0], size[1]]);
          });
        } else {
          getAndPush(mediaType, [bidRequest.mediaTypes[mediaType][sizeProp][0], bidRequest.mediaTypes[mediaType][sizeProp][1]]);
        }
      } else {
        getAndPush(mediaType, '*');
      }
    }
  });

  return floors;
}

/**
 * Try to find the value of `paramName` and set it to adUnit.params if
 * it has not already been set.
 * This function will check through:
 * - bidderSettings object
 * - ortb2.site.ext.data FPD…
 *
 * @param {*} bid
 * @param {String} paramName
 */
export function setExtraParam(bid, paramName) {
  bid.params = bid.params || {};

  // eslint-disable-next-line
  if (!!(bid.params[paramName])) {
    return;
  }

  const adgGlobalConf = config.getConfig('adagio') || {};
  const ortb2Conf = bid.ortb2;

  const detected = adgGlobalConf[paramName] || deepAccess(ortb2Conf, `site.ext.data.${paramName}`, null);
  if (detected) {
    // First Party Data can be an array.
    // As we consider that params detected from FPD are fallbacks, we just keep the 1st value.
    if (Array.isArray(detected)) {
      if (detected.length) {
        bid.params[paramName] = detected[0].toString();
      }
      return;
    }

    bid.params[paramName] = detected.toString();
  }
}

function autoFillParams(bid) {
  // adUnitElementId …
  const adgGlobalConf = config.getConfig('adagio') || {};

  bid.params = bid.params || {};

  // adgGlobalConf.siteId is a shortcut to facilitate the integration for publisher.
  if (adgGlobalConf.siteId) {
    bid.params.organizationId = adgGlobalConf.siteId.split(':')[0];
    bid.params.site = adgGlobalConf.siteId.split(':')[1];
  }

  // Edge case. Useful when Prebid Manager cannot handle properly params setting…
  if (adgGlobalConf.useAdUnitCodeAsPlacement === true || bid.params.useAdUnitCodeAsPlacement === true) {
    bid.params.placement = bid.adUnitCode;
  }

  bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.elementId', null) || bid.params.adUnitElementId;

  if (!bid.params.adUnitElementId) {
    if (adgGlobalConf.useAdUnitCodeAsAdUnitElementId === true || bid.params.useAdUnitCodeAsAdUnitElementId === true) {
      bid.params.adUnitElementId = bid.adUnitCode;
    } else {
      bid.params.adUnitElementId = autoDetectAdUnitElementIdFromGpt(bid.adUnitCode);
    }
  }

  // extra params
  setExtraParam(bid, 'pagetype');
  setExtraParam(bid, 'category');
}

function getPageDimensions() {
  if (isSafeFrameWindow() || !canAccessTopWindow()) {
    return '';
  }

  // the page dimension can be computed on window.top only.
  const wt = getWindowTop();
  const body = wt.document.querySelector('body');

  if (!body) {
    return '';
  }
  const html = wt.document.documentElement;
  const pageWidth = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth);
  const pageHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);

  return `${pageWidth}x${pageHeight}`;
}

/**
 * @todo Move to prebid Core as Utils.
 * @returns
 */
function getViewPortDimensions() {
  if (!isSafeFrameWindow() && !canAccessTopWindow()) {
    return '';
  }

  const viewportDims = { w: 0, h: 0 };

  if (isSafeFrameWindow()) {
    const ws = getWindowSelf();

    if (typeof ws.$sf.ext.geom !== 'function') {
      logWarn(LOG_PREFIX, 'Unable to compute from safeframe api.');
      return '';
    }

    const sfGeom = ws.$sf.ext.geom();

    if (!sfGeom || !sfGeom.win) {
      logWarn(LOG_PREFIX, 'Unable to compute from safeframe api. Missing `geom().win` property');
      return '';
    }

    viewportDims.w = Math.round(sfGeom.w);
    viewportDims.h = Math.round(sfGeom.h);
  } else {
    // window.top based computing
    const wt = getWindowTop();
    viewportDims.w = wt.innerWidth;
    viewportDims.h = wt.innerHeight;
  }

  return `${viewportDims.w}x${viewportDims.h}`;
}

function getSlotPosition(adUnitElementId) {
  if (!adUnitElementId) {
    return '';
  }

  if (!isSafeFrameWindow() && !canAccessTopWindow()) {
    return '';
  }

  const position = { x: 0, y: 0 };

  if (isSafeFrameWindow()) {
    const ws = getWindowSelf();

    if (typeof ws.$sf.ext.geom !== 'function') {
      logWarn(LOG_PREFIX, 'Unable to compute from safeframe api.');
      return '';
    }

    const sfGeom = ws.$sf.ext.geom();

    if (!sfGeom || !sfGeom.self) {
      logWarn(LOG_PREFIX, 'Unable to compute from safeframe api. Missing `geom().self` property');
      return '';
    }

    position.x = Math.round(sfGeom.t);
    position.y = Math.round(sfGeom.l);
  } else if (canAccessTopWindow()) {
    try {
      // window.top based computing
      const wt = getWindowTop();
      const d = wt.document;

      let domElement;

      if (inIframe() === true) {
        const ws = getWindowSelf();
        const currentElement = ws.document.getElementById(adUnitElementId);
        domElement = internal.getElementFromTopWindow(currentElement, ws);
      } else {
        domElement = wt.document.getElementById(adUnitElementId);
      }

      if (!domElement) {
        return '';
      }

      let box = domElement.getBoundingClientRect();

      const docEl = d.documentElement;
      const body = d.body;
      const clientTop = d.clientTop || body.clientTop || 0;
      const clientLeft = d.clientLeft || body.clientLeft || 0;
      const scrollTop = wt.pageYOffset || docEl.scrollTop || body.scrollTop;
      const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft;

      const elComputedStyle = wt.getComputedStyle(domElement, null);
      const mustDisplayElement = elComputedStyle.display === 'none';

      if (mustDisplayElement) {
        logWarn(LOG_PREFIX, 'The element is hidden. The slot position cannot be computed.');
      }

      position.x = Math.round(box.left + scrollLeft - clientLeft);
      position.y = Math.round(box.top + scrollTop - clientTop);
    } catch (err) {
      logError(LOG_PREFIX, err);
      return '';
    }
  } else {
    return '';
  }

  return `${position.x}x${position.y}`;
}

function getTimestampUTC() {
  // timestamp returned in seconds
  return Math.floor(new Date().getTime() / 1000) - new Date().getTimezoneOffset() * 60;
}

function getPrintNumber(adUnitCode, bidderRequest) {
  if (!bidderRequest.bids || !bidderRequest.bids.length) {
    return 1;
  }
  const adagioBid = find(bidderRequest.bids, bid => bid.adUnitCode === adUnitCode);
  return adagioBid.bidderRequestsCount || 1;
}

/**
 * domLoading feature is computed on window.top if reachable.
 */
function getDomLoadingDuration() {
  let domLoadingDuration = -1;
  let performance;

  performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance;

  if (performance && performance.timing && performance.timing.navigationStart > 0) {
    const val = performance.timing.domLoading - performance.timing.navigationStart;
    if (val > 0) {
      domLoadingDuration = val;
    }
  }

  return domLoadingDuration;
}

function storeRequestInAdagioNS(bidRequest) {
  const w = getCurrentWindow();
  // Store adUnits config.
  // If an adUnitCode has already been stored, it will be replaced.
  w.ADAGIO = w.ADAGIO || {};
  w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== bidRequest.adUnitCode);

  let printNumber
  if (bidRequest.features && bidRequest.features.print_number) {
    printNumber = bidRequest.features.print_number;
  } else if (bidRequest.params.features && bidRequest.params.features.print_number) {
    printNumber = bidRequest.params.features.print_number;
  }

  w.ADAGIO.pbjsAdUnits.push({
    code: bidRequest.adUnitCode,
    mediaTypes: bidRequest.mediaTypes || {},
    sizes: (bidRequest.mediaTypes && bidRequest.mediaTypes.banner && Array.isArray(bidRequest.mediaTypes.banner.sizes)) ? bidRequest.mediaTypes.banner.sizes : bidRequest.sizes,
    bids: [{
      bidder: bidRequest.bidder,
      params: bidRequest.params // use the updated bid.params object with auto-detected params
    }],
    auctionId: bidRequest.auctionId, // this auctionId has been generated by adagioBidAdapter
    pageviewId: internal.getPageviewId(),
    printNumber,
    localPbjs: '$$PREBID_GLOBAL$$',
    localPbjsRef: getGlobal()
  });

  // (legacy) Store internal adUnit information
  w.ADAGIO.adUnits[bidRequest.adUnitCode] = {
    auctionId: bidRequest.auctionId, // this auctionId has been generated by adagioBidAdapter
    pageviewId: internal.getPageviewId(),
    printNumber,
  };
}

// See https://support.bluebillywig.com/developers/vast-renderer/
const OUTSTREAM_RENDERER = {
  bootstrapPlayer: function(bid) {
    const rendererCode = bid.outstreamRendererCode;

    const config = {
      code: bid.adUnitCode,
    };

    if (bid.vastXml) {
      config.vastXml = bid.vastXml;
    } else if (bid.vastUrl) {
      config.vastUrl = bid.vastUrl;
    }

    if (!bid.vastXml && !bid.vastUrl) {
      logError(`${LOG_PREFIX} no vastXml or vastUrl on bid`);
      return;
    }

    if (!window.bluebillywig || !window.bluebillywig.renderers || !window.bluebillywig.renderers.length) {
      logError(`${LOG_PREFIX} no BlueBillywig renderers found!`);
      return;
    }

    const rendererId = this.getRendererId(BB_PUBLICATION, rendererCode);

    const override = {}
    if (bid.skipOffset) {
      override.skipOffset = bid.skipOffset.toString()
    }

    const renderer = window.bluebillywig.renderers.find(bbr => bbr._id === rendererId);
    if (!renderer) {
      logError(`${LOG_PREFIX} couldn't find a renderer with ID ${rendererId}`);
      return;
    }

    const el = document.getElementById(bid.adUnitCode);

    renderer.bootstrap(config, el, override);
  },
  newRenderer: function(adUnitCode, rendererCode) {
    const rendererUrl = BB_RENDERER_URL.replace('$RENDERER', rendererCode);

    const renderer = Renderer.install({
      url: rendererUrl,
      loaded: false,
      adUnitCode
    });

    try {
      renderer.setRender(this.outstreamRender);
    } catch (err) {
      logError(`${LOG_PREFIX} error trying to setRender`, err);
    }

    return renderer;
  },
  outstreamRender: function(bid) {
    bid.renderer.push(() => {
      OUTSTREAM_RENDERER.bootstrapPlayer(bid)
    });
  },
  getRendererId: function(publication, renderer) {
    // By convention, the RENDERER_ID is always the publication name (adagio) and the ad unit code (eg. renderer)
    // joined together by a dash. It's used to identify the correct renderer instance on the page in case there's multiple.
    return `${publication}-${renderer}`;
  }
};

export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  supportedMediaTypes: SUPPORTED_MEDIA_TYPES,

  isBidRequestValid(bid) {
    bid.params = bid.params || {};

    autoFillParams(bid);

    if (!(bid.params.organizationId && bid.params.site && bid.params.placement)) {
      logWarn(`${LOG_PREFIX} at least one required param is missing.`);
      // internal.enqueue(debugData());
      return false;
    }

    return true;
  },

  buildRequests(validBidRequests, bidderRequest) {
    // convert Native ORTB definition to old-style prebid native definition
    validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests);

    const secure = (location.protocol === 'https:') ? 1 : 0;
    const device = internal.getDevice();
    const site = internal.getSite(bidderRequest);
    const pageviewId = internal.getPageviewId();
    const gdprConsent = _getGdprConsent(bidderRequest) || {};
    const uspConsent = _getUspConsent(bidderRequest) || {};
    const coppa = _getCoppa();
    const gppConsent = _getGppConsent(bidderRequest)
    const schain = _getSchain(validBidRequests[0]);
    const eids = _getEids(validBidRequests[0]) || [];
    const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled')
    const usIfr = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio')

    // We don't validate the dsa object in adapter and let our server do it.
    const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa');

    const aucId = generateUUID()

    const adUnits = validBidRequests.map(rawBidRequest => {
      const bidRequest = deepClone(rawBidRequest);

      // Fix https://github.com/prebid/Prebid.js/issues/9781
      bidRequest.auctionId = aucId

      const globalFeatures = GlobalExchange.getOrSetGlobalFeatures();
      const features = {
        ...globalFeatures,
        print_number: getPrintNumber(bidRequest.adUnitCode, bidderRequest).toString(),
        adunit_position: getSlotPosition(bidRequest.params.adUnitElementId) // adUnitElementId à déplacer ???
      };

      // Force the Split Keyword to be a String
      if (bidRequest.params.splitKeyword) {
        if (isStr(bidRequest.params.splitKeyword) || isNumber(bidRequest.params.splitKeyword)) {
          bidRequest.params.splitKeyword = bidRequest.params.splitKeyword.toString();
        } else {
          delete bidRequest.params.splitKeyword;

          logWarn(LOG_PREFIX, 'The splitKeyword param have been removed because the type is invalid, accepted type: number or string.');
        }
      }

      // Enforce the organizationId param to be a string
      bidRequest.params.organizationId = bidRequest.params.organizationId.toString();

      // Force the Data Layer key and value to be a String
      if (bidRequest.params.dataLayer) {
        if (isStr(bidRequest.params.dataLayer) || isNumber(bidRequest.params.dataLayer) || isArray(bidRequest.params.dataLayer) || isFn(bidRequest.params.dataLayer)) {
          logWarn(LOG_PREFIX, 'The dataLayer param is invalid, only object is accepted as a type.');
          delete bidRequest.params.dataLayer;
        } else {
          let invalidDlParam = false;

          bidRequest.params.dl = bidRequest.params.dataLayer
          // Remove the dataLayer from the BidRequest to send the `dl` instead of the `dataLayer`
          delete bidRequest.params.dataLayer

          Object.keys(bidRequest.params.dl).forEach((key) => {
            if (bidRequest.params.dl[key]) {
              if (isStr(bidRequest.params.dl[key]) || isNumber(bidRequest.params.dl[key])) {
                bidRequest.params.dl[key] = bidRequest.params.dl[key].toString();
              } else {
                invalidDlParam = true;
                delete bidRequest.params.dl[key];
              }
            }
          });

          if (invalidDlParam) {
            logWarn(LOG_PREFIX, 'Some parameters of the dataLayer property have been removed because the type is invalid, accepted type: number or string.');
          }
        }
      }

      Object.keys(features).forEach((prop) => {
        if (features[prop] === '') {
          delete features[prop];
        }
      });

      bidRequest.features = features;

      internal.enqueue({
        action: 'features',
        ts: Date.now(),
        data: {
          features: bidRequest.features,
          params: bidRequest.params,
          adUnitCode: bidRequest.adUnitCode
        }
      });

      // Handle priceFloors module
      // We need to use `rawBidRequest` as param because:
      // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079)
      // - the priceFloors.getFloor() uses a `_floorDataForAuction` map to store the floors based on the auctionId.
      const computedFloors = _getFloors(rawBidRequest);
      if (isArray(computedFloors) && computedFloors.length) {
        bidRequest.floors = computedFloors

        if (deepAccess(bidRequest, 'mediaTypes.banner')) {
          const bannerObj = bidRequest.mediaTypes.banner

          const computeNewSizeArray = (sizeArr = []) => {
            const size = { size: sizeArr, floor: null }
            const bannerFloors = bidRequest.floors.filter(floor => floor.mt === BANNER)
            const BannerSizeFloor = bannerFloors.find(floor => floor.s === sizeArr.join('x'))
            size.floor = (bannerFloors) ? (BannerSizeFloor) ? BannerSizeFloor.f : bannerFloors[0].f : null
            return size
          }

          // `bannerSizes`, internal property name
          bidRequest.mediaTypes.banner.bannerSizes = (isArray(bannerObj.sizes[0]))
            ? bannerObj.sizes.map(sizeArr => {
              return computeNewSizeArray(sizeArr)
            })
            : computeNewSizeArray(bannerObj.sizes)
        }

        if (deepAccess(bidRequest, 'mediaTypes.video')) {
          const videoObj = bidRequest.mediaTypes.video
          const videoFloors = bidRequest.floors.filter(floor => floor.mt === VIDEO);
          const playerSize = (videoObj.playerSize && isArray(videoObj.playerSize[0])) ? videoObj.playerSize[0] : videoObj.playerSize
          const videoSizeFloor = (playerSize) ? videoFloors.find(floor => floor.s === playerSize.join('x')) : undefined

          bidRequest.mediaTypes.video.floor = (videoFloors) ? videoSizeFloor ? videoSizeFloor.f : videoFloors[0].f : null
        }

        if (deepAccess(bidRequest, 'mediaTypes.native')) {
          const nativeFloors = bidRequest.floors.filter(floor => floor.mt === NATIVE);
          if (nativeFloors.length) {
            bidRequest.mediaTypes.native.floor = nativeFloors[0].f
          }
        }
      }

      if (deepAccess(bidRequest, 'mediaTypes.video')) {
        _buildVideoBidRequest(bidRequest);
      }

      const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot');
      if (gpid) {
        bidRequest.gpid = gpid;
      }

      // store the whole bidRequest (adUnit) object in the ADAGIO namespace.
      storeRequestInAdagioNS(bidRequest);

      // Remove some params that are not needed on the server side.
      delete bidRequest.params.siteId;

      // whitelist the fields that are allowed to be sent to the server.
      const adUnit = {
        adUnitCode: bidRequest.adUnitCode,
        auctionId: bidRequest.auctionId,
        bidder: bidRequest.bidder,
        bidId: bidRequest.bidId,
        params: bidRequest.params,
        features: bidRequest.features,
        gpid: bidRequest.gpid,
        mediaTypes: bidRequest.mediaTypes,
        nativeParams: bidRequest.nativeParams,
        score: bidRequest.score,
        transactionId: bidRequest.transactionId,
      }

      return adUnit;
    });

    // Group ad units by organizationId
    const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => {
      const organizationId = adUnit.params.organizationId

      groupedAdUnits[organizationId] = groupedAdUnits[organizationId] || [];
      groupedAdUnits[organizationId].push(adUnit);

      return groupedAdUnits;
    }, {});

    // Adding more params on the original bid object.
    // Those params are not sent to the server.
    // They are used for further operations on analytics adapter.
    validBidRequests.forEach(rawBidRequest => {
      rawBidRequest.params.adagioAuctionId = aucId
      rawBidRequest.params.pageviewId = pageviewId
    });

    // Build one request per organizationId
    const requests = Object.keys(groupedAdUnits).map(organizationId => {
      return {
        method: 'POST',
        url: ENDPOINT,
        data: {
          organizationId: organizationId,
          secure: secure,
          device: device,
          site: site,
          pageviewId: pageviewId,
          adUnits: groupedAdUnits[organizationId],
          data: GlobalExchange.getExchangeData(),
          regs: {
            gdpr: gdprConsent,
            coppa: coppa,
            ccpa: uspConsent,
            gpp: gppConsent.gpp,
            gppSid: gppConsent.gppSid,
            dsa: dsa // populated if exists
          },
          schain: schain,
          user: {
            eids: eids
          },
          prebidVersion: '$prebid.version$',
          featuresVersion: FEATURES_VERSION,
          usIfr: usIfr,
          adgjs: storage.localStorageIsEnabled()
        },
        options: {
          contentType: 'text/plain'
        }
      };
    });

    return requests;
  },

  interpretResponse(serverResponse, bidRequest) {
    let bidResponses = [];
    try {
      const response = serverResponse.body;
      if (response) {
        if (response.data) {
          internal.enqueue({
            action: 'ssp-data',
            ts: Date.now(),
            data: response.data
          });
        }
        if (response.bids) {
          response.bids.forEach(bidObj => {
            const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId));

            if (bidReq) {
              // bidObj.meta is the `bidResponse.meta` object according to https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response
              bidObj.meta = deepAccess(bidObj, 'meta', {});
              bidObj.meta.mediaType = bidObj.mediaType;
              bidObj.meta.advertiserDomains = (Array.isArray(bidObj.aDomain) && bidObj.aDomain.length) ? bidObj.aDomain : [];

              if (bidObj.mediaType === VIDEO) {
                const mediaTypeContext = deepAccess(bidReq, 'mediaTypes.video.context');
                // Adagio SSP returns a `vastXml` only. No `vastUrl` nor `videoCacheKey`.
                if (!bidObj.vastUrl && bidObj.vastXml) {
                  bidObj.vastUrl = 'data:text/xml;charset=utf-8;base64,' + window.btoa(bidObj.vastXml.replace(/\\"/g, '"'));
                }

                if (mediaTypeContext === OUTSTREAM) {
                  bidObj.outstreamRendererCode = deepAccess(bidReq, 'params.rendererCode', BB_RENDERER_DEFAULT)

                  if (deepAccess(bidReq, 'mediaTypes.video.skip')) {
                    const skipOffset = deepAccess(bidReq, 'mediaTypes.video.skipafter', 5) // default 5s.
                    bidObj.skipOffset = skipOffset
                  }

                  bidObj.renderer = OUTSTREAM_RENDERER.newRenderer(bidObj.adUnitCode, bidObj.outstreamRendererCode);
                }
              }

              if (bidObj.mediaType === NATIVE) {
                _parseNativeBidResponse(bidObj);
              }

              bidObj.site = bidReq.params.site;
              bidObj.placement = bidReq.params.placement;
              bidObj.pagetype = bidReq.params.pagetype;
              bidObj.category = bidReq.params.category;
            }
            bidResponses.push(bidObj);
          });
        }
      }
    } catch (err) {
      logError(err);
    }
    return bidResponses;
  },

  getUserSyncs(syncOptions, serverResponses) {
    if (!serverResponses.length || serverResponses[0].body === '' || !serverResponses[0].body.userSyncs) {
      return false;
    }

    const syncs = serverResponses[0].body.userSyncs.map(sync => ({
      type: sync.t === 'p' ? 'image' : 'iframe',
      url: sync.u
    }));

    return syncs;
  },

  /**
   * Handle custom logic in s2s context
   *
   * @param {*} params
   * @param {boolean} isOrtb Is an s2s context
   * @param {*} adUnit
   * @param {*} bidRequests
   * @returns {object} updated params
   */
  transformBidParams(params, isOrtb, adUnit, bidRequests) {
    // We do not have a prebid server adapter. So let's return unchanged params.
    return params;
  }
};

initAdagio();

registerBidder(spec);