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;
};
import { SESSION_ID } from './constants';
import { sessionStore } from './storage';
import { uuid7ToTimestampMs, uuidv7 } from './uuidv7';
import { window } from './utils/globals';
import { isArray, isNumber, isUndefined } from './utils/type-utils';
import { createLogger } from './utils/logger';
import { clampToRange } from './utils/number-utils';
import { addEventListener } from './utils';
var logger = createLogger('[SessionId]');
export var DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS = 30 * 60; // 30 minutes
export var MAX_SESSION_IDLE_TIMEOUT_SECONDS = 10 * 60 * 60; // 10 hours
var MIN_SESSION_IDLE_TIMEOUT_SECONDS = 60; // 1 minute
var SESSION_LENGTH_LIMIT_MILLISECONDS = 24 * 3600 * 1000; // 24 hours
var SessionIdManager = /** @class */ (function () {
function SessionIdManager(instance, sessionIdGenerator, windowIdGenerator) {
var _a;
this._sessionIdChangedHandlers = [];
if (!instance.persistence) {
throw new Error('SessionIdManager requires a PostHogPersistence instance');
}
if (instance.config.__preview_experimental_cookieless_mode) {
throw new Error('SessionIdManager cannot be used with __preview_experimental_cookieless_mode');
}
this.config = instance.config;
this.persistence = instance.persistence;
this._windowId = undefined;
this._sessionId = undefined;
this._sessionStartTimestamp = null;
this._sessionActivityTimestamp = null;
this._sessionIdGenerator = sessionIdGenerator || uuidv7;
this._windowIdGenerator = windowIdGenerator || uuidv7;
var persistenceName = this.config['persistence_name'] || this.config['token'];
var desiredTimeout = this.config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS;
this._sessionTimeoutMs =
clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, 'session_idle_timeout_seconds', DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
instance.register({ $configured_session_timeout_ms: this._sessionTimeoutMs });
this.resetIdleTimer();
this._window_id_storage_key = 'ph_' + persistenceName + '_window_id';
this._primary_window_exists_storage_key = 'ph_' + persistenceName + '_primary_window_exists';
// primary_window_exists is set when the DOM has been loaded and is cleared on unload
// if it exists here it means there was no unload which suggests this window is opened as a tab duplication, window.open, etc.
if (this._canUseSessionStorage()) {
var lastWindowId = sessionStore.parse(this._window_id_storage_key);
var primaryWindowExists = sessionStore.parse(this._primary_window_exists_storage_key);
if (lastWindowId && !primaryWindowExists) {
// Persist window from previous storage state
this._windowId = lastWindowId;
}
else {
// Wipe any reference to previous window id
sessionStore.remove(this._window_id_storage_key);
}
// Flag this session as having a primary window
sessionStore.set(this._primary_window_exists_storage_key, true);
}
if ((_a = this.config.bootstrap) === null || _a === void 0 ? void 0 : _a.sessionID) {
try {
var sessionStartTimestamp = uuid7ToTimestampMs(this.config.bootstrap.sessionID);
this._setSessionId(this.config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
}
catch (e) {
logger.error('Invalid sessionID in bootstrap', e);
}
}
this._listenToReloadWindow();
}
Object.defineProperty(SessionIdManager.prototype, "sessionTimeoutMs", {
get: function () {
return this._sessionTimeoutMs;
},
enumerable: false,
configurable: true
});
SessionIdManager.prototype.onSessionId = function (callback) {
var _this = this;
// KLUDGE: when running in tests the handlers array was always undefined
// it's yucky but safe to set it here so that it's always definitely available
if (isUndefined(this._sessionIdChangedHandlers)) {
this._sessionIdChangedHandlers = [];
}
this._sessionIdChangedHandlers.push(callback);
if (this._sessionId) {
callback(this._sessionId, this._windowId);
}
return function () {
_this._sessionIdChangedHandlers = _this._sessionIdChangedHandlers.filter(function (h) { return h !== callback; });
};
};
SessionIdManager.prototype._canUseSessionStorage = function () {
// We only want to use sessionStorage if persistence is enabled and not memory storage
return this.config.persistence !== 'memory' && !this.persistence.disabled && sessionStore.is_supported();
};
// Note: this tries to store the windowId in sessionStorage. SessionStorage is unique to the current window/tab,
// and persists page loads/reloads. So it's uniquely suited for storing the windowId. This function also respects
// when persistence is disabled (by user config) and when sessionStorage is not supported (it *should* be supported on all browsers),
// and in that case, it falls back to memory (which sadly, won't persist page loads)
SessionIdManager.prototype._setWindowId = function (windowId) {
if (windowId !== this._windowId) {
this._windowId = windowId;
if (this._canUseSessionStorage()) {
sessionStore.set(this._window_id_storage_key, windowId);
}
}
};
SessionIdManager.prototype._getWindowId = function () {
if (this._windowId) {
return this._windowId;
}
if (this._canUseSessionStorage()) {
return sessionStore.parse(this._window_id_storage_key);
}
// New window id will be generated
return null;
};
// Note: 'this.persistence.register' can be disabled in the config.
// In that case, this works by storing sessionId and the timestamp in memory.
SessionIdManager.prototype._setSessionId = function (sessionId, sessionActivityTimestamp, sessionStartTimestamp) {
var _a;
if (sessionId !== this._sessionId ||
sessionActivityTimestamp !== this._sessionActivityTimestamp ||
sessionStartTimestamp !== this._sessionStartTimestamp) {
this._sessionStartTimestamp = sessionStartTimestamp;
this._sessionActivityTimestamp = sessionActivityTimestamp;
this._sessionId = sessionId;
this.persistence.register((_a = {},
_a[SESSION_ID] = [sessionActivityTimestamp, sessionId, sessionStartTimestamp],
_a));
}
};
SessionIdManager.prototype._getSessionId = function () {
if (this._sessionId && this._sessionActivityTimestamp && this._sessionStartTimestamp) {
return [this._sessionActivityTimestamp, this._sessionId, this._sessionStartTimestamp];
}
var sessionIdInfo = this.persistence.props[SESSION_ID];
if (isArray(sessionIdInfo) && sessionIdInfo.length === 2) {
// Storage does not yet have a session start time. Add the last activity timestamp as the start time
sessionIdInfo.push(sessionIdInfo[0]);
}
return sessionIdInfo || [0, null, 0];
};
// Resets the session id by setting it to null. On the subsequent call to checkAndGetSessionAndWindowId,
// new ids will be generated.
SessionIdManager.prototype.resetSessionId = function () {
this._setSessionId(null, null, null);
};
/*
* Listens to window unloads and removes the primaryWindowExists key from sessionStorage.
* Reloaded or fresh tabs created after a DOM unloads (reloading the same tab) WILL NOT have this primaryWindowExists flag in session storage.
* Cloned sessions (new tab, tab duplication, window.open(), ...) WILL have this primaryWindowExists flag in their copied session storage.
* We conditionally check the primaryWindowExists value in the constructor to decide if the window id in the last session storage should be carried over.
*/
SessionIdManager.prototype._listenToReloadWindow = function () {
var _this = this;
addEventListener(window, 'beforeunload', function () {
if (_this._canUseSessionStorage()) {
sessionStore.remove(_this._primary_window_exists_storage_key);
}
},
// Not making it passive to try and force the browser to handle this before the page is unloaded
{ capture: false });
};
/*
* This function returns the current sessionId and windowId. It should be used to
* access these values over directly calling `._sessionId` or `._windowId`.
* In addition to returning the sessionId and windowId, this function also manages cycling the
* sessionId and windowId when appropriate by doing the following:
*
* 1. If the sessionId or windowId is not set, it will generate a new one and store it.
* 2. If the readOnly param is set to false, it will:
* a. Check if it has been > SESSION_CHANGE_THRESHOLD since the last call with this flag set.
* If so, it will generate a new sessionId and store it.
* b. Update the timestamp stored with the sessionId to ensure the current session is extended
* for the appropriate amount of time.
*
* @param {boolean} readOnly (optional) Defaults to False. Should be set to True when the call to the function should not extend or cycle the session (e.g. being called for non-user generated events)
* @param {Number} timestamp (optional) Defaults to the current time. The timestamp to be stored with the sessionId (used when determining if a new sessionId should be generated)
*/
SessionIdManager.prototype.checkAndGetSessionAndWindowId = function (readOnly, _timestamp) {
if (readOnly === void 0) { readOnly = false; }
if (_timestamp === void 0) { _timestamp = null; }
if (this.config.__preview_experimental_cookieless_mode) {
throw new Error('checkAndGetSessionAndWindowId should not be called in __preview_experimental_cookieless_mode');
}
var timestamp = _timestamp || new Date().getTime();
// eslint-disable-next-line prefer-const
var _a = __read(this._getSessionId(), 3), lastActivityTimestamp = _a[0], sessionId = _a[1], startTimestamp = _a[2];
var windowId = this._getWindowId();
var sessionPastMaximumLength = isNumber(startTimestamp) &&
startTimestamp > 0 &&
Math.abs(timestamp - startTimestamp) > SESSION_LENGTH_LIMIT_MILLISECONDS;
var valuesChanged = false;
var noSessionId = !sessionId;
var activityTimeout = !readOnly && Math.abs(timestamp - lastActivityTimestamp) > this.sessionTimeoutMs;
if (noSessionId || activityTimeout || sessionPastMaximumLength) {
sessionId = this._sessionIdGenerator();
windowId = this._windowIdGenerator();
logger.info('new session ID generated', {
sessionId: sessionId,
windowId: windowId,
changeReason: { noSessionId: noSessionId, activityTimeout: activityTimeout, sessionPastMaximumLength: sessionPastMaximumLength },
});
startTimestamp = timestamp;
valuesChanged = true;
}
else if (!windowId) {
windowId = this._windowIdGenerator();
valuesChanged = true;
}
var newActivityTimestamp = lastActivityTimestamp === 0 || !readOnly || sessionPastMaximumLength ? timestamp : lastActivityTimestamp;
var sessionStartTimestamp = startTimestamp === 0 ? new Date().getTime() : startTimestamp;
this._setWindowId(windowId);
this._setSessionId(sessionId, newActivityTimestamp, sessionStartTimestamp);
if (!readOnly) {
this.resetIdleTimer();
}
if (valuesChanged) {
this._sessionIdChangedHandlers.forEach(function (handler) {
return handler(sessionId, windowId, valuesChanged ? { noSessionId: noSessionId, activityTimeout: activityTimeout, sessionPastMaximumLength: sessionPastMaximumLength } : undefined);
});
}
return {
sessionId: sessionId,
windowId: windowId,
sessionStartTimestamp: sessionStartTimestamp,
changeReason: valuesChanged ? { noSessionId: noSessionId, activityTimeout: activityTimeout, sessionPastMaximumLength: sessionPastMaximumLength } : undefined,
lastActivityTimestamp: lastActivityTimestamp,
};
};
SessionIdManager.prototype.resetIdleTimer = function () {
var _this = this;
clearTimeout(this._enforceIdleTimeout);
this._enforceIdleTimeout = setTimeout(function () {
// enforce idle timeout a little after the session timeout to ensure the session is reset even without activity
_this.resetSessionId();
}, this.sessionTimeoutMs * 1.1);
};
return SessionIdManager;
}());
export { SessionIdManager };
//# sourceMappingURL=sessionid.js.map