Flutter
Connect Flutter telemetry, remote flags, headless surveys and optional structural replay.
On this page
anectico_flutter bridges Dart to the native iOS and Android SDKs. Identity, sessions, persistence,
transport, and native crash handling remain in the native runtimes; the plugin adds Dart error
capture.
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. Explicit logout and reset retains its behavior.
See native intake keys for creation and verification.
Offline event delivery on Android
On Android, pending product events are bound to the capture endpoint and captureApiKey.
Restarting with the same values retries the original events and IDs. Changing either value discards
pending events with a warning, including a capture-key rotation within the same project. Flush and
confirm success before switching or rotating. Changing only the OTLP apiKey preserves this queue.
Temporarily omitting the capture key on the same endpoint keeps pending events until that key returns; restoring a different key discards them. The first upgrade to recipient-bound persistence also discards older queues whose destination cannot be verified. Endpoint or key changes never renew withdrawn analytics consent.
Add and configure the plugin
During early access, use the source checkout supplied during onboarding, replacing this path:
dependencies:
anectico_flutter:
path: /path/to/anectico/sdks/flutter
Run flutter pub get. After the public pub.dev release, replace the path dependency with the
published version constraint. For iOS,
enable Flutter's Swift Package Manager integration once with
flutter config --enable-swift-package-manager. The iOS deployment target is 15 or later.
import 'package:anectico_flutter/anectico_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Anectico.configure(
const AnecticoOptions(
apiKey: 'an_...',
captureApiKey: "an_<mobile-capture-key>",
environment: 'production',
serviceVersion: '1.4.0',
),
);
runApp(const MyApp());
}
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.
await Anectico.identify('user_8842', set: {'plan': 'pro'});
await Anectico.group('company', 'acme', set: {'plan': 'enterprise'});
await Anectico.capture('checkout_completed', properties: {'total': 42});
await Anectico.screen('Checkout');
// On logout:
await 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.
release/serviceVersion above forward verbatim to the underlying native iOS and Android SDK, which
stamps the reserved $release/$app_version string properties on every captured event (not only
the automatic $app_opened). Neither is invented: omit them and events carry 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:
await Anectico.capture('purchase', properties: {r'$revenue': 19.99, r'$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.
Use raw strings (r'$revenue') so Dart does not read the dollar sign as interpolation.
Errors and crashes
Anectico.configure installs handlers for Flutter framework errors and uncaught asynchronous Dart errors
unless enableDartErrorCapture is false. Existing handlers remain chained.
try {
await checkout();
} catch (error, stack) {
await Anectico.captureError(
error,
stack,
options: const CaptureOptions(tags: {'screen': 'cart'}),
);
}
Native crashes use the underlying iOS and Android handlers. Upload their dSYM or ProGuard mapping.
Issue details identify Dart failures as anectico-flutter / dart and preserve their already-symbolic
package: frames without native artifact guidance. A Java and Kotlin, Swift, Objective-C, or signal
crash from the same Flutter application remains a native occurrence and still receives the relevant
ProGuard or dSYM status.
Each occurrence also inherits SDK-owned app and build, OS, and privacy-safe hardware-model context from the native transport. Anectico never collects a stable device identifier or user-assigned device name.
Anectico does not currently symbolicate obfuscated Dart frames produced by --obfuscate; keep debug
information and use flutter symbolize out of band when that build mode is required.
Propagate and shut down
await Anectico.propagationHeaders() returns W3C trace and customer baggage for trusted backend calls.
Call await Anectico.flush() before a test assertion and await Anectico.shutdown() when releasing the
runtime.
Verify
Identify a test customer, capture a Dart error and event, and confirm both appear together. Ask your
agent to read that customer's timeline (get_person_timeline over MCP, or anectico persons timeline <customer-id>); the MCP result carries a link that opens the customer as a read-only
proof page. If iOS
fails to link the native package, confirm Swift Package Manager support is enabled and rebuild the
application rather than hot reloading.
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 Flutter delivery reference
defines TelemetryFlushResult, ErrorCaptureResult, partial outcomes and retry limits.
Remote flags and headless surveys
Flags and surveys use optional native capabilities. Configure applicationEndpoint, projectID
and separately issued CLIENT_DELIVERY keys: flagDeliveryApiKey has flags:evaluate, and
surveyDeliveryApiKey has surveys:respond. Neither key is inferred from apiKey,
captureApiKey or endpoint. Omit either delivery key to leave that capability unconfigured.
The intake keys keep their existing purposes. Never embed an agent or management credential.
await Anectico.configure(const AnecticoOptions(
apiKey: 'an_native_ingest_key',
captureApiKey: 'an_mobile_capture_key',
endpoint: 'https://api.anectico.com',
applicationEndpoint: 'https://console.example.com',
projectID: '22222222-2222-4222-8222-222222222222',
flagDeliveryApiKey: 'an_flag_client_delivery_key',
surveyDeliveryApiKey: 'an_survey_client_delivery_key',
surveyScreenPaths: {'Checkout': '/checkout'},
));
await Anectico.identify('customer-123');
final FlagDecisionDocument document = await Anectico.flags.reload(keys: ['new_checkout']);
final FlagRead read = await Anectico.flags.value('new_checkout');
final FlagBooleanRead button = await Anectico.flags.boolean('new_checkout', fallback: false);
// button.usedApplicationFallback and button.reason explain an application fallback.
FlagDecisionDocument contains projectID, the original evaluatedAt string, maxAgeSeconds,
decisionContextID, requestedKeyCount, complete and the selected decisions map.
Each FlagDecision retains key, state, valuePresent, typed value, payloadPresent,
JSON payload, exact definitionVersion, isFallback and inputContext. BooleanFlagValue
and VariantFlagValue distinguish a present false from absence. A present JSON null has
payloadPresent == true and payload == null; absence has payloadPresent == false.
Version timestamps retain all six fractional digits. Do not replace a definition version with
decisionContextID or convert it through a millisecond timestamp.
Reload requires 1–100 unique keys. Decisions expire after their original 30-second lifetime.
flags.status(key) inspects without an exposure; flags.value(key) and flags.payload(key)
are served reads and use native exposure admission when product consent permits it. An evaluated
false, unknown, expired, evaluation_error and unavailable_flag remain distinct.
isFallback describes a server fallback; usedApplicationFallback describes the application's
explicit boolean fallback. A decision does not prove that an exposure was delivered or stored.
After product withdrawal, or without captureApiKey, flags require a current explicit application
binding. Use flags.setEvaluationBinding(distinctID: ..., personProperties: ..., groups: ...),
flags.clearEvaluationBinding() and flags.snapshot(). Replacing a binding does not identify a
person, change the telemetry session or send an identity event. It invalidates old decisions and
pending flag work. Without a current binding, reload reports identity_unavailable.
Snapshots carry a native opaque token and available binding generation. For an explicit bootstrap,
save the snapshot before the original request, then pass it with its keys, project and original
JSON response to FlagBootstrap and flags.installBootstrap. Optional AnecticoOptions.bootstrap
uses that same live native snapshot. Tokens do not survive shutdown, a new configuration or an
incompatible identity or context. Importing a bootstrap never refreshes its original expiry.
final List<SurveyOffer> offers = await Anectico.surveys.refresh(screen: 'Checkout');
if (offers.isNotEmpty) {
final SurveyOffer offer = offers.first;
// Render offer.questions in your application's UI, then record actual display:
final SurveyReceipt shown = await offer.shown();
final SurveyQuestion rating = offer.questions.firstWhere(
(q) => q.type == SurveyQuestionType.rating,
);
final SurveyReceipt receipt = await offer.submit(answers: [
SurveyAnswer(questionID: rating.id, rating: 0),
]);
// receipt.at is the validated server timestamp; receipt.replayed is a bool.
}
The zero-rating example requires a rating question whose scale includes zero and an offer with
no other unanswered required question. Questions support singleChoice, multipleChoice, rating
and openText, stable IDs, prompts, required fields, options and scales. Answers have questionID
and optional integer rating, optionIDs or text. Boolean, string and fractional ratings are
refused. Native validation rejects unknown or duplicate questions or options and out-of-range answers.
Use offer.dismiss() for dismissal. Retain the same offer, delivery ID and answers when retrying
an uncertain operation; native code retains the original request and source session in memory.
There is no persistent answer queue or automatic survey event capture.
surveys.eligible() reads the current native offers without requesting new eligibility. Refresh
has a five-second floor and returns at most three offers, each with at most ten questions. Choice
questions have 2–12 options, rating scales stay within 0–10, and open text is limited to 2,000
characters. Screen names map only to declared slash templates; never pass route arguments or raw
screen text as a path. Offers are native opaque handles for the original person and revision;
identity, consent, configuration and shutdown boundaries retire them.
Capability failures and runtime qualification
Asynchronous reload, eligibility and delivery fail with CapabilityException, carrying status,
rawStatus, literal code, optional httpStatus, retryable and retryAfterMs. Inspect refusal,
transport error, cancellation, unavailable identity and unconfigured capability separately from a
valid empty offer list. Malformed native results report invalid_response. An unlinked plugin
reports CapabilityStatus.capabilityUnavailable; it cannot return a healthy empty list, false decision
or submitted receipt. Anectico.cancelPendingCapabilities() cancels pending capability calls and
retires current offer tokens. Pending calls settle once; late callbacks cannot install new offers.
Existing best-effort telemetry and Anectico.deliveryErrors keep their separate delivery contract.
A validated receipt is an acknowledged operation, not query visibility. Response rows are not response totals. Your agent must independently read the received evidence and both identity aliases before claiming a shared person timeline. Public attributable product-event IDs await the shared mobile delivery API; error IDs and a successful flush do not substitute for them.
Dart unit tests use a mocked channel. Android JVM tests exercise the real Android codec and native clients. Swift tests on Linux exercise native core and Foundation mapping only. A separate Flutter 3.47.7 Android API 36 x86_64 emulator run builds and installs the real app, verifies native HTTPS flag and survey delivery and independently reads the shared person timeline with original sessions. Apple runtimes, physical devices, other configurations and published packages need separate qualification. The optional structural adapter has its own source coverage and runtime qualification boundary.
Native structural replay
Flutter's optional structural adapter is off by default. It needs current native capture policy,
native recording store permission and a separately issued, project-bound REPLAY_UPLOAD key with
only replay:write. Keep the usual separate capture and ingest keys. The replay endpoint is an
explicit HTTPS origin, and its project must match the capture configuration. A deployment without
native support refuses capture. Browser replay permission does not authorize this source.
await Anectico.configure(AnecticoOptions(
apiKey: ingestKey,
captureApiKey: captureKey,
nativeReplay: AnecticoReplayOptions(
endpoint: replayOrigin,
projectID: projectID,
uploadApiKey: replayKey,
enabled: true,
declaredLabels: const {'checkout.continue': 'Continue'},
),
));
await Anectico.replay.bindReplayOwner(currentCustomerID);
// Only after this person's independent replay permission:
await Anectico.replay.grantReplayConsent();
await Anectico.replay.refreshPolicy();
Mount ReplayView(controller: Anectico.replay, declaredScreen: 'checkout', child: ...) inside your
safe area. Use a static screen token, never a route parameter or customer value. capture() requests
one frame; start() schedules bounded frames and stop() stops Dart sampling. Call
withdrawReplayConsent() to stop native work and purge unsent replay. Explicit owner changes and
reset close the old recording; a new owner requires renewed consent. A frozen analytics identity,
flag binding or submitted survey answer never supplies replay ownership. No identity event is
created just by binding replay. Anonymous alias continuation is not inferred.
| Instrumentation | Recorded shape |
|---|---|
ReplayRegion with a container, button or scroll primitive |
Geometry and opaque action target |
| Text, editable field, toggle or image region | Masked opaque box; no content, typed length or checked state |
ReplayMask |
Masks every descendant and suppresses action labels |
ReplayBlock |
One placeholder; descendants are never inspected |
ReplayUnsupported or Unknown renderers, custom painting or embedded regions |
Visible unsupported omission and gap |
Ordinary supported text, editable and image renderers remain masked. The small exact layout-renderer mapping is documented in the SDK guide; custom subclasses, textures, platform views, embedded web content and slivers remain unqualified gaps. Native hierarchy inspection does not capture Dart UI. Survey answers remain masked even when the application intentionally submits them elsewhere.
Use a ReplayTarget on a declared region and call tap, inputChanged, endInputEpisode on focus
loss, or scroll with logical offsets from your own hooks. Input changes are coalesced placeholders;
these helpers accept no value or typed length. An optional static ReplayLabel on taps also needs
an identical local configuration declaration, remote eligibility and unchanged source redaction.
Labels remain bounded, untrusted application evidence, never instructions. Ancestor masks and
blocks dominate all action declarations. Nothing extracts visible or accessibility strings, assets,
URLs, images or executable markup.
Native admission requires a current expiring context and assigns ordered events. Missing plugins,
stale identity, session, consent, policy or configuration and malformed frames return explicit unavailable,
refused or cancelled outcomes. Background, withdrawal, expiry, reset and retirement stop old work.
Frames are bounded before traversal and allocation: at most 256 nodes, depth 32, 512 visited render
objects and 65,536 source bytes, lowered by current policy. Two frames and twenty events per rolling
second are safety maxima; physical-device rates remain unmeasured. Unsupported and dropped regions disclose
gaps. Anectico.replay.metrics reports host work counts; diagnostics() reports native queue and loss
state, without credentials or recorded content.
The native recorder alone owns uploads and the protected bounded spool. Retries keep their original recipient, owner, session and bytes. A receipt may acknowledge discard and does not establish stored steps, playback or server erasure. Raw recording content stays outside person archives; retained evidence remains subject to its source's current policy, retention and erasure. Local withdrawal cannot recall a request already dispatched.
The separate Flutter 3.47.7 Android API 36 x86_64 emulator run verifies the linked source, retained player and steps, privacy fences and release emulator measurements. Thirty-six of forty profile capture attempts were admitted; four were cancelled. No sustainable frame rate or physical-device overhead is established. Apple runtimes, other configurations, custom and unqualified regions remain omissions. See the API reference and native privacy guidance.
On Android, asynchronous replay submissions buffer one open chunk and persist it at 8 frames, 64 events, 2 seconds after its first event, or on replay flush, background or shutdown. A crash can lose this open window; a later recording reports unknown loss after fresh owner binding, consent and policy. A restart still discards retired local replay. Withdrawal and other privacy purges drop buffered events too. Call replay flush when an explicit durable boundary is needed.
Unchanged frames and paced recording
capture() may return ReplayStatus.unchanged. Keep the latest admitted action targets.
An unchanged frame creates no event or upload. Equal unsupported placeholders can be unchanged;
a changed rectangle, kind, privacy treatment or identity requires a complete frame. A whole
unavailable view remains an explicit gap.
start() schedules automatic captures; captureAutomatically() offers one paced capture.
Automatic work waits for native policy readiness and the next admission deadline, including when
a timer fires early. Bind the owner and grant consent first. Manual capture() keeps explicit
rate refusals. Starting and readiness never grant consent or change the bound owner. Call stop()
when pausing and dispose() when the controller is no longer owned by the application.