Offline Behavior
Durable on-disk event queue, background URLSession delivery, network-aware flush
The Convert iOS SDK is built so tracking events are never lost, even on a flaky or absent network. This is a mobile-SDK capability with no equivalent in the request-scoped JS or PHP SDKs — it relies on on-device durable storage, a background URLSession, and an app-lifecycle observer.
What happens when the network is down
flowchart TD
A[Decision / conversion call] --> B[Event enqueued in memory]
B --> C{Flush succeeds?}
C -->|Yes| D[Delivered to Convert]
C -->|No / offline| E[Persisted to on-disk queue<br>Application Support / convert-sdk-events.json]
E --> F[Background URLSession<br>delivery task]
F -->|App suspended / terminated| G[OS completes upload in background]
G --> D
H[App returns to foreground] -->|cold-start recovery| B
I[Next launch] -->|start recovers persisted events| B
- Config fetch fails → the last successfully cached config is loaded from the on-disk cache (Application Support, coordinated file). If no cache exists,
runExperiencereturnsnilandrunFeaturereturns a disabledFeatureuntil the next successful fetch. - Tracking events generated while offline → persisted to a durable on-disk queue (
CoordinatedFileEventQueueStore— an atomicNSFileCoordinator-backed write in Application Support). The queue survives app restarts and process termination. - App is suspended or terminated before the queue drains → the pending batch is uploaded by a background
URLSessiontask (BackgroundSessionManager). The OS can complete the upload after your process is gone. - Next launch → the SDK recovers any events persisted by the previous process (
EventQueue.start()drains the on-disk queue into memory and arms the flush timer), so events are not lost across a termination. No integrator wiring is required for this zero-config durability guarantee.
Storage locations
| What | Where |
|---|---|
| Offline event queue | Application Support / convert-sdk-events.json (app-private, coordinated file write) |
| Last-good config cache | Application Support / convert-sdk-config.json (coordinated file write) |
| Visitor ID (system of record) | Keychain (app-scoped, survives reinstall depending on Keychain sharing settings) |
| Visitor ID (fast-read mirror) | UserDefaults (cleared on uninstall) |
All paths are inside the app's private sandbox (Application Support / Keychain). They are discarded on app uninstall (UserDefaults, Application Support) or remain depending on Keychain configuration.
Foreground / background handoff (UIKit lifecycle)
The SDK's LifecycleObserver registers for UIApplication foreground/background notifications:
- On background transition — the in-memory queue is flushed. Any events that could not be uploaded are persisted to the on-disk queue, and a background
URLSessionupload task is enqueued to ship them after the app suspends.UIApplication.beginBackgroundTaskbuys extra time for the flush attempt before suspension. - On foreground return / next launch —
EventQueue.start()recovers events from the on-disk queue into memory and arms the release timer, so they deliver on the next batch cycle.
LifecycleObserver is UIKit-gated (#if canImport(UIKit)) and built only on the production path; test paths inject a MockEventSink and skip the observer.
Optional background-completion forwarding
For prompt background-wake completion, forward your app delegate's callback to the SDK:
// AppDelegate.swift
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
sdk.handleEventsForBackgroundURLSession(
identifier: identifier,
completionHandler: completionHandler
)
}This is optional — the SDK reconciles persisted uploads on the next init regardless. An integrator that never wires this loses no events; they simply rely on the cold-start recovery path rather than the background-session acknowledgment path. The SDK rejects any identifier other than its own canonical background-session id (com.convertexperiments.sdk.background-upload), so a callback for an unrelated URLSession is never stored on its manager.
The bounded data-loss boundary
The durable queue has one documented failure mode: if the on-disk queue file is present but its bytes fail to decode (partial write from an unexpected kill, or external corruption), the SDK discards that file, logs a [WARN] line through the Logger port, reinitializes an empty queue, and continues. The events inside that corrupt batch are lost; nothing else is. A missing queue file — the normal state on first launch or right after a successful flush cleared it — is not corruption: it is treated as an empty queue silently, with no warning.
What you do not need to do
- No manual flush — the SDK batches and flushes on its own timer (
eventsReleaseIntervalMs). - No manual config refresh — the
ConfigRefreshScheduler(started after the first config lands) handles re-fetching while the app is in the foreground. - No background-task registration — the SDK registers its lifecycle observer synchronously in
ConvertSwiftSDK.init; the backgroundURLSessionis also wired at init time.
What is not persistent
- Events inside a corrupt queue batch (see bounded data-loss boundary above).
- Events that were never enqueued because
networkTrackingwasfalseor the runtimesetTrackingEnabled(false)gate was closed — suppressed events are never buffered. - The visitor ID in
UserDefaultsis cleared on app uninstall (the Keychain copy may survive depending on the app's Keychain sharing settings).
No platform permission required
Unlike Android's ACCESS_NETWORK_STATE, iOS does not require an explicit entitlement for the connectivity observation the SDK performs. The background URLSession requires the Background Modes → Background fetch capability in your Info.plist if you want the OS to wake your app proactively; without it, uploads still complete when the OS schedules the background session opportunistically.
Inspecting the queue during development
The on-disk queue file is a JSON array of tracking events at:
<app sandbox>/Library/Application Support/convert-sdk-events.json
Access it via Xcode's Devices and Simulators → Download Container or directly in the iOS Simulator file system at ~/Library/Developer/CoreSimulator/Devices/<device-id>/data/Containers/Data/Application/<app-id>/Library/Application Support/. See Troubleshooting.
Related pages
- Tracking Control — disabling tracking interacts with the queue
- API Communication & Tracking — the shared batching/queue concept doc
- Persistent DataStore — how sticky decisions persist