UNPKG

posthog-js

Version:

Posthog-js allows you to automatically capture usage and send events to PostHog.

243 lines • 11 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startNetworkMetrics = void 0; var core_1 = require("@posthog/core"); var general_utils_1 = require("@posthog/browser-common/utils/general-utils"); var request_utils_1 = require("@posthog/browser-common/utils/request-utils"); var globals_1 = require("@posthog/browser-common/utils/globals"); var logger_1 = require("@posthog/browser-common/utils/logger"); var request_1 = require("../request"); var patch_1 = require("./replay/rrweb-plugins/patch"); var logger = (0, logger_1.createLogger)('[NetworkMetrics]'); var DEFAULT_METRIC_NAME = 'http.client.request.duration'; // oxlint-disable-next-line compat/compat var now = function () { var _a; return (((_a = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.performance) === null || _a === void 0 ? void 0 : _a.now) ? globals_1.window.performance.now() : Date.now()); }; // All-digit segments, or hex/uuid-like segments of 8+ characters that contain a digit. var isIdLikeSegment = function (segment) { return /^\d+$/.test(segment) || (segment.length >= 8 && /^[0-9a-f-]*\d[0-9a-f-]*$/i.test(segment)); }; var templatePath = function (pathname) { return pathname .split('/') .map(function (segment) { return (isIdLikeSegment(segment) ? ':id' : segment); }) .join('/'); }; var DEFAULT_PORTS = { 'http:': 80, 'https:': 443 }; var KNOWN_HTTP_METHODS = new Set(['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']); var urlAttributes = function (url) { return ({ 'server.address': url.hostname.startsWith('[') && url.hostname.endsWith(']') ? url.hostname.slice(1, -1) : url.hostname, 'server.port': Number(url.port) || DEFAULT_PORTS[url.protocol], 'url.scheme': url.protocol.slice(0, -1), 'url.template': templatePath(url.pathname), }); }; // OTel: a 4xx or 5xx response reports its status as the error type; a request that // got no response reports the exception name, or `_OTHER` when there is none. var errorType = function (status, failure) { if (status) { return status >= 400 ? String(status) : undefined; } return (failure === null || failure === void 0 ? void 0 : failure.name) || '_OTHER'; }; var methodAttributes = function (method) { var original = KNOWN_HTTP_METHODS.has(method) ? method : '_OTHER'; return __assign({ 'http.request.method': original }, (original === '_OTHER' && method !== original ? { 'http.request.method_original': method } : {})); }; var networkConfig = function (instance) { var _a; var network = (_a = instance.config.metrics) === null || _a === void 0 ? void 0 : _a.network; return network === true ? {} : network || undefined; }; var observe = function (method, url) { var _a; var observedUrl = String(url); return { method: String(method).toUpperCase(), url: ((_a = (0, request_utils_1.convertToURL)(observedUrl)) === null || _a === void 0 ? void 0 : _a.href) || observedUrl, }; }; var record = function (instance, observed, status, start, enabled, failure, fulfilled) { var _a, _b; if (fulfilled === void 0) { fulfilled = false; } try { var config = enabled(); if (!config) { return; } // A status of 0 means no response arrived (fetch's opaque no-cors responses report it too), // which the callback contract calls `undefined`. Normalised once here for both transports. var normalisedStatus = status || undefined; var durationMs = now() - start; var url = (0, request_utils_1.convertToURL)(observed.url); if (url && url.protocol !== 'http:' && url.protocol !== 'https:') { return; } var request = { url: (url === null || url === void 0 ? void 0 : url.href) || observed.url, method: observed.method }; var name_1 = (0, core_1.isFunction)(config.name) ? config.name(request) : (0, core_1.isString)(config.name) ? config.name : DEFAULT_METRIC_NAME; if (!name_1) { return; } var error = fulfilled && !normalisedStatus ? undefined : errorType(normalisedStatus, failure); var attributes = __assign(__assign(__assign(__assign(__assign({}, methodAttributes(request.method)), (url ? urlAttributes(url) : {})), (normalisedStatus ? { 'http.response.status_code': normalisedStatus } : {})), (error ? { 'error.type': error } : {})), (_a = config.attributes) === null || _a === void 0 ? void 0 : _a.call(config, request, { status: normalisedStatus, durationMs: durationMs })); (_b = instance.metrics) === null || _b === void 0 ? void 0 : _b.histogram(name_1, durationMs, { unit: 'ms', attributes: attributes }); } catch (e) { logger.error('Failed to record network metric', e); } }; var noop = function () { }; var patchFetch = function (instance, enabled) { if (!(0, core_1.isFunction)(globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.fetch)) { return noop; } return (0, patch_1.patch)(globals_1.window, 'fetch', function (originalFetch) { return function () { var _a, _b, _c; var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var start = now(); var result = originalFetch.apply(this, args); if (!enabled()) { return result; } try { var _d = __read(args, 2), input = _d[0], init = _d[1]; var observed_1 = observe((_b = (_a = init === null || init === void 0 ? void 0 : init.method) !== null && _a !== void 0 ? _a : input === null || input === void 0 ? void 0 : input.method) !== null && _b !== void 0 ? _b : 'GET', (_c = input === null || input === void 0 ? void 0 : input.url) !== null && _c !== void 0 ? _c : input); return result.then(function (response) { record(instance, observed_1, response === null || response === void 0 ? void 0 : response.status, start, enabled, undefined, true); return response; }, function (error) { record(instance, observed_1, undefined, start, enabled, error); throw error; }); } catch (e) { logger.error('Failed to observe fetch', e); return result; } }; }); }; var patchXHR = function (instance, enabled) { var _a; var prototype = (_a = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.XMLHttpRequest) === null || _a === void 0 ? void 0 : _a.prototype; if (!prototype) { return noop; } var requests = new WeakMap(); var restoreOpen = (0, patch_1.patch)(prototype, 'open', function (originalOpen) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } try { if (!(0, request_1.isPostHogXHR)(this)) { requests.set(this, observe(args[0], args[1])); } } catch (e) { logger.error('Failed to observe XHR open', e); } return originalOpen.apply(this, args); }; }); var restoreSend = (0, patch_1.patch)(prototype, 'send', function (originalSend) { return function () { var _this = this; var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var onLoadEnd; try { var observed_2 = requests.get(this); if (observed_2 && enabled()) { var start_1 = now(); onLoadEnd = function () { _this.removeEventListener('loadend', onLoadEnd); record(instance, observed_2, _this.status, start_1, enabled); }; (0, general_utils_1.addEventListener)(this, 'loadend', onLoadEnd); } } catch (e) { logger.error('Failed to observe XHR send', e); } try { return originalSend.apply(this, args); } catch (e) { if (onLoadEnd) { this.removeEventListener('loadend', onLoadEnd); } throw e; } }; }); return function () { restoreOpen(); restoreSend(); }; }; /** * Records a duration histogram for every HTTP or HTTPS `fetch` and * `XMLHttpRequest` the page makes, except the SDK's own. Observation only: the * wrappers never change the request arguments or its settlement. Fetch returns * a derived promise so rejected requests remain observable to the caller and to * the browser's unhandled-rejection handling. * * Each transport is measured to the boundary its API exposes: a `fetch` promise * settles when the response headers arrive, and `loadend` fires after the whole * XHR response body. Aligning them would mean reading the fetch response body, * which an observer must not do. * * While `metrics.network` is off the wrappers pass every request straight * through. The returned function removes them; if another wrapper was layered * on top and ours cannot be spliced out, it stays in place as a pass-through. */ var startNetworkMetrics = function (instance) { var active = true; var enabled = function () { return (active ? networkConfig(instance) : undefined); }; var restoreFetch = patchFetch(instance, enabled); var restoreXHR = patchXHR(instance, enabled); return function () { active = false; restoreFetch(); restoreXHR(); }; }; exports.startNetworkMetrics = startNetworkMetrics; //# sourceMappingURL=network-metrics.js.map