UNPKG

@financial-times/o-tracking

Version:

Provides tracking for a product. Tracking requests are sent to the Spoor API.

211 lines (177 loc) 4.96 kB
import {get as getSetting} from './settings.js'; import {broadcast, is, safelyStringifyJson, merge, addEvent, log} from '../utils.js'; import {Queue} from './queue.js'; import {get as getTransport} from './transports/index.js'; /** * Queue queue. * * @type {Queue} */ let queue; /** * Consistent check to see if we should use sendBeacon or not. * * @returns {boolean} Should we use sendBeacon? */ function should_use_sendBeacon() { let config = getSetting('config') || {}; if (config.queue === true) { return false; } else { return Boolean(navigator.sendBeacon); } } /** * Attempts to send a tracking request. * * @param {object} request The request to be sent. * @param {Function=} callback Callback to fire the next item in the queue. * @returns {void} */ function sendRequest(request, callback) { const queueTime = request.queueTime; const offlineLag = new Date().getTime() - queueTime; const transport = should_use_sendBeacon() ? getTransport('sendBeacon')() : window.XMLHttpRequest && 'withCredentials' in new window.XMLHttpRequest() ? getTransport('xhr')() : getTransport('image')(); const user_callback = request.callback; const core_system = getSetting('config') && getSetting('config').system || {}; const system = merge(core_system, { version: getSetting('version'), // Version of the tracking client e.g. '1.2' source: getSetting('source'), // Source of the tracking client e.g. 'o-tracking' transport: transport.name, // The transport method used. }); if (getSetting('config').test || getSetting('config').test_data) { system.is_live = false; } else { system.is_live = true; } request = merge({ system: system }, request); // Only bothered about offlineLag if it's longer than a second, but less than 12 months. (Especially as Date can be dodgy) if (offlineLag > 1000 && offlineLag < 12 * 30 * 24 * 60 * 60 * 1000) { request.time = request.time || {}; request.time.offset = offlineLag; } delete request.callback; delete request.type; delete request.queueTime; log('user_callback', user_callback); log('PreSend', request); const stringifiedData = safelyStringifyJson(request); transport.complete(function (error) { if (is(user_callback, 'function')) { user_callback.call(request); log('calling user_callback'); } if (error) { // Re-add to the queue if it failed. // Re-apply queueTime here request.queueTime = queueTime; queue.add(request).save(); broadcast('oErrors', 'log', { error: error.message, info: { module: 'o-tracking' } }); } else if (callback) { callback(); } }); /** * Default collection server. */ let url = 'https://spoor-api.ft.com/ingest'; if (request && request.category && request.action) { url += `?type=${request.category}:${request.action}`; } transport.send(url, stringifiedData); } /** * Adds a new request to the list of pending requests * * @param {object} request The request to queue * @returns {void} */ function add(request) { request.queueTime = new Date().getTime(); if (should_use_sendBeacon()) { sendRequest(request); } else { queue.add(request).save(); } log('AddedToQueue', queue); } /** * If there are any requests queued, attempts to send the next one * Otherwise, does nothing * * @param {Function} [callback] - Optional callback * @returns {void} */ function run(callback = function () { /* empty */}) { // Investigate queue lengths bug // https://jira.ft.com/browse/DTP-330 const all_events = queue.all(); if (all_events.length > 200) { const counts = {}; all_events.forEach(function (event) { const label = [event.category, event.action].join(':'); if (!Object.prototype.hasOwnProperty.call(counts, label)) { counts[label] = 0; } counts[label] += 1; }); queue.replace([]); queue.add({ category: 'o-tracking', action: 'queue-bug', context: { url: document.URL, queue_length: all_events.length, counts: counts, storage: queue.storage.storage._type } }); } const next = function () { run(); callback(); }; const nextRequest = queue.shift(); // Cancel if we've run out of requests. if (!nextRequest) { return callback(); } // Send this request, then try run again. return sendRequest(nextRequest, next); } /** * Convenience function to add and run a request all in one go. * * @param {object} request The request to queue and run. * @returns {void} */ function addAndRun(request) { add(request); run(); } /** * Init a queue and send any leftover events. * * @returns {Queue} An initialised queue. */ function init() { queue = new Queue('requests'); // If any tracking calls are made whilst offline, try sending them the next time the device comes online addEvent(window, 'online', function() { run(); }); // On startup, try sending any requests queued from a previous session. run(); return queue; } export { init, add, run, addAndRun };