UNPKG

posthog-js

Version:

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

1,144 lines • 56.2 kB
"use strict";
// Portions of this file are derived from getsentry/sentry-javascript
// Copyright (c) 2012 Functional Software, Inc. dba Sentry
// Licensed under the MIT License: https://github.com/getsentry/sentry-javascript/blob/develop/LICENSE
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
    return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRecordNetworkPlugin = exports.NETWORK_PLUGIN_NAME = exports.NEVER_RECORD_BODY_CONTENT_TYPES = void 0;
exports.findLast = findLast;
exports.shouldRecordBody = shouldRecordBody;
exports._contentLengthExceedsLimit = _contentLengthExceedsLimit;
exports._readBody = _readBody;
exports._tryReadBodyStreaming = _tryReadBodyStreaming;
var core_1 = require("@posthog/core");
var type_utils_1 = require("@posthog/browser-common/utils/type-utils");
var logger_1 = require("@posthog/browser-common/utils/logger");
var request_utils_1 = require("@posthog/browser-common/utils/request-utils");
var patch_1 = require("../rrweb-plugins/patch");
var denylist_1 = require("../../../extensions/replay/external/denylist");
var config_1 = require("./config");
var logger = (0, logger_1.createLogger)('[Recorder]');
var isNavigationTiming = function (entry) {
    return entry.entryType === 'navigation';
};
var isResourceTiming = function (entry) { return entry.entryType === 'resource'; };
function findLast(array, predicate) {
    var length = array.length;
    for (var i = length - 1; i >= 0; i -= 1) {
        if (predicate(array[i])) {
            return array[i];
        }
    }
    return undefined;
}
var noopHandler = function () {
    //
};
// a partial navigation entry is readable before load and the observer delivers it again once complete.
// readiness turns `complete` before the load event fires, so only a non-zero `loadEventEnd` is final
function isCompletedNavigationTiming(win, entry) {
    var _a;
    return isNavigationTiming(entry) && ((_a = win.document) === null || _a === void 0 ? void 0 : _a.readyState) === 'complete' && entry.loadEventEnd > 0;
}
// an `entryTypes` observation never delivers an entry that completed before it started
function completedNavigationEntries(win) {
    var _a;
    if (((_a = win.document) === null || _a === void 0 ? void 0 : _a.readyState) !== 'complete') {
        return [];
    }
    return win.performance
        .getEntriesByType('navigation')
        .filter(function (entry) { return isCompletedNavigationTiming(win, entry); });
}
// if we are only observing timings then we could have a single observer for all types, with buffer true,
// but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
// will deal with those.
// so we have a block which captures requests from before fetch/xhr is wrapped
// these are marked `isInitial` so playback can display them differently if needed
// they will never have method/status/headers/body because they are pre-wrapping that provides that
// resource entries stay behind `recordInitialRequests`, the navigation entry is the document load
// timing the waterfall is built from, so it is read whenever navigation is observed at all
function initialEntries(win, options) {
    if (options.recordInitialRequests) {
        return win.performance
            .getEntries()
            .filter(function (entry) {
            return isCompletedNavigationTiming(win, entry) ||
                (isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
        });
    }
    return options.performanceEntryTypeToObserve.includes('navigation') ? completedNavigationEntries(win) : [];
}
// undefined means this frame cannot observe performance entries, so the caller can tell
// "nothing to tear down" apart from "nothing started here"
function initPerformanceObserver(cb, win, options) {
    // Masking errors are isolated per record. Guard initial entry collection/preparation too:
    // rrweb tears down all registered observers if plugin initialization throws.
    try {
        cb({
            requests: initialEntries(win, options).flatMap(function (entry) {
                return prepareRequest(win, {
                    entry: entry,
                    method: undefined,
                    status: undefined,
                    networkRequest: {},
                    isInitial: true,
                });
            }),
            isInitial: true,
        });
    }
    catch (e) {
        logger.error('Failed to capture initial performance entries for network capture', e);
    }
    // some frames have no PerformanceObserver, or one without the static list of entry types,
    // so live network capture is not available there
    var performanceObserverClass = win.PerformanceObserver;
    var supportedEntryTypes = performanceObserverClass === null || performanceObserverClass === void 0 ? void 0 : performanceObserverClass.supportedEntryTypes;
    if (!performanceObserverClass || !(0, core_1.isArray)(supportedEntryTypes)) {
        return undefined;
    }
    var entryTypes = supportedEntryTypes.filter(function (x) { return options.performanceEntryTypeToObserve.includes(x); });
    if (!entryTypes.length) {
        // observe() throws when it is given no valid entry type
        return undefined;
    }
    var observer = new performanceObserverClass(function (entries) {
        // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
        // as the wrapped functions will do that. Otherwise, this filter becomes a noop
        // because we do want to record them here
        var wrappedInitiatorFilter = function (entry) {
            return options.recordBody || options.recordHeaders
                ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch'
                : true;
        };
        var performanceEntries = entries.getEntries().filter(function (entry) {
            return isNavigationTiming(entry) ||
                (isResourceTiming(entry) &&
                    options.initiatorTypes.includes(entry.initiatorType) &&
                    // TODO if we are _only_ capturing timing we don't want to filter initiator here
                    wrappedInitiatorFilter(entry));
        });
        cb({
            requests: performanceEntries.flatMap(function (entry) {
                return prepareRequest(win, { entry: entry, method: undefined, status: undefined, networkRequest: {} });
            }),
        });
    });
    // initial records are gathered above, so we don't need to observe and buffer each type separately
    observer.observe({ entryTypes: entryTypes });
    return function () {
        observer.disconnect();
    };
}
function shouldRecordHeaders(type, recordHeaders) {
    return !!recordHeaders && ((0, core_1.isBoolean)(recordHeaders) || recordHeaders[type]);
}
function isRequest(value) {
    if (typeof Request === 'undefined') {
        return false;
    }
    if (value instanceof Request) {
        return true;
    }
    try {
        return Object.prototype.toString.call(value) === '[object Request]';
    }
    catch (_a) {
        return false;
    }
}
// binary/asset bodies are large and useless for replay debugging (and capturing an image body
// duplicates what the recording already shows), so we never record them even when recordBody is on
exports.NEVER_RECORD_BODY_CONTENT_TYPES = [
    'image/',
    'video/',
    'audio/',
    'font/',
    'application/octet-stream',
    'application/pdf',
    'application/zip',
    'application/wasm',
];
function shouldRecordBody(_a) {
    var type = _a.type, recordBody = _a.recordBody, headers = _a.headers, url = _a.url;
    function matchesContentType(contentTypes) {
        var contentTypeHeader = Object.keys(headers).find(function (key) { return key.toLowerCase() === 'content-type'; });
        var contentType = contentTypeHeader && headers[contentTypeHeader];
        return contentTypes.some(function (ct) { return contentType === null || contentType === void 0 ? void 0 : contentType.toLowerCase().includes(ct); });
    }
    /**
     * particularly in canvas applications we see many requests to blob URLs
     * e.g. blob:https://video_url
     * these blob/object URLs are local to the browser, we can never capture that body
     * so we can just return false here
     */
    function isBlobURL(url) {
        try {
            if (typeof url === 'string') {
                return url.startsWith('blob:');
            }
            if (url instanceof URL) {
                return url.protocol === 'blob:';
            }
            if (isRequest(url)) {
                return isBlobURL(url.url);
            }
            return false;
        }
        catch (_a) {
            return false;
        }
    }
    if (!recordBody)
        return false;
    if (isBlobURL(url))
        return false;
    // never record binary/asset bodies, regardless of the recordBody setting
    if (matchesContentType(exports.NEVER_RECORD_BODY_CONTENT_TYPES))
        return false;
    if ((0, core_1.isBoolean)(recordBody))
        return true;
    if ((0, core_1.isArray)(recordBody))
        return matchesContentType(recordBody);
    var recordBodyType = recordBody[type];
    if ((0, core_1.isBoolean)(recordBodyType))
        return recordBodyType;
    return matchesContentType(recordBodyType);
}
function getRequestPerformanceEntry(win_1, initiatorType_1, url_1, start_1, end_1) {
    return __awaiter(this, arguments, void 0, function (win, initiatorType, url, start, end, attempt) {
        var urlPerformanceEntries, performanceEntry;
        if (attempt === void 0) { attempt = 0; }
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    if (attempt > 10) {
                        logger.warn('Failed to get performance entry for request', { url: url, initiatorType: initiatorType });
                        return [2 /*return*/, null];
                    }
                    urlPerformanceEntries = win.performance.getEntriesByName(url);
                    performanceEntry = findLast(urlPerformanceEntries, function (entry) {
                        return isResourceTiming(entry) &&
                            entry.initiatorType === initiatorType &&
                            ((0, core_1.isUndefined)(start) || entry.startTime >= start) &&
                            ((0, core_1.isUndefined)(end) || entry.startTime <= end);
                    });
                    if (!!performanceEntry) return [3 /*break*/, 2];
                    return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 50 * attempt); })];
                case 1:
                    _a.sent();
                    return [2 /*return*/, getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1)];
                case 2: return [2 /*return*/, performanceEntry];
            }
        });
    });
}
/**
 * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
 * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
 * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
 *
 * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
 */
function _tryReadXHRBody(_a) {
    var body = _a.body, options = _a.options, url = _a.url;
    if ((0, core_1.isNullish)(body)) {
        return null;
    }
    var _b = (0, denylist_1.isHostOnDenyList)(url, options), hostname = _b.hostname, isHostDenied = _b.isHostDenied;
    if (isHostDenied) {
        return hostname + ' is in deny list';
    }
    if ((0, core_1.isString)(body)) {
        return body;
    }
    if ((0, type_utils_1.isDocument)(body)) {
        return body.textContent;
    }
    if ((0, core_1.isFormData)(body)) {
        return (0, request_utils_1.formDataToQuery)(body);
    }
    if ((0, core_1.isObject)(body)) {
        try {
            return JSON.stringify(body);
        }
        catch (_c) {
            return '[SessionReplay] Failed to stringify response object';
        }
    }
    return '[SessionReplay] Cannot read body of type ' + toString.call(body);
}
function initXhrObserver(cb, win, options) {
    if (!options.initiatorTypes.includes('xmlhttprequest')) {
        return function () {
            //
        };
    }
    var recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
    var recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
    var restorePatch = (0, patch_1.patch)(win.XMLHttpRequest.prototype, 'open', 
    // oxlint-disable-next-line typescript/ban-ts-comment
    // @ts-ignore
    function (originalOpen) {
        return function (method, url, async, username, password) {
            if (async === void 0) { async = true; }
            // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
            // oxlint-disable-next-line typescript/ban-ts-comment
            // @ts-ignore
            var xhr = this;
            // All of the capture instrumentation below runs _before_ we delegate to the original
            // `open`. If any of it throws (e.g. `new Request(url)` rejecting a URL/method the host
            // application would have handled), we must not let that exception escape into the host's
            // `xhr.open()` call and misattribute a failure to session replay. Wrap it so that if
            // instrumentation fails we degrade gracefully and the original request still proceeds.
            try {
                // check IE earlier than this, we only initialize if Request is present
                var req_1 = new Request(url);
                var networkRequest_1 = {};
                var start_1;
                var end_1;
                var requestHeaders_1 = {};
                var originalSetRequestHeader_1 = xhr.setRequestHeader.bind(xhr);
                xhr.setRequestHeader = function (header, value) {
                    requestHeaders_1[header] = value;
                    return originalSetRequestHeader_1(header, value);
                };
                if (recordRequestHeaders) {
                    networkRequest_1.requestHeaders = requestHeaders_1;
                }
                var originalSend_1 = xhr.send.bind(xhr);
                xhr.send = function (body) {
                    if (shouldRecordBody({
                        type: 'request',
                        headers: requestHeaders_1,
                        url: url,
                        recordBody: options.recordBody,
                    })) {
                        networkRequest_1.requestBody = _tryReadXHRBody({ body: body, options: options, url: url });
                    }
                    start_1 = win.performance.now();
                    return originalSend_1(body);
                };
                // Cleanup function to remove all event listeners and prevent memory leaks
                var cleanup_1 = function () {
                    xhr.removeEventListener('readystatechange', readyStateListener_1);
                    xhr.removeEventListener('error', cleanup_1);
                    xhr.removeEventListener('abort', cleanup_1);
                    xhr.removeEventListener('timeout', cleanup_1);
                };
                var readyStateListener_1 = function () {
                    if (xhr.readyState !== xhr.DONE) {
                        return;
                    }
                    // Clean up all listeners immediately when done to prevent memory leaks
                    cleanup_1();
                    end_1 = win.performance.now();
                    var responseHeaders = {};
                    var rawHeaders = xhr.getAllResponseHeaders();
                    var headers = rawHeaders.trim().split(/[\r\n]+/);
                    headers.forEach(function (line) {
                        var parts = line.split(': ');
                        var header = parts.shift();
                        var value = parts.join(': ');
                        if (header) {
                            responseHeaders[header] = value;
                        }
                    });
                    if (recordResponseHeaders) {
                        networkRequest_1.responseHeaders = responseHeaders;
                    }
                    if (shouldRecordBody({
                        type: 'response',
                        headers: responseHeaders,
                        url: url,
                        recordBody: options.recordBody,
                    })) {
                        networkRequest_1.responseBody = _tryReadXHRBody({ body: xhr.response, options: options, url: url });
                    }
                    getRequestPerformanceEntry(win, 'xmlhttprequest', req_1.url, start_1, end_1)
                        .then(function (entry) {
                        var requests = prepareRequest(win, {
                            entry: entry,
                            method: method,
                            status: xhr === null || xhr === void 0 ? void 0 : xhr.status,
                            networkRequest: networkRequest_1,
                            start: start_1,
                            end: end_1,
                            url: url.toString(),
                            initiatorType: 'xmlhttprequest',
                        });
                        cb({ requests: requests });
                    })
                        .catch(function () {
                        //
                    });
                };
                // This is very tricky code, and making it passive won't bring many performance benefits,
                // so let's ignore the rule here.
                // oxlint-disable-next-line posthog-js/no-add-event-listener
                xhr.addEventListener('readystatechange', readyStateListener_1);
                // Also clean up on error, abort, and timeout to prevent memory leaks
                // oxlint-disable-next-line posthog-js/no-add-event-listener
                xhr.addEventListener('error', cleanup_1);
                // oxlint-disable-next-line posthog-js/no-add-event-listener
                xhr.addEventListener('abort', cleanup_1);
                // oxlint-disable-next-line posthog-js/no-add-event-listener
                xhr.addEventListener('timeout', cleanup_1);
            }
            catch (e) {
                logger.error('Failed to instrument XHR for network capture', e);
            }
            originalOpen.call(xhr, method, url.toString(), async, username, password);
        };
    });
    return function () {
        restorePatch();
    };
}
/**
 *  Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
 *  NB PerformanceNavigationTiming extends PerformanceResourceTiming
 *  Here we don't care which interface it implements as both expose `serverTimings`
 */
var exposesServerTiming = function (event) {
    return !(0, core_1.isNull)(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
};
// entry times count from the recorded frame's time origin, so the epoch base must come from that frame's clock
function prepareRequest(win, _a) {
    var e_1, _b;
    var entry = _a.entry, method = _a.method, status = _a.status, networkRequest = _a.networkRequest, isInitial = _a.isInitial, start = _a.start, end = _a.end, url = _a.url, initiatorType = _a.initiatorType;
    start = entry ? entry.startTime : start;
    end = entry ? entry.responseEnd : end;
    // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
    // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
    // can't start observer if performance.now() is not available
    var timeOrigin = Math.floor(Date.now() - win.performance.now());
    // clickhouse can't ingest timestamps that are floats
    // (in this case representing fractions of a millisecond we don't care about anyway)
    // use timeOrigin if we really can't gather a start time
    var timestamp = Math.floor(timeOrigin + (start || 0));
    // toJSON() keeps browser objects such as PerformanceServerTiming,
    // and postMessage from a cross-origin iframe cannot clone them
    var entryJSON = entry ? JSON.parse(JSON.stringify(entry.toJSON())) : { name: url };
    var requests = [
        __assign(__assign({}, entryJSON), { startTime: (0, core_1.isUndefined)(start) ? undefined : Math.round(start), endTime: (0, core_1.isUndefined)(end) ? undefined : Math.round(end), timeOrigin: timeOrigin, timestamp: timestamp, method: method, initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined, status: status, requestHeaders: networkRequest.requestHeaders, requestBody: networkRequest.requestBody, responseHeaders: networkRequest.responseHeaders, responseBody: networkRequest.responseBody, isInitial: isInitial }),
    ];
    if (exposesServerTiming(entry)) {
        try {
            for (var _c = __values(entry.serverTiming || []), _d = _c.next(); !_d.done; _d = _c.next()) {
                var timing = _d.value;
                requests.push({
                    timeOrigin: timeOrigin,
                    timestamp: timestamp,
                    startTime: Math.round(entry.startTime),
                    name: timing.name,
                    duration: timing.duration,
                    // the spec has a closed list of possible types
                    // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
                    // but, we need to know this was a server timing so that we know to
                    // match it to the appropriate navigation or resource timing
                    // that matching will have to be on timestamp and $current_url
                    entryType: 'serverTiming',
                });
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
            }
            finally { if (e_1) throw e_1.error; }
        }
    }
    return requests;
}
var contentTypePrefixDenyList = ['video/', 'audio/'];
function _checkForCannotReadResponseBody(_a) {
    var _b;
    var r = _a.r, options = _a.options, url = _a.url;
    if (r.headers.get('Transfer-Encoding') === 'chunked') {
        return 'Chunked Transfer-Encoding is not supported';
    }
    // `get` and `has` are case-insensitive
    // but return the header value with the casing that was supplied
    var contentType = (_b = r.headers.get('Content-Type')) === null || _b === void 0 ? void 0 : _b.toLowerCase();
    var contentTypeIsDenied = contentTypePrefixDenyList.some(function (prefix) { return contentType === null || contentType === void 0 ? void 0 : contentType.startsWith(prefix); });
    if (contentType && contentTypeIsDenied) {
        return "Content-Type ".concat(contentType, " is not supported");
    }
    var _c = (0, denylist_1.isHostOnDenyList)(url, options), hostname = _c.hostname, isHostDenied = _c.isHostDenied;
    if (isHostDenied) {
        return hostname + ' is in deny list';
    }
    return null;
}
function isReadableStreamBody(body) {
    return (0, core_1.isObject)(body) && (0, core_1.isFunction)(body.getReader) && (0, core_1.isFunction)(body.tee);
}
// reading a body must never hold up the page's request for long, so we cap every read attempt
var BODY_READ_TIMEOUT_MS = 500;
var BODY_READ_TIMEOUT_MESSAGE = '[SessionReplay] Timeout while trying to read body';
var BODY_READ_FAILED_MESSAGE = '[SessionReplay] Failed to read body';
function bodyReadFailedMessage(reason) {
    return "".concat(BODY_READ_FAILED_MESSAGE, ": ").concat(reason);
}
function bodyTooLargeMessage(limitBytes) {
    return "[SessionReplay] Body too large to record (> ".concat(limitBytes, " bytes)");
}
// when the body declares a content-length over the limit we can skip reading it entirely.
// this trusts content-length the same way config.ts enforcePayloadSizeLimit does.
// known accepted risk: for a compressed response content-length is the compressed size while the
// streaming reader counts decoded bytes, so a body that is over the limit compressed but under it
// decoded is dropped to the placeholder rather than recorded. it never breaks the page, so we accept it.
function _contentLengthExceedsLimit(r, limitBytes) {
    var _a, _b;
    try {
        var headerValue = (_b = (_a = r.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'content-length');
        if (!headerValue) {
            return false;
        }
        var contentLength = parseInt(headerValue, 10);
        return Number.isFinite(contentLength) && contentLength > limitBytes;
    }
    catch (_c) {
        return false;
    }
}
function _readBody(r, options) {
    if (!options.streamNetworkBody) {
        return _tryReadBody(r);
    }
    var limitBytes = (0, config_1.effectivePayloadLimitBytes)(options);
    // skip the read for bodies the headers already tell us are over the limit
    if (_contentLengthExceedsLimit(r, limitBytes)) {
        return Promise.resolve(bodyTooLargeMessage(limitBytes));
    }
    return _tryReadBodyStreaming(r, limitBytes);
}
function _tryReadBody(r) {
    // there are now already multiple places where we're using Promise...
    // oxlint-disable-next-line compat/compat
    return new Promise(function (resolve) {
        var timeout = setTimeout(function () { return resolve(BODY_READ_TIMEOUT_MESSAGE); }, BODY_READ_TIMEOUT_MS);
        try {
            r.clone()
                .text()
                .then(function (txt) { return resolve(txt); }, function (reason) { return resolve(bodyReadFailedMessage(reason)); })
                .finally(function () { return clearTimeout(timeout); });
        }
        catch (_a) {
            clearTimeout(timeout);
            resolve(BODY_READ_FAILED_MESSAGE);
        }
    });
}
function concatChunks(chunks, totalBytes) {
    var e_2, _a;
    var merged = new Uint8Array(totalBytes);
    var offset = 0;
    try {
        for (var chunks_1 = __values(chunks), chunks_1_1 = chunks_1.next(); !chunks_1_1.done; chunks_1_1 = chunks_1.next()) {
            var chunk = chunks_1_1.value;
            merged.set(chunk, offset);
            offset += chunk.byteLength;
        }
    }
    catch (e_2_1) { e_2 = { error: e_2_1 }; }
    finally {
        try {
            if (chunks_1_1 && !chunks_1_1.done && (_a = chunks_1.return)) _a.call(chunks_1);
        }
        finally { if (e_2) throw e_2.error; }
    }
    return merged;
}
// Reads a clone of the body chunk by chunk, stopping as soon as the running total exceeds the
// limit, so a very large body is never fully buffered. Like _tryReadBody it only ever reads a
// clone (never the stream the page consumes) and is guaranteed to resolve, never reject.
function _tryReadBodyStreaming(r, limitBytes) {
    // oxlint-disable-next-line compat/compat
    return new Promise(function (resolve) {
        var settled = false;
        var reader;
        function cancel() {
            try {
                void (reader === null || reader === void 0 ? void 0 : reader.cancel().catch(function () { }));
            }
            catch (_a) {
                // the reader may already be released; nothing to clean up
            }
        }
        // resolving always releases the reader, so a slow/hung stream stops being pulled (and stops
        // buffering the clone) the moment we settle — whether via success, the cap, an error, or the timeout
        function done(value) {
            if (settled) {
                return;
            }
            settled = true;
            clearTimeout(timeout);
            cancel();
            resolve(value);
        }
        var timeout = setTimeout(function () { return done(BODY_READ_TIMEOUT_MESSAGE); }, BODY_READ_TIMEOUT_MS);
        var clone;
        try {
            clone = r.clone();
        }
        catch (_a) {
            done(BODY_READ_FAILED_MESSAGE);
            return;
        }
        var body = clone.body;
        // no readable stream (or no TextDecoder) available — fall back to the buffered read of the clone
        if (!isReadableStreamBody(body) || typeof TextDecoder === 'undefined') {
            try {
                clone.text().then(function (txt) { return done(txt); }, function (reason) { return done(bodyReadFailedMessage(reason)); });
            }
            catch (_b) {
                done(BODY_READ_FAILED_MESSAGE);
            }
            return;
        }
        try {
            reader = body.getReader();
        }
        catch (_c) {
            done(BODY_READ_FAILED_MESSAGE);
            return;
        }
        var chunks = [];
        var received = 0;
        function pump() {
            reader.read().then(function (_a) {
                var streamDone = _a.done, value = _a.value;
                // already resolved (e.g. the timeout fired) — stop reading and release the stream
                if (settled) {
                    cancel();
                    return;
                }
                if (streamDone) {
                    done(new TextDecoder().decode(concatChunks(chunks, received)));
                    return;
                }
                if (value) {
                    if (received + value.byteLength > limitBytes) {
                        done(bodyTooLargeMessage(limitBytes));
                        return;
                    }
                    received += value.byteLength;
                    chunks.push(value);
                }
                pump();
            }, function (reason) { return done(bodyReadFailedMessage(reason)); });
        }
        pump();
    });
}
function _tryReadRequestBody(_a) {
    return __awaiter(this, arguments, void 0, function (_b) {
        var _c, hostname, isHostDenied;
        var r = _b.r, options = _b.options, url = _b.url;
        return __generator(this, function (_d) {
            _c = (0, denylist_1.isHostOnDenyList)(url, options), hostname = _c.hostname, isHostDenied = _c.isHostDenied;
            if (isHostDenied) {
                return [2 /*return*/, Promise.resolve(hostname + ' is in deny list')];
            }
            return [2 /*return*/, _readBody(r, options)];
        });
    });
}
function _tryReadResponseBody(_a) {
    return __awaiter(this, arguments, void 0, function (_b) {
        var cannotReadBodyReason;
        var r = _b.r, options = _b.options, url = _b.url;
        return __generator(this, function (_c) {
            cannotReadBodyReason = _checkForCannotReadResponseBody({ r: r, options: options, url: url });
            if (!(0, core_1.isNull)(cannotReadBodyReason)) {
                return [2 /*return*/, Promise.resolve(cannotReadBodyReason)];
            }
            return [2 /*return*/, _readBody(r, options)];
        });
    });
}
// Preserve the pre-#5200 wrapper unless the private init option explicitly opts in.
// Keep this path unchanged: publishing a new unversioned recorder must not opt customers in.
function initLegacyFetchObserver(cb, win, options) {
    if (!options.initiatorTypes.includes('fetch')) {
        return function () {
            //
        };
    }
    var recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
    var recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
    // oxlint-disable-next-line typescript/ban-ts-comment
    // @ts-ignore
    var restorePatch = (0, patch_1.patch)(win, 'fetch', function (originalFetch) {
        return function (url, init) {
            return __awaiter(this, void 0, void 0, function () {
                var req, res, networkRequest, start, end, requestHeaders_2, requestBodyIsReadableStream, _a, e_3, _b, responseHeaders_1, _c, e_4;
                return __generator(this, function (_d) {
                    switch (_d.label) {
                        case 0:
                            try {
                                // check IE earlier than this, we only initialize if Request is present
                                req = new Request(url, init);
                            }
                            catch (e) {
                                logger.error('Failed to instrument fetch for network capture', e);
                                return [2 /*return*/, originalFetch(url, init)];
                            }
                            networkRequest = {};
                            _d.label = 1;
                        case 1:
                            _d.trys.push([1, , 16, 17]);
                            _d.label = 2;
                        case 2:
                            _d.trys.push([2, 5, , 6]);
                            requestHeaders_2 = {};
                            req.headers.forEach(function (value, header) {
                                requestHeaders_2[header] = value;
                            });
                            if (recordRequestHeaders) {
                                networkRequest.requestHeaders = requestHeaders_2;
                            }
                            requestBodyIsReadableStream = isReadableStreamBody(init === null || init === void 0 ? void 0 : init.body);
                            if (!(!requestBodyIsReadableStream &&
                                shouldRecordBody({
                                    type: 'request',
                                    headers: requestHeaders_2,
                                    url: url,
                                    recordBody: options.recordBody,
                                }))) return [3 /*break*/, 4];
                            _a = networkRequest;
                            return [4 /*yield*/, _tryReadRequestBody({ r: req, options: options, url: url })];
                        case 3:
                            _a.requestBody = _d.sent();
                            _d.label = 4;
                        case 4: return [3 /*break*/, 6];
                        case 5:
                            e_3 = _d.sent();
                            logger.error('Failed to record fetch request for network capture', e_3);
                            return [3 /*break*/, 6];
                        case 6:
                            start = win.performance.now();
                            if (!isRequest(url)) return [3 /*break*/, 8];
                            return [4 /*yield*/, originalFetch(req)];
                        case 7:
                            _b = _d.sent();
                            return [3 /*break*/, 10];
                        case 8: return [4 /*yield*/, originalFetch(url, init)];
                        case 9:
                            _b = _d.sent();
                            _d.label = 10;
                        case 10:
                            // Use `req` for recording metadata/body only. For fetch(url, init), do not pass this internally-created
                            // Request downstream: it exposes request.body as a ReadableStream, and wrappers that forward that body
                            // can trigger Safari's "ReadableStream uploading is not supported" error. For fetch(Request), we must
                            // pass the cloned Request because constructing `req` may consume the original Request body.
                            res = _b;
                            end = win.performance.now();
                            _d.label = 11;
                        case 11:
                            _d.trys.push([11, 14, , 15]);
                            responseHeaders_1 = {};
                            res.headers.forEach(function (value, header) {
                                responseHeaders_1[header] = value;
                            });
                            if (recordResponseHeaders) {
                                networkRequest.responseHeaders = responseHeaders_1;
                            }
                            if (!shouldRecordBody({
                                type: 'response',
                                headers: responseHeaders_1,
                                url: url,
                                recordBody: options.recordBody,
                            })) return [3 /*break*/, 13];
                            _c = networkRequest;
                            return [4 /*yield*/, _tryReadResponseBody({ r: res, options: options, url: url })];
                        case 12:
                            _c.responseBody = _d.sent();
                            _d.label = 13;
                        case 13: return [3 /*break*/, 15];
                        case 14:
                            e_4 = _d.sent();
                            logger.error('Failed to record fetch response for network capture', e_4);
                            return [3 /*break*/, 15];
                        case 15: return [2 /*return*/, res];
                        case 16:
                            getRequestPerformanceEntry(win, 'fetch', req.url, start, end)
                                .then(function (entry) {
                                var requests = prepareRequest(win, {
                                    entry: entry,
                                    method: req.method,
                                    status: res === null || res === void 0 ? void 0 : res.status,
                                    networkRequest: networkRequest,
                                    start: start,
                                    end: end,
                                    url: req.url,
                                    initiatorType: 'fetch',
                                });
                                cb({ requests: requests });
                            })
                                .catch(function () {
                                //
                            });
                            return [7 /*endfinally*/];
                        case 17: return [2 /*return*/];
                    }
                });
            });
        };
    });
    return function () {
        restorePatch();
    };
}
function initFetchObserver(cb, win, options) {
    if (!options.initiatorTypes.includes('fetch')) {
        return function () {
            //
        };
    }
    var recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
    var recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
    // oxlint-disable-next-line typescript/ban-ts-comment
    // @ts-ignore
    var restorePatch = (0, patch_1.patch)(win, 'fetch', function (originalFetch) {
        return function (url, init) {
            var _this = this;
            // Constructing the capture Request happens _before_ we delegate to the original fetch, so
            // if it throws (e.g. a URL/method the host application would have handled) we must not let
            // that exception escape and misattribute a failure to session replay. Degrade gracefully and
            // let the original request still proceed.
            var req;
            try {
                // check IE earlier than this, we only initialize if Request is present
                req = new Request(url, init);
            }
            catch (e) {
                logger.error('Failed to instrument fetch for network capture', e);
                try {
                    // oxlint-disable-next-line compat/compat
                    return Promise.resolve(originalFetch(url, init));
                }
                catch (fetchError) {
                    // oxlint-disable-next-line compat/compat
                    return new Promise(function (_resolve, reject) { return reject(fetchError); });
                }
            }
            var networkRequest = {};
            var start;
            var end;
            // Start body capture without awaiting it. The host fetch must be invoked in the application's
            // synchronous call stack so a native rejection retains the application call site.
            // Body capture must reach req.clone() before its first yield: downstream fetch can consume req
            // immediately for Request inputs, making any later clone fail.
            var requestCapture = (function () { return __awaiter(_this, void 0, void 0, function () {
                var requestHeaders_3, requestBodyIsReadableStream, _a, e_5;
                return __generator(this, function (_b) {
                    switch (_b.label) {
                        case 0:
                            _b.trys.push([0, 3, , 4]);
                            requestHeaders_3 = {};
                            req.headers.forEach(function (value, header) {
                                requestHeaders_3[header] = value;
                            });
                            if (recordRequestHeaders) {
                                networkRequest.requestHeaders = requestHeaders_3;
                            }
                            requestBodyIsReadableStream = isReadableStreamBody(init === null || init === void 0 ? void 0 : init.body);
                            if (!(!requestBodyIsReadableStream &&
                                shouldRecordBody({
                                    type: 'request',
                                    headers: requestHeaders_3,
                                    url: url,
                                    recordBody: options.recordBody,
                                }))) return [3 /*break*/, 2];
                            _a = networkRequest;
                            return [4 /*yield*/, _tryReadRequestBody({ r: req, options: options, url: url })];
                        case 1:
                            _a.requestBody = _b.sent();
                            _b.label = 2;
                        case 2: return [3 /*break*/, 4];
                        case 3:
                            e_5 = _b.sent();
                            logger.error('Failed to record fetch request for network capture', e_5);
                            return [3 /*break*/, 4];
                        case 4: return [2 /*return*/];
                    }
                });
            }); })();
            var captureResponse = function (res) { return __awaiter(_this, void 0, void 0, function () {
                var responseHeaders_2, _a, e_6;
                return __generator(this, function (_b) {
                    switch (_b.label) {
                        case 0:
                            _b.trys.push([0, 3, , 4]);
                            responseHeaders_2 = {};
                            res.headers.forEach(function (value, header) {
                                responseHeaders_2[header] = value;
                            });
                            if (recordResponseHeaders) {
                                networkRequest.responseHeaders = responseHeaders_2;
                            }
                            if (!shouldRecordBody({
                                type: 'response',
                                headers: responseHeaders_2,
                                url: url,
                                recordBody: options.recordBody,
                            })) return [3 /*break*/, 2];
                            _a = networkRequest;
                            return [4 /*yield*/, _tryReadResponseBody({ r: res, options: options, url: url })];
                        case 1:
                            _a.responseBody = _b.sent();
                            _b.label = 2;
                        case 2: return [3 /*break*/, 4];
                        case 3:
                            e_6 = _b.sent();
                            logger.error('Failed to record fetch response for network capture', e_6);
                            return [3 /*break*/, 4];
                        case 4: return [2 /*return*/];
                    }
                });
            }); };
            var captureTiming = function (res) {
                return getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(function (entry) {
                    var requests = prepareRequest(win, {
                        entry: entry,
                        method: req.method,
                        status: res === null || res === void 0 ? void 0 : res.status,
                        networkRequest: networkRequest,
                        start: start,
                        end: end,
                        url: req.url,
                        initiatorType: 'fetch',
                    });
                    cb({ requests: requests });
                });
            };
            try {
                start = win.performance.now();
            }
            catch (_a) {
                // Missing timing data must not prevent the host fetch.
            }
            var fetchPromise;
            try {
                // Use `req` for recording metadata/body only. For fetch(url, init), do not pass this internally-created
                // Request downstream: it exposes request.body as a ReadableStream, and wrappers that forward that body
                // can trigger Safari's "ReadableStream uploading is not supported" error. For fetch(Request), we must
                // pass the cloned Request because constructing `req` may consume the original Request body.
                fetchPromise = isRequest(url) ? originalFetch(req) : originalFetch(url, init);
            }
            catch (e) {
                // The previous async wrapper converted synchronous downstream throws into rejected promises.
                // oxlint-disable-next-line compat/compat
                fetchPromise = new Promise(function (_resolve, reject) { return reject(e); });
            }
            // Attach both handlers immediately so a fast rejection is always observed while request body capture
            // finishes. Calling the host fetch above without first yielding preserves the application's call site.
            // oxlint-disable-next-line compat/compat
            var fetchResult = Promise.resolve(fetchPromise).then(function (response) {
                try {
                    end = win.performance.now();
                }
                catch (_a) {
                    // Missing timing data must not affect the response.
                }
                return { status: 'fulfilled', response: response };
            }, function (error) { return ({ status: 'rejected', error: error }); });
            return (function () { return __awaiter(_this, void 0, void 0, function () {
                var response, result;
                return __generator(this, function (_a) {
                    switch (_a.label) {
                        case 0:
                            _a.trys.push([0, , 4, 5]);
                            return [4 /*yield*/, requestCapture];
                        case 1:
                            _a.sent();
                            return [4 /*yield*/, fetchResult];
                        case 2:
                            result = _a.sent();
                            if (result.status === 'rejected') {
                                throw result.error;
                            }
                            response = result.response;
                            return [4 /*yield*/, captureResponse(response)];
                        case 3:
                            _a.sent();
                            return [2 /*return*/, response];
                        case 4:
                            void captureTiming(response).catch(function () {
                                // Recording failures must never affect the host fetch or create unhandled rejections.
                            });
                            return [7 /*endfinally*/];
                        case 5: return [2 /*return*/];
                    }
                });
            }); })();
        };
    });
    return function () {
        restorePatch();
    };
}
var initialisedHandler = null;
function initNetworkObserver(callback, win, // top window or in an iframe
options) {
    if (!('performance' in win)) {
        return function () {
            //
        };
    }
    if (initialisedHandler) {
        logger.warn('Network observer already initialised, doing nothing');
        return function () {
            // the first caller should already have this handler and will be responsible for teardown
        };
    }
    var networkOptions = (options ? Object.assign({}, config_1.defaultNetworkOptions, options) : config_1.defaultNetworkOptions);
    var active = true;
    var maskingFailureLogged = false;
    var cb = function (data) {
        // Body reads and timing lookups can finish after this observer is replaced.
        if (!active) {
            return;
        }
        var requests = [];
        var parentRequestDropped = false;
        data.requests.forEach(function (request) {
            var isServerTiming = request.entryType === 'serverTiming';
            // Server timings are emitted immediately after their resource or navigation entry.
            // If a parent is dropped (i.e. a PostHog ingestion request), derived timings must too or an endless capture loop ensues.
            if (isServerTiming && parentRequestDropped) {
                return;
            }
            var maskedRequest;
            try {
                maskedRequest = networkOptions.maskRequestFn(request);
            }
            catch (_a) {
                if (!maskingFailureLogged) {
                    maskingFailureLogged = true;
                    logger.warn('Network capture masking callback failed; dropping the record. Further masking failures will not be logged for this observer.');
                }
                if (!isServerTiming) {
                    parentRequestDropped = true;
                }
                return;
            }
            if (!isServerTiming) {
                // A null-filtered initial parent is replaced by a strict URL-less fallback so replay keeps its
                // required timing metadata. Treat that fallback as dropped for derived server timings, which
                // can still contain customer-controlled names and durations.
                parentRequestDropped = !maskedRequest || (0, config_1.isInitialMaskFallback)(maskedRequest);
            }
            if (maskedRequest) {
                requests.push(maskedRequest);
            }
        });
        if (requests.length > 0) {
            callback(__assign(__assign({}, data), { requests: requests }));
        }
    };
    var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
    // only wrap fetch and xhr if headers or body are being recorded
    var wrapsNetworkPrimitives = networkOptions.recordHeaders || networkOptions.recordBody;
    // initialisedHandler is shared by every frame being recorded, so a frame that captures
    // nothing must leave it for a frame that can, or one degraded frame silences the page
    if (!performanceObserver && !wrapsNetworkPrimitives) {
        active = false;
        return noopHandler;
    }
    var xhrObserver = function () { };
    var fetchObserver = function () { };
    if (wrapsNetworkPrimitives) {
        xhrObserver = initXhrObserver(cb, win, networkOptions);
        fetchObserver =
            networkOptions.captureFetchSynchronously === true
                ? initFetchObserver(cb, win, networkOptions)
                : initLegacyFetchObserver(cb, win, networkOptions);
    }
    initialisedHandler = function () {
        active = false;
        performanceObserver === null || performanceObserver === void 0 ? void 0 : performanceObserver();
        xhrObserver();
        fetchObserver();
        initialisedHandler = null;
    };
    return initialisedHandler;
}
// use the plugin name so that when this functionality is adopted into rrweb
// we can remove this plugin and use the core functionality with the same data
exports.NETWORK_PLUGIN_NAME = 'rrweb/network@1';
// TODO how should this be typed?
// oxlint-disable-next-line typescript/ban-ts-comment
// @ts-ignore
var getRecordNetworkPlugin = function (options) {
    return {
        name: exports.NETWORK_PLUGIN_NAME,
        observer: initNetworkObserver,
        options: options,
    };
};
exports.getRecordNetworkPlugin = getRecordNetworkPlugin;
// rrweb/networ@1 ends
//# sourceMappingURL=network-plugin.js.map