UNPKG

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
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); } }); }