On this page

Set up Agent Analytics

Early Access

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.

This page describes how to customize your Agent Analytics set up. To complete the in-depth set up, you'll need to modify your environment with the Agent Analytics SDK. The Agent Analytics SDK is the full developer reference, covering install, initialization, instrumenting sessions and tools, provider notes, edge runtimes, OTel ingestion, cost handling, and the full API. If you want to quickly get started with basic Agent Analytics, go to the Agent Analytics Quickstart Guide. The quickstart guide does not allow for any customizations. You can always complete the quickstart guide to get insights as fast as possible and then come back to this set up guide to customize your Agent Analytics insights.

The timeline below shows what your instrumentation produces. Click any event to inspect its shape.

From the SDKreal-time during sessionFrom Amplitudepost-hoc enrichmentLOADTURN 1TURN 2ENDViewedPage(browser SDK)User MessageTool CallAI ResponseUser MessageTool CallAI Response···Session EndALSO EMITTED — INSIDE A TURNSpanSession RecordEvaluator Result × N
Event type
[Agent] AI Responsefrom the SDK
Fired at
22:33:48
Identity
[Agent] Session ID4ddcc6b2-1041-432a-aa8c-ebe3eccac40b
[Agent] Agent IDsupport-chatbot
[Agent] Trace IDb4f63d43-d752-4b1f-8489-d234ddf586b2
Event-specific
$llm_message.textI can help. Your subscription renews on Aug 15…
[Agent] Model Namegpt-4o-mini
[Agent] Provideropenai
[Agent] Input Tokens1245
[Agent] Output Tokens87
[Agent] Latency Ms3420
[Agent] Cost USD0.0012
Closes the turn. Carries the eight fields the SDK doctor checks at setup: Session ID, Agent ID, Model, Provider, Latency Ms, Input/Output Tokens, Cost USD. Emitted by s.trackAiMessage(...) or a provider wrapper.

How setup works

You must have previously completed the following prerequisites before data shows up in Amplitude:

  1. You've selected which Amplitude project the data will flow into and you have access to the corresponding project API Key.
  2. 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.
  3. You've decided what leaves your app. Privacy mode controls whether prompt and response text reaches Amplitude.

Choose an instrumentation path

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:

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.

  1. Explicit close (recommended): Call trackSessionEnd() (Node) or track_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, the session() 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 Record event 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 End event 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 ID property 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.

  2. 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.

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.

typescript
// 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.

Notes:

  • In full mode, 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_enriched is not exclusive. You can send Session Enrichment events in any mode. customer_enriched is simply the mode where your enrichment is the only session-level quality data, because no content is available for Amplitude's signals.
  • In full mode, 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_only or customer_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.

If Amplitude encounters a known device ID that is already tied to a user ID in a different project, Amplitude assumes the device ID is tied to that user ID in all projects, even if you don't have the Portfolio add-on. For more information, refer to Portfolios.

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.
Decide your ID strategy on day one of instrumentation.

Install the SDK

plaintext
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.

typescript
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 Call when 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.

Rules that hand-rolled instrumentation gets wrong most often:

  • $llm_message must 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 ID per 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 same user_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 USD on 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:

javascript
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,
});
Don't build your own short idle timer. Rotating session IDs after a few quiet minutes chops one conversation into several sessions: enrichment judges half-conversations, task completion looks artificially low, and, since billing is per agent session, one conversation bills as several. If your product genuinely defines sessions by inactivity, pass a matching [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.text on User Message and AI Response
  • [ ] [Agent] System Prompt on AI Response
  • [ ] [Agent] Tool Input and [Agent] Tool Output on Tool Call (redact recursively; payloads are objects)
  • [ ] [Agent] Comment on 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_id gives 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:

plaintext
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:

  1. 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.
  2. Target the message. Rate the specific AI response the user reacted to by passing its [Agent] Message ID as the target, with target type message. Use target type session only for end-of-conversation ratings like a CSAT prompt.
  3. Value 1 for up, 0 for down, source user.
typescript
// 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:

typescript
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. Reserve user-feedback for 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] Score where Score Name is user-feedback is 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.

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:

  1. Events arriving at all
  2. Model Name and Provider populated
  3. Token counts populated
  4. Cost USD populated
  5. Latency populated
  6. Session ID is your real ID, not an auto-generated placeholder
  7. Agent ID set per component
  8. 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?