posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
243 lines • 11 kB
JavaScript
;
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