SaaS Tracker Docs

Tracking AI usage (AI Cost Analytics)

AI Cost Analytics shows what your AI features really cost — by customer, feature, model and plan — without storing prompts or generated content.

The MVP is event-based: your application keeps calling AI providers (OpenAI, Anthropic, Azure, etc.) the same way as today, and emits a standardized SaaS Tracker event after each AI call or value signal. There is no gateway, no proxy, no prompt logging, no model routing in this version.

Privacy by design. SaaS Tracker tracks AI usage, cost and value metadata — not your prompts or generated content by default. Do not send prompt, messages, completion, output, response_text or any chat transcripts. They are not part of the schema and will be ignored.

Why send AI usage events

AI Cost Analytics is included on the AI Cost plan (€49/mo) and on Starter, Growth and Enterprise.

Supported AI events (MVP)

Send these as normal events to the existing Ingest API. The event name goes in the standard event field; AI metadata goes in a properties object.

ai_call_completed is the primary event. ai_call_failed enables error rate tracking. The three output events feed value signals (cost-per-accepted-output, etc.) on Growth and above.

Example: ai_call_completed

{
  "event": "ai_call_completed",
  "user_hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
  "customer_org_id": "acme-corp",
  "properties": {
    "feature": "support_reply_generator",
    "provider": "openai",
    "model": "gpt-5.4-mini",
    "request_type": "generate",
    "input_tokens": 1200,
    "output_tokens": 350,
    "total_tokens": 1550,
    "estimated_cost_eur": 0.014,
    "latency_ms": 1420,
    "success": true,
    "workflow_id": "support-ticket-789",
    "ai_call_id": "call_01HV..."
  }
}

Example: ai_call_failed

{
  "event": "ai_call_failed",
  "user_hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
  "customer_org_id": "acme-corp",
  "properties": {
    "feature": "support_reply_generator",
    "provider": "openai",
    "model": "gpt-5.4-mini",
    "latency_ms": 8500,
    "error_code": "rate_limited",
    "workflow_id": "support-ticket-789",
    "ai_call_id": "call_01HV..."
  }
}

Do not put raw provider error responses or stack traces in error_message. Use a stable, short error category in error_code instead.

Example: ai_output_accepted / rejected / edited

Always include feature, provider, model and ai_call_id on output events — the dashboard uses them to compute per-feature acceptance rate, value-signal-by-provider/model and accurate cost-per-accepted-output (which joins on ai_call_id).

{
  "event": "ai_output_accepted",
  "user_hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
  "customer_org_id": "acme-corp",
  "properties": {
    "feature": "support_reply_generator",
    "provider": "openai",
    "model": "gpt-5.4-mini",
    "workflow_id": "support-ticket-789",
    "ai_call_id": "call_01HV..."
  }
}
{
  "event": "ai_output_edited",
  "user_hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
  "customer_org_id": "acme-corp",
  "properties": {
    "feature": "support_reply_generator",
    "provider": "openai",
    "model": "gpt-5.4-mini",
    "workflow_id": "support-ticket-789",
    "ai_call_id": "call_01HV...",
    "edit_ratio": 0.12
  }
}

Field reference

All AI fields go under properties. They are all optional unless noted. Token and latency fields default to 0 in ClickHouse when omitted; cost stays unknown (not 0) unless you send an explicit amount or ingest resolves it from the catalog.

ai_call_completed

FieldTypeRequiredNotes
featurestringyesStable name of the AI feature (e.g. support_reply_generator)
providerstringyese.g. openai, anthropic, azure_openai
modelstringyese.g. gpt-5.4-mini, claude-sonnet-4-6 (must match catalog or an alias)
request_typestringoptionalgenerate | summarize | classify | embed | chat | extract | other
input_tokensnumberoptionalPrompt/input tokens
output_tokensnumberoptionalCompletion/output tokens
total_tokensnumberoptionalSum; if not sent we fall back to input_tokens + output_tokens
estimated_cost_eurnumberoptionalExplicit cost in EUR (highest priority). 0 means known zero; omit when unknown
estimated_cost_usdnumberoptionalExplicit cost in USD; converted to EUR when an FX rate exists
cost_amountnumberoptionalExplicit cost amount (pair with cost_currency)
cost_currencystringoptionale.g. EUR, USD — with cost_amount
cached_input_tokensnumberoptionalCached prompt tokens (when provider reports them)
statusstringoptionale.g. failed — used with failed calls without token counts
latency_msnumberoptionalEnd-to-end latency in milliseconds
successbooleanoptionalDefaults to true for ai_call_completed
workflow_idstringoptionalUse to correlate calls and value signals (e.g. ticket id)
ai_call_idstringoptionalStable id for this individual AI call

ai_call_failed

FieldTypeRequiredNotes
featurestringyes
providerstringoptional
modelstringoptional
request_typestringoptional
latency_msnumberoptional
error_codestringoptionalStable, short category — not the raw provider message
error_messagestringoptionalShort human description. Do not store sensitive provider responses.
workflow_idstringoptional
ai_call_idstringoptional

ai_output_accepted / rejected / edited

FieldTypeRequiredNotes
featurestringyesSame value as on the originating ai_call_completed
providerstringrecommendedSame as on the originating ai_call_completed; needed for value signals by provider/model
modelstringrecommendedSame as on the originating ai_call_completed; needed for value signals by provider/model
ai_call_idstringrecommendedStable id linking back to the original ai_call_completed. Required for accurate cost-per-accepted-output (we join ai_output_accepted.ai_call_id to ai_call_completed.ai_call_id).
workflow_idstringoptionalCoarser-grained correlation than ai_call_id (e.g. ticket id)
value_signalstringoptionalDefaults: accepted, rejected, edited
edit_rationumberoptionalOnly on ai_output_edited — fraction of output that was changed (0–1)

Why we ask for provider, model and ai_call_id on output events too. ClickHouse doesn’t automatically know which ai_call_completed an output event belongs to. To compute per-feature acceptance rate, edit-ratio distributions or true cost-per-accepted-output, we either group by the fields you send on the output event itself or join to the matching call via ai_call_id. If you only send feature, you’ll still get acceptance rate per feature, but provider/model breakdowns and the joined cost view will be incomplete.

Privacy and what NOT to send

If prompt/output logging becomes useful later, it will be a separate, opt-in enterprise capability. The default product never stores them.

Cost calculation

SaaS Tracker resolves cost at ingest time (per event, not recalculated later). Priority:

  1. Explicit event costcost_amount + cost_currency, or estimated_cost_eur / estimated_cost_usd
  2. Customer-specific unit price in the model catalog (Admin)
  3. Global catalog unit price (EUR per 1M tokens)
  4. Unknown — cost stays empty (NULL in analytics), not confused with zero

Send provider and model strings exactly as your app uses them. They must match either:

Nested properties.ai.* is supported (fields are flattened at ingest).

Resolution metadata (stored on the event)

After ingest, these fields may appear on the stored JSON (and in ai_usage_events):

FieldMeaning
ai_cost_statuse.g. calculated, explicit_event_cost, unknown_model, missing_price, missing_tokens
ai_pricing_sourceevent_explicit, customer_override, global_catalog, none
ai_canonical_model_keyResolved catalog key when known
estimated_cost_eurTarget EUR amount when known (including explicit 0)

Unknown vs zero: missing cost is not stored as 0. Send explicit 0 only when cost is truly zero.

Operator: unmapped models (Admin portal)

Superusers open Admin portal → Unmapped AI models to see (provider, model) pairs that returned unknown_model, then Create alias to map them to the catalog. Historical events are not backfilled.

Best practices

What you’ll see in the product

Open Analytics → AI Cost in the dashboard. You’ll get:

If you haven’t sent any AI events yet, the page shows an empty state with a copy-paste tracking snippet and this same privacy note.


Next: Plans and limits or Ingest API event schema.