Browser SDK

Send traces to Neatlogs directly from front-end web apps with the zero-dependency browser client.

The browser SDK (neatlogs/browser) is a tiny, zero-dependency client for sending traces straight from a web app. It has only fetch, so it bundles cleanly into front-end code. It POSTs plain JSON to the backend, which generates the trace and span IDs, builds the hierarchy from your nesting and infers cost from model + tokens.

Warning

Use a write key, not your project key. Browser code is public, and a project key can read your traces as well as send them. Use your personal write key (nlw_…) instead: open Settings → Project → API keys, find the HTTP ingest key section, and select Generate key (or Reveal key to copy an existing one). It is ingest-only: it can send traces to /v1/trace but never read data. It is personal and works for any project in your organization, so pass the target project's name as project. If it leaks, select Rotate key.

Install

npm install neatlogs

Quick start

Note

EU projects: add endpoint: 'https://eu.ingest.neatlogs.com' to new Neatlogs({...}). Use the base URL, without /v1/trace.

import { Neatlogs } from 'neatlogs/browser';

const nl = new Neatlogs({
  apiKey: 'nlw_your_write_key',   // ingest-only write key
  project: 'my-web-app',          // your project NAME (required with a write key)
});

// One AI interaction → a one-span LLM trace
await nl.trackAI({
  name: 'chat',
  model: 'gpt-4o',
  input: 'What is the capital of France?',
  output: 'Paris.',
  tokens: { prompt: 12, completion: 3 },
});

That's the whole setup — the call renders as a trace in your dashboard. Telemetry never throws into your app: transport errors go to onError (default console.warn).

Sending a nested trace

Pass a tree of spans via children; the backend wires up the parent/child hierarchy. The root node's name becomes the workflow name.

await nl.trace({
  name: 'support-chat',
  children: [
    { name: 'retrieve', kind: 'RETRIEVER', query: userQuestion, documents: docs },
    { name: 'answer', model: 'gpt-4o', input: prompt, output: reply,
      tokens: { prompt: 240, completion: 60 } },
  ],
});

Streaming: start then finish

When the output arrives incrementally, open the trace up front and complete it when done — nothing is sent until finish():

const t = nl.startTrace({ name: 'chat', model: 'gpt-4o', input: prompt });
// … stream tokens …
await t.finish({ output: fullText, tokens: { prompt: 20, completion: 130 } });

Span fields

Each span node accepts these shortcut fields (all optional except name):

FieldTypeFor
namestringRequired. The span label (root name = workflow name).
kindspan kindOptional — the backend infers it from the fields present when omitted.
input / outputunknownThe step's input and output.
modelstringLLM model name.
tokens{ prompt, completion, total }Token usage.
query / documentsunknownRetriever / reranker query and results.
tool_namestringTool / MCP tool identifier.
passed / scoreboolean / numberGuardrail result.
duration_msnumberLatency (simplest way — end is derived from start + duration_ms).
metadataobjectArbitrary metadata.
attributesobjectEscape hatch — any canonical neatlogs.* attribute (e.g. { "neatlogs.llm.temperature": 0.7 }). Explicit attributes win over the shortcut fields.
childrenspan[]Nested spans.
logs{ level, message, timestamp }[]Structured logs on the span.
endUserIdstringRoot only. The end-user this conversation belongs to (overrides the client default). One end-user per session — stamp the same id on every turn's trace root.
endUserMetadataobjectRoot only. Arbitrary end-user fields (e.g. { plan: 'pro' }), overrides the client default.
sessionIdstringRoot only. The session (conversation) this trace belongs to (overrides the client default). A one-shot run is one trace; a multi-turn conversation is one trace per turn, all sharing the same sessionId.

Constructor options

OptionTypeDefaultDescription
apiKeystring—Required. Your write key (nlw_…), not a project API key.
projectstring—Required. Target project name.
endpointstringhttps://ingest.neatlogs.comBackend base URL; set to https://eu.ingest.neatlogs.com for EU projects.
enabledbooleantrueSet false to validate calls without sending.
onError(err) => voidconsole.warnCalled on transport errors instead of throwing.
endUserIdstring—Default end-user (the person using your app) for every trace. Overridden by a per-call endUserId.
endUserMetadataobject—Default arbitrary end-user fields (e.g. { plan: 'pro' }).
sessionIdstring—Default session (conversation) for every trace. Overridden by a per-call sessionId. Set it per conversation.

Sessions & end-users

Identify who each trace belongs to and which conversation it's part of. Set defaults on the client, or override per call — only the trace root carries them.

// A chatbot: one Neatlogs client per conversation, tagged with the user + session.
const nl = new Neatlogs({
  apiKey: 'nlw_...',
  project: 'my-app',
  endUserId: 'u_812',              // who — the person in this conversation
  endUserMetadata: { plan: 'pro' },
  sessionId: conversationId,        // which conversation — the same id for every turn
});

// Each turn is its own trace; all turns share `sessionId`, so the dashboard groups
// them into one multi-turn conversation.
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input: turn1, output: reply1 });
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input: turn2, output: reply2 });

// Or per call (overrides the client defaults):
await nl.trackAI({
  name: 'chat', model: 'gpt-4o', input, output,
  endUserId: 'u_999', sessionId: 'conv_42',
});

The browser SDK uses the same field names as the Node/Python SDKs (endUserId, endUserMetadata, sessionId). An end-user is per session — a session (conversation) belongs to one person; stamp the same endUserId on every turn. See Sessions and End-user identity for the full model and how to filter by them.

Note

Under the hood the browser SDK calls the same POST /v1/trace endpoint documented in the HTTP API — use that directly from any language or runtime that isn't JavaScript.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.