Configuration
Precedence chain, environment variables, and config file format
The SDK resolves configuration from multiple sources with a clear precedence order. The first source that provides a value wins.
Precedence chain
Code options → Environment variables → .yaviorc.json → Defaults → No-op- Code options — passed directly to
withYavio(server, { apiKey, endpoint, capture, shutdownTimeoutMs, flushTimeoutMs, waitUntilTimeoutMs, sessionKey })orcreateYavio({ … });sessionKeyexists only here (a function) - Environment variables —
YAVIO_API_KEY,YAVIO_ENDPOINT,YAVIO_SERVER_ONLY,YAVIO_INTENT,YAVIO_SHUTDOWN_TIMEOUT_MS,YAVIO_FLUSH_TIMEOUT_MSandYAVIO_WAIT_UNTIL_TIMEOUT_MS - Config file —
.yaviorc.jsonfound by walking up fromprocess.cwd() - Defaults — built-in defaults for
endpoint,capture,shutdownTimeoutMs,flushTimeoutMsandwaitUntilTimeoutMs - No-op — if no API key is found from any source, the SDK is disabled
Environment variables
| Variable | Description | Example |
|---|---|---|
YAVIO_API_KEY | Project API key | yav_abc123... |
YAVIO_ENDPOINT | Ingestion API URL. Defaults to https://ingest.yavio.ai/v1/events; only set it to point at a self-hosted ingest. Empty counts as unset. A value that is not an http(s):// URL (a missing scheme, a typo) puts the SDK in no-op mode with one YAVIO-1002 error — it never falls back to the hosted endpoint. | https://ingest.yavio.ai/v1/events |
YAVIO_SHUTDOWN_TIMEOUT_MS | Deadline for delivering queued events on shutdown, an integer number of milliseconds in [100, 2147483647]. Empty counts as unset. Anything else logs YAVIO-1006 once and the default 4 s applies. The code option shutdownTimeoutMs wins over it, the file's shutdownTimeoutMs comes after it. The way to set one value for the whole process: on SIGTERM/SIGINT the SDK fixes one deadline at signal + D, where D is the smallest value set explicitly (option, this env var or .yaviorc.json) by any client, withYavio() call or shutdown() in progress, and the env var counts even when no client is live; participants that set none do not lower it, and with nothing set it is 4 s. A SIGTERM after the SIGINT that started the drain (Claude Code's stop sequence for a stdio server) moves that deadline in to 400 ms after the SIGINT (or 300 ms after the SIGTERM, when sooner), and a higher value does not extend that (since 0.5.5). It also lowers the 1.5 s bound of a natural exit (a process that ends on its own with events queued, such as a stdio server whose client closed stdin); a higher value does not raise that bound. Rule of thumb: your close cap + 1 s. See Setup for what it bounds. | 5000 |
YAVIO_FLUSH_TIMEOUT_MS | How long createYavio().flush() and flushWhenSent() wait for ingest, an integer number of milliseconds in [100, 2147483647]. Past it, flushWhenSent()'s flush still waits for the batch it sent, up to YAVIO_WAIT_UNTIL_TIMEOUT_MS. Empty counts as unset. Anything else logs YAVIO-1006 once and the default 1 s applies. The code option flushTimeoutMs wins over it, the file's flushTimeoutMs comes after it. Nothing is dropped when it runs out: the events stay queued for the next flush. Keep it well inside the function's own timeout when the flush is awaited (AWS Lambda's default is 3 s). Since 0.5.5. | 1000 |
YAVIO_WAIT_UNTIL_TIMEOUT_MS | How long the flush flushWhenSent() hands to waitUntil may keep waiting for the batch it sent, past flushTimeoutMs, an integer number of milliseconds in [100, 2147483647]. Empty counts as unset; anything else logs YAVIO-1006 once and the default 10 s applies. The code option waitUntilTimeoutMs wins over it, the file's comes after it. Lower it on Vercel, where the function runs while the flush waits, if an ingest that hangs must cost less. Since 0.5.5. | 10000 |
YAVIO_API_KEY=yav_abc123...
# Optional — omit to use the hosted endpoint. Point it at your own ingest when
# self-hosting, or at http://localhost:3001/v1/events when running the stack locally.
YAVIO_ENDPOINT=https://ingest.yavio.ai/v1/eventsConfig file
The SDK walks up the directory tree from process.cwd() looking for a .yaviorc.json file. This is useful for local development and monorepo setups.
{
"apiKey": "yav_abc123...",
"endpoint": "http://localhost:3001/v1/events",
"shutdownTimeoutMs": 5000,
"flushTimeoutMs": 1000,
"waitUntilTimeoutMs": 10000
}Place it in your project root or any parent directory. The nearest file wins.
Add .yaviorc.json to your .gitignore to avoid committing API keys.
Defaults
| Setting | Default |
|---|---|
endpoint | https://ingest.yavio.ai/v1/events |
capture.inputValues | true |
capture.outputValues | true |
capture.geo | true |
capture.rejectedInputValues | false |
tools | none |
shutdownTimeoutMs | 4000 |
flushTimeoutMs | 1000 |
waitUntilTimeoutMs | 10000 |
sessionKey | none |
Capture options
The capture object controls which data is collected on every tool_call event.
withYavio(server, {
capture: {
inputValues: false, // no input_keys, input_types, input_values, no client metadata
outputValues: false, // no output_content, no error_message from isError results
geo: "country", // country_code, but only { country } in input_values (false: no location at all)
},
});| Flag | Off means |
|---|---|
inputValues | No input_keys, input_types, input_values, no client metadata (locale, country_code, region, end_user_agent, subject_id), and no error_message from a thrown error (messages tend to interpolate the arguments) |
outputValues | No output_content. An isError result still sets status and error_category, but its text is not stored as error_message (it is output). A thrown error's message is still stored unless inputValues is off |
geo | No country_code or region, no openai/userLocation inside the captured _meta, and of the rest of _meta only known keys (see below) |
rejectedInputValues | Off by default. A call rejected before the handler runs (arguments the schema refused, unknown or disabled tool) keeps the names and types (input_keys, input_types) of the parameters the tool's schema declares, the _-prefixed request context in input_values, the client metadata and a value-free reason in error_message (McpServer's text as-is when it names only the tool — unknown, disabled — otherwise a fixed "Input validation error" reason), but no argument values, no key the model invented and no output_content. On: the raw arguments and McpServer's error text are stored |
geo takes two more values between "everything" and "nothing":
"region":country_codeandregion(the state or province, as the platform names it — on ChatGPT an English name such asNorth Rhine-Westphalia, not an ISO 3166-2 code) are set, andopenai/userLocationin the captured_metais replaced by{ country, region }. Use it for a breakdown by state or province without city or coordinates."country":country_codeis set, andopenai/userLocationis replaced by{ country }. Use it when a privacy review asks for country-level analytics only.
In both, city, timezone, coordinates and any field the platform adds to the location object later are not stored; without a valid country the object is removed, and a region only counts next to a valid country. The region is normalised the way ingest does it: typographic apostrophes and dashes folded, at most 64 characters, no digits or @ (a name that fails is dropped). In city-states the region is the city — ChatGPT reports Berlin as State of Berlin, and likewise Hamburg, Bremen, Vienna or Washington, D.C. — so say so if your privacy policy promises "no city". true sets region too and keeps _meta whole.
With any geo other than true ("region", "country", false), the rest of the captured _meta keeps exactly openai/locale, openai/userAgent, openai/subject and openai/session. Everything else is dropped — ChatGPT's top-level timezone and timezone_offset_minutes, the conversation and request ids (sessionId, windowId, callId, itemId, openai/request_id), openai/organization, the client capabilities, and any key the platform adds later, since nothing guarantees a new location field gets a recognisable name. The names of dropped keys (never their values) are stored in input_values._metaDroppedKeys, so you can see when a new one appears; a name with a run of 8 or more hex digits is only counted, in _metaDroppedUnnamed. The allow-listed request headers in _requestInfo are not affected. Before 0.4.1, "country" and false kept the top-level timezone. geo has no per-tool override. The React widget's widget_render event stores the browser's time zone regardless of capture.geo.
Under every geo, true included, the trace context never stays in the captured _meta (SDK 0.5.5 and later): baggage (free-form context such as user or tenant ids that the client's tracing sends along), tracestate, .NET's Correlation-Context and Datadog's _dd_trace_context, in any letter case, are dropped and only their key names are stored in _metaDroppedKeys, under geo: true too; Jaeger's uberctx-… and OpenTracing's ot-baggage-… keys, one baggage member each, are dropped and only counted in _metaDroppedUnnamed. A valid traceparent is stored once, as the tool call's metadata.traceparent, also when it arrives as an HTTP header or inside Datadog's _dd_trace_context, and for tools without arguments. Before 0.5.5, geo: true kept all of these verbatim.
Every capture flag fails closed, in capture and in a per-tool tools override alike: inputValues, outputValues, rejectedInputValues, tokens, retries and the per-tool intent accept exactly true and false, geo accepts true, false, "country" and "region". Any other value (for example the string "false" in .yaviorc.json, which is truthy) turns that capture off and logs YAVIO-1006 once per flag and process.
Client metadata (locale, country_code, region, end_user_agent, subject_id, and metadata.traceparent) is captured under inputValues for every tool, including tools that take no arguments.
rejectedInputValues exists because a successful call only ever stores values the tool's schema accepted, while a rejected call would store whatever the model sent. With an enum field such as plan: z.enum(["basic", "premium"]), the model may send "premium, my wife is pregnant"; the schema rejects it, and with the flag on that sentence is stored. Leave it off if your privacy policy says only fixed values are collected. It has no effect while inputValues is off.
tokens and retries are accepted but reserved — they have no effect today.
Per-tool overrides
tools overrides the capture flags for individual tools, keyed by tool name. A key that is omitted inherits the global value.
withYavio(server, {
capture: { inputValues: true, outputValues: true },
tools: {
"book-contract": { inputValues: false, outputValues: false, intent: false },
},
});{
"apiKey": "yav_abc123...",
"tools": { "book-contract": { "inputValues": false, "outputValues": false } }
}Use this for a tool whose input must never reach analytics — an IBAN, a message body, a birth date. The tool stays on the wrapped server, so it still produces tool_call events with latency and status, and yavio.conversion() and friends still work from inside its handler. Registering the tool on the unwrapped server instead loses all of that under withYavio(): no tool_call event, and tracking calls from the handler are dropped (see Tracking API). Under createYavio().wrap() it does not even keep the input out: wrap() instruments every tool on the server, so the override is the only way.
rejectedInputValues can be set per tool as well, in both directions.
intent: false additionally keeps the tool out of intent capture: the context parameter is neither advertised nor captured for it, and a context a client still sends from a cached schema is stripped before validation.
Tool names are not validated — tools register after withYavio() runs — so an override for a name that never registers is simply unused. There is no environment-variable form.
The SDK's PII stripper only matches emails, Luhn-valid card numbers, US Social Security numbers and US-format phone numbers. It is not a substitute for an override on a tool that takes names, addresses, IBANs or dates of birth.
Session key
sessionKey tells the SDK which conversation a call belongs to when the call carries no session key the SDK knows — over HTTP every such call would otherwise be a session of its own (since SDK 0.5.5, on any host; 0.5.4 put all key-less callers of a server that reuses one stateless transport into one), which the dashboard does not count (session_source: "per_request"). Code option only, on createYavio() and withYavio():
const analytics = createYavio({
serverOnly: true,
sessionKey: ({ headers }) => headers["x-conversation-id"],
});The SDK takes a call's session from the first key it finds: Mcp-Session-Id, ChatGPT's _meta["openai/session"], VS Code's _meta["vscode.conversationId"], the Codex CLI's _meta.threadId (only from codex-mcp-client), then sessionKey; without any, a session of its own for the call over HTTP (per_request) and the connection's own session on stdio (connection). sessionKey(request) gets { headers, meta, authInfo, extra } — the HTTP headers (names lower-cased, {} off HTTP), the request _meta, what your auth middleware set as req.auth, and the MCP SDK's handler context — the same on MCP SDK v1 and v2. It runs synchronously before the handler, at most once per call. Return a string of at most 1 024 characters, or undefined / "" for none; a throw, a promise, a non-string or a longer string counts as none, with one YAVIO-1102 warning per process, and never affects the call. A value that is not a function logs YAVIO-1006 once and is ignored. The SDK stores ses_ + an HMAC of the value keyed with the API key, never the value.
Return a conversation id. Funnels, paths, the step sequence and identify() work per session, and a session keeps its first identified user. A per-user id on its own — an auth subject such as authInfo.extra.sub — would chain all of that user's conversations into one endless session and one funnel attempt. Never return authInfo.clientId (the OAuth client, shared by every user of a client app), the bearer token, a constant, or a per-request id.
Each session a process sees takes an entry in its session store (1 000, least recently used out) and in the connection-event marks (10 000); with more concurrent conversations than that an idle one ages out and starts over. A call without a key on an HTTP request takes no entry in the session store. See Setup for the order of the keys and what per_request means.
Example configurations
Production (Yavio Cloud)
// Code — just the API key, everything else uses defaults
withYavio(server, {
apiKey: process.env.YAVIO_API_KEY,
});Local development
{
"apiKey": "yav_dev_key",
"endpoint": "http://localhost:3001/v1/events"
}// No options needed — config file is auto-discovered
withYavio(server);Disabled (CI / testing)
// No API key anywhere → no-op mode, zero overhead
withYavio(server);