Skip to content
AI Product Engineer Wiki

    ↑ ↓ to move between results, Enter to open, Esc to close

    Menu

    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.

    Level: Advanced 7 min read Updated
    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:

    The life of an AI output, as events. Every output ends in exactly one outcome event, so the funnel always adds up.
    Show text descriptionHide text description

    A sequence of 4 steps.

    1. Shown : The entry point was visible to the user
    2. Requested : The user asked for output
    3. Completed : Output arrived, with how it ended, tokens and cost
    4. 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.

    lib/ai-analytics.ts
    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, by variant.
    • 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

    1. List the steps an output goes through in your feature, from entry point to outcome.
    2. Define events for each step, with the base fields above and any properties specific to your feature.
    3. Write the zod schema, and send events from a staging build as you use the feature.
    4. 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: ticketCategory on ai_requested, chosen from the helpdesk’s categories.
    • Staging check found: pressing Stop sent no ai_completed event. Fixed by sending it with outcome: 'cancelled'.
    • Query: accepted ÷ completed, grouped by promptVersion and week.

    Check your understanding

    Question 1 of 4: Which event best shows that an AI output was useful?

    Show answer

    C. ai_accepted

    Acceptance means the user actually used the output; the others measure exposure or curiosity.

    Question 2 of 4: Why include promptVersion and model on every event?

    Show answer

    B. So every metric can be split by configuration, for example to see whether a new prompt helped

    Without configuration fields, you cannot attribute changes in metrics to the change that caused them.

    Question 3 of 4: Why does the schema have no field for prompts or outputs?

    Show answer

    B. So user content cannot be sent to analytics by accident

    Analytics records behavior. Keeping content out of the schema makes the privacy rule hard to break.

    Question 4 of 4: How do you compute cost per success from these events?

    Show answer

    B. Sum of costUsd ÷ number of ai_accepted

    Total cost, including outputs nobody used, divided by the outputs users accepted.

    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.