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. Create a write key (nlw_…) from your account settings in Neatlogs — an ingest-only, per-user credential safe to embed client-side (it can send traces but never read data) — and pass your project name as project.

Install

npm install neatlogs

Quick start

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
apiKeystringRequired. Your write key (nlw_…) or project key.
projectstringProject name — required when using a write key.
endpointstringhttps://ingest.neatlogs.comBackend base URL (self-hosted/custom only).
enabledbooleantrueSet false to validate calls without sending.
onError(err) => voidconsole.warnCalled on transport errors instead of throwing.
endUserIdstringDefault end-user (the person using your app) for every trace. Overridden by a per-call endUserId.
endUserMetadataobjectDefault arbitrary end-user fields (e.g. { plan: 'pro' }).
sessionIdstringDefault 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.