Skip to content
Console
Browse documentation
Reference

iOS SDK API

Functions, options, parameters, return values, and propagation helpers for the Anectico Swift SDK.

On this page

This is the public Swift API for the native Anectico iOS SDK. It supports iOS 15 and later. configure throws AnecticoConfigurationError for invalid options; telemetry calls made before configuration remain safe no-ops with a log warning. Event and error network work runs off the calling thread.

Use separate project-bound native_ingest and mobile_capture keys. Optional native structural recording has independent controls; see native replay controls.

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 and 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.

Configure the SDK

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"
    )
)
API Parameters Returns Behavior
Anectico.configure(options) AnecticoOptions throws; Void Validates options, then configures identity, sessions, event queue, error transport, crash handlers, lifecycle hooks, and timers. If the app is already active, emits its missed $app_opened exactly once. Repeated calls are ignored; calls while shutdown is still draining throw. Ordinary configuration preserves analytics withdrawal across restart.
Anectico.configureApplication(options) ApplicationOptions? throws; Void Replaces optional flag and survey clients without changing telemetry identity, session or consent. Pass nil to clear both. Old decisions, offers and pending operations are retired; invalid options preserve the current clients. Requires initial SDK configuration.
Anectico.flush(completion:) callback taking TelemetryFlushResult Void Starts an asynchronous diagnostic-event flush and hands the completion the per-signal delivery outcomes. See Knowing whether your events arrived.
Anectico.shutdown(completion:) callback taking TelemetryFlushResult Void Stops timers and hooks, flushes queued events, releases state, and permits reconfiguration after its completion fires. The completion carries the final flush's outcome, like flush.
Anectico.disableAnalytics() — Bool Stops analytics admission and retries and persists withdrawal. true confirms the withdrawal marker and queue purge; see below for failure handling.
Anectico.configure(options, resumeAnalyticsCollection: true) AnecticoOptions; explicit renewed consent throws; Void After shutdown completion, purges withdrawn events before clearing the marker and creating a fresh client.
Anectico.stats() — AnalyticsStats? Delivery counters — recorded, delivered, dropped, queued — or nil before configure.
Anectico.onDeliveryError(_:) handler taking DeliveryError, or nil Void Registers a handler called once per permanently discarded batch with status, dropped, and reason. Safe to call before configure.

Knowing whether your events arrived

Anectico.flush and Anectico.shutdown return a per-signal TelemetryFlushResult through the native completion or bridge future. Read productEvents, logs and traces: each has accepted, rejected, pending, unknown, sampled, warnings, status and nullable unavailableReason. Logs are null on iOS, which has no public log producer. Traces include errors, messages, crashes and agent-run spans. Pending is the queue and spool snapshot; the other counts describe outcomes since the previous completed public flush or shutdown. An empty accepted queue with count zero proves no delivery. Read every count: pending work can coexist with a refused sibling.

Local queue eviction and readable expired or excess error payloads count as rejected records. Pending counts readable records rather than spool files. Corrupt payloads and failed directory listing report unknown with an unavailable reason; zero pending then does not prove an empty queue. A durable pre-send claim excludes each batch from restart replay. Interrupted claims are quarantined as unknown; only a retryable receipt can restore retryable work. Failed claim persistence prevents the send, and failed terminal cleanup cannot replay a partially accepted or unknown batch.

A complete OTLP response preserves rejected counts and warnings, even when HTTP 200 refused every record. Partial batches are retired without replaying admitted siblings. An unreadable success receipt is reported as unknown. Accepted counts describe intake acceptance, not unique stored rows or query visibility. An unavailable native bridge reports unknown with a reason.

Error and message capture return ErrorCaptureResult with nullable errorId, outcome and reason. Outcomes are sampled, queued, rejected and unknown. Queued means local admission; a sampled or rejected attempt can still have an ID, and that ID proves no emitted occurrence. Use the trace result and then read the stored evidence before concluding delivery.

For product events, the cumulative Anectico.stats() counters and drop callbacks remain:

What happened Your events flush stats()?.queued drop callback
Permanently rejected — a bad request, or a key that is revoked, wrong, or missing the analytics:write scope discarded, never retried failure excludes refused positions fires once
Temporarily unavailable — no network, rate limited, or a server error still queued for the next flush failure includes them silent

Anectico.stats() reports recorded, delivered, dropped, and queued, and recorded always equals the other three added together. delivered counts positions acknowledged queued in a complete version-one capture ledger, not unique stored rows or immediate query visibility. queued includes in-flight events. A mixed receipt retires queued and explicitly refused positions while preserving uncertain original IDs, timestamps, identities and payloads ahead of unsent events. The durable spool retains those uncertain events for recovery after restart. Malformed, oversized (over 64 KiB), incomplete or contradictory receipts at any HTTP status retain the entire affected chunk. HTTP 200 alone cannot claim delivery, and an untyped HTTP 4xx cannot authorize a drop. Without a server hint, consecutive uncertain attempts back off from one second, doubling up to 60 seconds and resetting after a fully settled receipt; server Retry-After hints are capped at 60 seconds. The default HTTP attempt has a 15-second wait bound. OTLP error and crash delivery uses its separate transport policy.

Session and identity changes remain effective in memory when local persistence fails. Logging out rotates both identities even if storage cannot be cleared; a backward clock change starts a new session. Property serialization stops at depth 32 and emits [MaxDepth] for deeper values. HTTP redirects are rejected so event credentials are sent only to the configured endpoint.

Anectico.onDeliveryError(_:) fires once per discarded batch with the response HTTP status (including 200 for ledger refusals), how many events were lost, and a stable lower-case reason. Ledger refusals use invalid_event, quota_exceeded, or mixed_rejection when one chunk has multiple refusal causes. Typed request refusals use bad_request, unauthorized, forbidden, or payload_too_large. Local drops report status 0: queue_overflow (the queue filled up and the oldest events were evicted) and not_serializable (an event's properties could not be encoded). collection_disabled marks withdrawal of pending or uncertain positions. A temporary failure alone does not fire it. The handler runs outside the queue and bookkeeping locks, on the caller thread for local drops or the flush dispatcher for delivery outcomes.

Capturing does not wait for network delivery: capture, identify, group, and screen serialize and persist the queued event, then return, so the delivery outcome reaches you through the three signals above and never through their own return value.

Anectico.onDeliveryError { error in
    print("lost \(error.dropped) events: \(error.reason.rawValue) (HTTP \(error.status))")
}

Anectico.flush { result in
    print(result.productEvents.pending, result.traces.rejected, result.traces.unknown)
}

Withdraw analytics collection

Call Anectico.disableAnalytics() when analytics consent is withdrawn. It blocks subsequent capture, screen, identify, group and analytics reset operations, stops the periodic analytics timer, discards pending events, and records collection_disabled with status 0. SDK lifecycle events also stop being admitted. Identity and group state are not changed by refused analytics calls. The method performs synchronous local disk I/O but does not wait for network delivery, so a delivery error callback may invoke it safely. Use flush(completion:) outside that callback to observe final counters after an already-admitted request settles.

Already-admitted requests may finish. Validated queued or refused receipt positions keep their actual outcomes; uncertain positions and later unsent chunks are discarded without retry. A discarded uncertain event may already have reached the server. Withdrawal neither erases remote data nor stops the separate error, crash and OTLP producers.

The returned Bool confirms both the persisted withdrawal marker and deletion of the analytics spool. If it is false, collection still stops in this configured process, but disk cleanup or marker persistence failed: retry disableAnalytics() and do not assume restart safety until it returns true. Calling it before configuration returns false. Repeated calls retry the disk operation without reopening admissions or double-counting discarded events.

Ordinary configure(options) respects the durable marker after a restart. Only after obtaining new consent should the application wait for shutdown completion, then call try Anectico.configure(options, resumeAnalyticsCollection: true). Renewal removes the old spool even if the marker is absent before clearing the marker; filesystem failure throws and leaves the SDK unconfigured. Never set that argument automatically on every launch. React Native and Flutter expose corresponding awaited disableAnalytics and explicit renewed-consent configuration controls; see their SDK references.

AnecticoOptions

Only apiKey is required. Invalid endpoints, timers, capacities, frame limits, and non-finite or out-of-range sampling values throw AnecticoConfigurationError rather than being clamped.

Option Type and default Purpose
apiKey String; required native_ingest key for OTLP.
captureApiKey optional Separate mobile_capture key for product events, identify and group.
endpoint String; https://api.anectico.com Base URL for /v1/traces and /api/v1/capture.
environment String; development deployment.environment resource value.
release String?; service version when real Regression and symbol-artifact identifier.
dist String; empty Build and artifact discriminator.
serviceName String?; bundle ID service.name.
serviceVersion String?; bundle marketing version service.version.
flushAt Int; 20 Event-count flush threshold.
flushIntervalMs Int; 5000 Foreground-only periodic event flush; zero disables the timer.
maxStackTraceFrames Int; 50 Maximum frames per error.
breadcrumbCapacity Int; 100 Ring-buffer capacity.
maxQueueSize Int; 10000 Diagnostic-event queue cap; oldest entries drop at the cap.
errorSampleRate Double; 1 Non-fatal error sampling in the inclusive range 0–1.
enableCrashReporting Bool; true Install uncaught NSException and fatal-signal handlers.
debug Bool; false Emit SDK diagnostics through Apple logging.

Identity, accounts, and events

Method and property Parameters Returns Behavior
Anectico.identify(distinctId, set:) stable ID; optional person properties Void Links the anonymous ID to a known person and queues identify.
Anectico.group(type:key:set:) account type and key; optional properties Void Records membership and queues $groupidentify.
Anectico.capture(event, properties:) event name; properties Void Queues a diagnostic event with current person, session, and groups.
Anectico.screen(name, properties:) screen name; properties Void Queues $screen with $screen_name.
Anectico.reset() — Void Logout: creates a new anonymous ID, clears groups, rotates the session, and clears global error-user context and breadcrumbs.
Anectico.distinctId property String? Current known or anonymous ID, or nil before configuration.
Anectico.sessionId property String? Current durable session ID without extending its idle window.

identify and group update local identity only after their control event passes queue admission. An event that cannot be serialized or whose UTF-8 request envelope exceeds 1 MiB is dropped with status 0 and reason not_serializable or payload_too_large. The previous person ID and account memberships remain in use for subsequent telemetry. Valid controls are queued before events use the new identity; the call does not wait for network delivery.

capture and screen inherit accounts set with group. An explicit properties["$groups"] replaces the entire account map for that event; omitted account types are not inherited. An empty dictionary suppresses account attribution for that event. The override changes neither the cached accounts nor your property dictionaries, so the next event without an override still inherits the cached accounts.

Errors, messages, users, and breadcrumbs

Method Parameters Returns Behavior
Anectico.captureError(error, options:) Swift Error; optional CaptureOptions ErrorCaptureResult Captures a caught error and returns its ID, or "" before configuration.
Anectico.captureError(exception, options:) NSException; optional options ErrorCaptureResult Captures an Objective-C exception without changing app behavior.
Anectico.captureMessage(message, level:options:) message; level default info; options ErrorCaptureResult Captures a diagnostic message and returns its ID.
Anectico.addBreadcrumb(category:message:level:data:) strings; level default info; data Void Adds trail context attached to a later error.
Anectico.setUser(user) AnecticoUser Void Sets global error user context. Cross-signal identity still comes from identify.
Anectico.clearUser() — Void Clears global error user context.
Anectico.captureRawError(...) type, message, frame JSON, fatal, mechanism, tags ErrorCaptureResult Bridge API for React Native and Flutter pre-parsed stacks. Native Swift apps should use captureError.

CaptureOptions fields are tags: [String:String], extra: [String:Any], optional user, optional level, and optional fingerprint: [String]. Fingerprint components are comma-joined; do not put commas inside components. AnecticoUser supports id, email, username, ipAddress, and segment.

Fatal native crashes are persisted and delivered on the next application launch. Keep the exact archive dSYM and upload the bundle or its DWARF binary with anectico symbols upload-dsym MyApp.dSYM. Anectico matches the project's dSYM slices by exact Mach-O image UUID; release and distribution are not dSYM lookup keys.

For native symbolication, each frame carries its image UUID and load address. The complete loaded image set stays in the private crash sidecar; only a bounded table of frame-referenced images rides the recovered occurrence, so large simulator processes cannot exceed the ingest attribute cap.

Every iOS error occurrence automatically carries severity, handled state, an honest capture mechanism, release and distribution, SDK and platform, hardware model code or family, OS version, and app version and build. Caught Swift and explicitly captured Objective-C errors are handled and nonfatal; uncaught Objective-C exceptions and signals are unhandled and fatal. Signal crashes use the crash-time snapshot after relaunch.

These SDK-owned fields take precedence over colliding CaptureOptions.tags. The SDK records a model code such as iPhone15,4, not a unique phone: it never collects IDFV, the user-assigned device name, a serial number, or an installation ID.

The SDK-owned $app_opened lifecycle event carries the same privacy-safe release, app and build, platform, OS, and device-family context. It is emitted once per real foreground transition, while duplicate notifications in one active period are ignored. Backgrounding emits one $app_backgrounded, pauses recurring flushes, and performs one explicit flush; foregrounding recreates exactly one timer without rotating the session by itself. Anectico uses its versioned lifecycle marker to count canonical-person release adoption and the adopted-person error-free rate; host properties cannot override the marker context.

The reserved $release/$app_version string properties are stamped on every captured event (not only $app_opened), sourced from the same release/serviceVersion configure() resolved. Neither is invented: an app that never sets release and has no bundle version to fall back to sends events with neither property. See Reserved event properties.

Agent runs

Anectico.startAgentRun(_ agent: String, options: AgentRunOptions = AgentRunOptions()) -> AgentRun starts a local run handle. It snapshots the start time, identity, and session.

let run = Anectico.startAgentRun(
    "checkout-helper",
    options: AgentRunOptions(agentId: "checkout", agentVersion: "1.4.0", conversationId: "chat-42")
)
// Perform the agent's work, then declare its actual outcome.
let accepted = run.end(.completed)
// On failure instead: run.end(.failed, terminalReason: "tool_failed")

agentId, agentVersion, and conversationId are optional strings mapping to gen_ai.agent.id, gen_ai.agent.version, and gen_ai.conversation.id. The agent name maps to gen_ai.agent.name. Configure and identify the customer before starting a run. A terminal record is an invoke_agent span sent with ingest:write; it does not require an agent-read or agent-write management scope. It is not automatic instrumentation of nested model and tool calls.

end(_ status: AgentRunTerminalStatus = .completed, terminalReason: String? = nil) -> Bool accepts .completed, .failed, .timedOut, .cancelled, or .maxSteps. The multiword wire values are timed_out and max_steps.

Use a stable, non-sensitive reason code, not an exception message, prompt, or customer value. Reasons are trimmed, lowercased, and must match ^[a-z][a-z0-9_]{0,63}$. Omitted reasons default to timeout for a timeout, user_cancelled for cancellation, and the wire status for other outcomes. These are source-declared outcomes; Anectico does not infer success from the absence of an error.

An invalid reason returns false and leaves the handle open. The first accepted outcome wins; later valid calls return true without emitting another span. Acceptance is a local handoff, not confirmation of network delivery. The event-queue flush result does not certify this separate span transport.

recordAgentRun(agent:options:status:terminalReason:startTimeUnixNano:) is the native bridge entry point used by React Native and Flutter. Swift application code should use the typed startAgentRun handle.

Backend propagation

API Parameters Returns Behavior
Anectico.propagationHeaders() — [String:String] Creates a fresh traceparent and identity baggage when configured.
AnecticoPropagation.apply(to:&request) inout URLRequest Void Applies propagation while preserving unrelated baggage.
AnecticoPropagation.newTraceparent() — String Creates a sampled W3C traceparent.
AnecticoPropagation.baggageWithDistinctId(id, existing:) ID; existing baggage String Replaces the Anectico member and preserves unrelated baggage.
AnecticoPropagation.percentEncode(value) string String Encodes a W3C baggage value.

Apply identity baggage only to application backends you trust. A backend must authenticate the mobile request rather than treating the baggage value itself as proof of identity.

Remote flags

Anectico.flags is FlagsClient?. It exists only with applicationEndpoint, canonical projectID and flagDeliveryApiKey. Anectico.surveys independently requires surveyDeliveryApiKey. surveyScreenPaths declares screen templates. applicationTransport optionally injects an ApplicationTransport; it receives immutable ApplicationRequest values and a cancellation token. The default transport bounds the complete attempt to ten seconds and the complete response to 1 MiB, refuses redirects and rejects truncated or invalid JSON. It shares no delivery credentials or work queue with product and error collection.

These are source APIs awaiting iOS device and simulator qualification. Linux core tests establish protocol behavior, not an Apple platform build or stored customer evidence.

Swift API Result and meaning
flags.reload(keys:) async throws FlagDecisionDocument: projectID, evaluatedAt, maxAgeSeconds, decisionContextID, decisions, complete, requestedKeyCount and expiresAt.
flags.value(for:testCaseID:) FlagRead with status, optional bool or string value, payload, decision metadata, expiry and failure. Admits an allowed served exposure.
flags.payload(for:testCaseID:) The same typed read, preserving absent payload and ApplicationJSON.null.
flags.status(for:) The same state without exposure admission.
flags.boolean(for:fallback:testCaseID:) FlagBooleanRead: value, reason, usedApplicationFallback, expiresAt and the single underlying read. Fallback is required; a string value yields type_mismatch.
flags.setEvaluationBinding(_:) Replaces the current FlagEvaluationBinding independently of telemetry identity.
flags.clearEvaluationBinding() Clears the explicit binding and retires its decisions and work.
flags.evaluationSnapshot() Binding, evaluation generation, telemetry generation, configuration generation and whether the binding is explicit.
flags.installBootstrap(_:) throws Installs only a matching original request snapshot and valid complete document, preserving the original expiry.

Decision documents and flag reads carry expiresAtTimestamp, preserving the exact fractional expiry text alongside expiresAt. Input context preserves optional inputBinding metadata. To construct FlagBootstrap from original wire documents, pass projectID, the native request snapshot, requestJSON, document and optional evaluationGeneration; a different binding or generation is refused before installation.

Runtime application configuration keeps the existing telemetry recipient. Its first declared project stays bound to that recipient, including after clearing capabilities. Flags configured for another project can return decisions but admit no exposure through the original recipient. To collect exposure evidence in a different project, shut down and configure the SDK with that project's matching intake and delivery credentials.

FlagValue is .bool(Bool) or .string(String). ApplicationJSON supports null, bool, finite number, string, array and object. FlagDecision retains state, value, payload, definitionVersion, isFallback and inputContext. Evaluated definition versions remain their original UTC strings with six fractional digits. Missing required-input metadata remains unknown. Invalid or missing authoritative versions and fallback metadata cannot become evaluated decisions.

Decision states are evaluated, evaluation_error and unavailable_flag. FlagRead.status also distinguishes unknown, expired, cancelled, refused, unconfigured, identity_unavailable and transport_error. A safely returned server error fallback retains evaluation_error and isFallback; it differs from your boolean convenience read's usedApplicationFallback. No unavailable value is silently replaced with false. Optional testCaseID is application correlation, not proof.

Reload accepts 1–100 unique keys matching [A-Za-z0-9_-]{1,200}, ten groups and a 16 KiB JSON targeting envelope. Properties are limited to depth 16, 250 entries per object or array, 512-byte keys and 4,096 total values. The complete request is capped at 128 KiB. Responses must contain exactly the requested keys, correct project and coverage, UTC evaluation time and a 30-second max age. Concurrent identical reloads coalesce; a different request returns busy. Retry advice sets a bounded retry floor. There is no background polling or persistent decision cache.

FlagBootstrap(projectID:request:keys:document:) takes the original FlagEvaluationSnapshot, requested keys and complete JSON decision bytes. Bootstrap rejects a changed project, exact identity, properties, groups, key set or generation. Installation never restarts its lifetime.

When product collection is unavailable, supply a current explicit binding or evaluation sends nothing and returns identity_unavailable. An explicit binding emits no telemetry identity event and does not change the telemetry session. Exposures require consent and the matching admitted telemetry subject. Deduplication includes identity, key, typed value and exact definition version. Uncertain product retries retain original IDs, timestamps, sessions, properties and batch bytes, including across restart. A changed capture endpoint, key or explicit project discards pending product events with a warning; flush successfully before changing them. Recipient pins contain no raw credentials. Consent withdrawal remains in force across those changes.

Headless surveys

Swift API Result and meaning
surveys.setScreenMapping(_:) throws Declares static screen names and slash-prefixed templates; retires old screen work.
surveys.refresh(screen:) async throws At most three SurveyOffer values; explicit refreshes and screen changes coalesce with a five-second minimum gap.
offer.survey, id, revision, questions, isClosed Immutable offered definition and current handle availability. Revision is Int64.
offer.shown(id:) async throws Validated SurveyReceipt; call only after actual display.
offer.submit(answers:responseID:) async throws Validated receipt for copied SurveyAnswer values. Rating zero is present.
offer.dismiss(id:) async throws Validated dismissal receipt.

SurveyReceipt contains the original at timestamp string and replayed boolean. Malformed success documents are uncertain, never submitted. ApplicationFailure retains kind, code, status, retryable and retryAfterMs. SURVEY_IDENTITY_COVERAGE_UNSUPPORTED is distinct from a valid empty result. Permanent refusal closes the handle; retryable failure retains only its immutable in-memory operation. Repeating the operation resends its first body, including its original session, answers and ID, regardless of new arguments.

SurveyAnswer(questionID:rating:optionIDs:text:) uses the field for that question's type. There are 1–10 questions, 2–12 distinct choice options, stable lowercase IDs of at most 32 characters, prompts up to 500 characters and labels up to 200. Rating bounds start at 0 or 1 and end at no more than 10. Open text is normalized and accepts 1–2,000 characters. Unknown questions, duplicate answers, unknown or duplicate options, missing required answers and fractional ratings are refused. Delivery IDs are 1–128 printable ASCII bytes without spaces. Environment and source session are at most 200 UTF-8 bytes; session identifiers follow capture's identifier rules.

Surveys require product consent and available product identity. Withdrawal, admitted identity or group changes, reset, shutdown and screen replacement close incompatible handles and clear copied answers. No open-text spool or client survey-outcome event is created. A receipt is delivery acknowledgement; verify stored answers and the joined customer timeline separately. Multiple choice rows are not response totals. The flag API adds no separate public exposure 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 replay controls

AnecticoOptions.nativeReplay defaults to absent. NativeReplayOptions.enabled defaults to false. An enabled controller needs explicit replay HTTPS origin, project ID, separately issued replay-upload key, the existing mobile capture key and a current native policy with native store permission. Endpoint or key syntax is not proof of their server-side authority. See setup.

API Behavior
Anectico.replay Optional owned controller, independent of flags and analytics consent
bindReplayOwner(_:) Explicit exact UTF-8 ID, 1–200 bytes; never inferred from a previous identity
grantReplayConsent() Explicit local replay consent; not renewed by configuration
withdrawReplayConsent() Cancels work and purges unsent local content; returns local purge success
start() disabled, recording, paused or refused, plus reason, format and version and capture revision
diagnostics() Pending chunks, envelope bytes, bounded loss counts and accepted-with-retention-unknown count
selectReplaySource(_:adapter:) One hierarchy or external source per recording; changing closes and rotates
acquireReplayCaptureContext() One opaque current token, at most five seconds and never beyond policy expiry
submitReplayStructure(token:sourceSequence:events:nodeKeys:buffered:) Synchronous atomic admission; buffered defaults to false. Buffered submission can also return unchanged; native assigns ordinals
endInputEpisode(token:frameID:nodeID:) External focus-loss hook, bound to the current token and input target; records no value or event
reconfigure(_:) Retires pending content and old contexts before new endpoint, key, project or privacy configuration
flush() Attempts one original chunk; a matching receipt returns acceptedRetentionUnknown
shutdown() Stops extraction, attempts one bounded final upload, then purges unsent content

External candidates are sanitized NR1 JSON without ordinals, at most 16 events, one frame and 65,536 bytes. Sequence starts at one and advances only for an admitted batch. Native read-only context facts recordingID, elapsedMs and nextSourceSequence let an adapter reset its source namespace after rotation and continue it after token renewal; these facts confer no upload authority. Renewal never resets sequence or frame references. Optional node declarations match privacy rules but are not recorded. A stale token cannot authorize a new owner or source. Selecting an external source disables UIKit traversal; it does not qualify a framework adapter.

UIKit source classes Observed structure
UIView and UIStackView Bounded containers
UILabel Masked text box, no text
UIButton Button box and explicitly declared eligible action label only
UITextField and UITextView Masked input placeholder; no value, length or per-keystroke count
UISwitch Masked toggle, no checked state
UIImageView Masked box, no asset or URL
UIScrollView Masked scroll box with explicit bounded offsets
Custom subclasses, SwiftUI hosts, embedded web and custom drawing Unsupported placeholder and gap

The source uses exact supported classes and explicit app handlers. Frame and action hooks observe after the native consent gate and encode on the replay worker; read their asynchronous completion. Masks dominate labels and propagate to descendants. A changed declaration immediately retires cached targets and pending content. Blocks and unsupported regions stop child inspection. Input-change hooks coalesce an editing episode until focus loss, screen change or pause. No accessibility text, pixels, executable content, console or network bodies enter this format. Optional labels are bounded, redacted application declarations and remain untrusted evidence.

Hard safety ceilings include two frames and 20 events per rolling second, 256 nodes per frame and 32 levels, 262,144 bytes per chunk, eight local chunks and 2,097,152 envelope bytes, 60-second local age and ten-minute recordings. Local and remote ceilings only narrow these maxima. They are not measured device rates or allowances. A full frame drops as a whole and declares loss; incomplete hierarchies are never presented as complete screens. Disk faults refuse capture.

Retries preserve original bytes, recipient, owner, session, format and index. All receipt bindings, including the SHA-256 of the exact submitted event array, must match before a local item is released. Malformed or same-sized different-body receipts remain uncertain. Native policy is refreshed at most 15 seconds apart and expires within 60 seconds; a changed projection retires old content. Restarts require fresh explicit owner, consent and policy before any compatible original retry. Withdrawal, reset, owner changes, policy retirement and ordinary shutdown purge unsent content. Stop is not server erasure, and an accepted upload does not establish retained content or completed playback.

Replay capture and durability

A UIKit worker capture can return unchanged when the observed scene matches the latest admitted frame. It adds no frame, event or upload and leaves action targets on that exact frame. Screen or viewport changes, a coverage gap, pause or a change of owner, consent or policy requires a fresh frame. Unsupported regions keep their gap markers.

Buffered capture stores sanitized events in one bounded open chunk. It becomes durable at eight frames, 64 events or two seconds after its first event, and when you flush, background or shut down. Withdrawal and every other privacy purge discard open events too. A crash can lose that open interval; recovery reports a gap with unknown counts. Synchronous forced snapshots keep their immediate durability. An admitted result confirms local acceptance; an upload acknowledgement still does not establish retention or playback availability.