posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
283 lines • 12.4 kB
JavaScript
;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
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.");
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sessionRecordingLoggerForPostHogInstance = void 0;
exports.browserConsoleLogger = browserConsoleLogger;
exports.getChangedState = getChangedState;
exports.posthogKeaLogger = posthogKeaLogger;
exports.posthogReduxLogger = posthogReduxLogger;
var core_1 = require("@posthog/core");
var logger_1 = require("@posthog/browser-common/utils/logger");
/**
* Default title function for Redux events
*/
function defaultTitleFunction(stateEvent) {
var type = stateEvent.type, executionTimeMs = stateEvent.executionTimeMs;
var timeText = (0, core_1.isNullish)(executionTimeMs) ? '' : " (".concat(executionTimeMs.toFixed(2), "ms)");
return "".concat(type).concat(timeText);
}
// we need a posthog logger for the rate limiter
var phConsoleLogger = (0, logger_1.createLogger)('[PostHog Action RateLimiting]');
function browserConsoleLogger(title, stateEvent) {
// but the posthog logger swallows messages unless debug is on
// so we don't want to use it in this default logger
// oxlint-disable-next-line no-console
console.log(title, stateEvent);
}
/**
* Logger that sends state events to PostHog session recordings
* Requires that the loaded posthog instance is provided
* And returns the function to use as the logger
*
* e.g. const config = { logger: sessionRecordingLoggerForPostHogInstance(posthog) }
*/
var sessionRecordingLoggerForPostHogInstance = function (postHogInstance) {
return function (title, stateEvent) {
var _a;
(_a = postHogInstance === null || postHogInstance === void 0 ? void 0 : postHogInstance.sessionRecording) === null || _a === void 0 ? void 0 : _a.tryAddCustomEvent('app-state', { title: title, stateEvent: stateEvent });
};
};
exports.sessionRecordingLoggerForPostHogInstance = sessionRecordingLoggerForPostHogInstance;
/**
* Get only the changed keys from two states
* NB exported for testing purposes only, not part of the public API and may change without warning
*
* Returns { prevState: changedKeysOnly, nextState: changedKeysOnly }
*/
function getChangedState(prevState, nextState, maxDepth) {
var e_1, _a;
if (maxDepth === void 0) { maxDepth = 5; }
// Fast bailouts
if (typeof prevState !== 'object' || typeof nextState !== 'object')
return {};
if (prevState === nextState)
return {};
// all keys changed when no previous state
if (!prevState && nextState)
return nextState;
// something weird has happened, return empty
if (!nextState || !prevState)
return {};
var changed = {};
var allKeys = new Set(__spreadArray(__spreadArray([], __read(Object.keys(prevState)), false), __read(Object.keys(nextState)), false));
try {
for (var allKeys_1 = __values(allKeys), allKeys_1_1 = allKeys_1.next(); !allKeys_1_1.done; allKeys_1_1 = allKeys_1.next()) {
var key = allKeys_1_1.value;
var prevValue = prevState[key];
var nextValue = nextState[key];
// Key exists in only one object
if ((0, core_1.isUndefined)(prevValue)) {
changed[key] = nextValue;
continue;
}
if ((0, core_1.isUndefined)(nextValue)) {
changed[key] = prevValue;
continue;
}
// Same value
if (prevValue === nextValue) {
continue;
}
// Both null/undefined
if ((0, core_1.isNullish)(prevValue) && (0, core_1.isNullish)(nextValue)) {
continue;
}
// Primitive or one is null
if (!(0, core_1.isObject)(prevValue) || !(0, core_1.isObject)(nextValue)) {
changed[key] = nextValue;
continue;
}
// Both are objects, recurse if under max depth
if (maxDepth > 1) {
var childChanged = getChangedState(prevValue, nextValue, maxDepth - 1);
if (!(0, core_1.isEmptyObject)(childChanged)) {
changed[key] = childChanged;
}
}
else {
changed[key] = "max depth reached, checking for changed value";
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (allKeys_1_1 && !allKeys_1_1.done && (_a = allKeys_1.return)) _a.call(allKeys_1);
}
finally { if (e_1) throw e_1.error; }
}
return changed;
}
// Debounced logger for rate limit messages
var createDebouncedActionRateLimitedLogger = function () {
var timeout = null;
var ignoredCount = 0;
var lastActionType = null;
return {
info: function (actionType) {
if (lastActionType !== actionType) {
// Reset counter when action type changes
ignoredCount = 0;
lastActionType = actionType;
}
ignoredCount++;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function () {
var count = ignoredCount;
if (count === 1) {
phConsoleLogger.info("action \"".concat(actionType, "\" has been rate limited"));
}
else {
phConsoleLogger.info("action \"".concat(actionType, "\" has been rate limited (").concat(count, " times)"));
}
ignoredCount = 0;
timeout = null;
}, 1000);
},
};
};
var debouncedActionRateLimitedLogger = createDebouncedActionRateLimitedLogger();
/**
* Creates a Kea plugin that logs actions and state changes to a provided logger
* This can be used as a plugin in any Kea setup to capture state changes
*/
function posthogKeaLogger(config) {
if (config === void 0) { config = {}; }
var middleware = posthogReduxLogger(config);
return {
name: 'posthog-kea-logger',
events: {
beforeReduxStore: function (options) {
options.middleware.push(middleware);
},
},
};
}
/**
* Creates a Redux middleware that logs actions and state changes to a provided logger
* This can be used as middleware in any Redux store to capture state changes
*
* The logging uses token-bucket rate limiting to avoid flooding the logging with many changes
* by default logging rate limiting captures ten action instances before rate limiting by action type
* refills at a rate of one token / 1-second period
* e.g. will capture 1 rate limited action every 1 second until the burst ends
*/
function posthogReduxLogger(config
// the empty object is the recommended typing from redux docs
//oxlint-disable-next-line typescript/no-empty-object-type
) {
if (config === void 0) { config = {}; }
var maskAction = config.maskAction, maskState = config.maskState, _a = config.titleFunction, titleFunction = _a === void 0 ? defaultTitleFunction : _a, _b = config.logger, logger = _b === void 0 ? browserConsoleLogger : _b, _c = config.include, include = _c === void 0 ? {
prevState: true,
nextState: false,
changedState: true,
} : _c, _d = config.rateLimiterRefillRate, rateLimiterRefillRate = _d === void 0 ? 1 : _d, _e = config.rateLimiterBucketSize, rateLimiterBucketSize = _e === void 0 ? 10 : _e, __stateComparisonDepth = config.__stateComparisonDepth;
var rateLimiter = new core_1.BucketedRateLimiter({
refillRate: rateLimiterRefillRate,
bucketSize: rateLimiterBucketSize,
refillInterval: 1000, // one second in milliseconds,
_logger: phConsoleLogger,
});
return function (store) {
return function (next) {
return function (action) {
var typedAction = action;
// Get the state before the action
var prevState = store.getState();
// Track execution time
// oxlint-disable-next-line compat/compat
var startTime = performance.now();
var result = next(typedAction);
// oxlint-disable-next-line compat/compat
var endTime = performance.now();
var executionTimeMs = endTime - startTime;
// Get the state after the action
var nextState = store.getState();
var maskedAction = maskAction ? maskAction(typedAction) : typedAction;
if (!maskedAction) {
return result;
}
var isRateLimited = rateLimiter.consumeRateLimit(typedAction.type);
if (isRateLimited) {
debouncedActionRateLimitedLogger.info(typedAction.type);
}
else {
// Apply masking to states
try {
var maskedPrevState = maskState ? maskState(prevState, maskedAction) : prevState;
var maskedNextState = maskState ? maskState(nextState, maskedAction) : nextState;
var changedState = include.changedState
? getChangedState(maskedPrevState, maskedNextState, __stateComparisonDepth !== null && __stateComparisonDepth !== void 0 ? __stateComparisonDepth : 5)
: undefined;
var type = maskedAction.type, actionData = __rest(maskedAction, ["type"]);
var reduxEvent = {
type: type,
payload: actionData,
timestamp: Date.now(),
executionTimeMs: executionTimeMs,
prevState: include.prevState ? maskedPrevState : undefined,
nextState: include.nextState ? maskedNextState : undefined,
changedState: include.changedState ? changedState : undefined,
};
var title = titleFunction(reduxEvent);
logger(title, reduxEvent);
}
catch (e) {
// logging should never throw errors and break someone's app
phConsoleLogger.error('Error logging state:', e);
}
}
return result;
};
};
};
}
//# sourceMappingURL=posthogReduxLogger.js.map