UNPKG

posthog-js

Version:

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

781 lines • 40.6 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)); }; 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."); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostHogLogs = exports.RECORDER_MAX_AGE_MS = void 0; var constants_1 = require("./constants"); var config_1 = __importDefault(require("./config")); var core_1 = require("@posthog/core"); var globals_1 = require("@posthog/browser-common/utils/globals"); var globals_2 = require("./utils/globals"); var general_utils_1 = require("@posthog/browser-common/utils/general-utils"); var logger_1 = require("@posthog/browser-common/utils/logger"); var logs_defaults_1 = require("./logs-defaults"); var logs_types_1 = require("./logs-types"); var patch_1 = require("./extensions/replay/rrweb-plugins/patch"); var console_original_1 = require("./utils/console-original"); var extension_tokens_1 = require("./extension-tokens"); var request_utils_1 = require("@posthog/browser-common/utils/request-utils"); // Backstop for a page where remote config or the logs script never settles the // question: the buffer holds live references to console arguments, so it is // released after this long regardless. exports.RECORDER_MAX_AGE_MS = 30000; var LOGS_ENDPOINT = '/i/v1/logs'; // OTLP instrumentation-scope name for console auto-capture, distinguishing it from // programmatic logs (which use the SDK scope) in scope-based dashboards/queries. var CONSOLE_SCOPE_NAME = 'console'; // Safety backstop for a `_send_request` that never calls back. Set above the // request layer's own 60s timeout so a real (slow-but-completing) request always // settles via its callback first; this only fires on a genuinely callback-less // send (e.g. request enqueued before load, or a transport that never reports). var LOGS_SEND_TIMEOUT_MS = 90000; // Mirrors the event retry queue's status-0 budget (see retry-queue.ts // `STATUS_CODE_ZERO_MAX_RETRIES`): a request that dies before any HTTP response // while the browser reports itself online is almost always deterministically // blocked (ad blocker, CORS, extension), so retrying forever only burns network. // After this many consecutive such failures we stop sending and drop batches; // the `online` event reopens the pipe. // NOTE: keep the constant value and the warning copy in sync with retry-queue.ts. var MAX_CONSECUTIVE_STATUS_ZERO_FAILURES = 3; var HANDLED_LOGS_REQUEST_ERROR = '__posthogHandledLogsRequestError'; var markLogsRequestErrorAsHandled = function (error, fallbackMessage) { var handledError = (error instanceof Error ? error : new Error(fallbackMessage)); handledError[HANDLED_LOGS_REQUEST_ERROR] = true; return handledError; }; var isHandledLogsRequestError = function (error) { return !!error && typeof error === 'object' && error[HANDLED_LOGS_REQUEST_ERROR] === true; }; var PostHogLogs = /** @class */ (function () { function PostHogLogs(_instance) { var _this = this; var _a; this._instance = _instance; this.name = extension_tokens_1.LogsExtension; this._isLogsEnabled = false; this._isLoaded = false; this._isLoading = false; this._logger = (0, logger_1.createLogger)('[logs]'); // Core owns retry/backoff but the browser request layer owns transport logging. // Filter only errors explicitly branded by this adapter; fatal core errors remain visible. this._coreLogger = __assign(__assign({}, this._logger), { error: function () { var _a; var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (!args.some(isHandledLogsRequestError)) { (_a = _this._logger).error.apply(_a, __spreadArray([], __read(args), false)); } } }); // In-memory only; records do not survive a page reload. this._queue = []; // Console auto-capture uses a dedicated core + queue (its `service.name` // defaults to `posthog-browser-logs`). Built lazily, only when console runs. this._consoleQueue = []; // Shared across both cores: they send to the same endpoint, so one blocker // verdict covers both. this._consecutiveStatusZeroFailures = 0; this._disposed = false; // Console recorder: `console` is patched and the buffer fills until either the logs // script takes it over (`_takeConsoleBuffer`) or something drops it // (`_stopConsoleRecorder`). this._consoleBuffer = []; this._consoleRecorderUnpatchers = []; this._isRecordingConsole = false; // Mirrors the entrypoint's own guard. Snapshotting the SDK context reaches into // session, persistence and feature flags; anything on that path that writes // through the global `console` would re-enter the recorder while the outer entry // is still being built. `logger._log` escapes via `__rrweb_original__`; // `logger.critical` writes straight to the global `console`. this._isRecordingConsoleEntry = false; this._onReconnect = function () { var _a, _b; if (_this._disposed) { return; } _this._consecutiveStatusZeroFailures = 0; (_a = _this._core) === null || _a === void 0 ? void 0 : _a.onReconnect(); (_b = _this._consoleCore) === null || _b === void 0 ? void 0 : _b.onReconnect(); }; if (this._instance && ((_a = this._instance.config.logs) === null || _a === void 0 ? void 0 : _a.captureConsoleLogs)) { this._isLogsEnabled = true; } // Flush on reconnect rather than waiting out the retry backoff. if (globals_1.window) { (0, general_utils_1.addEventListener)(globals_1.window, 'online', this._onReconnect); } } // Cores are built lazily (the extension exists before `init` applies config) // and rebuilt when `config.logs` is swapped. Callers reset the old core first // so its timer can't double-flush the shared queue; a flush already in flight // may still re-send its head batch on a mid-swap — a duplicate, never a loss. PostHogLogs.prototype._buildCore = function (getQueue, setQueue, opts, scopeName) { var _this = this; var _a, _b; var config = (0, logs_defaults_1.resolveLogsConfig)((_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.logs, opts); var core = new core_1.PostHogLogs(this._createHost(getQueue, setQueue), config, this._coreLogger, function () { return _this._getSdkContext(); }, function (fn) { return fn(); }, undefined, scopeName); return [core, config]; }; PostHogLogs.prototype._getCore = function () { var _a; var _this = this; var _b, _c, _d; var logsConfig = (_c = (_b = this._instance) === null || _b === void 0 ? void 0 : _b.config) === null || _c === void 0 ? void 0 : _c.logs; if (!this._core || this._resolvedFrom !== logsConfig) { (_d = this._core) === null || _d === void 0 ? void 0 : _d.reset(); this._resolvedFrom = logsConfig; _a = __read(this._buildCore(function () { return _this._queue; }, function (q) { _this._queue = q; }), 2), this._core = _a[0], this._resolvedConfig = _a[1]; } return this._core; }; // Like `_getCore`, but with the console service name + scope, backed by `_consoleQueue`. PostHogLogs.prototype._getConsoleCore = function () { var _a; var _this = this; var _b, _c, _d; var logsConfig = (_c = (_b = this._instance) === null || _b === void 0 ? void 0 : _b.config) === null || _c === void 0 ? void 0 : _c.logs; if (!this._consoleCore || this._consoleResolvedFrom !== logsConfig) { (_d = this._consoleCore) === null || _d === void 0 ? void 0 : _d.reset(); this._consoleResolvedFrom = logsConfig; _a = __read(this._buildCore(function () { return _this._consoleQueue; }, function (q) { _this._consoleQueue = q; }, { serviceNameDefault: 'posthog-browser-logs', consoleCapture: true }, CONSOLE_SCOPE_NAME), 2), this._consoleCore = _a[0], this._consoleResolvedConfig = _a[1]; } return this._consoleCore; }; PostHogLogs.prototype.setup = function (client) { var _this = this; var _a, _b, _c; if (this._disposed) { return; } this._client = client; // Non-slim bundles build this extension in the `PostHog` constructor, while // `config` is still the defaults, so the opt-in is re-read here. if ((_c = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.logs) === null || _c === void 0 ? void 0 : _c.captureConsoleLogs) { this._isLogsEnabled = true; } // Both routes to console capture have a window before the logs script can run, // so both get a recorder. Local config is the caller's own opt-in for this page // load; a persisted `true` is only a hint remote config may since have withdrawn. // Neither emits anything here. Started before subscribing, because a replayed // config calls back synchronously. if (this._isLogsEnabled || (this._remoteConfigWillArrive() && this._persistedCaptureHint())) { this._startConsoleRecorder(); } var replayedEnabledConfig = false; var subscription = client.onRemoteConfig(function (result) { var _a; replayedEnabledConfig = result.ok && ((_a = result.config.logs) === null || _a === void 0 ? void 0 : _a.captureConsoleLogs) === true; _this.onRemoteConfig(result); }); if (this._disposed) { subscription.dispose(); return; } this._remoteConfigSubscription = subscription; if (!replayedEnabledConfig) { this.loadIfEnabled(); } }; PostHogLogs.prototype.dispose = function () { var _a, _b, _c, _d; if (this._disposed) { return; } this._disposed = true; this._stopConsoleRecorder(); (_a = this._remoteConfigSubscription) === null || _a === void 0 ? void 0 : _a.dispose(); this._remoteConfigSubscription = undefined; this._client = undefined; this._isLoading = false; globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.removeEventListener('online', this._onReconnect); // TODO: Multiplex console capture across instances and settle pending log sends so // wrappers and request timers cannot outlive their final owner. (_b = this._consoleLogsDispose) === null || _b === void 0 ? void 0 : _b.call(this); this._consoleLogsDispose = undefined; (_c = this._core) === null || _c === void 0 ? void 0 : _c.reset(); (_d = this._consoleCore) === null || _d === void 0 ? void 0 : _d.reset(); }; PostHogLogs.prototype.onRemoteConfig = function (result) { var _a; var _b, _c, _d; if (this._disposed) { return; } // A failed fetch and a response without a `logs` key behave the same: no fresh // verdict arrived. var logCapture = result.ok ? (_b = result.config.logs) === null || _b === void 0 ? void 0 : _b.captureConsoleLogs : undefined; if ((0, core_1.isNullish)(logCapture)) { // Nothing to persist, and a recorder the hint alone started stands down // rather than holding console arguments for a handover that may never come. this._stopRecorderStartedByPersistedHint(); return; } (_d = (_c = this._instance) === null || _c === void 0 ? void 0 : _c.persistence) === null || _d === void 0 ? void 0 : _d.register((_a = {}, _a[constants_1.LOGS_CAPTURE_ENABLED_SERVER_SIDE] = !!logCapture, _a)); if (!logCapture) { // The server reports `false` for every project that has not turned console // capture on, so it cannot distinguish "not enabled" from "turned off" and // does not override a local `captureConsoleLogs` opt-in. It does withdraw the // persisted hint, which is the only thing it granted. this._stopRecorderStartedByPersistedHint(); return; } this._isLogsEnabled = true; if (!this._isLoaded) { // On a first visit there is no local option and no persisted hint, so `setup` // started no recorder and this is the first thing to enable capture. The // script it kicks off below does not land in the same tick, and console calls // made in between would otherwise fall through the gap. The recorder runs // until `loadIfEnabled` hands its buffer to the script. this._startConsoleRecorder(); } this.loadIfEnabled(); }; PostHogLogs.prototype.reset = function () { var _a, _b, _c, _d; // Buffered entries carry the pre-reset identity, so they are dropped rather // than replayed under whoever the SDK is told to be next. this._stopConsoleRecorder(); (_a = this._core) === null || _a === void 0 ? void 0 : _a.clearQueue(); this._queue = []; (_b = this._core) === null || _b === void 0 ? void 0 : _b.reset(); (_c = this._consoleCore) === null || _c === void 0 ? void 0 : _c.clearQueue(); this._consoleQueue = []; (_d = this._consoleCore) === null || _d === void 0 ? void 0 : _d.reset(); this._consecutiveStatusZeroFailures = 0; }; PostHogLogs.prototype.captureLog = function (options) { if (!this._disposed) { this._getCore().captureLog(options); } }; // Console auto-capture (the lazy `logs` chunk) routes here so its records run // through the shared core pipeline and carry `service.name: posthog-browser-logs`. /** @internal */ PostHogLogs.prototype.captureConsoleLog = function (options) { if (!this._disposed) { this._getConsoleCore().captureLog(options); } }; // Replay path for entries the recorder buffered before the `logs` chunk existed. // Same pipeline as `captureConsoleLog`, but the record is stamped with the state // captured at the console call rather than the state at replay. Unmangled for the // same reason `captureConsoleLog` is: the caller lives in a separately built bundle. /** @internal */ PostHogLogs.prototype.captureBufferedConsoleLog = function (options, context, occurredAtMs) { if (!this._disposed) { this._getConsoleCore().captureLog(options, { context: context, occurredAtMs: occurredAtMs }); } }; PostHogLogs.prototype._persistedCaptureHint = function () { var _a, _b, _c; return !!((_c = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.persistence) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c[constants_1.LOGS_CAPTURE_ENABLED_SERVER_SIDE]); }; // A hint is only worth acting on if something can still confirm or withdraw it. // With flags disabled and no preloaded config, `onRemoteConfig` never fires, so a // recorder started from the hint alone would hold console arguments until the // max-age backstop for a handover that cannot happen. PostHogLogs.prototype._remoteConfigWillArrive = function () { var _a, _b, _c, _d; if (!((_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a._shouldDisableFlags) === null || _b === void 0 ? void 0 : _b.call(_a))) { return true; } return !!((_d = (_c = globals_2.assignableWindow._POSTHOG_REMOTE_CONFIG) === null || _c === void 0 ? void 0 : _c[this._instance.config.token]) === null || _d === void 0 ? void 0 : _d.config); }; /** * Releases everything console capture is holding when capturing is turned off: the * pre-load buffer, the `console` patches, and console records already queued — at * that moment rather than whenever the page next writes to the console. * * @internal */ PostHogLogs.prototype._onOptOut = function () { var _a; this._stopConsoleRecorder(); // Console autocapture is passive mirroring the app never asked for, so records // already queued go too. `captureLog`/`logger` records were captured explicitly // while consent stood, so `_queue` is left alone and a flush already armed will // still send them — the same as events queued before an opt-out. (_a = this._consoleCore) === null || _a === void 0 ? void 0 : _a.clearQueue(); this._consoleQueue = []; }; // Withdraws a recorder that nothing but the persisted hint ever granted. A running // recorder with `_isLogsEnabled` unset can only have come from the hint, and once it // is set capture has been granted for real and the handover is coming — so the hint // is no longer the only thing holding the recorder up. PostHogLogs.prototype._stopRecorderStartedByPersistedHint = function () { if (this._isLogsEnabled) { return; } this._stopConsoleRecorder(); }; PostHogLogs.prototype._startConsoleRecorder = function () { var e_1, _a; var _this = this; var _b, _c; if (this._isRecordingConsole || !(globals_2.assignableWindow === null || globals_2.assignableWindow === void 0 ? void 0 : globals_2.assignableWindow.console)) { return; } // Deliberately tighter than the console queue's own depth: entries here pin // live argument graphs rather than serialized records, so the ceiling is core's // flush threshold rather than its eviction cap. var maxBufferSize = (0, logs_defaults_1.resolveLogsConfig)((_c = (_b = this._instance) === null || _b === void 0 ? void 0 : _b.config) === null || _c === void 0 ? void 0 : _c.logs).maxBufferSize; var _loop_1 = function (level) { var trueOriginal; try { trueOriginal = (0, console_original_1.originalConsoleMethod)(globals_2.assignableWindow.console[level]); } catch (_d) { return "continue"; } if (!trueOriginal) { return "continue"; } this_1._consoleRecorderUnpatchers.push((0, patch_1.patch)(globals_2.assignableWindow.console, level, function (next) { var wrapped = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } try { _this._recordConsoleEntry(level, args, maxBufferSize); } catch (_a) { // Recording must never break the page's own console output. } return next.apply(globals_2.assignableWindow.console, args); }; wrapped.__rrweb_original__ = trueOriginal; return wrapped; })); }; var this_1 = this; try { for (var BUFFERED_CONSOLE_LEVELS_1 = __values(logs_types_1.BUFFERED_CONSOLE_LEVELS), BUFFERED_CONSOLE_LEVELS_1_1 = BUFFERED_CONSOLE_LEVELS_1.next(); !BUFFERED_CONSOLE_LEVELS_1_1.done; BUFFERED_CONSOLE_LEVELS_1_1 = BUFFERED_CONSOLE_LEVELS_1.next()) { var level = BUFFERED_CONSOLE_LEVELS_1_1.value; _loop_1(level); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (BUFFERED_CONSOLE_LEVELS_1_1 && !BUFFERED_CONSOLE_LEVELS_1_1.done && (_a = BUFFERED_CONSOLE_LEVELS_1.return)) _a.call(BUFFERED_CONSOLE_LEVELS_1); } finally { if (e_1) throw e_1.error; } } this._isRecordingConsole = true; this._consoleRecorderTimeout = setTimeout(function () { _this._stopConsoleRecorder(); }, exports.RECORDER_MAX_AGE_MS); }; // Keeps the earliest calls on overflow rather than evicting oldest-first like the // logs queue: the early ones are exactly what the live path would otherwise miss. PostHogLogs.prototype._recordConsoleEntry = function (level, args, maxBufferSize) { var _a; if (!this._isRecordingConsole || this._isRecordingConsoleEntry || args.length === 0) { return; } if (!((_a = this._instance) === null || _a === void 0 ? void 0 : _a.is_capturing())) { // Opting out mid-window releases the arguments already held, not just // future ones. this._stopConsoleRecorder(); return; } if (this._consoleBuffer.length >= maxBufferSize) { return; } this._isRecordingConsoleEntry = true; try { this._consoleBuffer.push({ level: level, args: args, occurredAtMs: Date.now(), context: this._getSdkContext() }); } finally { this._isRecordingConsoleEntry = false; } }; // Stops recording and drops anything buffered, releasing the live argument // references. `_takeConsoleBuffer` takes the buffer before calling this. PostHogLogs.prototype._stopConsoleRecorder = function () { var e_2, _a; this._consoleBuffer = []; if (!this._isRecordingConsole) { return; } this._isRecordingConsole = false; if (this._consoleRecorderTimeout) { clearTimeout(this._consoleRecorderTimeout); this._consoleRecorderTimeout = undefined; } try { for (var _b = __values(this._consoleRecorderUnpatchers), _c = _b.next(); !_c.done; _c = _b.next()) { var unpatch = _c.value; unpatch(); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } this._consoleRecorderUnpatchers = []; }; PostHogLogs.prototype._takeConsoleBuffer = function () { var buffered = this._consoleBuffer; this._stopConsoleRecorder(); return buffered; }; Object.defineProperty(PostHogLogs.prototype, "logger", { get: function () { var _this = this; if (!this._capture_logger) { this._capture_logger = { trace: function (body, attributes) { return _this.captureLog({ body: body, level: 'trace', attributes: attributes }); }, debug: function (body, attributes) { return _this.captureLog({ body: body, level: 'debug', attributes: attributes }); }, info: function (body, attributes) { return _this.captureLog({ body: body, level: 'info', attributes: attributes }); }, warn: function (body, attributes) { return _this.captureLog({ body: body, level: 'warn', attributes: attributes }); }, error: function (body, attributes) { return _this.captureLog({ body: body, level: 'error', attributes: attributes }); }, fatal: function (body, attributes) { return _this.captureLog({ body: body, level: 'fatal', attributes: attributes }); }, }; } return this._capture_logger; }, enumerable: false, configurable: true }); // An explicit transport drains the whole queue in one request over that transport // (core's batched flush can't force a transport, and the unload sendBeacon must be // synchronous). No transport → core's batched, 413-aware, retrying flush. PostHogLogs.prototype.flushLogs = function (transport) { var _this = this; if (transport) { this._flushViaTransport(transport); return; } if (this._core) { void this._core.flush().catch(function (err) { return _this._logFlushError(err); }); } if (this._consoleCore) { void this._consoleCore.flush().catch(function (err) { return _this._logFlushError(err); }); } }; PostHogLogs.prototype._logFlushError = function (error) { if (!isHandledLogsRequestError(error)) { this._logger.error('PostHog logs flush failed:', error); } }; PostHogLogs.prototype.loadIfEnabled = function () { var _this = this; if (this._disposed || !this._isLogsEnabled || this._isLoaded || this._isLoading) { return; } // Both are terminal. A plain `no-external` bundle never creates // `__PosthogExtensions__` at all, and a `full.no-external` one creates it without // a loader, so the handover can never come and the recorder has to come off now // rather than at the max-age backstop. var phExtensions = globals_2.assignableWindow === null || globals_2.assignableWindow === void 0 ? void 0 : globals_2.assignableWindow.__PosthogExtensions__; if (!phExtensions) { this._logger.error('PostHog Extensions not found.'); this._stopConsoleRecorder(); return; } var loadExternalDependency = phExtensions.loadExternalDependency; if (!loadExternalDependency) { this._logger.error(constants_1.LOAD_EXT_NOT_FOUND); this._stopConsoleRecorder(); return; } this._isLoading = true; try { loadExternalDependency(this._instance, 'logs', function (err) { var _a, _b, _c; _this._isLoading = false; if (_this._disposed || !_this._isLogsEnabled) { // Remote config turned capture off while the script was in flight. return; } var logsExtension = phExtensions.logs; if (err || !(logsExtension === null || logsExtension === void 0 ? void 0 : logsExtension.initializeLogs)) { _this._logger.error('Could not load logs script', err); _this._stopConsoleRecorder(); } else { // Unpatch the recorder before the entrypoint patches `console`, so a // call made in this tick is either buffered or captured live, never // both. Both steps run synchronously here, so stopping first opens no // capture gap. var buffered = _this._takeConsoleBuffer(); _this._consoleLogsDispose = logsExtension.initializeLogs((_a = _this._client) !== null && _a !== void 0 ? _a : _this._instance); _this._isLoaded = true; if (buffered.length > 0) { (_b = logsExtension.replayConsoleBuffer) === null || _b === void 0 ? void 0 : _b.call(logsExtension, (_c = _this._client) !== null && _c !== void 0 ? _c : _this._instance, buffered); } } }); } catch (error) { this._isLoading = false; throw error; } }; // Host adapter for core's `PostHogLogs`; structurally checked against `LogsHost` // at the `new CorePostHogLogs` call, so no explicit annotation is needed. The // queue accessors are parameterized so the programmatic and console instances // each bind to their own queue. PostHogLogs.prototype._createHost = function (getQueue, setQueue) { var _this = this; var ph = this._instance; return { // The browser gates capture through `is_capturing()` (see `optedOut`). get isDisabled() { return false; }, get optedOut() { return !ph.is_capturing(); }, // Live queue by reference; core mutates it in place and persists via the setter. getPersistedProperty: function (key) { return key === core_1.PostHogPersistedProperty.LogsQueue ? getQueue() : undefined; }, setPersistedProperty: function (key, value) { var _a; if (key === core_1.PostHogPersistedProperty.LogsQueue) { setQueue((_a = value) !== null && _a !== void 0 ? _a : []); } }, _sendLogsBatch: function (payload) { return _this._sendLogsBatch(payload); }, getLibraryId: function () { return config_1.default.LIB_NAME; }, getLibraryVersion: function () { return config_1.default.LIB_VERSION; }, }; }; PostHogLogs.prototype._sendLogsBatch = function (payload) { var _this = this; // eslint-disable-next-line compat/compat return new Promise(function (resolve) { if ((0, request_utils_1.isStatusZeroFailureCircuitBreakerTripped)(_this._consecutiveStatusZeroFailures, MAX_CONSECUTIVE_STATUS_ZERO_FAILURES)) { // Tripped: drop the batch without touching the network. `fatal` // advances the queue so records don't pile up while blocked. // The `onLine` guard ensures genuine offline periods still queue // for the reconnect flush instead of being fatally dropped. resolve({ kind: 'fatal', error: markLogsRequestErrorAsHandled(undefined, 'logs endpoint is unreachable, dropping batch'), }); return; } var settled = false; var settle = function (outcome) { if (settled) { return; } settled = true; clearTimeout(timer); resolve(outcome); }; // Backstop for `_send_request` paths that never call back, so the promise // always settles and core's flush can't wedge. Keeps records for retry. var timer = setTimeout(function () { _this._logger.warn('Logs request timed out before receiving a response'); settle({ kind: 'retry-later', error: markLogsRequestErrorAsHandled(undefined, 'logs request timed out'), }); }, LOGS_SEND_TIMEOUT_MS); _this._instance._send_request({ method: 'POST', url: _this._logsUrl(), data: payload, compression: 'best-available', batchKey: 'logs', // Notify on the drop paths (not loaded, rate limited) so they retry, not stall. fireCallbackOnDrop: true, callback: function (response) { var _a; var status = response.statusCode; _this._trackEndpointReachability(status); if (status >= 200 && status < 300) { settle({ kind: 'ok' }); } else if (status === 413) { settle({ kind: 'too-large' }); } else if (status === 0 || status === 408 || status === 429 || status >= 500) { // Transient (network / timeout / rate-limit / server error): keep and retry. if (status === 0) { // `_send_request` already logs fetch failures. Bare status 0 is the // XHR/synthetic path, so keep one warning for it here. if (!response.error) { _this._logger.warn('Logs request failed before receiving an HTTP response'); } settle({ kind: 'retry-later', error: markLogsRequestErrorAsHandled(response.error, 'logs request failed before receiving an HTTP response'), }); } else { // Preserve error severity for real HTTP failures. settle({ kind: 'retry-later', error: (_a = response.error) !== null && _a !== void 0 ? _a : new Error("logs request failed with status ".concat(status)), }); } } else { // Client error (4xx): won't succeed on retry, drop. settle({ kind: 'fatal', error: new Error("logs request failed with status ".concat(status)) }); } }, }); }); }; // Feeds the status-0 circuit breaker checked at the top of `_sendLogsBatch`. PostHogLogs.prototype._trackEndpointReachability = function (statusCode) { var _this = this; // Before `init` completes, `_send_request` synthesizes `{ statusCode: 0 }` // without any network attempt (the `fireCallbackOnDrop` path), so only // post-load failures count — a deferred init must not arrive to an // already-tripped breaker. `__loaded` flips on init, not on a successful // request, so a blocked-from-the-start page still trips as intended. if (statusCode === 0 && !this._instance.__loaded) { return; } this._consecutiveStatusZeroFailures = (0, request_utils_1.updateStatusZeroFailureCount)(statusCode, this._consecutiveStatusZeroFailures, MAX_CONSECUTIVE_STATUS_ZERO_FAILURES, function () { return _this._logger.warn('Log requests are failing before receiving an HTTP response; this can happen due to network issues, CORS, browser blocking, or ad blockers. Stopped sending logs; will try again when connectivity changes.'); }); }; // Drains both the programmatic and console queues over the given transport. // Each queue carries its own resolved config so the two `service.name`s are // preserved. Non-empty queue → its core was built → its resolved config is set, // so the length guards also avoid lazily building an unused core for config. // TODO: future optimization — merge both into one multi-`resourceLogs` payload // so a page-unload only fires a single sendBeacon instead of two. PostHogLogs.prototype._flushViaTransport = function (transport) { var _this = this; if (this._queue.length > 0) { // Invariant: _resolvedConfig is set whenever _queue has items. this._drainQueueViaTransport(transport, this._queue, this._resolvedConfig, config_1.default.LIB_NAME, function (q) { _this._queue = q; }); } if (this._consoleQueue.length > 0) { // Invariant: _consoleResolvedConfig is set whenever _consoleQueue has items. this._drainQueueViaTransport(transport, this._consoleQueue, this._consoleResolvedConfig, CONSOLE_SCOPE_NAME, function (q) { _this._consoleQueue = q; }); } }; PostHogLogs.prototype._drainQueueViaTransport = function (transport, queue, config, scopeName, setQueue) { if (queue.length === 0) { return; } var records = queue.map(function (e) { return e.record; }); setQueue([]); // Shared with the core flush path so resource attributes can't drift. The // scope name labels the stream (console vs SDK); `telemetry.sdk.name` stays // the SDK id (`Config.LIB_NAME`) regardless. var payload = (0, core_1.buildOtlpLogsPayload)(records, (0, core_1.buildResourceAttributes)(config, config_1.default.LIB_NAME, config_1.default.LIB_VERSION), scopeName, config_1.default.LIB_VERSION); // Intentionally bypasses the circuit breaker and does not feed // `_trackEndpointReachability`: this is a best-effort "last gasp" send // (page unload or explicit transport flush) where `sendBeacon` in particular // is sometimes honoured even by blockers, and the callback-less path means // we can't track the outcome anyway. this._instance._send_request({ method: 'POST', url: this._logsUrl(), data: payload, compression: 'best-available', batchKey: 'logs', transport: transport, }); }; PostHogLogs.prototype._logsUrl = function () { return (this._instance.requestRouter.endpointFor('api', LOGS_ENDPOINT) + '?token=' + encodeURIComponent(this._instance.config.token)); }; PostHogLogs.prototype._getSdkContext = function () { var _a; var context = {}; context.distinctId = this._instance.get_distinct_id(); if (this._instance.sessionManager) { var _b = this._instance.sessionManager.checkAndGetSessionAndWindowId(true), sessionId = _b.sessionId, windowId = _b.windowId, sessionStartTimestamp = _b.sessionStartTimestamp, lastActivityTimestamp = _b.lastActivityTimestamp; context.sessionId = sessionId; context.windowId = windowId; if (!(0, core_1.isNullish)(sessionStartTimestamp)) { context.sessionStartTimestamp = sessionStartTimestamp; } if (!(0, core_1.isNullish)(lastActivityTimestamp)) { context.lastActivityTimestamp = lastActivityTimestamp; } } if ((_a = globals_2.assignableWindow === null || globals_2.assignableWindow === void 0 ? void 0 : globals_2.assignableWindow.location) === null || _a === void 0 ? void 0 : _a.href) { context.currentUrl = this._instance.config.disable_capture_url_hashes ? (0, core_1.stripUrlHash)(globals_2.assignableWindow.location.href) : globals_2.assignableWindow.location.href; } if (this._instance.featureFlags) { var flags = this._instance.featureFlags.getFlags(); if (flags && flags.length > 0) { context.activeFeatureFlags = flags; } } return context; }; return PostHogLogs; }()); exports.PostHogLogs = PostHogLogs; //# sourceMappingURL=posthog-logs.js.map