Skip to content

fix(amplitude-session): fix 0-second sessions on iOS#1169

Open
abueide wants to merge 1 commit intomasterfrom
amp/fix-0-second-sessions
Open

fix(amplitude-session): fix 0-second sessions on iOS#1169
abueide wants to merge 1 commit intomasterfrom
amp/fix-0-second-sessions

Conversation

@abueide
Copy link
Contributor

@abueide abueide commented Mar 12, 2026

Summary

Fix AmplitudeSessionPlugin producing 0-second sessions (session_start + session_end at the same timestamp) on iOS. Addresses customer escalation from v0.4.2 rewrite (PR #1104).

Root cause: The plugin ignored iOS's inactive AppState. On iOS, the lifecycle is active → inactive → background, but the plugin only handled background, so the active → inactive transition was a no-op. This meant lastEventTime was never updated on backgrounding, causing immediate session expiration when the app resumed.

Changes

Handle iOS inactive AppState (primary fix)

Track _previousAppState and treat inactive the same as background for session timeout, matching the core library's pattern at analytics.ts:771. Without this, every iOS background/foreground cycle produces a spurious session_end + session_start.

Await onForeground

onForeground was fire-and-forget, so the session transition could race with incoming events. Now awaited.

Replace resetPending with _sessionTransition promise

The resetPending flag was cleared asynchronously via the event pipeline when session_start flowed back through track(). Between the flag being set and cleared, a race window existed where concurrent execute() calls could skip the transition check. The promise lets concurrent callers await the same in-flight transition.

Remove dual-ID concept (eventSessionId)

The v0.4.2 rewrite added eventSessionId as a separate ID for event enrichment, intended to let events keep the old session ID during transitions. But track() overwrote it when session events flowed back through the pipeline — by that point the transition had already completed, so eventSessionId held the new value anyway. With the transition promise serializing access (no event can be enriched mid-transition), the second ID is unnecessary. Removed _eventSessionId, EVENT_SESSION_ID_KEY, and related getter/setter.

Fix track() to preserve existing session_id

When session_start/session_end events flow back through track(), preserve the session_id already set by performSessionTransition instead of overwriting it with the current sessionId.

Fix reset() to fire session_end

reset() now fires session_end before clearing state, matching the Swift SDK behavior. Uses multiRemove for atomic cleanup.

Batch persistence

Use AsyncStorage.multiSet in transitions instead of individual fire-and-forget setItem calls. Removes sessionId setter's persistence side-effect (now only persisted in performSessionTransition and reset()).

Cleanup

Remove 3 unconditional console.log debug statements and a dead //await this.saveSessionData() comment left from the v0.4.2 rewrite.

Test plan

  • All 23 plugin tests pass (including new concurrency, app state, and persistence tests)
  • Full test suite passes (66 suites, 363 tests, 0 failures)
  • Manual verification on iOS device: background/foreground cycles no longer produce 0-second sessions

🤖 Generated with Claude Code

Fix AmplitudeSessionPlugin producing 0-second sessions (session_start +
session_end at the same timestamp) on iOS. Root cause: the plugin ignored
iOS's `inactive` AppState, so the `active → inactive` transition before
backgrounding was treated as a no-op, causing stale lastEventTime and
immediate session expiration on resume.

Changes:

- Handle iOS `inactive` AppState: track `_previousAppState` and treat
  `inactive` the same as `background` for session timeout purposes,
  matching the core library's pattern in analytics.ts

- Await `onForeground`: was fire-and-forget, so session transitions
  could race with incoming events

- Replace `resetPending` flag with `_sessionTransition` promise: the
  flag was cleared asynchronously via the event pipeline when
  `session_start` flowed back through `track()`, creating a race
  window. The promise lets concurrent callers await the same transition.

- Remove dual-ID concept (`eventSessionId` / `_eventSessionId`): the
  v0.4.2 rewrite added a separate `eventSessionId` for enrichment, but
  `track()` overwrote it when session events flowed back through the
  pipeline after the transition completed. With the transition promise
  serializing access, no event can be enriched mid-transition, making
  the second ID unnecessary. Removed `EVENT_SESSION_ID_KEY` from
  persistence as well.

- Fix `track()` to preserve existing `session_id`: when session events
  (session_start/session_end) flow back through `track()`, preserve the
  session_id already set by `performSessionTransition` instead of
  overwriting it with the current sessionId.

- Fix `reset()` to fire `session_end` before clearing state, matching
  the Swift SDK behavior.

- Batch persistence: use `AsyncStorage.multiSet` in transitions and
  `multiRemove` in reset instead of individual fire-and-forget writes.

- Remove 3 unconditional `console.log` debug statements and a dead
  `//await this.saveSessionData()` comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant