React Native
Connect React Native telemetry, remote flags, headless surveys and optional structural replay.
On this page
@anectico/react-native is a thin bridge over the native iOS and Android SDKs. It shares their identity,
session, event queue, native crash handling, and retry behavior while adding JavaScript error capture.
React Native 0.73 and later is supported through the NativeModule interoperability layer.
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.
Install and configure
During early access, install the artifact supplied during onboarding, replacing the path below:
npm install /path/to/anectico-react-native.tgz
cd ios && pod install
After the public npm release, use npm install @anectico/react-native instead. Rebuild the native
app after installation; a JavaScript-only reload does not install the native module.
import * as Anectico from '@anectico/react-native';
await Anectico.configure({
apiKey: 'an_...',
captureApiKey: "an_<mobile-capture-key>",
environment: 'production',
release: '1.4.0',
dist: '42',
});
await Anectico.identify('user_8842', { plan: 'pro' });
await Anectico.capture('checkout_completed', { total: 42 });
await Anectico.screen('Checkout');
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.
Use separate project-bound native_ingest and mobile_capture keys. Do not include read, management,
or agent scopes in the application.
Call await Anectico.reset() on logout. The native reset rotates identity and session,
clears groups, and drops global error-user context plus buffered breadcrumbs so
prior-account diagnostics cannot cross a shared-device logout boundary. Call
await Anectico.shutdown() when the runtime is being torn down.
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', { $revenue: 19.99, $currency: 'USD', plan: 'pro' });
$revenue is a number in major units (19.99, not 1999), negative for a refund, with at most
four decimal places; $currency is three upper-case letters. A value sent as text, with more
decimal places, or with a lower-case currency is counted as invalid and left out of every figure.
Errors and crashes
try {
await checkout();
} catch (error) {
await Anectico.captureError(error, { tags: { screen: 'Checkout' } });
}
configure installs handlers for uncaught JavaScript errors and unhandled promise rejections unless
enableJsErrorCapture is false. The previous handlers remain chained, so React Native's normal
error and crash behavior continues. Every JavaScript error includes an SDK-owned mechanism:
rn.captureError for the public API, rn.globalHandler for uncaught errors, or
rn.unhandledRejection for rejected promises. On both iOS and Android, Anectico also owns the JavaScript
error's level and handled state: fatal errors are fatal / unhandled and other errors are error /
handled. Application tags cannot override those classification values. In Release builds, a fatal
global error is persisted synchronously before React Native terminates; Anectico recognizes RN's
immediate native wrapper and shows one frame-accurate JavaScript Issue instead of a duplicate
JavaScript and native pair. Genuine native crashes are still captured by the underlying SDK. If the
synchronous bridge is unavailable or persistence fails, native fallback capture remains enabled so
the error is not lost.
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.
Upload all release artifacts
A release build can need more than one artifact:
- Metro source maps for JavaScript frames;
- an iOS dSYM for native iOS frames; and
- Android
mapping.txtfor R8 or ProGuard frames.
Use the same release and dist used at configuration. Upload Metro source maps with the JavaScript
source-map uploader and native files with anectico symbols. Upload the map under its bundle filename
(for example, index.bundle, even when the map file is index.bundle.map); a bare bundle name
applies wherever the bundle is served from, and Anectico automatically matches Metro frame URLs,
including the JSC-safe index.bundle//&platform=... spelling, to that bundle.
Propagate to your backend
await Anectico.propagationHeaders() returns traceparent and baggage. Add them only to requests sent
to a trusted, instrumented backend. Do not add customer identity baggage to third-party endpoints.
Verify
Capture one caught JavaScript error and one native test event for an identified test user. Confirm
both appear under that customer: ask your agent to read the 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 the native module is unavailable, rebuild the application after
installing pods or Gradle dependencies; a Metro refresh alone cannot add native code.
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 React Native delivery reference
defines TelemetryFlushResult, ErrorCaptureResult, partial outcomes and retry limits.
Flags and surveys
Android and iOS delegate typed remote flags and headless surveys to their native clients. The iOS delegation passes tests of the shared Swift core; iOS app, simulator and device qualification remain pending. Rebuild the app after linking; a JavaScript package install or Expo Go alone cannot supply the native runtime.
Keep intake credentials separate from delivery. Set an explicit applicationEndpoint, projectID,
and optional project-bound CLIENT_DELIVERY keys: flagDeliveryApiKey with flags:evaluate,
surveyDeliveryApiKey with surveys:respond. Request separately issued keys. An omitted
capability sends no requests. Your ingestion endpoint and keys are never used as defaults.
await Anectico.configure({
apiKey: nativeIngestKey, captureApiKey: mobileCaptureKey,
applicationEndpoint, projectID, flagDeliveryApiKey, surveyDeliveryApiKey,
surveyScreenPaths: { Checkout: '/checkout' },
});
const reloaded = await Anectico.flags.reload({ keys: ['new_checkout'] });
if (reloaded.status === 'accepted') {
const read = await Anectico.flags.value('new_checkout');
if (read.status === 'accepted' && read.decision.valuePresent) {
useCheckoutDecision(read.decision.value); // A present false stays false; strings are variants.
}
}
Native decisions expire after 30 seconds. Inspect state, isFallback, exact
definitionVersion, coverage and expiry. flags.boolean(key, explicitFallback) also returns the
underlying read and a separate applicationFallback with value, used marker and reason.
flags.payload(key) preserves every JSON type: payloadPresent: true with payload: null
differs from absent payload. Unknown, expired and failed decisions remain inspectable.
When analytics is disabled or capture is unconfigured, explicitly declare the current application
subject with flags.setEvaluationBinding({ distinctId, personProperties, groups }). Without that
binding, reload returns identity_unavailable. It never reuses a previous person's identity.
Setting this binding does not identify a telemetry user or change their session. A flag exposure
requires product consent and a matching admitted telemetry subject.
Render a screen survey
Declare screen names once with surveyScreenPaths. Send a template such as /checkout, with no
navigation arguments, raw URLs or arbitrary screen text. Your app renders the returned questions
and records shown only when that UI appears. This example assumes a prepared survey whose only
required question is the rating question score:
const result = await Anectico.surveys.refresh('Checkout');
if (result.status === 'accepted') {
const offer = result.offers[0];
if (offer) {
renderSurvey(offer.survey);
const shown = await Anectico.surveys.shown(offer.handle);
if (shown.status === 'accepted') {
const response = await Anectico.surveys.submit(offer.handle, [{ questionId: 'score', rating: 0 }]);
if (response.status === 'accepted') displayReceipt(response.receipt.at, response.receipt.replayed);
}
}
}
surveys.eligible(screen) uses the same native refresh gate, with a five-second floor and at most
three offers. A handle closes when its native identity, consent or configuration is retired.
surveys.dismiss(handle) records dismissal. Keep the original handle for uncertain retries;
native code retains the original answers, identity, session and delivery ID. Answers stay in memory.
Ratings must be finite integers within the question's scale; zero is valid. The native client
validates required questions, option IDs, duplicate answers and the 2,000-character text limit.
Read the outcome
A valid empty offer list has status: 'accepted' and offers: []. Refusal, cancellation,
transport failure and malformed responses have distinct statuses, with literal failure.code,
HTTP status and retry advice when available. A missing native module returns
capability_unavailable with native_module_unlinked; it never returns healthy empty offers.
A decision is not exposure evidence. A validated { at, replayed } receipt confirms the survey
operation's acceptance, while stored events can become visible later. Multiple selected options
can produce several answer rows, so row count is not response total. Verify the stable signed-in
ID in the same project as web and read the resulting person timeline before claiming shared
person coverage. Host unit tests do not establish linked device or stored timeline delivery.
Delivery credentials and qualification
Follow mobile setup and limits for the separate
application HTTPS host and project-bound delivery keys. Keep ingestion endpoint, apiKey
(native_ingest, only ingest:write) and captureApiKey (mobile_capture, only
analytics:write) separate. Never embed management, MCP, flags:read or server APPLICATION
experiment-delivery authority. Private endpoints require the approved private network path;
do not publish an API to bypass that boundary.
Source availability and host tests do not establish a linked operating-system, installed-package or stored person and session pass. Each operating system needs its own runtime proof with original application IDs and independent stored readback. The public native per-exposure original-ID and outcome handoff remains unavailable; store-discovered IDs and aggregate flush counts cannot replace it. A flush or survey receipt is not shared-person proof. A separate Android API 36 x86_64 emulator run with React Native 0.86, Fabric and Hermes verifies actual native HTTPS flag and survey delivery and the shared person timeline with original sessions. Other configurations and physical devices require their own verification. Native replay remains outside this capability.
Native structural replay
The optional native structural source uses explicit host refs and independent replay controls. It records bounded geometry and declared actions, with no screenshots, extracted text, input values, typed length, accessibility content or image assets. The Android API 36 x86_64 emulator run with React Native 0.86, Fabric and Hermes separately verifies actual recording, retained steps, proof viewing and release emulator measurements. Other architectures, Apple runtimes, physical devices and battery behavior remain unqualified. Every actual host frame currently carries a visible unsupported gap. A bridge result alone proves none of those capabilities.
Configure a separately issued project-bound replay-upload key, current native capture policy and native store permission. An absent or expired native policy leaves recording off. Existing intake, flags, survey and management keys cannot substitute for the replay key.
await Anectico.configure({
apiKey: 'YOUR_INGEST_KEY',
captureApiKey: 'YOUR_CAPTURE_KEY',
endpoint: 'https://api.example.com',
nativeReplay: {
enabled: true,
endpoint: 'https://console.example.com',
projectID: '22222222-2222-4222-8222-222222222222',
uploadApiKey: 'YOUR_SEPARATE_REPLAY_UPLOAD_KEY',
declaredLabels: { 'checkout.continue': 'Continue' },
},
});
await Anectico.replay.bindOwner(currentCustomerID);
// Only after the person grants independent replay consent:
await Anectico.replay.grantConsent();
await Anectico.replay.refreshPolicy();
const result = await Anectico.replay.start();
// Check result.state and result.context before supplying the context to the UI.
A retained analytics ID is never the replay owner. This applies when analytics is disabled too. Unrelated identify requests and reset fence the old replay even if analytics refuses the change. Bind the next owner and obtain renewed replay consent before starting again.
Declare component privacy
Render the native-issued context through ReplayProvider. ReplayMask and ReplayBlock create
non-collapsible View boundaries. A mask includes descendants and suppresses labels. A block stops
child inspection and interactions. Enclose survey answers in a mask or block.
<Anectico.ReplayProvider context={result.context ?? null}>
<Anectico.ReplayMask nodeKey="checkout.answers">
<YourSurvey />
</Anectico.ReplayMask>
<Anectico.ReplayBlock nodeKey="checkout.private">
<YourPrivatePanel />
</Anectico.ReplayBlock>
<Anectico.ReplaySafeAction labelKey="checkout.continue">
<YourContinueControl />
</Anectico.ReplaySafeAction>
</Anectico.ReplayProvider>
SafeAction observes its own host's touch-end and sends no event payload. Its label text comes only from the static configuration declaration and must also be remotely eligible. Native source redaction can drop the whole label. Labels are bounded, untrusted application evidence; a matching phrase is not a guarantee against arbitrary secrets. Never declare person IDs, values, answer text or URL parameters as keys or labels.
Use useReplayNode('View', { nodeKey: 'checkout.root' }) on the application's actual visible root
View, keeping its ref and collapsable={false}. The same hook accepts Text, TextInput, Image,
ScrollView and Unsupported declarations. Register each actual host that should contribute a box;
undeclared, composite, custom, embedded, transformed and unresolved hosts remain omissions. Text
and input internal children are never inspected. The iOS resolver recognizes selected ordinary Fabric hosts. Scope remains limited to supported
host classes; module interoperability alone does not qualify an architecture.
Request one snapshot with replay.capture(rootHost, 'checkout', context), at most twice per second.
The root excludes system insets, keyboard and OS chrome. For direct input hooks, call
inputChanged(inputHost, context) without its value or event payload and inputFocusLost(inputHost, context)
on blur. Changes coalesce into one masked placeholder per editing episode. tap and scroll
likewise take only a host ref and current context; native code supplies frame targets and offsets.
There is no JavaScript hierarchy collection or uploader.
Respect recording lifetimes
start() never grants consent. It returns an expiring context for at most five seconds. Renew it
before expiry and render that fresh context through the provider; native code retains authority
over its recording generation. Missing refs and late registration are explicit omissions. Hooks
unregister on unmount or replacement, and old leases cannot clear a replacement's hook.
Withdrawal, expiry, background, shutdown and changes to owner, session, configuration or policy
cancel old work. stop() withdraws replay consent and purges local unsent replay; restarting needs
another explicit grant. Local purge is not server erasure. Native flush acceptance can mean discard,
so inspect actual stored steps and visible gaps before describing a replay as retained or playable.
Read the replay controls and source matrix
and native recording privacy.
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
replay.capture may return outcome: "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.
For automatic offers, use await replay.startAutomatically() after binding the owner and granting
consent, render its context through ReplayProvider, then call
await replay.captureAutomatically(rootHost, 'checkout', context). Starting waits for native
policy readiness. Capture waits for native admission and source registration, including when a
timer fires early. Refresh the context as it expires and render a changed token through the
provider. Source registration waits use a native event and the context deadline, with no polling.
Registrations are batched within each provider; automatic Android lease renewals with unchanged
recording authority keep them. Manual capture calls keep their explicit rate refusals.
Readiness never grants consent or changes the bound owner.