import { EventPlugin, EventType, IdentifyEventType, PluginType, SegmentAPISettings, SegmentEvent, TrackEventType, ScreenEventType, GroupEventType, UpdateType, AliasEventType, SegmentClient, } from '@segment/analytics-react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { AppState } from 'react-native'; const MAX_SESSION_TIME_IN_MS = 300000; const SESSION_ID_KEY = 'previous_session_id'; const EVENT_SESSION_ID_KEY = 'event_session_id'; const LAST_EVENT_TIME_KEY = 'last_event_time'; const AMP_SESSION_START_EVENT = 'session_start'; const AMP_SESSION_END_EVENT = 'session_end'; export class AmplitudeSessionPlugin extends EventPlugin { type = PluginType.enrichment; key = 'Actions Amplitude'; active = false; private _sessionId = -1; private _eventSessionId = -1; private _lastEventTime = -1; resetPending = false; get eventSessionId() { return this._eventSessionId; } set eventSessionId(value: number) { this._eventSessionId = value; if (value !== -1) { AsyncStorage.setItem(EVENT_SESSION_ID_KEY, value.toString()).catch( (err) => console.warn( '[AmplitudeSessionPlugin] Failed to persist eventSessionId:', err ) ); } } get lastEventTime() { return this._lastEventTime; } set lastEventTime(value: number) { this._lastEventTime = value; if (value !== -1) { AsyncStorage.setItem(LAST_EVENT_TIME_KEY, value.toString()).catch((err) => console.warn( '[AmplitudeSessionPlugin] Failed to persist lastEventTime:', err ) ); } } get sessionId() { return this._sessionId; } set sessionId(value: number) { this._sessionId = value; if (value !== -1) { AsyncStorage.setItem(SESSION_ID_KEY, value.toString()).catch((err) => console.warn( '[AmplitudeSessionPlugin] Failed to persist sessionId:', err ) ); } } configure = async (analytics: SegmentClient): Promise => { this.analytics = analytics; await this.loadSessionData(); AppState.addEventListener('change', this.handleAppStateChange); }; update(settings: SegmentAPISettings, type: UpdateType) { if (type !== UpdateType.initial) { return; } this.active = settings.integrations?.hasOwnProperty(this.key) ?? false; } async execute(event: SegmentEvent) { if (!this.active) { return event; } if (this.sessionId === -1 || this.lastEventTime === -1) { await this.loadSessionData(); } await this.startNewSessionIfNecessary(); let result = event; switch (result.type) { case EventType.IdentifyEvent: result = this.identify(result); break; case EventType.TrackEvent: result = this.track(result); break; case EventType.ScreenEvent: result = this.screen(result); break; case EventType.AliasEvent: result = this.alias(result); break; case EventType.GroupEvent: result = this.group(result); break; } this.lastEventTime = Date.now(); //await this.saveSessionData(); return result; } identify(event: IdentifyEventType) { return this.insertSession(event) as IdentifyEventType; } track(event: TrackEventType) { const eventName = event.event; if (eventName === AMP_SESSION_START_EVENT) { this.resetPending = false; this.eventSessionId = this.sessionId; } if (eventName === AMP_SESSION_END_EVENT) { console.log(`[AmplitudeSession] EndSession = ${this.eventSessionId}`); } if ( eventName.startsWith('Amplitude') || eventName === AMP_SESSION_START_EVENT || eventName === AMP_SESSION_END_EVENT ) { const integrations = this.disableAllIntegrations(event.integrations); return { ...event, integrations: { ...integrations, [this.key]: { session_id: this.eventSessionId }, }, }; } return this.insertSession(event) as TrackEventType; } screen(event: ScreenEventType) { event.properties = { ...event.properties, name: event.name, }; return this.insertSession(event) as ScreenEventType; } group(event: GroupEventType) { return this.insertSession(event) as GroupEventType; } alias(event: AliasEventType) { return this.insertSession(event) as AliasEventType; } async reset() { this.sessionId = -1; this.eventSessionId = -1; this.lastEventTime = -1; await AsyncStorage.removeItem(SESSION_ID_KEY); } private insertSession = (event: SegmentEvent) => { const integrations = event.integrations || {}; const existingIntegration = integrations[this.key]; const hasSessionId = typeof existingIntegration === 'object' && existingIntegration !== null && 'session_id' in existingIntegration; if (hasSessionId) { return event; } return { ...event, integrations: { ...integrations, [this.key]: { session_id: this.sessionId }, }, }; }; private onBackground = () => { this.lastEventTime = Date.now(); }; private onForeground = () => { this.startNewSessionIfNecessary(); }; private async startNewSessionIfNecessary() { if (this.eventSessionId === -1) { this.eventSessionId = this.sessionId; } if (this.resetPending) { return; } const current = Date.now(); const withinSessionLimit = this.withinMinSessionTime(current); const isSessionExpired = this.sessionId === -1 || this.lastEventTime === -1 || !withinSessionLimit; if (this.sessionId >= 0 && !isSessionExpired) { return; } // End old session and start a new one await this.startNewSession(); } /** * Handles the entire process of starting a new session. * Can be called directly or from startNewSessionIfNecessary() */ private async startNewSession() { if (this.resetPending) { return; } this.resetPending = true; const oldSessionId = this.sessionId; if (oldSessionId >= 0) { await this.endSession(oldSessionId); } const newSessionId = Date.now(); this.sessionId = newSessionId; this.eventSessionId = this.eventSessionId === -1 ? newSessionId : this.eventSessionId; this.lastEventTime = newSessionId; console.log(`[AmplitudeSession] startNewSession -> ${newSessionId}`); await this.trackSessionStart(newSessionId); } /** * Extracted analytics tracking into its own method */ private async trackSessionStart(sessionId: number) { this.analytics?.track(AMP_SESSION_START_EVENT, { integrations: { [this.key]: { session_id: sessionId }, }, }); } private async endSession(sessionId: number) { if (this.sessionId === -1) { return; } console.log(`[AmplitudeSession] endSession -> ${this.sessionId}`); this.analytics?.track(AMP_SESSION_END_EVENT, { integrations: { [this.key]: { session_id: sessionId }, }, }); } private async loadSessionData() { const storedSessionId = await AsyncStorage.getItem(SESSION_ID_KEY); const storedLastEventTime = await AsyncStorage.getItem(LAST_EVENT_TIME_KEY); const storedEventSessionId = await AsyncStorage.getItem( EVENT_SESSION_ID_KEY ); this.sessionId = storedSessionId != null ? Number(storedSessionId) : -1; this.lastEventTime = storedLastEventTime != null ? Number(storedLastEventTime) : -1; this.eventSessionId = storedEventSessionId != null ? Number(storedEventSessionId) : -1; } // eslint-disable-next-line @typescript-eslint/no-explicit-any private disableAllIntegrations(integrations?: Record) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}; if (!integrations) { return result; } for (const key of Object.keys(integrations)) { result[key] = false; } return result; } private withinMinSessionTime(timestamp: number): boolean { const timeDelta = timestamp - this.lastEventTime; return timeDelta < MAX_SESSION_TIME_IN_MS; } private handleAppStateChange = (nextAppState: string) => { if (nextAppState === 'active') { this.onForeground(); } else if (nextAppState === 'background') { this.onBackground(); } }; }