---
title: Verify React Native SDK events
description: "Trace a React Native SDK event through each stage: what the SDK captured, how plugins transformed it, what it sent, and what Amplitude received."
product: analytics
lang: en
last_updated: 2026-11-10
token_estimate: 2897
---
# Verify React Native SDK events

An event moves through four stages between your `track()` call and Amplitude's servers. Verify each stage in order. The first stage where the event looks wrong is where the problem is.

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

| Stage | Question | Where to look |
| --- | --- | --- |
| [Captured](#check-what-the-sdk-captured) | Did the SDK accept the call? | Debug logs in the Metro terminal or device log |
| [Transformed](#check-how-plugins-transformed-the-event) | What did plugins add, change, or drop? | Verbose logs, or an inspection plugin |
| [Sent](#check-what-the-sdk-sent) | Did the request leave the device, and with what payload? | The final-event log line, a proxy, or a custom transport |
| [Received](#check-what-amplitude-received) | Did Amplitude accept and store the event? | The upload result, then _User Lookup_ in Amplitude |

For problems common to every Amplitude SDK, go to [SDK troubleshooting and debugging](https://amplitude.com/docs/sdks/sdk-debugging). If behavior changed after an SDK upgrade, check the [React Native SDK release notes](https://amplitude.com/docs/sdks/analytics/react-native/react-native-sdk-release-notes).

## Turn on debug logging

Set `logLevel` to `Debug` while you verify. Debug logs include every public method call, the internal state before and after it, each plugin step, and the server response.

```ts
amplitude.init("AMPLITUDE_API_KEY", undefined, {
  logLevel: amplitude.Types.LogLevel.Debug,
});
```

The levels are `None` (0), `Error` (1), `Warn` (2, the default), `Verbose` (3), and `Debug` (4). The SDK writes to `console`, so messages appear in the Metro terminal, in the React Native DevTools console, and in Xcode or Logcat. Each message starts with `Amplitude Logger`, so filter on that prefix.

Return `logLevel` to `Warn` before you ship. Debug output includes event payloads and identifiers.

## Check what the SDK captured

With debug logging on, each public call prints a block like this:

```text
Amplitude Logger [Debug]: {
  "type": "invoke public method",
  "name": "track",
  "args": ["Sign Up Clicked", { "plan": "starter" }],
  "stacktrace": ["at onPress (SignUpScreen.tsx:42:13)", "..."],
  "time": { "start": "2026-11-05T22:59:41.957Z", "end": "2026-11-05T22:59:41.989Z" },
  "states": {
    "before": { "config.apiKey": "...", "timeline.queue.length": 0 },
    "after": { "config.apiKey": "...", "timeline.queue.length": 0 }
  }
}
```

For methods that return a promise, such as `track()`, `identify()`, and `flush()`, the SDK prints the block after the promise resolves. A `track` block therefore appears after the upload attempt.

Confirm three things:

- **The call ran.** A `track` block appears. If not, either your code didn't reach the call or `init()` never ran on the instance you called. A second instance from `createInstance()` needs its own `init()`.
- **The arguments are right.** `args` shows the event name and properties exactly as the SDK received them.
- **The SDK was ready.** Calls made before `init()` completes don't fail. They wait in a queue and run after `init()`, so a call that appears early can still send later.

At this stage you can also read the identity the SDK attaches to every event:

```ts
amplitude.getDeviceId(); // "64b29057-7f8c-..."
amplitude.getUserId(); // "user-12345" or undefined
amplitude.getSessionId(); // 1793919426773
```

## Check how plugins transformed the event

After capture, each event passes through the plugin pipeline in a fixed order:

1. **Before plugins** (`type: "before"`): The built-in context plugin adds `device_id`, `user_id`, `session_id`, `time`, `insert_id`, `library`, `app_version`, and the device fields that [`trackingOptions`](https://amplitude.com/docs/sdks/analytics/react-native/react-native-sdk/reference#trackingoptions) allows, such as `platform`, `os_name`, `device_model`, `language`, and `ip`. On iOS and Android, the SDK's native module supplies the device fields. The network checker pauses uploads while the device is offline.
2. **Enrichment plugins** (`type: "enrichment"`, the default): Your own enrichment plugins run here, in the order you add them.
3. **Destination plugins** (`type: "destination"`): The built-in `amplitude` destination queues the event for upload.

A before or enrichment plugin that returns `null` drops the event. To find the plugin responsible for a change or a drop, set `logLevel` to `Verbose` or `Debug`. The SDK then logs each step:

```text
Amplitude Logger [Log]: Timeline.apply: Event after before plugin '@amplitude/plugin-context-react-native', event: {...}
Amplitude Logger [Log]: Timeline.apply: Final event before destinations, event: {...}
```

The `Final event before destinations` line is the exact event the SDK sends. Compare it with what you expect.

To inspect events in code, for example in an automated test, add a destination plugin that records each event without sending it anywhere:

```ts
const captured: amplitude.Types.Event[] = [];

amplitude.add({
  name: "inspect-events",
  type: "destination",
  execute: async (event) => {
    captured.push({ ...event });
    return { event, code: 200, message: "inspected" };
  },
});
```

Destination plugins receive the event after every before and enrichment plugin has run. Remove the plugin with `amplitude.remove("inspect-events")` when you're done.

## Check what the SDK sent

The SDK uploads events with a `POST` request from JavaScript `fetch`:

| Configuration | Request URL |
| --- | --- |
| Default (US) | `https://api2.amplitude.com/2/httpapi` |
| `serverZone: "EU"` | `https://api.eu.amplitude.com/2/httpapi` |
| `useBatch: true` (US) | `https://api2.amplitude.com/batch` |
| `useBatch: true` (EU) | `https://api.eu.amplitude.com/batch` |
| `serverUrl` set | The URL you set |

Requests leave the device `flushIntervalMillis` (1 second by default) after the first event enters the queue. Each request carries up to `flushQueueSize` (30 by default) events. To send right away, call `await amplitude.flush().promise`.

To inspect the request itself, use one of these tools:

- The **Network** panel in React Native DevTools, which lists `fetch` requests from JavaScript.
- A local HTTP proxy, such as Charles or Proxyman, with the device or simulator configured to use it.
- A custom [`transportProvider`](https://amplitude.com/docs/sdks/analytics/react-native/react-native-sdk/instrument-and-configure#send-events-through-a-proxy) that logs the URL and payload before it sends.
- For react-native-web, the browser **Network** tab.

The request body has this shape:

```json
{
  "api_key": "AMPLITUDE_API_KEY",
  "events": [
    {
      "event_type": "Sign Up Clicked",
      "event_properties": { "plan": "starter" },
      "device_id": "64b29057-7f8c-...",
      "session_id": 1793919426773,
      "time": 1793919426774,
      "event_id": 1,
      "insert_id": "f393d9ad-...",
      "app_version": "1.0.0",
      "platform": "iOS",
      "os_name": "ios",
      "os_version": "17.0",
      "device_manufacturer": "Apple",
      "device_model": "iPhone15,2",
      "language": "en-US",
      "ip": "$remote",
      "library": "amplitude-react-native-ts/1.13.1"
    }
  ],
  "options": {},
  "client_upload_time": "2026-11-05T22:59:41.990Z"
}
```

`"ip": "$remote"` tells Amplitude to use the request's IP address. Set `trackingOptions.ipAddress` to `false` to omit it. When you set `minIdLength`, `options` carries it as `min_id_length`.

> **Note:** Compressed request bodies
>
> For Amplitude's own endpoints, the SDK gzip-compresses larger request bodies only when the JavaScript runtime provides `CompressionStream`. Hermes doesn't, so requests from iOS and Android apps are plain JSON. On react-native-web, compressed requests carry a `Content-Encoding: gzip` header, and the **Network** tab shows binary data. To read those events, use the `Final event before destinations` log line or an inspection plugin instead.

If no request appears:

| Cause | How to confirm | Fix |
| --- | --- | --- |
| The device is offline | When connectivity changes, debug logs show `Network connectivity status: offline.` | Expected. The SDK keeps the events and uploads them when the device reconnects. |
| You set `offline` to `true` | `offline: true` in your init options. | Remove it. To turn off automatic offline detection, set `offline: null` instead. |
| Opt-out is on | Every result is `0`, `Event skipped due to optOut config`. | Call `amplitude.setOptOut(false)`. |
| The flush interval hasn't passed | You set a large `flushIntervalMillis`. | Wait for the interval, or call `amplitude.flush()`. |

## Check what Amplitude received

### Read the upload result

Every tracking method returns `{ promise }`. The promise resolves to a `Result` after the upload attempt finishes:

```ts
const { event, code, message } =
  await amplitude.track("Sign Up Clicked").promise;
```

`event` is the final event as sent. `code` and `message` come from the server response or from the SDK:

| `code` | `message` | Meaning | What the SDK does |
| --- | --- | --- | --- |
| `200` | `Event tracked successfully` | Amplitude accepted the event. | Removes the event from the queue. |
| `400` | Server error text, for example `Invalid API key: ...` | Amplitude rejected the event or the whole request. | Drops events the server marked invalid, and retries the rest. |
| `400` | `Event rejected due to missing API key` | `init()` received an empty API key. | Drops the event. |
| `413` | Server error text, for example `Payload too large` | The request was too large. | Halves `flushQueueSize` and retries. Drops a single event that's still too large. |
| `500` | `Event rejected due to exceeded retry count` | The upload failed `flushMaxRetries` (5) times. Network errors, `429` throttling, and server errors end here. | Drops the event. |
| `0` | `Event skipped due to optOut config` | Opt-out is on. | Never sends the event. |
| `0` | `Event skipped due to empty event type` | The event name is empty. | Never sends the event. |
| `0` | Empty | A before or enrichment plugin returned `null`. | Never sends the event. The SDK logs an error. |

A successful response body looks like this:

```json
{
  "code": 200,
  "events_ingested": 1,
  "payload_size_bytes": 412,
  "server_upload_time": 1793919426800
}
```

A `200` response means Amplitude's ingestion API accepted the event. Amplitude can still exclude an accepted event from analysis later, for example when a [block filter](https://amplitude.com/docs/data/remove-invalid-data) matches it.

> **Note:** Manual flushes don't retry
>
> `amplitude.flush()` sends every queued event once and doesn't retry failures. Each event's promise resolves with the response status and body as the message, for example `invalid: {"error": "Invalid API key: ..."}`. Scheduled uploads retry and use the messages in the table.

### Find the event in Amplitude

1. Open _User Lookup_ and search for the event's `user_id`, or its `device_id` if the user is anonymous.
2. Select the **Activity** tab and turn on **Live event updates**.
3. Open the event and compare its properties with the `Final event before destinations` log line.

For events from many users, use the [Ingestion Debugger](https://amplitude.com/docs/analytics/debug-analytics#ingestion-debugger) to review accepted and rejected requests for the project. Amplitude's chart queries can take a few minutes longer to include new events than User Lookup does.

