UNPKG

@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/>

992 lines (814 loc) 30.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var requesterBrowserXhr = require('@algolia/requester-browser-xhr'); 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; //# sourceMappingURL=algolia-browser-telemetry.cjs.development.js.map