Skip to content
This repository has been archived by the owner on Mar 18, 2024. It is now read-only.

Commit

Permalink
New slot level events (#248)
Browse files Browse the repository at this point in the history
* Added slot-info to some events payload

* Some refinements on the metrics

* Removed console.logs

* Some more work on slot-events

* Moved metrics logic from n-ui to o-ads

* Renamed slot events

* Added missing "name" attribute to slot events payload

* Fixed linting warnings

* Made code more robust

* Fixed broken unit tests

* Added unit tests for changes in broadcast

* Added missing unit tests

* Added some more unit tests

* Added a couple of istanbul ignores

* Fixed some indentation

* Added some more unit tests

* Improved a test

* Temporarily remove samsung galaxy from e2e tests

* Relaxed condition for a targeting API response

* Allow bigger bundle size

* Update src/js/ad-servers/gpt.js

Accept comma separators for multiple pos values

* Update src/js/ad-servers/gpt.js

Use `join` to join array instead of `toString` for consistency

* Update src/js/ad-servers/gpt.js

Fixed indentation

* Increase bundle size

* Fix linting problem
  • Loading branch information
carlesandres committed Apr 24, 2019
1 parent b820c89 commit 7719110
Show file tree
Hide file tree
Showing 22 changed files with 539 additions and 101 deletions.
18 changes: 9 additions & 9 deletions docs/docs/developer-guide/advanced-display.md
Expand Up @@ -167,36 +167,36 @@ Via component

## Events

#### `oAds.startInitialisation`
#### `oAds.initialising`
Triggered when the library starts the initialisation process. At this point in time, if `targetingApi` has been defined in the configuration, two separate calls are made to the targeting api in order to get 'user' and 'page' targeting parameters.

Also at this point, if `validateAdsTraffic` is set to `true`, `o-ads` will check if the traffic validation script (currently `moat.js`) is available and use it to check if the traffic source is a valid one.

#### `oAds.apiRequestsComplete`
#### `oAds.adsAPIComplete`
If targeting has been configured, this event is triggered when both requests to the targeting api ('user' and 'page') have been fullfilled (whether successfully or not).

#### `oAds.moatIVTcomplete`
#### `oAds.adsIVTComplete`
If `validateAdsTraffic` is set to `true`, this event is triggered as soon as the traffic has been validated or, if the traffic validation script can't been found, when the associated timeout period expires.

#### `oAds.initialised`
Triggered when the library has been initialised and the config has been set. (Note: the GPT library may not have been loaded by this point).

#### `oAds.adServerLoadSuccess`
#### `oAds.serverScriptLoaded`
Triggered when both the GPT library is loaded and `oAds.initialised` has happened. This marks the completion of the page-level tasks required to enable requests to the ad server.

#### `oAds.adServerLoadError`
Triggered if the library fails to load the external JS GPT library, meaning no advertising will work. Can be used if you wish to have a fallback when you know the adverts will not display.

#### `oAds.ready`
#### `oAds.slotReady`
Slot has been inited in the oAds library and is about to be requested from the ad server (deferred if lazy loading is on).

#### `oAds.rendered`
#### `oAds.slotRenderStart`
Triggered once the ad has been rendered on the page.

#### `oAds.complete`
If and when a creative has been returned, this event announces it has now been initialised in oAds, requested from the ad server and displayed. Triggered after `oAds.rendered`.
#### `oAds.slotExpand`
If and when a creative has been returned, this event announces it has now been initialised in oAds, requested from the ad server and displayed. Triggered after `oAds.slotRenderStart`.

#### `oAds.render`
#### `oAds.slotCanRender`
Lazy loaded advert has been requested.

#### `oAds.refresh`
Expand Down
2 changes: 1 addition & 1 deletion karma.conf.js
Expand Up @@ -81,7 +81,7 @@ if (process.env.COVERAGE) {
},
each: {
statements: 97,
branches: 94,
branches: 92,
functions: 96,
lines: 97
}
Expand Down
4 changes: 2 additions & 2 deletions main.js
Expand Up @@ -55,7 +55,7 @@ Ads.prototype.init = function(options) {
const targetingApi = this.config().targetingApi;
const validateAdsTraffic = this.config().validateAdsTraffic;

this.utils.broadcast('startInitialisation');
this.utils.broadcast('initialising');

// Don't need to fetch anything if no targeting or validateAdsTraffic configured.
if(!targetingApi && !validateAdsTraffic) {
Expand Down Expand Up @@ -182,4 +182,4 @@ function removeDOMEventListener() {
}

const ads = new Ads();
export default ads;
export default ads;
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -12,7 +12,7 @@
"test-cy:open": "cypress open",
"test-nw": "npm run test-nw:basic && npm run test-nw:extended",
"test-nw:local": "nightwatch -c ./test/nightwatch/config/nightwatch.conf.local.js",
"test-nw:basic": "npm run nightwatch-bs -- --group basic --env chrome,edge,galaxy_s8",
"test-nw:basic": "npm run nightwatch-bs -- --group basic --env chrome,edge",
"test-nw:extended": "npm run nightwatch-bs -- --group extended --env chrome",
"coverage": "export COVERAGE=true && karma start && unset COVERAGE",
"ci": "export COVERAGE=true && export CI=true && karma start && unset COVERAGE && unset CI",
Expand Down Expand Up @@ -88,7 +88,7 @@
"bundlesize": [
{
"path": "./build/main.js",
"maxSize": "111 kB"
"maxSize": "115 kB"
}
]
}
23 changes: 16 additions & 7 deletions src/js/ad-servers/gpt.js
Expand Up @@ -25,8 +25,8 @@ function init() {
const gptConfig = config('gpt') || {};
breakpoints = config('responsive');
initGoogleTag();
utils.on('ready', onReady.bind(null, slotMethods));
utils.on('render', onRender);
utils.on('slotReady', onReady.bind(null, slotMethods));
utils.on('slotCanRender', onRender);
utils.on('refresh', onRefresh);
utils.on('resize', onResize);
googletag.cmd.push(setup.bind(null, gptConfig));
Expand All @@ -47,7 +47,7 @@ function initGoogleTag() {
}

utils.attach('//www.googletagservices.com/tag/js/gpt.js', true,
() => { utils.broadcast('adServerLoadSuccess'); },
() => { utils.broadcast('serverScriptLoaded'); },
(err) => { utils.broadcast('adServerLoadError', err); }
);
}
Expand Down Expand Up @@ -107,6 +107,7 @@ function setPageTargeting(targetingData) {
});
});
} else {
/* istanbul ignore next */
utils.log.warn('invalid targeting object passed', targetingData);
}

Expand Down Expand Up @@ -238,6 +239,13 @@ function onRenderEnded(event) {
const iframeId = `google_ads_iframe_${gptSlotId.getId()}`;
data.type = domId.pop();
data.name = domId.join('-');
data.size = event.size && event.size.length ? event.size.join() : '';

const slotTargeting = event.slot.getTargetingMap && event.slot.getTargetingMap();
if (slotTargeting && slotTargeting.pos) {
data.pos = slotTargeting.pos.length ? slotTargeting.pos.join() : '';
}

const detail = data.gpt;
detail.isEmpty = event.isEmpty;
detail.size = event.size;
Expand All @@ -252,9 +260,10 @@ function onRenderEnded(event) {
iFrameEl.setAttribute('title', 'Advertisement');
detail.iframe = iFrameEl;
} else {
/* istanbul ignore next */
utils.log.warn('No iFrame found matching GPT SlotID');
}
utils.broadcast('rendered', data);
utils.broadcast('slotRenderStart', data);
}

/*
Expand Down Expand Up @@ -340,9 +349,9 @@ const slotMethods = {
*/
display: function() {
window.googletag.cmd.push(() => {
utils.broadcast('gptDisplay');
googletag.display(this.gpt.id);
});
this.fire('slotGoRender');
googletag.display(this.gpt.id);
});
return this;
},
/**
Expand Down
2 changes: 1 addition & 1 deletion src/js/data-providers/api.js
Expand Up @@ -30,7 +30,7 @@ Api.prototype.getPageData = function(target, timeout) {
};

Api.prototype.handleResponse = function(response) {
utils.broadcast('apiRequestsComplete');
utils.broadcast('adsAPIComplete');
this.data = response;

for(let i = 0; i < response.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/js/data-providers/moat.js
Expand Up @@ -25,7 +25,7 @@ Moat.prototype.init = function() {
}, 1000);
});

const fireCompleteEvent = () => { utils.broadcast('moatIVTcomplete'); };
const fireCompleteEvent = () => { utils.broadcast('adsIVTComplete'); };
promise.then( fireCompleteEvent, fireCompleteEvent );

return promise;
Expand Down
9 changes: 6 additions & 3 deletions src/js/slot.js
Expand Up @@ -128,7 +128,7 @@ const onChangeBreakpoint = (event) => {
* @constructor
*/
function Slot(container, screensize, initLazyLoading) {
const renderEvent = 'rendered';
const renderEvent = 'slotRenderStart';
const cfg = config();
let slotConfig = config('slots') || {};
const disableSwipeDefault = config('disableSwipeDefault') || false;
Expand Down Expand Up @@ -271,7 +271,7 @@ Slot.prototype.initLazyLoad = function() {
};

Slot.prototype.render = function() {
this.fire('render');
this.fire('slotCanRender');
/* istanbul ignore else */
if(this.lazyLoadObserver) {
this.lazyLoadObserver.unobserve(this.container);
Expand Down Expand Up @@ -387,11 +387,14 @@ Slot.prototype.submitImpression = function() {
*/
Slot.prototype.fire = function(name, data) {
const details = {
name: this.name,
name: this.name || '',
pos: this.targeting && this.targeting.pos || '',
size: this.gpt && this.gpt.size || '',
slot: this
};

if (utils.isPlainObject(data)) {
data.pMarkDetails = details;
utils.extend(details, data);
}

Expand Down
10 changes: 5 additions & 5 deletions src/js/slots.js
Expand Up @@ -152,7 +152,7 @@ Slots.prototype.initSlot = function(container) {
/* istanbul ignore else */
if (slot && !this[slot.name]) {
this[slot.name] = slot;
slot.fire('ready');
slot.fire('slotReady');
} else if (this[slot.name]) {
utils.log.error('slot %s is already defined!', slot.name);
}
Expand Down Expand Up @@ -182,11 +182,11 @@ Slots.prototype.initRefresh = function() {
};

/*
* listens for the rendered event from a slot and fires the complete event,
* listens for the rendered event from a slot and fires the slotExpand event,
* after extending the slot with information from the server.
*/
Slots.prototype.initRendered = function() {
utils.on('rendered', function(slots, event) {
utils.on('slotRenderStart', function(slots, event) {
const slot = slots[event.detail.name];
/* istanbul ignore else */
if (slot) {
Expand All @@ -195,7 +195,7 @@ Slots.prototype.initRendered = function() {
const format = findFormatBySize(size);
slot.setFormatLoaded(format);
slot.maximise(size);
slot.fire('complete', event.detail);
slot.fire('slotExpand', event.detail);
}
}.bind(null, this));
return this;
Expand Down Expand Up @@ -257,7 +257,7 @@ Slots.prototype.initPostMessage = function() {

// TODO: Remove adIframeLoaded once we can tag onto GPTs `slotRenderEnded` event
if(type === 'adIframeLoaded') {
document.body.dispatchEvent( new CustomEvent('oAds.adIframeLoaded'));
slot.fire('slotRenderEnded');
}

// Received message to Collapse ad slot.
Expand Down
19 changes: 16 additions & 3 deletions src/js/utils/events.js
Expand Up @@ -5,22 +5,35 @@
* @see utils
*/

// Creates a timestamp in the browser's performance entry buffer
// for later use
export const perfMark = name => {
/* istanbul ignore next */
const performance = window.LUX || window.performance || window.msPerformance || window.webkitPerformance || window.mozPerformance;
if (performance && performance.mark) {
performance.mark(name);
}
};

/**
* Broadscasts an o-ads event
* @param {string} name The name of the event
* @param {object} data The data to send as event detail
* @param {HTMLElement} target The element to attach the event listener to
*/
export function broadcast(name, data, target) {
export function broadcast(eventName, data, target) {
/* istanbul ignore next: ignore the final fallback as hard trigger */
target = target || document.body || document.documentElement;
name = `oAds.${name}`;
eventName = `oAds.${eventName}`;
const opts = {
bubbles: true,
cancelable: true,
detail: data
};
target.dispatchEvent(new CustomEvent(name, opts));

const markName = typeof data === 'object' && 'pos' in data && 'name' in data ? [eventName, data.pos, data.name, data.size.length ? data.size.toString() : ''].join('__') : eventName;
perfMark(markName);
target.dispatchEvent(new CustomEvent(eventName, opts));
}

/**
Expand Down
9 changes: 6 additions & 3 deletions src/js/utils/index.js
@@ -1,4 +1,5 @@
import { on, off, once, broadcast } from './events';
import { on, off, once, broadcast, perfMark } from './events';
import { setupMetrics } from './metrics';
import messenger from './messenger';
import responsive, { getCurrent } from './responsive';
import log, { isOn, start, end, info, warn, error, table, attributeTable} from './log';
Expand Down Expand Up @@ -481,5 +482,7 @@ export default {
iframeToSlotName,
buildObjectFromArray,
cookie,
getVersion
};
getVersion,
setupMetrics,
perfMark
};
68 changes: 68 additions & 0 deletions src/js/utils/metrics.js
@@ -0,0 +1,68 @@
function getMarksForEvents(events, suffix) {
const markNames = events.map( eventName => 'oAds.' + eventName + suffix );
const performance = window.LUX || window.performance || window.msPerformance || window.webkitPerformance || window.mozPerformance;
if (!performance || !performance.getEntriesByName) {
/* istanbul ignore next */
return {};
}

const marks = {};
markNames.forEach(function(mName) {
const pMarks = performance.getEntriesByName(mName);
const markName = mName.replace('oAds.', '').replace(suffix, '');
if (pMarks && pMarks.length) {
// We don't need sub-millisecond precision
marks[markName] = Math.round(pMarks[0].startTime);
}
});
return marks;
}

export function setupMetrics(definitions, callback) {
if (!Array.isArray(definitions)) {
this.log.warn('Metrics definitions should be an array. o-Ads will not record any metrics.');
return;
}

definitions.forEach( function(eDef) {
const triggers = Array.isArray(eDef.triggers) ? eDef.triggers : [];
triggers.forEach(function(trigger) {
sendMetricsOnEvent('oAds.' + trigger, eDef, callback);
});
});
}

function sendMetricsOnEvent(eventName, eMarkMap, callback) {
document.addEventListener(eventName, function listenOnInitialised(event) {
sendMetrics(eMarkMap, event.detail, callback);
if (!eMarkMap.multiple) {
document.removeEventListener(eventName, listenOnInitialised);
}
});
}

function sendMetrics(eMarkMap, eventDetails, callback) {
let suffix = '';
if (eventDetails && 'pos' in eventDetails && 'name' in eventDetails) {
suffix = '__' + [eventDetails.pos, eventDetails.name, eventDetails.size].join('__');
}

const marks = getMarksForEvents(eMarkMap.marks, suffix);

const eventPayload = {
category: 'ads',
action: eMarkMap.spoorAction,
timings: { marks: marks }
};

if (eventDetails && 'pos' in eventDetails) {
eventPayload.creative = {
name: eventDetails.name,
pos: eventDetails.pos,
size: eventDetails.size && eventDetails.size.toString()
};
}

callback(eventPayload);
}

6 changes: 3 additions & 3 deletions test/cypress/examples/unit/main.test.js
Expand Up @@ -6,11 +6,11 @@ describe('Main', () => {
cy.clearCookie('FTConsent');
});

it.only('oAds.ready event fires after o.DOMContentLoaded', () => {
it.only('oAds.slotReady event fires after o.DOMContentLoaded', () => {
document.body.insertAdjacentHTML('beforeend', '<div data-o-ads-name="banlb2" data-o-ads-formats="MediumRectangle"></div>');
new Ads();
const oAdsReadySpy = cy.spy();
utils.on('ready', oAdsReadySpy);
utils.on('slotReady', oAdsReadySpy);

/*
cy runs two iframes. One loads the test, the other loads
Expand Down Expand Up @@ -75,4 +75,4 @@ describe('Main', () => {
expect(gptInit.calledTwice);
});
});
});
});

0 comments on commit 7719110

Please sign in to comment.