UNPKG

posthog-js

Version:

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

335 lines • 14.9 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; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildNetworkRequestOptions = exports.isInitialMaskFallback = exports.defaultNetworkOptions = exports.MAX_PAYLOAD_SIZE_BYTES = void 0; exports.effectivePayloadLimitBytes = effectivePayloadLimitBytes; var core_1 = require("@posthog/core"); var request_utils_1 = require("@posthog/browser-common/utils/request-utils"); var logger_1 = require("@posthog/browser-common/utils/logger"); var autocapture_utils_1 = require("@posthog/browser-common/utils/autocapture-utils"); var general_utils_1 = require("@posthog/browser-common/utils/general-utils"); var LOGGER_PREFIX = '[SessionRecording]'; var REDACTED = 'redacted'; // the hard ceiling for a recorded request/response body — 1MB, even if a larger limit is configured exports.MAX_PAYLOAD_SIZE_BYTES = 1000000; // the smallest of the 1MB ceiling or the configured limit if there is one function effectivePayloadLimitBytes(options) { var _a; return Math.min(exports.MAX_PAYLOAD_SIZE_BYTES, (_a = options.payloadSizeLimitBytes) !== null && _a !== void 0 ? _a : exports.MAX_PAYLOAD_SIZE_BYTES); } exports.defaultNetworkOptions = { initiatorTypes: [ 'audio', 'beacon', 'body', 'css', 'early-hint', 'embed', 'fetch', 'frame', 'iframe', 'icon', 'image', 'img', 'input', 'link', 'navigation', 'object', 'ping', 'script', 'track', 'video', 'xmlhttprequest', ], maskRequestFn: function (data) { return data; }, recordHeaders: false, recordBody: false, recordInitialRequests: false, recordPerformance: false, performanceEntryTypeToObserve: [ // 'event', // This is too noisy as it covers all browser events 'first-input', // 'mark', // Mark is used too liberally. We would need to filter for specific marks // 'measure', // Measure is used too liberally. We would need to filter for specific measures 'navigation', 'paint', 'resource', ], payloadSizeLimitBytes: exports.MAX_PAYLOAD_SIZE_BYTES, payloadHostDenyList: [ '.lr-ingest.io', '.ingest.sentry.io', '.clarity.ms', // NB no leading dot here // GA4/gtag beacons go to *.google-analytics.com; with Google Signals on they also hit // analytics.google.com (region1.analytics.google.com/g/collect), so deny both 'google-analytics.com', 'analytics.google.com', // New Relic browser agent (bam + bam-cell) 'nr-data.net', // Datadog browser RUM intake 'datadoghq.com', 'datadoghq.eu', 'ddog-gov.com', // other third-party analytics / session-replay vendors whose telemetry has no replay value 'segment.io', 'rudderstack.com', 'amplitude.com', 'mixpanel.com', // Hotjar uses both .com and .io for data collection 'hotjar.com', 'hotjar.io', 'fullstory.com', ], streamNetworkBody: false, }; var HEADER_DENY_LIST = [ 'authorization', 'x-forwarded-for', 'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-real-ip', 'remote-addr', 'forwarded', 'proxy-authorization', 'x-csrf-token', 'x-csrftoken', 'x-xsrf-token', ]; var PAYLOAD_CONTENT_DENY_LIST = [ 'password', 'secret', 'passwd', 'api_key', 'apikey', 'auth', 'credentials', 'mysql_pwd', 'privatekey', 'private_key', 'token', ]; // substrings that mark a header as credential-bearing - catches custom names // (e.g. x-gist-encoded-user-token) that aren't in the exact deny list above var HEADER_DENY_SUBSTRINGS = [ 'auth', 'token', 'secret', 'session', 'api-key', 'apikey', 'api_key', 'credential', 'password', 'passwd', 'cookie', 'csrf', 'xsrf', ]; var isDeniedHeader = function (header) { var lower = header.toLowerCase(); return HEADER_DENY_LIST.includes(lower) || HEADER_DENY_SUBSTRINGS.some(function (s) { return lower.includes(s); }); }; var redactDeniedHeaders = function (headers) { if (!(0, core_1.isNullish)(headers)) { (0, general_utils_1.each)(Object.keys(headers), function (header) { if (isDeniedHeader(header)) { headers[header] = REDACTED; } }); } }; // we always remove headers on the deny list because we never want to capture this sensitive data. // applies to both request and response headers (e.g. set-cookie is a response header) var removeAuthorizationHeader = function (data) { redactDeniedHeaders(data.requestHeaders); redactDeniedHeaders(data.responseHeaders); return data; }; var POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/']; // want to ignore posthog paths when capturing requests, or we can get trapped in a loop // because calls to PostHog would be reported using a call to PostHog which would be reported.... var ignorePostHogPaths = function (data, apiHostConfig, isIngestionEndpoint) { var _a; if (isIngestionEndpoint === null || isIngestionEndpoint === void 0 ? void 0 : isIngestionEndpoint(data.name)) { return undefined; } var url = (0, request_utils_1.convertToURL)(data.name); // we need to account for api host config as e.g. pathname could be /ingest/s/ and we want to ignore that var replaceValue = apiHostConfig.indexOf('http') === 0 ? (_a = (0, request_utils_1.convertToURL)(apiHostConfig)) === null || _a === void 0 ? void 0 : _a.pathname : apiHostConfig; if (replaceValue === '/') { replaceValue = ''; } var pathname = url === null || url === void 0 ? void 0 : url.pathname.replace(replaceValue || '', ''); if (url && pathname && POSTHOG_PATHS_TO_IGNORE.some(function (path) { return pathname.indexOf(path) === 0; })) { return undefined; } return data; }; function estimateBytes(payload) { return new Blob([payload]).size; } function enforcePayloadSizeLimit(payload, headers, limit, description) { if ((0, core_1.isNullish)(payload)) { return payload; } var requestContentLength = (headers === null || headers === void 0 ? void 0 : headers['content-length']) || estimateBytes(payload); if ((0, core_1.isString)(requestContentLength)) { requestContentLength = parseInt(requestContentLength); } if (requestContentLength > limit) { return LOGGER_PREFIX + " ".concat(description, " body too large to record (").concat(requestContentLength, " bytes)"); } return payload; } // people can have arbitrarily large payloads on their site, but we don't want to ingest them var limitPayloadSize = function (options) { var limit = effectivePayloadLimitBytes(options); return function (data) { if (data === null || data === void 0 ? void 0 : data.requestBody) { data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request'); } if (data === null || data === void 0 ? void 0 : data.responseBody) { data.responseBody = enforcePayloadSizeLimit(data.responseBody, data.responseHeaders, limit, 'Response'); } return data; }; }; function scrubPayload(payload, label) { if ((0, core_1.isNullish)(payload)) { return payload; } var scrubbed = payload; if (!(0, autocapture_utils_1.shouldCaptureValue)(scrubbed, false)) { scrubbed = LOGGER_PREFIX + ' ' + label + ' body ' + REDACTED; } (0, general_utils_1.each)(PAYLOAD_CONTENT_DENY_LIST, function (text) { if ((scrubbed === null || scrubbed === void 0 ? void 0 : scrubbed.length) && (scrubbed === null || scrubbed === void 0 ? void 0 : scrubbed.indexOf(text)) !== -1) { scrubbed = LOGGER_PREFIX + ' ' + label + ' body ' + REDACTED + ' as might contain: ' + text; } }); return scrubbed; } function scrubPayloads(capturedRequest) { if ((0, core_1.isUndefined)(capturedRequest)) { return undefined; } capturedRequest.requestBody = scrubPayload(capturedRequest.requestBody, 'Request'); capturedRequest.responseBody = scrubPayload(capturedRequest.responseBody, 'Response'); return capturedRequest; } var initialMaskFallbackRequests = new WeakSet(); var isInitialMaskFallback = function (request) { return !!request && initialMaskFallbackRequests.has(request); }; exports.isInitialMaskFallback = isInitialMaskFallback; /** * whether a maskRequestFn is provided or not, * we ensure that we remove the denied header from requests * we _never_ want to record that header by accident * if someone complains then we'll add an opt-in to let them override it */ var buildNetworkRequestOptions = function (instanceConfig, remoteNetworkOptions, isIngestionEndpoint) { var config = { payloadSizeLimitBytes: exports.defaultNetworkOptions.payloadSizeLimitBytes, performanceEntryTypeToObserve: __spreadArray([], __read(exports.defaultNetworkOptions.performanceEntryTypeToObserve), false), payloadHostDenyList: __spreadArray(__spreadArray([], __read((remoteNetworkOptions.payloadHostDenyList || [])), false), __read(exports.defaultNetworkOptions.payloadHostDenyList), false), }; // client can always disable despite remote options var canRecordHeaders = instanceConfig.session_recording.recordHeaders === false ? false : remoteNetworkOptions.recordHeaders; var canRecordBody = instanceConfig.session_recording.recordBody === false ? false : remoteNetworkOptions.recordBody; var canRecordPerformance = instanceConfig.capture_performance === false ? false : remoteNetworkOptions.recordPerformance; var payloadLimiter = limitPayloadSize(config); var enforcedCleaningFn = function (d) { return payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.api_host, isIngestionEndpoint)); }; var hasDeprecatedMaskFunction = (0, core_1.isFunction)(instanceConfig.session_recording.maskNetworkRequestFn); if (hasDeprecatedMaskFunction && (0, core_1.isFunction)(instanceConfig.session_recording.maskCapturedNetworkRequestFn)) { logger_1.logger.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.'); } if (hasDeprecatedMaskFunction) { instanceConfig.session_recording.maskCapturedNetworkRequestFn = function (data) { var cleanedURL = instanceConfig.session_recording.maskNetworkRequestFn({ url: data.name }); // Preserve the nullish signal for initial entries so the required-metadata fallback below can // remove all customer-controlled content. Keep the deprecated URL-only behavior otherwise. if (!cleanedURL && data.isInitial) { return cleanedURL; } // the deprecated mask fn can suppress the URL, leaving `name` undefined on purpose // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return __assign(__assign({}, data), { name: cleanedURL === null || cleanedURL === void 0 ? void 0 : cleanedURL.url }); }; } config.maskRequestFn = (0, core_1.isFunction)(instanceConfig.session_recording.maskCapturedNetworkRequestFn) ? function (data) { var _a, _b; var cleanedRequest = enforcedCleaningFn(data); if (!cleanedRequest) { return undefined; } // Initial entries are required performance metadata, but their URL is still customer data. // Keep only required, non-content fields before invoking the callback because callbacks may // mutate their argument. In particular, do not copy customer-controlled server timing data. var requiredInitialMetadata = cleanedRequest.isInitial ? { name: '', entryType: cleanedRequest.entryType, startTime: cleanedRequest.startTime, duration: cleanedRequest.duration, endTime: cleanedRequest.endTime, timeOrigin: cleanedRequest.timeOrigin, timestamp: cleanedRequest.timestamp, isInitial: true, } : undefined; var maskedRequest = (_b = (_a = instanceConfig.session_recording).maskCapturedNetworkRequestFn) === null || _b === void 0 ? void 0 : _b.call(_a, cleanedRequest); // A nullish result normally drops the request. Initial timing metadata must remain for replay, // so retain it without the URL or any network content rather than exposing deliberately filtered data. if ((0, core_1.isNullish)(maskedRequest) && requiredInitialMetadata) { initialMaskFallbackRequests.add(requiredInitialMetadata); return requiredInitialMetadata; } return maskedRequest !== null && maskedRequest !== void 0 ? maskedRequest : undefined; } : function (data) { return scrubPayloads(enforcedCleaningFn(data)); }; return __assign(__assign(__assign({}, exports.defaultNetworkOptions), config), { recordHeaders: canRecordHeaders, recordBody: canRecordBody, recordPerformance: canRecordPerformance, recordInitialRequests: canRecordPerformance, streamNetworkBody: instanceConfig.session_recording.streamNetworkBody === true }); }; exports.buildNetworkRequestOptions = buildNetworkRequestOptions; //# sourceMappingURL=config.js.map