RevRag React (Web) SDK — AI Agent Integration Master Prompt
How to use: paste this entire document into your AI coding agent (Copilot, Cursor, Claude Code, etc.) inside the web app you want to integrate — React SPA, Next.js, React 17 or Preact. The agent will inspect your app, install and wire
@revrag-ai/embed-react, ask you the few decisions only you can make, then validate the integration end-to-end and report.Every rule below is derived from the SDK's source (v1.5.0). Where this prompt and a generic React tutorial disagree, this prompt is correct for this SDK. Versions up to 1.4.4 predate most of it — do not apply it to them.
You are an expert web integration agent. Take this application from zero → working → validated RevRag SDK integration with minimal manual work.
Prime directives
- Inspect first, modify second. Never assume the framework, router, React version, bundler, package manager, or hosting setup. Read the project.
- Preserve host behavior. Merge into the existing root, layout, providers, bundler
config and CSP; never overwrite
main.tsx,app/layout.tsx,_app.tsx,vite.config,next.configortsconfigblindly. - Never claim success without running the validation in §16–§21 and observing the
log lines and the
verifyIntegration()result named there. - Ask the developer for the decisions in §13 — do not guess them.
- Never hide an error. Report SDK-side issues you cannot fix.
- Use only the public API. Never import from
@revrag-ai/embed-core,@revrag-ai/embed-react/dist/...or any internal path. Do not add dependencies, polyfills or config the SDK does not require.
1. Inspect the existing application
Record each of these before changing anything:
| What | How to find it |
|---|---|
| React / React DOM version | package.json; npm ls react react-dom |
| Preact? | preact in deps; @preact/preset-vite in vite.config.*; react aliased to preact/compat |
| Framework | next.config.* ⇒ Next.js. Then: app/layout.tsx ⇒ App Router; pages/_app.tsx ⇒ Pages Router (both ⇒ App Router is primary). angular.json ⇒ stop, use the Angular SDK |
| Next.js version | npm ls next (12/13 need --legacy-peer-deps) |
| Bundler | Vite, webpack (CRA, Next ≤ 12, custom), Rspack, Parcel — decides the React 17 entry (§2) |
| Router | react-router-dom (version; BrowserRouter / HashRouter / createBrowserRouter; basename?), Next router, preact-iso, preact-router, TanStack Router, or none / state-driven |
| Where the router mounts | Is it conditional (auth gate, suspense, feature flag)? Is there a persistent layout/shell that never unmounts? |
| Root component / entry | src/main.tsx / src/index.tsx, app/layout.tsx, pages/_app.tsx |
| React roots | grep -rn "createRoot|ReactDOM.render|hydrateRoot" — more than one ⇒ micro-frontends (§22) |
StrictMode | in the entry file (affects §19 in dev only) |
| TypeScript | tsconfig.json compilerOptions.moduleResolution (node needs a paths entry for /react17) |
| Package manager | package-lock.json / yarn.lock (+ .yarnrc.yml ⇒ berry) / pnpm-lock.yaml |
| Env/config system | import.meta.env.VITE_*, process.env.NEXT_PUBLIC_*, process.env.REACT_APP_*, runtime config |
| App version source | package.json version, a build id, CI env var, next.config env |
| Auth flow | where login completes (user id becomes known) and where sign-out happens |
| CSP / headers | <meta http-equiv="Content-Security-Policy">, next.config headers(), server/CDN config, Permissions-Policy |
| Hosting | HTTPS everywhere? Served inside an iframe? |
| Existing overlays / widgets | chat widgets, toasts, cookie banners, sticky footers, other fixed-position floating buttons |
| Existing LiveKit / WebRTC | npm ls livekit-client |
| Component library | MUI, Radix, shadcn, Chakra, Ant, Headless UI, Ionic / Shoelace / Stencil (web components) |
| Sensitive screens / fields | login, OTP, PIN, payment, KYC, account numbers |
2. Determine compatibility
Supported: React 17, 18, 19; Preact 10 via preact/compat; Next.js App
Router and Pages Router; any current browser with WebRTC and getUserMedia; a
secure context (HTTPS, or localhost in development).
Pick the entry point — the API is identical; only the import path changes:
| Host | Import from |
|---|---|
| React 18 / 19, any bundler | @revrag-ai/embed-react |
| React 17 on Vite | @revrag-ai/embed-react |
| React 17 on webpack (CRA, Next.js 12, custom) | @revrag-ai/embed-react/react17 |
Preact (Vite with @preact/preset-vite, or webpack with aliases) | @revrag-ai/embed-react |
Use one entry consistently across the whole app. Diagnostics always come from
@revrag-ai/embed-react/diagnostics.
Peer ranges:
| Package | Range | Notes |
|---|---|---|
react, react-dom | >=17.0.0 | required |
next | >=14.0.0 | optional; the SDK never imports it. Next 12/13 ⇒ ERESOLVE — install with --legacy-peer-deps |
react-router-dom | >=6.0.0 | optional; the SDK never imports it. React Router 5 ⇒ same ERESOLVE workaround |
TypeScript: moduleResolution: "bundler" or "nodenext". With "node", the
/react17 subpath needs a paths entry (§7).
Do not upgrade or downgrade unrelated dependencies to make the SDK fit. If something is incompatible, explain it, apply the workaround above, and re-validate.
3. Install the SDK
npm install @revrag-ai/embed-react@^1.5.0
# yarn add @revrag-ai/embed-react@^1.5.0 / pnpm add @revrag-ai/embed-react@^1.5.0
# Next.js 12–13 or React Router 5: add --legacy-peer-depsIf given a local tarball, install that path instead. Then verify:
npm ls @revrag-ai/embed-react # 1.5.0 or later
ls node_modules/@revrag-ai/embed-react/{index.js,index.react17.js,diagnostics.js,style.css}
npm ls react react-dom # exactly ONE copy each (monorepo / pnpm hoisting!)
npm ls livekit-client --all # the SDK brings its own; note any second copy from the hostNever add livekit-client, framer-motion or lottie-web to the app for the SDK —
they are the SDK's own dependencies. livekit-client loads only when a call starts.
4. Configure the bundler and stylesheet
Stylesheet (required)
Import once, at the app's entry point:
import '@revrag-ai/embed-react/style.css';| Framework | File |
|---|---|
| React SPA | src/main.tsx / src/index.tsx |
| Next App Router | app/layout.tsx (a Server Component may import it) |
| Next Pages Router | pages/_app.tsx |
The path is exactly @revrag-ai/embed-react/style.css. …/dist/style.css does not
resolve from the published package. Without the stylesheet the button gets only a
minimal fallback and looks broken.
Preact
- Vite +
@preact/preset-vite: nothing to do; the preset aliasesreact,react-domandreact/jsx-runtimetopreact/compat. - webpack / others: add to
resolve.alias:react: 'preact/compat', 'react-dom/test-utils': 'preact/test-utils', 'react-dom': 'preact/compat', 'react/jsx-runtime': 'preact/jsx-runtime', - npm may install
react/react-domto satisfy peers. Harmless — the alias keeps them out of the bundle. Do not "fix" it.
Package-manager notes
- yarn berry with PnP: if resolution of the SDK's dependencies fails, switch to
nodeLinker: node-modulesand tell the developer. - pnpm: do not enable
strict-peer-dependenciesfor this; pin the SDK version. - Monorepos: the app must resolve one
react/react-dom. Two copies give "Invalid hook call".
5. Mount: framework-specific integration
Three steps for everyone: initialize once (useInitialize), mount one provider
(EmbedProvider), identify the user after login (§11). Pick the block that matches §1.
5A. React SPA with React Router (Vite / CRA)
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, 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;
// Module scope — never inline visibility config in JSX (§14).
const EXCLUDED = ['/login', '/otp'];
// The provider lives INSIDE the router so it can read the path.
function RevragShell({ children }: { children: React.ReactNode }) {
const { pathname } = useLocation();
return (
<EmbedProvider currentPath={pathname} excludeScreens={EXCLUDED}>
{children}
</EmbedProvider>
);
}
// Initialize once, in a component that never unmounts, at or above the provider.
function App() {
const { error } = useInitialize(API_KEY, { appVersion: APP_VERSION });
if (error) console.error('[Revrag] init failed:', error);
return (
<BrowserRouter>
<RevragShell>
<AppRoutes />
</RevragShell>
</BrowserRouter>
);
}
createRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>);HashRouter:useLocation().pathnameis the part after#— pass it as-is.- Data routers (
createBrowserRouter+RouterProvider): putRevragShellin the root route's layout element (it must be inside the router), keepuseInitializeaboveRouterProvider. useLocation()throws outside the router —RevragShellmust sit inside it.
5B. Next.js App Router
// 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 (
<EmbedProvider currentPath={pathname ?? undefined} excludeScreens={EXCLUDED}>
{children}
</EmbedProvider>
);
}// app/layout.tsx — Server Component; merge into the existing layout
import '@revrag-ai/embed-react/style.css';
import { RevragRoot } from './revrag-root';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<RevragRoot>{children}</RevragRoot>
</body>
</html>
);
}- Root layout only. A provider in a page or nested layout remounts on navigation and ends a live call.
- The package ships
"use client", butuseInitializeandusePathnameare hooks, so this one client component is still required. Never call them from a Server Component. - Expose the version at build time, e.g.
next.config:env: { NEXT_PUBLIC_APP_VERSION: require('./package.json').version }.
5C. Next.js Pages Router
// pages/_app.tsx — merge into the existing _app
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.
<EmbedProvider currentPath={router.pathname} excludeScreens={EXCLUDED}>
<Component {...pageProps} />
</EmbedProvider>
);
}Use router.pathname, never router.asPath — asPath carries query and hash and
never matches the lists. On Next.js 12 + React 17, import from /react17.
5D. React 17
Same code as 5A/5C. On webpack hosts, every SDK import comes from
@revrag-ai/embed-react/react17 (it swaps framer-motion for a built-in fallback). On
Vite, the default entry works. Animations may be simpler — expected; add no polyfills.
5E. Preact
§4 aliases, then 5A. Pass the router's path: preact-iso → useLocation().path
(EmbedProvider must sit inside <LocationProvider>); preact-router → its current URL
pathname.
5F. No router (tabs, steps, wizards in state)
Give each state a synthetic, privacy-safe path:
<EmbedProvider currentPath={`/onboarding/${step}`} excludeScreens={['/onboarding/pin']}>Never put a user id, account number or token in a path.
Rules for every shape
EmbedProviderrenders the button itself. Never also render<EmbedButton />— that mounts a second widget with a second call manager.- Exactly one provider per page. Two React roots or a micro-frontend mounting its own ⇒ two widgets that drift apart.
- Always pass
currentPath(orusePathHook). Without it the SDK readswindow.location.pathnameand only re-reads it on back/forward, so client-side navigations are missed. useInitializemust be in a component that stays mounted and is at or above the provider. Unmounting it stops tracking.
6. Hosting, CSP and microphone
The SDK requests the microphone itself on the first call — write no permission flow. Make sure the browser can ask:
- HTTPS everywhere outside
localhost. On HTTP the call fails with no prompt. - Inside an iframe: the frame needs
allow="microphone", and noPermissions-Policyheader on the parent may block it. - CSP — if the app sends one, add (merge, never replace):
| Directive | Allow | Why |
|---|---|---|
connect-src | https://embed.revrag.ai (or the baseUrl host) | init, events, token, screen map |
connect-src | the call server over both wss: and https: — ask RevRag for the host | WebRTC signalling |
connect-src | the avatar animation host, and https://corsproxy.io | avatar fetch + its fallback |
img-src | data:, https://revrag-dev.s3.ap-south-1.amazonaws.com, the avatar image host | bundled avatar, in-call icons |
script-src | 'unsafe-eval' only if the avatar uses Lottie expressions | otherwise not needed |
style-src | 'unsafe-inline' only if the stylesheet is not imported | fallback <style> |
- RevRag-hosted S3 avatars fall back to
/s3-lottie/<path>on the host's own origin, notcorsproxy.io. Onlocalhostthat path is used immediately — the dev server must proxy/s3-lottieto the bucket, or the avatar animation does not load in dev. - The route tracker may try
new Functionto find the Next.js router; without'unsafe-eval'that is caught and harmless but may log a CSP violation report. - Corporate networks: if UDP is blocked, calls need TURN over TCP/TLS 443 — flag it.
7. Bundler & TypeScript validation
Do not replace configs. Confirm:
- One
react/react-dom(or Preact aliases) in the bundle. - The stylesheet import resolves (no "Can't resolve '…/dist/style.css'").
moduleResolution: "node"+/react17⇒ add:{ "compilerOptions": { "paths": { "@revrag-ai/embed-react/react17": ["./node_modules/@revrag-ai/embed-react/index.d.ts"] } } }npm run build(andtsc --noEmitif the app type-checks separately) passes. On failure decide SDK-related vs pre-existing; fix; rebuild. Never suppress errors.- SSR: nothing touches
windowat import; the button renders a hidden placeholder until hydration. A hydration warning mentioning the widget is an SDK issue — report it.
8. SDK initialization (exact contract)
import { useInitialize } from '@revrag-ai/embed-react';
const { isInitialized, isLoading, error } = useInitialize(API_KEY, {
appVersion: APP_VERSION, // REQUIRED — selects the dashboard config for this version
baseUrl: BASE_URL, // optional; omit in production (https://embed.revrag.ai)
enableTracker: true, // optional; default true (click/route tracking + anonymous id)
});- Returns
{ isInitialized, isLoading, error, sessionData, clearSessionData, getStorageInfo }.erroris the only place a failure surfaces — always log it. - Runs once per API key. Changing options after the first render is ignored. A
failed init is not retried until the key changes or the component remounts, and
the handshake times out after 5 s. So do not render the hook before
appVersionand the key are known. - Fail-open: never block rendering on
isInitialized. The widget appears by itself once init completes. - Once, at the root. Not in a route/page component.
- API key and
baseUrlare a pair. A dev key against production (or the reverse) fails init and the button never appears.
9. API key validation
Verify a non-empty key exists in the app's env system and reaches useInitialize. Read
it from env (VITE_*, NEXT_PUBLIC_*, REACT_APP_*), never a literal in source when an
env system exists. Remember browser env vars are public — the key is a client key by
design. If missing ⇒ stop, report "API key missing", ask the developer. Confirm
which environment (baseUrl) the key belongs to.
10. App version validation
appVersion is required, and useInitialize's options are its only source —
there is no EmbedProvider prop, no <meta> tag, no build-variable auto-detection.
Without it init fails with [RevRag] app_version is required. Pass it at initialization…
and the widget never appears.
Derive it from the app's real version (package.json, build id, CI env). Never pass a
placeholder. The value must match a version the RevRag dashboard has a config for —
on the web, every deploy that bumps it is a dashboard change. Tell the developer to add
"dashboard config exists for this version" to their release checklist.
Verify: error is null; the /embedded-agent/initialize request in the network
tab succeeds.
11. USER_DATA and logout
After login, and only once isInitialized is true:
import { useEffect } from 'react';
import { embedEvent, EventKeys } from '@revrag-ai/embed-react';
useEffect(() => {
if (!isInitialized || !user) return;
void embedEvent.event({
eventKey: EventKeys.USER_DATA,
data: { app_user_id: user.id, name: user.name },
});
}, [isInitialized, user?.id]);Facts the integration must respect:
app_user_idis the stable id everything is attributed to. Send once per user/session, not on every render.- Sent before init ⇒
SDK not initialized; the id is stored but the backend never hears about the user until the next event. - Before login with
enableTracker: true(default), init registers an anonymous id (user_<timestamp>_<random>), so calls work. WithenableTracker: falsethere is no anonymous id: a call beforeUSER_DATAfails withapp_user_id is required. embedEvent.eventresolves{ success, error? }and does not throw — as long asdatais passed (use{}if empty); withoutdatait rejects.
On sign-out, wire EmbedLogout() into the app's existing sign-out, before the app's
own logic:
import { EmbedLogout } from '@revrag-ai/embed-react';
await EmbedLogout(); // ends any call, stops the agent, wipes this user's cached screens, forgets the idIt never rejects and is safe to call twice. Afterwards calls cannot start until the next
USER_DATA (the anonymous id is only created on a page load).
Required order: useInitialize → isInitialized → USER_DATA → widget config → path
reported → button visible → call.
12. Widget configuration validation
The button renders only after init stores its result (including widget_config) in
sessionStorage. Position, look and the inactivity behaviour come from the dashboard,
per app version — not code.
| Case | Expected | Observe |
|---|---|---|
| A. config exists | button appears on allowed paths; agent_visible fires | isInitialized: true, error: null |
B. init fails (bad key / env mismatch / unknown appVersion / offline) | no button, no crash, app fully usable | error set and logged |
C. sessionStorage blocked (strict privacy, sandboxed iframe) | SDK loads, never throws, no button | verifyIntegration() V2 fails |
| D. slow network (> 5 s handshake) | init fails for this mount; app unaffected | error set; not retried until remount |
| E. widget render error | widget hides itself; app unaffected | [RevRag] The voice widget crashed and was hidden; your app is unaffected. |
Also check with the developer:
- Position (
widgetPosition, paddings) against sticky footers and other floating widgets — changed on the dashboard, not in code. collapsedView.inactivityBehavior: auto_triggeron the dashboard makes the widget start a call by itself ~1.5 s after the button appears, without a click. Confirm that is intended.
13. Ask the developer (do not guess)
After inspecting, list the actual paths the app reports (what you will pass as
currentPath — e.g. /loans/apply, or /orders/[id] on the Pages Router), then ask:
- Where should the button appear? All paths except auth (default,
excludeScreens), or an explicit allow-list (includeScreens, optionallymatchMode="startsWith")? - Which paths must never be seen by the agent (login, OTP, PIN, payment, KYC)? ⇒
excludeScreens— matches the path and everything below it, and ends a live call on entry. - Which journeys must keep a live call across navigation? On the web the call lives
in the button: any hide ends the call — an excluded path, a path outside the
allow-list, or an entrance delay. Journeys need one
continuousgroup with no delay. (There is noendCallWhenHiddenByVisibilityoption on web.) - Entrance delays? (
embedButtonDelayMs, groupdelayMs,delayPolicy) — remember each delayed appearance ends a live call. - Click and route tracking (
enableTracker, default on) — acceptable for this app's privacy posture? Button text and routes reach RevRag and are not masked. - Which fields/regions are sensitive? ⇒
data-revrag-mask/data-revrag-embed-self. - App version source, API key location, and which
baseUrlper environment. - Any iframe-hosted, web-component (shadow DOM), rich-text editor, file-upload, canvas, or cross-origin-iframe (payments) flows the agent is expected to operate (§22).
- Is the dashboard's
auto_triggerbehaviour wanted?
Wait for answers before finalizing visibility config.
14. Screen integration (how the SDK actually detects screens)
- Path source, in order:
currentPathprop →usePathHook()→window.location.pathname(only re-read on back/forward). OncecurrentPath/usePathHookis passed it is the single source of truth for the button, the agent, the screen map and analytics; the SDK's own route inference is ignored. Debug log:[RevRag] scope host router reports routes — SDK route inference is now ignored. - Matching is exact and case-sensitive.
/Loans≠/loans;/loans/≠/loans. Pass the pathname only — no?query, no#hash. - Dynamic segments:
useLocation().pathname/usePathname()give the real path (/orders/8812). Cover a subtree withmatchMode="startsWith"+includeScreens, orexcludeScreens(always prefix). Pages Routerrouter.pathnameis the pattern. getScreenNamenames screens the URL can't (wizard steps on one path, query-driven screens). It is the screen's identity in the agent's map: unique per screen, stable per screen, cheap (runs every capture), returnsnullto fall back to the URL. It does not affect button visibility.getScreenName={() => (pathname === '/apply' ? `/apply/step-${step}` : null)}- Visibility algorithm:
excludeScreenswins over everything — unlessshowOnAllScreens={false}, which ignoresexcludeScreensentirely. EmptyincludeScreens(and no groups) ⇒ everywhere not excluded. - ⚠ Defining any group turns on the allow-list. Group
screensare added toincludeScreens; once a group exists, the button shows only on group paths andincludeScreens, and is hidden — ending a live call — everywhere else. - ⚠ Define
includeScreensandembedButtonVisibilityConfigat module scope. The provider re-checks on identity change; inline values are new every render and, with a delay configured, every parent re-render hides the button and ends a call. (excludeScreens/showOnAllScreenscompare by content — inline is harmless, but changes apply on the next path change.) - A path in a list that the app never reports silently hides the button — log the path you pass and write the lists against it.
import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-react';
const EXCLUDED = ['/login', '/otp', '/settings/security'];
const VISIBILITY: EmbedButtonVisibilityConfig = {
defaultDelayMs: 0,
groups: [
{ id: 'loan', screens: ['/loans', '/loans/apply', '/loans/review'], continuity: 'continuous' },
],
};
// NOTE: with this group defined, add every other path the button should appear on to includeScreens.Delay precedence: group delayMs → defaultDelayMs → embedButtonDelayMs.
delayPolicy: perScreen | oncePerGroupEntry | oncePerAppSession.
Test: path A (button appears) → B → back to A; a dynamic route; a modal; a hash/query change; browser back/forward; a full reload — exactly one button throughout.
15. Continuous vs per-screen — what to verify
| User does | Expected |
|---|---|
Moves between paths in one continuous group | button stays, no blink, no re-delay, call keeps running |
| Moves between allowed paths with no delay | button stays, call keeps running |
| Enters a path with an entrance delay | button hides for the delay, call ends |
Opens a path outside includeScreens | button hides, call ends |
| Opens an excluded path | button hides, call ends, agent stops seeing the page — debug: [RevRag] call disconnecting: navigated onto an excluded screen |
| Full page load / reload | call ends; SDK initializes again |
This is stricter than the mobile SDKs (where only exclusion ends a call). If the
developer expects a call to survive a hide, explain this and restructure as a
continuous group.
16. Event ordering validation
Turn on debug logs first — they are often off in dev too (they need a runtime
process.env.NODE_ENV many browser builds don't define):
window.__revragDebug = true; // in the browser console, no reload neededLines are prefixed [RevRag] <area> (call, runtime, scope, capture, cache,
sync, widget); agent messages print as [RevRag ▸ type] (outgoing) /
[RevRag ◂ type] (incoming), with JSON under [RevRag wire].
Subscribe a dev-only logger so agent events are visible:
import { useEffect } from 'react';
import { embedOnAgent, embedOffAgent } from '@revrag-ai/embed-react';
useEffect(() => {
if (process.env.NODE_ENV === 'production') return;
const handle = embedOnAgent((e) => console.info('[revrag event]', e.type, e));
return () => embedOffAgent(handle); // MUST release — one subscription per mount
}, []);Confirm this order, and nothing out of order:
useInitialize→isInitialized: true,error: nullUSER_DATAresolves{ success: true }[RevRag] scope host router reported route "<path>"→[RevRag] cache "<screen>" merged→[RevRag] sync uploading N file(s)/batch acceptedagent_visible- (call)
microphone_permission_allowed→[RevRag] call connected — agent runtime armed→[RevRag] runtime agent runtime started→agent_conversation_started→[RevRag ▸ ui_snapshot] - (hang up)
agent_conversation_endedwithreason: 'manual_disconnect'
Invalid patterns to catch: a call or event before init (SDK not initialized); a call
before any user id (app_user_id is required); agent_visible never firing; duplicate
embedOnAgent handlers (every log line twice); two providers / a provider plus
<EmbedButton /> (two buttons).
17. UI tree / snapshot validation
During a call, for every configured path, observe [RevRag ▸ ui_snapshot] on: connect,
navigation, after scrolling, after user interaction (~2.5 s quiet), dialog open/close,
after each agent action, and on agent request. Off-call, the screen map logs
[RevRag] cache "<screen>" merged (or unchanged — not re-dirtied). On an out-of-scope
path: [RevRag] capture skipped — screen is out of capture scope.
Limits: live snapshot ≤ 150 elements / 30 KB, dialogs first; text truncated at 100 chars per element; element visible when ≥ 5% is in the viewport; off-screen elements are still captured (so the agent can scroll to them).
What the agent can find: elements by stable id — data-testid → id → visible text
(first four words) → role + inner label. Ids are normalized (lowercased, non a–z0–9
dropped, separators → _, max six words): loan-amount → loan_amount, but
applyPersonal → applypersonal; non-Latin ids normalize to nothing. Names come from
own text, else aria-labelledby → aria-label → <label> → title → placeholder.
Apply to the host (only on meaningful controls; do not blanket-tag wrappers):
<button data-testid="apply_personal">Apply now</button>
<input id="loan_amount" aria-label="Loan amount" />
<button aria-label="Close dialog"><XIcon /></button> {/* icon-only */}
{bills.map((b) => <button key={b.id} data-testid={`pay_${b.id}`}>Pay</button>)} {/* unique per row */}Sensitive data — there is no automatic PII detection:
| Want | Do |
|---|---|
| Password never leaves | <input type="password"> (automatic). A show/hide toggle to type="text" exposes it — add data-revrag-mask too |
| Value never leaves, agent may still fill | data-revrag-mask (or {...{[MASK_ATTR]: ''}}) on the field or an ancestor — inherits |
| Region invisible to the agent | data-revrag-embed-self on the wrapper |
| Whole screen off-limits | its path in excludeScreens |
A masked field's label, placeholder, aria-label and title are still sent — keep
secrets out of them. Run verifyIntegration() (§19) — V8 (capture) and V9 (masking).
Dialogs: while a blocking modal is open (<dialog> via showModal(), or
aria-modal="true" and visible), the agent sees only the modal. A full-width
top/bottom strip (≥ 80% wide, ≤ 25% tall) with aria-modal does not block. Body-portaled
role="listbox" dropdowns and plain popovers disappear while a modal blocks — give
them role="menu" or render them inside the modal. Give real modals aria-modal="true".
actionRootRef narrows capture to one region; portaled <dialog>, role="dialog",
role="alertdialog", role="menu" are still included, body-portaled
role="listbox" is not. Only use it if the app has no such dropdowns.
18. Event capture validation
Two separate systems — do not confuse them:
-
Data events (you → RevRag):
embedEvent.event({ eventKey, data, ... }).Key Wire Use EventKeys.USER_DATAuser_dataidentity — must carry app_user_idEventKeys.ANALYTICS_DATAanalytics_datanamed product events — requires event_nameEventKeys.CUSTOM_EVENTcustom_eventfree-form context for the agent EventKeys.FORM_STATEform_stateform progress EventKeys.SCREEN_VIEWscreen_staterarely needed — routes are tracked automatically EventKeys.OFFER_DATAoffer_datanot accepted by the backend — do not use AGENT_CONNECTED/AGENT_DISCONNECTED— SDK-internal — never send Every key except
USER_DATAneeds a stored user id. -
Agent events (SDK → you):
embedOnAgent(cb)returns a handle; release withembedOffAgent(handle). Compare againstAgentEvent.*constants, never strings. Only raised while a user id is stored. Payload:type,timestamp,session_id; the conversation events addcall_id,reason,callDuration.GEN_TOOL_TRIGGEREDand the deprecatedAGENT_CONNECTED/AGENT_DISCONNECTEDare never emitted. -
Automatic tracking (
enableTracker: true): the last click in each 2.5 s window — the element'sname,id, or else visible text (≤ 50 chars for non-buttons) — and every route change, sent toPUT /embedded-agent/user-context/update. Masking does not apply to tracking: give sensitive buttons anid, keep personal data out of URLs, or setenableTracker: false(then sendUSER_DATAbefore any call).
Test: send one ANALYTICS_DATA with event_name and confirm { success: true }; click
a control and confirm the tracking request in the network tab; confirm agent events
appear once each. Do not invent event APIs. Do not forward agent payloads wholesale to
third-party analytics — they carry app_user_id and call ids.
19. Call and self-check validation
Calls
Preconditions: button visible on the current path (the call lives in the button), init done, a user id stored, secure context, microphone allowed.
import { startCall, endCall, isCallActive } from '@revrag-ai/embed-react';
<button onClick={() => void startCall()}>Talk to an expert</button>startCall()never rejects and is a no-op if a call is connecting/live. Call it from a click (mic prompt + audio autoplay). On a hidden path it only warns[RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted).- A denied mic raises
microphone_permission_denied(witherror_message,error_name) — the widget shows nothing, so the host must show its own message. - A token/network failure raises no event; it logs
Token generation failedand the button returns to idle. Treat "noagent_conversation_startedwithin a few seconds ofstartCall()" as failure. expandWidget()/collapseWidget()/isWidgetExpanded()/subscribeWidgetExpanded(cb)change only what is shown — no call, no analytics.
Validate on a real browser: start → agent_conversation_started → isCallActive()
true → talk → end → agent_conversation_ended (callDuration, reason) → a second
call starts cleanly with no duplicate listeners. Test desktop Chrome and iOS Safari,
on the production build and CSP.
Self-check
Add a dev-only trigger (the browser console cannot import a bare package specifier):
import { verifyIntegration } from '@revrag-ai/embed-react/diagnostics';
// dev-only button or effect, after login and after visiting two routes
const result = await verifyIntegration(); // prints a report; { offline: true } skips network checks
console.log(result.ok, result.summary);After the first call the result is also cached on window.__REVRAG_VERIFY__ (an object,
not a function).
| Id | Check | If it fails |
|---|---|---|
| V1 | SDK initialized | §8–§10 |
| V2 | API key accepted (reads sessionStorage) | §9; storage blocked? |
| V3 | Provider mounted exactly once | §5 |
| V4 | Widget rendered, or hidden with a reason | §14 |
| V5 | User identity set | §11 |
| V6 | Route changes detected (needs 2 routes; reads the screen cache) | §5 currentPath; dashboard localCache off; just logged out? |
| V7 | Events accepted (sends a real custom_event revrag_verify_integration) | §8 / §6 CSP |
| V8 | Capture healthy | §17 labels/ids |
| V9 | Masking honored | §17 masking |
| V10 | RevRag backend reachable (not the call server or avatar host) | §6 CSP |
skip is not a failure (V7 without a user id, V9 without masked fields). For each
fail, apply its remediation, re-run; if the same check fails 3 times, stop and
report its id and detail — never work around it through SDK internals.
20. Error / failure conditions to test explicitly
| Area | Cases |
|---|---|
| Init | succeeds · bad key · key/baseUrl env mismatch · offline · slow (> 5 s) · called twice · options changed after mount — app never crashes |
| API key | present · missing · empty |
| appVersion | present · missing (app_version is required) · version with no dashboard config |
| USER_DATA | before init · after init · missing with enableTracker: false · after EmbedLogout() · re-login as another user |
| Widget config | exists · init failed · sessionStorage blocked · render crash (hides itself) |
| Paths | allowed · excluded · outside allow-list · trailing slash / case mismatch · dynamic segment · query/hash change · back/forward · reload |
| Visibility | continuous group · per-screen group · delay · inline config re-render |
| UI tree | captured · out of scope · masked fields · modal open (agent sees modal only) · body-portaled listbox under a modal |
| Call | before init · before user id · on hidden path · success · denied mic · token failure (no event) · ended · repeated · navigation mid-call (per §15) · full reload |
| Hosting | HTTP (no prompt) · iframe without allow="microphone" · production CSP |
21. Error visibility — grep the console for these
[RevRag] app_version is required (§10) · SDK not initialized (event/call before
init) · app_user_id is required (no user id) · [RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted) (hidden path, or dev StrictMode) · Token generation failed (backend/env/CSP) · [RevRag] call room disconnected unexpectedly ·
[RevRag] The voice widget crashed and was hidden (SDK-side — report) · Refused to connect / Content Security Policy (§6) · NotAllowedError / Permission denied
(mic) · Invalid hook call (two React copies) · createContext only works in Client Components (hook in a Server Component, or SDK ≤ 1.4.4) · useSyncExternalStore /
framer-motion errors on React 17 (use /react17) · Cannot find module '@revrag-ai/embed-react/react17' (TS paths) · ERESOLVE (peer ranges) · Can't resolve '@revrag-ai/embed-react/dist/style.css' (wrong path) · [RevRag AI] … payload is … bytes — close to or over LiveKit (oversize snapshot). Never swallow an error;
classify app-side vs SDK-side.
22. Unknown-unknowns — inspect the host for these (detect → why → do)
Work through every row. "Ask" = a developer decision.
Layout / overlays
- Sticky bottom bars, cookie banners, other floating widgets (Intercom, Zendesk, toasts) → overlap with the button; adjust position on the dashboard.
- Scripts that also force themselves last in
<body>→ the SDK backs off after ~20 moves/s for 10 s (page never freezes); button ends up second-last — only matters if both use max z-index. Verify visually. popoverelements → top layer; the button does not follow them and can be covered.<dialog>.showModal()/ fullscreen → the SDK moves the button into the dialog and back; a native<select>options copy may render beneath it (known limitation).- Modals without
aria-modal="true"→ the agent sees and can click the page behind; add it. - Body-portaled
role="listbox"dropdowns (Radix Select, MUI Autocomplete, React Select) inside modals → invisible to the agent while the modal blocks; ask, or userole="menu"/ render inside the modal. - The button is draggable (snaps to left/right edge; resets on remount;
centercan't drag). There is no setting to disable it — tell the developer.
Components / design system
- Icon-only buttons without
aria-label→ no name; label them. - Custom Button wrappers that drop
id/data-testid/aria-*→ weak ids; forward them (grep the design system). - Toggles using only
aria-pressed→ read as stateless; userole="switch"+aria-checked, or a real checkbox. - Custom dropdown options as bare
<div role="option">→ not tappable to the agent; needs<button>,role="menuitem",onclick,cursor: pointerortabindex. - List rows with identical text and no unique id →
pay,pay_2…; add per-rowdata-testid. - camelCase or non-Latin
ids → normalized badly; use lowercase_/-ASCII ids. - Labels in Hindi / non-Latin scripts → text matching unreliable; ids required.
Inputs / forms
- Show/hide password toggles, OTP boxes, card/Aadhaar/PAN fields → captured unless
masked; add
data-revrag-maskper field or on the wrapper. <input type="file">→ never supported (file_input_unsupported); ask.<input type="date">→ ambiguous strings parse US-order (05/10/2026= 10 May).time/month/weekneed the browser's exact format.- ARIA-only sliders → readable, not settable; native
rangeonly. - Rich-text / code editors (
contenteditable, Quill, ProseMirror, Monaco, CodeMirror) → agent can type, cannot read; ask. - Canvas / SVG controls → one opaque element; add
aria-label+role.
Navigation / routing
currentPathnot passed → client navigations missed; always pass it.- Next Pages Router
asPath→ query/hash break matching; usepathname. - Trailing-slash config (
trailingSlash: true), case differences, routerbasename→ lists must match the exact string passed. - IDs, emails or tokens in paths, queries or hashes → they reach RevRag via routes and the screen map; ask to move them out, or exclude those paths.
- Provider inside a page / nested layout / auth-gated subtree → remounts end calls; move to the persistent root.
- Multiple React roots, micro-frontends, module federation → one provider in the shell that owns navigation; others get no SDK code.
- Multi-page app (full page loads) → every navigation re-inits and ends a call; put the SDK in a shared layout and tell the developer.
Platform / hosting
- App served in an iframe →
allow="microphone"; agent sees only that document. - Cross-origin iframes (payment forms) → opaque box; agent cannot read or fill. Same-origin iframe form fields also cannot be filled today.
- Web components / shadow DOM (Ionic, Shoelace, Stencil, LWC) → closed roots skipped; slotted content in open roots not captured; ask.
- Strict CSP /
Permissions-Policy→ silent call failures; §6. - Corporate VPN / UDP blocked → TURN over 443; test on users' network.
- iOS Safari → mic/audio only from a user gesture; dashboard
auto_triggermay be blocked. - Strict privacy / blocked storage → no button (sessionStorage required); IndexedDB falls back to memory.
Build / tooling
- React 17 on webpack using the default entry → animation library errors;
/react17. - Two React copies (monorepo, linked packages) → Invalid hook call; dedupe.
- TS
moduleResolution: "node"→/react17types missing;paths. - Next.js 12–13 / React Router 5 →
--legacy-peer-deps. - Large first-load bundle → the default avatar animation ships in the main bundle; disclose, do not try to tree-shake SDK internals.
Runtime / lifecycle
- Sentry / LogRocket / Datadog console or network capture → SDK debug lines and
payloads carry
app_user_id; gatewindow.__revragDebugand event logging to dev. StrictModein dev →startCall()/isCallActive()may report no provider; production unaffected — do not "fix".- Sign-out that doesn't call
EmbedLogout()→ cached screens upload under the next user's id; wire it in. - Inline visibility config → re-render ends calls when delays exist; move to module scope.
appVersionfrom a value that changes every deploy (commit SHA) → the dashboard needs a config per value; ask what version scheme to use.
Security / privacy
excludeScreensis the privacy control for pages; masking is per field. Unmasked text and input values are captured and cached off-call (IndexedDB →ui-graph).- Tracking ignores masking — click text and routes leave regardless; ask about
enableTracker. - Data leaving the browser (for the security reviewer): init (key header, version), events, click/route tracking, call token request, mic/agent audio over WebRTC, live snapshots (data channel), screen map. No cookies.
23. Final checklist & report
[ ] Framework/router/React/bundler/package manager detected [ ] SDK ≥1.5.0 installed; entries + style.css present
[ ] Correct entry (default vs /react17) used everywhere [ ] One react/react-dom copy (or Preact aliases)
[ ] style.css imported once at the entry point (not /dist/) [ ] TS resolves (paths for /react17 if "node")
[ ] HTTPS; iframe allow="microphone"; CSP merged (backend, call server wss+https, avatar host, corsproxy.io)
[ ] useInitialize once, persistent component, at/above provider, apiKey + appVersion from env; error logged
[ ] appVersion matches a dashboard config; baseUrl matches the key's environment
[ ] Exactly ONE EmbedProvider in the persistent root/layout, inside the router; no <EmbedButton/>
[ ] currentPath passed (pathname only; Pages Router uses router.pathname)
[ ] Visibility config at module scope; excludes/groups/delays chosen by developer
[ ] Journeys that must keep a call are one continuous group with no delay
[ ] USER_DATA after login + isInitialized; EmbedLogout() wired into sign-out
[ ] Key controls have unique id/data-testid; icon-only buttons have aria-label
[ ] Sensitive fields masked / regions embed-self / screens excluded; real modals aria-modal="true"
[ ] Debug logs observed in the §16 order; one handler per agent event
[ ] Button appears on chosen paths; one widget; navigation tested (A→B→A, dynamic, modal, back, reload)
[ ] Snapshots observed per path ([RevRag ▸ ui_snapshot]); screen map merges logged
[ ] Call start/end/restart tested; denied mic handled with a host message
[ ] verifyIntegration() ok (or every skip explained)
[ ] Production build + production CSP + desktop Chrome + iOS Safari call tested
[ ] §22 unknown-unknowns reviewed [ ] No integration-related errorsFinal report format
Integration Status: SUCCESS / PARTIAL / FAILED
SDK version: | Entry: default | /react17 | React/Preact: | Framework + router: | Bundler: | TS moduleResolution:
Dependencies added/changed (+why):
Files changed (root, layout, provider shell, logout, labels/masks):
Bundler/TS/CSP changes:
Widget config — paths: | excludes: | includes/matchMode: | groups + continuity: | delays: | enableTracker:
Validation — Init: | API key: | app_version: | USER_DATA: | Logout: | Widget rendering: | Path reporting: |
UI tree: | Masking: | Event capture: | Call flow: | verifyIntegration (N/N, skips): | Build: | Error scan:
Unknown-unknowns found + handling:
Remaining issues (and whether app-side or SDK-side):Do not declare SUCCESS while any critical validation is failing.