Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ All HTTP services return errors in this shape:

## Important Implementation Notes

- The SDK uses `AsyncLocalStorage` for context propagation — `ctx.yavio` in handlers, singleton for deep utilities
- The SDK uses `AsyncLocalStorage` for context propagation — import the `yavio` singleton from `@yavio/sdk` and call methods inside tool handlers
- Event transport: SDK buffers in memory, flushes HTTP batch to ingestion endpoint
- Widget auto-config: API key and endpoint injected via `window.__YAVIO__` by server-side proxy
- Dashboard uses Next.js Server Components for ClickHouse queries
Expand Down
2 changes: 1 addition & 1 deletion .specs/01_executive-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Self-hosted and Cloud run the same codebase. Cloud monetizes through usage-based
|----------|--------|-----------|
| Instrumentation | Proxy pattern (`withYavio` wraps `McpServer`) | Best DX: 3 lines, zero config. Abstraction layer allows transport-level interception later. |
| User identification | `.identify(userId, traits)` on server + widget | Ties events to known users. Enables retention, cohorts, per-user analytics. Stored as `user_id` on events + `users_mv` materialized view. |
| Instance access (server) | Context injection + AsyncLocalStorage singleton | `ctx.yavio` for scoped handler use; singleton for deep utility functions. |
| Instance access (server) | AsyncLocalStorage singleton (`yavio`) | Import `yavio` from `@yavio/sdk` and call methods inside tool handlers. |
| Instance access (widget) | `useYavio()` React hook with auto-config | Zero-config: API key and endpoint auto-injected by server via `window.__YAVIO__`. |
| Event transport (server) | HTTP batch POST to ingestion API | SDK buffers events in memory, flushes to ingestion endpoint. Stateless, retryable. |
| Event transport (widget) | Direct HTTP to ingestion API | Widget sends events directly to ingestion endpoint using short-lived JWT (minted by server-side proxy). Project API key never reaches the browser. |
Expand Down
2 changes: 1 addition & 1 deletion .specs/07_error-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Errors originating in the `@yavio/sdk` SDK (both server and widget entry points)
| `YAVIO-1102` | warn | Session ID derivation failed | Could not derive `session_id` from MCP `initialize` handshake. A random session ID is generated as fallback. | Ensure the MCP client sends a valid `initialize` request. |
| `YAVIO-1103` | warn | Widget token minting failed | `POST /v1/widget-tokens` returned an error or was unreachable. Widget falls back to no-op mode. | Check ingestion API availability and API key validity. |
| `YAVIO-1104` | warn | Widget config injection skipped | Response interception could not detect `_meta.ui.resourceUri` or injection failed. Widget operates without analytics. | Ensure tool response follows MCP widget response format. |
| `YAVIO-1105` | warn | Context injection unavailable | `AsyncLocalStorage` context lost. Explicit tracking calls (`yavio.track()`, etc.) outside a request context will lack `traceId` and `sessionId`. | Call explicit methods from within a tool handler or use `ctx.yavio` instead of the module singleton. |
| `YAVIO-1105` | warn | Context unavailable | `AsyncLocalStorage` context lost. Tracking calls (`yavio.track()`, etc.) outside a request context will lack `traceId` and `sessionId`. | Ensure calls are made from within a tool handler wrapped by `withYavio()`. |
| `YAVIO-1106` | warn | Tool response inspection failed | Error inspecting tool handler response for size, content type, or zero-result detection. Event captured without response metadata. | Report the bug. |

### Transport & Delivery (1200–1299)
Expand Down
4 changes: 2 additions & 2 deletions .specs/infrastructure/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ Run against real Docker services using Testcontainers or a shared `docker-compos
| Server SDK → ClickHouse | `withYavio()` wraps tool → tool called → SDK flushes → query ClickHouse | Event row exists with correct `project_id`, `event_type=tool_call`, `trace_id` |
| Widget → ClickHouse | Mint widget JWT → POST widget event to ingest → query ClickHouse | Widget event exists, `source=widget`, `trace_id` matches JWT |
| Trace correlation | Server tool call + widget events share `trace_id` → query by trace | Both server and widget events returned, ordered by timestamp |
| Identify propagation | `ctx.yavio.identify(userId, traits)` → subsequent events | All events after identify carry `user_id`, `user_traits` populated |
| Conversion tracking | `ctx.yavio.conversion("purchase", { value: 29.99, currency: "USD" })` | Event has `conversion_value=29.99`, `conversion_currency=USD` |
| Identify propagation | `yavio.identify(userId, traits)` → subsequent events | All events after identify carry `user_id`, `user_traits` populated |
| Conversion tracking | `yavio.conversion("purchase", { value: 29.99, currency: "USD" })` | Event has `conversion_value=29.99`, `conversion_currency=USD` |
| Materialized views | Insert events → wait for MV refresh → query `sessions_mv` and `users_mv` | Aggregated rows match expected counts and sums |

### Dashboard Query Accuracy
Expand Down
8 changes: 4 additions & 4 deletions .specs/metrics/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Fires on `tools/list` request interception. Tracks how often and when clients re

### step

Funnel progression point. Called via `ctx.yavio.step()`.
Funnel progression point. Called via `yavio.step()`.

| Field | Type | Description |
|-------|------|-------------|
Expand All @@ -109,7 +109,7 @@ Funnel progression point. Called via `ctx.yavio.step()`.

### track

Generic custom event. Called via `ctx.yavio.track()`.
Generic custom event. Called via `yavio.track()`.

| Field | Type | Description |
|-------|------|-------------|
Expand All @@ -118,7 +118,7 @@ Generic custom event. Called via `ctx.yavio.track()`.

### conversion

Revenue attribution event. Called via `ctx.yavio.conversion()`.
Revenue attribution event. Called via `yavio.conversion()`.

| Field | Type | Description |
|-------|------|-------------|
Expand All @@ -129,7 +129,7 @@ Revenue attribution event. Called via `ctx.yavio.conversion()`.

### identify

User identification event. Called via `ctx.yavio.identify()`.
User identification event. Called via `yavio.identify()`.

| Field | Type | Description |
|-------|------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion .specs/sdk/react-widget-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
The React entry point (`@yavio/sdk/react`) provides a hook-based API for tracking user interactions inside ChatGPT App widgets. It shares the same method names as the server-side API (including `.identify()`) but sends events directly to the Yavio ingestion API over HTTP.

> **Same API, Different Transport**
> Server-side: `ctx.yavio.step()` buffers events and flushes to the ingestion API in batches. Widget-side: `useYavio().step()` does the same — buffers events in memory and sends them via HTTP POST directly to the ingestion API. Configuration (API key, endpoint, traceId) is auto-injected by the server via `window.__YAVIO__`. The developer calls `useYavio()` with no arguments.
> Server-side: `yavio.step()` buffers events and flushes to the ingestion API in batches. Widget-side: `useYavio().step()` does the same — buffers events in memory and sends them via HTTP POST directly to the ingestion API. Configuration (API key, endpoint, traceId) is auto-injected by the server via `window.__YAVIO__`. The developer calls `useYavio()` with no arguments.

## 4.2 Developer-Facing API

Expand Down
16 changes: 8 additions & 8 deletions .specs/sdk/server-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,22 @@ For business-level telemetry that the proxy cannot infer automatically, develope
server.registerTool("book_room", {
description: "Book a hotel room",
inputSchema: { userId: z.string(), roomType: z.string() },
}, async (params, ctx) => {
}, async (params) => {
// User identification: tie this session to a known user
ctx.yavio.identify(params.userId, { plan: "premium", country: "DE" });
yavio.identify(params.userId, { plan: "premium", country: "DE" });

const rooms = await searchRooms(params);

// Funnel step: mark progress through a user journey
ctx.yavio.step("rooms_found", { count: rooms.length });
yavio.step("rooms_found", { count: rooms.length });

// Custom event: track anything not auto-captured
ctx.yavio.track("cache_hit", { provider: "memory" });
yavio.track("cache_hit", { provider: "memory" });

const booking = await confirmBooking(rooms[0]);

// Conversion: revenue attribution
ctx.yavio.conversion("booking_completed", {
yavio.conversion("booking_completed", {
value: booking.price,
currency: "EUR",
});
Expand All @@ -94,8 +94,8 @@ server.registerTool("book_room", {
});

// The deprecated tool() API is also supported
server.tool("book_room", async (params, ctx) => {
ctx.yavio.identify(params.userId);
server.tool("book_room", async (params) => {
yavio.identify(params.userId);
// ...
});
```
Expand Down Expand Up @@ -284,7 +284,7 @@ The `traceId` is the correlation key that stitches server-side and widget-side e
2. AI platform invokes MCP tool → `withYavio()` proxy intercepts
3. Proxy generates traceId (e.g., `"tr_" + nanoid(21)`) and inherits `session_id` from the current MCP connection (see [Section 3.7](#37-session-lifecycle))
4. Proxy starts AsyncLocalStorage context with traceId and sessionId
5. Tool handler executes (`ctx.yavio.step()` calls inherit traceId)
5. Tool handler executes (`yavio.step()` calls inherit traceId via AsyncLocalStorage)
6. If tool returns widget: proxy calls `POST /v1/widget-tokens` with traceId and sessionId to mint a short-lived JWT
7. Proxy injects `window.__YAVIO__` with traceId + sessionId + widget JWT + endpoint (API key stays on server)
8. Widget renders in iframe, initializes `useYavio()` which reads `window.__YAVIO__`
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"db:generate": "pnpm --filter @yavio/db db:generate",
"db:studio": "pnpm --filter @yavio/db db:studio"
},
"pnpm": {
"overrides": {
"hono": ">=4.12.2",
"rollup": ">=4.59.0"
}
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"dotenv-cli": "^11.0.0",
Expand Down
12 changes: 7 additions & 5 deletions packages/docs/content/docs/01-getting-started/01-quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,23 @@ That's it. Every tool call is now automatically captured with latency, status, p

## Track users and conversions

Inside any tool handler, use `ctx.yavio` to identify users and track custom events:
Inside any tool handler, import the `yavio` singleton to identify users and track custom events:

```ts title="server.ts"
import { withYavio, yavio } from "@yavio/sdk";

instrumented.tool(
"checkout",
{ userId: { type: "string" }, plan: { type: "string" } },
async ({ userId, plan }, ctx) => {
async ({ userId, plan }) => {
// Tie this session to a known user
ctx.yavio.identify(userId, { plan });
yavio.identify(userId, { plan });

// Track a custom event
ctx.yavio.track("plan_selected", { plan });
yavio.track("plan_selected", { plan });

// Record a conversion with monetary value
ctx.yavio.conversion("upgrade", {
yavio.conversion("upgrade", {
value: 49,
currency: "USD",
});
Expand Down
43 changes: 23 additions & 20 deletions packages/docs/content/docs/02-sdk/02-tracking-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,47 @@ description: Use .identify(), .step(), .track(), and .conversion() to capture us

import { Callout } from "fumadocs-ui/components/callout";

The tracking API is available inside tool handlers via `ctx.yavio`, and outside handlers via the `yavio` module singleton.
The tracking API is available via the `yavio` singleton, which you import from `@yavio/sdk`.

## Accessing the context

### Inside tool handlers

The `ctx.yavio` object is injected into every tool handler callback. Both the deprecated `tool()` and the modern `registerTool()` APIs are supported:
Import the `yavio` singleton and call its methods from within any tool handler wrapped by `withYavio()`:

```ts
import { withYavio, yavio } from "@yavio/sdk";

// Using registerTool (recommended, MCP SDK v1.26+)
instrumented.registerTool("search", {
description: "Search for items",
inputSchema: { query: { type: "string" } },
}, async ({ query }, ctx) => {
ctx.yavio.identify("user_123");
ctx.yavio.track("search_executed", { query });
}, async ({ query }) => {
yavio.identify("user_123");
yavio.track("search_executed", { query });

return { content: [{ type: "text", text: "Results..." }] };
});

// Using tool (deprecated but still supported)
instrumented.tool("search", { query: { type: "string" } }, async ({ query }, ctx) => {
ctx.yavio.identify("user_123");
ctx.yavio.track("search_executed", { query });
instrumented.tool("search", { query: { type: "string" } }, async ({ query }) => {
yavio.identify("user_123");
yavio.track("search_executed", { query });

return { content: [{ type: "text", text: "Results..." }] };
});
```

### Outside tool handlers

For code that runs outside of a tool handler (e.g., middleware or background tasks within the same async context), import the `yavio` singleton:
The `yavio` singleton also works in any code that runs within the same async context as a tool handler (e.g., helper functions called from a tool handler):

```ts
import { yavio } from "@yavio/sdk";

// Works within the same async context as a tool handler
yavio.track("background_task_completed");
function processResults(results: Result[]) {
yavio.track("results_processed", { count: results.length });
}
```

<Callout type="warn">
Expand All @@ -54,7 +57,7 @@ yavio.track("background_task_completed");
Ties the current session to a known user ID. Call this as early as possible in your tool handler.

```ts
ctx.yavio.identify(userId: string, traits?: Record<string, unknown>): void
yavio.identify(userId: string, traits?: Record<string, unknown>): void
```

### Parameters
Expand All @@ -71,7 +74,7 @@ ctx.yavio.identify(userId: string, traits?: Record<string, unknown>): void
- All subsequent events in the session include the `user_id` and traits.

```ts
ctx.yavio.identify("user_42", {
yavio.identify("user_42", {
plan: "enterprise",
company: "Acme Corp",
});
Expand All @@ -84,7 +87,7 @@ See [User Identification](/docs/07-concepts/05-user-identification) for a deeper
Records a named step in a multi-step workflow. Steps are auto-numbered within a trace for funnel analysis.

```ts
ctx.yavio.step(name: string, meta?: Record<string, unknown>): void
yavio.step(name: string, meta?: Record<string, unknown>): void
```

### Parameters
Expand All @@ -97,8 +100,8 @@ ctx.yavio.step(name: string, meta?: Record<string, unknown>): void
### Example

```ts
instrumented.tool("wizard", { step: { type: "string" } }, async ({ step }, ctx) => {
ctx.yavio.step(step, { page: 1 });
instrumented.tool("wizard", { step: { type: "string" } }, async ({ step }) => {
yavio.step(step, { page: 1 });

// ... handle the step

Expand All @@ -113,7 +116,7 @@ Steps generate events with `event_type: "step"` and an auto-incrementing `step_s
Records a custom event with arbitrary properties.

```ts
ctx.yavio.track(event: string, properties?: Record<string, unknown>): void
yavio.track(event: string, properties?: Record<string, unknown>): void
```

### Parameters
Expand All @@ -126,7 +129,7 @@ ctx.yavio.track(event: string, properties?: Record<string, unknown>): void
### Example

```ts
ctx.yavio.track("document_generated", {
yavio.track("document_generated", {
format: "pdf",
pages: 12,
});
Expand All @@ -139,7 +142,7 @@ Custom events use `event_type: "track"` with the event name in the `event_name`
Records a conversion event with monetary value. Used for revenue attribution and ROI analysis.

```ts
ctx.yavio.conversion(
yavio.conversion(
name: string,
data: {
value: number;
Expand All @@ -161,7 +164,7 @@ ctx.yavio.conversion(
### Example

```ts
ctx.yavio.conversion("subscription_upgrade", {
yavio.conversion("subscription_upgrade", {
value: 99,
currency: "USD",
meta: { plan: "enterprise", annual: true },
Expand Down
10 changes: 5 additions & 5 deletions packages/docs/content/docs/07-concepts/01-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,23 @@ Fires on `tools/list` requests. Tracks how often clients request the tool catalo

## Server-Side Explicit Events

These events require developer code using `ctx.yavio` within a tool handler:
These events require developer code using `yavio` within a tool handler:

### step

Funnel progression point. Called via `ctx.yavio.step("step_name")`. Each step receives an auto-incrementing `step_sequence` for deterministic ordering.
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 `ctx.yavio.track("event_name", properties)`. An escape hatch for anything not auto-captured.
Generic custom event. Called via `yavio.track("event_name", properties)`. An escape hatch for anything not auto-captured.

### conversion

Revenue attribution event. Called via `ctx.yavio.conversion("name", { value, currency })`. Powers ROI analytics.
Revenue attribution event. Called via `yavio.conversion("name", { value, currency })`. Powers ROI analytics.

### identify

User identification event. Called via `ctx.yavio.identify(userId, traits)`. Ties the session to a known user.
User identification event. Called via `yavio.identify(userId, traits)`. Ties the session to a known user.

## Widget Auto-Captured Events

Expand Down
4 changes: 2 additions & 2 deletions packages/docs/content/docs/07-concepts/03-traces.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Here's what happens when a user interacts with your MCP app:
1. User sends a prompt to the AI platform (ChatGPT, Claude, Cursor, etc.)
2. The AI platform invokes an MCP tool — `withYavio()` intercepts the call
3. The proxy generates a `trace_id` (e.g., `tr_` + a random ID) and inherits the `session_id` from the current MCP connection
4. The tool handler executes — any `ctx.yavio.step()` calls inherit the trace ID
4. The tool handler executes — any `yavio.step()` calls inherit the trace ID
5. If the tool returns a widget response, the proxy mints a short-lived widget JWT
6. The proxy injects `window.__YAVIO__` with the trace ID, session ID, widget JWT, and endpoint
7. The widget renders and initializes `useYavio()`, which reads the injected config
Expand Down Expand Up @@ -53,7 +53,7 @@ Both server and widget events share the same `session_id`. The `trace_id` provid

## Step Sequencing

When you call `ctx.yavio.step()` or `useYavio().step()`, each step receives an auto-incrementing `step_sequence` number. On the server, the sequence starts at 0 and increments with each step call within the trace. On the widget, the sequence continues from where the server left off (the starting value is passed via the trace ID context).
When you call `yavio.step()` or `useYavio().step()`, each step receives an auto-incrementing `step_sequence` number. On the server, the sequence starts at 0 and increments with each step call within the trace. On the widget, the sequence continues from where the server left off (the starting value is passed via the trace ID context).

This enables deterministic ordering even when multiple steps share the same millisecond timestamp, and ensures the combined funnel view displays steps in the correct order across server and widget boundaries.

Expand Down
Loading