Skip to content

Commit ada90d2

Browse files
abueideclaude
andcommitted
fix(amplitude-session): prevent 0-second sessions from iOS Background Fetch
iOS Background Fetch can briefly trigger AppState 'active' without user interaction, causing rapid session creation/destruction cycles. Add a 1-second timestamp guard in onForeground() to prevent new sessions from starting within 1 second of the last session creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 123061e commit ada90d2

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

packages/plugins/plugin-amplitudeSession/src/AmplitudeSessionPlugin.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ export class AmplitudeSessionPlugin extends EventPlugin {
207207
};
208208

209209
private onForeground = () => {
210+
// Guard against rapid session creation from iOS Background Fetch.
211+
// iOS can briefly trigger AppState 'active' during background tasks,
212+
// causing 0-second sessions from rapid foreground/background cycles.
213+
const now = Date.now();
214+
if (this.sessionId > 0 && now - this.sessionId < 1000) {
215+
return;
216+
}
210217
this.startNewSessionIfNecessary();
211218
};
212219

packages/plugins/plugin-amplitudeSession/src/__tests__/AmplitudeSessionPlugin.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,45 @@ describe('AmplitudeSessionPlugin', () => {
618618
expect(plugin.analytics?.track).not.toHaveBeenCalled();
619619
});
620620

621+
it('should NOT start new session on rapid foreground cycles (iOS Background Fetch)', async () => {
622+
const baseTime = Date.now();
623+
jest.setSystemTime(baseTime);
624+
625+
// Session was just created (less than 1 second ago)
626+
plugin.sessionId = baseTime - 500; // 500ms ago
627+
plugin.lastEventTime = baseTime - (MAX_SESSION_TIME_IN_MS + 10000); // expired lastEventTime
628+
629+
const startNewSessionSpy = jest.spyOn(
630+
plugin as any,
631+
'startNewSessionIfNecessary'
632+
);
633+
634+
// Simulate rapid foreground from Background Fetch
635+
appStateChangeHandler('active');
636+
637+
// Should NOT call startNewSessionIfNecessary due to 1-second guard
638+
expect(startNewSessionSpy).not.toHaveBeenCalled();
639+
});
640+
641+
it('should allow new session on foreground when session is older than 1 second', async () => {
642+
const baseTime = Date.now();
643+
jest.setSystemTime(baseTime);
644+
645+
// Session was created more than 1 second ago and has expired
646+
plugin.sessionId = baseTime - 2000; // 2 seconds ago
647+
plugin.lastEventTime = baseTime - (MAX_SESSION_TIME_IN_MS + 10000); // expired
648+
649+
const startNewSessionSpy = jest.spyOn(
650+
plugin as any,
651+
'startNewSessionIfNecessary'
652+
);
653+
654+
appStateChangeHandler('active');
655+
656+
// Should call startNewSessionIfNecessary since session is >1s old
657+
expect(startNewSessionSpy).toHaveBeenCalled();
658+
});
659+
621660
it('should update lastEventTime when app goes to background', async () => {
622661
const baseTime = Date.now();
623662
jest.setSystemTime(baseTime);

0 commit comments

Comments
 (0)