overcentric
Version:
Overcentric watches your website, product, and users - and tells you what matters and what to do about it.
108 lines (107 loc) • 4.77 kB
JavaScript
import { generateUuid } from './identity';
import { onUserActivity } from './activity';
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds
const COOKIE_MAX_AGE = 30 * 60; // 30 minutes in seconds
function sessionCookieName(projectId) { return `overcentric_session_id_${projectId}`; }
function activityCookieName(projectId) { return `overcentric_session_activity_${projectId}`; }
function sessionKey(projectId) { return `overcentric_session_${projectId}`; }
function setCrossDomainCookie(name, value, maxAge) {
try {
const isSecure = window.location.protocol === 'https:';
const secureFlag = isSecure ? '; Secure' : '';
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${secureFlag}`;
try {
const domain = window.location.hostname.split('.').slice(-2).join('.');
document.cookie = `${name}=${value}; domain=.${domain}; path=/; max-age=${maxAge}; SameSite=Lax${secureFlag}`;
}
catch (e) {
console.debug('Could not set cookie on parent domain');
}
}
catch (e) {
console.warn(`Failed to set ${name} cookie:`, e);
}
}
function getCookie(name) {
var _a, _b;
try {
const value = (_b = (_a = document.cookie
.split('; ')
.find(row => row.startsWith(`${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')) === null || _b === void 0 ? void 0 : _b[1];
return value || null;
}
catch (e) {
console.warn(`Failed to get ${name} cookie:`, e);
return null;
}
}
/**
* Persist the session id + activity timestamp across cookie and localStorage so
* both storage paths agree. This prevents a stale localStorage record from
* resurrecting a session that the cookie path has already expired.
*/
function persistSession(projectId, id, activityTs) {
setCrossDomainCookie(sessionCookieName(projectId), id, COOKIE_MAX_AGE);
setCrossDomainCookie(activityCookieName(projectId), String(activityTs), COOKIE_MAX_AGE);
try {
localStorage.setItem(sessionKey(projectId), JSON.stringify({ id, lastActivity: activityTs }));
}
catch (e) { }
}
/**
* Get or create a session ID scoped to the given project.
*
* A session is considered alive only while there has been genuine user activity
* within SESSION_TIMEOUT. We renew the activity timestamp from the real
* user-input signal (see activity.ts) rather than from the mere fact that
* getSessionId was called — otherwise background work (e.g. the recorder's
* periodic flush) would keep an idle session alive indefinitely.
*/
export function getSessionId(projectId) {
const now = Date.now();
const SESSION_COOKIE_NAME = sessionCookieName(projectId);
const ACTIVITY_COOKIE_NAME = activityCookieName(projectId);
const SESSION_KEY = sessionKey(projectId);
const storedSessionId = getCookie(SESSION_COOKIE_NAME);
const lastActivityStr = getCookie(ACTIVITY_COOKIE_NAME);
const cookieActivity = lastActivityStr ? parseInt(lastActivityStr, 10) : 0;
// Whether to continue an existing session is decided purely from the STORED
// activity timestamp. getLastActivity() must never be used here: it is reset
// to "now" on every page load (activity.ts), so feeding it in would make any
// stale session look fresh and resurrect an id that should have expired.
if (storedSessionId && cookieActivity && (now - cookieActivity) < SESSION_TIMEOUT) {
persistSession(projectId, storedSessionId, now);
return storedSessionId;
}
// Fallback: Try localStorage for backward compatibility. localStorage has no
// expiry, so the stored timestamp is the only thing that can age it out.
const stored = localStorage.getItem(SESSION_KEY);
if (stored) {
try {
const session = JSON.parse(stored);
const lsActivity = session.lastActivity || 0;
if (lsActivity && (now - lsActivity) < SESSION_TIMEOUT) {
persistSession(projectId, session.id, now);
return session.id;
}
}
catch (error) {
console.error('Error parsing stored session:', error);
}
}
const newId = generateUuid();
persistSession(projectId, newId, now);
return newId;
}
/**
* Keep the activity cookie fresh while the user is genuinely active, so the
* session window reflects real interaction even between getSessionId calls.
*/
export function initSessionActivityTracking(projectId) {
onUserActivity(() => {
const id = getCookie(sessionCookieName(projectId));
if (id) {
setCrossDomainCookie(activityCookieName(projectId), String(Date.now()), COOKIE_MAX_AGE);
}
});
}