Browse In-App Agent

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

  1. Inspect first, modify second. Never assume the Angular version, bootstrap style, router setup, builder, package manager, SSR or hosting setup. Read the project.
  2. Preserve host behavior. Merge into the existing AppComponent, app.config.ts / AppModule, angular.json / project.json, environments and CSP; never overwrite them 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. Import only from @revrag-ai/embed-angular and @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:

WhatHow 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 versionnpm ls @angular/core — 16 or newer required (§2)
RxJS versionnpm ls rxjs — 7.4 or newer required (§2)
Bootstrap stylemain.ts: bootstrapApplication(...) ⇒ standalone; platformBrowserDynamic().bootstrapModule(AppModule) ⇒ NgModule
Where providers livestandalone: app.config.ts or the providers array in main.ts; NgModule: app.module.ts
Root componentthe component bootstrapped in main.ts — usually AppComponent. Does it stay mounted for the app's whole life?
RouterprovideRouter(...) / RouterModule.forRoot(...); hash location (withHashLocation(), useHash: true)? <base href> not /? Or no router (tabs / state)
Where <router-outlet> livesIn the root component, or behind an auth gate / *ngIf / a lazy shell?
AnimationsprovideAnimations() / provideAnimationsAsync() / BrowserAnimationsModule present? @angular/animations installed (a fresh ng new app often lacks it)?
Builderangular.json → architect.build.builder: application (esbuild) or browser (webpack)
SSR@angular/ssr / @nguniversal/*, server.ts, main.server.ts, provideClientHydration()
Zonezone.js in polyfills? provideZonelessChangeDetection() / provideExperimentalZonelessChangeDetection() ⇒ zoneless (§2)
Several Angular apps on one pageAngular Elements, module federation, micro-frontends (§22)
TypeScriptversion, tsconfig.json moduleResolution
Package managerpackage-lock.json / yarn.lock (+ .yarnrc.yml ⇒ berry) / pnpm-lock.yaml
Env/config systemsrc/environments/environment*.ts, fileReplacements, a runtime config service
App version sourcepackage.json version, a build id, CI env written into environment.ts
Auth flowwhere 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
HostingHTTPS everywhere? Served inside an iframe?
Existing overlays / widgetschat widgets, snack bars, cookie banners, sticky footers, other fixed-position floating buttons
Component libraryAngular Material / CDK, PrimeNG, ng-bootstrap, Ionic (web components), Taiga
Sensitive screens / fieldslogin, 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.0 declares @angular/* >=16.0.0, so npm refuses the install with ERESOLVE; 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-angular 1.0.7, or upgrade Angular first. Never use --force / --legacy-peer-deps to 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 with ERESOLVE). Upgrading RxJS is the developer's decision.
  • Zoneless app ⇒ tell the developer before continuing. The SDK has only been verified with zone.js change 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):

PackageRangeNotes
@angular/core, @angular/common, @angular/animations, @angular/router>=16.0.0all 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.0voice calls; loaded in its own chunk when a call starts
lottie-web^5.10.0avatar 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 pins

If 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? Recent ng new apps do not create one. Run ng 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 in package.json.
  • npm with legacy-peer-deps skips peers too — install every peer from §2 yourself.
  • yarn berry with PnP: if resolution fails, switch to nodeLinker: node-modules and 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 Router and re-evaluates on every NavigationEnd, with ?query and #fragment stripped). 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 .html file, 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 the isPlatformBrowser guard from 5A.
  • Guard window / document (typeof window !== 'undefined') inside any getScreenName you 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 appVersion to the existing initialize(...) 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:

    DeprecatedUse instead
    EmbedWidgetComponent, AnimatedButtonComponent, ExpandedViewComponent, CallTextPanelComponent, TooltipComponent, WaveformVisualizerComponent, TypewriterComponent (and their revrag-… tags)<revrag-embed-provider> (EmbedProviderComponent), which renders the whole widget. EmbedModule still exports them
    EmbedLiveKitServiceEmbedCallService — 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 your embedEvent.addCallback listeners receive
    Types 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. (EmbedButtonComponent exists 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. Calling initialize() 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:

  1. HTTPS everywhere outside localhost. On plain HTTP the call fails silently: no prompt and no console error; the button just returns to idle.
  2. Inside an iframe: the frame needs allow="microphone", and no Permissions-Policy header may block it (microphone=(self) at minimum).
  3. CSP — if the app sends one, add (merge, never replace):
DirectiveAllowWhy
connect-srchttps://embed.revrag.ai (or your baseUrl host)Initialization, events, call token, screen map
connect-srcThe 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-srcYour avatar animation's host, if the dashboard sets oneThe SDK fetches the animation file; if it cannot, it shows the bundled orb
img-srcdata:, plus your avatar image's host if the dashboard sets an image avatarThe call icons and the default avatar are bundled as data: images
style-srcNothing new if your app's Angular styles already pass your CSP (nonce or 'unsafe-inline'). 'unsafe-inline' only if widget.css is not loadedThe 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 expressionsOtherwise not needed
  • An avatar animation that cannot be fetched shows the SDK's bundled orb. There is no corsproxy.io fallback 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. On localhost that 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 as proxy.conf.json (merge into an existing proxy config) and reference it from the serve target's proxyConfig:

    {
      "/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-web and 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 is 1.0.7; install 2.0.0.
  • One @angular/core copy (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)
});
  • EmbedInitService is providedIn: 'root' — inject it; never new it or add it to a providers array. It exposes isInitialized$, isLoading$, error$ (string | null), sessionData$ and a synchronous isInitialized. Do not use its reset() / clearSessionData() in an integration (sign-out is EmbedLogout(), §11).
  • Without appVersion, initialize() rejects (and error$ emits). Every other failure (empty or bad key, wrong baseUrl, offline, the 5 s timeout) resolves the promise and surfaces only in error$ — 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.
  • trackerCallback is accepted but never called — use embedEvent.addCallback (§18).
  • 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 — 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_id is 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 in localStorage).
  • Sent before init ⇒ resolves with success: false and SDK 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 for isInitialized$.
  • 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. 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.

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.

CaseExpectedObserve
A. config existsbutton 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 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; app unaffectederror$ set; a later initialize() retries
E. animations missingbutton shows; NG05105 when it opensadd 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_trigger with a nudgeDelay above 0 on the dashboard makes the widget expand and start a call by itself nudgeDelay seconds 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, a nudgeDelay above 0 and a tooltipMessage; an automatic call needs auto_trigger and a nudgeDelay above 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:

  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 — hides the button, matches the path and everything below it, and ends a live call on entry.
  3. 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 continuous group with no delay.
  4. Entrance delays? (embedButtonDelayMs, group delayMs, delayPolicy) — each delayed appearance ends a live call.
  5. Click and route analytics (enableTracker, default on) — acceptable for this app's privacy posture? Control names and routes reach RevRag.
  6. Which fields/regions are sensitive? ⇒ data-revrag-mask, [actionRoot] or excludeScreens (§17).
  7. App version source, API key location, and which baseUrl per environment.
  8. 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).
  9. Is the dashboard's auto_trigger behaviour wanted (§12)?
  10. 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):

InputTypeDefaultEffect
[excludeScreens]string[]noneDeny-list: button hidden, capture off, live call ends. Matches the path and everything below it
[showOnAllScreens]booleantruefalse = 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]number0Entrance delay
[embedButtonVisibilityConfig]EmbedButtonVisibilityConfignoneGroups with continuity and per-group delays
[actionRoot]ElementRef or HTMLElementdocument.bodyNarrow what the agent can see (§17)
[currentPath]stringnoneOverrides the router (§5C)
[getScreenName]() => string | null | undefinednoneNames screens the URL can't (below)
  • Path source, in order: [currentPath] → the Angular Router (URL after redirects, ?query and #fragment stripped, on every NavigationEnd) → window.location.pathname (no router; read on start only). With the router or currentPath, 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 with matchMode="startsWith" + includeScreens, or excludeScreens (always prefix).
  • 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. Use an arrow-function property so this is kept:
    resolveScreenName = (): string | null => (this.router.url.startsWith('/apply') ? `/apply/step-${this.step}` : null);
    <revrag-embed-provider [getScreenName]="resolveScreenName">
  • Visibility algorithm: excludeScreens wins over everything — unless [showOnAllScreens]="false", which ignores excludeScreens entirely and makes includeScreens (plus group screens) the agent's allow-list too. Empty includeScreens (and no groups) ⇒ everywhere not excluded. Pick one mode — do not mix them.
  • ⚠ 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.
  • Treat these inputs as fixed (class fields, as in 5A). The button applies a changed list, matchMode, delay or embedButtonVisibilityConfig only on the next navigation; the agent's scope updates at once, but only when includeScreens, excludeScreens or showOnAllScreens change. Template array literals are fine (Angular memoizes them).
  • A path in a list that the app never reports silently hides the button — log router.url and 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 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: "<path>" is in excludeScreens — stopping capture and ENDING THE CALL
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, even in development:

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

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:

  1. initialize() → 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 — on each navigation that shows the button, once a user id is stored (a first visit can miss it on the very first page)
  5. (call) microphone_permission_allowed → [RevRag] runtime agent runtime started → [RevRag] call connected — agent runtime armed → [RevRag ▸ ui_snapshot] and agent_conversation_started (when the agent joins; either order)
  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 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:

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 (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-limitsits 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? }).

    KeyWireUse
    EventKeys.USER_DATAuser_dataidentity — must carry app_user_id
    EventKeys.ANALYTICS_DATAanalytics_datanamed product events — requires a top-level 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_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_DATA needs a stored user id. embedEvent exposes only event, customEvent, addCallback, removeCallback and getStoredAppUserId.

  • Agent events (SDK → you): embedOnAgent(cb) returns a handle; release with embedOffAgent(handle). Compare against AgentEvent.* 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 add call_id, reason, callDuration. GEN_TOOL_TRIGGERED and the deprecated AGENT_CONNECTED/AGENT_DISCONNECTED are never emitted. Update component state from a handler inside NgZone.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 own analytics_data event, as it happens.

    • event_name is <name>_pressed; _opened / _closed for a dropdown trigger; _selected for an option, select or radio; _toggled for a checkbox. The name is the first of data-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 existing ids just to rename an event.
    • Existing names are stable. A click earlier versions already reported under an id or name (a button, role="button", [onclick] or data-clickable element with an id or name) keeps its exact <id>_pressed name, with or without data-revrag-name. Its old name is also in legacy_event_name.
    • data.source is user, agent or script.
    • 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 needs data-clickable on 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's clickTracking setting.

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). EmbedCallService controls the mounted widget's call; it never opens a second one.
  • A denied mic raises microphone_permission_denied (with error_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 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

  1. Run the app (ng serve or 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.
  2. Run verifyIntegration(). The browser console cannot import a bare package name, so await 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.

IdCheckStepIf it fails
V1SDK initializedS3-api-key§8–§10
V2API key accepted (reads sessionStorage)S3-api-key§9; storage blocked?
V3Provider mounted exactly onceS2-provider-mount§5
V4Widget rendered, or hidden with a reasonS8-scope-config§14
V5User identity set (an anonymous id also passes)S5-user-identification§11
V6Route changes detected (needs 2 routes; reads the screen cache)S4-route-wiring§14 router / currentPath; dashboard localCache off; just logged out?
V7Events accepted (sends a real custom_event revrag_verify_integration)S3-api-key / S9-csp-allowlist§8 / §6 CSP
V8Capture healthy; counts unlabeled controlsS8-scope-config / S2-provider-mountnothing captured ⇒ §17 actionRoot; a capture error or SDK leak ⇒ report it
V9Masking honoredS7-field-masking§17 masking (a leak is SDK-side — report it)
V10RevRag backend reachable (not the call server or avatar host)S9-csp-allowlist§6 CSP
  • skip is not a failure (V7 without a user id, V9 without masked fields, V7/V10 with offline).
  • For each fail, apply its remediation, go to the section for its step, re-run from step 1. If the same check fails 3 times, stop and report its id and detail — 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 against excludeScreens (plus includeScreens when [showOnAllScreens]="false"). On a path the button skips because of includeScreens, 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 ok is true, print the summary field verbatim. It reads:
✅ RevRag Embed SDK successfully integrated (N/N checks passed)

20. Error / failure conditions to test explicitly

AreaCases
Initsucceeds · bad key · key/baseUrl env mismatch · offline · slow (> 5 s) · called twice · retried after failure — app never crashes
API keypresent · missing · empty
appVersionpresent · missing (initialize() rejects) · version with no dashboard config
Animationspresent · missing (NG05105 when the widget opens)
USER_DATAbefore init · after init · missing with enableTracker: false · after EmbedLogout() · re-login as another user
Widget configexists · init failed · sessionStorage blocked
Pathsallowed · excluded · outside allow-list · trailing slash / case mismatch · route parameter · query/fragment change · back/forward · reload
Visibilitycontinuous group · per-screen group · delay
UI treecaptured · out of scope · masked fields · dialog open (agent sees dialog only) · mat-select / autocomplete panel
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 · SSR render

21. Error visibility — grep the console and build output for these

MessageMeaning
[RevRag] app_version is requiredappVersion 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 endpointinit handshake failed — network, baseUrl, CSP (§6, §8)
NG05105: Unexpected synthetic propertyno animations provider (§4)
an unstyled widgetwidget.css missing from styles (§4)
no button on an allowed pathinit 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 requiredno 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 allowedOFFER_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 failedbackend, 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 deniedmicrophone blocked
ERESOLVE during installunpinned @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 componentSDK-side — report
[RevRag AI] … payload is … bytes — close to or over LiveKitoversize 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-autocomplete inside a MatDialog → 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.css is not fully scoped → it sets outline: none !important; border: none !important on every button:focus / button:focus-visible, and --button-* / --container-width variables on :root. Check the app's focus rings and any variables with those names; report it as SDK-side.

Components / design system

  • mat-icon-button and other icon-only buttons without aria-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; use role="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: pointer or tabindex.
  • *ngFor rows 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-mask per 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 range only.
  • 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 to AppComponent.
  • 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 (log router.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_trigger may be blocked.
  • Strict privacy / blocked storage → no button (sessionStorage required); 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 initial budgets → 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.7 still 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; gate window.__revragDebug and 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_DATA sent from a getter or template binding → sent on every change detection; move it to the login handler.
  • 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).
  • 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

  1. Angular 15 or older, or RxJS older than 7.4 ⇒ stop and report. Never --force / --legacy-peer-deps past ERESOLVE.
  2. Install @angular/animations and @angular/router pinned to the app's Angular version before the SDK, plus livekit-client and lottie-web.
  3. Import only from @revrag-ai/embed-angular and @revrag-ai/embed-angular/diagnostics. Never install or import @revrag-ai/embed-core; never use deprecated exports.
  4. provideAnimations() / BrowserAnimationsModule, and widget.css in the build styles — both required.
  5. 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.
  6. initialize() once, browser only, with the key and appVersion from the environment files. Log both the rejected promise and error$.
  7. appVersion is the app's real version and has a dashboard config.
  8. USER_DATA with a stable string app_user_id after login, after init.
  9. EmbedLogout() first in the app's sign-out.
  10. Visibility inputs are class fields; any hide of the button ends a live call.
  11. Mark every sensitive field; masking is never automatic beyond password inputs.
  12. Zoneless app ⇒ tell the developer before continuing.
  13. Run verifyIntegration() from a dev-only trigger (never the browser console), and never declare success without ok: true (or every skip explained).

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 errors

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