diff --git a/packages/plugins/plugin-amplitudeSession/src/AmplitudeSessionPlugin.tsx b/packages/plugins/plugin-amplitudeSession/src/AmplitudeSessionPlugin.tsx index 3d0644bc8..0cd1099d5 100644 --- a/packages/plugins/plugin-amplitudeSession/src/AmplitudeSessionPlugin.tsx +++ b/packages/plugins/plugin-amplitudeSession/src/AmplitudeSessionPlugin.tsx @@ -10,30 +10,48 @@ import { 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 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; - sessionId: number | undefined; - sessionTimer: ReturnType | undefined; - - update(settings: SegmentAPISettings, _: UpdateType) { - const integrations = settings.integrations; - if (this.key in integrations) { - this.active = true; - this.refreshSession(); + sessionId = -1; + lastEventTime = -1; + + 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; } - execute(event: SegmentEvent) { + async execute(event: SegmentEvent) { if (!this.active) { return event; } - this.refreshSession(); + if (this.sessionId === -1 || this.lastEventTime === -1) { + await this.loadSessionData(); + } + + await this.startNewSessionIfNecessary(); let result = event; switch (result.type) { @@ -53,6 +71,10 @@ export class AmplitudeSessionPlugin extends EventPlugin { result = this.group(result); break; } + + this.lastEventTime = Date.now(); + await this.saveSessionData(); + return result; } @@ -65,6 +87,10 @@ export class AmplitudeSessionPlugin extends EventPlugin { } screen(event: ScreenEventType) { + event.properties = { + ...event.properties, + name: event.name, + }; return this.insertSession(event) as ScreenEventType; } @@ -76,39 +102,105 @@ export class AmplitudeSessionPlugin extends EventPlugin { return this.insertSession(event) as AliasEventType; } - reset() { - this.resetSession(); + async reset() { + this.sessionId = -1; + this.lastEventTime = -1; + await AsyncStorage.removeItem(SESSION_ID_KEY); + await AsyncStorage.removeItem(LAST_EVENT_TIME_KEY); } private insertSession = (event: SegmentEvent) => { - const returnEvent = event; - const integrations = event.integrations; - returnEvent.integrations = { - ...integrations, - [this.key]: { - session_id: this.sessionId, + 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 }, }, }; - return returnEvent; }; - private resetSession = () => { - this.sessionId = Date.now(); - this.sessionTimer = undefined; + private onBackground = () => { + this.lastEventTime = Date.now(); + this.saveSessionData(); + }; + + private onForeground = () => { + this.startNewSessionIfNecessary(); }; - private refreshSession = () => { - if (this.sessionId === undefined) { - this.sessionId = Date.now(); + private async startNewSessionIfNecessary() { + const current = Date.now(); + + const sessionExpired = + this.sessionId === -1 || + this.lastEventTime === -1 || + current - this.lastEventTime >= MAX_SESSION_TIME_IN_MS; + + // Avoid loop: if session just started recently, skip restarting + if (!sessionExpired || current - this.sessionId < 1000) { + return; } - if (this.sessionTimer !== undefined) { - clearTimeout(this.sessionTimer); + await this.endSession(); + await this.startNewSession(); + } + + private async startNewSession() { + this.sessionId = Date.now(); + this.lastEventTime = this.sessionId; + await this.saveSessionData(); + + this.analytics?.track(AMP_SESSION_START_EVENT, { + integrations: { + [this.key]: { session_id: this.sessionId }, + }, + }); + } + + private async endSession() { + if (this.sessionId === -1) { + return; } - this.sessionTimer = setTimeout( - () => this.resetSession(), - MAX_SESSION_TIME_IN_MS + this.analytics?.track(AMP_SESSION_END_EVENT, { + integrations: { + [this.key]: { session_id: this.sessionId }, + }, + }); + } + + private async loadSessionData() { + const storedSessionId = await AsyncStorage.getItem(SESSION_ID_KEY); + const storedLastEventTime = await AsyncStorage.getItem(LAST_EVENT_TIME_KEY); + this.sessionId = storedSessionId != null ? Number(storedSessionId) : -1; + this.lastEventTime = + storedLastEventTime != null ? Number(storedLastEventTime) : -1; + } + + private async saveSessionData() { + await AsyncStorage.setItem(SESSION_ID_KEY, this.sessionId.toString()); + await AsyncStorage.setItem( + LAST_EVENT_TIME_KEY, + this.lastEventTime.toString() ); + } + + private handleAppStateChange = (nextAppState: string) => { + if (nextAppState === 'active') { + this.onForeground(); + } else if (nextAppState === 'background') { + this.onBackground(); + } }; }