On this page

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

Verify Browser SDK 2 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 problems common to every Amplitude SDK, go to SDK troubleshooting and debugging. If behavior changed after an SDK upgrade, check the 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.

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.

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 Chrome extension. It lists each event the page sends, with its properties.

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.

If no request appears:

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:

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 matches it.

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

Was this helpful?