Set up Agent Analytics
This feature is in Early Access. During this time, aspects of the functionality may still be developed, and this documentation may not always be up to date. If you have any questions, contact Amplitude Support.
The timeline below shows what your instrumentation produces. Click any event to inspect its shape.
| [Agent] Session ID | 4ddcc6b2-1041-432a-aa8c-ebe3eccac40b |
| [Agent] Agent ID | support-chatbot |
| [Agent] Trace ID | b4f63d43-d752-4b1f-8489-d234ddf586b2 |
| $llm_message.text | I can help. Your subscription renews on Aug 15… |
| [Agent] Model Name | gpt-4o-mini |
| [Agent] Provider | openai |
| [Agent] Input Tokens | 1245 |
| [Agent] Output Tokens | 87 |
| [Agent] Latency Ms | 3420 |
| [Agent] Cost USD | 0.0012 |
s.trackAiMessage(...) or a provider wrapper.How setup works
You must have previously completed the following prerequisites before data shows up in Amplitude:
- You've selected which Amplitude project the data will flow into and you have access to the corresponding project API Key.
- You've instrumented your code. The Amplitude AI SDK (Node and Python) covers most stacks. Runtimes the SDK doesn't bundle into can send events directly to the HTTP API. These runtimes include Cloudflare Workers, other edges, and unsupported languages such as Java, Go, and Ruby.
- You've decided what leaves your app. Privacy mode controls whether prompt and response text reaches Amplitude.
Choose an instrumentation path
| Path | Effort | What you get | Use when |
|---|---|---|---|
| Setup agent (recommended) | Minutes | Full instrumentation: wrappers, sessions, identity, verification test | Any codebase you can modify |
| Manual Instrumentation | Varies | Same as above, at your own pace | You want full control |
| HTTP API | Varies | Send [Agent] events directly from any stack | The AI SDK doesn't fit your runtime |
Whatever the path, the goal is the same: agents plus sessions plus wrapped providers. That combination is what unlocks per-user analytics, enrichment, and quality measurement.
Define your sessions
An agent session is one unit of work with an outcome. Pass the ID your application already has:
| Agent type | Session ID to use |
|---|---|
| Chatbot or copilot | Thread or conversation ID |
| Support agent | Ticket ID |
| Coding agent | Task or ticket ID |
| Voice agent | Call ID |
| Background or autonomous agent | Run or job ID |
New goal, new session: when the user starts a different job, use a new session ID.
How sessions close
This is the most important lifecycle concept in Agent Analytics, because enrichment only runs on closed sessions. If your [Agent] Session Record events aren't appearing, start here.
A session closes one of two ways.
Explicit close (recommended): Call
trackSessionEnd()(Node) ortrack_session_end()(Python) when the unit of work finishes: ticket resolved, call ended, run completed. Sessions opened with a scope helper close automatically when that scope exits —run()in Node, thesession()context manager in Python. The close is written when the next ingestion batch processes the Session End event, so in the typical case expect the[Agent] Session Recordevent within about 15 to 20 minutes; it can take longer while an org has an enrichment backlog.Closing without the AI SDK: The server closes a session on the
[Agent] Session Endevent itself, so you can send that event directly from any Amplitude SDK or the HTTP API. Set the event type to[Agent] Session End, include an[Agent] Session IDproperty matching the session you're closing, and send it to the project where Agent Analytics is enabled.json{ "event_type": "[Agent] Session End", "user_id": "user-123", "event_properties": { "[Agent] Session ID": "session-abc" } }This closes the session exactly as an SDK-emitted close does. Any additional properties of your own are stored with the event but don't affect closing.
Idle timeout (automatic fallback): If you never close explicitly, the server closes the session after 30 minutes of inactivity by default, measured from the last agent event received for the session. Any of these six
[Agent]events resets the clock: User Message, AI Response, Tool Call, Session End, Score, and Span. Server-generated enrichment events, including Session Record and Evaluator Result, do not. Ingestion runs on a cycle, so the close lands within roughly 15 minutes after the idle window elapses.
What closing does and doesn't do. Closing marks completion; it doesn't lock the session. Later events are still accepted and stored, but enrichment runs once, so late turns never reach the session's signals, rollups, or Session Record. Only the first close counts: duplicates are ignored, and an [Agent] Session End event arriving after the idle timeout already closed the session is ignored too. The recommendation is to close when the job is genuinely done; if real work continues, start a new session.
Choosing a timeout: On top of the idle window, sessions without a per-session override also close after 24 hours regardless of activity. Setting any override — a positive value or -1 — exempts the session from that cap.
| Value | Behavior |
|---|---|
30 | This is also the default setting. Session closes after 30 minutes of inactivity |
240, etc. | Raise the timeout for jobs that wait on humans, such as support tickets, coding agents. |
-1 | Idle window becomes 90 days and the 24-hour cap is waived, so an explicit Session End is effectively the only close. |
Set the timeout too low and a long pause closes the session early: follow-up events still attach and are stored, but they land after enrichment has run, so they never reach the Session Record. Reach for -1 when quiet stretches are normal and unbounded, such as a ticket idle over a weekend, a long-running background job, and any fixed timeout would eventually split a real job. The tradeoff is that a session your app forgets to close stays open and unenriched until the 90-day backstop closes it.
Two caveats on -1:
- Agents with enrichment disabled close on the org default schedule
- The 90-day limit counts from when the session started, so a session that stays active for more than 90 days straight never auto-closes at all.
Where to set the override: Set idleTimeoutMinutes (Node) or idle_timeout_minutes (Python) when you open the session, and also include an idle_timeout_minutes key in the agent's context. Today the context route is what reliably reaches the server for sessions that never close explicitly; setting both guarantees the override takes effect and keeps working after the server-side fix ships.
// Node
const agent = ai.agent('support-bot', {
context: { idle_timeout_minutes: 240 }, // reliable today
});
const session = agent.session({
userId,
sessionId: ticketId,
idleTimeoutMinutes: 240, // the intended parameter
});
Checking which path ran: Every Session Record carries [Agent] Close Reason, either explicit_close or timeout, so you can monitor how often the fallback is doing the work.
Choose a privacy mode
Three content modes control what leaves your infrastructure. Set the mode once in SDK config.
| full (default) | metadata_only | customer_enriched | |
|---|---|---|---|
Message content ($llm_message.text) | Sent | Not sent | Not sent |
| System prompt | Sent | Not sent | Not sent |
| Tool inputs and outputs | Sent | Not sent | Not sent |
| Score comments | Sent | Not sent | Not sent |
| Tokens, cost, latency, models, session grouping | Yes | Yes | Yes |
| Amplitude enrichment (signals, evaluators) | Runs | Limited | Runs |
Your own enrichment through trackSessionEnrichment() | Available | Available | Available |
Notes:
- In
fullmode, the system prompt is an optional field; you can omit it. - In
customer_enriched, Amplitude enrichment runs on the labels you provide. - Amplitude-generated enrichments (signals, topics, custom evaluators) run in every mode. The difference is what it can see. In
metadata_only, expect content-dependent signals such as task completion and response quality to carry low information, while structural signals (errors, friction patterns from behavior, cost, latency) remain useful. customer_enrichedis not exclusive. You can send Session Enrichment events in any mode.customer_enrichedis simply the mode where your enrichment is the only session-level quality data, because no content is available for Amplitude's signals.- In
fullmode, PII redaction is on by default and scrubs emails, phone numbers, SSNs, credit cards, and IP addresses before events leave your process. Built-in phone and SSN detection is tuned for US formats; add custom regex patterns or plug in your own redaction (for example, Presidio) for international locales or domain-specific identifiers. - Regulated environments (healthcare, finance) typically run
metadata_onlyorcustomer_enriched. Decide the mode before wide rollout; it determines what your legal review needs to cover.
Setting user IDs
Amplitude counts unique users by combining three identifiers (device ID, user ID, and Amplitude ID) into a single profile across anonymous sessions, sign-ins, and multiple devices. Send a stable user ID as soon as a person authenticates so anonymous events on the same device merge into one profile and your active-user counts stay accurate.Every [Agent] event will carry your stable user ID and unlocks cross-domain funnels, cohorts, and retention chart analyses.
Two rules keep a single user from splitting into two:
- Never pass a placeholder user ID such as "anonymous", an empty string, or a temporary ID. A user ID can't be changed once set, so a placeholder creates a permanent separate user that won't merge later. Omit the userId instead; anonymous activity merges into the known identity through standard identity resolution once the user identifies.
- Reuse the same device ID across a pre-account session. If your backend generates a new device ID per request, the user identity merge breaks. If possible, read the device ID from the Browser SDK and forward it.
Install the SDK
Instrument this app with @amplitude/ai. Follow node_modules/@amplitude/ai/amplitude-ai.md
Paste this prompt into your AI coding agent (Cursor, Claude Code, Windsurf, GitHub Copilot, or Codex). The agent reads the linked instructions file, scans your codebase, finds every LLM call site and the session lifecycle, and instruments them.
Manual Instrumentation
Wrap your LLM client, name an agent, and run each conversation inside a session. That pattern produces every event type the product expects.
import { AmplitudeAI, OpenAI } from '@amplitude/ai';
const ai = new AmplitudeAI({ apiKey: process.env.AMPLITUDE_AI_API_KEY });
const openai = new OpenAI({ amplitude: ai, apiKey: process.env.OPENAI_API_KEY });
const agent = ai.agent('my-agent');
const session = agent.session({ userId, sessionId });
await session.run(async (s) => {
s.trackUserMessage(message);
return openai.chat.completions.create({ model, messages });
});
await ai.flush();
Send agent events without the AI SDK
The AI SDK is the lowest-friction path, but it is not required, and it is Node.js and Python only. If your stack can't run it, emit the [Agent] taxonomy directly with a standard Amplitude SDK or the HTTP API. Common cases: browsers and client-heavy SPAs, edge runtimes (Cloudflare Workers, Deno), and AI app builders such as Lovable, Superblocks, v0, or Bolt, where the generated app is browser-first and importing @amplitude/ai fails on Node-only modules.
This section is the canonical wire contract for that path. Without the SDK, three responsibilities shift to you: session lifecycle (you emit Session End), privacy (you redact before tracking), and ID discipline (you generate and thread the IDs below).
The event contract
Emit these events with track() from any standard Amplitude SDK, or via the HTTP API.
Minimum viable:
[Agent] User Message[Agent] AI Response[Agent] Session End- Optional: add
[Agent] Tool Callwhen your agent uses tools.
Every event carries the shared event properties: [Agent] Session ID, [Agent] Turn ID, [Agent] Trace ID, [Agent] Agent ID, plus [Agent] Env, [Agent] Runtime (for example browser), and optionally [Agent] Context. Send the same user ID you use for product analytics.
| Event | Required beyond the envelope | Notes |
|---|---|---|
[Agent] User Message | [Agent] Message ID, $llm_message: { text } when your privacy posture sends content | Emit at request start. |
[Agent] Tool Call | [Agent] Invocation ID, [Agent] Tool Name, [Agent] Tool Success, [Agent] Latency Ms, [Agent] Parent Message ID (the triggering user message) | One event per invocation, emitted in execution order, before the AI Response. Tool payloads go in [Agent] Tool Input / [Agent] Tool Output, redacted. No token properties here. |
[Agent] AI Response | [Agent] Message ID, [Agent] Model Name, [Agent] Provider, [Agent] Latency Ms, token counts, [Agent] Cost USD if you compute it, $llm_message: { text } when sending content | System prompt is a property ([Agent] System Prompt) on this event, not a separate event. Token properties belong only here. |
[Agent] Session End | Optional: [Agent] Output State, [Agent] Abandonment Turn, [Agent] Session Idle Timeout Minutes | Emit when the job genuinely ends. Use these taxonomy properties only; don't invent close-reason or duration properties, the server derives [Agent] Close Reason and rollups on the Session Record. |
[Agent] Score | Refer to thumbs feedback | Same contract with or without the SDK. |
Rules that hand-rolled instrumentation gets wrong most often:
$llm_messagemust be an object:{ text: "..." }. A plain string is silently ignored and the thread view renders no message content.- Turn ID identifies the exchange, not the event. Increment it once per user-message round trip and stamp the same value on the user message, every tool call, and the AI response of that exchange. Ordering within a turn comes from event time, so emit tool calls before the AI Response in real execution order. Generate one
[Agent] Trace IDper round trip and share it across the same events. - Stay inside the taxonomy. Don't invent event types or properties under the
[Agent]prefix; unregistered ones may not be queryable in charts. For business actions the agent performs (a purchase, a booking, a recommendation click), emit your standard product events with your existing names and properties, carrying the sameuser_id; that keeps agent-driven and click-driven journeys comparable in one funnel. - Token and cost properties live only on AI Response. Tool calls don't consume tokens; the deciding call and the follow-up call are both part of the AI Response. Don't place
[Agent] Cost USDon other event types: the server sums that property from any event it ingests, so a stray cost on a Tool Call or Session End silently inflates the session total.
Session lifecycle without the SDK
The server closes any idle session after 30 minutes automatically, so you have a safety net even if you never emit Session End. Emit [Agent] Session End when the job genuinely ends (chat closed, ticket resolved, run completed), and let the server timeout catch abandonment.
Setting the idle override without the SDK: include an idle_timeout_minutes key inside the [Agent] Context JSON on your events (any event works; first value wins), and mirror it as [Agent] Session Idle Timeout Minutes on your Session End if you send one. The Context route is what the server reliably reads today. For example, on the first user message of the session:
amplitude.track('[Agent] User Message', {
'[Agent] Session ID': ticketId,
'[Agent] Turn ID': 1,
'[Agent] Trace ID': traceId,
'[Agent] Agent ID': 'support-bot',
'[Agent] Message ID': messageId,
'[Agent] Context': JSON.stringify({ idle_timeout_minutes: 240 }),
$llm_message: { text: redactedText },
});
// ...and if you emit an explicit close when the ticket resolves:
amplitude.track('[Agent] Session End', {
'[Agent] Session ID': ticketId,
'[Agent] Agent ID': 'support-bot',
'[Agent] Session Idle Timeout Minutes': 240,
});
[Agent] Session Idle Timeout Minutes and keep the window generous. Refer to the FAQ for the two-clocks explanation.Privacy without the SDK
You own redaction, and it must run before track(). The most common mistake is gating message content while forgetting the other content-bearing properties. Gate all four consistently with your chosen posture:
- [ ]
$llm_message.texton User Message and AI Response - [ ]
[Agent] System Prompton AI Response - [ ]
[Agent] Tool Inputand[Agent] Tool Outputon Tool Call (redact recursively; payloads are objects) - [ ]
[Agent] Commenton Score
For metadata_only behavior, omit all four entirely; you still get cost, latency, tokens, and session grouping. For full behavior, redact PII (emails, phones, SSNs, cards, IPs) before tracking.
Browser and edge notes
- Identity: a persistent anonymous UUID in localStorage as
user_idgives anonymous visitors stable identity; standard identity resolution merges them when they sign in. - Session Replay: with the replay plugin active, stamp the replay properties onto every agent event so sessions link to recordings.
- Do not set the beacon transport globally. Calling
setTransport('beacon'), for example inside a session-end handler, applies permanently to the whole SDK: every later event fires fire-and-forget with no retry and drops silently. Use the default transport. - Latency and tokens are best measured server-side and returned to the client alongside the response, then attached to the events.
Recipe: instrument inside an AI app builder
In Lovable, Superblocks, or any prompt-driven builder, paste this into the builder's agent and review what it produces:
Instrument this app's chat agent with Amplitude Agent Analytics using the standard
@amplitude/analytics-browser SDK (do NOT use @amplitude/ai, it is Node-only and will
crash this runtime). Emit these events via amplitude.track(): [Agent] User Message at
request start, one [Agent] Tool Call per tool invocation in execution order,
[Agent] AI Response after the reply, and [Agent] Session End when the chat genuinely ends
(no short idle timers; the server auto-closes idle sessions). On every event include
[Agent] Session ID (one stable UUID per conversation), [Agent] Turn ID (increment once
per user-message exchange and stamp the same value on all events of that exchange),
[Agent] Trace ID (new UUID per exchange), [Agent] Agent ID (a stable name for this agent),
and [Agent] Runtime: "browser". Message text goes in $llm_message as { text: "..." }
(an object, not a string) on User Message and AI Response only. Put [Agent] Model Name,
[Agent] Provider, token counts, [Agent] Latency Ms, and [Agent] System Prompt on AI
Response only. Put [Agent] Tool Name, [Agent] Tool Success, [Agent] Latency Ms,
[Agent] Invocation ID, and [Agent] Parent Message ID on Tool Call. Redact PII (emails,
phones, SSNs, cards, IPs) from all message text, system prompts, and tool payloads
before tracking. Use one persistent anonymous UUID from localStorage as the Amplitude
user_id. Never call setTransport("beacon"). Use functional state updates when computing
turn counters from UI state so follow-up messages aren't lost to stale closures.
Then verify: send two messages, one that triggers a tool, and confirm in Live Events that all events share a session ID, each exchange shares one Turn ID and Trace ID, tool calls precede their AI Response, and $llm_message.text renders in the session thread view.
Instrument thumbs up / thumbs down feedback
Wire your feedback control to the SDK's scoring method so every rating lands as an [Agent] Score event. Three decisions matter:
- Name it
user-feedback. This exact Score Name has special semantics: your explicit feedback overrides the detected negative feedback signal for the session, so a thumbs up from the user beats a false-positive frustration detection, and a thumbs down flags the session even when the model saw nothing wrong. - Target the message. Rate the specific AI response the user reacted to by passing its
[Agent] Message IDas the target, with target typemessage. Use target typesessiononly for end-of-conversation ratings like a CSAT prompt. - Value 1 for up, 0 for down, source
user.
// Node: inside the session, when the user clicks the control
s.score('user-feedback', 1, aiMessageId, { source: 'user' }); // thumbs up
s.score('user-feedback', 0, aiMessageId, {
source: 'user',
comment: freeTextFromUser, // optional; gated by privacy mode
}); // thumbs down
Feedback often arrives after the request that produced the response has finished (the user clicks minutes later, from the frontend). For that path, call the client-level method with the session and target IDs you stored alongside the message:
ai.trackScore({
userId,
name: 'user-feedback',
value: 0,
targetId: aiMessageId,
targetType: 'message',
source: 'user',
sessionId,
});
Notes:
- Ratings on a five-star or CSAT scale work the same way: pass the numeric value and, if it isn't binary thumbs, use a different Score Name (for example
csat) so your thumbs metric stays clean. Reserveuser-feedbackfor the binary control because of its override semantics. - Don't worry about sparse clicks. Built-in signals score every session regardless, and implicit behavior (copy, regenerate, abandonment) is captured automatically; explicit ratings sharpen the picture where you have them.
- Amplitude never generates Score events, so a chart of
[Agent] Scorewhere Score Name isuser-feedbackis a pure measure of what your users said. The classic quality chart is the disagreement view: sessions the signals judged fine that users thumbed down.
Link to Session Replay
The agent sessionId and the browser session ID are different things and both have a home: the agent session is the job, the browser session is the visit.
What links a session to Session Replay
If your frontend runs Amplitude's Session Replay, link agent sessions to the recording by passing browserSessionId and deviceId when you open the agent session. The Session Replay ID (<deviceId>/<sessionId>) then lands as [Amplitude] Session Replay ID on the session's [Agent] events. Once at least one carries it, a session replay link shows in the sessions pane.
Multi-agent systems
Create child agents from a parent to record delegation. Child agents automatically carry the parent reference, sessions record the root agent and chain depth, and provider wrappers suppress spurious user-message events inside delegated calls. Give every agent a stable, human-readable ID; those IDs become the primary dimension for comparing quality and cost across your system.
Verify your instrumentation
Verification checks eight gates on your events:
- Events arriving at all
- Model Name and Provider populated
- Token counts populated
- Cost USD populated
- Latency populated
- Session ID is your real ID, not an auto-generated placeholder
- Agent ID set per component
- User ID from your auth layer
The setup agent writes a verification test as part of instrumentation. If you instrumented by hand, run one real interaction and check the gates in Live Events. Gates 6 through 8 intentionally fail on placeholder IDs; that's the ladder, not a bug.
Production checklist
Once your traces start flowing into Amplitude, validate the following:
- Valid user IDs are being used
- Valid session IDs are being used
- There are explicit session close events where the outcome is known, and there are idle timeouts when expected
- Privacy mode decided and reviewed
- Agent IDs are stable and human-readable; child agents are wired for delegation
Was this helpful?