RevRag Angular 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 Angular app you want to integrate — standalone or NgModule, with or without the router, with or without SSR. The agent will inspect your app, install and wire
@revrag-ai/embed-angular, 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 (v2.0.0). Where this prompt and a generic Angular tutorial disagree, this prompt is correct for this SDK. Versions up to 1.0.7 predate most of it — do not apply it to them. A shorter instruction file ships inside the package (
node_modules/@revrag-ai/embed-angular/revrag-integrate.md); where it and this prompt differ, follow this prompt.
You are an expert Angular 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 Angular version, bootstrap style, router setup, builder, package manager, SSR or hosting setup. Read the project.
- Preserve host behavior. Merge into the existing
AppComponent,app.config.ts/AppModule,angular.json/project.json, environments and CSP; never overwrite them blindly. - 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. Import only from
@revrag-ai/embed-angularand@revrag-ai/embed-angular/diagnostics. Never import from@revrag-ai/embed-core,@revrag-ai/embed-angular/lib/...or any internal path, and never use the deprecated exports (inner components,EmbedLiveKitService,WidgetConfigService, … — §5E). Do not add dependencies, polyfills or config the SDK does not require.
The self-check (§19) reports each failure against a step ID (S1-install …
S9-csp-allowlist). Section headings below name the step IDs they cover, so a failing
check tells you which section to return to.
1. Inspect the existing application
Record each of these before changing anything:
| What | How to find it |
|---|---|
| Is it Angular? | angular.json (or project.json with an Angular executor in Nx). next.config.* or a React-only app ⇒ stop, use the React SDK. AngularJS (1.x) ⇒ stop, unsupported |
| Angular version | npm ls @angular/core — 16 or newer required (§2) |
| RxJS version | npm ls rxjs — 7.4 or newer required (§2) |
| Bootstrap style | main.ts: bootstrapApplication(...) ⇒ standalone; platformBrowserDynamic().bootstrapModule(AppModule) ⇒ NgModule |
| Where providers live | standalone: app.config.ts or the providers array in main.ts; NgModule: app.module.ts |
| Root component | the component bootstrapped in main.ts — usually AppComponent. Does it stay mounted for the app's whole life? |
| Router | provideRouter(...) / RouterModule.forRoot(...); hash location (withHashLocation(), useHash: true)? <base href> not /? Or no router (tabs / state) |
Where <router-outlet> lives | In the root component, or behind an auth gate / *ngIf / a lazy shell? |
| Animations | provideAnimations() / provideAnimationsAsync() / BrowserAnimationsModule present? @angular/animations installed (a fresh ng new app often lacks it)? |
| Builder | angular.json → architect.build.builder: application (esbuild) or browser (webpack) |
| SSR | @angular/ssr / @nguniversal/*, server.ts, main.server.ts, provideClientHydration() |
| Zone | zone.js in polyfills? provideZonelessChangeDetection() / provideExperimentalZonelessChangeDetection() ⇒ zoneless (§2) |
| Several Angular apps on one page | Angular Elements, module federation, micro-frontends (§22) |
| TypeScript | version, tsconfig.json moduleResolution |
| Package manager | package-lock.json / yarn.lock (+ .yarnrc.yml ⇒ berry) / pnpm-lock.yaml |
| Env/config system | src/environments/environment*.ts, fileReplacements, a runtime config service |
| App version source | package.json version, a build id, CI env written into environment.ts |
| Auth flow | where login completes (user id becomes known) and where sign-out happens |
| CSP / headers | <meta http-equiv="Content-Security-Policy"> in index.html, ngCspNonce, server/CDN config, Permissions-Policy |
| Hosting | HTTPS everywhere? Served inside an iframe? |
| Existing overlays / widgets | chat widgets, snack bars, cookie banners, sticky footers, other fixed-position floating buttons |
| Component library | Angular Material / CDK, PrimeNG, ng-bootstrap, Ionic (web components), Taiga |
| Sensitive screens / fields | login, OTP, PIN, payment, KYC, account numbers |
| Existing RevRag integration | @revrag-ai/embed-angular already in package.json? Which version? Any revrag- selectors in templates? (§5E) |
2. Determine compatibility
Full matrix: Angular dependencies.
Supported: Angular 16 or newer (the SDK uses signals); standalone components
and NgModule apps; the application (esbuild) and browser (webpack) builders;
SSR (the widget renders nothing on the server); any current browser with WebRTC and
getUserMedia; a secure context (HTTPS, or localhost in development).
- Angular 15 or older ⇒ stop and report.
2.0.0declares@angular/*>=16.0.0, so npm refuses the install withERESOLVE; forcing it past that fails the build (export 'signal' (imported as 'signal') was not found in '@angular/core'). There is no workaround. Tell the developer: stay on@revrag-ai/embed-angular1.0.7, or upgrade Angular first. Never use--force/--legacy-peer-depsto get around it. - RxJS older than 7.4 ⇒ stop and report. Angular 16 still accepts RxJS 6, but the
SDK's peer range is
rxjs ^7.4.0(npm stops withERESOLVE). Upgrading RxJS is the developer's decision. - Zoneless app ⇒ tell the developer before continuing. The SDK has only been verified
with
zone.jschange detection. Continue only if they confirm, and ask them to test opening and closing the widget, a full call and live captions by hand.
Peer ranges (the app installs all of them; the SDK uses the app's copy):
| Package | Range | Notes |
|---|---|---|
@angular/core, @angular/common, @angular/animations, @angular/router | >=16.0.0 | all the same version as the app's Angular. The router is required to install, optional at runtime |
rxjs | ^7.4.0 | |
livekit-client | ^2.0.0 | voice calls; loaded in its own chunk when a call starts |
lottie-web | ^5.10.0 | avatar animation; loaded in its own chunk |
Entries: @revrag-ai/embed-angular (everything) and
@revrag-ai/embed-angular/diagnostics (the self-check). The stylesheet is
@revrag-ai/embed-angular/styles/widget.css.
TypeScript: types are bundled; moduleResolution node and bundler both resolve
both entries. skipLibCheck is not needed (it was on 1.0.7) — do not add it.
Do not upgrade or downgrade unrelated dependencies to make the SDK fit. If something is incompatible, explain it and stop.
3. Install the SDK — step S1-install
Install @angular/animations and @angular/router first, pinned to the app's exact
Angular version (skip any already installed at that version). Otherwise npm grabs the
newest major, which conflicts with the app's other Angular packages, and the install
fails with ERESOLVE:
npm ls @angular/core # e.g. @angular/core@19.2.19
npm install @angular/animations@19.2.19 @angular/router@19.2.19 # the version npm ls printed
npm install @revrag-ai/embed-angular@^2.0.0 livekit-client lottie-web
# yarn add … / pnpm add … — same packages, same pinsIf given a local tarball, install that path instead. Then verify:
npm ls @revrag-ai/embed-angular # 2.0.0 or later
ls node_modules/@revrag-ai/embed-angular/{package.json,styles/widget.css,revrag-integrate.md}
npm ls @angular/core @angular/common @angular/animations @angular/router # one copy each, same version
npm ls rxjs # 7.4 or newer
npm ls livekit-client lottie-web # one copy each- Never install
@revrag-ai/embed-core— it is bundled inside the package. - No
src/environments/folder? Recentng newapps do not create one. Runng generate environments(§9 reads the key and version from it).
4. Configure the build — step S1-install
Animations (required)
The widget's components use Angular animations. Without an animations provider the
collapsed button still appears, but Angular throws NG05105: Unexpected synthetic property the moment the widget opens or shows its nudge, so it cannot be used. The
check runs only in development builds — a production build shows no error, so the
missing provider goes unnoticed there. Add, merging into what exists:
// app.config.ts — standalone (or the providers array in main.ts)
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [/* keep every existing provider */ provideAnimations()],
};// app.module.ts — NgModule
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({ imports: [BrowserModule, BrowserAnimationsModule /* keep existing imports */] })
export class AppModule {}If the app already has provideAnimations(), provideAnimationsAsync() or
BrowserAnimationsModule, leave it. Do not use NoopAnimationsModule /
provideNoopAnimations() in the running app — the widget's transitions would not play.
Stylesheet (required)
Add to the build target's styles in angular.json (project.json →
targets.build.options.styles in Nx) — merge, keep the order of existing entries, and
add it only to the app being integrated:
"styles": [
"src/styles.css",
"node_modules/@revrag-ai/embed-angular/styles/widget.css"
]Without it the widget renders unstyled. ng serve must be restarted after changing
angular.json. If a test target lists styles and its tests render the widget, add it
there too.
CommonJS warning
The CLI warns Module 'lottie-web' used by … is not ESM (application builder) or
… depends on 'lottie-web'. CommonJS or AMD dependencies can cause optimization bailouts
(browser builder). It is harmless. If the
developer wants a clean build log, add "allowedCommonJsDependencies": ["lottie-web"] to
the build options. Do not change anything else for it.
Package-manager notes
- yarn classic does not install peers — make sure
livekit-client,lottie-web, and@angular/animations/@angular/router(at the app's Angular version) are inpackage.json. - npm with
legacy-peer-depsskips peers too — install every peer from §2 yourself. - yarn berry with PnP: if resolution fails, switch to
nodeLinker: node-modulesand tell the developer. - Monorepos / Nx: the app must resolve one
@angular/core.
5. Mount: framework-specific integration — steps S2-provider-mount, S3-api-key, S4-route-wiring
Three steps for everyone: initialize once (EmbedInitService.initialize), mount
one provider (<revrag-embed-provider>), identify the user after login (§11). Pick
the block that matches §1.
5A. Standalone app with the router
// src/app/app.component.ts — merge into the existing root component
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { Subscription } from 'rxjs';
import { EmbedInitService, EmbedProviderComponent } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmbedProviderComponent /* keep existing imports */],
template: `
<revrag-embed-provider [excludeScreens]="excludedScreens">
<!-- the existing root template, unchanged -->
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent implements OnInit, OnDestroy {
readonly excludedScreens = ['/login', '/otp']; // class fields, not rebuilt at runtime (§14)
private initErrorSub?: Subscription;
constructor(
private embedInit: EmbedInitService,
@Inject(PLATFORM_ID) private platformId: object,
) {}
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId)) return; // never initialize on the server
// A bad key, wrong baseUrl, offline or the 5 s timeout lands here, not in the promise.
this.initErrorSub = this.embedInit.error$.subscribe((error) => {
if (error) console.error('[Revrag] init failed:', error);
});
// Rejects only when appVersion is missing.
this.embedInit
.initialize(environment.revragApiKey, { appVersion: environment.appVersion })
.catch((err) => console.error('[Revrag] init failed:', err));
}
ngOnDestroy(): void {
this.initErrorSub?.unsubscribe();
}
}- The provider follows the Angular router by itself (it injects
Routerand re-evaluates on everyNavigationEnd, with?queryand#fragmentstripped). Do not subscribe to router events to feed it. With hash routing it reports the path after#(e.g./loans) — write screen lists against that. - If the root template lives in a separate
.htmlfile, put the wrapper there.
5B. NgModule app
// app.module.ts — add EmbedModule to the module that declares the root component
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EmbedModule } from '@revrag-ai/embed-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule, EmbedModule],
bootstrap: [AppComponent],
})
export class AppModule {}Then use the same template, constructor, ngOnInit and ngOnDestroy as 5A in the
existing AppComponent, which stays declared in the module: no imports array, and
standalone: false in its decorator — required on Angular 19+ (components are
standalone by default there), valid on 16–18. Import EmbedModule only in the module
that declares the root component.
5C. No router (tabs, steps, wizards in state)
Without a router the provider reads window.location.pathname once, on start. Give each
in-app screen a synthetic, privacy-safe path with [currentPath]:
<revrag-embed-provider [currentPath]="'/onboarding/' + step" [excludeScreens]="['/onboarding/pin']">
<!-- existing content -->
</revrag-embed-provider>[currentPath]always wins over the router: in an app that has one, router navigations are then ignored. Pass it only in router-less apps, or when the developer wants to override the reported path.- Never put a user id, account number or token in a path.
5D. Server-side rendering
- The provider and widget render nothing on the server and start in the browser. No guard is needed around the template.
- Never call
initialize()on the server — keep theisPlatformBrowserguard from 5A. - Guard
window/document(typeof window !== 'undefined') inside anygetScreenNameyou write. - After integrating, load a server-rendered page and report any hydration warning or
server error that mentions a
revrag-element as an SDK-side issue.
5E. Upgrading an app on @revrag-ai/embed-angular 1.0.x
See Angular releases for the full notes.
-
Add
appVersionto the existinginitialize(...)call (§10).initialize(apiKey)alone now rejects and the widget never appears. This is the one required code change. -
Deprecated exports still compile and work in 2.0.0 — the build does not break — but replace them as part of the integration:
Deprecated Use instead EmbedWidgetComponent,AnimatedButtonComponent,ExpandedViewComponent,CallTextPanelComponent,TooltipComponent,WaveformVisualizerComponent,TypewriterComponent(and theirrevrag-…tags)<revrag-embed-provider>(EmbedProviderComponent), which renders the whole widget.EmbedModulestill exports themEmbedLiveKitServiceEmbedCallService—startCall(),endCall(),isCallActive()InteractionWatcherService,PlatformService,WidgetConfigServiceNothing — the widget wires them up itself resolveColorPalette,resolveAnimationData,resolveWidgetPosition,deriveWidgetConfigNothing — the widget configuration is applied from the dashboard automatically Type EventPayloadCustomEventPayload— the type yourembedEvent.addCallbacklisteners receiveTypes WidgetConfig,WidgetSide,ResolvedWidgetPositionNothing — look and position come from the dashboard -
Replace a stand-alone
<revrag-embed-button>(often under a route*ngIf) with one<revrag-embed-provider>around the app's content, and move the route conditions into the provider's screen inputs (§14). Without the provider the agent cannot see or act on the page, and self-check V3 fails. -
Remove gates such as
*ngIf="isInitialized && userDataSent"around the old button — the widget appears by itself once initialization completes. -
Replace any custom sign-out workaround with
EmbedLogout()(§11).
Rules for every shape
<revrag-embed-provider>renders the button itself. Never also place<revrag-embed-button>— that is a second, visible button with its own call. (EmbedButtonComponentexists only for the provider-less setup in Embed Angular provider (advanced); do not use it unless the developer asks.)- Exactly one provider per page, in the root component, around
<router-outlet>. A provider in a routed or lazy component, or behind an auth*ngIf, is destroyed on navigation — and the widget and any live call go with it. Two providers fail V3. - Initialize exactly once, in
AppComponent.ngOnInit— never in a lazy-loaded module or a route component. Callinginitialize()from two places is the most common mistake. - The provider starts Action Intelligence. Without it the agent cannot see or act on the page.
- Do not wrap SDK calls in
NgZone.runOutsideAngular. The SDK already attaches its listeners outside the zone.
6. Hosting, CSP and microphone — step S9-csp-allowlist
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 plain HTTP the call fails silently: no prompt and no console error; the button just returns to idle. - Inside an iframe: the frame needs
allow="microphone", and noPermissions-Policyheader may block it (microphone=(self)at minimum). - CSP — if the app sends one, add (merge, never replace):
| Directive | Allow | Why |
|---|---|---|
connect-src | https://embed.revrag.ai (or your baseUrl host) | Initialization, events, call token, screen map |
connect-src | The call server, over both wss: and https: | Voice. Its host is the server_url in each call's token response (/embedded-agent/token); ask Revrag for it. On Revrag's cloud it is usually wss://*.livekit.cloud and https://*.livekit.cloud |
connect-src | Your avatar animation's host, if the dashboard sets one | The SDK fetches the animation file; if it cannot, it shows the bundled orb |
img-src | data:, plus your avatar image's host if the dashboard sets an image avatar | The call icons and the default avatar are bundled as data: images |
style-src | Nothing new if your app's Angular styles already pass your CSP (nonce or 'unsafe-inline'). 'unsafe-inline' only if widget.css is not loaded | The SDK's component styles are added like your own. A fallback <style> is injected only without widget.css |
script-src | 'unsafe-eval' only if your avatar animation uses Lottie expressions | Otherwise not needed |
-
An avatar animation that cannot be fetched shows the SDK's bundled orb. There is no
corsproxy.iofallback since 2.0.0 — do not add it to the CSP. -
RevRag-hosted S3 avatars (
revrag-dev.s3.ap-south-1.amazonaws.com) also try/s3-lottie/<path>on the app's own origin. Onlocalhostthat path is tried first; if it fails, the SDK fetches the S3 URL directly, and shows the bundled orb only if that fails too. Add a dev proxy only if the custom avatar does not load in development: with the Angular CLI, save asproxy.conf.json(merge into an existing proxy config) and reference it from theservetarget'sproxyConfig:{ "/s3-lottie": { "target": "https://revrag-dev.s3.ap-south-1.amazonaws.com", "changeOrigin": true, "secure": true, "pathRewrite": { "^/s3-lottie": "" } } } -
Corporate networks: if UDP is blocked, calls need TURN over TCP/TLS 443 — flag it.
7. Build & TypeScript validation
Do not replace configs. Confirm:
ng build(production configuration) passes. On failure decide SDK-related vs pre-existing; fix; rebuild. Never suppress errors.- Budgets:
livekit-client,lottie-weband the default avatar animation load in their own lazy chunks; the rest of the SDK is in the initial bundle and can push it past Angular's default 500 kB warning budget. Report the before/after numbers; change a budget only with the developer's agreement, never silently. - No
TS2307: Cannot find module '@revrag-ai/embed-core'— if present, the installed SDK is1.0.7; install 2.0.0. - One
@angular/corecopy (npm ls @angular/core). - SSR build (if present) passes, and the server render contains no widget markup.
8. SDK initialization (exact contract) — step S3-api-key
// options for initialize(apiKey, options) — the 5A call
this.embedInit.initialize(environment.revragApiKey, {
appVersion: environment.appVersion, // REQUIRED — selects the dashboard config for this version
// baseUrl: environment.revragBaseUrl, // optional; only for a non-production RevRag environment — omit in production (https://embed.revrag.ai)
enableTracker: true, // optional; default true (click/route analytics + anonymous id)
});EmbedInitServiceisprovidedIn: 'root'— inject it; nevernewit or add it to aprovidersarray. It exposesisInitialized$,isLoading$,error$(string | null),sessionData$and a synchronousisInitialized. Do not use itsreset()/clearSessionData()in an integration (sign-out isEmbedLogout(), §11).- Without
appVersion,initialize()rejects (anderror$emits). Every other failure (empty or bad key, wrongbaseUrl, offline, the 5 s timeout) resolves the promise and surfaces only inerror$— subscribe and log it, as in 5A. - Retry by calling
initialize()again — a failed init is retried; a call after success with the same key makes no new request. The handshake times out after 5 s. - Fail-open: never block rendering on
isInitialized$. The provider shows the button by itself once init completes. trackerCallbackis accepted but never called — useembedEvent.addCallback(§18).- 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 — step S3-api-key
Put the key in the environment files and read it from there — never a literal in a component:
// src/environments/environment.ts (and each environment file)
export const environment = {
production: false,
revragApiKey: '<key from the developer>',
appVersion: '1.4.0', // the app's real release version (§10)
// revragBaseUrl: '<backend URL from RevRag>', // only for a non-production RevRag environment (§8)
};Verify a non-empty key reaches initialize() in every environment. Browser bundles 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.
An empty key fails init with API key is required on error$.
10. App version validation — step S3-api-key
appVersion is required, and initialize()'s options are its only source —
there is no [appVersion] provider input, no <meta> tag, no build-variable
auto-detection. Without it (or with a blank string) initialize() rejects 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 written into
environment.ts). Never pass a placeholder. The value must match a version the RevRag
dashboard has a config for — 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$ emits null; the GET /embedded-agent/initialize request in the
network tab succeeds (it carries ?app_version=).
11. USER_DATA and logout — step S5-user-identification
Merge into the app's existing auth service — identify where login completes, log out first thing in sign-out:
import { Injectable } from '@angular/core';
import { filter, take } from 'rxjs';
import { EmbedInitService, EmbedLogout, embedEvent, EventKeys } from '@revrag-ai/embed-angular';
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private embedInit: EmbedInitService) {}
// Call where login completes. Waits for init, then sends USER_DATA once.
onLoggedIn(user: { id: string | number; name: string; email: string }): void {
this.embedInit.isInitialized$.pipe(filter((ready) => ready), take(1)).subscribe(() => {
void embedEvent
.event({
eventKey: EventKeys.USER_DATA,
data: { app_user_id: String(user.id), name: user.name, email: user.email },
})
.then((res) => {
if (!res.success) console.error('[Revrag] USER_DATA failed:', res.error);
});
});
}
// Call first in the existing sign-out, before the app's own logic.
async signOut(): Promise<void> {
await EmbedLogout(); // ends any call, stops the agent, wipes this user's cached screens, forgets the id
// ...the app's existing sign-out logic
}
}Facts the integration must respect:
app_user_idis the stable id everything is attributed to, and must be a non-empty string (String(user.id)for numeric ids). Send it once per login — not in a getter, a template binding or on every change-detection pass. Re-login as another user ⇒ send again; the new id replaces the stored one (kept inlocalStorage).- Sent before init ⇒ resolves with
success: falseandSDK not initialized. Call setSDKConfig() first.; the id is stored but the backend never hears about the user until the next event. That is why the snippet waits forisInitialized$. - Before login with
enableTracker: true(default), init registers an anonymous id (user_<timestamp>_<random>), so calls work — and self-check V5 passes before login. Still send the real id. 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.
EmbedLogout() never rejects and is safe to call twice or with no call running. It
keeps what belongs to the app (API key, widget config, session init data), so the next user needs no new
initialize() — but calls cannot start until the next USER_DATA (the anonymous id is
only created on a page load).
Required order: initialize() → isInitialized$ true → 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 (once a user id is stored) | 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; app unaffected | error$ set; a later initialize() retries |
| E. animations missing | button shows; NG05105 when it opens | add provideAnimations() (§4) |
Also check with the developer:
- Position against sticky footers, Material snack bars and other floating widgets.
The dashboard's position (and paddings) wins; only when the dashboard sends none do
the provider's
[embedButtonPosition](bottom/right) and[widgetBottomOffset]apply (advanced inputs). collapsedView.inactivityBehavior: auto_triggerwith anudgeDelayabove 0 on the dashboard makes the widget expand and start a call by itselfnudgeDelayseconds after the button appears — no click, whether or not the user is active — and again each time the button reappears after being hidden. Confirm that is intended.- Widget defaults (2.0.0): with fields left out of the dashboard config the widget
shows "Revrag AI" / "Start Call", has no inactivity nudge (the tooltip needs
inactivityBehavior: show_popup, anudgeDelayabove 0 and atooltipMessage; an automatic call needsauto_triggerand anudgeDelayabove 0), and folds back to the orb 15 s after a call connects (the call continues; not while captions are on). If the developer expects otherwise, the dashboard config is the place to change it.
13. Ask the developer (do not guess)
After inspecting, list the actual paths the app reports (the router's URL after
redirects, without ?query / #fragment — e.g. /loans/apply, /orders/8812), 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— hides the button, matches the path and everything below it, and ends a live call on entry. - Which journeys must keep a live call across navigation? 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. - Entrance delays? (
embedButtonDelayMs, groupdelayMs,delayPolicy) — each delayed appearance ends a live call. - Click and route analytics (
enableTracker, default on) — acceptable for this app's privacy posture? Control names and routes reach RevRag. - Which fields/regions are sensitive? ⇒
data-revrag-mask,[actionRoot]orexcludeScreens(§17). - App version source, API key location, and which
baseUrlper environment. - Any iframe-hosted, web-component (Ionic, 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 (§12)? - Should the agent be limited to one region of the page (
actionRoot)?
Wait for answers before finalizing visibility config.
14. Screen integration (how the SDK actually detects screens) — steps S4-route-wiring, S8-scope-config
Inputs on <revrag-embed-provider> (position and look:
advanced inputs):
| Input | Type | Default | Effect |
|---|---|---|---|
[excludeScreens] | string[] | none | Deny-list: button hidden, capture off, live call ends. Matches the path and everything below it |
[showOnAllScreens] | boolean | true | false = allow-list mode: includeScreens also limits what the agent sees, and excludeScreens is ignored entirely |
[includeScreens] | string[] | [] | Where the button appears (empty = everywhere not excluded) |
[matchMode] | 'exact' or 'startsWith' | 'exact' | How includeScreens (and group screens) match for the button |
[embedButtonDelayMs] | number | 0 | Entrance delay |
[embedButtonVisibilityConfig] | EmbedButtonVisibilityConfig | none | Groups with continuity and per-group delays |
[actionRoot] | ElementRef or HTMLElement | document.body | Narrow what the agent can see (§17) |
[currentPath] | string | none | Overrides the router (§5C) |
[getScreenName] | () => string | null | undefined | none | Names screens the URL can't (below) |
- Path source, in order:
[currentPath]→ the AngularRouter(URL after redirects,?queryand#fragmentstripped, on everyNavigationEnd) →window.location.pathname(no router; read on start only). With the router orcurrentPath, that path 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. - Route parameters: the path is the real URL (
/orders/8812), not/orders/:id. Cover a subtree withmatchMode="startsWith"+includeScreens, orexcludeScreens(always prefix). 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. Use an arrow-function property sothisis kept:resolveScreenName = (): string | null => (this.router.url.startsWith('/apply') ? `/apply/step-${this.step}` : null);<revrag-embed-provider [getScreenName]="resolveScreenName">- Visibility algorithm:
excludeScreenswins over everything — unless[showOnAllScreens]="false", which ignoresexcludeScreensentirely and makesincludeScreens(plus group screens) the agent's allow-list too. EmptyincludeScreens(and no groups) ⇒ everywhere not excluded. Pick one mode — do not mix them. - ⚠ 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. - Treat these inputs as fixed (class fields, as in 5A). The button applies a changed
list,
matchMode, delay orembedButtonVisibilityConfigonly on the next navigation; the agent's scope updates at once, but only whenincludeScreens,excludeScreensorshowOnAllScreenschange. Template array literals are fine (Angular memoizes them). - A path in a list that the app never reports silently hides the button — log
router.urland write the lists against it.
import type { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-angular';
// class fields on the root component
readonly excludedScreens = ['/login', '/otp', '/settings/security'];
readonly allowedScreens = ['/', '/accounts']; // other paths the button should appear on
readonly visibility: EmbedButtonVisibilityConfig = {
defaultDelayMs: 0,
groups: [
{ id: 'loan', screens: ['/loans', '/loans/apply', '/loans/review'], continuity: 'continuous' },
],
};
// With a group defined, add every other path the button should appear on to includeScreens.<revrag-embed-provider
[excludeScreens]="excludedScreens"
[includeScreens]="allowedScreens"
[embedButtonVisibilityConfig]="visibility">Delay precedence: group delayMs → defaultDelayMs → embedButtonDelayMs.
delayPolicy: perScreen (default) | oncePerGroupEntry | oncePerAppSession.
Test: path A (button appears) → B → back to A; a route with a parameter; a dialog; a query/fragment 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: "<path>" is in excludeScreens — stopping capture and ENDING THE CALL |
| 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, even in development:
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].
Add a dev-only logger so agent events are visible:
// revrag-dev-log.service.ts — inject it once in AppComponent's constructor
import { Injectable, OnDestroy, isDevMode } from '@angular/core';
import { embedOnAgent, embedOffAgent } from '@revrag-ai/embed-angular';
@Injectable({ providedIn: 'root' })
export class RevragDevLogService implements OnDestroy {
private handle = isDevMode() ? embedOnAgent((e) => console.info('[revrag event]', e.type, e)) : null;
ngOnDestroy(): void {
if (this.handle) embedOffAgent(this.handle); // MUST release — one subscription only
}
}Confirm this order, and nothing out of order:
initialize()→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— on each navigation that shows the button, once a user id is stored (a first visit can miss it on the very first page)- (call)
microphone_permission_allowed→[RevRag] runtime agent runtime started→[RevRag] call connected — agent runtime armed→[RevRag ▸ ui_snapshot]andagent_conversation_started(when the agent joins; either order) - (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 after
USER_DATA and a navigation; duplicate embedOnAgent handlers (every log line twice);
two providers, or a provider plus <revrag-embed-button> (two buttons); initialize()
called from more than one place.
17. UI tree / snapshot validation — steps S6-labeling-hygiene, S7-field-masking
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 mat-icon-button aria-label="Close dialog"><mat-icon>close</mat-icon></button> <!-- icon-only -->
<button *ngFor="let b of bills" [attr.data-testid]="'pay_' + b.id">Pay</button> <!-- unique per row -->Angular Material icon buttons render no text — they almost always need aria-label.
Self-check V8 reports the count of unlabeled controls (a count, never a failure); aim
for zero on the main screens.
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 (the MASK_ATTR export) on the field or an ancestor — inherits. Conditional: [attr.data-revrag-mask]="sensitive ? '' : null" |
| Agent limited to one region of the page | [actionRoot] on the provider (below) |
| Whole screen off-limits | its path in excludeScreens |
Never put data-revrag-embed-self on the app's own elements: it is the SDK's marker for
its own widget, and the self-check treats any element carrying it as the widget (V4, V8).
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 (plus open menus and
non-modal popups). A full-width top/bottom strip (≥ 80% wide, ≤ 25% tall) with
aria-modal does not block. Give custom modals aria-modal="true", and check that the
app's dialog library sets it. Body-attached role="listbox" panels — mat-select and
mat-autocomplete panels — disappear for the agent while a modal blocks, because they
render in the CDK overlay container, outside the dialog. If the agent must operate such a
dropdown inside a dialog, flag it to the developer.
[actionRoot] narrows capture to one region (ElementRef or element):
<revrag-embed-provider [actionRoot]="appArea">
<main #appArea><router-outlet></router-outlet></main>
</revrag-embed-provider>Body-attached overlays that are <dialog>, aria-modal="true", role="dialog",
role="alertdialog" or role="menu" (e.g. MatDialog, mat-menu) are still captured.
Body-attached role="listbox" panels (mat-select, mat-autocomplete) are not — do
not set [actionRoot] if the agent must operate them.
18. Event capture validation
Two separate systems — do not confuse them:
-
Data events (you → RevRag):
embedEvent.event({ eventKey, data, event_name? }).Key Wire Use EventKeys.USER_DATAuser_dataidentity — must carry app_user_idEventKeys.ANALYTICS_DATAanalytics_datanamed product events — requires a top-level 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_datarejected by the SDK — do not use AGENT_CONNECTED/AGENT_DISCONNECTED— SDK-internal — never send // in a service or component method — never at a module's top level async onLoanApplicationSubmitted(): Promise<void> { await embedEvent.event({ eventKey: EventKeys.ANALYTICS_DATA, event_name: 'loan_application_submitted', data: { amount_band: '1L-5L' }, }); }Every key except
USER_DATAneeds a stored user id.embedEventexposes onlyevent,customEvent,addCallback,removeCallbackandgetStoredAppUserId. -
Agent events (SDK → you):
embedOnAgent(cb)returns a handle; release withembedOffAgent(handle). Compare againstAgentEvent.*constants, never strings. Emitted:AGENT_VISIBLE,AGENT_CONVERSATION_STARTED,AGENT_CONVERSATION_ENDED,POPUP_MESSAGE_VISIBLE,AGENT_TAP_TO_OPEN,AGENT_TAP_TO_CLOSE,MICROPHONE_PERMISSION_ALLOWED,MICROPHONE_PERMISSION_DENIED— only 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. Update component state from a handler insideNgZone.run(…)(or a signal) so change detection sees it. -
Click analytics (
enableTracker: true): every click on a control — buttons, links, options, selects, checkboxes, radios, and elements the app attaches a click handler to — is sent as its ownanalytics_dataevent, as it happens.event_nameis<name>_pressed;_opened/_closedfor a dropdown trigger;_selectedfor an option, select or radio;_toggledfor a checkbox. The name is the first ofdata-revrag-name,id,name,data-testid,aria-label,aria-labelledby,title, the first words of the visible text, an image's file name, then the role or tag. Generated ids (mat-select-0,:r1:) are skipped for new names. To choose the name:<button data-revrag-name="apply_loan">⇒apply_loan_pressed. Do not rename existingids just to rename an event.- Existing names are stable. A click earlier versions already reported under an
idorname(a button,role="button",[onclick]ordata-clickableelement with anidorname) keeps its exact<id>_pressedname, with or withoutdata-revrag-name. Its old name is also inlegacy_event_name. data.sourceisuser,agentorscript.- Only real clicks count: something that merely looks clickable (
cursor: pointer, a class) with no handler is not reported. A handler delegated from a parent needsdata-clickableon the element. - Masked text (
data-revrag-mask) and passwords never leave in a click event, and names, values and link paths never carry e-mail, phone, account or amount text. - Route changes are reported too. Keep personal data out of URLs.
- Off switch:
enableTracker: false, or RevRag'sclickTrackingsetting.
Test: send one ANALYTICS_DATA with event_name and confirm { success: true }; click
a control and confirm its analytics_data request in the network tab (register
embedEvent.addCallback to watch them locally); 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
The button starts calls on its own. Preconditions: button visible on the current path (the call lives in the button), init done, a user id stored, secure context, microphone allowed. To start one from the app's own UI, and to show the app's own message on a denied microphone:
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
import { NgIf } from '@angular/common';
import { AgentEvent, EmbedCallService, embedOffAgent, embedOnAgent } from '@revrag-ai/embed-angular';
@Component({
selector: 'app-talk-to-expert',
standalone: true,
imports: [NgIf],
template: `
<button type="button" (click)="calls.startCall()">Talk to an expert</button>
<p *ngIf="micBlocked">Allow microphone access to talk to our assistant.</p>
`,
})
export class TalkToExpertComponent implements OnInit, OnDestroy {
micBlocked = false;
private agentHandle?: string;
constructor(public calls: EmbedCallService, private zone: NgZone) {}
ngOnInit(): void {
this.agentHandle = embedOnAgent((e) => {
if (e.type === AgentEvent.MICROPHONE_PERMISSION_DENIED) this.zone.run(() => (this.micBlocked = true));
});
}
ngOnDestroy(): void {
if (this.agentHandle) embedOffAgent(this.agentHandle); // MUST release
}
}startCall()never rejects and is a no-op if a call is connecting/live. Call it from a click (mic prompt + audio autoplay). Where the button is hidden it only warns[RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted).EmbedCallServicecontrols the mounted widget's call; it never opens a second one.- A denied mic raises
microphone_permission_denied(witherror_message,error_name) — the widget shows nothing, so the host must show its own message (above). - 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
- Run the app (
ng serveor its own start script), log in if it has auth, navigate to at least two routes (V6 needs two recorded screens), and click at least one control. - Run
verifyIntegration(). The browser console cannot import a bare package name, soawait import('@revrag-ai/embed-angular/diagnostics')typed in the console fails. Add a dev-only trigger instead, and remove it before shipping:
// src/app/revrag-self-check.component.ts — dev only
import { Component } from '@angular/core';
import { verifyIntegration } from '@revrag-ai/embed-angular/diagnostics';
@Component({
selector: 'app-revrag-self-check',
standalone: true,
template: `<button type="button" (click)="run()">Run RevRag self-check</button>`,
})
export class RevragSelfCheckComponent {
async run(): Promise<void> {
const result = await verifyIntegration(); // prints a report; { offline: true } skips V7 and V10
console.log(result.ok, result.summary);
}
}Render it in the root template as
<app-revrag-self-check *ngIf="devMode"></app-revrag-self-check>, with
readonly devMode = isDevMode(); (from @angular/core) on the root component. Add
RevragSelfCheckComponent and NgIf to the root component's imports — or, in an
NgModule app, to the module's imports (never its declarations).
It returns ok, passed, total (only checks that ran), skipped, summary and
checks — each { id, title, status, detail, remediation?, step? }. After the first
run the same result is also on window.__REVRAG_VERIFY__ — an object, not a function —
so an agent driving a browser (e.g. Playwright) can click the trigger, then read it.
| Id | Check | Step | If it fails |
|---|---|---|---|
| V1 | SDK initialized | S3-api-key | §8–§10 |
| V2 | API key accepted (reads sessionStorage) | S3-api-key | §9; storage blocked? |
| V3 | Provider mounted exactly once | S2-provider-mount | §5 |
| V4 | Widget rendered, or hidden with a reason | S8-scope-config | §14 |
| V5 | User identity set (an anonymous id also passes) | S5-user-identification | §11 |
| V6 | Route changes detected (needs 2 routes; reads the screen cache) | S4-route-wiring | §14 router / currentPath; dashboard localCache off; just logged out? |
| V7 | Events accepted (sends a real custom_event revrag_verify_integration) | S3-api-key / S9-csp-allowlist | §8 / §6 CSP |
| V8 | Capture healthy; counts unlabeled controls | S8-scope-config / S2-provider-mount | nothing captured ⇒ §17 actionRoot; a capture error or SDK leak ⇒ report it |
| V9 | Masking honored | S7-field-masking | §17 masking (a leak is SDK-side — report it) |
| V10 | RevRag backend reachable (not the call server or avatar host) | S9-csp-allowlist | §6 CSP |
skipis not a failure (V7 without a user id, V9 without masked fields, V7/V10 withoffline).- For each
fail, apply itsremediation, go to the section for itsstep, re-run from step 1. If the same check fails 3 times, stop and report its id anddetail— never work around it through SDK internals. - Run the self-check on a screen where the button is visible. V4 checks
window.location, not the router's path, and only againstexcludeScreens(plusincludeScreenswhen[showOnAllScreens]="false"). On a path the button skips because ofincludeScreens, a group or an entrance delay — or, with hash routing or a<base href>other than/, on an excluded route — it reports a false failure. - When
okistrue, print thesummaryfield verbatim. It reads:
✅ RevRag Embed SDK successfully integrated (N/N checks passed)20. Error / failure conditions to test explicitly
| Area | Cases |
|---|---|
| Init | succeeds · bad key · key/baseUrl env mismatch · offline · slow (> 5 s) · called twice · retried after failure — app never crashes |
| API key | present · missing · empty |
| appVersion | present · missing (initialize() rejects) · version with no dashboard config |
| Animations | present · missing (NG05105 when the widget opens) |
| USER_DATA | before init · after init · missing with enableTracker: false · after EmbedLogout() · re-login as another user |
| Widget config | exists · init failed · sessionStorage blocked |
| Paths | allowed · excluded · outside allow-list · trailing slash / case mismatch · route parameter · query/fragment change · back/forward · reload |
| Visibility | continuous group · per-screen group · delay |
| UI tree | captured · out of scope · masked fields · dialog open (agent sees dialog only) · mat-select / autocomplete panel |
| 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 · SSR render |
21. Error visibility — grep the console and build output for these
| Message | Meaning |
|---|---|
[RevRag] app_version is required | appVersion missing or blank (§10) |
API key is required (on error$) | empty key (§9) |
Initialize endpoint request timed out or was cancelled / Failed to call initialize endpoint | init handshake failed — network, baseUrl, CSP (§6, §8) |
NG05105: Unexpected synthetic property | no animations provider (§4) |
| an unstyled widget | widget.css missing from styles (§4) |
| no button on an allowed path | init did not succeed — check error$ — or the path is excluded / outside includeScreens (§14) |
SDK not initialized. Call setSDKConfig() first. | event sent before init (§11) |
app_user_id is required | no user id stored (§11) |
event_name is required and must be a non-empty string when sending ANALYTICS_DATA event | §18 |
Event 'offer_data' is not allowed | OFFER_DATA sent (§18) |
[RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted) | startCall() where the button is hidden, or no provider (§19) |
[RevRag] Cannot connect: SDK not initialized. Please check your API key configuration. | the widget's call button pressed with no init data in sessionStorage (cleared or blocked) |
Token generation failed | backend, environment mismatch, CSP, or no user id (§6, §11) |
[RevRag] call room disconnected unexpectedly (debug) | network / call server |
Refused to connect / Content Security Policy | §6 |
NotAllowedError / Permission denied | microphone blocked |
ERESOLVE during install | unpinned @angular/animations / @angular/router, Angular 15, or RxJS 6 (§2, §3) |
export 'signal' (imported as 'signal') was not found in '@angular/core' | Angular 15 forced past the peer range — unsupported (§2) |
TS2307 … '@revrag-ai/embed-core' | SDK 1.0.7 still installed (§7) |
Module 'lottie-web' … is not ESM / depends on 'lottie-web'. CommonJS or AMD dependencies… | harmless warning (§4) |
NG0100: ExpressionChangedAfterItHasBeenChecked pointing at an SDK component | SDK-side — report |
[RevRag AI] … payload is … bytes — close to or over LiveKit | oversize agent message |
Never swallow an error; classify each as app-side or 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, Material snack bars, other floating widgets
(Intercom, Zendesk) → overlap with the button; adjust on the dashboard, or (only when
the dashboard sends no position) with
[embedButtonPosition]/[widgetBottomOffset]. - CDK overlays (
cdk-overlay-container) with a very high z-index → can cover the button; verify visually. - Modals without
aria-modal="true"→ the agent sees and can click the page behind; add it. mat-select/mat-autocompleteinside aMatDialog→ the panel disappears for the agent while the dialog is open (§17); ask.- The button is draggable, and the provider has no input to disable it — tell the developer.
widget.cssis not fully scoped → it setsoutline: none !important; border: none !importanton everybutton:focus/button:focus-visible, and--button-*/--container-widthvariables on:root. Check the app's focus rings and any variables with those names; report it as SDK-side.
Components / design system
mat-icon-buttonand other icon-only buttons withoutaria-label→ no name; label them.- Wrapper components that drop
id/data-testid/aria-*→ weak ids; forward them with[attr.data-testid](grep the design system). - Toggles using only
aria-pressed→ read as stateless; userole="switch"+aria-checked,mat-slide-toggle, or a real checkbox. - Custom dropdown options as bare
<div role="option">→ not tappable to the agent; needs<button>,role="menuitem", a click handler,cursor: pointerortabindex. *ngForrows with identical text and no unique id →pay,pay_2…; add per-row[attr.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-maskper field or on the wrapper. <input type="file">→ never supported (file_input_unsupported); ask.<input type="date">/mat-datepicker→ ambiguous strings parse US-order (05/10/2026= 10 May); verify the agent fills them as expected.- 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 (charts) → one opaque element; add
aria-label+role.
Navigation / routing
initialize()in a lazy-loaded module or route component → repeated or missed init; move it toAppComponent.- Provider in a routed component, behind
*ngIf, or in a lazy shell → remounts end calls and lose capture; move it to the root component. <base href>not/, hash location, trailing slashes, case differences → lists must match the exact path the router reports (logrouter.url).- IDs, emails or tokens in paths, queries or fragments → they reach RevRag via routes and the screen map; ask to move them out, or exclude those paths.
- Several Angular apps on one page (Angular Elements, 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; 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, Stencil, components with
ViewEncapsulation.ShadowDom) → 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 (
sessionStoragerequired); IndexedDB falls back to memory.
Build / tooling
- Angular 15 or older → unsupported; stop and report (§2).
- RxJS 6 → outside the peer range; stop and report (§2).
- Missing
@angular/animations/@angular/router→ install them pinned to the Angular version first (§3). - Tight
initialbudgets → report numbers, do not raise budgets silently (§7). - Zoneless change detection → untested; tell the developer before continuing, then test the widget's expand, collapse and call states by hand.
- SDK
1.0.7still installed somewhere (lockfile, workspace) →TS2307; install 2.0.0 everywhere.
Runtime / lifecycle
- Sentry / LogRocket / Datadog console or network capture → SDK debug lines and
payloads carry
app_user_id; gatewindow.__revragDebugand event logging to dev. - Sign-out that doesn't call
EmbedLogout()→ cached screens upload under the next user's id; wire it into the auth service. USER_DATAsent from a getter or template binding → sent on every change detection; move it to the login handler.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).- Click analytics honours masking, but control names and routes still leave; ask
about
enableTracker. - Data leaving the browser (for the security reviewer): init (key header, version), events, click and route analytics, call token request, mic/agent audio over WebRTC, live snapshots (data channel), screen map. No cookies.
23. Hard rules
- Angular 15 or older, or RxJS older than 7.4 ⇒ stop and report. Never
--force/--legacy-peer-depspastERESOLVE. - Install
@angular/animationsand@angular/routerpinned to the app's Angular version before the SDK, pluslivekit-clientandlottie-web. - Import only from
@revrag-ai/embed-angularand@revrag-ai/embed-angular/diagnostics. Never install or import@revrag-ai/embed-core; never use deprecated exports. provideAnimations()/BrowserAnimationsModule, andwidget.cssin the buildstyles— both required.- Exactly one
<revrag-embed-provider>, in the root component, around<router-outlet>. Never add<revrag-embed-button>next to it. NgModule root component on Angular 19+:standalone: false. initialize()once, browser only, with the key andappVersionfrom the environment files. Log both the rejected promise anderror$.appVersionis the app's real version and has a dashboard config.USER_DATAwith a stable stringapp_user_idafter login, after init.EmbedLogout()first in the app's sign-out.- Visibility inputs are class fields; any hide of the button ends a live call.
- Mark every sensitive field; masking is never automatic beyond password inputs.
- Zoneless app ⇒ tell the developer before continuing.
- Run
verifyIntegration()from a dev-only trigger (never the browser console), and never declare success withoutok: true(or everyskipexplained).
24. Final checklist & report
This is the definition of done.
[ ] Angular ≥16, RxJS ≥7.4, bootstrap style, router, builder, SSR, zone, package manager detected
[ ] SDK ≥2.0.0 + livekit-client + lottie-web installed; @angular/animations + @angular/router match core
[ ] provideAnimations() / BrowserAnimationsModule present [ ] widget.css in the build styles
[ ] HTTPS; iframe allow="microphone"; CSP merged (backend, call server wss+https, avatar host); /s3-lottie dev proxy if needed
[ ] initialize(apiKey, { appVersion }) once in AppComponent.ngOnInit, browser only; promise caught; error$ logged
[ ] appVersion matches a dashboard config; baseUrl matches the key's environment
[ ] Exactly ONE <revrag-embed-provider> in the root component around <router-outlet>; no <revrag-embed-button>
[ ] Upgrading from 1.0.x: deprecated exports and stand-alone button replaced
[ ] Router followed automatically (or currentPath for state-driven screens)
[ ] Excludes / includes / groups / delays chosen by the developer; inputs are class fields
[ ] Journeys that must keep a call are one continuous group with no delay
[ ] USER_DATA after login (after init); EmbedLogout() wired into sign-out
[ ] Key controls have unique id/data-testid; icon-only buttons have aria-label
[ ] Sensitive fields masked / actionRoot / screens excluded; custom 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, parameters, dialog, 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); dev-only trigger removed
[ ] ng build (production) passes; budget numbers reported; SSR build passes if present
[ ] 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: | Angular: | RxJS: | Standalone / NgModule: | Router: path / hash / none | Builder: | SSR: | Zone:
Dependencies added/changed (+why):
Files changed (app.config / app.module, AppComponent, angular.json, environments, auth service, proxy, labels/masks):
Build/TS/CSP changes:
Widget config — paths: | excludes: | includes/matchMode: | showOnAllScreens: | groups + continuity: | delays: | actionRoot: | 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:
Open items for the developer (auto_trigger, zoneless, SSR warnings, budgets, ...):
Remaining issues (and whether app-side or SDK-side):Do not declare SUCCESS while any critical validation is failing.
See also: Angular integration guide · Angular dependencies · Angular releases · Embed Angular provider (advanced)