import { AppState, AppStateStatus } from 'react-native'; import type { SessionConfig, SessionInfo } from '../types'; import { generateId } from '../utils'; const DEFAULT_TIMEOUT = 30 * 60 * 1000; // 30 minutes type SessionEventCallback = (event: { name: string; properties?: Record }) => void; /** * Tracks user sessions across foreground/background transitions. * A new session starts when the app comes to foreground after being * inactive longer than sessionTimeout. */ export class SessionManager { private session: SessionInfo; private config: Required; private onSessionEvent: SessionEventCallback; private appStateSubscription?: any; private backgroundedAt?: number; private currentScreen?: string; constructor(config: SessionConfig = {}, onSessionEvent: SessionEventCallback) { this.config = { sessionTimeout: config.sessionTimeout ?? DEFAULT_TIMEOUT, autoTrackSessionEvents: config.autoTrackSessionEvents ?? true, }; this.onSessionEvent = onSessionEvent; this.session = this.createSession(); } start(): void { this.appStateSubscription = AppState.addEventListener( 'change', this.handleAppStateChange ); if (this.config.autoTrackSessionEvents) { this.onSessionEvent({ name: 'session_start', properties: { session_id: this.session.sessionId }, }); } } stop(): void { if (this.appStateSubscription) { this.appStateSubscription.remove(); } this.endSession(); } reset(): void { this.endSession(); this.session = this.createSession(); this.currentScreen = undefined; } recordEvent(): void { this.session.eventCount++; this.session.lastActiveTime = Date.now(); } recordScreen(screenName: string): void { this.currentScreen = screenName; this.session.screenCount++; this.session.lastActiveTime = Date.now(); } getSessionId(): string { return this.session.sessionId; } getSession(): SessionInfo { return { ...this.session }; } getCurrentScreen(): string | undefined { return this.currentScreen; } private handleAppStateChange = (state: AppStateStatus): void => { if (state === 'background' || state === 'inactive') { this.backgroundedAt = Date.now(); } else if (state === 'active') { const now = Date.now(); const elapsed = this.backgroundedAt ? now - this.backgroundedAt : 0; if (elapsed > this.config.sessionTimeout) { this.endSession(); this.session = this.createSession(); if (this.config.autoTrackSessionEvents) { this.onSessionEvent({ name: 'session_start', properties: { session_id: this.session.sessionId }, }); } } this.session.lastActiveTime = now; this.backgroundedAt = undefined; } }; private endSession(): void { this.session.isActive = false; if (this.config.autoTrackSessionEvents) { const duration = Date.now() - this.session.startTime; this.onSessionEvent({ name: 'session_end', properties: { session_id: this.session.sessionId, duration_ms: duration, screen_count: this.session.screenCount, event_count: this.session.eventCount, }, }); } } private createSession(): SessionInfo { return { sessionId: generateId(), startTime: Date.now(), lastActiveTime: Date.now(), screenCount: 0, eventCount: 0, isActive: true, }; } }