End-User Identity
Attach your app's end-user to every trace to analyze usage, cost, and errors by customer and segment.
The end-user is the user of your application — the person interacting with the AI product you built. Attaching it to every trace turns raw telemetry into customer analytics: you can slice usage, cost, latency, and errors by user and by segment (plan tier, region, cohort), see who your power users are versus who's churning, spot the one customer a bug actually affects, and pull up a specific person's history when they reach out to support. Anonymous traces answer "is the agent working?"; end-user identity answers "for whom, at what cost, and are they coming back?"
End-user is not the operator user_id. user_id (set on init()) identifies whoever runs the SDK — a developer, a service account. end_user_id identifies your application's user.
The model: one end-user per session
End-user is session-level — a session belongs to one end-user. A multi-turn chat is one session with many traces, all the same person; a single workflow run is a session of one trace (where session_id = trace_id). Either way, every trace in the session carries the same end-user.
You declare it on the trace root of each turn — the WORKFLOW decorator or the top-level trace() — in the same place, and the same way, you set the session_id. Pass the same end_user_id on every turn of a conversation. Setting it on a nested child span has no effect; the root owns the identity, and Neatlogs rolls it up to the trace and its session.
On the trace root (the common case)
On a server that handles many users, read the id from your per-request context and set it on the root you open in the handler — never hardcode it. Set the session here too:
@app.post("/chat")
def chat(req):
with neatlogs.trace(
"chat",
session_id=f"conv_{req.conversation_id}",
end_user_id=str(req.user.id),
end_user_metadata={"plan": req.user.plan}, # optional, arbitrary fields
):
return run_agent(req.message) # child spans inherit the trace's identityDecorated workflow root
When your entry point is a decorated function, set it on the decorator:
@neatlogs.span(kind="WORKFLOW", session_id=f"conv_{cid}", end_user_id=str(user_id))
def handle_request(request):
...Wrapper-only code: identify()
If you only use a wrapper (neatlogs.wrap(...) / WrapGenAI) and don't open a root yourself, bind the end-user (and session) with identify() — the wrapper's auto-root picks it up:
client = neatlogs.wrap(OpenAI())
with neatlogs.identify(
session_id=f"conv_{cid}",
end_user_id=str(user_id),
end_user_metadata={"plan": "pro"},
):
client.chat.completions.create(...) # auto-root carries the end-userEnd-user is per-request, so it is not set on init(). On a shared server, a process-global default would tag every user's traces with the same id. Always set it at the request boundary — on the trace root, or via identify() for wrapper-only code.
Browser SDK
In neatlogs/browser, set the end-user once on the client, or per call:
const nl = new Neatlogs({
apiKey: 'nlw_...',
project: 'my-app',
endUserId: 'u_812', // default for every trace
endUserMetadata: { plan: 'pro' },
sessionId: conversationId, // pair the end-user with their session
});
// or per call (overrides the client default):
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output, endUserId: 'u_999' });The browser SDK uses the same field names as the Node/Python SDKs — endUserId, endUserMetadata, sessionId. It also takes a first-class sessionId (client default or per call) — see Sessions.
Filtering by end-user
Once traces carry an end-user, open the Filters on the traces page and add the End-user filter — is, contains, starts with, etc. — exactly like the Workflow filter. Custom fields in end_user_metadata are filterable via the metadata: filter (e.g. metadata:plan is pro).
Reference
| Python | TypeScript | Go | |
|---|---|---|---|
| On the root | trace(end_user_id=, end_user_metadata=) · @span(...) | trace({endUserId, endUserMetadata}) · span({...}) | Identify(ctx, IdentifyOptions{EndUserID, EndUserMetadata}) then Trace(ctx, name) |
| Wrapper-only | with identify(end_user_id=, end_user_metadata=) | identify({endUserId, endUserMetadata}, fn) | Identify(ctx, IdentifyOptions{...}) (same) |
| Canonical attributes | neatlogs.end_user.id · neatlogs.end_user.metadata (root span) | same | same |
Related: Sessions · Tags · PII Redaction
