Skip to content
Console
Browse documentation
Guide

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.txt for 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.