Events
Event types, session semantics, and what gets captured automatically
Everything in Yavio starts with events. An event is a single data point representing something that happened — a tool was called, a widget was rendered, a user converted. All events are stored in a single ClickHouse events table and share a common set of fields.
Common Event Fields
Every event carries these fields:
| Field | Description |
|---|---|
event_id | Unique identifier (UUID), used for deduplication |
event_type | The type of event (e.g., tool_call, step, widget_render) |
trace_id | Correlation key linking server and widget events for the same tool invocation |
session_id | The conversation the event belongs to (ses_ prefix), see below |
session_source | How the server SDK derived session_id (SDK 0.5.4 and later; a 0.5.4 widget stamps its server session's source too), see below |
timestamp | When the event occurred |
platform | Which AI platform initiated the action (e.g., chatgpt, claude, cursor) |
source | Whether the event originated from server or widget |
Session Semantics
A session_id stands for one conversation. The server SDK derives it from the first key the call carries, and records which in session_source:
session_source | Derived from |
|---|---|
mcp_session_id | The transport's Mcp-Session-Id (a sessionful Streamable HTTP server) |
openai_session | ChatGPT's _meta["openai/session"] |
vscode_conversation | VS Code / Copilot Chat's _meta["vscode.conversationId"] |
codex_thread | The Codex CLI's _meta.threadId (only from codex-mcp-client) |
custom | Your sessionKey option — stored as a hash keyed with your API key |
connection | No key, a non-HTTP transport (stdio): one session per client process |
per_request | No key on an HTTP request, on any server. It is not counted as a session. Since SDK 0.5.5 it is one call, not a conversation; on 0.5.4 a server that reuses one stateless transport put all its key-less callers into one |
The same key gives the same session_id on every server instance and process. Both server and widget events share the session ID — the server propagates it to the widget via the configuration injection. Multiple traces (tool invocations) can share the same session.
Server-Side Auto-Captured Events
These events are captured automatically by the withYavio() proxy without any code changes:
tool_call
Fires on every tool invocation. This is the core event that powers most analytics. A tool that asks the user for input over several rounds (MCP SDK v2 inputRequired()) is still one call: SDK 0.5.5 and later send one event, from the round whose result is final.
| Field | Description |
|---|---|
event_name | Tool name |
latency_ms | Handler execution time in milliseconds. Absent when the handler never ran (see validation). For a tool that asked for input: the handler time of the rounds inside the request, never the time the user took to answer (every round on MCP SDK v2's 2025-era shim, the final request only on MCP 2026-07-28) |
metadata | Unset on an ordinary call without a trace context. traceparent (SDK 0.5.5 and later): a valid W3C traceparent from the request _meta (MCP 2026-07-28, SEP-414), else from the HTTP traceparent header, else from Datadog's _meta._dd_trace_context, stored intact (the PII stripper leaves the exact format alone) under capture.inputValues, for every geo and for tools without arguments; join a call to your own traces with JSONExtractString(metadata, 'traceparent'), whose second field is the trace id. For a tool that asked for input (SDK 0.5.5 and later): input_rounds, how many rounds asked for input inside the request, and input_retry: true when the request is the client's MCP 2026-07-28 retry. A 2026-07-28 call whose client never retries leaves no tool_call |
status | success or error. A call is an error when the handler throws, returns a result with isError: true, or when the MCP SDK rejects the call before or after the handler (SDK 0.4.0 and later; earlier SDKs only count throws) |
error_category | tool_error — the handler returned isError: true; unknown — the handler threw; validation — the call failed before the handler ran (unknown or disabled tool, arguments the schema rejected; by default such an event has the declared argument names and types and a value-free reason, but no values, see capture.rejectedInputValues); server — the result failed the tool's own outputSchema, the tool asked for input that the MCP SDK could not get or refused, or a 2026-07-28 retry of a tool that asked for input was rejected before its handler (a refused requestState, a tool changed or removed between rounds). The client sees the same shape in every case, so the distinction is server-side only. Reserved: auth, timeout, rate_limit |
error_message | The exception message, or the first text item of an isError result. PII-stripped, at most 500 characters. The result text is output, so it is only stored while capture.outputValues is on. For a validation event it is McpServer's text only with capture.rejectedInputValues on (that text can echo the rejected value); by default it is a value-free reason: McpServer's text when it names only the tool (unknown or disabled tool), otherwise a fixed "Input validation error" text |
is_retry | Reserved. Not set by the SDK today — always 0 |
input_keys | Parameter key names (JSON) |
input_types | Key-to-type mapping (JSON) |
input_values | The tool's arguments plus, under _-prefixed keys, the request _meta verbatim (_meta; since SDK 0.5.5 without the trace context: baggage, tracestate, .NET's Correlation-Context, Datadog's _dd_trace_context and an invalid traceparent are only named in _metaDroppedKeys, Jaeger's uberctx-… and OpenTracing's ot-baggage-… keys only counted in _metaDroppedUnnamed, a valid traceparent is metadata.traceparent), the MCP task id (_taskId) and four allow-listed request headers with the URL sans query (_requestInfo; five, with traceparent, before 0.5.5). PII-stripped. See the SDK README for the full list and the disclosure note |
output_content | The tool result, with binary content replaced by size placeholders. PII-stripped |
tokens_in / tokens_out | Reserved. Not set by the SDK today |
country_code | ISO 3166-1 alpha-2 from the platform's request _meta (openai/userLocation.country); omitted with capture.geo: false, which also removes the location object from input_values._meta; with capture.geo: "country" that object is reduced to { country } |
region | State or province from the platform's request _meta (openai/userLocation.region), as the platform names it: on ChatGPT an English name such as North Rhine-Westphalia, not an ISO 3166-2 code, and not on every call. Sent by SDK 0.4.1 and later with capture.geo true or "region", captured with capture.inputValues like the other client metadata. Stored only next to a valid country_code, with typographic apostrophes and dashes folded; a malformed name is stored as NULL, the event itself is kept |
locale / end_user_agent / subject_id | Client metadata from the request _meta, captured with capture.inputValues |
intent_context | How the SDK advertised the intent context parameter for the tool when the call arrived: required; optional on widget-invoked tools (the widget's own calls cannot send it) or with intent required: false; none when nothing was advertised: intent capture off, per-tool intent: false, a tool with its own context argument or a schema the SDK cannot extend (oneOf/anyOf/allOf, passthrough), or an unknown tool. Absent from SDKs before 0.4.0. It says nothing about who made the call, and it is the server's advertisement: a host still showing a cached schema can differ until it refreshes. The dashboard's Call Coverage counts required calls (and older rows), and shows optional-context calls as a separate share |
connection
Sent with a session's first tool call, not on connect, and there is no disconnect event. Captures protocol_version, client_name and client_version (connection_duration_ms is reserved and never set). protocol_version comes from the MCP 2026-07-28 request envelope, else the mcp-protocol-version header, else the version the server answered initialize with; SDKs before 0.5.5 read the header only, so their stdio and in-memory connections have none. metadata carries intent_enabled, client_info_source (where the client name came from: envelope, initialize or user_agent) and, from SDK 0.5.5, client_capabilities: the names of the capabilities the client declared (elicitation, roots, sampling, …), never their values, sorted, at most 20 (absent when there are none, and for a 2025-era client of a per-request host, whose server instance never saw initialize). Emitted at least once per session per server process; for key-less HTTP calls (session_source: per_request) once per process per client (platform, client name and version, protocol version) instead of once per call, so it records the first caller's client_capabilities. On an MCP SDK v1 1.24/1.25 server that reuses one stateless transport, capabilities from the handshake are the last client's to initialize, as is a client name from the handshake.
resource_access
Fires on resources/read and resources/list interception. Tracks which resources are accessed and how often.
prompt_usage
Fires on prompts/list and prompts/get interception. Records prompt name and argument patterns.
sampling_call
Fires on sampling/createMessage interception (server-to-client). Captures latency and token counts.
elicitation
Fires on elicitation/requestInput interception. Tracks response latency and field schema.
widget_response
Fires when the server-side proxy detects a widget response in a tool call return value. This is the server-side counterpart to the widget's widget_render event.
tool_discovery
Sent once per tool per server process, on the first tools/list that lists it: the tool's name, description (up to 2048 characters) and input JSON Schema exactly as the client sees it, without the intent context parameter. Schemas longer than 16,384 characters are left out. Feeds the tool registry shown on the dashboard's tool pages. Under withYavio(), tools registered on the unwrapped server are not discovered; createYavio().wrap() instruments every tool on the server.
Server-Side Explicit Events
These events require developer code using yavio within a tool handler:
step
Funnel progression point. Called via yavio.step("step_name"). Each step receives an auto-incrementing step_sequence for deterministic ordering.
track
Generic custom event. Called via yavio.track("event_name", properties). An escape hatch for anything not auto-captured.
conversion
Revenue attribution event. Called via yavio.conversion("name", { value, currency }). Powers ROI analytics.
identify
User identification event. Called via yavio.identify(userId, traits). Ties the session to a known user.
Widget Auto-Captured Events
These events are captured automatically by the useYavio() React hook in ChatGPT widgets:
| Event | Trigger |
|---|---|
widget_render | First useYavio() initialization — captures viewport, device info, network type |
widget_error | Unhandled JS error or promise rejection |
widget_visibility | Widget enters or exits viewport (IntersectionObserver) |
widget_click | Click or tap event — captures target element and click count |
widget_scroll | Scroll within widget container — captures max scroll depth |
widget_form_field | Focus and blur of form inputs — captures time spent, not values |
widget_form_submit | Form submission attempt — captures success/error status |
widget_link_click | Click on anchor or external link — captures destination domain |
widget_navigation | View or route change within multi-step widget |
widget_focus | Widget iframe gains or loses focus |
widget_performance | PerformanceObserver data on widget load (time to interactive, first paint) |
widget_rage_click | 3+ clicks within 500ms on the same element |
Widget Explicit Events
The widget SDK also exposes step(), track(), conversion(), and identify() via the useYavio() hook. These share the same schema as server-side explicit events but are sent with source: "widget".