End-User Identity
Attach your app's end-user to every trace, then filter and analyze traces by user.
The end-user is the user of your application — the person interacting with the AI product you built. Attaching it to a trace lets you filter the traces page by user and (soon) analyze usage per user.
End-user is not the same as user_id. user_id identifies the operator running the SDK (a developer, a service account). end_user_id identifies your application's user. They are stored separately and never mix.
The model: one end-user per trace
A trace belongs to exactly one end-user — there is no per-span override. End-user is effectively session-level: a multi-turn chat is one session with many traces, all the same person; a plain workflow is a single trace. Set it once on the trace and Neatlogs rolls it up to the session.
You set it on the trace root — the WORKFLOW span or the top-level trace() you open at your request entry point. Setting it on a nested child span has no effect (the root owns the identity).
Multi-tenant server (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:
@app.post("/chat")
def chat(req):
with neatlogs.trace(
"chat",
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 end-user automaticallyFor a multi-turn chat, also set a session (session_id or auto_session) so the per-turn traces group together; set the same end_user_id on each turn's root.
Decorated workflow root
When your entry point is a decorated function, set it on the decorator:
@neatlogs.span(kind="WORKFLOW", end_user_id=str(request.user.id))
def handle_request(request):
...Single-user process (CLI / per-user worker)
If one process serves exactly one user, set it once on init() — it lands on every trace's root automatically (including auto-instrumented wrap() roots):
neatlogs.init(
api_key=os.environ["NEATLOGS_API_KEY"],
workflow_name="batch-job",
end_user_id="u_812",
)init() runs once per process, so its end_user_id is a process-global default. On a shared multi-tenant server, set the end-user per request via trace(...) / @span(...) instead — that value takes precedence over the init() default.
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',
endUser: 'u_812', // default for every trace
endUserMetadata: { plan: 'pro' },
});
// or per call (overrides the client default):
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output, endUser: 'u_999' });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
| Parameter | Python | TypeScript | Where |
|---|---|---|---|
| End-user id | end_user_id | endUserId | init(), trace(), @span/span() |
| End-user metadata | end_user_metadata (dict) | endUserMetadata (object) | same |
Related: Sessions · Tags · PII Redaction