View as Markdownllms.txt

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

  1. Inspect first, modify second. Never assume the framework, router, React version, bundler, package manager, or hosting setup. Read the project.
  2. 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.config or tsconfig blindly.
  3. Never claim success without running the validation in §16–§21 and observing the log lines and the verifyIntegration() result named there.
  4. Ask the developer for the decisions in §13 — do not guess them.
  5. Never hide an error. Report SDK-side issues you cannot fix.
  6. 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:

WhatHow to find it
React / React DOM versionpackage.json; npm ls react react-dom
Preact?preact in deps; @preact/preset-vite in vite.config.*; react aliased to preact/compat
Frameworknext.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 versionnpm ls next (12/13 need --legacy-peer-deps)
BundlerVite, webpack (CRA, Next ≤ 12, custom), Rspack, Parcel — decides the React 17 entry (§2)
Routerreact-router-dom (version; BrowserRouter / HashRouter / createBrowserRouter; basename?), Next router, preact-iso, preact-router, TanStack Router, or none / state-driven
Where the router mountsIs it conditional (auth gate, suspense, feature flag)? Is there a persistent layout/shell that never unmounts?
Root component / entrysrc/main.tsx / src/index.tsx, app/layout.tsx, pages/_app.tsx
React rootsgrep -rn "createRoot|ReactDOM.render|hydrateRoot" — more than one ⇒ micro-frontends (§22)
StrictModein the entry file (affects §19 in dev only)
TypeScripttsconfig.json compilerOptions.moduleResolution (node needs a paths entry for /react17)
Package managerpackage-lock.json / yarn.lock (+ .yarnrc.yml ⇒ berry) / pnpm-lock.yaml
Env/config systemimport.meta.env.VITE_*, process.env.NEXT_PUBLIC_*, process.env.REACT_APP_*, runtime config
App version sourcepackage.json version, a build id, CI env var, next.config env
Auth flowwhere 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
HostingHTTPS everywhere? Served inside an iframe?
Existing overlays / widgetschat widgets, toasts, cookie banners, sticky footers, other fixed-position floating buttons
Existing LiveKit / WebRTCnpm ls livekit-client
Component libraryMUI, Radix, shadcn, Chakra, Ant, Headless UI, Ionic / Shoelace / Stencil (web components)
Sensitive screens / fieldslogin, 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:

HostImport 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:

PackageRangeNotes
react, react-dom>=17.0.0required
next>=14.0.0optional; the SDK never imports it. Next 12/13 ⇒ ERESOLVE — install with --legacy-peer-deps
react-router-dom>=6.0.0optional; 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-deps

If 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 host

Never 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';
FrameworkFile
React SPAsrc/main.tsx / src/index.tsx
Next App Routerapp/layout.tsx (a Server Component may import it)
Next Pages Routerpages/_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 aliases react, react-dom and react/jsx-runtime to preact/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-dom to 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-modules and tell the developer.
  • pnpm: do not enable strict-peer-dependencies for 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().pathname is the part after # — pass it as-is.
  • Data routers (createBrowserRouter + RouterProvider): put RevragShell in the root route's layout element (it must be inside the router), keep useInitialize above RouterProvider.
  • useLocation() throws outside the router — RevragShell must 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", but useInitialize and usePathname are 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

  • EmbedProvider renders 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 (or usePathHook). Without it the SDK reads window.location.pathname and only re-reads it on back/forward, so client-side navigations are missed.
  • useInitialize must 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:

  1. HTTPS everywhere outside localhost. On HTTP the call fails with no prompt.
  2. Inside an iframe: the frame needs allow="microphone", and no Permissions-Policy header on the parent may block it.
  3. CSP — if the app sends one, add (merge, never replace):
DirectiveAllowWhy
connect-srchttps://embed.revrag.ai (or the baseUrl host)init, events, token, screen map
connect-srcthe call server over both wss: and https: — ask RevRag for the hostWebRTC signalling
connect-srcthe avatar animation host, and https://corsproxy.ioavatar fetch + its fallback
img-srcdata:, https://revrag-dev.s3.ap-south-1.amazonaws.com, the avatar image hostbundled avatar, in-call icons
script-src'unsafe-eval' only if the avatar uses Lottie expressionsotherwise not needed
style-src'unsafe-inline' only if the stylesheet is not importedfallback <style>
  • RevRag-hosted S3 avatars fall back to /s3-lottie/<path> on the host's own origin, not corsproxy.io. On localhost that path is used immediately — the dev server must proxy /s3-lottie to the bucket, or the avatar animation does not load in dev.
  • The route tracker may try new Function to 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 (and tsc --noEmit if the app type-checks separately) passes. On failure decide SDK-related vs pre-existing; fix; rebuild. Never suppress errors.
  • SSR: nothing touches window at 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 }. error is 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 appVersion and 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 baseUrl are 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_id is 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. With enableTracker: false there is no anonymous id: a call before USER_DATA fails with app_user_id is required.
  • embedEvent.event resolves { success, error? } and does not throw — as long as data is passed (use {} if empty); without data it 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 id

It 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.

CaseExpectedObserve
A. config existsbutton appears on allowed paths; agent_visible firesisInitialized: true, error: null
B. init fails (bad key / env mismatch / unknown appVersion / offline)no button, no crash, app fully usableerror set and logged
C. sessionStorage blocked (strict privacy, sandboxed iframe)SDK loads, never throws, no buttonverifyIntegration() V2 fails
D. slow network (> 5 s handshake)init fails for this mount; app unaffectederror set; not retried until remount
E. widget render errorwidget 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_trigger on 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:

  1. Where should the button appear? All paths except auth (default, excludeScreens), or an explicit allow-list (includeScreens, optionally matchMode="startsWith")?
  2. 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.
  3. 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 continuous group with no delay. (There is no endCallWhenHiddenByVisibility option on web.)
  4. Entrance delays? (embedButtonDelayMs, group delayMs, delayPolicy) — remember each delayed appearance ends a live call.
  5. Click and route tracking (enableTracker, default on) — acceptable for this app's privacy posture? Button text and routes reach RevRag and are not masked.
  6. Which fields/regions are sensitive? ⇒ data-revrag-mask / data-revrag-embed-self.
  7. App version source, API key location, and which baseUrl per environment.
  8. 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).
  9. Is the dashboard's auto_trigger behaviour wanted?

Wait for answers before finalizing visibility config.


14. Screen integration (how the SDK actually detects screens)

  • Path source, in order: currentPath prop → usePathHook() → window.location.pathname (only re-read on back/forward). Once currentPath/usePathHook is 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 with matchMode="startsWith" + includeScreens, or excludeScreens (always prefix). Pages Router router.pathname is the pattern.
  • getScreenName names 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), returns null to fall back to the URL. It does not affect button visibility.
    getScreenName={() => (pathname === '/apply' ? `/apply/step-${step}` : null)}
  • Visibility algorithm: excludeScreens wins over everything — unless showOnAllScreens={false}, which ignores excludeScreens entirely. Empty includeScreens (and no groups) ⇒ everywhere not excluded.
  • ⚠ Defining any group turns on the allow-list. Group screens are added to includeScreens; once a group exists, the button shows only on group paths and includeScreens, and is hidden — ending a live call — everywhere else.
  • ⚠ Define includeScreens and embedButtonVisibilityConfig at 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/showOnAllScreens compare 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 doesExpected
Moves between paths in one continuous groupbutton stays, no blink, no re-delay, call keeps running
Moves between allowed paths with no delaybutton stays, call keeps running
Enters a path with an entrance delaybutton hides for the delay, call ends
Opens a path outside includeScreensbutton hides, call ends
Opens an excluded pathbutton hides, call ends, agent stops seeing the page — debug: [RevRag] call disconnecting: navigated onto an excluded screen
Full page load / reloadcall 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 needed

Lines 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:

  1. useInitialize → isInitialized: true, error: null
  2. USER_DATA resolves { success: true }
  3. [RevRag] scope host router reported route "<path>" → [RevRag] cache "<screen>" merged → [RevRag] sync uploading N file(s) / batch accepted
  4. agent_visible
  5. (call) microphone_permission_allowed → [RevRag] call connected — agent runtime armed → [RevRag] runtime agent runtime started → agent_conversation_started → [RevRag ▸ ui_snapshot]
  6. (hang up) agent_conversation_ended with reason: '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:

WantDo
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 filldata-revrag-mask (or {...{[MASK_ATTR]: ''}}) on the field or an ancestor — inherits
Region invisible to the agentdata-revrag-embed-self on the wrapper
Whole screen off-limitsits 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, ... }).

    KeyWireUse
    EventKeys.USER_DATAuser_dataidentity — must carry app_user_id
    EventKeys.ANALYTICS_DATAanalytics_datanamed product events — requires event_name
    EventKeys.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_DATA needs a stored user id.

  • Agent events (SDK → you): embedOnAgent(cb) returns a handle; release with embedOffAgent(handle). Compare against AgentEvent.* constants, never strings. Only raised while a user id is stored. Payload: type, timestamp, session_id; the conversation events add call_id, reason, callDuration. GEN_TOOL_TRIGGERED and the deprecated AGENT_CONNECTED/AGENT_DISCONNECTED are never emitted.

  • Automatic tracking (enableTracker: true): the last click in each 2.5 s window — the element's name, id, or else visible text (≤ 50 chars for non-buttons) — and every route change, sent to PUT /embedded-agent/user-context/update. Masking does not apply to tracking: give sensitive buttons an id, keep personal data out of URLs, or set enableTracker: false (then send USER_DATA before 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 (with error_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 failed and the button returns to idle. Treat "no agent_conversation_started within a few seconds of startCall()" 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).

IdCheckIf it fails
V1SDK initialized§8–§10
V2API key accepted (reads sessionStorage)§9; storage blocked?
V3Provider mounted exactly once§5
V4Widget rendered, or hidden with a reason§14
V5User identity set§11
V6Route changes detected (needs 2 routes; reads the screen cache)§5 currentPath; dashboard localCache off; just logged out?
V7Events accepted (sends a real custom_event revrag_verify_integration)§8 / §6 CSP
V8Capture healthy§17 labels/ids
V9Masking honored§17 masking
V10RevRag 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

AreaCases
Initsucceeds · bad key · key/baseUrl env mismatch · offline · slow (> 5 s) · called twice · options changed after mount — app never crashes
API keypresent · missing · empty
appVersionpresent · missing (app_version is required) · version with no dashboard config
USER_DATAbefore init · after init · missing with enableTracker: false · after EmbedLogout() · re-login as another user
Widget configexists · init failed · sessionStorage blocked · render crash (hides itself)
Pathsallowed · excluded · outside allow-list · trailing slash / case mismatch · dynamic segment · query/hash change · back/forward · reload
Visibilitycontinuous group · per-screen group · delay · inline config re-render
UI treecaptured · out of scope · masked fields · modal open (agent sees modal only) · body-portaled listbox under a modal
Callbefore init · before user id · on hidden path · success · denied mic · token failure (no event) · ended · repeated · navigation mid-call (per §15) · full reload
HostingHTTP (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.
  • popover elements → 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 use role="menu" / render inside the modal.
  • The button is draggable (snaps to left/right edge; resets on remount; center can'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; use role="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: pointer or tabindex.
  • List rows with identical text and no unique id → pay, pay_2…; add per-row data-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-mask per 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/week need the browser's exact format.
  • ARIA-only sliders → readable, not settable; native range only.
  • 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

  • currentPath not passed → client navigations missed; always pass it.
  • Next Pages Router asPath → query/hash break matching; use pathname.
  • Trailing-slash config (trailingSlash: true), case differences, router basename → 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_trigger may 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" → /react17 types 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; gate window.__revragDebug and event logging to dev.
  • StrictMode in 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.
  • appVersion from a value that changes every deploy (commit SHA) → the dashboard needs a config per value; ask what version scheme to use.

Security / privacy

  • excludeScreens is 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 errors

Final 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.