# React (1.5.0) > Web SDK Integration Guide - add the Revrag AI voice agent to React, Next.js and Preact apps (@revrag-ai/embed-react 1.5.0) URL: /embed/integration/react/v1.5.0 Markdown: /embed/integration/react/v1.5.0.md # Revrag Embed — Web Integration Guide [#revrag-embed--web-integration-guide] The complete guide for adding the Revrag AI voice agent to a web app. | | | | ---------------- | --------------------------------------------------------------------------------------------------------------------- | | **What you get** | One floating button that follows your user across every route, and a voice agent that can see your page and act on it | | **Package** | `@revrag-ai/embed-react` `1.5.0` | | **React** | 17, 18 or 19 | | **Next.js** | App Router and Pages Router | | **Preact** | 10, via `preact/compat` | | **TypeScript** | Types included | | **API key** | Get it from [https://app.revrag.ai](https://app.revrag.ai) | **This guide documents `1.5.0`. Do not follow it against `1.4.4` or earlier.** Earlier versions (up to `1.4.4`) predate most of it. They have: * no `/react17` or `/diagnostics` entry * no `appVersion` option * no Action Intelligence, no call controls and no agent events * no `EmbedLogout()` **Every web app integrates in the same three steps** — a Vite or CRA single-page app, a Next.js site on either router, React 17 or Preact: | Step | What you do | Details | | ----------------- | ------------------------------------------- | ----------------------------------------- | | 1. **Initialize** | Call `useInitialize` once, at your app root | [§2](#2-initialize) | | 2. **Mount** | Wrap your app in one `EmbedProvider` | [§3](#3-mount) | | 3. **Identify** | Send your user (`USER_DATA`) after login | [Identify your user](#identify-your-user) | **[Section 5](#5-your-apps-shape) shows the exact setup for your app.** Everything before it applies to every app. *** ## Table of contents [#table-of-contents] 1. [Install](#1-install) — package and stylesheet 2. [Initialize](#2-initialize) — one hook, once, at the root 3. [Mount](#3-mount) — one provider, and the SDK follows the user 4. [Decide where it appears](#4-decide-where-it-appears) — paths, groups, delays, and the call licence 5. [Your app's shape](#5-your-apps-shape) — **pick one**, complete code 6. [Control it from your own code](#6-control-it-from-your-own-code) — calls and the widget card 7. [Events and analytics](#7-events-and-analytics) — what you send, what you can listen to 8. [How paths become screen names](#8-how-paths-become-screen-names) 9. [Verify it works](#9-verify-it-works) 10. [Troubleshooting](#10-troubleshooting) 11. [Known limitations](#11-known-limitations) 12. [Checklist](#12-checklist) * [Reference](#reference) *** ## 1. Install [#1-install] Install the package, import the stylesheet once, and check the peer dependencies and requirements. ### Package [#package] ```bash npm install @revrag-ai/embed-react # or yarn add @revrag-ai/embed-react pnpm add @revrag-ai/embed-react ``` ### Stylesheet — required [#stylesheet--required] Import it once, at your app's entry point: ```ts import '@revrag-ai/embed-react/style.css'; ``` * **Without it, the button looks broken.** It gets only a minimal fallback (position and layout). * **The path is `@revrag-ai/embed-react/style.css`.** The old `@revrag-ai/embed-react/dist/style.css` path does not resolve. ### Peer dependencies [#peer-dependencies] | Package | Range | Required | Notes | | -------------------- | ---------- | -------- | --------------------------------------------------------------------- | | `react`, `react-dom` | `>=17.0.0` | Yes | React 17 has its own entry on webpack hosts — see [5D](#5d--react-17) | | `next` | `>=14.0.0` | Optional | The SDK never imports `next` | | `react-router-dom` | `>=6.0.0` | Optional | The SDK never imports it | * **`next`** — on **Next.js 12 or 13**, npm refuses the install with `ERESOLVE`. Use `npm install --legacy-peer-deps` until the range is widened. * **`react-router-dom`** — React Router **5** hits the same `ERESOLVE`. ### Requirements [#requirements] | | | | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | | React | 17, 18 or 19. Preact 10 through `preact/compat` | | Browser | Any current browser with WebRTC and `getUserMedia` | | Page | A **secure context** — HTTPS, or `localhost` in development. Browsers refuse the microphone anywhere else | | Rendering | Safe to server-render: nothing touches `window` at import, and the button renders a hidden placeholder until it hydrates | | TypeScript | `moduleResolution: "bundler"` or `"nodenext"`. With `"node"`, the `/react17` subpath needs a `paths` entry ([5D](#5d--react-17)) | | API key | From Revrag, per environment | **Your API key and environment are a pair.** * The SDK talks to `https://embed.revrag.ai` unless you pass `baseUrl`. * A development key used against production (or the reverse) fails initialization. * `useInitialize` returns the reason in `error`, and the button never appears. ### What it pulls in [#what-it-pulls-in] * **`livekit-client` (WebRTC)** — loaded only when a call starts. * **`lottie-web`** — bundled into a separate chunk for the avatar animation. * **`framer-motion`** — drives the widget's animations on the default entry; the React 17 entry replaces it with a small built-in stub. * **Expect a noticeable first-load cost.** Most of it is the default avatar animation, which ships in the main bundle today. **Read this before pinning versions.** Five known-good stacks (React 19, Next.js App Router, React 17, React 17 + Next.js 12, Preact), the peer ranges, everything the SDK installs into your app, and the workarounds for hosts outside the ranges — like Next.js 12–13 or React 17 on webpack. *** ## 2. Initialize [#2-initialize] Initialize once at the root, identify your user after login, and call `EmbedLogout()` when they sign out. * **Call `useInitialize` once**, in a component that stays mounted for the whole session — your app root, or the root layout. * **Never call it inside a route or page component.** ```tsx import { useInitialize } from '@revrag-ai/embed-react'; const API_KEY = 'YOUR_API_KEY'; const APP_VERSION = '4.2.0'; // the version your dashboard config is provisioned for export function App() { const { isInitialized, error } = useInitialize(API_KEY, { appVersion: APP_VERSION }); if (error) console.error('[Revrag] init failed:', error); return ; // your app; see section 3 for the provider } ``` **`appVersion` is required.** * **Without it, initialization fails** with `app_version is required…` and the widget never appears. * **The value must match a version your dashboard knows.** Revrag provisions the widget's look and behaviour per platform **and per app version**. * **Source it from your build:** `package.json`, a build id, or an environment variable. ### Options [#options] | Option | Default | Meaning | | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------ | | `apiKey` (first argument) | — | Your tenant's key, for this environment | | `appVersion` | — | **Required.** Selects the dashboard config for this version | | `baseUrl` | `https://embed.revrag.ai` | The Revrag backend. Omit it in production; pass the matching URL for any other environment | | `enableTracker` | `true` | Click and route tracking, and an anonymous user id until you send a real one (see below) | ### What it returns [#what-it-returns] | Field | Type | Meaning | | --------------- | ---------------- | ------------------------------------------------------------- | | `isInitialized` | `boolean` | The handshake with the backend succeeded | | `isLoading` | `boolean` | The handshake is in flight | | `error` | `string \| null` | Why initialization failed — the only place a failure surfaces | **Three things worth knowing:** * **It runs once per API key.** * Changing `options` after the first render is ignored. * A failed initialization is **not retried** until the key changes or the component remounts — so do not render the hook before you know `appVersion`. * The handshake gives up after **5 seconds**, so on a very slow network it can fail for the rest of that mount. * **Unmounting it stops tracking.** * Leaving the component that called `useInitialize` disposes the SDK's click and route trackers; the button and calls keep working. * Keep it at or above the component that renders `EmbedProvider`. * **Every version bump is a dashboard change.** * On the web you deploy often; a deploy that bumps `appVersion` to a version the dashboard has no config for makes the widget disappear. * Put "dashboard config exists for this version" on your release checklist. ### Identify your user [#identify-your-user] After login, and after `isInitialized` is `true`, tell the SDK who the user is: ```tsx import { useEffect } from 'react'; import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; export function useRevragIdentity(isInitialized: boolean, user: { id: string; name?: string } | null) { useEffect(() => { if (!isInitialized || !user) return; void embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: user.id, name: user.name }, }); }, [isInitialized, user?.id]); } ``` * **`app_user_id` is the important one** — your stable id for this user. Everything the agent does is attributed to it. * **Before login, calls still work.** With `enableTracker` on (the default), the SDK registers an anonymous id (`user__`) during initialization. Your `USER_DATA` replaces it. * **With `enableTracker: false`**, there is no anonymous id: a call started before `USER_DATA` fails with `app_user_id is required`. * **Send it after initialization.** Sent earlier, the request fails with `SDK not initialized`. The id is still stored, but the backend never hears about this user until the next event. ### On logout [#on-logout] When your user signs out, call `EmbedLogout()`: ```ts import { EmbedLogout } from '@revrag-ai/embed-react'; async function signOut() { await EmbedLogout(); // …your own sign-out } ``` It does four things, in order: 1. **Ends the call**, including one that is still connecting. 2. **Stops the agent**, cancelling anything it was doing on the page. 3. **Wipes this user's cached screens** (IndexedDB `revrag_embed_cache`). Cached screens are tagged with the user id when they upload, so anything left behind would upload under the *next* user's id. 4. **Forgets the user id**, from memory and from `localStorage`. **Worth knowing:** * **Your API key and the widget's config stay**, so you do not initialize again. * **Send `USER_DATA` for the next user** as soon as they log in. * **`EmbedLogout()` never rejects** and is safe to call twice, or when no call is running. * **Until the next `USER_DATA`, calls cannot start** (`app_user_id is required`). * The anonymous id that initialization creates is only made on a page load. * A full reload to your login page restores it, if you want calls to work before login. * **Same function as React Native**, which also exports `EmbedLogout()`. *** ## 3. Mount [#3-mount] `EmbedProvider` is the only component you render. Put it once, around your app. * **Put it in a component that persists across navigation.** * **Put it inside your router**, so it can read the current path. ```tsx import { EmbedProvider } from '@revrag-ai/embed-react'; import { useLocation } from 'react-router-dom'; export function RevragShell({ children }: { children: React.ReactNode }) { const { pathname } = useLocation(); return {children}; } ``` * **The provider renders the button itself.** Do not also render `` — that mounts a second widget with a second call manager. * **Mount it once.** Two providers (two React roots, a micro-frontend that mounts its own) means two widgets that drift apart. * **Always pass the path.** Without `currentPath` or `usePathHook`, the SDK falls back to `window.location.pathname`: * The SDK only re-reads it on browser back/forward. * Client-side navigations are missed until something re-renders the provider. ### Full prop list [#full-prop-list] Every prop, with its default: ```tsx string, called as a hook on every render // Where the button appears (section 4) — define these OUTSIDE your component showOnAllScreens={true} // true: excludeScreens apply. false: excludeScreens are ignored includeScreens={[]} // paths where the button may appear; empty (and no groups) = everywhere excludeScreens={[]} // paths where it never appears; landing on one ends a call matchMode="exact" // 'exact' | 'startsWith', for includeScreens embedButtonDelayMs={0} // entrance delay on every path change embedButtonVisibilityConfig={undefined} // journeys: groups, continuity, per-group delays // Action Intelligence getScreenName={undefined} // () => string | null | undefined — names a screen the URL can't actionRootRef={undefined} // RefObject — narrow what the agent sees > {children} ``` The props type is not exported by name; use `React.ComponentProps` if you need it. **The provider does more than render the button.** * It keeps the agent's highlight overlay mounted even where the button is hidden, and starts Action Intelligence for the session. * It wraps its own UI in an error boundary: if the widget ever fails to render, it hides itself and logs `[RevRag] The voice widget crashed and was hidden; your app is unaffected.` * Your own components are never inside that boundary. *** ## 4. Decide where it appears [#4-decide-where-it-appears] Choose which paths show the button, which journeys keep a call alive, and any entrance delays. * **You do not register screens; you decide about paths.** * **A path is `location.pathname`** (or whatever you pass as `currentPath`), matched **exactly and case-sensitively**. ```tsx import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-react'; // Module scope — never inline these in JSX (see the callout below). // Never show the button on these, and END a live call when the user lands on one. // Matches the path itself and anything below it: '/settings' also covers '/settings/pin'. const EXCLUDED = ['/login', '/otp', '/settings']; // Optional allow-list. Empty means "everywhere not excluded" — but only while you // define no groups: group screens join this list (see the warning below). const INCLUDED: string[] = []; const VISIBILITY: EmbedButtonVisibilityConfig = { defaultDelayMs: 0, groups: [ // A journey: the button stays put — no re-delay, and a live call keeps running — // while the user moves between these paths. { id: 'loan', screens: ['/loans', '/loans/apply', '/loans/review'], continuity: 'continuous' }, // A per-screen group: re-evaluated on every path change. { id: 'help', screens: ['/help'], continuity: 'perScreen', delayMs: 3000, delayPolicy: 'oncePerAppSession' }, ], }; ``` ### What this buys you, with no further code [#what-this-buys-you-with-no-further-code] | User does | Widget does | | --------------------------------------------- | -------------------------------------------------------------------- | | Moves between paths in one `continuous` group | Stays visible — no blink, no re-delay; **a live call keeps running** | | Moves between allowed paths with **no** delay | Stays visible; a live call keeps running | | Enters a path that has an entrance delay | Hides for the delay, then appears — **a live call ends** | | Opens a path outside `includeScreens` | Hides — **a live call ends** | | Opens a path in `excludeScreens` | Hides — **a live call ends**, and the agent stops seeing the page | | Reloads the page | Starts fresh: the call ends; the SDK initializes again | **Where the button disappears, the call ends.** * On the web the call lives inside the button, so *any* hide ends it — an excluded path, a path outside `includeScreens`, or an entrance delay. * To keep a call alive across a journey, put those paths in one `continuous` group and give them no delay. * This is stricter than the mobile SDKs, where only exclusion ends a call; see [§11](#11-known-limitations). **Defining a group turns on the allow-list.** * Every group's `screens` are added to `includeScreens`. So once you define any group, the button appears **only** on the paths in your groups and in `includeScreens`. * Everywhere else it is hidden, ending a live call. The example above shows the button only on the three loan paths and `/help`. * To keep it on other paths too, add them to `includeScreens` (with `matchMode="startsWith"`, a section prefix like `'/accounts'` covers everything below it). **Define `includeScreens` and `embedButtonVisibilityConfig` outside your component.** * The provider re-checks visibility whenever their *identity* changes. * Written inline in JSX, they are new on every render — and with a delay configured, every parent re-render hides the button again, ending a live call. * `excludeScreens` and `showOnAllScreens` are compared by content, so inline values are harmless there; but a change to them takes effect on the next path change, not immediately. ### All options [#all-options] **`EmbedProvider` visibility props** | Prop | Default | Meaning | | ----------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `includeScreens` | `[]` | Paths where the button may appear. **Empty means everywhere** (that is not excluded). Group `screens` count as included too | | `excludeScreens` | `[]` | Paths where it never appears. Matches the path or anything below it. Wins over includes and groups — **unless `showOnAllScreens` is `false`, which ignores it** | | `showOnAllScreens` | `true` | Despite the name, it does not force the button on. `true`: `excludeScreens` apply. `false`: `excludeScreens` are **ignored** | | `matchMode` | `'exact'` | How `includeScreens` match: `'exact'`, or `'startsWith'` (the path, or anything below it) | | `embedButtonDelayMs` | `0` | Entrance delay in ms on every path change, unless a group or `defaultDelayMs` says otherwise | | `embedButtonVisibilityConfig` | — | Groups (journeys) and a default delay | * **`excludeScreens`** — landing on one ends a live call and stops the agent seeing the page. * **`showOnAllScreens`** — effect on the paths the agent may see: * `true`: the agent may see every path except `excludeScreens`. * `false`: `includeScreens` (plus group screens) also limits which paths the agent may see — always by prefix, whatever `matchMode` says. **`EmbedButtonVisibilityConfig`** | Field | Default | Meaning | | ---------------- | ------- | -------------------------------------------------------------------------- | | `defaultDelayMs` | — | Delay for paths that no group sets one for. Wins over `embedButtonDelayMs` | | `groups` | `[]` | Journeys | **`EmbedButtonGroupConfig`** | Field | Default | Meaning | | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | — | Any stable string | | `screens` | — | The paths in this journey, matched **exactly** | | `continuity` | — (required) | `'continuous'` keeps the button and a live call across the group; `'perScreen'` re-evaluates on every path | | `delayMs` | — | Entrance delay for this group. Wins over `defaultDelayMs` and `embedButtonDelayMs` | | `delayPolicy` | `'perScreen'` | `'perScreen'` (every path change), `'oncePerGroupEntry'` (only when entering the group), `'oncePerAppSession'` (once per group until the page reloads or the provider remounts) | * **`screens`** — membership also matches the last path segment, so `'apply'` matches `/loans/apply` — but the button only shows there if the full path is also included. * **`delayPolicy`** — for `'oncePerAppSession'`, it counts only once the delay has finished. Delay precedence: **group `delayMs` → `defaultDelayMs` → `embedButtonDelayMs`.** **A path in `includeScreens` that your app never reports does not error — it silently hides the button there.** * Log the path you pass ([§8](#8-how-paths-become-screen-names)) and write the lists against it. *** ## 5. Your app's shape [#5-your-apps-shape] Find your app below. Each is complete and copy-pasteable. | Your app | Go to | | ----------------------------------------------------------------- | ---------------------------------------------------- | | Vite or CRA single-page app with React Router | [5A](#5a--single-page-app-with-react-router) | | Next.js, App Router | [5B](#5b--nextjs-app-router) | | Next.js, Pages Router | [5C](#5c--nextjs-pages-router) | | React 17 (any bundler) | [5D](#5d--react-17) | | Preact | [5E](#5e--preact) | | No router — tabs, steps or wizards in state | [5F](#5f--no-router) | | Several React roots, micro-frontends, iframes, or full page loads | [5G](#5g--several-roots-iframes-and-multi-page-apps) | ### 5A — Single-page app with React Router [#5a--single-page-app-with-react-router] ```tsx // src/main.tsx — the entire integration import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom'; import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; const API_KEY = import.meta.env.VITE_REVRAG_API_KEY as string; const APP_VERSION = import.meta.env.VITE_APP_VERSION as string; // 1) Visibility config at module scope (section 4). const EXCLUDED = ['/login', '/otp']; // 2) The provider lives INSIDE the router, so it can read the path. function RevragShell({ children }: { children: React.ReactNode }) { const { pathname } = useLocation(); return ( {children} ); } // 3) Initialize once, in the component that never unmounts. function App() { useInitialize(API_KEY, { appVersion: APP_VERSION }); return ( } /> } /> } /> ); } createRoot(document.getElementById('root')!).render( ); ``` * **Your pages stay exactly what they are** — no Revrag code in them. * **`HashRouter` works too:** pass `useLocation().pathname`, which is the part after `#`. **Dynamic segments: `useLocation().pathname` is the *real* path.** * It is `/orders/8812`, not `/orders/:id`. * Allow a whole subtree with `matchMode="startsWith"` and `includeScreens={['/orders']}`. * Or exclude it with `excludeScreens={['/orders']}` (exclusion always covers everything below). ### 5B — Next.js, App Router [#5b--nextjs-app-router] * **`"use client"` ships inside the package**, so the components work from Server Components. * **`useInitialize` and `usePathname` are hooks**, so they still need one small client component: ```tsx // app/revrag-root.tsx 'use client'; import type { ReactNode } from 'react'; import { usePathname } from 'next/navigation'; import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react'; const API_KEY = process.env.NEXT_PUBLIC_REVRAG_API_KEY as string; const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION as string; const EXCLUDED = ['/login']; export function RevragRoot({ children }: { children: ReactNode }) { useInitialize(API_KEY, { appVersion: APP_VERSION }); const pathname = usePathname(); return ( {children} ); } ``` ```tsx // app/layout.tsx — a Server Component import type { ReactNode } from 'react'; import '@revrag-ai/embed-react/style.css'; import { RevragRoot } from './revrag-root'; export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ); } ``` * **Use the root layout.** It persists across navigations, so the provider — and a live call — survive route changes. A provider in a page component remounts on every navigation. * **Expose the version at build time**, e.g. in `next.config`: `env: { NEXT_PUBLIC_APP_VERSION: require('./package.json').version }`. * **`pathname` is the real path** (`/orders/8812`). For dynamic routes, see the note in [5A](#5a--single-page-app-with-react-router). ### 5C — Next.js, Pages Router [#5c--nextjs-pages-router] ```tsx // pages/_app.tsx — the entire integration import type { AppProps } from 'next/app'; import { useRouter } from 'next/router'; import { EmbedProvider, useInitialize } from '@revrag-ai/embed-react'; import '@revrag-ai/embed-react/style.css'; const EXCLUDED = ['/login']; export default function MyApp({ Component, pageProps }: AppProps) { useInitialize(process.env.NEXT_PUBLIC_REVRAG_API_KEY as string, { appVersion: process.env.NEXT_PUBLIC_APP_VERSION as string, }); const router = useRouter(); return ( // router.pathname is the route PATTERN — '/orders/[id]' — ideal for screen lists. ); } ``` * **Use `router.pathname`, not `router.asPath`.** `asPath` carries the query string and hash, so it never matches your lists exactly. * **On Next.js 12 with React 17**, import from `@revrag-ai/embed-react/react17` ([5D](#5d--react-17)), and install with `--legacy-peer-deps` ([§1](#peer-dependencies)). ### 5D — React 17 [#5d--react-17] On **webpack** hosts — Create React App, Next.js 12, custom webpack — import everything from the React 17 entry: ```tsx import { EmbedProvider, useInitialize, embedEvent, EventKeys } from '@revrag-ai/embed-react/react17'; import '@revrag-ai/embed-react/style.css'; ``` * **The API is identical; only the import path changes.** * **The React 17 entry swaps out the animation library** (`framer-motion`), which webpack rejects on React 17, for a small built-in fallback; everything else is the same code. * **On Vite, the default entry also works on React 17.** With TypeScript on `moduleResolution: "node"`, map the subpath: ```json { "compilerOptions": { "paths": { "@revrag-ai/embed-react/react17": ["./node_modules/@revrag-ai/embed-react/index.d.ts"] } } } ``` ### 5E — Preact [#5e--preact] * **Use the regular package through `preact/compat`.** * **With Vite, there is no extra config.** `@preact/preset-vite` already aliases `react`, `react-dom` and `react/jsx-runtime` to `preact/compat`: ```ts // vite.config.ts import { defineConfig } from 'vite'; import preact from '@preact/preset-vite'; export default defineConfig({ plugins: [preact()] }); ``` * **Then integrate exactly as in [5A](#5a--single-page-app-with-react-router).** With Preact Router or `preact-iso`, pass the current path as `currentPath`. * **On webpack, add the three aliases to `resolve.alias` yourself.** * **npm installs `react`/`react-dom` to satisfy the peer range**; the alias means they are never bundled. ### 5F — No router [#5f--no-router] If your "screens" are state — tabs, wizard steps, a modal-driven flow — give each one a synthetic path: ```tsx ``` **Use stable, privacy-safe values:** never a user id, an account number or a session token. ### 5G — Several roots, iframes, and multi-page apps [#5g--several-roots-iframes-and-multi-page-apps] * **Several React roots or micro-frontends:** exactly **one** `EmbedProvider` for the whole page. Render it in the shell that owns navigation; the other roots need no Revrag code. * **The app runs inside an iframe:** the iframe needs `allow="microphone"`, and the agent sees only that iframe's document. * **Your page embeds iframes:** a cross-origin iframe (a payment form, a third-party widget) is one opaque box to the agent. It cannot read or fill it. * **Multi-page app (full page loads):** each page load initializes the SDK again, and a live call ends on every full navigation. Put `useInitialize` and the provider in a layout shared by every page. *** ## 6. Control it from your own code [#6-control-it-from-your-own-code] Start and end calls, and open or close the widget card, from your own code. ### Start and end calls [#start-and-end-calls] ```tsx import { startCall, endCall, isCallActive } from '@revrag-ai/embed-react'; ``` | Function | Returns | Behaviour | | ---------------- | --------------- | -------------------------------------------------------------------------------------------- | | `startCall()` | `Promise` | Connects a call. **Never rejects**, and does nothing if a call is already connecting or live | | `endCall()` | `void` | Ends the call. A no-op when there is none | | `isCallActive()` | `boolean` | `true` while a call is connected | * **`startCall()`** — call errors: * A denied microphone raises [`microphone_permission_denied`](#agent-events--embedonagent--embedoffagent). * A network or token failure raises no event — it logs `Token generation failed` to the console. **Three things worth knowing:** * **Call `startCall()` from a click.** The browser shows the microphone prompt then, and may block audio playback for calls started without a user gesture. * **The button must be on screen.** The call lives in the button: on a path where it is hidden, `startCall()` only warns `[RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted)` and does nothing. * **The dashboard can start calls too — make sure that is what your product wants.** * If the dashboard sets `collapsedView.inactivityBehavior` to `auto_trigger`, the widget expands and **starts a call by itself** once the nudge delay (default 1.5 s) has passed after the button appears. * It does so whatever the user is doing, and again each time the button reappears. * It starts without a click, so the browser may block the microphone prompt or audio. ### The widget card [#the-widget-card] ```tsx import { expandWidget, collapseWidget, isWidgetExpanded, subscribeWidgetExpanded } from '@revrag-ai/embed-react'; expandWidget(); // open the card; returns true if it changed collapseWidget(); // back to the floating button; returns true if it changed isWidgetExpanded(); // boolean useEffect(() => { setOpen(isWidgetExpanded()); // the listener is not called with the current value return subscribeWidgetExpanded((expanded) => setOpen(expanded)); // returns the unsubscribe }, []); ``` * **These only change what is shown.** They start no call and send no analytics. * **Before the dashboard config has loaded**, calling `expandWidget()` or `collapseWidget()` makes the SDK ignore the dashboard's initial collapsed/expanded setting for that page load. *** ## 7. Events and analytics [#7-events-and-analytics] Send your own context to Revrag, and listen to what the widget and the call are doing. There are **two separate systems**: * **Data events** — `embedEvent.event(…)`. *You* send context to Revrag (who the user is, what they did). They go to the backend. * **Agent events** — `embedOnAgent(…)`. *The SDK* tells you what the widget and the call are doing. They stay in the browser (and are also reported to Revrag). ### Sending — `embedEvent.event` [#sending--embedeventevent] ```ts import { embedEvent, EventKeys } from '@revrag-ai/embed-react'; // Who the user is — after login (section 2). await embedEvent.event({ eventKey: EventKeys.USER_DATA, data: { app_user_id: 'user-123', email: 'a@b.co' } }); // Something happened in your product. event_name is required for ANALYTICS_DATA. await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, event_name: 'loan_offer_viewed', data: { amount: 500000 }, context: { screen: '/loans', flow: 'loan_application' }, }); // Free-form context for the agent. await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { cart_value: 1299 } }); ``` * **As long as you pass `data`**, every `embedEvent.event(…)` call resolves with `{ success, error? }` and does not throw. * **Without `data`, the promise rejects.** Pass `{}` if you have nothing. | Key | Wire value | Use it for | | -------------------------- | ---------------- | ------------------------------------------- | | `EventKeys.USER_DATA` | `user_data` | Who the user is. Must carry `app_user_id` | | `EventKeys.ANALYTICS_DATA` | `analytics_data` | Named product events. Requires `event_name` | | `EventKeys.CUSTOM_EVENT` | `custom_event` | Free-form context | **Every key except `USER_DATA` needs a user id** — your `USER_DATA`, or the anonymous one from initialization. #### Watch every event in the browser [#watch-every-event-in-the-browser] Register a listener to see the events you send *and* the ones the SDK raises. `removeCallback` matches on the **same function instance**: ```ts import { embedEvent, type CustomEventPayload } from '@revrag-ai/embed-react'; const onEvent = (event: CustomEventPayload) => console.debug('[revrag event]', event.type, event); embedEvent.addCallback(onEvent); // later embedEvent.removeCallback(onEvent); ``` ### Agent events — `embedOnAgent` / `embedOffAgent` [#agent-events--embedonagent--embedoffagent] | `AgentEvent` | Value | Fires when | | ------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `AGENT_VISIBLE` | `agent_visible` | The button becomes visible on the current path | | `AGENT_CONVERSATION_STARTED` | `agent_conversation_started` | The agent joins a connected call (a moment after `isCallActive()` turns `true`) | | `AGENT_CONVERSATION_ENDED` | `agent_conversation_ended` | A call the agent had joined ends, for any reason. A call that fails before the agent joins raises none | | `POPUP_MESSAGE_VISIBLE` | `popup_message_visible` | The inactivity nudge appears | | `AGENT_TAP_TO_OPEN` / `AGENT_TAP_TO_CLOSE` | `agent_tap_to_open` / `agent_tap_to_close` | The user expands or collapses the widget | | `MICROPHONE_PERMISSION_ALLOWED` | `microphone_permission_allowed` | Microphone access granted | | `MICROPHONE_PERMISSION_DENIED` | `microphone_permission_denied` | Microphone access denied — carries `error_message` and `error_name` | | `GEN_TOOL_TRIGGERED` | `gen_tool_triggered` | Reserved; not emitted on web yet | | `AGENT_CONNECTED` / `AGENT_DISCONNECTED` | `agent_connected` / `agent_disconnected` | **Deprecated**, never emitted. Use the conversation events | **Payload:** * **Every event** has `type`, `timestamp` (ISO string) and `session_id`, plus its own fields. * **The conversation events** add `call_id`, `reason` and `callDuration` (seconds; `0` on start). **`reason` on an end is one of:** | `reason` | Meaning | | -------------------------- | ----------------------------------------------------------- | | `manual_disconnect` | Hung up, or the user landed on an excluded path | | `destroyed` | The button unmounted — see [§4](#4-decide-where-it-appears) | | `room_disconnected` | Network or server | | `participant_disconnected` | The agent left | * **Compare against the `AgentEvent` constants**, never string literals. * **Agent events are only raised while a user id is stored** — your `USER_DATA`, or the anonymous one. ### A drop-in event logger [#a-drop-in-event-logger] Paste this once to see every agent event with a readable label. It is the fastest way to watch the widget working, and the hook where you forward events to your own analytics: ```tsx // useRevragEventLog.ts import { useEffect, useRef } from 'react'; import { AgentEvent, embedOnAgent, embedOffAgent, type AgentEventPayload } from '@revrag-ai/embed-react'; const LABELS: Partial> = { [AgentEvent.AGENT_VISIBLE]: 'button visible', [AgentEvent.AGENT_TAP_TO_OPEN]: 'user opened the widget', [AgentEvent.AGENT_TAP_TO_CLOSE]: 'user closed the widget', [AgentEvent.POPUP_MESSAGE_VISIBLE]: 'nudge shown', [AgentEvent.MICROPHONE_PERMISSION_ALLOWED]: 'microphone allowed', [AgentEvent.MICROPHONE_PERMISSION_DENIED]: 'microphone DENIED', [AgentEvent.AGENT_CONVERSATION_STARTED]: 'call started', [AgentEvent.AGENT_CONVERSATION_ENDED]: 'call ended', }; export function useRevragEventLog(options: { log?: boolean; onEvent?: (e: AgentEventPayload) => void } = {}) { const latest = useRef(options); latest.current = options; useEffect(() => { const handle = embedOnAgent((event) => { if (latest.current.log) console.info(`[revrag] ${LABELS[event.type] ?? event.type}`, event); latest.current.onEvent?.(event); }); return () => embedOffAgent(handle); // one subscription, released on unmount }, []); } ``` ```tsx // In your root component, development only: useRevragEventLog({ log: true }); ``` **Gate logging to development.** * Payloads carry `app_user_id` and call ids. * Do not forward them wholesale to error trackers or third-party analytics. *** ## 8. How paths become screen names [#8-how-paths-become-screen-names] Learn where the SDK reads the path from, the rules for the path you pass, and how `getScreenName` names screens. The SDK identifies "where the user is" by a **path**: | | Source | When it is used | | - | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | 1 | `currentPath` | You pass it — recommended | | 2 | `usePathHook()` | You pass a hook (e.g. `usePathname`); called on every render, so pass a stable function and never switch between passing it and not | | 3 | `window.location.pathname` | Nothing passed. Only re-read on browser back/forward — client-side navigations are missed | **Rules worth knowing:** * **Paths are matched exactly and are case-sensitive.** `/Loans` ≠ `/loans`, and `/loans/` ≠ `/loans`. * **Pass only the pathname** as `currentPath` — the button's lists match it exactly, so strip `?…` and `#…` if your router includes them. * **Never put identifiers in a path you control** (`/onboarding/${userId}`): paths become the agent's screen map and travel to Revrag. * **`getScreenName`** names the screen the user is on, for when the URL alone can't — a wizard whose steps share one path, or screens chosen by a query parameter. * **Without it**, the screen is the path plus the hash. * **It takes no arguments:** read your own state (or `location`) and return a name, or `null` to fall back to the URL: ```tsx getScreenName={() => (pathname === '/apply' ? `/apply/step-${step}` : null)} ``` * **Its value is the screen's identity, not a display label.** The agent's screen map is keyed on it, so two screens must never return the same name, and one screen must not return different names. * **It runs on every capture**, so keep it cheap. * **It does not affect where the button appears**, which always uses the path. * **Your path is the single source of truth.** * **Once you pass `currentPath` (or `usePathHook`)**, it is the only route the agent, the screen map and Revrag's analytics see. The SDK's own route inference (`pushState`, `replaceState`, back/forward, `hashchange`, Next.js route events, with `enableTracker` on) is ignored. * **Without either prop, that inference is the source instead** — it reports the path **plus hash**, and on Next.js the full URL with query, so keep tokens and personal data out of query strings and hashes. *** ## 9. Verify it works [#9-verify-it-works] Turn on debug logs, run the self-check, and walk through a good first run. ### Turn on debug logs [#turn-on-debug-logs] * **The SDK's debug logs are often off, even in development.** They are on only when your bundle defines `process.env.NODE_ENV` as something other than `production` at runtime — which many browser builds do not. * **Turn them on in the browser console** (no reload needed): ```js window.__revragDebug = true; // false turns them off again ``` **What the logs look like:** * **Every line starts with `[RevRag]` and an area** — `call`, `runtime`, `scope`, `capture`, `cache`, `sync`, `widget`. * **Messages to and from the agent** print as `[RevRag ▸ …]` (outgoing) and `[RevRag ◂ …]` (incoming), with one JSON line per message under `[RevRag wire]`. * **Failures log to the console even with debug off** — a failed API request, `Token generation failed`, the `startCall` warning. ### Run the self-check [#run-the-self-check] ```ts import { verifyIntegration } from '@revrag-ai/embed-react/diagnostics'; const result = await verifyIntegration(); // prints a report to the console console.log(result.ok, result.summary); ``` * **Run it in development** after you have logged in and visited two routes. * **Pass `{ offline: true }`** to skip the checks that make network requests. | Id | Check | | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | V1 | SDK initialized | | V2 | API key accepted — reads the stored initialization result, so it also fails when `sessionStorage` is blocked | | V3 | Provider mounted exactly once | | V4 | Widget rendered, or hidden with a reason | | V5 | User identity set | | V6 | Route changes detected (needs two visited routes; reads the screen cache, so it fails when the dashboard turns `localCache` off, or right after `EmbedLogout()`) | | V7 | Events accepted — sends a real `custom_event` named `revrag_verify_integration` | | V8 | Capture healthy — fails only if nothing actionable is captured or the SDK's own UI leaks in; unlabeled controls and truncated containers are notes | | V9 | Masking honored | | V10 | The Revrag backend is reachable (it does not check the call server or the avatar host) | * **Each failing check carries a `remediation`** telling you what to change. * **Skipped checks:** V7 when no user id is stored; V9 when the page has no masked fields. * **Pass `{ report: false }`** to skip the console report. ### A good first run [#a-good-first-run] 1. Page load (with `window.__revragDebug = true`) → `[RevRag] …` initialization lines; `useInitialize` returns `isInitialized: true`, `error: null`. 2. Log in → your `USER_DATA` is sent. 3. Navigate to an allowed path → the button appears; `agent_visible` fires. 4. Tap the button and talk → the microphone prompt (first time) → `microphone_permission_allowed` → `agent_conversation_started`. 5. Navigate inside a `continuous` group → the call keeps running. 6. Hang up → `agent_conversation_ended` with `reason: 'manual_disconnect'`. 7. Navigate to an excluded path → the button is gone. **Invalid patterns:** a call before initialization; `agent_visible` never firing; two buttons. *** ## 10. Troubleshooting [#10-troubleshooting] Find your symptom below, then work through its causes and fixes. ### The button never appears [#the-button-never-appears] Check these in order — the first three account for most cases. 1. **Did initialization succeed?** Log `error` from `useInitialize`. * `app_version is required…` → pass `appVersion`. * Any other error → check the API key and `baseUrl` belong to the same environment, and that the dashboard has a config for this `appVersion`. 2. **Did you import the stylesheet?** Without `@revrag-ai/embed-react/style.css` the button gets only a minimal fallback style and can look absent. 3. **Does the path match?** Log the `currentPath` you pass. Matching is exact and case-sensitive; a trailing slash counts. 4. **Is the path excluded?** `excludeScreens` beats everything, and covers every path below it. 5. **Is an entrance delay running?** A 3000 ms delay looks like "not working" for three seconds. 6. **Did `useInitialize` run before the provider?** The button renders only once initialization has stored its result. (Unmounting `useInitialize` later only stops tracking; the button stays.) 7. **Is `sessionStorage` blocked?** Strict privacy settings and sandboxed iframes block it; the widget cannot render without it. ### The call ends when the user navigates [#the-call-ends-when-the-user-navigates] * **Cause:** the button was hidden — and on the web, a hidden button ends the call ([§4](#4-decide-where-it-appears)). Either: * the next path is outside `includeScreens` or excluded * the next path has an entrance delay * your visibility config is written inline and re-applies its delay on re-render * **Fix:** put the journey in one `continuous` group, give it no delay, and move the config to module scope. ### Two buttons appear [#two-buttons-appear] * **Cause:** two widgets are mounted — two `EmbedProvider`s (two React roots, a micro-frontend), or a provider *and* an ``. * **Fix:** keep exactly one provider and no standalone button. ### The call does not start [#the-call-does-not-start] * **No microphone prompt:** the page is not a secure context (HTTPS), or it runs in an iframe without `allow="microphone"`. * **The user denied the microphone:** listen for `microphone_permission_denied` and tell them how to re-enable it in the site settings. * **`app_user_id is required`:** you set `enableTracker: false` and have not sent `USER_DATA` yet. * **Nothing in particular:** turn on debug logs and read the `[RevRag] call` lines; check your CSP allows the call server over `wss:` and `https:`. ### `startCall()` logs "no EmbedProvider mounted" [#startcall-logs-no-embedprovider-mounted] * **Cause:** the button is not mounted on this path (hidden by visibility), so there is nothing to start a call from. * **In development:** with React `StrictMode`, this can also happen on a path where the button *is* visible — production is unaffected ([§11](#11-known-limitations)). ### Next.js: "createContext only works in Client Components" / hook errors [#nextjs-createcontext-only-works-in-client-components--hook-errors] * **Cause:** you are on `1.4.4`, whose files carry no `"use client"`. **Fix:** upgrade. * **Cause:** you called `useInitialize` / `usePathname` from a Server Component. **Fix:** move them into a client component ([5B](#5b--nextjs-app-router)). ### npm `ERESOLVE` on install [#npm-eresolve-on-install] * **Cause:** you are on Next.js 12–13 or React Router 5, which the peer range does not include yet. * **Fix:** install with `--legacy-peer-deps` (or an `overrides` entry). The SDK imports neither package, so this is safe. * **More:** every out-of-range workaround is in the [Dependency & Compatibility Guide](/embed/integration/react/v1.5.0/react-dependencies#5-outside-the-ranges-workarounds). ### React 17: errors about `useSyncExternalStore` or the animation library [#react-17-errors-about-usesyncexternalstore-or-the-animation-library] * **Cause:** you are on a webpack host using the default entry. * **Fix:** import from `@revrag-ai/embed-react/react17` ([5D](#5d--react-17)). ### TypeScript: "Cannot find module '@revrag-ai/embed-react/react17'" [#typescript-cannot-find-module-revrag-aiembed-reactreact17] * **Cause:** your `moduleResolution` is `"node"`. * **Fix:** switch to `"bundler"`, or add a `paths` entry ([5D](#5d--react-17)). ### Agent events never fire [#agent-events-never-fire] * **Cause:** they are only raised while a user id is stored. * **Fix:** with `enableTracker: false`, send `USER_DATA` first. * **Also check** that you compare `event.type` against `AgentEvent` constants. ### Something else [#something-else] 1. Turn on debug logs. 2. Reload. 3. Reproduce. 4. Send Revrag: * the console output from page load * the `verifyIntegration()` report * the SDK version * the browser and OS * the list of paths you pass as `currentPath` *** ## 11. Known limitations [#11-known-limitations] Each is a current behaviour, its effect, and what to do. | Id | Limitation | What to do | | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | W1 | **Hiding the button ends a call.** The call lives in the button, so an entrance delay or a path outside `includeScreens` ends it, not only `excludeScreens` | `continuous` groups without delays for journeys | | W2 | **After `EmbedLogout()`, calls need a new user id.** The anonymous id is only created on a page load | Send `USER_DATA` after the next login, or reload to your login page ([§2](#on-logout)) | | W3 | **Call errors are not shown in the widget, and only one reaches your code.** A denied microphone raises `microphone_permission_denied` | Handle `microphone_permission_denied` with your own message; for connection failures, treat "no `agent_conversation_started` within a few seconds of `startCall()`" as a failure | | W4 | **A failed initialization is not retried** until the API key changes or the component remounts | Render `useInitialize` only once you know `appVersion` | | W5 | **`sessionStorage` is required.** Where it is blocked, the widget does not render | — | | W6 | **`startCall()` and `isCallActive()` in development `StrictMode`** may report no provider. Production builds are unaffected | — | | W7 | **Without `currentPath` or `usePathHook`,** the button misses client-side navigations, and the agent falls back to the SDK's inferred routes | Always pass one | | W8 | **First-load size:** the default avatar animation ships in the main bundle | — | | W9 | **Peer ranges** exclude Next.js 12–13 and React Router 5 | `--legacy-peer-deps` | | W10 | **No automatic PII detection;** unmasked text and input values are captured and cached off-call, and click and route tracking ignore masking | Mask sensitive fields (`data-revrag-mask`), keep personal data out of URLs, and disclose to your security reviewer | | W11 | **The avatar fetch falls back to `corsproxy.io`** (Revrag's own S3 avatars fall back to `/s3-lottie/…` on your site instead) | Allow it in CSP, or ask Revrag to serve your avatar from a CORS-enabled origin | | W12 | **Debug logs are often off in development too** (they depend on a runtime `process.env.NODE_ENV`) | `window.__revragDebug = true` | * **W3** — a failed connection (token, network, a room that drops before the agent joins) raises no event at all — the button simply returns to idle. *** ## 12. Checklist [#12-checklist] Check off each item for your integration. #### Install and serve [#install-and-serve] * [ ] `@revrag-ai/embed-react` installed (`--legacy-peer-deps` on Next.js 12–13 or React Router 5) * [ ] `@revrag-ai/embed-react/style.css` imported once at the entry point * [ ] Served over HTTPS; `allow="microphone"` on any iframe that hosts the app #### Initialize and identify [#initialize-and-identify] * [ ] `useInitialize(apiKey, { appVersion })` once, in a component that never unmounts, at or above the provider * [ ] API key and `baseUrl` from the same environment; production omits `baseUrl` * [ ] Dashboard has a widget config (and avatar) for this exact `appVersion` — re-checked on every release that bumps it * [ ] `error` from `useInitialize` logged * [ ] `USER_DATA` with your stable `app_user_id` sent after login, after `isInitialized` * [ ] Sign-out calls `EmbedLogout()` ([§2](#on-logout)) #### Mount and visibility [#mount-and-visibility] * [ ] Exactly **one** `EmbedProvider`, in a persistent layout, inside the router; no standalone `` * [ ] `currentPath` (or `usePathHook`) passed — pathname only, no query, hash or identifiers * [ ] Visibility lists and config defined at module scope, written against the paths your app actually reports * [ ] Login, OTP, PIN and payment paths in `excludeScreens` * [ ] Journeys that must keep a call alive are one `continuous` group, with no entrance delay #### Privacy and CSP [#privacy-and-csp] * [ ] Sensitive fields masked (`data-revrag-mask`) or excluded * [ ] CSP allows the backend, the call server (`wss:` and `https:`), the avatar host and `corsproxy.io` #### Verify and test [#verify-and-test] * [ ] `verifyIntegration()` passes in development * [ ] A real call tested on desktop Chrome **and** iOS Safari, on the production build and CSP *** ## Reference [#reference] Look up package entries, working examples, the backend contract and the other platform guides. | | | | ------------------- | ----------------------------------------------------------------------------------------------------------------- | | Package | `@revrag-ai/embed-react` — entries `.`, `/react17`, `/diagnostics`, `/style.css` (also `/styles/widget.css`) | | Working examples | `examples/nextjs` · `examples/react` · `examples/react17-smoke` · `examples/next17-smoke` · `examples/preact-poc` | | Backend contract | `docs/BE_DATA_CONTRACT.md` · `docs/WEB_TREE_PAYLOADS.md` | | Other platforms | [Android](/embed/integration/android) · [React Native](/embed/integration/react/v1.5.0-native) | | Versions & pitfalls | [Dependency & Compatibility Guide](/embed/integration/react/v1.5.0/react-dependencies) | | Release notes | [React SDK release notes](/embed/integration/react/react-releases) | * **Working examples** — copy imports from this guide, not from them. Most build against the SDK's source through bundler aliases — including `…/dist/style.css`, which does not resolve from the published package. * `examples/nextjs` — Next.js, App Router * `examples/react` — React 19 + React Router * `examples/react17-smoke` — React 17 * `examples/next17-smoke` — Next.js 12, Pages Router * `examples/preact-poc` — Preact, with Action Intelligence fixtures