Documentation / iOS

Integrate an iOS app with Logister.

iOS apps use the public Swift Package Manager repository https://github.com/taimoorq/logister-ios.git. The package exposes a Swift async client named Logister for errors, logs, metrics, transactions, spans, and check-ins.

Before you start

Create the project with the iOS integration type.

What you needWhy it matters
iOS project in LogisterLocks the project type so collected iOS data is interpreted as mobile telemetry.
Backend token issuerKeeps the server project API token out of the IPA and returns short-lived mobile ingest tokens to the app.
Logister instance base URLBuilds the /api/v1/ingest_events endpoint used by the SDK.
Xcode or Swift Package ManagerResolves the public Git package and the Logister library product.
Optional GitHub App installationLets Logister use mobile source context and CI deployment records for source excerpts and deploy links.

What is supported

Use explicit reports for caught errors and MetricKit for OS diagnostics.

EvidenceSwift APILogister behavior
Handled reportcaptureExceptionAppears as Reported error with the reporting thread. It is never labelled fatal.
Apple diagnosticLogisterMetricKitCollectorUploads delayed crash, hang, CPU-exception, and disk-write diagnostics with triggered threads, binary UUIDs, raw addresses, and replay-safe event IDs.
LogcaptureMessageExplore › Events and event detail.
MetriccaptureMetricExplore › Charts, Explore › Events, and custom metric series.
TransactioncaptureTransactionPerformance and Charts for screen loads, sync work, and app jobs.
SpancaptureSpan with LogisterSpanRequest load waterfalls and detailed timing.
Check-incheckInMonitor status for recurring mobile work.
Source contextrepository, commitSHA, branch on LogisterClientSource lookup and deployment context when a GitHub repository is connected to the project.

Install

Add the package from GitHub with Swift Package Manager.

Privacy-safe error policies and minimized Apple context are available in package version 0.3.0. In Xcode, add the package URL https://github.com/taimoorq/logister-ios.git, choose the version rule your app prefers, and add the Logister product to your app target.

swift
dependencies: [
    .package(url: "https://github.com/taimoorq/logister-ios.git", from: "0.7.0")
]
swift
.product(name: "Logister", package: "logister-ios")

Configure

Create one client with app and release defaults.

Use an async LogisterTokenProvider, the app bundle ID as service, and the app version/build in release. Do not compile a long-lived Logister project API key into the app; your backend should mint short-lived mobile ingest tokens with POST /api/v1/mobile_ingest_tokens.

swift
import Foundation
import Logister

struct AppBackendTokenProvider: LogisterTokenProvider {
    func fetchToken() async throws -> LogisterToken {
        LogisterToken(
            token: "short-lived-mobile-token",
            expiresAt: Date().addingTimeInterval(900)
        )
    }
}

let client = LogisterClient(
    baseURL: URL(string: "https://your-logister-host.example")!,
    tokenProvider: AppBackendTokenProvider(),
    environment: "production",
    release: "1.4.0+42",
    repository: "acme/ios-app",
    commitSHA: "4f8c2d1",
    branch: "main",
    service: Bundle.main.bundleIdentifier,
    exceptionDataPolicy: .typeAndStacktrace,
    platformContextPolicy: .minimized
)

Keep real project API tokens on your backend only. Use a build setting or generated file for commitSHA and branch so shipped events point at the source that produced the app build.

baseURL must be the final https:// address of your Logister instance. The SDK does not follow redirects, and plain http works only for loopback hosts or with an explicit development opt-in; see Delivery and queue.

Source and deployments

Attach source context in the app, and record deployments from CI/CD.

The Swift client adds repository, commit_sha, and branch to event context when you set repository, commitSHA, and branch on LogisterClient. If the project is connected to the GitHub App, Logister can use those values to resolve stack frames and link to source.

Deployment records should be sent from CI/CD, not from the mobile app. After an App Store, TestFlight, Firebase App Distribution, internal, or enterprise deploy succeeds, POST a deployment envelope to /api/v1/deployments with the release, environment, repository, commit SHA, branch, and workflow URL.

json
{
  "deployment": {
    "release": "1.4.0+42",
    "environment": "production",
    "repository": "acme/ios-app",
    "commit_sha": "4f8c2d1a9b7e6c5d4a3b2c1d0e9f8a7b6c5d4e3f",
    "branch": "main",
    "release_tag": "ios-v1.4.0",
    "workflow_run_url": "https://github.com/acme/ios-app/actions/runs/123"
  }
}

Verify it

Send one test exception from a build using the same release. Open the event detail and check for source lookup status, deployment context, and GitHub links. If source is missing, confirm the GitHub App is installed, linked to the project, and the repository is connected; then check that CI sent a deployment record or the app event has repository and commit_sha.

Events

Send handled reports, logs, metrics, and transactions with async Swift.

swift
try await client.captureMessage(
    "Checkout opened",
    options: LogisterEventOptions(
        sessionID: "session-123",
        context: ["screen_name": .string("Checkout")]
    )
)

try await client.captureMetric("cart.item_count", value: 3, unit: "count")

try await client.captureTransaction(
    "screen.load",
    durationMs: 142.7,
    options: LogisterEventOptions(context: ["screen_name": .string("Checkout")])
)

do {
    try await runCheckout()
} catch {
    try await client.captureException(
        error,
        options: LogisterEventOptions(
            sessionID: "session-123",
            installationIDHash: "rotating-random-pseudonym",
            distributionChannel: "testflight",
            inForeground: true,
            breadcrumbs: [
                LogisterBreadcrumb(category: "navigation", message: "Opened checkout")
            ]
        )
    )
}

captureException records a caught, handled error. The SDK structures the reporting-thread frames and marks the event as nonfatal. Metric values should use stable names and units, and transactions should describe app work such as screen loads, local processing, sync jobs, or API calls. What a handled error sends depends on the error data policy.

Error data policy

Handled errors send the type and stack unless you choose .full.

exceptionDataPolicy on LogisterClient controls how much of a caught error captureException sends. The default is .typeAndStacktrace. Initializers that do not pass a policy, including the deprecated ones, use it.

PolicyWhat the event containsWhen to use it
.typeAndStacktrace (default)The error type and the structured stack only. No localized message or userInfo text.Every app, unless the error text has passed a privacy review.
.fullAdds the error's localizedDescription, the NSError domain and code, and a reviewed userInfo subset: failure reason, recovery suggestion, failing URL without its query, and the domains and codes of underlying errors.Only after you have reviewed that text against the app's data policy.

Exception data from either policy still passes through the shared redaction rule before it is queued. LogisterMetricKitCollector takes its own dataPolicy and also uses the safe policy by default.

Verify it

Capture a test NSError whose NSLocalizedDescriptionKey is a recognizable string. With the default policy, the event in the inbox identifies the error type and frames but does not contain that string. If the response is not accepted, check the base URL, token expiry, and your backend's mobile-token response before changing SDK code.

Delivery and queue

Events are written to a durable queue first, then sent over https with bounded retries.

The default transport sends on its own ephemeral URLSession with a 15 second request timeout and a 60 second resource timeout. It stores no cookies or cache and does not follow redirects, so the bearer token only reaches the configured endpoint. Point baseURL at the final URL. You can inject your own transport for tests or another networking stack.

The endpoint must use https. Plain http is accepted only for localhost, *.localhost, 127.x.x.x and ::1, or when you set LogisterConfiguration.allowsInsecureTransport for a development server and pass that configuration as configuration: to LogisterClient:

swift
var configuration = LogisterConfiguration()
configuration.allowsInsecureTransport = true // local development only

Without it, capture against an http endpoint returns a .dropped response and nothing is queued.

SituationWhat the SDK does
Network failure, HTTP 408, 425, 429, or 5xxRetries up to three attempts by default. Computed delays are jittered between half and all of the exponential backoff, and Retry-After is honored up to the configured maximum delay. If the attempts run out, the event stays queued.
401Fetches a new token once and resends. If the server still answers 401, the event stays queued with backoff.
Token provider fails or takes longer than tokenProviderTimeout (30 seconds by default)The attempt fails and the event stays queued. Invalidating a token does not cancel a refresh that other callers share.
Any other permanent non-2xx, including 403The response is not accepted and the event is removed so it cannot block later events.
http endpoint without the opt-in, or an internal payload failureCapture returns a .dropped response instead of throwing. Nothing is queued.

Queued events leave the queue when they are delivered, when they pass maximumQueueAge, or when the queue bounds evict them. The package never reports queued delivery as server acceptance. To disable retries or tune the bounds, pass a retry policy:

swift
let client = LogisterClient(
    baseURL: URL(string: "https://your-logister-host.example")!,
    tokenProvider: AppBackendTokenProvider(),
    retryPolicy: LogisterRetryPolicy(
        maximumAttempts: 2,
        baseDelay: 0.5,
        maximumDelay: 10
    )
)

Every event receives its UUID and exact capture time, then is persisted before token acquisition or network delivery. Each queued event is its own file, so an enqueue, delivery, or retry writes only that event. The queue lives in Application Support, is excluded from backup, and is scoped by endpoint, bundle/service, optional storageScope on LogisterConfiguration, and process name. Tokens are never persisted. The main app and each extension own separate queues; sharing one queue through an App Group across processes is not supported.

On first launch after upgrading to 0.7.0, events still waiting in the previous delivery-<scope>.json file are migrated into a delivery-<scope>/ directory with one file per event.

  • Call flushQueuedEvents() after authentication to force a retry.
  • A queued response has deliveryState == .queued and accepted == false.
  • setCollectionEnabled(false) stops capture and, by default, purges this client's queue and rotating installation pseudonym.
  • healthSnapshot() exposes only collection state, queue and drop counts, last delivery and error, MetricKit subscription, decoder, and installation-capability state.

Verify it

With the device offline, capture one message and check that the response has deliveryState == .queued and healthSnapshot() reports a queued event. Reconnect, call flushQueuedEvents(), and confirm the event appears once in Logister. If every capture returns .dropped, check that baseURL uses https.

MetricKit diagnostics

Keep one opt-in collector alive for the app lifetime.

MetricKit is the supported automatic source for Apple crash and hang evidence on the package's current deployment targets. Delivery is delayed and controlled by the operating system; it is not a real-time fatal callback.

swift
@available(iOS 15.0, *)
final class AppDiagnostics {
    let collector: LogisterMetricKitCollector

    init(client: LogisterClient) {
        collector = LogisterMetricKitCollector(client: client, dataPolicy: .typeAndStacktrace) { message in
            print("MetricKit upload failed: \(message)")
        }
        collector.start()
    }

    deinit { collector.stop() }
}

The collector uploads crash, hang, CPU-exception, and disk-write diagnostics. Safe mode omits the raw diagnostic payload and termination reason and bounds normalized threads and frames. Threads are sent once as flattened frames with the triggering thread first, so that thread is kept when the payload budget trims the others. Every payload gets a deterministic event UUID. If Apple redelivers it or a transient retry reaches Rails twice, Logister returns the original accepted event without increasing occurrence, installation, or session impact.

Delivery behavior

MetricKit uploads use the same short-lived-token path, durable queue, and retry policy as other events. See Delivery and queue. The SDK never reports queued work as server acceptance.

Production evidence

Manage symbols and Apple reports as separate sources.

WherePurposeHealthy state
Releases → Artifacts → Upload a dSYM archiveUploads a zipped dSYM to private archive storage and verifies its binary UUID and architecture in a background worker.Ready. Awaiting tooling means this worker cannot run Apple dwarfdump; Issues keeps showing raw addresses. Artifacts also shows which observed builds each dSYM covers.
Project Settings → Integrations → App Store ConnectResolves the configured bundle ID and imports Apple's iOS power/performance report with a short-lived ES256 API token.A selected app, last-success timestamp, report state, freshness note, and no current error.

Store the App Store private key in the host secret store under an environment variable whose name starts with LOGISTER_INTEGRATION_ (for example LOGISTER_INTEGRATION_APP_STORE_CONNECT_KEY) and enter only that name in Logister. Other names are rejected unless the operator lists them in LOGISTER_INTEGRATION_CREDENTIAL_ENV_ALLOWLIST; when upgrading an integration that uses an older name such as APP_STORE_CONNECT_PRIVATE_KEY, rename the secret and update the reference, or add the old name to that allowlist on the web and worker processes and restart them. Sync failures for credentials show a generic reason such as Credential could not be parsed. Store aggregates are not SDK occurrences and are never silently added to MetricKit or crash-free counts.

Spans and check-ins

Use spans for waterfalls and check-ins for recurring iOS work.

swift
try await client.captureSpan(
    LogisterSpan(
        traceID: "trace-123",
        spanID: "span-456",
        parentSpanID: "span-root",
        name: "GET /checkout",
        kind: "http",
        status: "ok",
        durationMs: 42.5,
        context: ["screen_name": .string("Checkout")]
    )
)

try await client.checkIn(
    "daily-sync",
    status: "ok",
    options: LogisterEventOptions(
        durationMs: 812.4,
        context: ["expected_interval_seconds": .number(86_400)]
    )
)

Use shared trace IDs when a screen action triggers multiple spans. Check-ins are useful for recurring sync, cleanup, cache refresh, background fetch, or other scheduled app work where a missed run should be visible.

Mobile context

Start with the automatic Apple context, then add only deliberate correlation.

ContextCollectionWhy it helps
Bundle, version/build, process, and inferred releaseAutomatic from Bundle.mainRelease and build filtering without duplicated app configuration.
Apple platform, OS version/build, device family/model, architecture, localeAutomatic from platform APIsFinds OS-, hardware-, and architecture-specific failures.
Session IDOptional app-provided valueEnables affected-session impact when your privacy model permits it.
Installation hashOptional rotating random pseudonymEnables affected-installation impact without a hardware or advertising identifier.
Distribution, foreground, screen, breadcrumbsOptional app-provided valuesExplains the app state and bounded trail before a report.
Repository, commit SHA, branchBuild-time valuesConnects frames and CI deployment records to exact source.

Privacy defaults

Do not turn device identity or raw errors into monitoring context.

The SDK recursively removes common IDFA, IDFV, serial, and hardware-identifier aliases before encoding an event. Rails repeats the same normalization at ingestion. This is a guardrail, not a substitute for reviewing custom context.

  • .typeAndStacktrace is the default error data policy. Switch to .full only after raw error text has completed a privacy review.
  • Sensitive keys and credential-shaped strings are replaced with "[REDACTED]" by the shared redaction rule. Add app-specific key names to that rule rather than relying on custom context being clean.
  • Use platformContextPolicy: .minimized when exact model, locale, architecture, and OS build are not needed.
  • MetricKit safe mode omits raw payloads and termination reasons while retaining bounded frames and stable codes.
  • Do not send API tokens, authorization headers, cookies, payment data, medical data, raw request/response bodies, IDFA, or raw IDFV.
  • Use an app-generated rotating pseudonym only when affected-installation impact is necessary, and hash it before capture.
  • Keep breadcrumbs bounded and free of typed input, raw URLs, query values, and user content.

Redaction and payload limits

Sensitive values become "[REDACTED]", and every event is fitted to the server's limits.

Every event passes through the shared Logister redaction rule before it is queued. That covers context, breadcrumbs and their data, span and HTTP fields, exception data, MetricKit evidence, and anything beforeSend adds.

  • A key is sensitive when, after lowercasing and removing every non-alphanumeric character, it equals one of LogisterPayloadPolicy.defaultSensitiveKeys (password, passwd, pwd, secret, token, apikey, authorization, proxyauthorization, cookie, setcookie, sessiontoken, sessionkey, credential, credentials, privatekey, clientsecret, accesstoken, refreshtoken, idtoken, authtoken, xapikey, xauthtoken, xaccesstoken, xcsrftoken, xxsrftoken, signature, ssn, creditcard, cardnumber, cvv), or contains token, secret, password, passwd, apikey, authorization, credential or cookie. So accessToken, x-api-key and Set-Cookie all match. The key is kept and its value becomes "[REDACTED]".
  • Strings are scrubbed for Bearer <token> and Basic <credentials>, JWTs, and lgst_ keys. URLs inside strings keep scheme, host and path; userinfo, query and fragment are removed.
  • IDFA, IDFV, serial numbers and common hardware-identifier keys are removed.
  • The SDK's own envelope fields (uuid, event_type, occurred_at, trace_id, request_id, session_id) are never redacted.

Add your own keys by extending the default list. Entries are normalized the same way and must match exactly:

swift
let configuration = LogisterConfiguration(
    payloadPolicy: LogisterPayloadPolicy(
        sensitiveKeys: LogisterPayloadPolicy.defaultSensitiveKeys.union(["pin_code", "account-number"])
    )
)

LogisterPayloadPolicy also bounds depth, items, string length, total object keys (maximumKeys, 512) and bytes (maximumEnvelopeBytes, 1,000,000). The key and byte defaults match the Logister server's per-event limits. When a budget runs out, the SDK keeps every envelope field and its own platform and error context, keeps small objects before large ones and the newest breadcrumbs, drops the rest, and sets context._truncated to true. An event that is still too large is shrunk further rather than dropped. NaN and infinite numbers are sent as null.

Verify it

Send one captureMessage with a context key such as accessToken or one of your custom keys, then open the event detail and confirm the key is present with the value "[REDACTED]". If an event shows _truncated in its context, reduce the size of custom context or the number of breadcrumbs attached to it.

Verification

Confirm one event from the app before widening instrumentation.

Checklist
Swift Package Manager resolves the Logister product
client can send one captureMessage call
Events page shows the iOS log event
Issues shows a test exception when captureException is called
Test exception shows the error type and frames, not its localized text
Sensitive context keys show the value "[REDACTED]"
Offline captures report deliveryState == .queued and arrive after flushQueuedEvents()
Explore › Charts can filter by environment, release, service, or screen_name
Performance shows transactions or spans after timing events arrive
Event detail shows source context when repository and commitSHA are configured
Event detail shows deployment context after CI posts /api/v1/deployments
Manual capture is labelled Reported error, not Fatal
MetricKit test evidence shows its source and does not duplicate on redelivery
dSYM status shows Ready or an explicit worker-tooling action
App Store settings show provenance, last success, freshness, and errors

Upgrading to 0.7.0

No source changes are required, but several defaults change.

0.7.0 is a security and reliability release with no source-breaking API changes. Set your package rule to 0.7.0 or later, then review these defaults. The install snippet above follows the verified published-version catalog.

Area0.7.0 defaultWhat to check
Error dataInitializers that do not pass exceptionDataPolicy, including the deprecated ones, use .typeAndStacktrace. .full sends localizedDescription, the NSError domain and code, and a reviewed userInfo subset.Pass .full explicitly if you need error messages and have reviewed them. See Error data policy.
RedactionThe shared Logister rule. Matching keys stay in the event with the value "[REDACTED]", and more keys match.Searches or exports that expect those keys to be absent. Add app-specific keys to defaultSensitiveKeys. See Redaction and payload limits.
Payload sizeEach event is held to 512 object keys and 1,000,000 bytes. Large contexts and long breadcrumb lists are trimmed and marked with context._truncated.Events with _truncated in context.
TransportDedicated ephemeral URLSession with 15 second request and 60 second resource timeouts, no cookies or cache, and no redirects. http endpoints are refused unless the host is loopback or allowsInsecureTransport is set.Point baseURL at the final https URL. Development builds that use a non-loopback http server need the opt-in.
Tokens and retriesToken provider calls time out after tokenProviderTimeout (30 seconds). A repeated 401 keeps the event queued. Computed retry delays are jittered.Your backend's mobile-token endpoint answers well within 30 seconds.
Durable queueOne file per event in a delivery-<scope>/ directory. Events in an existing delivery-<scope>.json file are migrated on first launch.healthSnapshot() queue counts after the first launch of the upgraded build.
MetricKit diagnosticsCrash diagnostic events omit diagnostic.call_stack_tree (it repeated the thread frames) and source_evidence.diagnostic. The triggering thread is sent first. With .full, the raw payload is still sent as context.metrickit.Saved searches or exports that read those two fields.

Verify: run the checklist above against a build that uses 0.7.0. A handled test error should arrive without its localized text, a sensitive context key should show "[REDACTED]", and an offline capture should be delivered after flushQueuedEvents(). Read the package changelog for the full list of changes.

Package releases

iOS SDK releases are versioned separately from the Rails app.

Swift Package Manager distribution resolves from the public Git repository and semantic version tags. The iOS release workflow verifies the package and creates the matching GitHub Release from the checked-in changelog, so app teams can update through Xcode or SwiftPM when a new tag is available.

Follow a request into the backend

Logister 3.7+ supports explicit app/backend connections; 3.8 adds captured HTTP outcome, timing, app build and issue ownership to related requests. Use SDK 0.6.0+ for the opt-in HTTP wrapper and origin allowlist. SDK 0.6.1 adds optional method, status code, failure category, attempt and duration-scope metadata. Verify the required release is available in the package channel before upgrading.

The URLSession wrapper measures through receipt of the response body. Each explicit wrapper call has its own immutable request handle; attach that handle to a handled error from the same call. Client and backend durations measure different work; do not subtract them to estimate network latency. The enrichment does not capture additional request/response bodies or headers, or join user identities across projects.

  1. Connect the mobile project to its backend with exact environment mappings and enable related requests for both projects and the instance.
  2. Configure the wrapper with an exact backend-origin allowlist and exclude token-issuer URLs. Keep each project's own release and environment.
  3. Trigger a synthetic failed call, attach its request handle to the handled error, then inspect Related requests and Inspect connected impact.

Read the SDK wrapper example, connection setup and impact and coverage limits. Installation examples above follow the verified published-version catalog; feature minimum versions may be newer.