Skip to content

fix: websocket close codes, backoff, app lifecycle, no reconnect on unchanged identity - #54

Merged
boehlerlukas merged 1 commit into
mainfrom
fix/websocket-lifecycle
Oct 11, 2026
Merged

boehlerlukas merged 1 commit into
mainfrom
fix/websocket-lifecycle

Conversation

@boehlerlukas

Copy link
Copy Markdown
Contributor

Why

Server diagnostics (EU, 2026-10-11):

  • ~80% of SDK websockets close within 30 s; ~70% of those are iOS SDK sockets.
  • Mostly close code 1005 (closed without a status code), plus 1006.
  • Unique contacts ≈ connects, so these are not single clients looping. Each app user opens and quickly closes sockets on app start, identify and background/foreground.

What the code did (GleapWebSocketHelper.m / GleapSessionHelper.m):

  • It closed with a plain [webSocketTask cancel], which sends no close code. The server logs that as 1005.
  • After any receive error it reconnected after a fixed 5 s, with no backoff or jitter.
  • It reconnected on every session start and on every identify, even for the same identity. A session start followed by an identify opened two sockets.
  • It had no background/foreground handling. A suspended app's socket died as 1006, and the 40 s ping timer kept running with no socket.

Changes (public API unchanged)

All socket state stays on one serial queue, the main queue, where the socket's answers, timers and lifecycle notifications already arrive. Calls from other threads hop over to it.

  1. Close codes. Every close the SDK makes on purpose uses cancelWithCloseCode:reason:. It sends 1000 (normal) when it replaces or drops a socket and 1001 (going away) when the app stays in the background.
  2. Backoff with jitter. Reconnects after an error wait 2 s → 4 → 8 → 16 → 32 → 60 s (cap), each ±20%. The count starts over once a socket received a message or stayed open ≥ 30 s, or when the network comes back. No reconnect is attempted while the app is in the background or offline. Offline is detected with NWPathMonitor, so Network is now linked in Package.swift and the podspec.
  3. App lifecycle.
    • On UIApplicationDidEnterBackgroundNotification the socket gets a 10 s grace, then closes with 1001. A background task gives the close frame time to go out, and the grace is cancelled if the app returns.
    • On WillEnterForeground / DidBecomeActive it reconnects, debounced.
    • An app launched in the background (push, background fetch) opens no socket until it comes to the foreground.
    • Scene-based apps are covered too: UIKit posts these app-level notifications when the last scene goes to the background or the first one comes back.
  4. Connect only on a channel change, debounced. connectToURL: now only asks for a channel. The socket opens 1.5 s after the last ask, so a session start followed by an identify opens one socket. If a socket is already open or opening for the same channel (same gleapId, gleapHash, apiKey and URL; query order doesn't matter), it is kept.
  5. Ping. The 40 s interval is unchanged. The ping timer now starts with a socket and stops when it closes. Pong answers from a replaced socket are ignored.

The rules live in the new internal, stateless GleapWebSocketPolicy (Sources/Internal, hidden like the other internal classes): the connect decision, the reconnect delay and the backoff reset.

Expected effect

On new SDK versions this should mean:

  • far fewer connects per user: one socket per foreground session and identity, not one per start/identify
  • 1000/1001 close codes in place of 1005, and fewer 1006s from suspended apps
  • no reconnect storms during outages

Old SDK versions are mitigated server-side separately.

Testing

  • New Tests/GleapTests/GleapWebSocketTests.swift (6 tests):
    • backoff curve and jitter bounds (2 → 60 s, ±20%, clamping)
    • backoff reset after a message or ≥ 30 s open
    • should-reconnect decision: unchanged identity → keep, including reordered query; changed gleapId/gleapHash/apiKey or a dead socket → connect; background/offline → wait; no session → nothing
    • integration: after a background notification, connectToURL: opens no socket for the new channel; after returning to the foreground it connects
  • .github/scripts/test.sh (xcodebuild test, iPhone 17 Pro simulator, iOS 26.4): all 132 tests pass, including the existing testTheWebSocketCarriesTheSessionCredentials, which now covers the debounced connect.
  • pod lib lint Gleap.podspec --allow-warnings --quick passes. The full lint runs in CI.
  • Not tested on a device against the live server. The 1000/1001 codes and fewer connects should be checked in the server close-code stats once a release carrying this is out.

No version bump and no release. That goes through the SDK release process later.

🤖 Generated with Claude Code

…nchanged identity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boehlerlukas
boehlerlukas merged commit dfad161 into main Oct 11, 2026
1 check passed
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