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 automatically

For 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

ParameterPythonTypeScriptWhere
End-user idend_user_idendUserIdinit(), trace(), @span/span()
End-user metadataend_user_metadata (dict)endUserMetadata (object)same

Related: Sessions · Tags · PII Redaction

On this page