Yavio

React Widget SDK

Track user interactions inside ChatGPT App widgets with useYavio()

Installation

The React widget SDK is included in the same @yavio/sdk package — import from the /react subpath:

npm install @yavio/sdk

Peer dependencies: react >= 18, react-dom >= 18.

Quick start

import { useYavio } from "@yavio/sdk/react";

function BookingWidget({ userId }: { userId: string }) {
  const yavio = useYavio(); // zero config — auto-detects injected config

  const handleSelect = (room: { type: string }) => {
    yavio.step("room_selected", { roomType: room.type });
  };

  const handleBooking = (booking: { price: number }) => {
    yavio.conversion("booking_completed", {
      value: booking.price,
      currency: "EUR",
    });
  };

  return <div>{/* widget UI */}</div>;
}

The widget SDK uses the same method names as the server SDK: .identify(), .step(), .track(), and .conversion(). See the Tracking API for method signatures.

How configuration works

The server-side withYavio() proxy automatically injects widget configuration into every tool result. The widget SDK picks it up without any manual setup.

Server-side injection

After each tool handler returns, the proxy:

  1. Mints a short-lived JWT via POST /v1/widget-tokens
  2. Injects { token, endpoint, traceId, sessionId } into the result's _meta.yavio field

The project API key never reaches the widget — only the short-lived JWT is forwarded.

Client-side detection

useYavio() resolves configuration from the first available source:

PrioritySourceDescription
1window.__YAVIO__Global injected by the server into the widget HTML bundle
2<meta name="yavio-config">JSON meta tag for non-standard setups
3_meta.yavio / .yavioExtracted from tool result metadata passed to the hook
4Explicit configuseYavio({ token, endpoint, traceId, sessionId })

If no configuration is found, the hook returns a no-op instance that silently discards all events.

Using with tool results

When your widget receives a tool result containing _meta.yavio, you can pass it directly to useYavio():

import { useYavio } from "@yavio/sdk/react";

function Widget({ toolResult }: { toolResult: Record<string, unknown> }) {
  // Config is extracted from toolResult._meta.yavio automatically
  const yavio = useYavio(toolResult);

  return <div>{/* widget UI */}</div>;
}

extractWidgetConfig()

For cases where you need to extract the config separately (e.g., to check if it exists before rendering):

import { extractWidgetConfig, useYavio } from "@yavio/sdk/react";

function Widget({ toolResult }: { toolResult: Record<string, unknown> }) {
  const config = extractWidgetConfig(toolResult);
  const yavio = useYavio(config ?? undefined);

  if (!config) {
    return <div>Analytics not available</div>;
  }

  return <div>{/* widget UI */}</div>;
}

extractWidgetConfig() checks for config at .yavio (Skybridge responseMetadata) or ._meta.yavio (raw MCP tool result) and returns a validated WidgetConfig or null.

Noop-to-active upgrade

If the hook is first called without config (e.g., tool result not yet available), it starts in no-op mode. When a valid config arrives on a subsequent render, the hook automatically upgrades to an active widget with a real transport.

function Widget({ toolResult }: { toolResult?: Record<string, unknown> }) {
  // First render: no config → no-op mode
  // After tool result arrives: upgrades to active mode
  const yavio = useYavio(toolResult);

  yavio.track("widget_loaded"); // silently discarded until config arrives
  return <div />;
}

Transport behavior

ParameterValue
Flush interval5 seconds
Early flush20 buffered events
Buffer cap200 events (oldest dropped on overflow)
Retry3 attempts with exponential backoff (1s, 2s, 4s)
Auth failure401 stops retrying immediately
Teardownnavigator.sendBeacon() on visibilitychange and pagehide
AuthenticationAuthorization: Bearer <widgetJwt> (short-lived, trace-scoped)

Auto-captured events

The widget SDK automatically captures these events when initialized:

EventTrigger
widget_renderOnce on init (viewport, device info, timezone)
widget_errorwindow.onerror + unhandledrejection
widget_clickClick/tap on any element
widget_scrollThrottled scroll (250ms)
widget_form_fieldFocus/blur on inputs
widget_form_submitForm submission
widget_link_clickAnchor element clicks
widget_navigationpopstate + History API
widget_focusWidget focus/blur
widget_visibilityIntersectionObserver
widget_performancePerformanceObserver (navigation timing)
widget_rage_click3+ clicks within 500ms on same element

Auto-capture uses feature detection — unsupported APIs are silently skipped.

Session sharing

Widget events share the server's session ID. The traceId correlates a specific tool call with the widget it spawned, while the shared sessionId ensures session-level analytics (duration, tool count, retention) include both server and widget activity.

Session (ses_xxx) ← shared by server + widget
  ├── Trace 1 (tr_aaa) ← tool_call "search_rooms"
  │   ├── step: "rooms_found"           (source: server)
  │   ├── widget_render                  (source: widget)
  │   └── conversion: "booking_completed" (source: widget)
  └── Trace 2 (tr_bbb) ← tool_call "cancel_booking"

Security

  • The widget never receives the project API key
  • The JWT is trace-scoped, write-only, and expires after 15 minutes
  • window.__YAVIO__ is deleted after reading to reduce XSS exposure
  • Events from widget JWTs are rate-limited per token

On this page