---
title: Verify Node.js SDK events
description: "Trace a Node.js SDK event through each stage: what the SDK captured, how plugins transformed it, what it sent to Amplitude, and what Amplitude received."
product: analytics
lang: en
last_updated: 2026-11-10
token_estimate: 2438
---
# Verify Node.js 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 on standard output |
| [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 process, and with what? | A local capture server |
| [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 [Node.js SDK release notes](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk-release-notes).

## Turn on debug logging

Set `logLevel` to `Debug` while you verify. Debug logs include every public method call, the SDK's state before and after it, each event as plugins change it, and the upload result.

```js
import { init, track, Types } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", {
  logLevel: Types.LogLevel.Debug,
});

track("Sign Up Completed", { plan: "starter" }, { user_id: "user-12345" });
```

The levels are `0` None, `1` Error, `2` Warn (default), `3` Verbose, and `4` Debug. Each message starts with `Amplitude Logger`. The default logger writes errors with `console.error`, warnings with `console.warn`, and Verbose and Debug messages with `console.log`. Filter both standard output and standard error on that prefix.

> **Warning:** Debug logs contain your API key
>
> At the `Debug` level, the SDK logs its configuration, including the API key, and every event payload. Return `logLevel` to `Warn` before you deploy, and don't ship Debug logs to a shared log store.

## 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 Completed", { "plan": "starter" }, { "user_id": "user-12345" }],
  "stacktrace": ["at file:///srv/app/signup.mjs:6:7", "..."],
  "time": { "start": "2026-11-10T19:47:54.361Z", "end": "2026-11-10T19:48:04.383Z" },
  "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 10 seconds after the call.

Check the block for these problems:

| What you see | What it means |
| --- | --- |
| No `track` block | The call never ran, or the upload never finished. Check that the code path runs, and that the process stays alive until the next upload. |
| No `user_id` or `device_id` in `args` | Amplitude rejects the event. Pass one of them in the third argument. |
| `Event skipped due to optOut config` before it | Opt-out is on for this instance. Go to [Opt out of tracking](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/instrument-and-configure#opt-out-of-tracking). |
| `Event skipped due to empty event type` before it | The event name is empty or not a string. |

## Check how plugins transformed the event

At `Verbose` or `Debug` level, the SDK logs the event after each `before` and `enrichment` plugin, then the final event it hands to destinations:

```text
Amplitude Logger [Log]: Timeline.apply: Event after before plugin 'context', event: {"time":1794340064240,"insert_id":"9dfa11d7-...","event_type":"Sign Up Completed","user_id":"user-12345",...}
Amplitude Logger [Log]: Timeline.apply: Final event before destinations, event: {...}
```

A plugin that returns `null` drops the event, and the log shows `Event filtered out by` with the plugin's name. The built-in `context` plugin adds `time`, `insert_id`, `event_id`, and `library` to every event.

To inspect events in code, add an enrichment plugin after `init()`. Enrichment plugins run after the built-in `context` plugin, so your plugin sees the fields the SDK added:

```js
import { init, add, track } from "@amplitude/analytics-node";

await init("AMPLITUDE_API_KEY").promise;

add({
  name: "inspect-events",
  type: "enrichment",
  execute: async (event) => {
    console.log("Amplitude event:", JSON.stringify(event));
    return event;
  },
});

track("Sign Up Completed", { plan: "starter" }, { user_id: "user-12345" });
```

The plugin prints the event as the SDK sends it:

```json
{
  "event_type": "Sign Up Completed",
  "user_id": "user-12345",
  "event_properties": { "plan": "starter" },
  "time": 1794340064240,
  "insert_id": "9dfa11d7-2592-47b5-ba83-a599db8f8536",
  "event_id": 0,
  "library": "amplitude-node-ts/1.5.83"
}
```

`event_id` is a counter that starts at `0` for each SDK instance. The SDK overwrites any `event_id` or `library` value you pass. Remove the inspection plugin when you finish, so event payloads don't reach your production logs.

## Check what the SDK sent

A server process has no _Network_ tab. To see the exact request body, point `serverUrl` at a local server that prints each request and answers like Amplitude:

```js
// capture-server.mjs: run with node capture-server.mjs
import http from "node:http";

http
  .createServer((req, res) => {
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      console.log(req.method, req.url, body);
      const events = JSON.parse(body).events ?? [];
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          code: 200,
          events_ingested: events.length,
          payload_size_bytes: body.length,
          server_upload_time: Date.now(),
        }),
      );
    });
  })
  .listen(8080);
```

Then send events to it:

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY", {
  serverUrl: "http://localhost:8080/2/httpapi",
});

track("Sign Up Completed", { plan: "starter" }, { user_id: "user-12345" });
```

The server prints one `POST /2/httpapi` request per upload, with a body like this:

```json
{
  "api_key": "AMPLITUDE_API_KEY",
  "events": [
    {
      "time": 1794340064240,
      "insert_id": "9dfa11d7-2592-47b5-ba83-a599db8f8536",
      "event_type": "Sign Up Completed",
      "user_id": "user-12345",
      "event_properties": { "plan": "starter" },
      "event_id": 0,
      "library": "amplitude-node-ts/1.5.83"
    }
  ],
  "options": {},
  "client_upload_time": "2026-11-10T19:48:04.374Z"
}
```

The SDK sends plain JSON. It doesn't compress request bodies. `options.min_id_length` appears only when you set `minIdLength`. Remove `serverUrl` when you finish, so events go to Amplitude again.

## Check what Amplitude received

### Read the upload result

`track()`, `identify()`, `groupIdentify()`, `setGroup()`, and `revenue()` return an object whose `promise` resolves to a result with `event`, `code`, and `message`:

```js
import { init, track } from "@amplitude/analytics-node";

init("AMPLITUDE_API_KEY");

const result = await track(
  "Sign Up Completed",
  { plan: "starter" },
  { user_id: "user-12345" },
).promise;

console.log(result.code, result.message);
```

| `code` | `message` | Meaning |
| --- | --- | --- |
| `200` | `Event tracked successfully` | Amplitude accepted the event on a scheduled upload. |
| `200` | `success: {...}`, with the server's JSON response | Amplitude accepted the event on an upload that `flush()` started. |
| `0` | `Event skipped due to optOut config` | Opt-out is on. The SDK sent nothing. |
| `0` | `Event skipped due to empty event type` | The event name is empty. The SDK sent nothing. Requires 1.5.70 or later. |
| `0` | Empty string | A `before` or `enrichment` plugin returned `null` and dropped the event. |
| `0` | `Unexpected error occurred` | The request never reached a server. The SDK doesn't retry these. |
| `100` | `Event not tracked, no destination plugins on the instance` | No destination ran. Usually `flush()` ran before `init()` finished. |
| `400` | `Event rejected due to missing API key` | `init()` received an empty key. |
| `400` | The server's error, such as `Request missing required field` | Amplitude rejected this event. The SDK retries other events in the request. |
| `413` | The server's error | A single event is larger than Amplitude accepts. |
| `429` | The server's error | The user or device exceeded Amplitude's daily event quota. |
| `500` | `Event rejected due to exceeded retry count` | Every attempt failed. The SDK made `flushMaxRetries` attempts. |

For how the SDK retries each status, go to [How the SDK uploads events](https://amplitude.com/docs/sdks/analytics/node/node-js-sdk/instrument-and-configure#how-the-sdk-uploads-events).

### Find the event in Amplitude

1. In Amplitude, open **User Lookup** and search for the `user_id` or `device_id` you sent.
2. Open the **Activity** tab and turn on **Live event updates**.
3. Find the event by name, and confirm its properties.

If the upload result is `200` but the event doesn't appear, check these settings:

- The API key belongs to the project you're viewing.
- The project's data center matches `serverZone`.

