@financial-times/o-ads
Version:
This package contains the core functionality used by the FT in providing ads across all of its sites. This includes ft.com, howtospendit.com, ftadviser.com and other specialist titles.
643 lines (544 loc) • 18.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.DEFAULT_LAZY_LOAD = void 0;
var _config = _interopRequireDefault(require("../config.js"));
var _index = _interopRequireDefault(require("../utils/index.js"));
var _targeting = _interopRequireDefault(require("../targeting.js"));
var _url = require("../utils/url.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*globals googletag: true */
/* eslint no-inner-declarations: 1 */
/**
* @fileOverview
* ad server modules for o-ads implementing Google publisher tags ad requests.
*
* @author Robin Marr, robin.marr@ft.com
*/
var DEFAULT_LAZY_LOAD = {
fetchMarginPercent: 500,
renderMarginPercent: 200,
mobileScaling: 2.0
};
exports.DEFAULT_LAZY_LOAD = DEFAULT_LAZY_LOAD;
var DEFAULT_COLLAPSE_MODE = 'never';
var breakpoints = false;
/*
//###########################
// Initialisation handlers ##
//###########################
*/
/*
* Initialise Google publisher tags functionality
*/
function init() {
var gptConfig = (0, _config.default)('gpt') || {};
breakpoints = (0, _config.default)('responsive');
loadGPT();
_index.default.on('slotReady', onReady.bind(null, slotMethods));
_index.default.on('slotCanRender', onRender);
_index.default.on('refresh', onRefresh);
_index.default.on('resize', onResize);
googletag.cmd.push(setup.bind(null, gptConfig));
}
/*
* initalise the googletag global namespace and add the google publish tags library to the page
*/
function loadGPT() {
if (!window.googletag) {
// set up a place holder for the gpt code downloaded from google
window.googletag = {}; // this is a command queue used by GPT any methods added to it will be
// executed when GPT code is available, if GPT is already available they
// will be executed immediately
window.googletag.cmd = [];
}
_index.default.attach('//securepubads.g.doubleclick.net/tag/js/gpt.js', true, () => {
_index.default.broadcast('serverScriptLoaded');
}, err => {
_index.default.broadcast('adServerLoadError', err);
});
}
/*
//#################################
// Global configuration handlers ##
//#################################
*/
/*
* Configure the GPT library for the current page
* this method is pushed onto the googletag command queue and run
* when the library is available
*/
function setup(gptConfig) {
var nonPersonalized = (0, _config.default)('nonPersonalized') ? 1 : 0;
googletag.pubads().addEventListener('slotRenderEnded', onRenderEnded);
enableCompanions(gptConfig);
setRenderingMode(gptConfig);
setPageTargeting(_targeting.default.get());
setPageCollapseEmpty();
if (gptConfig.hasOwnProperty('enableLazyLoad')) {
enableLazyLoad(gptConfig.enableLazyLoad);
}
var url = (0, _url.stripUrlParams)({
href: window.location.href,
filters: {
root: _url.SEARCH_PARAMS
}
});
googletag.enableServices();
googletag.pubads().setTargeting('url', url);
googletag.pubads().setRequestNonPersonalizedAds(nonPersonalized);
return true;
}
/**
* Enables GPT's Lazy Loading for serving ads only when it's necessary.
* The lazy load config ooptions are:
*
* - fetchMarginPercent, minimum distance from the current viewport a slot
* must be before we fetch the ad as a percentage of viewport size.
* 0 means "when the slot enters the viewport",
* 100 means "when the ad is 1 viewport away"
*
* - renderMarginPercent, minimum distance from the current viewport a slot
* must be before we render an ad.
* This allows for prefetching the ad, but waiting to render and
* download other subresources. The value works just like
* fetchMarginPercent as a percentage of viewport.
*
* - mobileScaling, a multiplier applied to margins on mobile devices.
* This allows varying margins on mobile vs. desktop. For example,
* a mobileScaling of 2.0 will multiply all margins by 2 on mobile
* devices, increasing the minimum distance a slot can be before
* fetching and rendering.
*
* @param {object|boolean} lazyLoadConf The lazy load config object or a boolean.
* @returns {boolean} true if the object is of type Array, otherwise false
*/
function enableLazyLoad(lazyLoadConf) {
if (lazyLoadConf && (typeof lazyLoadConf === 'object' || typeof lazyLoadConf === 'boolean')) {
var opts = Object.assign({}, DEFAULT_LAZY_LOAD, typeof lazyLoadConf === 'object' ? lazyLoadConf : {});
window.googletag.pubads().enableLazyLoad(opts);
} else {
_index.default.log.warn('lazyLoadConf must be either an object or a boolean', lazyLoadConf);
}
}
/*
* set the gpt rendering mode to either sync or async
* default is async
*/
function setRenderingMode(gptConfig) {
var rendering = gptConfig.rendering;
if (rendering === 'sync') {
googletag.pubads().enableSyncRendering();
} else if (rendering === 'sra') {
googletag.pubads().enableSingleRequest();
} else {
googletag.pubads().enableAsyncRendering();
}
}
/**
* Adds page targeting to GPT ad calls
* @name setPageTargeting
* @memberof GPT
* @lends GPT
*/
function setPageTargeting(targetingData) {
if (_index.default.isPlainObject(targetingData)) {
googletag.cmd.push(() => {
var pubads = googletag.pubads();
Object.keys(targetingData).forEach(key => {
// Convert to string as boolean values are invalid arguments for GPT setTargeting()
pubads.setTargeting(key, String(targetingData[key]));
});
});
} else {
/* istanbul ignore next */
_index.default.log.warn('invalid targeting object passed', targetingData);
}
return targetingData;
}
/**
* Removes page targeting for a specified key from GPT ad calls
*/
function clearPageTargetingForKey(key) {
if (!window.googletag) {
_index.default.log.warn('Attempting to clear page targeting before the GPT library has initialized');
return;
}
if (!key) {
_index.default.log.warn('Refusing to unset all keys - a key must be specified');
return;
}
googletag.cmd.push(() => {
googletag.pubads().clearTargeting(key);
});
}
/**
* Sets behaviour of empty slots can be 'after', 'before' or 'never'
* * 'after' collapse slots that return an empty ad
* * 'before' collapses all slots and only displays them when an ad is found
* * 'never' does not collapse any empty slot, the collapseEmptyDivs method is not called in that case
*/
function setPageCollapseEmpty() {
var mode = (0, _config.default)('collapseEmpty');
if (mode === 'before') {
googletag.pubads().collapseEmptyDivs(true);
}
if (mode === 'after') {
googletag.pubads().collapseEmptyDivs(false);
}
}
/**
* When companions are enabled we delay the rendering of ad slots until
* either a master is returned or all slots are returned without a master
*/
function enableCompanions(gptConfig) {
if (gptConfig.companions) {
googletag.pubads().disableInitialLoad();
googletag.companionAds().setRefreshUnfilledSlots(true);
}
}
/*
//##################
// Event handlers ##
//##################
*/
/*
* Event handler for when a slot is ready for an ad to rendered
*/
function onReady(slotMethods, event) {
var slot = event.detail.slot;
/* istanbul ignore else */
if (slot.server === 'gpt') {
slot.gpt = {}; // extend the slot with gpt methods
_index.default.extend(slot, slotMethods); // setup the gpt configuration the ad
googletag.cmd.push(() => {
slot.defineSlot().addServices().setCollapseEmpty().setTargeting().setURL();
if (!slot.defer && slot.hasValidSize()) {
slot.display();
}
});
}
}
/*
* Render is called when a slot is not rendered when the ready event fires
*/
function onRender(event) {
var slot = event.detail.slot;
/* istanbul ignore else */
if (_index.default.isFunction(slot.display)) {
slot.display();
}
}
/*
* refresh is called when a slot requests the ad be flipped
*/
function onRefresh(event) {
window.googletag.cmd.push(() => {
var targeting = event.detail.targeting;
if (_index.default.isPlainObject(targeting)) {
Object.keys(targeting).forEach(name => {
event.detail.slot.gpt.slot.setTargeting(name, targeting[name]);
});
}
googletag.pubads().refresh([event.detail.slot.gpt.slot]);
});
return this;
}
function onResize(event) {
var iframe = event.detail.slot.gpt.iframe;
var size = event.detail.size;
iframe.width = size[0];
iframe.height = size[1];
}
/*
* function passed to the gpt library that is run when an ad completes rendering
*/
function onRenderEnded(event) {
var data = {
gpt: {}
};
var gptSlotId = event.slot.getSlotId();
var domId = gptSlotId.getDomId().split('-');
var iframeId = "google_ads_iframe_".concat(gptSlotId.getId());
data.type = domId.pop();
data.name = domId.join('-');
data.size = event.size && event.size.length ? event.size.join() : '';
data.creativeId = event.creativeId;
var slotTargeting = event.slot.getTargetingMap && event.slot.getTargetingMap();
if (slotTargeting && slotTargeting.pos) {
data.pos = slotTargeting.pos.length ? slotTargeting.pos.join() : '';
}
var detail = data.gpt;
detail.isEmpty = event.isEmpty;
detail.size = event.size;
detail.creativeId = event.creativeId;
detail.lineItemId = event.lineItemId;
detail.serviceName = event.serviceName;
var iFrameEl = document.getElementById(iframeId);
if (iFrameEl) {
iFrameEl.setAttribute('tabindex', '-1');
iFrameEl.setAttribute('aria-hidden', 'true');
iFrameEl.setAttribute('role', 'presentation');
iFrameEl.setAttribute('title', 'Advertisement');
detail.iframe = iFrameEl;
} else {
/* istanbul ignore next */
_index.default.log.warn('No iFrame found matching GPT SlotID');
}
_index.default.broadcast('slotRenderStart', data);
}
/*
//################
// Slot methods ##
//################
* Set of methods extended on to the slot constructor for GPT served slots
*/
var slotMethods = {
/**
* define a GPT slot
*/
defineSlot: function defineSlot() {
window.googletag.cmd.push(() => {
this.gpt.id = "".concat(this.name, "-gpt");
this.inner.setAttribute('id', this.gpt.id);
this.setUnitName();
if (!this.outOfPage) {
if (breakpoints && _index.default.isObject(this.sizes)) {
this.initResponsive();
this.gpt.slot = googletag.defineSlot(this.gpt.unitName, [0, 0], this.gpt.id).defineSizeMapping(this.gpt.sizes);
} else {
this.gpt.slot = googletag.defineSlot(this.gpt.unitName, this.sizes, this.gpt.id);
}
} else {
this.gpt.slot = googletag.defineOutOfPageSlot(this.gpt.unitName, this.gpt.id);
}
});
return this;
},
clearSlot: function clearSlot(gptSlot) {
if (window.googletag.pubadsReady && window.googletag.pubads) {
gptSlot = gptSlot || this.gpt.slot;
return googletag.pubads().clear([gptSlot]);
} else {
return false;
}
},
initResponsive: function initResponsive() {
window.googletag.cmd.push(() => {
_index.default.on('breakpoint', event => {
var slot = event.detail.slot;
var screensize = event.detail.screensize;
updatePageTargeting({
res: screensize
});
if (slot.hasValidSize(screensize)) {
/* istanbul ignore else */
if (slot.gpt.iframe) {
slot.fire('refresh');
} else if (!this.defer) {
slot.display();
}
}
}, this.container);
var mapping = googletag.sizeMapping();
Object.keys(breakpoints).forEach(breakpoint => {
if (this.sizes[breakpoint]) {
mapping.addSize(breakpoints[breakpoint], this.sizes[breakpoint]);
}
});
this.gpt.sizes = mapping.build();
});
return this;
},
/**
* Tell gpt to destroy the slot and its metadata
*/
destroySlot: function destroySlot(gptSlot) {
if (window.googletag.pubadsReady && window.googletag.pubads) {
gptSlot = gptSlot || this.gpt.slot;
return googletag.destroySlots([gptSlot]);
} else {
return false;
}
},
/*
* Tell gpt to request an ad
*/
display: function display() {
window.googletag.cmd.push(() => {
this.fire('slotGoRender');
googletag.display(this.gpt.id);
});
return this;
},
/**
* Set the DFP unit name for the slot.
*/
setUnitName: function setUnitName() {
window.googletag.cmd.push(() => {
var unitName;
var gpt = (0, _config.default)('gpt') || {};
var attr = this.container.getAttribute('data-o-ads-gpt-unit-name');
if (_index.default.isNonEmptyString(attr)) {
unitName = attr;
} else if (_index.default.isNonEmptyString(gpt.unitName)) {
unitName = gpt.unitName;
} else {
var network = gpt.network;
var site = gpt.site;
var zone = gpt.zone;
unitName = "/".concat(network);
unitName += _index.default.isNonEmptyString(site) ? "/".concat(site) : '';
unitName += _index.default.isNonEmptyString(zone) ? "/".concat(zone) : '';
}
this.gpt.unitName = unitName;
});
return this;
},
/**
* Add the slot to the pub ads service and add a companion service if configured
*/
addServices: function addServices(gptSlot) {
window.googletag.cmd.push(() => {
var gpt = (0, _config.default)('gpt') || {};
gptSlot = gptSlot || this.gpt.slot;
gptSlot.addService(googletag.pubads());
if (gpt.companions && this.companion !== false) {
gptSlot.addService(googletag.companionAds());
}
});
return this;
},
/**
* Sets the GPT collapse empty mode for a given slot
* values can be 'after', 'before', 'never'
*/
setCollapseEmpty: function setCollapseEmpty() {
window.googletag.cmd.push(() => {
var mode = this.collapseEmpty || (0, _config.default)('collapseEmpty') || DEFAULT_COLLAPSE_MODE;
if (mode === 'before') {
this.gpt.slot.setCollapseEmptyDiv(true, true);
} else if (mode === 'after') {
this.gpt.slot.setCollapseEmptyDiv(true);
} else if (mode === 'never') {
this.gpt.slot.setCollapseEmptyDiv(false);
}
});
return this;
},
submitGptImpression: function submitGptImpression() {
/* istanbul ignore next */
function getImpressionURL(iframe) {
var trackingUrlElement = iframe.contentWindow.document.querySelector('[data-o-ads-impression-url]');
if (trackingUrlElement) {
return trackingUrlElement.dataset.oAdsImpressionUrl;
} else {
_index.default.log.warn('Impression URL not found, this is set via a creative template.');
return false;
}
}
if (this.outOfPage && this.gpt.iframe) {
var impressionURL = getImpressionURL(this.gpt.iframe);
/* istanbul ignore next */
if (impressionURL) {
_index.default.attach(impressionURL, true, () => {
_index.default.log.info('Impression Url requested');
}, () => {
_index.default.log.info('CORS request to submit an impression failed');
}, true);
}
} else {
_index.default.log.warn('Attempting to call submitImpression on a non-oop slot');
}
},
/**
* Sets page url to be sent to google
* prevents later url changes via javascript from breaking the ads
*/
setURL: function setURL(gptSlot) {
window.googletag.cmd.push(() => {
gptSlot = gptSlot || this.gpt.slot;
var canonical = (0, _config.default)('canonical');
gptSlot.set('page_url', canonical ? canonical : _index.default.getLocation());
});
return this;
},
/**
* Adds key values from a given object to the slot targeting
*/
setTargeting: function setTargeting(gptSlot) {
window.googletag.cmd.push(() => {
gptSlot = gptSlot || this.gpt.slot;
/* istanbul ignore else */
if (_index.default.isPlainObject(this.targeting)) {
Object.keys(this.targeting).forEach(param => {
gptSlot.setTargeting(param, this.targeting[param]);
});
}
});
return this;
}
};
/*
//####################
// External methods ##
//####################
*/
/**
* The correlator is a random number added to ad calls.
* It is used by the ad server to determine which impressions where served to the same page
* Updating is used to tell the ad server to treat subsequent ad calls as being on a new page
*/
function updateCorrelator() {
_index.default.log.warn('[DEPRECATED]: Updatecorrelator is being phased out by google and removed from o-ads in future releases.');
googletag.cmd.push(() => {
googletag.pubads().updateCorrelator();
});
}
function clearPageTargeting() {
if (window.googletag) {
googletag.cmd.push(() => {
googletag.pubads().clearTargeting();
});
} else {
_index.default.log.warn('Attempting to clear page targeting before the GPT library has initialized');
}
}
function updatePageTargeting(override) {
if (window.googletag) {
var params = _index.default.isPlainObject(override) ? override : _targeting.default.get();
setPageTargeting(params);
} else {
_index.default.log.warn('Attempting to set page targeting before the GPT library has initialized');
}
} //https://developers.google.com/doubleclick-gpt/common_implementation_mistakes#scenario-2:-checking-the-googletag-object-to-know-whether-gpt-is-ready
function hasGPTLoaded() {
if (window.googletag && window.googletag.apiReady) {
return true;
} else {
return false;
}
}
function debug() {
var log = _index.default.log;
var conf = (0, _config.default)('gpt');
if (!conf) {
return;
}
log.start('gpt');
log.attributeTable(conf);
log.end();
}
var _default = {
init,
setup,
updateCorrelator,
updatePageTargeting,
clearPageTargeting,
clearPageTargetingForKey,
hasGPTLoaded,
loadGPT,
debug
};
exports.default = _default;