posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
553 lines • 29.3 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 __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 __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.EventReceiver = void 0;
var posthog_surveys_types_1 = require("../posthog-surveys-types");
var action_matcher_1 = require("../extensions/surveys/action-matcher");
var property_utils_1 = require("@posthog/browser-common/utils/property-utils");
var core_1 = require("@posthog/core");
/**
* Abstract base class for receiving events and matching them to triggerable items.
* Subclasses implement type-specific behavior for surveys and product tours.
*/
var EventReceiver = /** @class */ (function () {
function EventReceiver(instance) {
var _this = this;
var _a, _b;
/**
* Items armed by an event or action but not yet shown live here, in memory only.
* They are intentionally NOT persisted, so they do not survive a page reload: an
* event trigger only displays an item in the session the event fired in. Once an
* item is shown, surviving items are promoted into persistence (see `onEvent`) so
* a reload re-reads and re-displays them until the user interacts — but that
* persisted activation is scoped to the triggering session (see
* `_getPersistedActivatedIds`), so it does not leak into a brand-new session where
* the trigger never fired.
*/
this._pendingActivatedItems = [];
this._instance = instance;
this._eventToItems = new Map();
this._cancelEventToItems = new Map();
this._actionToItems = new Map();
// A persisted activation belongs to the session the item was shown in. When the session
// rotates (idle timeout, max length, cross-tab adoption) the trigger did not fire in the
// new session, so the activation is stale and must be dropped. We subscribe to rotations
// here rather than relying only on reading the session id on the display path: that read
// is read-only and so cannot observe an idle-expired session (and must not force a
// rotation, since merely checking whether to show a survey should never keep a session
// alive). The read-time check in `_getPersistedActivatedIds` remains as a complementary
// backstop for a session that had already rotated in persistence before this page loaded.
this._sessionIdUnsubscribe = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.onSessionId) === null || _b === void 0 ? void 0 : _b.call(_a, function (sessionId) { return _this._onSessionIdChanged(sessionId); });
}
/**
* Whether an item armed by a trigger should be persisted immediately (session-scoped)
* instead of kept in memory until it is shown. Overridden by receivers whose items carry a
* display delay that must survive a navigation, so the delay can resume from where it left
* off on the next page. Default: keep in memory, so the arming is page-scoped and an
* exit-intent-style trigger cannot leak an armed item onto a later page load.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EventReceiver.prototype._shouldPersistArmedActivation = function (_itemId) {
return false;
};
/**
* Persistence key under which per-item activation timestamps are read from, or null for
* receivers that don't track them. Used to resume a display delay across navigations. Writes
* go through `_writeActivationTimestamps` / `_clearActivationTimestampsStore` so the register
* sink always resolves to a literal key constant.
*/
EventReceiver.prototype._getActivationTimestampsKey = function () {
return null;
};
/** Persist the activation timestamp map. Overridden by receivers that track timestamps. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EventReceiver.prototype._writeActivationTimestamps = function (_timestamps) { };
/** Forget the whole activation timestamp map. Overridden by receivers that track timestamps. */
EventReceiver.prototype._clearActivationTimestampsStore = function () { };
EventReceiver.prototype._doesEventMatchFilter = function (eventConfig, eventPayload) {
if (!eventConfig) {
return false;
}
return (0, property_utils_1.matchPropertyFilters)(eventConfig.propertyFilters, eventPayload === null || eventPayload === void 0 ? void 0 : eventPayload.properties);
};
EventReceiver.prototype._buildEventToItemMap = function (items, conditionField) {
var map = new Map();
items.forEach(function (item) {
var _a, _b, _c;
(_c = (_b = (_a = item.conditions) === null || _a === void 0 ? void 0 : _a[conditionField]) === null || _b === void 0 ? void 0 : _b.values) === null || _c === void 0 ? void 0 : _c.forEach(function (event) {
if (event === null || event === void 0 ? void 0 : event.name) {
var existing = map.get(event.name) || [];
existing.push(item.id);
map.set(event.name, existing);
}
});
});
return map;
};
/**
* build a map of (Event1) => [Item1, Item2, Item3]
* used for items that should be [activated|cancelled] by Event1
*/
EventReceiver.prototype._getMatchingItems = function (eventName, eventPayload, conditionField) {
var _this = this;
var itemIdMap = conditionField === posthog_surveys_types_1.SurveyEventType.Activation ? this._eventToItems : this._cancelEventToItems;
var itemIds = itemIdMap.get(eventName);
var items = [];
this._getItems(function (allItems) {
items = allItems.filter(function (item) { return itemIds === null || itemIds === void 0 ? void 0 : itemIds.includes(item.id); });
});
return items.filter(function (item) {
var _a, _b, _c;
var eventConfig = (_c = (_b = (_a = item.conditions) === null || _a === void 0 ? void 0 : _a[conditionField]) === null || _b === void 0 ? void 0 : _b.values) === null || _c === void 0 ? void 0 : _c.find(function (e) { return e.name === eventName; });
return _this._doesEventMatchFilter(eventConfig, eventPayload);
});
};
EventReceiver.prototype.register = function (items) {
this._register(items, false);
};
EventReceiver.prototype.replace = function (items) {
this._register(items, true);
};
EventReceiver.prototype._register = function (items, replace) {
var _a;
if ((0, core_1.isUndefined)((_a = this._instance) === null || _a === void 0 ? void 0 : _a._addCaptureHook)) {
return;
}
this._setupEventBasedItems(items, replace);
this._setupActionBasedItems(items, replace);
};
EventReceiver.prototype._setupActionBasedItems = function (items, replace) {
var _this = this;
var _a;
var actionBasedItems = items.filter(function (item) { var _a, _b, _c; return (_c = (_b = (_a = item.conditions) === null || _a === void 0 ? void 0 : _a.actions) === null || _b === void 0 ? void 0 : _b.values) === null || _c === void 0 ? void 0 : _c.length; });
if (replace) {
this._actionToItems.clear();
}
if (actionBasedItems.length === 0) {
if (replace) {
(_a = this._actionMatcher) === null || _a === void 0 ? void 0 : _a.replace([]);
}
return;
}
if (!this._actionMatcher) {
this._actionMatcher = new action_matcher_1.ActionMatcher(this._instance);
this._actionMatcher.init();
this._actionMatcher._addActionHook(function (actionName) { return _this.onAction(actionName); });
}
var actions = [];
actionBasedItems.forEach(function (item) {
var _a, _b;
(_b = (_a = item.conditions) === null || _a === void 0 ? void 0 : _a.actions) === null || _b === void 0 ? void 0 : _b.values.forEach(function (action) {
var _a;
actions.push(action);
if (action.name) {
var matchingItems = (_a = _this._actionToItems.get(action.name)) !== null && _a !== void 0 ? _a : [];
if (!matchingItems.includes(item.id)) {
matchingItems.push(item.id);
}
_this._actionToItems.set(action.name, matchingItems);
}
});
});
if (replace) {
this._actionMatcher.replace(actions);
}
else {
this._actionMatcher.register(actions);
}
};
EventReceiver.prototype._mergeItemMaps = function (target, source) {
source.forEach(function (itemIds, eventName) {
var _a;
var matchingItems = (_a = target.get(eventName)) !== null && _a !== void 0 ? _a : [];
itemIds.forEach(function (itemId) {
if (!matchingItems.includes(itemId)) {
matchingItems.push(itemId);
}
});
target.set(eventName, matchingItems);
});
};
EventReceiver.prototype._setupEventBasedItems = function (items, replace) {
var _this = this;
var _a, _b;
var eventBasedItems = items.filter(function (item) { var _a, _b, _c, _d; return ((_a = item.conditions) === null || _a === void 0 ? void 0 : _a.events) && ((_d = (_c = (_b = item.conditions) === null || _b === void 0 ? void 0 : _b.events) === null || _c === void 0 ? void 0 : _c.values) === null || _d === void 0 ? void 0 : _d.length) > 0; });
var itemsWithCancelEvents = items.filter(function (item) { var _a, _b, _c, _d; return ((_a = item.conditions) === null || _a === void 0 ? void 0 : _a.cancelEvents) && ((_d = (_c = (_b = item.conditions) === null || _b === void 0 ? void 0 : _b.cancelEvents) === null || _c === void 0 ? void 0 : _c.values) === null || _d === void 0 ? void 0 : _d.length) > 0; });
var eventToItems = this._buildEventToItemMap(items, posthog_surveys_types_1.SurveyEventType.Activation);
var cancelEventToItems = this._buildEventToItemMap(items, posthog_surveys_types_1.SurveyEventType.Cancellation);
if (replace) {
this._eventToItems = eventToItems;
this._cancelEventToItems = cancelEventToItems;
}
else {
this._mergeItemMaps(this._eventToItems, eventToItems);
this._mergeItemMaps(this._cancelEventToItems, cancelEventToItems);
}
if (eventBasedItems.length === 0 && itemsWithCancelEvents.length === 0) {
return;
}
// match any events to its corresponding item.
var matchEventToItem = function (eventName, eventPayload) {
_this.onEvent(eventName, eventPayload);
};
(_a = this._captureHookUnsubscribe) !== null && _a !== void 0 ? _a : (this._captureHookUnsubscribe = (_b = this._instance) === null || _b === void 0 ? void 0 : _b._addCaptureHook(matchEventToItem));
};
EventReceiver.prototype.onEvent = function (event, eventPayload) {
var _this = this;
var _a, _b;
var logger = this._getLogger();
// An item reacting to one of its own lifecycle events (shown / dismissed / sent).
var itemId = ((_a = eventPayload === null || eventPayload === void 0 ? void 0 : eventPayload.properties) === null || _a === void 0 ? void 0 : _a.$survey_id) || ((_b = eventPayload === null || eventPayload === void 0 ? void 0 : eventPayload.properties) === null || _b === void 0 ? void 0 : _b.$product_tour_id);
if (itemId && this.getActivatedIds().includes(itemId)) {
var outcome = this._activationOutcome(event, itemId);
if (outcome === 'consume') {
logger.info('event consumed activated item, removing it', { event: event, itemId: itemId });
this._deactivateItems([itemId]);
return;
}
if (outcome === 'persist') {
logger.info('shown item promoted to persisted activation', { event: event, itemId: itemId });
this._persistActivation(itemId);
// The display delay has served its purpose once the item was shown, so drop the
// activation time: a later page load in this session waits the full delay again
// instead of re-rendering the item instantly on every navigation.
this._clearActivationTimestamps([itemId]);
return;
}
// 'ignore': no activation transition for this item on this event — fall through.
}
// check if this event should cancel any pending items
if (this._cancelEventToItems.has(event)) {
var itemsToCancel = this._getMatchingItems(event, eventPayload, posthog_surveys_types_1.SurveyEventType.Cancellation);
if (itemsToCancel.length > 0) {
logger.info('cancel event matched, cancelling items', {
event: event,
itemsToCancel: itemsToCancel.map(function (s) { return s.id; }),
});
this._deactivateItems(itemsToCancel.map(function (item) { return item.id; }));
// cancel any pending timeout for these items
itemsToCancel.forEach(function (item) { return _this._cancelPendingItem(item.id); });
}
}
// if the event is not in the eventToItems map, nothing else to do
if (!this._eventToItems.has(event)) {
return;
}
logger.info('event name matched', {
event: event,
eventPayload: eventPayload,
items: this._eventToItems.get(event),
});
var matchedItems = this._getMatchingItems(event, eventPayload, posthog_surveys_types_1.SurveyEventType.Activation);
this._activateItems(matchedItems.map(function (item) { return item.id; }));
};
EventReceiver.prototype.onAction = function (actionName) {
if (this._actionToItems.has(actionName)) {
this._activateItems(this._actionToItems.get(actionName) || []);
}
};
/**
* Arm items. Most are held in memory only (not persisted) until they are shown, so their
* arming is page-scoped. Items that opt in via `_shouldPersistArmedActivation` (those with a
* display delay) are persisted right away and stamped with the activation time, so the delay
* survives a navigation and resumes from the elapsed time on the next page.
*/
EventReceiver.prototype._activateItems = function (itemIds) {
var e_1, _a;
var _b, _c;
if (itemIds.length === 0) {
return;
}
// A persisted activation is scoped to the current session, so it can only be persisted
// when a session id is resolvable. With none (e.g. cookieless mode) we keep the in-memory
// arming — a reload can't be scoped anyway, so persisting would make it unreadable.
var canPersist = !!((_c = (_b = this._instance) === null || _b === void 0 ? void 0 : _b.get_session_id) === null || _c === void 0 ? void 0 : _c.call(_b));
var armedInMemory = [];
try {
for (var itemIds_1 = __values(itemIds), itemIds_1_1 = itemIds_1.next(); !itemIds_1_1.done; itemIds_1_1 = itemIds_1.next()) {
var itemId = itemIds_1_1.value;
if (canPersist && this._shouldPersistArmedActivation(itemId)) {
if (this._persistActivation(itemId)) {
this._recordActivationTimestamp(itemId);
}
}
else {
armedInMemory.push(itemId);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (itemIds_1_1 && !itemIds_1_1.done && (_a = itemIds_1.return)) _a.call(itemIds_1);
}
finally { if (e_1) throw e_1.error; }
}
if (armedInMemory.length > 0) {
this._pendingActivatedItems = __spreadArray([], __read(new Set(__spreadArray(__spreadArray([], __read(this._pendingActivatedItems), false), __read(armedInMemory), false))), false);
}
this._getLogger().info('updating activated items', { activatedItems: this.getActivatedIds() });
};
/**
* Move an in-memory activation into persistence so it survives a page reload within the
* triggering session. The set is (re)stamped with the current session id; reading it back
* via `_getPersistedActivatedIds` discards it once the session rolls over. Because we build
* on top of the session-scoped read, a stale set left over from a previous session is
* dropped here rather than accumulated.
*/
EventReceiver.prototype._persistActivation = function (itemId) {
this._pendingActivatedItems = this._pendingActivatedItems.filter(function (id) { return id !== itemId; });
var persisted = this._getPersistedActivatedIds();
if (persisted.includes(itemId)) {
return false;
}
this._setActivatedItems(__spreadArray(__spreadArray([], __read(persisted), false), [itemId], false));
this._stampActivationSession();
return true;
};
/** Drop items from both the in-memory and persisted activation sets. */
EventReceiver.prototype._deactivateItems = function (itemIds) {
var remove = new Set(itemIds);
this._pendingActivatedItems = this._pendingActivatedItems.filter(function (id) { return !remove.has(id); });
var persisted = this._getRawPersistedActivatedIds();
var nextPersisted = persisted.filter(function (id) { return !remove.has(id); });
if (nextPersisted.length !== persisted.length) {
this._setActivatedItems(nextPersisted);
if (nextPersisted.length === 0) {
this._clearActivationSession();
}
}
this._clearActivationTimestamps(itemIds);
};
/** The raw persisted activation timestamps as stored, ignoring session scoping. */
EventReceiver.prototype._getRawActivationTimestamps = function () {
var _a, _b;
var key = this._getActivationTimestampsKey();
if (!key) {
return {};
}
var stored = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.persistence) === null || _b === void 0 ? void 0 : _b.props[key];
return stored && typeof stored === 'object' ? stored : {};
};
/**
* Stamp when an item enters persisted activation, so a resumed display delay can be computed
* from the elapsed time on a later page load. Repeated triggers do not call this method because
* `_persistActivation` returns false while the current activation is still live.
*/
EventReceiver.prototype._recordActivationTimestamp = function (itemId) {
var _a;
var key = this._getActivationTimestampsKey();
if (!key) {
return;
}
var timestamps = this._getRawActivationTimestamps();
this._writeActivationTimestamps(__assign(__assign({}, timestamps), (_a = {}, _a[itemId] = Date.now(), _a)));
};
/** Drop the activation timestamps for the given items. */
EventReceiver.prototype._clearActivationTimestamps = function (itemIds) {
var e_2, _a;
var key = this._getActivationTimestampsKey();
if (!key) {
return;
}
var timestamps = this._getRawActivationTimestamps();
var next = {};
var changed = false;
try {
for (var _b = __values(Object.entries(timestamps)), _c = _b.next(); !_c.done; _c = _b.next()) {
var _d = __read(_c.value, 2), id = _d[0], ts = _d[1];
if (itemIds.includes(id)) {
changed = true;
}
else {
next[id] = ts;
}
}
}
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; }
}
if (!changed) {
return;
}
if ((0, core_1.isEmptyObject)(next)) {
this._clearActivationTimestampsStore();
return;
}
this._writeActivationTimestamps(next);
};
/** Forget all activation timestamps (e.g. on reset or a session rollover). */
EventReceiver.prototype._clearAllActivationTimestamps = function () {
if (this._getActivationTimestampsKey()) {
this._clearActivationTimestampsStore();
}
};
/**
* The time (epoch ms) an item was armed by its trigger, or undefined if none is recorded.
* Session-scoped: a timestamp is only honored while its activation is still live for the
* current session, so a stale entry left in persistence cannot resurrect an expired delay.
*/
EventReceiver.prototype.getActivationTimestamp = function (itemId) {
if (!this._getPersistedActivatedIds().includes(itemId)) {
return undefined;
}
var ts = this._getRawActivationTimestamps()[itemId];
return (0, core_1.isNumber)(ts) ? ts : undefined;
};
/** The raw persisted set as stored, ignoring session scoping. */
EventReceiver.prototype._getRawPersistedActivatedIds = function () {
var _a, _b;
var activatedKey = this._getActivatedKey();
var existingActivatedItems = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.persistence) === null || _b === void 0 ? void 0 : _b.props[activatedKey];
return existingActivatedItems ? existingActivatedItems : [];
};
/**
* The persisted activations that still belong to the current session. A persisted activation
* is scoped to the session the item was shown in: an event/action trigger only earns a display
* in the session it fired in, so once the session rolls over the activation is stale and must
* not silently re-display the item in a brand-new session where the trigger never fired.
*/
EventReceiver.prototype._getPersistedActivatedIds = function () {
var _a, _b, _c, _d;
var ids = this._getRawPersistedActivatedIds();
if (ids.length === 0) {
return [];
}
var stampedSessionId = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.persistence) === null || _b === void 0 ? void 0 : _b.props[this._getActivatedSessionKey()];
// Read-only: this catches a session that had already rotated in persistence before this
// page loaded (the stamped id no longer matches the current one). It intentionally does
// NOT force a rotation of an idle-expired session — that case is handled by the
// `onSessionId` subscription in the constructor, which clears the activation when the
// session actually rotates on the next real event.
var currentSessionId = (_d = (_c = this._instance) === null || _c === void 0 ? void 0 : _c.get_session_id) === null || _d === void 0 ? void 0 : _d.call(_c);
// No resolvable session (e.g. cookieless mode) → treat the activation as un-scopable and
// do not carry it across a reload.
if (!currentSessionId || stampedSessionId !== currentSessionId) {
return [];
}
return ids;
};
/** Stamp the persisted activation set with the current session id. */
EventReceiver.prototype._stampActivationSession = function () {
var _a, _b;
var currentSessionId = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.get_session_id) === null || _b === void 0 ? void 0 : _b.call(_a);
if (currentSessionId) {
this._setActivatedSession(currentSessionId);
}
};
/** Forget the session stamp once nothing is persisted under it. */
EventReceiver.prototype._clearActivationSession = function () {
this._clearActivatedSession();
};
/**
* Drop a persisted activation once the session it was stamped under is no longer current.
* Fired on session rotation (idle timeout, max length, cross-tab adoption) — the cases the
* read-only session read on the display path cannot observe. Pending timers must also be
* cancelled so a fresh trigger in the new session starts its full delay from the new timestamp.
*/
EventReceiver.prototype._onSessionIdChanged = function (sessionId) {
var _this = this;
var _a, _b;
var stampedSessionId = (_b = (_a = this._instance) === null || _a === void 0 ? void 0 : _a.persistence) === null || _b === void 0 ? void 0 : _b.props[this._getActivatedSessionKey()];
if (stampedSessionId && stampedSessionId !== sessionId) {
var activatedItemIds = this._getRawPersistedActivatedIds();
var activationTimestamps_1 = this._getRawActivationTimestamps();
if (activatedItemIds.length > 0) {
this._setActivatedItems([]);
activatedItemIds
.filter(function (itemId) { return (0, core_1.isNumber)(activationTimestamps_1[itemId]); })
.forEach(function (itemId) { return _this._cancelPendingItem(itemId); });
}
this._clearActivationSession();
this._clearAllActivationTimestamps();
}
};
EventReceiver.prototype.getActivatedIds = function () {
var _this = this;
// The activated set is the union of in-memory (armed, not yet shown) and persisted
// (shown and surviving) items. In-memory ones do not survive a reload by design.
var all = __spreadArray([], __read(new Set(__spreadArray(__spreadArray([], __read(this._getPersistedActivatedIds()), false), __read(this._pendingActivatedItems), false))), false);
return all.filter(function (itemId) { return !_this._isItemPermanentlyIneligible(itemId); });
};
/**
* Clear all activations. Called on `posthog.reset()` so a logout or account switch
* (without a full page reload) does not leave an event-armed item live for the next
* user — the in-memory set would otherwise survive `persistence.clear()`.
*/
EventReceiver.prototype.dispose = function () {
var _a, _b, _c;
(_a = this._sessionIdUnsubscribe) === null || _a === void 0 ? void 0 : _a.call(this);
this._sessionIdUnsubscribe = undefined;
(_b = this._captureHookUnsubscribe) === null || _b === void 0 ? void 0 : _b.call(this);
this._captureHookUnsubscribe = undefined;
(_c = this._actionMatcher) === null || _c === void 0 ? void 0 : _c.dispose();
this._actionMatcher = undefined;
};
EventReceiver.prototype.reset = function () {
this._pendingActivatedItems = [];
if (this._getRawPersistedActivatedIds().length > 0) {
this._setActivatedItems([]);
}
this._clearActivationSession();
this._clearAllActivationTimestamps();
};
EventReceiver.prototype.getEventToItemsMap = function () {
return this._eventToItems;
};
EventReceiver.prototype._getActionMatcher = function () {
return this._actionMatcher;
};
return EventReceiver;
}());
exports.EventReceiver = EventReceiver;
//# sourceMappingURL=event-receiver.js.map