@algolia/algolia-browser-telemetry
Version:
<p align="center"> <img src="https://github.com/algolia/algolia-browser-telemetry/blob/master/doc/netinfo.blog.png?raw=true" height="380px"/> </p> <br/> <br/>
996 lines (818 loc) • 32.1 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@algolia/requester-browser-xhr')) :
typeof define === 'function' && define.amd ? define(['exports', '@algolia/requester-browser-xhr'], factory) :
(global = global || self, factory(global['@algolia/algolia-browser-telemetry'] = {}, global.requesterBrowserXhr));
}(this, (function (exports, requesterBrowserXhr) { 'use strict';
function isBrowserEnvironment() {
return typeof window !== 'undefined';
}
var isBrowser = /*#__PURE__*/isBrowserEnvironment(); // We are doing our own minification as minifiers do not hoist global variables.
// Cast as Window, but check if we are in browser env first. If we type it as Window|undefined
// then it makes our life harder in all other occurences where we use W.
// @ts-ignore
var W = isBrowser ? window : undefined; // @ts-ignore
var D = isBrowser ? document : undefined;
var navigator = W === null || W === void 0 ? void 0 : W.navigator;
var WEIGHTS;
(function (WEIGHTS) {
WEIGHTS[WEIGHTS["memory"] = 0.1] = "memory";
WEIGHTS[WEIGHTS["threads"] = 0.2] = "threads";
WEIGHTS[WEIGHTS["network"] = 0.35] = "network";
WEIGHTS[WEIGHTS["saveData"] = 0.35] = "saveData";
})(WEIGHTS || (WEIGHTS = {}));
function isLowEndDevice() {
var _navigator$connection, _navigator$connection3;
var signal = 0;
if ((navigator === null || navigator === void 0 ? void 0 : navigator.hardwareConcurrency) && (navigator === null || navigator === void 0 ? void 0 : navigator.hardwareConcurrency) <= 4) {
signal += WEIGHTS.threads;
} // @ts-ignore
if (!!(navigator === null || navigator === void 0 ? void 0 : navigator.deviceMemory) && navigator.deviceMemory <= 4) {
signal += WEIGHTS.memory;
} // @ts-ignore
if (!!(navigator === null || navigator === void 0 ? void 0 : (_navigator$connection = navigator.connection) === null || _navigator$connection === void 0 ? void 0 : _navigator$connection.effectiveType)) {
var _navigator$connection2;
if ( // @ts-ignore
['slow-2g', '2g', '3g'].indexOf(navigator === null || navigator === void 0 ? void 0 : (_navigator$connection2 = navigator.connection) === null || _navigator$connection2 === void 0 ? void 0 : _navigator$connection2.effectiveType) > -1) {
signal += WEIGHTS.network;
}
} // @ts-ignore
if (navigator === null || navigator === void 0 ? void 0 : (_navigator$connection3 = navigator.connection) === null || _navigator$connection3 === void 0 ? void 0 : _navigator$connection3.saveData) {
signal += WEIGHTS.saveData;
}
return signal >= 0.3;
}
/**
* Extract meaningful telemetry resources information and remove unnecessary query params;
* @param {string} resource
* @returns {string}
*/
function parseResource(resource) {
return resource.split('?')[0];
}
function getAppIDFromRequest(req) {
var appid = req.headers['x-algolia-application-id'];
if (appid) return appid;
var appidParam = getSearchParam(req.url.toLowerCase(), 'x-algolia-application-id');
if (appidParam) return appidParam;
return undefined;
}
function getSearchParam(url, name) {
// IE11- compat, as new URL(url).searchParams.get(name) would do otherwise
// https://stackoverflow.com/a/39768285 - CC-BY-SA-3
// from Aaron https://stackoverflow.com/users/222564/aaron
// from rodnaph https://stackoverflow.com/users/1139810/rodnaph
return (url.split('?' + name + '=')[1] || url.split('&' + name + '=')[1] || '').split('&')[0];
}
/**
* Round number to N decimals
* @param {number} number
* @returns {number}
*/
function roundToDecimals(number, decimals) {
if (decimals === void 0) {
decimals = 2;
}
var base = Math.pow(10, decimals);
return Math.round(number * base) / base;
}
/**
* Convert byte size response to KB
* @param {number} bytes
* @returns {number}
*/
function toKiloBytes(bytes) {
return bytes / 1024;
}
/**
* PromiseMicroTask executes microtask via Promise interface
* @param {Function} callback to execute in Promise microtask
* @returns {void}
*/
function PromiseMicroTask(task) {
var _W$Promise;
(_W$Promise = W.Promise) === null || _W$Promise === void 0 ? void 0 : _W$Promise.resolve().then(task);
}
/**
* MutationObserverMicroTask
* ensures a task is queued as a microtask using the mutation observer.
* For each task added, the "bit" is flipped and the observer callback is scheduled
*/
function MutationObserverMicroTask() {
if (!W.MutationObserver) return function (task) {
return task();
};
var i = 0;
var taskQueue = [];
var observer = new W.MutationObserver(function MutationObserverCallback() {
taskQueue.forEach(function (task) {
return task();
});
taskQueue = [];
});
var node = document.createTextNode('');
observer.observe(node, {
characterData: true
});
return function (task) {
taskQueue.push(task);
node.data = String((i + 1) % 2);
};
}
/**
* We check for native code support and not a
* polyfill to ensure execution as micro-task
*/
function supportsPromises() {
var _ref, _W$Promise, _W$Promise$toString;
return (_ref = (W === null || W === void 0 ? void 0 : (_W$Promise = W.Promise) === null || _W$Promise === void 0 ? void 0 : (_W$Promise$toString = _W$Promise.toString()) === null || _W$Promise$toString === void 0 ? void 0 : _W$Promise$toString.indexOf('[native code]')) !== -1) !== null && _ref !== void 0 ? _ref : false;
}
function supportsMutationObserver() {
var _ref2, _W$MutationObserver, _W$MutationObserver$t;
return (_ref2 = (W === null || W === void 0 ? void 0 : (_W$MutationObserver = W.MutationObserver) === null || _W$MutationObserver === void 0 ? void 0 : (_W$MutationObserver$t = _W$MutationObserver.toString()) === null || _W$MutationObserver$t === void 0 ? void 0 : _W$MutationObserver$t.indexOf('[native code]')) !== -1) !== null && _ref2 !== void 0 ? _ref2 : false;
}
function CreateMicroTaskEnqueueFn() {
if (supportsPromises()) return PromiseMicroTask;
if (supportsMutationObserver()) return MutationObserverMicroTask();
return function (task) {
return task();
};
}
var QueueMicroTask = /*#__PURE__*/CreateMicroTaskEnqueueFn();
/**
* Execute callback in next browser task
* @param {function} callback
* @returns {number}
*/
function requestIdleCallbackPolyfill(callback) {
return W === null || W === void 0 ? void 0 : W.setTimeout(callback, 0);
}
/**
* Check for native code support to ensure execution as micro-task
* @returns {boolean}
*/
function supportsNativeIdleCallback(callback) {
var _callback$toString;
return ((_callback$toString = callback === null || callback === void 0 ? void 0 : callback.toString()) !== null && _callback$toString !== void 0 ? _callback$toString : '').indexOf('[native code]') !== -1;
}
var requestIdleCallback = /*#__PURE__*/supportsNativeIdleCallback(W === null || W === void 0 ? void 0 : W.requestIdleCallback) ? W.requestIdleCallback : requestIdleCallbackPolyfill;
function isSafari() {
return !!(typeof W.safari === 'object' && W.safari.pushNotification);
}
function IdleQueue(options) {
if (!options.capacity || typeof options.capacity !== 'number') {
throw new Error('IdleQueue requires capacity number initializer');
}
if (!options.onFlush || typeof options.onFlush !== 'function') {
throw new Error('IdleQueue requires onFlush function initializer');
}
var TASK_QUEUE = [];
var TASK_RESULTS = [];
/**
* Check current queue size
* @returns {number}
*/
function size() {
return TASK_QUEUE.length;
}
/**
* Stores and enqueues task to run as microtask
* @param {Function} task
* @returns {void}
*/
function enqueue(task) {
TASK_QUEUE.push(task);
scheduleTasksToRun();
}
/**
* Process a task and check if queue should be flushed
* @returns {void}
*/
function processTask() {
var task = TASK_QUEUE.shift();
if (!task) return;
TASK_RESULTS.push(task());
if (TASK_RESULTS.length >= options.capacity) {
flush();
}
}
/**
* For task to run based on document visibility state
* @returns {void}
*/
function scheduleTasksToRun() {
D.visibilityState === 'hidden' ? QueueMicroTask(processTask) : requestIdleCallback(processTask);
}
/**
* Take queue results and call the callback
* @returns {void}
*/
function flush() {
if (!TASK_RESULTS.length) return;
var RESULTS = [];
while (TASK_RESULTS.length > 0) {
var result = TASK_RESULTS.shift();
if (result) {
RESULTS.push(result);
}
}
QueueMicroTask(function () {
return options.onFlush(RESULTS);
});
}
/**
* Schedules tasks to be force processed in an event where browser pages might
* unload and we want to reliably report results to the backend.
* @returns {void}
*/
function forceProcessTasks() {
while (TASK_QUEUE.length) {
var task = TASK_QUEUE.shift();
if (task) {
TASK_RESULTS.push(task());
}
}
flush();
}
function onDocumentVisibilityChangeHandler() {
if (D.visibilityState === 'hidden') {
forceProcessTasks();
}
}
/**
* Destroys all observers and cleans up all queue side effects
* @returns {void}
*/
function destroy() {
D.removeEventListener('visibilitychange', onDocumentVisibilityChangeHandler);
if (isSafari()) {
W.removeEventListener('beforeunload', forceProcessTasks);
} // Clear all telemetry data that might be sitting in memory
forceProcessTasks();
}
/**
* Register pageVisibilityListener to report results during
* likely to be idle times such as switching between tabs.
*/
D.addEventListener('visibilitychange', onDocumentVisibilityChangeHandler, true);
if (isSafari()) {
W.addEventListener('beforeunload', forceProcessTasks, true);
}
return {
size: size,
enqueue: enqueue,
destroy: destroy,
results: TASK_RESULTS
};
}
var TELEMETRY_API = 'https://telemetry.algolia.com/1/collector';
/**
* Ensure sendBeacon is supported and was not either overriden or disabled by the browser
* @returns {boolean}
*/
function supportsSendBeacon() {
var _ref, _W$navigator;
return (_ref = typeof (W === null || W === void 0 ? void 0 : (_W$navigator = W.navigator) === null || _W$navigator === void 0 ? void 0 : _W$navigator.sendBeacon) === 'function') !== null && _ref !== void 0 ? _ref : false;
}
function supportsNativeFetch() {
var _W$fetch$toString, _W$fetch;
return ((_W$fetch$toString = (_W$fetch = W.fetch) === null || _W$fetch === void 0 ? void 0 : _W$fetch.toString()) !== null && _W$fetch$toString !== void 0 ? _W$fetch$toString : '').indexOf('[native code]') !== -1;
}
/**
* Separate sendWithFetch from report so we can add a simple integration
* test as report does not expose the API response
* @param payload
* @returns {Promise}
*/
function sendWithFetch(payload) {
return W.fetch(TELEMETRY_API, {
method: 'POST',
body: JSON.stringify(payload),
keepalive: true
});
}
/**
* In cases where neither Fetch or sendBeacon is supported we use XMLHttpRequest as a best
* effort fallback. It gives us no guarantee that the data will be sent in onunload cases,
* but it's better than not sending it at all.
* @param payload
* @returns {XMLHttpRequest}
*/
function sendWithXMLHttpRequest(payload) {
var request = new W.XMLHttpRequest();
request.open('POST', TELEMETRY_API);
request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
request.withCredentials = true;
request.send(JSON.stringify(payload));
return request;
}
/**
* Send data to backend server collection and return boolean indicating if the
* data sending was successfully enqueued to discourage anyone from reading the resp
* @param {APIPayload} payload
* @returns {boolean}
*/
function report(payload) {
if (supportsSendBeacon()) {
return W.navigator.sendBeacon(TELEMETRY_API, JSON.stringify(payload));
}
if (supportsNativeFetch()) {
sendWithFetch(payload);
return true;
} // Couldnt find a reliable transport layer, use XML as best effort, but treat it as a failue.
// There is also no need to check for XMLHttpRequest support as we can assume that in order
// for search to work, it will have to be defined anyways or
sendWithXMLHttpRequest(payload);
return false;
}
/**
* Reports errors to backend or destination of choice
* @param {Error[]} errors
* @returns {boolean}
*/
function errorsReporter(errors) {
var payload = {
o: W.location.hostname || null,
e: errors
};
return report(payload);
}
/**
* Reports telemetry to backend or destination of choice
* @param {Error[]} errors
* @returns {boolean}
*/
function telemetryReporter(entries) {
var payload = {
o: W.location.hostname || null,
d: entries
};
return report(payload);
}
function createReporter() {
return {
error: errorsReporter,
telemetry: telemetryReporter
};
}
/**
* Determines if url is "algolia" like - used to prefilter the possible matches with faster indexOf
* instead of executing a RegExp on each possible match (https://jsbench.me/yeka86z5ty/1)
* @param {string} url
* @returns {boolean}
*/
function isAlgoliaLikeResource(resource) {
return (resource.initiatorType === 'xmlhttprequest' || resource.initiatorType === 'fetch') && resource.name.indexOf('algolia') !== -1;
}
/**
* Checks if the query is targeting a cluster - this can happen with /queries calls
* where the requests targets a cluster and executes multiple sub-queries
* @param {string} url
* @returns {boolean}
*/
function isClusterQuery(url) {
return /\/\/([d|t|s|c][0-9]+)-\w+(-[0-9])?.algolia\.?net/.test(url);
}
/**
* Checks if the query contains the appid - this can happen with /query calls
* where the requests targets an application DNS entry to executes a single query
* @param {string} url
* @returns {boolean}
*/
function isApplicationIDQuery(url) {
return /\/\/\w+-(dsn|[1-3]).algolia\.?net/.test(url);
}
/**
* Determine if a given resource name should be collected by the telemetry
* @param {string} url
* @returns {boolean}
*/
function isAlgoliaResourceEntry(resource) {
if (!isAlgoliaLikeResource(resource)) return false;
if (isApplicationIDQuery(resource.name)) return true;
if (isClusterQuery(resource.name)) return true;
return false;
}
/**
* Check if performance API is supported and can be used to observe resource entries
* @returns boolean
*/
function supportsPerformanceAPI() {
var _ref, _W$performance;
return (_ref = typeof ((_W$performance = W.performance) === null || _W$performance === void 0 ? void 0 : _W$performance.getEntriesByType) === 'function') !== null && _ref !== void 0 ? _ref : false;
}
/**
* Returns resource entries that are related to Algolia
* @returns {PerformanceResourceTiming[]}
*/
function getAlgoliaResourceEntries() {
return supportsPerformanceAPI() ? W.performance.getEntriesByType('resource').filter(function (entry) {
return isAlgoliaResourceEntry(entry);
}) : [];
}
/**
* PerformanceObserver polyfill with an in-memory cache implementation
* @param {PerformanceObserverEntryCallback} callback
* @returns PerformanceObserver
*/
var PerformanceObserverPolyfill = function PerformanceObserverPolyfill(callback) {
var POLLING_INTERVAL = 2000;
var POLLING_INTERVAL_ID = 0;
var RESOURCE_ENTRIES_CACHE = {};
var isNewResourceEntry = function isNewResourceEntry(entry) {
return !RESOURCE_ENTRIES_CACHE[entry.startTime];
};
var checkForNewEntries = function checkForNewEntries() {
var newEntries = getAlgoliaResourceEntries().filter(function (resource) {
return isNewResourceEntry(resource);
});
newEntries.forEach(function (resource) {
RESOURCE_ENTRIES_CACHE[resource.startTime] = resource;
callback(resource);
});
};
var disconnect = function disconnect() {
if (POLLING_INTERVAL_ID) {
RESOURCE_ENTRIES_CACHE = {};
W.clearInterval(POLLING_INTERVAL_ID);
}
};
var takeRecords = function takeRecords() {
var entriesCopy = Object.values(RESOURCE_ENTRIES_CACHE);
RESOURCE_ENTRIES_CACHE = {};
return entriesCopy;
};
var observe = function observe() {
checkForNewEntries(); // @ts-ignore
POLLING_INTERVAL_ID = setInterval(checkForNewEntries, POLLING_INTERVAL);
};
return {
observe: observe,
takeRecords: takeRecords,
disconnect: disconnect
};
};
/**
* Check for native performance observer implementation - as our code is not crtical to
* the website functionality, we do not want to potentionally rely on a synchronous polyfill implementation
* @param observer {string|undefined}
*/
function requiresObserverPolyfill(observer) {
var _ref, _observer$toString, _observer$supportedEn;
return ((_ref = (_observer$toString = observer === null || observer === void 0 ? void 0 : observer.toString()) !== null && _observer$toString !== void 0 ? _observer$toString : '') === null || _ref === void 0 ? void 0 : _ref.indexOf('[native code]')) === -1 || // @ts-ignore
((_observer$supportedEn = observer === null || observer === void 0 ? void 0 : observer.supportedEntryTypes) !== null && _observer$supportedEn !== void 0 ? _observer$supportedEn : []).indexOf('resource') === -1;
}
/**
* Creates a new performance observer and instruments it to listen to algolia's resource requests
* @param {PerformanceObserverEntryCallback} callback
* @returns PerformanceObserver
*/
function createNativePerformanceObserver(callback) {
// @ts-ignore
var observer = new W.PerformanceObserver(function (list) {
list.getEntriesByType('resource') // Execute a regexp for accurate detection
.filter(function (entry) {
return isAlgoliaResourceEntry(entry);
}).forEach(function (entry) {
return callback(entry);
});
});
return observer;
}
/**
* Creates a new instance of our performance observer depending on browser support and polyfill presence
* @param {PerformanceObserverEntryCallback} callback
* @returns {void}
*/
function createPerformanceObserver(callback) {
return !requiresObserverPolyfill(W.PerformanceObserver) ? createNativePerformanceObserver(callback) : PerformanceObserverPolyfill(callback);
}
/**
* Create a new error entry to be reported to the backend
* @param {Response} response
* @returns {Error}
*/
function createErrorEntry(request, response, requestDetails) {
var err = {
r: parseResource(request.url),
sc: response.status,
m: response.content,
// * Warning *
// indicates if response was deemed as timed out by the Algolia client internal timeout, not the network!
// it is thus possible to have a 408 status code and response.isTimedOut = 0
// See https://github.com/algolia/algoliasearch-client-javascript/blob/master/packages/requester-browser-xhr/src/createBrowserXhrRequester.ts#L41-L53
to: response.isTimedOut ? 1 : 0,
ts: new Date().toISOString()
};
if (requestDetails === null || requestDetails === void 0 ? void 0 : requestDetails.appid) {
err.a = requestDetails.appid;
}
return err;
}
var PROBE_IDENTIFIER = 'probe=1';
function isProbeEntry(url) {
return url.indexOf(PROBE_IDENTIFIER) > -1;
}
/**
* Execute request to probed target and
* @param url {string}
* @param onSuccess {function}
* @param onError {function}
*/
function probe(url, onSuccess, onError) {
var probeIdentifierUrl = url.indexOf('?') > -1 ? url + "&" + PROBE_IDENTIFIER : url + "?" + PROBE_IDENTIFIER;
if (typeof W.fetch === 'function') {
W.fetch(probeIdentifierUrl, {
method: 'GET',
mode: 'cors'
}).then(function (resp) {
// Warning: This bit is important, if we do not read the response, it seems that
// it is never picked up by the Performance.getResourceTiming API. I could not find any official doc, but from the time being, it seems to be the case.
if (!resp.ok) throw resp;
return resp.json();
}).then(function (json) {
if (json) {
onSuccess(probeIdentifierUrl);
}
})["catch"](function (resp) {
var response = {
content: resp.statusText || 'Network request failed',
status: resp.status,
isTimedOut: false
};
onError(probeIdentifierUrl, response);
});
return;
}
var request = new W.XMLHttpRequest();
request.open('GET', probeIdentifierUrl, true);
request.onerror = function () {
var response = {
content: request.statusText || 'Network request failed',
status: request.status,
isTimedOut: false
};
onError(probeIdentifierUrl, response);
};
request.send();
}
/**
* Create a new telemetry entry to be reported to the backend
* @param {PerformanceResourceTiming} entry
* @returns {Telemetry}
*/
function createTelemetryEntry(entry, details) {
var telemetry = {
r: parseResource(entry.name),
d: entry.domainLookupEnd && entry.domainLookupStart ? roundToDecimals(entry.domainLookupEnd - entry.domainLookupStart, 0) : 0,
t: entry.requestStart && entry.responseEnd ? roundToDecimals(entry.responseEnd - entry.requestStart, 0) : 0,
sz: entry.transferSize ? roundToDecimals(toKiloBytes(entry.transferSize), 2) : 0,
ts: new Date().toISOString()
};
if (details && details.appid) {
telemetry.a = details.appid;
}
if (isProbeEntry(entry.name)) {
telemetry.p = 1;
}
return telemetry;
}
/**
* Create probe scheduler that will probe all urls. In case of errors, add them to error queue,
* successfull probing responses will be picked up by the telemetry observer
* @param targets {string[]}
* @param onError {function}
*/
function createProbeScheduler(targets, onError) {
var _targets$slice;
var timeoutID;
var timeout = isLowEndDevice() ? 3000 : 1000;
var stopProbingFlag = false;
var shallowCopyTargets = (_targets$slice = targets === null || targets === void 0 ? void 0 : targets.slice(0)) !== null && _targets$slice !== void 0 ? _targets$slice : [];
/**
* Recursively probe a list of targets
*/
function probeTargets() {
var nextProbeTarget = shallowCopyTargets.shift(); // Check if we have no more targets to probe or if probing was manually stopped.
if (!nextProbeTarget || stopProbingFlag) return; // If probe succeeds, schedule the next probe - the data will be picked up by the performance observer
var onProbeSuccess = deferedProbeTargets; // If probe errors, enqueue the error and schedule the next probe
var onProbeError = function onProbeError(url, response) {
onError(url, response);
deferedProbeTargets();
};
probe(nextProbeTarget, onProbeSuccess, onProbeError);
}
/**
* Defer each probe, so that we dont open too many connections
*/
function deferedProbeTargets() {
// @ts-ignore
timeoutID = setTimeout(probeTargets, timeout);
return;
}
function start() {
stopProbingFlag = false;
deferedProbeTargets();
}
/**
* Clear timeouts and set stop flag too true
*/
function stop() {
if (timeoutID) W.clearTimeout(timeoutID);
return stopProbingFlag = true;
}
return {
start: start,
stop: stop
};
}
/**
* Validate custom queue capacity param
* @param {unknown} capacity
* @returns {boolean}
*/
function isValidQueueCapacity(capacity) {
return typeof capacity === 'number' && capacity > 0;
}
/**
* Creates a new instance of the Telemetry Client with the possibility to set custom queue sizes
* and initializes the performance observers along with the queues relevant to data collection
* @param {object} options
* @returns {TelemetryClient}
*/
function createTelemetryClient(options) {
var _options, _options$settings;
if (options === void 0) {
options = {};
}
if (options.errorQueueCapacity !== undefined && !isValidQueueCapacity(options.errorQueueCapacity)) {
throw new Error("Invalid error queue capacity \"" + options.errorQueueCapacity + "\", queue capacity needs to be of type number.");
}
if (options.telemetryQueueCapacity !== undefined && !isValidQueueCapacity(options.telemetryQueueCapacity)) {
throw new Error("Invalid telemetry queue capacity \"" + options.telemetryQueueCapacity + "\", queue capacity needs to be of type number.");
}
var getReqDetails = options.getRequestDetails ? options.getRequestDetails : function (_url) {
return undefined;
}; // Use a custom reporter if data should be reported or manipulated with before sending to backend
var reporter = options.reporter || createReporter(); // Initialize both error and telemetry queues
var errorQueue = IdleQueue({
capacity: options.errorQueueCapacity || 4,
onFlush: reporter.error
});
var telemetryQueue = IdleQueue({
capacity: options.telemetryQueueCapacity || 10,
onFlush: reporter.telemetry
});
var performanceObserver = createPerformanceObserver(function (entry) {
telemetryQueue.enqueue(function () {
return createTelemetryEntry(entry, getReqDetails(entry.name));
});
});
performanceObserver.observe({
entryTypes: ['resource']
});
if (((_options = options) === null || _options === void 0 ? void 0 : (_options$settings = _options.settings) === null || _options$settings === void 0 ? void 0 : _options$settings.probe) && options.settings.probe.length > 0) {
var probing = createProbeScheduler(options.settings.probe, function (url, response) {
errorQueue.enqueue(function () {
return createErrorEntry({
url: url
}, response);
});
});
probing.start();
}
/**
* Destroys all queues, listeners and side-effects instrumented by telemetry
*/
function destroy() {
errorQueue.destroy();
telemetryQueue.destroy();
performanceObserver.disconnect();
}
return {
error: function error(request, response) {
return errorQueue.enqueue(function () {
return createErrorEntry(request, response, getReqDetails(request.url));
});
},
destroy: destroy
};
}
var SETTINGS_ENDPOINT = 'https://telemetry.algolia.com/1/settings';
/* By default, do not collect anything */
var DEFAULT_SETTINGS = {
collect: 0,
probe: []
};
/**
* Fetch settings from telemetry server and instrument telemetry
* @param onSucces
*/
function getSettings(applications, onSuccess) {
var settings = encodeURI(SETTINGS_ENDPOINT + "?" + (applications ? "applications=" + applications.join(',') : ''));
if (typeof W.fetch === 'function') {
W.fetch(settings, {
mode: 'cors',
credentials: 'include'
}).then(function (resp) {
if (resp.ok) return resp.json();
return DEFAULT_SETTINGS;
}).then(function (settings) {
return onSuccess(settings);
})["catch"](function () {
return onSuccess(DEFAULT_SETTINGS);
});
return;
}
var request = new W.XMLHttpRequest();
request.overrideMimeType('application/json');
request.withCredentials = true;
request.open('GET', settings, true);
request.onload = function () {
try {
onSuccess(JSON.parse(request.responseText));
} catch (e) {
onSuccess(DEFAULT_SETTINGS);
}
};
request.onerror = function () {
onSuccess(DEFAULT_SETTINGS);
};
request.send();
}
/**
* Decorates a requester instance with telemetry capabilities
* @param {object} options
* @returns {TelemetryRequester}
*/
function createBrowserTelemetryClient(options) {
if (options === void 0) {
options = {};
}
// Check if we are running in browser env, else show the warning
if (!isBrowserEnvironment()) {
console.warn("\n \uD83D\uDEA8Telemetry is not supported in non browser environments.\uD83D\uDEA8\n\n Please use a requester that is compliant with the environment you are running your application in.\n\n The value of window object was: " + W + "\n\n "); // @ts-ignore
return {
send: function send() {
return Promise.resolve();
},
destroy: function destroy() {
return null;
}
};
}
var requester = options.requester || requesterBrowserXhr.createBrowserXhrRequester();
/** slight hack ahead
* To be able to quickly return and allow the client to start making requests to algolia,
* we create a client *without* telemetry instrumentation and later on, after we've fetched the
* settings, override the implementation with the one that includes telemetry.
* This way, the lazy init of the client is transparent and non blocking.
*/
var telemetryClient;
var errorCache = [];
var requestDetails = {};
var wrapperClient = {
send: function send(request) {
requestDetails[request.url.toLowerCase()] = {
appid: getAppIDFromRequest(request)
};
return requester.send(request).then(function (response) {
if (response.status >= 400 || response.status === 0 || response.isTimedOut) {
telemetryClient ? telemetryClient.error(request, response) : errorCache.push({
request: request,
response: response
});
}
return response;
});
},
destroy: function destroy() {
if (telemetryClient) {
telemetryClient.destroy();
}
}
}; // Fetch settings and override client implementation
getSettings(options.applications, function (settings) {
if (Math.random() > settings.collect) {
// Telemetry is disabled, drop everything and collect no data
return;
}
telemetryClient = createTelemetryClient({
reporter: options.reporter,
telemetryQueueCapacity: options.telemetryQueueCapacity,
errorQueueCapacity: options.errorQueueCapacity,
settings: settings,
getRequestDetails: function getRequestDetails(url) {
return requestDetails[url.toLowerCase()];
}
}); // process all errors that have happened before settings were fetched and clear processed entries
errorCache.forEach(function (error) {
telemetryClient.error(error.request, error.response);
});
errorCache = [];
});
return wrapperClient;
}
exports.default = createBrowserTelemetryClient;
Object.defineProperty(exports, '__esModule', { value: true });
})));
//# sourceMappingURL=algolia-browser-telemetry.umd.development.js.map