Android SDK API
Functions, options, parameters, return values, and propagation helpers for the Anectico Kotlin SDK.
On this page
This is the public Kotlin API for the native Anectico Android SDK (minSdk 24). init reports
failure rather than absorbing it: IllegalArgumentException for invalid configuration, and
AnecticoInitException (with the original cause attached) if anything else stops the SDK from being
set up. Telemetry calls made before a successful init stay no-ops that return "", but they are
never silent — see Calling before init. Delivery failures after
initialization are still never thrown into your app. Network I/O runs on background executors;
durable queue writes complete before a capture call returns.
Use separate project-bound native_ingest and mobile_capture keys. The native SDK does not record replay.
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 also refuses telemetry identity and session changes while the capture key is absent.
See native intake keys for creation and verification.
Initialize and stop
Anectico.init(
applicationContext,
AnecticoOptions(
apiKey = BuildConfig.ANECTICO_API_KEY,
captureApiKey = BuildConfig.ANECTICO_CAPTURE_API_KEY,
environment = "production",
),
)
| API | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.init(context, options) |
Android Context; AnecticoOptions |
Unit |
Validates options, then configures identity, sessions, durable event and log queues, error transport, crash and lifecycle handlers, and timers. Repeated calls are ignored. Invalid options throw IllegalArgumentException; any other setup failure throws AnecticoInitException with the cause. A failed init leaves the SDK uninitialized and a later init may retry. |
Anectico.flush(completion?) |
callback taking TelemetryFlushResult |
Unit |
Drains product events, logs and trace and error work, then returns each signal's outcomes. See Knowing whether your events arrived. |
Anectico.shutdown(completion?) |
callback taking TelemetryFlushResult |
Unit |
Stops workers and hooks, flushes, releases state, and permits a later init after completion fires. The completion carries the final flush's outcome, like flush. |
Anectico.disableAnalytics() |
— | Boolean |
Persist analytics withdrawal and purge queued data; false requires retry before restart safety can be assumed. |
Anectico.init(context, options, resumeAnalyticsCollection = true) |
Explicit new consent after shutdown completion | Unit |
Purges old queue data before reopening collection; failure throws AnecticoInitException. |
Anectico.stats() |
— | AnalyticsStats? |
Delivery counters — recorded, delivered, dropped, queued — or null before init. |
Anectico.onDeliveryError(handler?) |
handler taking DeliveryError, or null |
Unit |
Registers a handler called once per permanently discarded batch with status, dropped, and reason. Safe to call before init. |
Withdrawing analytics consent
Call Anectico.disableAnalytics() when analytics consent is withdrawn. The native SDK closes analytics admission,
including capture, identify, group, reset, and lifecycle events, and discards pending events without
flushing them. Errors, crashes, agent traces, and OTLP logs have separate controls and continue.
An already-admitted request can still reach the server: validated receipt outcomes remain credited;
uncertain retries and unsent remainder are discarded with collection_disabled and status 0.
This does not erase previously accepted data. recorded = delivered + dropped + queued remains true.
A true result confirms both the persistent withdrawal marker and deletion of the analytics spool.
On false, this configured client stays disabled, but persistence and purge needs retry; do not assume
restart safety until a retry succeeds. The call waits for local disk work, but does not wait for an active network request. An uninitialized SDK returns false.
Ordinary configuration preserves the marker across restart. Only after obtaining new consent,
wait for the Anectico.shutdown completion, then call Anectico.init(context, options, resumeAnalyticsCollection = true).
Explicit renewal purges all old analytics queue data even when the marker is absent; a purge failure
fails configuration. Repeated configuration of an already configured SDK does not renew consent.
Wait for shutdown completion before reconfiguring; configuration during retirement fails.
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.
Mixed receipts retire queued and refused positions while preserving uncertain original IDs, timestamps,
identities and payloads ahead of unsent events. The durable spool retains uncertain events for restart
recovery. Malformed, oversized (over 64 KiB), incomplete or contradictory receipts at any HTTP status
retain the affected chunk. HTTP 200 alone cannot claim delivery; 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. Retry-After hints are clamped to 1–60 seconds.
The HTTP transport uses a 10-second connection timeout and a 15-second socket read timeout.
The complete capture body must arrive within a 15-second body budget; an outstanding socket read
can delay cancellation by up to one read timeout. This is not a total connection, header, and write deadline.
OTLP error, log and crash delivery keeps 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 for multiple refusal causes in one chunk. Typed request refusals use bad_request,
unauthorized, forbidden, or payload_too_large. Two causes never reach the network and 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). A temporary failure alone never fires it; explicit withdrawal discards uncertain retries with collection_disabled. A handler that throws is swallowed and never reaches your capture or flush call.
Capturing stays non-blocking: capture, identify, group, and screen queue the event and return
immediately, so the delivery outcome reaches you through the three signals above and never through
their own return value.
Anectico.onDeliveryError { error ->
Log.w("app", "lost ${error.dropped} events: ${error.reason} (HTTP ${error.status})")
}
Anectico.flush { result -> println(result.toMap()) }
Calling before init
A telemetry call made while the SDK is not initialized records nothing and sends nothing. It reports itself rather than passing for a success:
- Debug builds —
AnecticoOptions.debug, or an application manifest marked debuggable — throwAnecticoNotInitializedExceptionat the call site, so the mistake surfaces where it was made. - Release builds keep the documented return (
""for the id-returning methods, no effect for the rest) and log the first such call at ERROR naming the method; later ones log at WARN, so a call in a loop cannot bury the first.
getDistinctId() and getSessionId() are exempt and simply answer null, which is already an
unambiguous result you can branch on; propagationHeaders() is built on that answer and returns
traceparent alone. shutdown(completion?) is exempt too — it is cleanup, safe to call whether or
not init ever succeeded, and its completion always runs.
AnecticoOptions
Only apiKey is required. Invalid endpoints, timers, capacities, frame limits, and non-finite or
out-of-range sampling values are rejected rather than 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, /v1/logs, and /api/v1/capture. |
environment |
String; development |
Deployment environment resource value. |
release |
String?; real service version |
Regression and symbol-artifact identifier. |
dist |
String; empty |
Build and artifact discriminator. |
serviceName |
String?; application ID |
service.name. |
serviceVersion |
String; app version name then 0.0.0 |
service.version. |
flushAt |
Int; 20 |
Independent event and log count flush threshold. |
flushIntervalMs |
Long; 5000 |
Foreground-only periodic event and log flush; zero disables it. |
maxStackTraceFrames |
Int; 50 |
Maximum frames per error. |
breadcrumbCapacity |
Int; 100 |
Ring-buffer capacity. |
maxQueueSize |
Int; 10000 |
Per-queue cap: up to this many durable events and independently this many durable logs. |
errorSampleRate |
Double; 1 |
Non-fatal error sampling from 0–1. |
enableCrashReporting |
Boolean; true |
Install the uncaught-exception handler. |
debug |
Boolean; false |
Emit SDK diagnostics through Logcat. |
Identity, accounts, and events
| Method | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.identify(distinctId, set?) |
stable ID; person properties | Unit |
Links anonymous and known identity and queues identify. |
Anectico.group(type, key, set?) |
account type and key; properties | Unit |
Records membership and $groupidentify. |
Anectico.capture(event, properties?) |
event; properties | Unit |
Queues a diagnostic event with current identity, session, and groups. |
Anectico.screen(name, properties?) |
screen; properties | Unit |
Queues $screen and $screen_name. |
Anectico.reset() |
— | Unit |
Logout: rotates anonymous identity or session and clears groups, global error-user context, and breadcrumbs. |
Anectico.getDistinctId() |
— | String? |
Current known or anonymous ID, or null before initialization. |
Anectico.getSessionId() |
— | String? |
Durable current 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 map suppresses account attribution for
that event. The override changes neither the cached accounts nor your property maps,
so the next event without an override still inherits the cached accounts.
Anectico.capture("checkout", mapOf("\$groups" to mapOf("company" to "account-B")))
Anectico.capture("ungrouped", mapOf("\$groups" to emptyMap<String, String>()))
Logs
| Method | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.captureLog(body, severity, attributes?) |
nonblank body; severity default info; attributes |
String |
Atomically queues an OTLP log and returns its stable 32-hex anectico.log.id, or "" before initialization, for invalid or oversized input, or if durable persistence fails. |
Severity accepts trace, debug, info, warn/warning, error, and fatal. Caller attribute
values are sanitized and stringified. Each occurrence snapshots its original time, identity,
non-activity session, groups, release and dist, app and build, device, service, and environment. Retry after
offline use or process recreation retains that snapshot and preserves oldest-first order.
The log spool is independently bounded to maxQueueSize records and 10 MiB, dropping oldest; one
record is capped at 1 MiB. It flushes at flushAt, periodically, on background, flush, shutdown,
and next initialization. Network failures, HTTP 429 and 5xx responses retry; 2xx acknowledges; other 4xx drops.
Android is currently the only native mobile SDK with this public log API; iOS, React Native and Flutter bridges
do not yet expose it.
The SDK-owned $app_opened lifecycle event carries the resolved release, app version and build,
platform, OS, and privacy-safe device family. One event is emitted per real foreground transition.
Backgrounding emits one $app_backgrounded, stops recurring flushes before one explicit event and log
flush, and the next foreground recreates exactly one timer without rotating the session by itself.
These properties cannot be overridden and contain no
installation and device identifier. Anectico uses the marker to count canonical-person release adoption and
the adopted-person error-free rate.
The reserved $release/$app_version string properties are stamped on every captured event
(not only $app_opened), sourced from the same release/serviceVersion AnecticoOptions
resolved. Neither is invented: an app that never sets release and has no versionName to fall
back to sends events with neither property. See
Reserved event properties.
Errors, messages, users, and breadcrumbs
| Method | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.captureError(throwable, options?) |
Throwable; optional CaptureOptions |
ErrorCaptureResult |
Captures a caught, handled error with mechanism java.caught_exception and returns its ID, or "" before init. |
Anectico.captureMessage(message, level, options?) |
message; level default info; options |
ErrorCaptureResult |
Captures a message and returns its ID. |
Anectico.addBreadcrumb(category, message, level, data?) |
fields; level default info |
Unit |
Adds context for a later error. |
Anectico.setUser(user) |
AnecticoUser |
Unit |
Sets global error user context. |
Anectico.clearUser() |
— | Unit |
Clears global error user context. |
Anectico.captureRawError(...) |
type, message, frame JSON, fatal, mechanism, tags | ErrorCaptureResult |
Bridge API for React Native and Flutter; native apps should use captureError. |
CaptureOptions fields are tags, extra, optional user, optional level, and optional
fingerprint. AnecticoUser fields are id, email, username, ipAddress, and segment.
Breadcrumb levels are debug, info, warning, and error.
Fatal crashes and failed manual error sends are spooled for the next launch. Upload the matching
R8 and ProGuard mapping.txt for every release and dist. The crash handler marks uncaught throwables as
unhandled fatal errors with mechanism java.uncaught_exception. Native level, handling, and
mechanism values are SDK-owned; caller tags cannot override them.
Each occurrence automatically includes release and distribution, Anectico SDK and platform, Android model and OS version, and app version and build. These fields are SDK-owned so Issue Story receives trustworthy device context. Anectico records the shared hardware model, not a physical-device identifier: Android ID, serial number, installation ID, and the user-assigned device name are never collected.
Agent runs
Anectico.startAgentRun(agent: String, options: AgentRunOptions = AgentRunOptions()): AgentRun
starts a local run handle and snapshots its start time, identity, and session.
import com.anectico.sdk.Anectico
import com.anectico.sdk.AgentRunOptions
import com.anectico.sdk.AgentRunTerminalStatus
val run = Anectico.startAgentRun(
"checkout-helper",
AgentRunOptions(agentId = "checkout", agentVersion = "1.4.0", conversationId = "chat-42"),
)
// Perform the agent's work, then declare its actual outcome.
val accepted = run.end(AgentRunTerminalStatus.COMPLETED)
// On failure instead: run.end(AgentRunTerminalStatus.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 = defaultReason(status)): Boolean
accepts COMPLETED, FAILED, TIMED_OUT, CANCELLED, or MAX_STEPS; their wire values are
completed, failed, timed_out, cancelled, 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 or rejected local scheduling attempt returns false and leaves the handle
open for retry. The first accepted outcome wins; later valid calls return true without a second
span. Acceptance is local scheduling, not confirmed network delivery.
recordAgentRun is the native bridge entry point for React Native and Flutter. Kotlin application
code should use the typed startAgentRun handle.
Backend propagation
| API | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.propagationHeaders() |
— | Map<String,String> |
Fresh traceparent plus identity baggage when available. |
AnecticoInterceptor() |
OkHttp interceptor | Interceptor |
Applies trace and identity headers to every request made by that OkHttp client. |
AnecticoPropagation.apply(connection) |
HttpURLConnection |
Unit |
Applies headers before connect(). |
AnecticoPropagation.headers(distinctId) |
optional ID | header map | Pure header builder. |
newTraceparent() |
— | String |
Creates a sampled W3C traceparent. |
baggageWithDistinctId(id, existing?) |
ID; baggage | String |
Replaces the Anectico member while preserving others. |
percentEncode(value) |
string | String |
Encodes a baggage value. |
Attach AnecticoInterceptor only to clients whose destinations you trust. Public-device baggage is
correlation context, not authenticated proof of the user.
Remote flags and headless surveys
Application reads use an explicit API endpoint and project. Provision two project-bound
CLIENT_DELIVERY keys: flags:evaluate for flags and surveys:respond for survey delivery.
They are separate from both intake keys. Your agent configures flags and surveys through its own
MCP and CLI credentials; never put those credentials in an application. Agent evaluation uses
flags:evaluate with mcp:read for MCP access. Delivery grants no definition, profile or answer reads.
Anectico.init(
applicationContext,
AnecticoOptions(
apiKey = BuildConfig.ANECTICO_INGEST_KEY,
captureApiKey = BuildConfig.ANECTICO_CAPTURE_KEY,
endpoint = BuildConfig.ANECTICO_INGEST_ENDPOINT,
applicationEndpoint = BuildConfig.ANECTICO_APPLICATION_ENDPOINT,
projectID = BuildConfig.ANECTICO_PROJECT_ID,
flagDeliveryApiKey = BuildConfig.ANECTICO_FLAG_DELIVERY_KEY,
surveyDeliveryApiKey = BuildConfig.ANECTICO_SURVEY_DELIVERY_KEY,
surveyScreenPaths = mapOf("Checkout" to "/checkout"),
),
)
Anectico.identify("user_8842")
val flags = requireNotNull(Anectico.flags)
flags.reload(listOf("new_checkout")).thenAccept { result ->
if (result.status == ApplicationStatus.ACCEPTED) {
val read = flags.value("new_checkout")
val enabled = (read.decision?.value as? FlagValue.BooleanValue)?.value
// Choose application behavior explicitly when enabled is absent.
}
}
The two nullable controllers are configured independently. No delivery key means no controller.
reload returns a CompletableFuture<FlagReloadResult>; never call get on the UI thread.
Network work runs on background workers. A completion may run on the caller or worker;
dispatch UI updates to your application's UI executor.
A successful document contains projectID, evaluatedAt, maxAgeSeconds, decisionContextID,
decisions and selected-key coverage. Each decision preserves its state, boolean or string value,
payload presence, exact definitionVersion, fallback flag and input-context metadata.
A present false value differs from absence; a present null payload differs from no payload.
flags.status(key) inspects without exposure; value(key, testCaseID?) and payload(key) are
served reads. flags.boolean(key, fallback = false) returns BooleanFlagResult with the value,
applicationFallbackUsed and the original typed read. Check read.status and read.decision.state:
evaluation_error and unavailable_flag are distinct from an evaluated off value.
Unknown, expired, malformed, cancelled and refused outcomes stay explicit.
Reload accepts 1–100 unique keys, ten groups, 16 KiB of targeting context, depth 16 and 250
entries per object or array. Decisions remain only in memory and expire at the original evaluation
instant plus 30 seconds. There is one active flag request and at most one replacement waiting;
identical work coalesces and further work returns BUSY. Retry advice prevents early network work;
explicit calls during backoff return THROTTLED. Binding replacement cancels the prior wait.
No local evaluation or polling occurs.
FlagBootstrap(projectID, requestJson, decisionJson, evaluationGeneration?) must include the
original requested keys, identity, properties and groups, match the current binding and project,
and remain before its original expiry. flags.installBootstrap(bootstrap) never renews that expiry.
Evaluation identity and consent
When collection is withdrawn or the capture key is absent, set the current application identity explicitly before requesting a decision:
flags.setEvaluationBinding("user_8842", personProperties = mapOf("plan" to "basic"))
val binding = flags.evaluationBinding() // immutable identity, properties, groups and generation
// To replace, call setEvaluationBinding again; to remove, call clearEvaluationBinding().
Without this binding the result is IDENTITY_UNAVAILABLE and no request is sent. Binding changes
emit no identity event and do not change telemetry identity or session. Targeting inputs are
application declarations. A shared customer identity does not imply equal assignments under
different anonymous IDs or contexts. Identify admission does not prove that identity joining is complete.
Served reads admit $feature_flag_called only with consent and the matching current telemetry
subject and context. Deduplication includes identity, key, value and exact definition version.
Retries retain the original event, session, time and destination. A decision or local admission
is not stored exposure proof; read back the original product evidence before drawing a conclusion.
Public attribution of individual exposure attempts is still being completed; aggregate flush counts
cannot identify a particular exposure.
Application-owned survey rendering
val surveys = requireNotNull(Anectico.surveys)
surveys.refresh("Checkout").thenAccept { result ->
if (result.status == ApplicationStatus.ACCEPTED) {
val offer = result.offers.firstOrNull()
// Render offer.survey.questions in the application's own UI.
// Only after actual display: offer?.shown()
// After an explicit user response to a rating question named "score":
// offer?.submit(listOf(SurveyAnswer("score", rating = 0)))
// Or: offer?.dismiss()
}
}
SurveyOffer exposes immutable survey.id, signed 64-bit survey.revision and typed questions.
shown(id?), submit(answers, id?) and dismiss(id?) return
CompletableFuture<SurveyDeliveryResult>. Accepted delivery contains SurveyReceipt(at, replayed).
A receipt is accepted only after validating the full timestamp and replay status; omitted false
means false. Supply all required answers. Question types are SINGLE_CHOICE, MULTIPLE_CHOICE,
RATING and OPEN_TEXT. SurveyAnswer(questionId, rating?, optionIds?, text?) preserves integer
zero; answer options must be distinct declared IDs. Text is normalized and limited to 2,000
characters. There are at most ten questions and twelve options per choice question.
Screen names must map to declared slash-prefixed templates of at most 512 characters, such as
/checkout or /mobile/checkout. Do not map raw URLs or route arguments. Screen changes and
explicit refreshes coalesce with a five-second minimum request gap. A result offers at most three
surveys for this identity, screen and time. An empty accepted list differs from transport failure,
an unavailable controller or SURVEY_IDENTITY_COVERAGE_UNSUPPORTED.
The first delivery freezes its ID, answers, offered revision, subject, optional source session, environment and destination. Retry the same method on the same handle after retry advice permits; replacement arguments cannot change an uncertain delivery. There is no automatic answer retry loop. Permanent refusals close the handle. Answers remain only in memory and are cleared on completion, identity change, withdrawal, shutdown or reconfiguration. The application must render again and obtain a new response after retirement. The SDK emits no duplicate survey outcome events. A receipt precedes eventual query visibility; multiple selected options are not separate responses.
Anectico.disableAnalytics() closes survey offers, purges answer and exposure memory and invalidates
implicit flag work. Restoring consent does not admit earlier refused identity changes.
Anectico.configureApplication(ApplicationOptions(...)) replaces application configuration;
passing null retires both controllers. This validates synchronously and leaves existing telemetry
destinations bound to their admitted events. Shutdown cancels application work. Logs and errors
retain their independent controls.
Time values
The SDK supports Android minSdk 24 without additional date and time configuration in the consuming
application. Public FlagDecisionDocument.expiresAtMs is a Long containing Unix epoch milliseconds;
retry delays use milliseconds. Timestamp strings in responses and framework bridge values retain
their RFC 3339 representation and fractional precision.
The source build and JVM protocol tests do not establish package publication, device behavior or stored cross-platform evidence. The Android archive remains an early-access installation.
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.
An isolated API 36 x86_64 Android emulator run separately builds and installs the linked apps, verifies native HTTPS delivery and reads the resulting person timeline. True, false and variant exposures, a changed version with the same true value, survey answers and dismissals preserve original sessions and join one person across the three apps. Physical devices, other runtime configurations, public original-attempt attribution and published-package installation remain separate qualification.
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.
flags.reload(listOf("enabled", "disabled", "checkout_variant")).thenAccept { result ->
val enabled = flags.boolean("enabled", fallback = false)
val disabled = flags.boolean("disabled", fallback = true)
val variant = flags.value("checkout_variant", testCaseID = "mobile-variant")
// Check result.status and typed read metadata; move UI work to your UI executor.
}
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:
val answers = listOf(
SurveyAnswer("plan", optionIds = listOf("pro")),
SurveyAnswer("features", optionIds = listOf("speed", "support")),
SurveyAnswer("score", rating = 0),
SurveyAnswer("comment", text = "Checkout was clear"),
)
// In the actual display callback: offer.shown()
// In the explicit user-submit callback:
val delivery = offer.submit(answers)
// Alternative user-dismiss callback, not after submitting: 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: NativeReplayOptions? is omitted by default. Its enabled defaults
false. The separate options pin an HTTPS origin, canonical project UUID and separately issued
project-bound replay upload credential. Anectico.replay is nullable;
Anectico.configureReplay(options) retires the old controller and requires fresh replay consent
and policy. Passing null removes it. See Android structural replay
for the complete source and privacy example and qualification limits.
| Controller operation | Result and boundary |
|---|---|
bindReplayOwner(ownerID) |
Explicit nonempty opaque distinct ID, at most 200 UTF-8 bytes; never inferred from telemetry or flag targeting |
grantReplayConsent() |
Independent local opt-in; does not override remote or store permission |
withdrawReplayConsent() |
Synchronous local stop or purge, boolean success; no server erasure claim |
selectReplaySource(mode) |
NATIVE_HIERARCHY or EXTERNAL_STRUCTURAL; changing closes and purges the old recording |
acquireReplayCaptureContext() |
Ready(token, remainingLifetimeMs) or Refused(reason); one current token, at most five seconds and policy lifetime |
submitReplayStructure(token, sourceSequence, events) |
Atomic bounded UTF-8 candidate array; positive uint32 sequence starts at one, candidates omit ordinal |
captureReplayView(root, declaredScreen) |
Main-thread bounded View snapshot through the same admission fence |
observeReplayView(root, declaredScreen) |
Nullable owned sampler; its close handle survives valid policy renewals and remains final when closed |
configureReplayView(view, nodeKey, treatment, actionLabelKey) |
Static local marker, default, mask or block; changes invalidate prior capture |
recordReplayTap(view) |
Action on latest complete frame; optional eligible static label only |
recordReplayInputChange(view) |
One masked placeholder per editing episode; no value or length argument |
replayInputFocusLost(view) |
End the editing episode when using manual capture hooks |
recordReplayScroll(view) |
Bounded quarter-dp offsets on a supported scroll target |
refreshReplayPolicy() |
Future boolean; reads existing capture configuration using the capture key |
flushReplay() |
Future ReplayUploadOutcome; dedicated replay worker, independent of telemetry flush |
sourcePrivacy() |
Sanitized effective mask and block equality rules, eligible label keys and limits; external adapters apply these before encoding |
diagnostics() |
State, refusal, queued chunks and bytes, bounded loss, format and version, revision and latest upload outcome |
ReplaySubmission is Admitted(firstOrdinal, lastOrdinal, eventCount, frameCount), Refused(reason)
or Cancelled. Admission assigns ordinals. Generated loss events count in the admitted interval.
Refused or cancelled batches consume no sequence or ordinal. Reasons are DISABLED, NO_OWNER,
NO_CONSENT, POLICY_UNAVAILABLE, POLICY_EXPIRED, WRONG_SOURCE, STALE_CONTEXT, OUT_OF_ORDER,
INVALID_STRUCTURE, LIMIT or SPOOL_UNAVAILABLE. An old context cannot append to a new generation.
Duplicate and out-of-order batches are refused; renewed tokens do not restart source sequence.
ReplayUploadOutcome.status is ACCEPTED, RETRY, REFUSED or CANCELLED, with a bounded reason,
optional HTTP status and retry delay. Accepted includes validated discard; it is no retention proof.
Backoff preserves the original request. Permanent refusal closes the recording rather than sending
later chunks without a predecessor. Restart, withdrawal, reset and retirement purge replay locally.
The source has JVM, Robolectric and socket verification; physical-device qualification and stored
native playback evidence are separate release requirements. No Compose support or device rate,
battery, published package, raw-export or erasure claim is established here.
The replay worker permits one active call and one waiting call. An excess flush request completes
with RETRY and reason busy; an excess policy refresh returns false. Queued work from a retired
generation is cancelled before dispatch. Treat these outcomes separately from accepted uploads.