---
title: Verify Browser SDK 2 events
description: "Trace a Browser SDK 2 event through each stage: what the SDK captured, how plugins transformed it, what it sent over the network, and what Amplitude received."
product: analytics
lang: en
last_updated: 2026-10-28
token_estimate: 3106
---
# Verify Browser SDK 2 events

> 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`.

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.

| Stage | Question | Where to look |
| --- | --- | --- |
| [Captured](#check-what-the-sdk-captured) | Did the SDK accept the call? | Debug logs in the browser console |
| [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 browser, and with what payload? | The browser **Network** tab |
| [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 [Browser SDK 2 release notes](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2-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, the final event, and the server response.

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

In environments where you can't import the enum, such as Google Tag Manager, use the number `4`. The levels are `0` None, `1` Error, `2` Warn (default), `3` Verbose, and `4` Debug. Each message starts with `Amplitude Logger`, so filter the console on that prefix.

> **Warning:** Use numbers in Google Tag Manager
>
> Don't pass a string such as `"LogLevel.None"`. The SDK compares `logLevel` as a number, so a non-numeric string turns on every level, including Debug.

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 handleClick (app.js:42:13)", "..."],
  "time": { "start": "2026-10-26T12:00:11.402Z", "end": "2026-10-26T12:00:12.611Z" },
  "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, about one second after the call.

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()`. To confirm when the page exposes the `amplitude` global (the script loader does), run `amplitude.init("YOUR_API_KEY")` in the browser console. If queued events then send, your own `init()` call didn't run, or it ran on a different instance.
- **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 in the log can still send later.

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

```ts
amplitude.getDeviceId(); // "f3b6c1e2-..."
amplitude.getUserId(); // "user-12345" or undefined
amplitude.getSessionId(); // 1761480000000
amplitude.getOptOut(); // false
```

If `getOptOut()` returns `true`, the SDK drops every event before any plugin runs. `getOptOut()` requires Browser SDK 2.18.0 or later.

## 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`, `event_id`, `insert_id`, `library`, `user_agent`, `platform`, `language`, and `ip`. The network checker pauses uploads while the browser is offline.
2. **Enrichment plugins** (`type: "enrichment"`, the default): Autocapture plugins add properties here. For example, page URL enrichment adds `[Amplitude] Page URL` and `[Amplitude] Previous Page Location`, and attribution adds campaign parameters. Your own enrichment plugins also 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 enrichment plugin 'my-plugin', event: {...}
Amplitude Logger [Log]: Timeline.apply: Event filtered out by enrichment plugin 'my-plugin', 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(structuredClone(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. Open the browser **Network** tab and filter on the host for your data center.

| 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 browser `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`.

The request body has this shape:

```json
{
  "api_key": "YOUR_API_KEY",
  "events": [
    {
      "event_type": "Sign Up Clicked",
      "event_properties": { "plan": "starter" },
      "device_id": "f3b6c1e2-...",
      "session_id": 1761480000000,
      "time": 1761480012345,
      "event_id": 4,
      "insert_id": "9c0e...",
      "platform": "Web",
      "language": "en-US",
      "ip": "$remote",
      "library": "amplitude-ts/2.48.1",
      "user_agent": "Mozilla/5.0 ..."
    }
  ],
  "options": {},
  "client_upload_time": "2026-10-26T12:00:12.400Z"
}
```

`"ip": "$remote"` tells Amplitude to use the request's IP address. Set `trackingOptions.ipAddress` to `false` to omit it.

To watch events without opening DevTools, install the [Amplitude Instrumentation Explorer](https://amplitude.com/docs/data/chrome-extension-debug) Chrome extension. It lists each event the page sends, with its properties.

> **Note:** Compressed request bodies
>
> For Amplitude's own endpoints, the SDK gzip-compresses request bodies of 2 KB or more when the browser supports `CompressionStream`. Those requests carry a `Content-Encoding: gzip` header, and the **Payload** tab shows binary data. To read the events, use the `Final event before destinations` log line or an inspection plugin instead. This [default changed in 2.42.0](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2-release-notes#2420).

If no request appears:

| Cause | How to confirm | Fix |
| --- | --- | --- |
| The browser is offline | `navigator.onLine` is `false`. Debug logs show `Skipping flush while offline.` | The SDK uploads automatically when the browser reconnects. |
| An ad blocker or privacy extension blocks the request | The request shows `(blocked:other)` or `net::ERR_BLOCKED_BY_CLIENT` in Chrome. Firefox names the blocking extension in the **Transferred** column. Safari doesn't list the request. | Route events through a [proxy](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2/usage-guide#send-events-through-a-proxy). |
| A Content Security Policy blocks the request | The console shows `Refused to connect to ...`. | Add `https://*.amplitude.com` to `connect-src`. |
| The page unloaded before the request finished | The request shows `(canceled)`. | Go to [Send events when the page unloads](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2/usage-guide#send-events-when-the-page-unloads). |
| `transport` is `"beacon"` | The request has type `ping`, and the response is unavailable. | Expected. `sendBeacon` doesn't expose responses. Switch to `"fetch"` while you verify. |

## 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 | The request was too large. | Halves `flushQueueSize` and retries. Drops a single event that's still too large. |
| `429` | Server error text | The user or device exceeded a rate limit. | Drops events for users or devices over their daily quota. Retries throttled events after 30 seconds. |
| `500` | `Event rejected due to exceeded retry count` | The upload failed `flushMaxRetries` (5) times. Network errors, blocked requests, 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. |
| `100` | `Event not tracked, no destination plugins on the instance` | You removed the `amplitude` destination. | Never sends the event. |

A successful response body looks like this:

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

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 `success: {"code": 200, ...}` or `invalid: {...}`. 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.

