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