Product analytics events for AI features
Instrument AI features with product analytics, with an event schema for requests, outcomes, edits and costs, and the questions it lets you answer.
On this page
Why this matters
On launch day, someone will ask “is it working?” Without the right events, you’ll answer with page views and button clicks, which tell you people are curious, not whether they’re helped. The metrics that matter for AI features, such as acceptance, edits, retries and cost per success, need a few events most analytics plans forget.
Instrumenting them takes an afternoon if you do it before launch, and is painful to retrofit after. This article lists the events every AI feature needs, gives you a typed schema that includes model, prompt version and cost, shows how to keep user content out of analytics, and maps the events to the questions they answer.
The events every AI feature needs
Think of each AI output as moving through a short journey. Each step is an event:
Show text descriptionHide text description
A sequence of 4 steps.
- Shown : The entry point was visible to the user
- Requested : The user asked for output
- Completed : Output arrived, with how it ended, tokens and cost
- Outcome : Accepted, edited, retried, dismissed or abandoned
| Event | When | Key properties |
|---|---|---|
ai_entry_shown |
The entry point appears | feature, placement |
ai_requested |
The user asks for output | feature, request ID, flag decision |
ai_completed |
The response finishes, or fails | stop reason or error type, latency, tokens, cost |
ai_accepted |
The user uses the output | edit ratio (0 if unchanged) |
ai_retried |
The user asks again | reason, if given |
ai_dismissed |
The user explicitly rejects it | reason, if given |
ai_feedback |
Thumbs or rating | value, reason |
“Abandoned” is usually inferred rather than tracked: a completed output with no outcome event within a session.
An event schema
Every event shares a base: which feature, which configuration (prompt version, model, A/B variant) and a request ID linking events for the same output together. Validating events at the point they’re sent catches instrumentation bugs early, before they pollute months of data.
import { z } from 'zod';
// Shared by every event: what produced the output, and an ID tying events for one output together.
const Base = z.object({
feature: z.string(),
requestId: z.string(),
promptVersion: z.string(),
model: z.string(),
variant: z.string().optional(),
userId: z.string(), // Pseudonymous ID, never an email or name.
});
const AiEvent = z.discriminatedUnion('type', [
Base.extend({ type: z.literal('ai_requested') }),
Base.extend({
type: z.literal('ai_completed'),
outcome: z.enum(['end_turn', 'max_tokens', 'refusal', 'other_stop', 'error', 'cancelled']),
latencyMs: z.number().nonnegative(),
inputTokens: z.number().int().nonnegative(),
outputTokens: z.number().int().nonnegative(),
costUsd: z.number().nonnegative(),
}),
Base.extend({ type: z.literal('ai_accepted'), editRatio: z.number().min(0).max(1) }),
Base.extend({ type: z.literal('ai_retried'), reason: z.string().max(100).optional() }),
Base.extend({ type: z.literal('ai_dismissed'), reason: z.string().max(100).optional() }),
Base.extend({ type: z.literal('ai_feedback'), value: z.enum(['up', 'down']), reason: z.string().max(100).optional() }),
]);
export type AiEvent = z.infer<typeof AiEvent>;
/**
* Validates and sends an event. Content (prompts, outputs, documents) is deliberately not part of the schema:
* analytics records behavior, not what users wrote. Free-text reasons are short, chosen from a list in the UI.
*/
export function track(event: AiEvent): void {
const parsed = AiEvent.safeParse(event);
if (!parsed.success) {
// An invalid event is an instrumentation bug: surface it in development, never send bad data.
console.error('Invalid analytics event', parsed.error.issues);
return;
}
// Replace with your analytics client, e.g. analytics.capture(parsed.data.type, parsed.data).
console.log(JSON.stringify(parsed.data));
}
const base = { feature: 'reply-drafts', requestId: 'req_123', promptVersion: 'reply-v4', model: 'claude-opus-5-5', userId: 'u_8f2c' };
track({ ...base, type: 'ai_requested' });
track({ ...base, type: 'ai_completed', outcome: 'end_turn', latencyMs: 3120, inputTokens: 2900, outputTokens: 410, costUsd: 0.0198 });
track({ ...base, type: 'ai_accepted', editRatio: 0.12 }); Three design choices are deliberate:
- The base fields make every event attributable. When a prompt change rolls out, you can split every metric by
promptVersion; when you A/B test, byvariant. - Outcome is an enum, not free text. It maps directly to the states your UI designs for, so dashboards can show truncation and refusal rates without parsing strings.
- There’s no field for content. You can’t accidentally send a user’s email to your analytics tool if the schema has nowhere to put it.
Privacy by design
Analytics tools are often shared widely inside a company and kept for years. Treat them accordingly:
- Log behavior, not content. Events say that a draft was accepted with 12% edits, not what it said.
- Use pseudonymous IDs. An internal user ID, never an email address or name.
- Keep free text short and structured. Offer reasons from a list (“Wrong facts”, “Wrong tone”, “Too long”) instead of open text boxes that invite personal details.
- Store content separately, if at all. If you need examples for debugging or evals, keep them in your tracing system with stricter access and shorter retention, and with consent where required. See tracing and data privacy.
Questions the events answer
With these events in place, the metrics from defining success metrics become straightforward queries:
| Question | Computed from |
|---|---|
| Do people find the feature? | ai_requested users ÷ ai_entry_shown users |
| Does it work when they try it? | ai_completed with end_turn ÷ ai_requested |
| Is the output useful? | ai_accepted ÷ ai_completed (acceptance rate) |
| How close is it to what they want? | Average editRatio on ai_accepted |
| Are they dissatisfied with first answers? | ai_retried ÷ ai_completed |
| What does a success cost? | Sum of costUsd ÷ count of ai_accepted |
| Did the new prompt help? | All of the above, split by promptVersion |
Exact definitions belong in one place, such as the metric definitions reference, so every dashboard computes them the same way.
Common mistakes
- Tracking clicks instead of outcomes. Clicks measure curiosity. Track accepted, edited, retried and dismissed.
- No prompt version or model on events. Without them, you can’t tell which configuration produced which results.
- Logging content in analytics. Keep user text out of analytics; record behavior and keep any content in stricter systems.
- Unvalidated events. Instrumentation bugs silently corrupt data. Validate events where they’re sent.
- Free-text feedback boxes. They collect personal details and are hard to analyze. Offer a short list of reasons.
Try it
Exercise
About 45 min
Write an event schema for one feature
- List the steps an output goes through in your feature, from entry point to outcome.
- Define events for each step, with the base fields above and any properties specific to your feature.
- Write the zod schema, and send events from a staging build as you use the feature.
- Check every event arrives with the right properties, then write the query for acceptance rate split by prompt version.
Show a hint
Use the feature in all its states (accept, edit, retry, dismiss, stop mid-stream, trigger an error) and check that each produces exactly one outcome event. Gaps usually appear in the unhappy paths.
Show a worked solution
Example for drafted replies:
- Events:
ai_entry_shown,ai_requested,ai_completed(outcome, latency, tokens, cost),ai_accepted(editRatio),ai_retried,ai_dismissed(reason from a list),ai_feedback. - Feature-specific property:
ticketCategoryonai_requested, chosen from the helpdesk’s categories. - Staging check found: pressing Stop sent no
ai_completedevent. Fixed by sending it withoutcome: 'cancelled'. - Query: accepted ÷ completed, grouped by
promptVersionand week.
Check your understanding
Key takeaways
- Track the full journey: shown, requested, completed, and one outcome per output.
- Put feature, request ID, prompt version, model and variant on every event, and validate events where they’re sent.
- Log behavior, not content: pseudonymous IDs, structured reasons, no text fields.
- With these events, acceptance, edits, retries and cost per success become simple queries, split by configuration.
Progress is saved in this browser and lights up the roadmap.
Related articles
- Defining success metrics before you build Choose success metrics for an AI feature before you build it, from task success and adoption to guardrails and cost per success, with code to compute them. Level: Beginner
- Feature flags and staged rollouts for AI Ship AI features gradually with feature flags, covering staged exposure, kill switches, flagging prompts and models, and rollback plans that work in minutes. Level: Advanced
- Tracing and observability for LLM calls Trace every LLM call in production, from what to record to linking traces with users and outcomes, reviewing traces, and monitoring cost and latency. Level: Advanced