Flutter SDK API
Functions, options, parameters, futures, and native behavior for the Anectico Flutter SDK.
On this page
anectico_flutter bridges Dart to the native iOS and Android SDKs. Native code owns identity, sessions,
persistence, transport, and native crashes; Dart adds Flutter framework and uncaught async error
capture. Configuration failures, including a missing native plugin, fail the returned future;
subsequent telemetry calls remain best-effort and swallow channel failures.
Use separate project-bound native_ingest and mobile_capture keys. Optional structural replay requires its own controls and runtime qualification; see Flutter 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. Explicit logout or reset retains its behavior.
See native intake keys for creation and verification.
Configure and shut down
await Anectico.configure(
const AnecticoOptions(
apiKey: 'an_...',
captureApiKey: "an_<mobile-capture-key>",
environment: 'production',
serviceVersion: '1.4.0',
),
);
| API | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.configure(options) |
AnecticoOptions |
Future<void> |
Configures native state and installs Dart handlers unless disabled. The future fails for invalid options or a missing native plugin, and isConfigured remains false. |
Anectico.flush() |
— | Future<TelemetryFlushResult> |
Flushes the native event queue and resolves the per-signal delivery outcomes. See Knowing whether your events arrived. |
Anectico.shutdown() |
— | Future<TelemetryFlushResult> |
Removes Dart and native hooks, flushes, and releases resources. Resolves the final flush's outcome, like flush. |
Anectico.configure(options, resumeAnalyticsCollection: true) |
Explicit new consent after awaited shutdown | Future<void> |
Purges all old analytics queue data before reopening collection; failures reject configuration. |
Anectico.disableAnalytics() |
— | Future<bool> |
Persist analytics withdrawal and purge queued data; await the result before relying on the native gate. |
Anectico.stats() |
— | Future<AnalyticsStats> |
Delivery counters — recorded, delivered, dropped, queued. Zeros before configure. |
Anectico.deliveryErrors |
property | Stream<DeliveryError> |
Emits once per permanently discarded batch with status, dropped, and reason. |
Anectico.isConfigured |
property | bool |
Dart-side view of whether configure completed. Diagnostic use only. |
AnecticoFlutter.installErrorCapture() |
— | void |
Installs and chains FlutterError.onError and PlatformDispatcher.onError. |
AnecticoFlutter.uninstallErrorCapture() |
— | void |
Stops automatic Dart capture and restores prior handlers when they are still owned by Anectico. Retained hooks continue forwarding without capturing. |
AnecticoFlutter.isErrorCaptureInstalled |
property | bool |
Whether Dart hooks are currently installed. |
Withdrawing analytics consent
Await 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. Await the future before relying on the native admission fence. A missing plugin or channel failure returns false.
Ordinary configuration preserves the marker across restart. Only after obtaining new consent,
await Anectico.shutdown(), then call await Anectico.configure(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.
A complete version-one receipt classifies each original position before the SDK updates its queue or counters:
| Receipt outcome | Your events | flush |
Queue and callback |
|---|---|---|---|
queued |
removed from the pending queue | succeeds if all positions were queued | increments delivered |
rejected |
refused positions are discarded | failure | increments dropped; fires a drop callback |
unknown, malformed, truncated, oversized, or absent receipt |
uncertain positions remain pending | failure | retains original message IDs; no drop callback |
Typed permanent request failures also discard the attempted batch. Network failures, rate limits, and dependency failures retain it. Repeated uncertainty backs off from one second to 60 seconds; a fully settled receipt resets that delay. Server retry hints are clamped to that range.
Anectico.stats() reports recorded, delivered, dropped, and queued; recorded equals
the other three added together. delivered counts validated queue acknowledgements,
not unique stored events.
The Anectico.deliveryErrors stream reports the number of discarded events once per affected chunk. A batch
receipt can refuse events with HTTP 200: invalid_event, quota_exceeded, or
mixed_rejection. Typed request failures use bad_request, unauthorized, forbidden,
not_found, payload_too_large, unprocessable, or rejected. Local queue_overflow
and not_serializable drops carry status 0. Unknown outcomes remain queued without a callback unless explicitly discarded on withdrawal with collection_disabled. An unrecognized reason maps to DeliveryErrorReason.unknown, preserving the original code in rawReason.
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.deliveryErrors.listen((DeliveryError error) {
debugPrint('lost ${error.dropped} events: ${error.rawReason} (HTTP ${error.status})');
});
final TelemetryFlushResult delivery = await Anectico.flush();
if (delivery.productEvents.status != SignalFlushStatus.accepted) {
final AnalyticsStats stats = await Anectico.stats();
// stats.queued > 0 means a retry is pending; otherwise the events are gone.
}
When the plugin is not installed in the running app, flush() and shutdown() report unknown signal outcomes and
stats() reports zeros — nothing was delivered, so nothing is reported as accepted.
AnecticoOptions
Only apiKey is required. Null fields are omitted across the channel so native defaults apply.
Invalid endpoints, timers, capacities, frame limits, and non-finite or out-of-range sampling values
fail configuration instead of 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?; native https://api.anectico.com |
API base URL. |
environment |
String?; native development |
Deployment environment. |
release, dist |
String? |
Release and artifact identifiers. |
serviceName, serviceVersion |
String? |
Native resource identity; defaults from the app. |
flushAt |
int?; native 20 |
Event-count flush threshold. |
flushIntervalMs |
int?; native 5000 |
Foreground-only native periodic flush; zero disables it. |
maxStackTraceFrames |
int?; native 50 |
Dart and native frame cap. |
breadcrumbCapacity |
int?; native 100 |
Native breadcrumb capacity. |
maxQueueSize |
int?; native 10000 |
Native event queue cap. |
errorSampleRate |
double?; native 1 |
Non-fatal error sampling from 0–1. |
enableCrashReporting |
bool?; native true |
Install native crash handlers. |
debug |
bool?; native false |
Native diagnostic logging. |
enableDartErrorCapture |
bool; true |
Dart-only automatic error handlers. |
Identity, accounts, and events
| Method | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.identify(distinctId, {set}) |
stable ID; person properties | Future<void> |
Links anonymous and known identity. |
Anectico.group(type, key, {set}) |
account type and key; properties | Future<void> |
Records membership and $groupidentify. |
Anectico.capture(event, {properties}) |
event; properties | Future<void> |
Queues a durable diagnostic event, natively stamped with the reserved $release/$app_version properties (see Reserved event properties) when configure() set release/serviceVersion. |
Anectico.screen(name, {properties}) |
screen; properties | Future<void> |
Queues $screen and $screen_name. |
Anectico.reset() |
— | Future<void> |
Logout: rotates native identity or session and clears groups, global error-user context, and breadcrumbs. |
Anectico.getDistinctId() |
— | Future<String> |
Current identity or empty string. |
Anectico.getSessionId() |
— | Future<String> |
Current session without extending idle lifetime, or empty string. |
identify and group update local identity only after their control event passes native 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. On both Android
and iOS, an explicit properties[r'$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 them.
await Anectico.capture('checkout', properties: {r'$groups': {'company': 'account-B'}});
await Anectico.capture('ungrouped', properties: {r'$groups': <String, String>{}});
Agent runs
Anectico.startAgentRun(AgentRunOptions options) synchronously returns a one-shot AgentRun.
final run = Anectico.startAgentRun(const AgentRunOptions(
agent: 'checkout-helper',
agentId: 'checkout',
agentVersion: '1.4.0',
conversationId: 'chat-42',
));
// Perform the agent's work, then declare its actual outcome.
await run.end(AgentRunTerminalStatus.completed);
// On failure instead:
// await run.end(AgentRunTerminalStatus.failed, terminalReason: 'tool_failed');
AgentRunOptions.agent is required. 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(AgentRunTerminalStatus status, {String? terminalReason}) returns Future<void>, not a
delivery boolean. Status is required: completed, failed, timedOut, cancelled, or
maxSteps; 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 fails the future with ArgumentError and leaves the handle open. A native
false result completes the future but leaves the handle open for a later retry; channel failures
are handled by the native bridge wrapper. Concurrent end calls share the in-flight future.
After native acceptance, later valid calls do nothing. Completion is not network-delivery
confirmation. Native identity is attached at completion, so end the run before resetting or
switching customers.
Errors, messages, users, and breadcrumbs
| Method | Parameters | Returns | Behavior |
|---|---|---|---|
Anectico.captureError(error, stackTrace, {options}) |
object; optional stack; options | Future<ErrorCaptureResult> |
Parses Dart frames and forwards a caught error to native. Only options.tags applies to this bridge path. |
Anectico.captureMessage(message, {level, options}) |
text; level default info; options |
Future<ErrorCaptureResult> |
Native message capture with full options fidelity. |
Anectico.addBreadcrumb(category, message, {level, data}) |
fields | Future<void> |
Adds native trail context for the next error. |
Anectico.setUser(user) |
AnecticoUser |
Future<void> |
Sets global error user context. |
Anectico.clearUser() |
— | Future<void> |
Clears error user context. |
CaptureOptions fields are tags, extra, user, level, and fingerprint. Only tags is used
by captureError; all fields can be used by captureMessage. AnecticoUser fields are id, email,
username, ipAddress, and segment. Breadcrumb constants live in AnecticoBreadcrumbLevel.
Automatic handlers use mechanisms AnecticoMechanism.frameworkError and
AnecticoMechanism.platformDispatcher. They forward to the prior handler afterward, preserving normal
Flutter behavior. Native fatal crashes are handled by the underlying SDK.
Installing automatic capture twice is a no-op. Uninstalling it, including through Anectico.shutdown(),
stops reporting even if another error reporter still holds an Anectico handler. That retained handler
continues forwarding to the handler that preceded it. A later installation captures once per error
through the current reporter chain. The platform hook preserves the previous handler's boolean
verdict and returns false when there was no previous handler, keeping the error unhandled.
The native transport adds SDK-owned app and build, OS, and privacy-safe hardware-model context. It does not collect stable device identifiers or user-assigned device names.
Backend propagation
Anectico.propagationHeaders(): Future<Map<String,String>> returns a fresh W3C traceparent and current
identity baggage. Send it only to a trusted application backend. The backend must authenticate the
request independently.
Stack utility exports
The package exports AnecticoFrame, parseStackTrace, parseStackString, framesToJson, and
maxDartFrames for custom error integrations. Normal application code should call
Anectico.captureError, which uses those helpers automatically.
Anectico does not currently symbolicate Dart frames obfuscated with --obfuscate. Retain Flutter debug
information and use flutter symbolize outside Anectico for those builds.
Native capture receipt migration
Both bundled native sources now validate complete version-one indexed capture acknowledgements.
Mixed receipts retire queued and refused positions while retaining uncertain original events in
their durable spools. Malformed or incomplete responses remain uncertain. Ledger refusals can carry
HTTP 200 with invalid_event, quota_exceeded, or mixed_rejection; those codes cross this bridge.
Without a hint, native retry delay doubles from one second to a 60-second cap until a receipt
fully settles. Retry-After is clamped to 1–60 seconds. Published-package, device and backend qualification remains open; this is not a completed release.
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 replay API
The optional AnecticoOptions.nativeReplay takes AnecticoReplayOptions: explicit HTTPS origin,
project ID, separate replay upload key, enabled (default false), static mask and block rules and
local declared labels. The adapter and external structural source mode are fixed. Native policy,
independent content permission, current replay owner and consent must all permit recording.
See setup and source coverage.
| API | Behavior |
|---|---|
Anectico.replay.bindReplayOwner(ownerID) |
Explicit current owner, independent of analytics; unrelated change purges and requires renewed consent |
grantReplayConsent() and withdrawReplayConsent() |
Separate local permission; withdrawal stops sampling and native work and purges unsent content |
refreshPolicy() |
Ask native for current policy; no cached allow survives expiry |
ReplayView |
One visible content viewport and static declared screen per controller |
ReplayRegion, ReplayMask, ReplayBlock, ReplayUnsupported |
Explicit primitive or privacy declarations; ancestor privacy dominates |
capture(), start(), stop() |
One frame, bounded scheduled observation, or stop Dart sampling respectively |
tap(target) |
Optional eligible static label only, never extracted text |
inputChanged(target), endInputEpisode(target) |
One masked placeholder per editing episode, no input values or length |
scroll(target, x: ..., y: ...) |
Finite nonnegative logical offsets; requires a declared scroll target |
flush() |
Native immutable upload attempt; acceptance retains unknown storage outcome |
diagnostics() and metrics |
Bounded native state and counters in result details, and Dart host traversal and encoding counts |
dispose() |
Retires this controller and withdraws native consent |
ReplayResult.status is admitted, refused, cancelled, unavailable or accepted for a control.
For flush(), read the typed ReplayResult.outcome: acceptedRetentionUnknown, idle,
cancelled, refused or retry. These use the same native upload distinctions as React Native.
An accepted receipt may mean discard; idle means there was no upload. Neither proves retention.
reason identifies refusal or loss; submitted results also carry source recording ID and source sequence and
fixed native_structural version 1. No owner or generation supplied by Dart grants authority.
An action needs the latest complete current frame. A gap or viewport or screen change invalidates old
targets. Missing plugins return unavailable, never a healthy empty recording. Input, image and text
regions are opaque masked leaves. Custom painting and embedded renderers are unsupported gaps.
Static ReplayLabel(key, text) must match nativeReplay.declaredLabels, current remote eligibility,
source redaction and all ancestor privacy rules. Keys are static 1–64 ASCII tokens; text is 1–48 ASCII
letters, spaces, dots, underscores or hyphens, starting with a letter. No semantic safety is implied
by this grammar. ReplayController(redactLabel: ...) can drop declarations by changing their text.
Use only the active controller for a viewport. Applications normally use Anectico.replay.
The maxima are 256 nodes, depth 32, 512 visited render objects, one frame and sixteen events per 65,536-byte source batch, lowered by policy. Geometry uses clipped quarter logical pixels. Scheduled source attempts cap at two frames and twenty events per rolling second, including gaps; these are engineering ceilings, not supported device rates. Native spool limits are eight chunks, 2 MiB and 60 seconds, with current authority checked again at dispatch. Withdrawal, expiry, background, owner changes or reset, shutdown and configuration retirement purge old pending work. A dispatched request can still arrive. Receipts are neither retention nor playback proof, and local withdrawal is not server erasure. Raw content remains excluded from person archives. Flutter OS and device support and release overhead remain unqualified by the host tests described here.
Unchanged structural frames
captureAutomatically() can reuse the last complete frame without walking the source or calling
the native bridge while no frame has rendered and its original finite context remains valid.
Rendered frames, source changes and authority or loss changes require fresh source facts. The
sampling timer does not request a UI frame. Manual capture() always walks the source.
capture() can return ReplayStatus.unchanged (channel value "unchanged") when the latest
admitted frame is still exact. It creates no event, chunk or upload and spends no frame or event
budget. The controller retains the latest admitted action targets. Permission changes, screen
or viewport changes, gaps and pauses require a fresh complete frame.