Skip to content
Console
Browse documentation
Guide

iOS

Capture iOS telemetry, remote flags, headless surveys and optional native structural replay.

On this page

The native Anectico package supports iOS 15 and later with no runtime dependencies beyond Apple system frameworks.

Intake credentials

Set apiKey to a project-bound native_ingest key (ingest:write) and captureApiKey to a different mobile_capture key (analytics:write). Replace the example placeholders with your issued keys. Never bundle server, read or management credentials. Mobile capture declares a public-client origin; it does not establish trusted server authority.

Without captureApiKey, product events, identify, group and automatic lifecycle events are no-ops before identity changes or queue admission; OTLP remains available and configuration logs a warning. Malformed or shared capture keys reject configuration. Missing credentials do not erase queued product events: the flush result reports pending product positions. Consent withdrawal still purges the queue and remains in force after a key is added. Logout and reset require available product collection. See native intake keys for creation and verification.

Install with Swift Package Manager

During early access, use the authorized repository and revision supplied during onboarding. In Xcode, choose File → Add Package Dependencies… and add that repository. To follow the current source while evaluating the SDK, add this to Package.swift:

dependencies: [
    .package(url: "https://github.com/anectico/anectico.git", branch: "main"),
],
targets: [
    .target(name: "MyApp", dependencies: [.product(name: "Anectico", package: "anectico")]),
],

Pin the onboarding commit before a reproducible build. After tagged SDK releases begin, replace the branch requirement with the published semantic version.

Configure once

Call Anectico.configure in your SwiftUI App initializer or application(_:didFinishLaunchingWithOptions:):

import Anectico

try Anectico.configure(
    AnecticoOptions(
        apiKey: "an_...",
        captureApiKey: "an_<mobile-capture-key>",
        environment: "production",
        release: "com.acme.Shop@2.4.1+318",
        dist: "318"
    )
)

configure throws AnecticoConfigurationError for invalid options. Handle that setup error in your application bootstrap. Telemetry methods called before configuration are no-ops with a warning. Async bootstrap is supported: if configuration finishes after the application is already active, Anectico catches up the missed UIKit notification and emits that process's $app_opened once. Later background-to-foreground transitions emit one additional $app_opened each; duplicate notifications within one lifecycle phase are ignored. Recurring flushes stay dormant in the background after one explicit background flush.

Use separate project-bound native_ingest and mobile_capture keys. Do not include read, management, or agent scopes in the application.

Connect identity and events

The first argument is the identifier. Use a stable id from your own system; it is stored exactly as sent. An email address sent as a property is stored as [REDACTED], so nobody can search by it or read it later. See What Anectico redacts and withholds.

Anectico.identify("user_8842", set: ["email": "buyer@acme.example", "plan": "pro"])
Anectico.group(type: "company", key: "acme")
Anectico.capture("checkout_started", properties: ["cart_size": 3])
Anectico.screen("Checkout")

// On logout:
Anectico.reset()

reset also clears global error-user context and buffered breadcrumbs, preventing the prior account's diagnostic context from attaching to the next account on a shared device.

Every captured event — not only the automatic $app_opened/$app_backgrounded lifecycle events — carries the reserved $release and $app_version properties, sourced from the same release/serviceVersion you pass to configure (or your app's bundle version when you don't set them explicitly). Neither is invented: an app that never configures a release sends events with neither property. See Reserved event properties.

Send revenue

To measure money with revenue analytics, send $revenue and $currency as ordinary properties on the event for each payment:

Anectico.capture("purchase", properties: ["$revenue": 19.99, "$currency": "USD", "plan": "pro"])

$revenue is a number in major units (19.99, not 1999), negative for a refund, with at most four decimal places; $currency is three upper-case letters. A value sent as text, with more decimal places, or with a lower-case currency is counted as invalid and left out of every figure.

Capture errors and crashes

Anectico.addBreadcrumb(category: "navigation", message: "opened checkout")
do {
    try checkout()
} catch {
    Anectico.captureError(error)
}

Fatal signals and uncaught exceptions are spooled and delivered on the next launch. Disable handler installation with enableCrashReporting: false only when another crash SDK must own the process handlers.

The SDK keeps the complete Mach-O image list only in its private crash sidecar. A recovered envelope carries the per-frame UUID and load address plus a bounded table of frame-referenced images, preventing large simulator image sets from exceeding the ingest attribute limit while preserving dSYM symbolication.

Anectico classifies a caught Swift error or explicitly captured Objective-C exception as handled and nonfatal. An uncaught Objective-C exception or fatal signal is unhandled and fatal, with a distinct mechanism in each case. The occurrence also includes release and distribution, SDK and platform, hardware model code and family, OS version, and app version and build. Fatal-signal recovery uses the crash-time snapshot even when the application is upgraded before relaunch.

This context is privacy-safe: a model code such as iPhone15,4 identifies a device family, not one phone. Anectico does not collect IDFV, the user-assigned device name, serial number, or installation ID. These automatic values and crash semantics cannot be replaced through CaptureOptions.tags.

Upload the release build's dSYM:

anectico symbols upload-dsym MyApp.dSYM

The CLI accepts either the .dSYM bundle or its exact Mach-O file under Contents/Resources/DWARF/; it locates the bundle's Xcode-named executable and uploads a fat and universal binary without splitting it. Anectico matches each in-app frame to an uploaded dSYM slice by the exact Mach-O image UUID; release and distribution are not dSYM lookup keys. Use the artifact from the archive that produced the installed application.

Propagate to your backend

var request = URLRequest(url: url)
AnecticoPropagation.apply(to: &request)

This adds W3C trace and customer baggage. Apply it only to trusted application backends. iOS does not swizzle networking, so propagation is explicit.

Verify

Identify a test user, capture an event and caught error, then call Anectico.flush. Confirm both appear under the same customer: ask your agent to read the customer's timeline (get_person_timeline over MCP, or anectico persons timeline <customer-id>). For a crash test, relaunch before checking because fatal data is sent on the next launch. Ask your agent for the issue (get_issue) and open the link in its result: the read-only issue proof page shows Handling, Mechanism, Release, Distribution, SDK / Platform, Device, OS, and App version / build, so you can compare them between the caught and fatal occurrences.

Delivery results

Wait for the completion or future returned by Anectico.flush or Anectico.shutdown. Inspect productEvents, nullable logs and traces independently, including all counts, warnings and unavailable reasons. Accepted intake is not stored evidence; read the test customer's timeline too. The iOS delivery reference defines TelemetryFlushResult, ErrorCaptureResult, partial outcomes and retry limits.

Remote flags and headless surveys

These optional APIs are available in the current Swift source. Core protocol tests run on Linux; iOS device and simulator qualification is pending. Pin the supplied source revision. This is not a qualified mobile release or evidence that exposures and answers have reached a customer timeline.

Keep ingestion and application delivery addresses and credentials separate:

try Anectico.configure(AnecticoOptions(
    apiKey: nativeIngestKey,
    captureApiKey: mobileCaptureKey,
    endpoint: "https://api.example.com",
    applicationEndpoint: "https://console.example.com",
    projectID: projectID,
    flagDeliveryApiKey: flagDeliveryKey,
    surveyDeliveryApiKey: surveyDeliveryKey,
    surveyScreenPaths: ["Checkout": "/checkout"]
))

Use a canonical project UUID. Issue separate project-bound CLIENT_DELIVERY keys with flags:evaluate for decisions and surveys:respond for survey delivery. Never embed a management, read or agent key. Delivery keys cannot replace capture or ingest credentials. Omit a delivery key to leave its controller absent; configuration creates no application requests or polling.

if let flags = Anectico.flags {
    let document = try await flags.reload(keys: ["new_checkout"])
    let checkout = flags.boolean(for: "new_checkout", fallback: false)
    // Inspect checkout.reason, checkout.usedApplicationFallback and document.evaluatedAt.
    let state = flags.status(for: "new_checkout")
    _ = state.decision?.definitionVersion
}

Unknown, expired, failed and unavailable decisions retain their reason. A present false value is a real result; it differs from using your explicit application fallback. Payload absence differs from explicit JSON null. Version strings preserve their exact precision. Decisions live in memory and expire 30 seconds after the original evaluation. Reload accepts 1–100 unique keys and bounded declared context; no local targeting rules or stored customer properties are downloaded.

When product collection is disabled or no capture key is configured, declare the current application evaluation subject independently:

try Anectico.flags?.setEvaluationBinding(FlagEvaluationBinding(
    distinctID: "customer-b",
    personProperties: ["plan": .string("basic")],
    groups: ["workspace": "example"]
))

Without that binding, evaluation returns identity_unavailable and sends nothing. Setting or clearing it does not identify a telemetry user or change the session. Withdrawal invalidates old implicit decisions. Renewing consent does not perform a previously refused identify or reset. Exposure collection additionally requires a matching admitted telemetry subject. Status reads emit no exposure; served value and payload reads use the existing durable product queue when allowed. Evaluation, local admission and flush are separate from stored evidence.

Your application owns survey rendering. For an offered rating question named score:

if let surveys = Anectico.surveys,
   let offer = try await surveys.refresh(screen: "Checkout").first {
    // Render offer.questions in your UI, then record the actual display.
    _ = try await offer.shown()
    let receipt = try await offer.submit(answers: [SurveyAnswer(questionID: "score", rating: 0)])
    // receipt.at and receipt.replayed are validated server acknowledgement fields.
}

Declare static screen templates such as /checkout or /mobile/checkout. Pass only their screen names, never raw URLs or route arguments. Eligibility coalesces with a five-second minimum gap and returns at most three offers for the current screen, identity and time. A valid empty list is distinct from failure and incomplete identity coverage. Single choice, multiple choice, integer rating and open text are supported; all required questions must be answered.

The first delivery fixes its ID, offered revision, answers, environment and source session. After a retryable failure, repeat that operation to retry the original body; changed arguments are ignored. ApplicationFailure preserves the server code, HTTP status and retry advice. Permanent refusal closes the handle. Identity changes, reset, withdrawal and shutdown cancel incompatible work and clear copied answers. Answers are not persisted in a separate queue. No survey outcome events are emitted by the SDK. A receipt precedes eventual query visibility; multiple selected options can produce several answer rows for one response.

Read the complete API for typed models, bounds and bootstrap. Verify exact versions, original exposure evidence, survey receipts, stored answers and the joined customer timeline independently before claiming delivery. The current flag API exposes no separate per-exposure public receipt.

Verify this source and deployment

Follow mobile evidence verification for exact flag versions, originating message and source IDs, aliases and per-device sessions. Decisions, exposure admission, survey receipts and stored evidence are separate checks. The native flag API has no public per-exposure receipt; original-attempt attribution remains unqualified. An ID discovered only after storage and aggregate flush counts cannot replace it.

The client preserves optional survey source sessions and recognizes SURVEY_IDENTITY_COVERAGE_UNSUPPORTED; the server propagates source sessions and refuses incomplete aliases. Independently verify stored correlation on your deployment. Earlier web or REST runs do not qualify the changed client-delivery authority. Treat an empty eligible list, transport failure, typed refusal and permanent delivery rejection separately. Retention, content policy, erasure, export and local withdrawal still apply; withdrawal does not erase data already stored. Read existing person and result proof links where supplied; flag evaluation has no proof page.

Use explicit private ingestion and application HTTPS hosts and the approved private network path. Do not expose the API publicly or put operator credentials into a build. Pin source revisions; source coordinates, package artifacts and core test results are not proof of package publication, device installation or stored cross-device qualification. React Native and Flutter have typed native delivery bridges; linked operating-system qualification remains unverified.

Distinct decision and answer examples

Configure independent definitions before expecting enabled true, disabled false and checkout_variant equal to the string treatment for the current ID and context. They are example keys, not built-in definitions. A missing key is unavailable; a deliberately failed dependency has an evaluation-error default and no authoritative version. Explicit fallback use must stay observable rather than being mistaken for evaluated false.

let document = try await flags.reload(keys: ["enabled", "disabled", "checkout_variant"])
let enabled = flags.boolean(for: "enabled", fallback: false)
let disabled = flags.boolean(for: "disabled", fallback: true)
let variant = flags.value(for: "checkout_variant", testCaseID: "mobile-variant")
// Inspect typed reasons, fallback use and decision metadata before choosing behavior.

For an actual offered survey whose questions are plan (single choice with option pro), features (multiple choice with options speed and support), score (rating 0–10) and comment (open text), these are the four answer shapes. Read the offered questions and use what the visitor actually selected; do not submit these constants automatically:

let answers = [
    SurveyAnswer(questionID: "plan", optionIDs: ["pro"]),
    SurveyAnswer(questionID: "features", optionIDs: ["speed", "support"]),
    SurveyAnswer(questionID: "score", rating: 0),
    SurveyAnswer(questionID: "comment", text: "Checkout was clear")
]
// In the actual display callback: _ = try await offer.shown()
// In the explicit user-submit callback:
let receipt = try await offer.submit(answers: answers)
// Alternative user-dismiss callback, not after submitting: try await offer.dismiss()

A rating of zero is present. This one response generates five answer event rows: one single choice, two selected options, one rating and one text. It remains one accepted response. The application owns rendering and callbacks. Do not record shown merely because refresh returned an offer, or dismiss a survey already submitted. Retry the same uncertain operation on the same handle with its original identity, optional session, revision, answers and ID.

Native structural replay

The optional iOS structural recorder is default off and separately consented. Its implementation requires a deployment serving the approved native capture policy, native store permission and a separate project-bound REPLAY_UPLOAD key carrying only replay:write. Without those controls it refuses recording. Native source and device qualification remain required before a support claim; this is not an every-screen or performance guarantee.

let replayOptions = NativeReplayOptions(
    enabled: true,
    endpoint: "https://console.example.com",
    projectID: "22222222-2222-4222-8222-222222222222",
    uploadApiKey: "an_<separately-issued-replay-upload-key>",
    privacy: NativeReplayPrivacy(declaredLabels: ["checkout.continue": "Continue"])
)
try Anectico.configure(AnecticoOptions(
    apiKey: "an_<native-ingest-key>",
    captureApiKey: "an_<mobile-capture-key>",
    endpoint: "https://intake.example.com",
    nativeReplay: replayOptions
))
if let replay = Anectico.replay {
    try replay.bindReplayOwner(currentCustomerID)
    replay.grantReplayConsent() // only after the application's independent replay consent
}

Use explicit current application ownership even when analytics is disabled. Binding creates no analytics identify event and does not authenticate the person holding the device. Owner changes close the previous recording and require renewed consent. Reset clears the replay binding and consent even if analytics reset is refused. Analytics withdrawal alone does not change replay consent. Call withdrawReplayConsent() to stop replay and purge unsent local content; this does not erase already stored content. Ordinary configuration never renews withdrawn consent.

For UIKit, retain a NativeReplayUIKit(recorder: replay) source, declare static keys with declare(view, key: "checkout", privacy: .masked) or .blocked, and call capture(root: contentView, screen: "checkout") { result in ... } after layout. Supply your visible content root inside the safe area. Connect app-owned handlers to tap, inputChanged, editingEnded and scrolled. UIKit frame and action hooks encode and persist on the replay worker; their completion may run off the main thread. There is no automatic UI observer. Screen names and optional action declarations are static templates, never route arguments, customer data or visible UI text. Remote label eligibility must also allow a declaration.

Text, secure and ordinary inputs, images and survey answers have no captured values. A mask propagates to descendants; a block prevents descendant inspection. Unqualified custom views, embedded web and SwiftUI hosting regions become visible unsupported gaps. Rotate and background the app in source tests; backgrounding pauses, invalidates old capture tokens and requires a fresh frame on resume. Read the source matrix and control reference and replay privacy guide.