Skip to content

api communication

Joseph Samir edited this page Jun 21, 2026 · 2 revisions

API Communication & Tracking

Welcome back! In RuleManager, we saw how the SDK acts like a bouncer, checking if visitors meet specific criteria before they can participate in an experiment. We've covered how the SDK manages visitors, experiments, features, data, bucketing, and rules internally.

But how does the SDK get the initial setup information (like the details of your experiments and those rules) from Convert's servers? And how does it report back what happened (like which variation a visitor saw, or if they completed a goal)?

The Problem: Talking to the Outside World

Imagine the Convert SDK running in your application is like a local branch office. It needs to:

  1. Get Instructions: Receive the latest operational plan (your project configuration, including experiments, features, audiences) from the headquarters (Convert servers).
  2. Send Reports: Send status updates (tracking events like "visitor A saw variation B", "visitor C completed goal X") back to headquarters so you can see the results in your Convert dashboard.

How does this local branch office (the SDK) communicate reliably with headquarters (Convert servers)?

The iOS Communication Stack

The iOS SDK splits API communication across two focused components rather than a single ApiManager:

  • ConfigFetchService (in Sources/ConvertSwiftSDK/) — fetches and caches the project configuration. It composes a URLSessionHTTPClient transport (a concrete HTTPClient port adapter backed by URLSession) with a CoordinatedFileStore for write-through on-disk caching.
  • EventQueue (in Sources/ConvertSwiftSDKCore/Event/) — an actor that batches produced tracking entries and ships them through a URLSessionEventUploader. For durable delivery after app suspension or termination it hands off to a BackgroundSessionManager that uses a dedicated background URLSession.

Both components are wired together in ConvertSwiftSDK.init (the composition root) and operate entirely behind the public API — you rarely interact with either directly.

Configuration Fetching (ConfigFetchService)

When the SDK initialises with an sdkKey, a detached Task in ConvertSwiftSDK.init builds a ConfigFetchService and runs a two-phase load:

  1. Cache hit first: loadCachedConfig() reads the on-disk cache from the Application Support directory. A cache miss is silent; corrupt bytes are deleted and logged as WARN.
  2. Live fetch: fetchLiveConfig() issues an HTTP GET to the config endpoint, write-through caches the verbatim response bytes (not a re-encode), and returns the decoded ProjectConfig.

The URL shape is {apiConfigEndpoint}/config/{sdkKey}, where apiConfigEndpoint defaults to https://cdn-4.convertexperiments.com/api/v1 (from ConvertConfiguration.defaultAPIBase). When environment is set a ?environment=... query item is appended; networkCacheLevel == .low adds ?_conv_low_cache=1.

Every outbound request — config fetch and event delivery alike — carries User-Agent: ConvertAgent/<version>. The URLSessionHTTPClient stamps this header last (replacing any caller-supplied value) so the bot-gate bypass is always in effect.

sequenceDiagram
    participant SDK as ConvertSwiftSDK (init Task)
    participant CFS as ConfigFetchService
    participant Cache as On-disk cache (Application Support)
    participant HTTP as URLSessionHTTPClient
    participant CDN as Convert Config CDN

    SDK->>+CFS: loadCachedConfig()
    CFS->>+Cache: read(from: cacheURL)
    Cache-->>-CFS: Data (or miss)
    CFS-->>-SDK: ProjectConfig? (nil on miss/corrupt)

    SDK->>+CFS: fetchLiveConfig()
    Note over CFS: Build URL: {apiConfigEndpoint}/config/{sdkKey}
    CFS->>+HTTP: get(url:, headers: {Authorization?})
    Note over HTTP: Stamps User-Agent: ConvertAgent/<version>
    HTTP->>+CDN: GET /api/v1/config/{sdkKey}
    CDN-->>-HTTP: Raw JSON bytes (200 OK)
    HTTP-->>-CFS: (Data, HTTPURLResponse)
    CFS->>Cache: write(verbatimBytes, to: cacheURL)
    CFS-->>-SDK: ProjectConfig
Loading

The decoded ProjectConfig is stored in ConfigStore, which fires SystemEvent.ready once and resumes all waitForReady() continuations. Post-ready refreshes fire SystemEvent.configUpdated instead.

Event Tracking (EventQueue)

The EventQueue actor is the EventSink every ConvertContext enqueues bucketing and conversion events through. It never sends an event immediately; instead it batches by two triggers:

  • Size trigger — once the buffer reaches eventsBatchSize (default Defaults.batchSize from ConvertConfiguration) the queue drains immediately without blocking the enqueueing caller.
  • Interval trigger — a timer loop sleeps eventsReleaseIntervalMs ms (default Defaults.releaseIntervalMs) then flushes whatever is buffered. The timer starts lazily on the first enqueue and runs on Task.sleep(nanoseconds:) (the iOS 15–safe form).

On a successful flush the queue fires SystemEvent.apiQueueReleased carrying the event count. On failure it restores the drained entries to the buffer in their original order so the next cycle re-delivers them.

The tracking endpoint URL is {apiTrackEndpoint}/track/{sdkKey}, assembled inside URLSessionEventUploader. Like the config fetch, every request carries User-Agent: ConvertAgent/<version>.

Wire source is "ios-sdk" (stamped into the tracking envelope; the JS SDK uses "js-sdk" by default).

sequenceDiagram
    participant Ctx as ConvertContext
    participant EQ as EventQueue (actor)
    participant Up as URLSessionEventUploader
    participant Track as Convert Track API

    Ctx->>+EQ: enqueue(.bucketing(…), for: visitorId)
    Note over EQ: Add to buffer. Buffer size = 1.
    Ctx->>+EQ: enqueue(.conversion(…), for: visitorId)
    Note over EQ: Add to buffer. Buffer size = 2.
    loop When batchSize reached OR release interval fires
        EQ->>EQ: drain() — copy buffer, clear buffer
        Note over EQ: Assemble visitors:[{visitorId, events}] payload
        EQ->>+Up: upload(payload)
        Up->>+Track: POST /api/v1/track/{sdkKey} (JSON)
        Note over Up: User-Agent: ConvertAgent/<version>
        Track-->>-Up: 200 OK
        Up-->>-EQ: success
        EQ->>EQ: fire .apiQueueReleased (eventCount)
    end
Loading

Background & Durable Delivery

The iOS SDK guarantees delivery even when the app is suspended or killed before a flush:

  • On-disk persistence (CoordinatedFileEventQueueStore) — when the app goes to the background, LifecycleObserver triggers the queue to write its buffer to disk. On the next start() (cold-start recovery), pending entries are read back and re-loaded into the buffer.
  • Background URLSession (BackgroundSessionManager) — uses a separate background URLSession with a SDK-owned identifier so uploads can complete while the app is suspended. The integrator forwards application(_:handleEventsForBackgroundURLSession:completionHandler:) through sdk.handleEventsForBackgroundURLSession(identifier:completionHandler:) for prompt OS-level completion acknowledgement (optional, for efficiency — the zero-config durability guarantee holds without it).

Key Configuration Knobs

All values live on ConvertConfiguration (passed to ConvertSwiftSDK.init(configuration:)):

Property Default Effect
apiConfigEndpoint "https://cdn-4.convertexperiments.com/api/v1" Base URL for config fetch
apiTrackEndpoint "https://cdn-4.convertexperiments.com/api/v1" Base URL for event delivery
eventsBatchSize Defaults.batchSize Events per flush batch
eventsReleaseIntervalMs Defaults.releaseIntervalMs Flush interval (ms)
networkTracking true Global tracking on/off (static, init-time)
networkCacheLevel .normal CDN cache hint; .low adds _conv_low_cache=1

A runtime tracking toggle is available via await sdk.setTrackingEnabled(false) / isTrackingEnabled() — see Tracking Control.

Platform-Specific Differences

Aspect Browser / Node.js (JS SDK) Server-side (PHP SDK) iOS SDK
HTTP client Built-in HttpClient (fetch/XHR) PSR-18 auto-discovered client URLSession via URLSessionHTTPClient
Queue flush trigger setTimeout release interval register_shutdown_function at request end Size trigger + Task.sleep interval timer
Background delivery navigator.sendBeacon() on page unload Not applicable (request-scoped) Persistent disk buffer + background URLSession; LifecycleObserver triggers persist-on-background
Release interval Configurable (default 10 000 ms) None Configurable via eventsReleaseIntervalMs
Retry / durability Log on failure; no automatic retry 2 retries with 100 ms / 300 ms backoff Failed flush restores buffer for the next cycle; on-disk persistence survives process kill
Timer management startQueue() / stopQueue() None Lifecycle-driven — LifecycleObserver persists on background / flushes on foreground

Conclusion

The iOS SDK's communication layer is split into two focused, actor-safe components:

  1. ConfigFetchService fetches the project configuration (ProjectConfig) via URLSession, caches it to Application Support, and supplies it to ConfigStore which signals readiness.
  2. EventQueue collects tracking entries produced by ConvertContext, batches them by size and interval, and ships them through URLSessionEventUploader — with durable background delivery via BackgroundSessionManager and on-disk persistence via CoordinatedFileEventQueueStore.

Both components stamp every outbound request with User-Agent: ConvertAgent/<version> so Convert's bot filter accepts them, and wire source is "ios-sdk" on the tracking payload.

Let's explore the internal notification system the SDK uses when important events happen next: EventManager!

Clone this wiki locally