Revrag Embed — Angular Integration Guide
The complete guide for adding the Revrag AI voice agent to an Angular app.
| What you get | One floating button that follows your user across every route, and a voice agent that can see your page and act on it |
| Package | @revrag-ai/embed-angular 2.0.0 |
| Angular | 16 or newer — standalone components or NgModule |
| Router | @angular/router, read automatically; apps without a router work too |
| SSR | Angular Universal / @angular/ssr — the widget renders nothing on the server |
| TypeScript | Types included |
| API key | Get it from https://app.revrag.ai |
This guide documents 2.0.0. Coming from 1.0.7? One code change is required — pass appVersion to initialize() — and Angular 15 is no longer supported. Read the upgrade steps first.
1.0.7 predates most of this guide. It has:
- no
appVersionoption - no Action Intelligence — the agent cannot see or act on your page
- no call controls (
EmbedCallService), no agent events and noEmbedLogout() - no
/diagnosticsentry
Staying on 1.0.7 (for example on Angular 15)? Follow the 1.0.7 guide instead.
Every Angular app integrates in the same three steps — standalone or NgModule, with or without the router:
| Step | What you do | Details |
|---|---|---|
| 1. Initialize | Call EmbedInitService.initialize() once, in your root component | §2 |
| 2. Mount | Wrap your app in one <revrag-embed-provider> | §3 |
| 3. Identify | Send your user (USER_DATA) after login | Identify your user |
Section 5 shows the exact setup for your app. Everything before it applies to every app.
Table of contents
- Install — package, stylesheet, animations, peers and CSP
- Initialize — one service call, once, at the root
- Mount — one provider, and the SDK follows the user
- Decide where it appears — paths, groups, delays, and what the agent can see
- Your app's shape — pick one, complete code
- Control it from your own code — calls and the widget card
- Events and analytics — what you send, what you can listen to
- How paths become screen names
- Verify it works
- Troubleshooting
- Known limitations
- Checklist
1. Install
Install the package and its peers, add the stylesheet, enable Angular animations, and check the requirements.
Package
If your app does not already have @angular/animations and @angular/router, install them first, at the same version as your @angular/core:
npm ls @angular/core # e.g. 19.2.19
npm install @angular/animations@19.2.19 @angular/router@19.2.19 # same version as @angular/coreThen install the SDK and its two runtime peers:
npm install @revrag-ai/embed-angular livekit-client lottie-web
# or
yarn add @revrag-ai/embed-angular livekit-client lottie-web
pnpm add @revrag-ai/embed-angular livekit-client lottie-web- Why these first:
@angular/animationsand@angular/routerare peer dependencies. If one is missing, npm 7+ installs it for you at the newest version>=16.0.0allows. That can be a newer major than the rest of your Angular packages, and the install fails withERESOLVE. Pinning both to your@angular/coreversion first avoids it. - You need
@angular/routerinstalled even if your app has no router. The SDK imports it. - The core engine is bundled inside the package. Never install or import
@revrag-ai/embed-core.
Stylesheet — required
Add it to the styles array of your build target in angular.json (or project.json in Nx):
"styles": [
"src/styles.css",
"node_modules/@revrag-ai/embed-angular/styles/widget.css"
]- It goes in
projects.<your-app>.architect.build.options.styles. - Restart
ng serveafter changingangular.json. A running dev server does not pick up a newstylesentry. - Without it, the button looks broken. It gets only a minimal fallback style (position and layout).
Animations — required
The widget uses Angular animations. Without the animations provider, the collapsed button still shows, but Angular throws NG05105: Unexpected synthetic property as soon as the widget opens (or shows its tooltip or agent panel). Only development builds throw; a production build shows no error, so a missing provider can go unnoticed there.
// src/app/app.config.ts — standalone apps
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideAnimations()],
};NgModule apps import BrowserAnimationsModule in AppModule instead — see 5B.
Peer dependencies
| Package | Range | Notes |
|---|---|---|
@angular/core, @angular/common, @angular/animations, @angular/router | >=16.0.0 | The SDK uses signals, which arrived in Angular 16 |
rxjs | ^7.4.0 | Every Angular 16+ app already has it |
livekit-client | ^2.0.0 | Voice calls |
lottie-web | ^5.10.0 | The avatar animation |
Your app also needs @angular/platform-browser (for the animations provider). Every Angular CLI app already has it.
Requirements
| Angular | 16 or newer. Angular 15 and older do not build |
| Browser | Any current browser with WebRTC and getUserMedia |
| Page | A secure context — HTTPS, or localhost in development. Browsers refuse the microphone anywhere else |
| Storage | sessionStorage must be available. The widget reads its configuration from it |
| Rendering | Safe with SSR: the widget renders nothing on the server and appears in the browser (5D) |
| TypeScript | Works with "skipLibCheck": false since 2.0.0 |
| API key | From Revrag, per environment |
Your API key and environment are a pair.
- The SDK talks to
https://embed.revrag.aiunless you passbaseUrl. - A development key used against production (or the reverse) fails initialization.
EmbedInitService.error$emits the reason, and the button never appears.
Content-Security-Policy
If your app sends a CSP, merge these in (never replace your policy):
| 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 |
For a default setup:
connect-src 'self' https://embed.revrag.ai wss://*.livekit.cloud https://*.livekit.cloud;
img-src 'self' data:;- Nothing else is fetched from third parties. Since
2.0.0the call icons are bundled, and there is nocorsproxy.iofallback. - An avatar animation that cannot be fetched (blocked by CSP or CORS, or missing) shows the bundled default orb instead.
- Avatars hosted on Revrag's
revrag-dev.s3.ap-south-1.amazonaws.combucket are also tried at/s3-lottie/<path>on your own origin. Onlocalhostthat path is tried first, then the S3 URL directly; elsewhere the S3 URL is tried first, then that path. The bundled orb shows only if both fail. Add a proxy for/s3-lottieonly if your custom avatar does not load.
What it pulls in
livekit-client(WebRTC) — loaded only when a call starts.lottie-web— loaded when the avatar animation first renders.- The default avatar animation (about 400 KB) — loaded on demand since
2.0.0, so it no longer counts toward your initial bundle. - The Angular CLI warns
Module 'lottie-web' … is not ESM. It is harmless; silence it with"allowedCommonJsDependencies": ["lottie-web"]in your build options.
2. Initialize
Initialize once at the root, identify your user after login, and call EmbedLogout() when they sign out.
- Call
initialize()once, in your rootAppComponent— the component that stays for the whole session. - Never call it in a route component or a lazy-loaded module.
Keep the key and version with your other environment settings:
// src/environments/environment.ts (environment.prod.ts: the same fields, with production: true)
export const environment = {
production: false,
revragApiKey: 'YOUR_API_KEY',
appVersion: '4.2.0', // the version your dashboard config is provisioned for
};// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet></router-outlet>`, // section 3 adds the provider here
})
export class AppComponent implements OnInit {
constructor(private embedInit: EmbedInitService) {}
ngOnInit(): void {
// Every failure lands here, including a missing appVersion.
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(() => {}); // already logged by error$ above
}
}- No
src/environmentsfolder? Runng generate environments, or keep the values wherever your app keeps its config.
appVersion is required.
- Without it,
initialize()rejects with[RevRag] app_version is required…,error$emits the same message, and the widget never appears. - TypeScript will not catch a missing
appVersion— the option is typed as optional. Check it yourself. - This option is its only source. The provider has no
[appVersion]input, and no<meta>tag or build variable is read. - The value must match a version your dashboard knows. Revrag provisions the widget's look and behaviour per platform and per app version.
- Source it from your build —
package.json, a build id, orenvironment.ts.
Options
| Option | Default | Meaning |
|---|---|---|
apiKey (first argument) | — | Your tenant's key, for this environment |
appVersion | — | Required. Selects the dashboard config for this version |
baseUrl | https://embed.revrag.ai | The Revrag backend. Omit it in production; pass the matching URL for any other environment |
enableTracker | true | Click and route tracking, and an anonymous user id until you send a real one (see below) |
trackerCallback | — | Accepted for compatibility; the SDK never calls it. Use embedEvent.addCallback (§7) |
What it returns
initialize() returns a Promise<void>:
- It resolves when the handshake with the backend has finished — whether it succeeded or failed.
- It rejects only when
appVersionis missing. Every failure, that one included, arrives througherror$.
EmbedInitService is providedIn: 'root'; inject it anywhere. It exposes the state as observables:
| Member | Type | Meaning |
|---|---|---|
isInitialized$ | Observable<boolean> | The handshake with the backend succeeded |
isLoading$ | Observable<boolean> | The handshake is in flight |
error$ | Observable<string | null> | Why initialization failed — the place every failure surfaces |
sessionData$ | Observable<SessionStorageData> | The cached session data |
isInitialized | boolean | Synchronous snapshot of isInitialized$ |
Things worth knowing:
- You do not need to wait for it. The provider shows the button by itself once initialization has stored its result.
- The handshake gives up after 5 seconds, so on a very slow network it can fail.
- After a failure, calling
initialize()again retries. - After a success, calling it again with the same key makes no new request.
- Calling it with a different key resets the SDK and initializes again.
- After a failure, calling
- Every version bump is a dashboard change.
- A deploy that bumps
appVersionto a version the dashboard has no config for makes the widget disappear. - Put "dashboard config exists for this version" on your release checklist.
- A deploy that bumps
Identify your user
After login, and after initialization has succeeded, tell the SDK who the user is. A small service keeps the timing right:
// src/app/revrag-identity.service.ts
import { Injectable } from '@angular/core';
import { filter, take } from 'rxjs';
import { EmbedInitService, embedEvent, EventKeys } from '@revrag-ai/embed-angular';
@Injectable({ providedIn: 'root' })
export class RevragIdentityService {
constructor(private embedInit: EmbedInitService) {}
/** Call once after login, with your own stable user id. */
identify(user: { id: string; 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: user.id, name: user.name, email: user.email },
});
});
}
}app_user_idis the important one — your stable id for this user, across sessions and devices. Everything the agent does is attributed to it.- Before login, calls still work. With
enableTrackeron (the default), the SDK registers an anonymous id (user_<timestamp>_<random>) during initialization. YourUSER_DATAreplaces it. - With
enableTracker: false, there is no anonymous id: a call started beforeUSER_DATAfails withapp_user_id is required. - Send it after initialization, once per login. Sent earlier, the request fails with
SDK not initialized. The id is still stored, but the backend never hears about this user until the next event. - The id is kept in the browser's
localStorage, so it survives reloads until anotherUSER_DATAorEmbedLogout()replaces it.
On logout
When your user signs out, call EmbedLogout():
// src/app/auth.service.ts
import { Injectable } from '@angular/core';
import { EmbedLogout } from '@revrag-ai/embed-angular';
@Injectable({ providedIn: 'root' })
export class AuthService {
async signOut(): Promise<void> {
await EmbedLogout(); // never rejects
// …your own sign-out
}
}It does four things, in order:
- Ends the call, including one that is still connecting.
- Stops the agent, cancelling anything it was doing on the page.
- Wipes this user's cached screens (IndexedDB
revrag_embed_cache). Cached screens are tagged with the user id when they upload, so anything left behind would upload under the next user's id. - Forgets the user id, from memory and from
localStorage, and drops click events still waiting to be sent for it.
Worth knowing:
- Your API key, the widget's config and the session's initialization data stay, so you do not initialize again.
- Send
USER_DATAfor the next user as soon as they log in. EmbedLogout()never rejects and is safe to call twice, or when no call is running.- Until the next
USER_DATA, calls cannot start (app_user_id is required). The anonymous id is only made on a page load; a full reload to your login page restores it. - Same function as React and React Native, which also export
EmbedLogout().
EmbedInitService.reset() and clearSessionData() are not a logout.
reset()disconnects the SDK from the backend — events then fail withSDK not initializeduntil you callinitialize()again.clearSessionData()deletes the stored widget configuration, which hides the button until the page reloads.- Neither one forgets the user. Use
EmbedLogout().
3. Mount
<revrag-embed-provider> is the only component you place. Put it once, around your app's content.
// src/app/app.component.ts — standalone
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmbedProviderComponent],
template: `
<revrag-embed-provider>
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent {
// initialize() from section 2
}- It follows the router by itself. The provider injects Angular's
Routerand re-evaluates on everyNavigationEnd— you do not need to pass the path. - The provider renders the button itself. Do not also place
<revrag-embed-button>— that mounts a second, visible button with its own call. - Mount it once. Two providers mean two buttons, and the self-check reports it (V3).
- Keep it in the root component. If the provider is destroyed, its button goes with it, and a live call ends.
- Wrapping is optional. The provider just shows its content. If your CSS depends on your root's direct children, place
<revrag-embed-provider></revrag-embed-provider>next to<router-outlet>instead. It works the same. - NgModule apps import
EmbedModuleinstead of the component; the template is the same (5B).
Full input list
Every input, with its default. Most apps set none, or only the visibility ones.
Where the user is (§8)
| Input | Default | Meaning |
|---|---|---|
currentPath | — | string. Overrides the router's path. Set it only without a router, or to report synthetic paths. Pathname only |
getScreenName | — | A function returning a name, or null. Names a screen the URL can't. Does not affect where the button appears |
Where the button appears (§4)
| Input | Default | Meaning |
|---|---|---|
includeScreens | [] | string[]. Paths where the button may appear. Empty (and no groups) means everywhere |
excludeScreens | — | string[]. Paths where the button never appears and the agent never looks. Landing on one ends a live call |
showOnAllScreens | true | boolean. false: includeScreens also limits what the agent sees, and excludeScreens is ignored |
matchMode | 'exact' | 'exact' or 'startsWith'. How includeScreens match |
embedButtonDelayMs | 0 | number. Entrance delay in ms on every path change |
embedButtonVisibilityConfig | — | EmbedButtonVisibilityConfig. Journeys: groups, continuity, per-group delays |
What the agent sees (§4)
| Input | Default | Meaning |
|---|---|---|
actionRoot | document.body | An ElementRef or an element. Narrows the part of the page the agent can see |
Placement
| Input | Default | Meaning |
|---|---|---|
widgetPositioning | 'fixed' | 'fixed' floats above the page. 'embedded' renders the widget in the normal page flow, after the provider's content, at full width |
embedButtonPosition | — (24px from the bottom and right) | { bottom?, right? }, in 'fixed' mode. A number means px; a string is used as-is. A side you leave out is 24px |
widgetSide | — | 'left' or 'right'. Which side to align to, in 'embedded' mode only |
widgetBottomOffset | 0 | number. Extra px added to the bottom offset, e.g. above a tab bar |
widgetClassName | '' | string. Has no effect today |
- A dashboard position replaces the placement inputs. If your Revrag dashboard config sets a position (
widgetPositionand paddings), it is used instead ofembedButtonPosition,widgetBottomOffsetandwidgetSide. - The user can drag the button to the other edge. The provider has no input to turn that off.
- Changes to the visibility inputs (
includeScreens,excludeScreens,matchMode, delays, groups) reach the button on the next navigation.currentPathand the placement inputs apply at once. - What the agent may see is updated at once, but only when
includeScreens,excludeScreensorshowOnAllScreenschange. A changedembedButtonVisibilityConfigreaches the agent only with the next change to one of those three. - More on placement: which position wins and embedded position.
The provider does more than render the button.
- It starts Action Intelligence — the part of the SDK that lets the agent see and act on your page — and reports every route to it.
- It keeps the agent's highlight overlay mounted, even on paths where the button is hidden.
- The button appears only after
initialize()has succeeded in this browser tab. After a reload it appears straight away, from the config stored insessionStorage, before the new handshake finishes.
What your dashboard controls
The widget's look and behaviour come from your Revrag dashboard config (widget_config), per app version. Since 2.0.0, fields the config leaves out use these defaults:
| Behaviour | Default when the config leaves it out | Dashboard field |
|---|---|---|
| Nudge (tooltip or automatic call) | None | collapsedView.inactivityBehavior (show_popup for the tooltip, auto_trigger for an automatic call) and nudgeDelay (seconds, above 0); the tooltip also needs tooltipMessage |
| After a call connects | The open card folds back to the button after 15 s; the call keeps running. Not while captions are on | collapsedView.collapsedTime (seconds) |
| Agent output panel | Closes 15 s after its text has finished | collapsedView.viewCloseDelay (ms) |
| Agent name / type | "Revrag AI" / "Onboarding Agent" | agentTextContent |
| Call button text | "Start Call" | agentTextContent.buttonText |
The full list of changes is in the release notes.
When to use EmbedButtonComponent
Almost never. <revrag-embed-button> is the bare button, without the provider. On its own it has:
- no route visibility (
includeScreens,excludeScreens, groups, delays) - no highlight overlay, so the agent's highlights are invisible
- no Action Intelligence mount, so the self-check reports no provider (V3)
It exists for the advanced case where you drive Action Intelligence yourself with ActionIntelligenceService.
4. Decide where it appears
Choose which paths show the button, which journeys keep a call alive, any entrance delays, and what the agent may see.
- You do not register screens; you decide about paths.
- A path is the router's URL without
?queryand#fragment(or whatever you pass ascurrentPath), matched exactly and case-sensitively.
// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedProviderComponent, type EmbedButtonVisibilityConfig } from '@revrag-ai/embed-angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmbedProviderComponent],
template: `
<revrag-embed-provider
[excludeScreens]="excluded"
[includeScreens]="included"
[embedButtonVisibilityConfig]="visibility"
>
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent {
// Never show the button here, never let the agent see these paths, and END a live call
// when the user lands on one. Matches the path and anything below it.
excluded = ['/login', '/otp', '/settings'];
// Optional allow-list for the button. Empty means "everywhere" — but only while
// you define no groups: group screens join this list (see the warning below).
included: string[] = [];
visibility: EmbedButtonVisibilityConfig = {
defaultDelayMs: 0,
groups: [
// A journey: the button stays put — no re-delay, and a live call keeps running.
{ id: 'loan', screens: ['/loans', '/loans/apply', '/loans/review'], continuity: 'continuous' },
// A per-screen group: re-evaluated on every path change.
{ id: 'help', screens: ['/help'], continuity: 'perScreen', delayMs: 3000, delayPolicy: 'oncePerAppSession' },
],
};
}What this buys you, with no further code
| User does | Widget does |
|---|---|
Moves between paths in one continuous group | Stays visible — no blink, no re-delay; a live call keeps running |
| Moves between allowed paths with no delay | Stays visible; a live call keeps running |
| Enters a path that has an entrance delay | Hides for the delay, then appears — a live call ends |
Opens a path outside includeScreens | Hides — a live call ends |
Opens a path in excludeScreens | Hides — a live call ends, and the agent stops seeing the page |
| Reloads the page | Starts fresh: the call ends; the SDK initializes again |
Where the button disappears, the call ends.
- On the web the call lives inside the button, so any hide ends it — an excluded path, a path outside
includeScreens, or an entrance delay. - To keep a call alive across a journey, put those paths in one
continuousgroup and give them no delay.
Defining a group turns on the allow-list.
- Every group's
screensare added toincludeScreens. So once you define any group, the button appears only on the paths in your groups and inincludeScreens. - Everywhere else it is hidden, ending a live call. The example above shows the button only on the three loan paths and
/help. - To keep it on other paths too, add them to
includeScreens(withmatchMode="startsWith", a section prefix like'/accounts'covers everything below it).
All options
Button visibility
| Input | Default | Meaning |
|---|---|---|
includeScreens | [] | Paths where the button may appear. Empty means everywhere. Group screens count as included too |
matchMode | 'exact' | How includeScreens match: 'exact', or 'startsWith' (the path, or anything below it) |
embedButtonDelayMs | 0 | Entrance delay in ms on every path change, unless a group or defaultDelayMs says otherwise |
embedButtonVisibilityConfig | — | Groups (journeys) and a default delay |
What the agent may see
| Input | Default | Meaning |
|---|---|---|
excludeScreens | — | Deny-list. The button hides, capture stops, no screen content is captured or sent, and an active call ends (click and screen-view analytics still report there). Matches the path or anything below it. Wins over includes and groups — unless showOnAllScreens is false, which ignores it |
showOnAllScreens | true | true: the agent may see every path except excludeScreens. false: includeScreens (plus group screens) is also the agent's allow-list — always by prefix, whatever matchMode says — and excludeScreens is ignored |
actionRoot | document.body | Narrows the agent to one region (below) |
Pick one mode for the agent — do not mix them. A deny-list (excludeScreens), or an allow-list ([showOnAllScreens]="false" + includeScreens).
EmbedButtonVisibilityConfig
| Field | Default | Meaning |
|---|---|---|
defaultDelayMs | — | Delay for paths that no group sets one for. Wins over embedButtonDelayMs |
groups | [] | Journeys |
EmbedButtonGroupConfig
| Field | Default | Meaning |
|---|---|---|
id | — | Any stable string |
screens | — | The paths in this journey, matched exactly |
continuity | — (required) | 'continuous' keeps the button and a live call across the group; 'perScreen' re-evaluates on every path |
delayMs | — | Entrance delay for this group. Wins over defaultDelayMs and embedButtonDelayMs |
delayPolicy | 'perScreen' | 'perScreen' (every path change), 'oncePerGroupEntry' (only when entering the group), 'oncePerAppSession' (until the button has appeared once in this group) |
'oncePerAppSession'— it counts only once the delay has finished: leave before it ends, and the delay runs again next time. The memory lasts until the page reloads or the provider is recreated.- Delay precedence: group
delayMs→defaultDelayMs→embedButtonDelayMs.
A path in includeScreens that your app never reports does not error — it silently hides the button there.
- Log the router's URL (§8) and write the lists against it.
What the agent can see
By default the agent can see document.body, except the SDK's own widget.
Narrow it to one region with actionRoot:
<revrag-embed-provider [actionRoot]="appArea">
<main #appArea><router-outlet></router-outlet></main>
</revrag-embed-provider>- Dialogs and menus stay visible. Elements attached to
document.bodyoutsideactionRootare still seen when they are a<dialog>, havearia-modal="true", or haverole="dialog","alertdialog"or"menu"— Angular CDK and Material dialogs, for example. - Other overlays do not. Panels with other roles, such as the
role="listbox"panels ofmat-selectandmat-autocomplete, are not seen when they sit outside a narrowedactionRoot. Keep the default root if the agent must use them. - While a modal
<dialog>is open, the agent sees and acts on that dialog alone.
Mask sensitive fields with data-revrag-mask. The agent still sees the field — its label, its position, that it can be typed into — but its text and value are always blank. It can fill the field for the user, and can never read it back.
<input data-revrag-mask aria-label="Card number" />
<!-- Inherited: masking a container masks everything inside it -->
<section data-revrag-mask>…</section>
<!-- Conditional -->
<div [attr.data-revrag-mask]="isSensitive ? '' : null">…</div><input type="password">is never read. No attribute needed.- There is no automatic detection. The SDK does not guess what looks like a card or account number. Mark everything sensitive yourself.
- To keep a whole area away from the agent, leave it outside
actionRoot, or keep it on a path inexcludeScreens. MASK_ATTRis exported as a constant ('data-revrag-mask') if you set the attribute from code.- Give icon-only buttons and unlabeled inputs an
aria-label. The agent finds controls by name.
5. Your app's shape
Find your app below. Each is complete and copy-pasteable.
| Your app | Go to |
|---|---|
Standalone components (bootstrapApplication) — the Angular 17+ default | 5A |
NgModule app (bootstrapModule) | 5B |
| Anything using the Angular Router — hash routing, lazy routes, route parameters | 5C |
Server-side rendering (@angular/ssr, Angular Universal) | 5D |
| No router — tabs, steps or wizards in state | 5E |
| Several apps on one page, micro-frontends, iframes, or full page loads | 5F |
5A — Standalone components
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideAnimations()],
};// src/app/app.component.ts — the entire integration
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService, EmbedProviderComponent } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmbedProviderComponent],
template: `
<revrag-embed-provider [excludeScreens]="excluded">
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent implements OnInit {
excluded = ['/login', '/otp'];
constructor(private embedInit: EmbedInitService) {}
ngOnInit(): void {
this.embedInit.error$.subscribe((error) => {
if (error) console.error('[Revrag] init failed:', error);
});
this.embedInit
.initialize(environment.revragApiKey, { appVersion: environment.appVersion })
.catch(() => {}); // already reported through error$
}
}Plus the stylesheet in angular.json (§1) and the environment file (§2).
- Your pages stay exactly what they are — no Revrag code in them.
5B — NgModule app
// src/app/app.module.ts
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 {}// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { EmbedInitService } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: false, // declared in AppModule
template: `
<revrag-embed-provider [excludeScreens]="excluded">
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent implements OnInit {
excluded = ['/login', '/otp'];
constructor(private embedInit: EmbedInitService) {}
ngOnInit(): void {
this.embedInit.error$.subscribe((error) => {
if (error) console.error('[Revrag] init failed:', error);
});
this.embedInit
.initialize(environment.revragApiKey, { appVersion: environment.appVersion })
.catch(() => {}); // already reported through error$
}
}- Import
EmbedModuleonce, in the module that declares the component holding the provider. - Keep
standalone: falseonAppComponent. Angular 19 and newer make components standalone by default, and a standalone component cannot be declared in an NgModule. It is valid on 16–18 too. - You can skip
EmbedModule.EmbedProviderComponentis standalone, so you can list it in your module'simportsdirectly. EmbedModulealso exports the widget's inner components (revrag-embed-widgetand others) for apps written against1.0.7. They are deprecated, not removed; use the provider.
5C — Angular Router
Nothing to wire up. The provider listens for the router's NavigationEnd and reports each path.
- The path is the router URL after redirects, without
?queryand#fragment. - Hash routing works too (
withHashLocation(), oruseHash: trueinRouterModule.forRoot): the router URL is the part after#, e.g./loans. - Lazy-loaded routes need nothing extra.
- Put the provider in the root component, not in a routed component — a routed component is destroyed on navigation, taking the button and the call with it.
Route parameters: the router URL is the real path.
- It is
/orders/8812, not/orders/:id. - Allow a whole subtree with
matchMode="startsWith"and[includeScreens]="['/orders']". - Or exclude it with
[excludeScreens]="['/orders']"(exclusion always covers everything below).
5D — Server-side rendering
The widget is browser-only. On the server, the provider renders no button, and route reporting and Action Intelligence do nothing.
Initialize only in the browser. ngOnInit also runs on the server, so guard the call:
// src/app/app.component.ts
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { EmbedInitService, EmbedProviderComponent } from '@revrag-ai/embed-angular';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmbedProviderComponent],
template: `
<revrag-embed-provider [excludeScreens]="excluded">
<router-outlet></router-outlet>
</revrag-embed-provider>
`,
})
export class AppComponent implements OnInit {
excluded = ['/login', '/otp'];
constructor(
private embedInit: EmbedInitService,
@Inject(PLATFORM_ID) private platformId: object,
) {}
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId)) return; // ngOnInit also runs on the server
this.embedInit.error$.subscribe((error) => {
if (error) console.error('[Revrag] init failed:', error);
});
this.embedInit
.initialize(environment.revragApiKey, { appVersion: environment.appVersion })
.catch(() => {}); // already reported through error$
}
}- Without the guard, the server would call Revrag's backend on every render and register anonymous users that no browser ever uses.
- Guard
windowinside anygetScreenNameyou pass:typeof window !== 'undefined'. - Using
provideClientHydration()? Test the widget on a hydrated page before you ship.
5E — No router
If your "screens" are state — tabs, wizard steps, a modal-driven flow — give each one a synthetic path with [currentPath]:
// src/app/onboarding-shell.component.ts
import { Component } from '@angular/core';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';
@Component({
selector: 'app-onboarding-shell',
standalone: true,
imports: [EmbedProviderComponent],
template: `
<revrag-embed-provider [currentPath]="'/onboarding/' + step" [excludeScreens]="excluded">
<!-- your step UI; update step as the user moves -->
<button type="button" (click)="step = 'address'">Next</button>
</revrag-embed-provider>
`,
})
export class OnboardingShellComponent {
step = 'details';
excluded = ['/onboarding/pin'];
}[currentPath]wins over the router whenever it is set.- Use stable, privacy-safe values: never a user id, an account number or a session token.
- Without a router and without
[currentPath], the provider readswindow.location.pathnameonce, when it starts, and never again.
5F — Several apps, iframes, and multi-page apps
- Several Angular apps or micro-frontends on one page: exactly one provider for the whole page. Render it in the shell that owns navigation; the other apps need no Revrag code.
- The app runs inside an iframe: the iframe needs
allow="microphone", and the agent sees only that iframe's document. - Your page embeds iframes: a cross-origin iframe (a payment form, a third-party widget) is one opaque box to the agent. It cannot read or fill it.
- Multi-page app (full page loads): each page load initializes the SDK again, and a live call ends on every full navigation.
6. Control it from your own code
Start and end calls, and open or close the widget card, from your own code.
Start and end calls
Inject EmbedCallService:
import { Component } from '@angular/core';
import { EmbedCallService } from '@revrag-ai/embed-angular';
@Component({
selector: 'app-talk-button',
standalone: true,
template: `
<button type="button" (click)="calls.startCall()">Talk to an expert</button>
<button type="button" (click)="calls.endCall()" [disabled]="!calls.isCallActive()">End call</button>
`,
})
export class TalkButtonComponent {
constructor(public calls: EmbedCallService) {}
}| Method | Returns | Behaviour |
|---|---|---|
startCall() | Promise<void> | Connects a call. Never rejects, and does nothing if a call is already connecting or live |
endCall() | void | Ends the call. A no-op when there is none |
isCallActive() | boolean | true while a call is connected (not while it is still connecting) |
startCall()— call errors:- A denied microphone raises
microphone_permission_denied. - A token failure (including a network error fetching it) raises no event — it logs
Token generation failedto the console. - A failure to connect to the call server raises no event and logs nothing from the SDK.
- A denied microphone raises
Three things worth knowing:
- Call
startCall()from a click. The browser shows the microphone prompt then, and may block audio playback for calls started without a user gesture. - The button must be on screen. The call lives in the button: on a path where it is hidden,
startCall()only warns[RevRag] Cannot startCall: SDK not initialized (no EmbedProvider mounted)and does nothing. - The dashboard can start calls too — make sure that is what your product wants.
- If the dashboard sets
collapsedView.inactivityBehaviortoauto_triggerwith anudgeDelayabove 0, the widget expands and starts a call by itself that many seconds after the button appears. - It does so whatever the user is doing, and again each time the button reappears.
- It starts without a click, so the browser may block the microphone prompt or audio.
- Since
2.0.0there is no nudge and no automatic call unless the dashboard sets one up.
- If the dashboard sets
The widget card
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
import {
expandWidget,
collapseWidget,
isWidgetExpanded,
subscribeWidgetExpanded,
} from '@revrag-ai/embed-angular';
@Component({
selector: 'app-help-toggle',
standalone: true,
template: `<button type="button" (click)="toggle()">{{ open ? 'Close assistant' : 'Open assistant' }}</button>`,
})
export class HelpToggleComponent implements OnInit, OnDestroy {
open = false;
private unsubscribe?: () => void;
constructor(private zone: NgZone) {}
ngOnInit(): void {
this.open = isWidgetExpanded(); // the listener is not called with the current value
this.unsubscribe = subscribeWidgetExpanded((expanded) =>
this.zone.run(() => (this.open = expanded)),
);
}
toggle(): void {
if (isWidgetExpanded()) collapseWidget(); // each returns true if it changed anything
else expandWidget();
}
ngOnDestroy(): void {
this.unsubscribe?.();
}
}- These only change what is shown. They start no call and send no analytics.
- Wrap your state change in
NgZone.run. The listener can be called from outside Angular's zone. - Before the dashboard config has loaded, calling
expandWidget()orcollapseWidget()makes the SDK ignore the dashboard's initial collapsed/expanded setting for that page load.
7. Events and analytics
Send your own context to Revrag, and listen to what the widget and the call are doing.
There are two separate systems:
- Data events —
embedEvent.event(…). You send context to Revrag (who the user is, what they did). They go to the backend. - Agent events —
embedOnAgent(…). The SDK tells you what the widget and the call are doing. They stay in the browser (and are also reported to Revrag).
Sending — embedEvent.event
Who the user is goes in USER_DATA, after login (§2). For everything else:
// src/app/revrag-events.service.ts
import { Injectable } from '@angular/core';
import { embedEvent, EventKeys } from '@revrag-ai/embed-angular';
@Injectable({ providedIn: 'root' })
export class RevragEventsService {
// Something happened in your product. event_name is required for ANALYTICS_DATA.
async loanOfferViewed(amount: number): Promise<void> {
const result = await embedEvent.event({
eventKey: EventKeys.ANALYTICS_DATA,
event_name: 'loan_offer_viewed',
data: { amount },
context: { screen: '/loans', flow: 'loan_application' },
});
if (!result.success) console.warn('[Revrag] event not sent:', result.error);
}
// Free-form context for the agent.
async cartContext(cartValue: number): Promise<void> {
await embedEvent.event({ eventKey: EventKeys.CUSTOM_EVENT, data: { cart_value: cartValue } });
}
}- As long as you pass
data, everyembedEvent.event(…)call resolves with{ success, error? }and does not throw. - Without
data, the promise rejects. Pass{}if you have nothing. - Without
context.screen, the event carries the browser's current pathname.
| Key | Wire value | Use it for |
|---|---|---|
EventKeys.USER_DATA | user_data | Who the user is. Must carry app_user_id |
EventKeys.ANALYTICS_DATA | analytics_data | Named product events. Requires event_name |
EventKeys.CUSTOM_EVENT | custom_event | Free-form context |
- Every key except
USER_DATAneeds a user id — yourUSER_DATA, or the anonymous one from initialization. - Use only these three keys. The other
EventKeysvalues are for the SDK's own use. embedEvent.customEvent(…)sends nothing to Revrag. It only notifies your ownaddCallbacklisteners.
embedEvent exposes event, customEvent, addCallback, removeCallback and getStoredAppUserId.
Watch every event in the browser
Register a listener to see the events you send and the ones the SDK raises. removeCallback matches on the same function instance:
import { embedEvent, type CustomEventPayload } from '@revrag-ai/embed-angular';
const onEvent = (event: CustomEventPayload) => console.debug('[revrag event]', event.type, event);
embedEvent.addCallback(onEvent);
// later
embedEvent.removeCallback(onEvent);Click analytics — data-revrag-name
With enableTracker on (the default), every click on something clickable — a button, link, form control, or an element with a click handler — is also sent to Revrag as an analytics_data event. Nothing to wire up.
The event is named after the control. To choose the name yourself, add data-revrag-name:
<button data-revrag-name="apply_loan">Apply now</button> <!-- event_name: apply_loan_pressed -->- 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. - The suffix says what happened:
_pressedfor a button or link,_opened/_closedfor a dropdown trigger,_selectedfor an option, select or radio,_toggledfor a checkbox. - 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. - Generated ids are skipped for new names, such as Angular Material's
mat-select-0— so adddata-revrag-nameto Material controls you care about. - A handler delegated from a parent: add
data-clickableto the element the user clicks. - Privacy: masked fields, password fields, and text that looks like an e-mail, phone, account or card number never become a name or a value. Clicks on the SDK's own widget are never reported.
- Off switch:
enableTracker: false, or theclickTrackingflag in your Revrag configuration.
Agent events — embedOnAgent / embedOffAgent
AgentEvent | Value | Fires when |
|---|---|---|
AGENT_VISIBLE | agent_visible | The provider shows the button on a path — on every navigation where it is shown, while a user id is stored (so it can be missing on the very first page) |
AGENT_CONVERSATION_STARTED | agent_conversation_started | The agent joins a connected call (a moment after isCallActive() turns true) |
AGENT_CONVERSATION_ENDED | agent_conversation_ended | A call the agent had joined ends, for any reason. A call that fails before the agent joins raises none |
POPUP_MESSAGE_VISIBLE | popup_message_visible | The inactivity nudge appears (only if your dashboard sets one up) |
AGENT_TAP_TO_OPEN / AGENT_TAP_TO_CLOSE | agent_tap_to_open / agent_tap_to_close | The user taps the button to expand or collapse the widget |
MICROPHONE_PERMISSION_ALLOWED | microphone_permission_allowed | Microphone access granted |
MICROPHONE_PERMISSION_DENIED | microphone_permission_denied | Microphone access denied — carries error_message and error_name |
GEN_TOOL_TRIGGERED | gen_tool_triggered | Reserved; not emitted on web yet |
AGENT_CONNECTED / AGENT_DISCONNECTED | agent_connected / agent_disconnected | Deprecated, never emitted. Use the conversation events |
Payload:
- Every event has
type,timestamp(ISO string) andsession_id, plus its own fields. - The conversation events add
call_id,reasonandcallDuration(seconds;0on start).
reason on an end is one of:
reason | Meaning |
|---|---|
manual_disconnect | Hung up, or the user landed on an excluded path |
destroyed | The button was removed — see §4 |
room_disconnected | Network or server |
participant_disconnected | The agent left |
- Compare against the
AgentEventconstants, never string literals. - Agent events are only raised while a user id is stored — your
USER_DATA, or the anonymous one.
A drop-in event logger
Add this service once to see every agent event with a readable label. It is the fastest way to watch the widget working, and the place to forward events to your own analytics:
// src/app/revrag-event-log.service.ts
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { AgentEvent, embedOnAgent, embedOffAgent, type AgentEventPayload } from '@revrag-ai/embed-angular';
const LABELS: Partial<Record<string, string>> = {
[AgentEvent.AGENT_VISIBLE]: 'button visible',
[AgentEvent.AGENT_TAP_TO_OPEN]: 'user opened the widget',
[AgentEvent.AGENT_TAP_TO_CLOSE]: 'user closed the widget',
[AgentEvent.POPUP_MESSAGE_VISIBLE]: 'nudge shown',
[AgentEvent.MICROPHONE_PERMISSION_ALLOWED]: 'microphone allowed',
[AgentEvent.MICROPHONE_PERMISSION_DENIED]: 'microphone DENIED',
[AgentEvent.AGENT_CONVERSATION_STARTED]: 'call started',
[AgentEvent.AGENT_CONVERSATION_ENDED]: 'call ended',
};
@Injectable({ providedIn: 'root' })
export class RevragEventLogService implements OnDestroy {
/** Every agent event, inside Angular's zone — subscribe to forward them. */
readonly events$ = new Subject<AgentEventPayload>();
log = false;
private readonly handle: string;
constructor(zone: NgZone) {
this.handle = embedOnAgent((event) => {
if (this.log) console.info(`[revrag] ${LABELS[event.type] ?? event.type}`, event);
zone.run(() => this.events$.next(event));
});
}
ngOnDestroy(): void {
embedOffAgent(this.handle); // one subscription, released with the app
}
}// In your root component's constructor:
constructor(eventLog: RevragEventLogService) {
eventLog.log = !environment.production; // log in development only
}Gate logging to development.
- Payloads carry
app_user_idand call ids. - Do not forward them wholesale to error trackers or third-party analytics.
8. How paths become screen names
Learn where the SDK reads the path from, the rules for it, and how getScreenName names screens.
The SDK identifies "where the user is" by a path:
| Source | When it is used | |
|---|---|---|
| 1 | [currentPath] | You pass a non-empty string — it always wins |
| 2 | The Angular Router | The router is present — the default for most apps. The URL after redirects, without ?query and #fragment, on every NavigationEnd |
| 3 | window.location.pathname | No router and no currentPath. Read once, when the provider starts |
Rules worth knowing:
- Paths are matched exactly and are case-sensitive.
/Loans≠/loans, and/loans/≠/loans. - Pass only the pathname as
currentPath— strip?…and#…yourself. - Never put identifiers in a path you control (
'/onboarding/' + userId): paths become the agent's screen map and travel to Revrag. getScreenNamenames the screen the user is on, for when the URL alone can't — a wizard whose steps share one path, or screens chosen by a query parameter.-
Without it, the screen is the path.
-
It takes no arguments. Read your own state and return a name, or
nullto fall back to the path. -
Pass an arrow-function property, not a method, so
thisstill works:// src/app/app.component.ts import { Component } from '@angular/core'; import { Router, RouterOutlet } from '@angular/router'; import { EmbedProviderComponent } from '@revrag-ai/embed-angular'; import { WizardService } from './wizard.service'; // your own state @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, EmbedProviderComponent], template: ` <revrag-embed-provider [getScreenName]="screenName"> <router-outlet></router-outlet> </revrag-embed-provider> `, }) export class AppComponent { constructor(private router: Router, private wizard: WizardService) {} // An arrow function keeps `this`. Return null to fall back to the path. screenName = (): string | null => this.router.url.startsWith('/apply') ? `/apply/step-${this.wizard.step}` : null; } -
Its value is the screen's identity, not a display label. The agent's screen map is keyed on it, so two screens must never return the same name, and one screen must not return different names.
-
It runs on every capture, so keep it cheap and free of side effects.
-
It does not affect where the button appears, which always uses the path.
-
- Your path is the single source of truth. Once the provider reports a path from
currentPathor the router, the SDK ignores its own route guesses (history changes,hashchange). That one path is what the agent, the screen map and Revrag's analytics see.
9. Verify it works
Turn on debug logs, run the self-check, and walk through a good first run.
Turn on debug logs
- In a normal Angular build, the SDK's debug logs are off — in development too.
- Turn them on in the browser console (no reload needed):
window.__revragDebug = true; // false turns them off again- A reload turns them off again. To see the logs from page load, set the flag in a
<script>inindex.html, before your app's bundle.
What the logs look like:
- Every line starts with
[RevRag]and an area —call,runtime,scope,capture,cache,sync,widget. - Messages to and from the agent print as
[RevRag ▸ …](outgoing) and[RevRag ◂ …](incoming), with one JSON line per message under[RevRag wire]. - Failures log to the console even with debug off — a failed API request,
Token generation failed, thestartCallwarning.
Run the self-check
verifyIntegration() lives in a separate entry, @revrag-ai/embed-angular/diagnostics. Call it from a development-only component or service:
// src/app/revrag-self-check.component.ts — development 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 to the console
console.log(result.ok, result.summary);
}
}- Render it only in development — for example behind
isDevMode()from@angular/core— and remove it before you ship. - Run it after you have logged in, visited two routes and clicked something.
- Pass
{ offline: true }to skip the checks that make network requests (V7 and V10). - Pass
{ report: false }to skip the console report. - The last result is also stored on
window.__REVRAG_VERIFY__, so you can read it from the console after a run.
| Id | Check |
|---|---|
| V1 | SDK initialized |
| V2 | API key accepted — reads the stored initialization result, so it also fails when sessionStorage is blocked |
| V3 | Provider mounted exactly once |
| V4 | Widget rendered, or hidden with a reason. It reads the browser's pathname plus #hash (not the router's path) and checks it only against excludeScreens (plus includeScreens when [showOnAllScreens]="false"). Run it on a screen where the button is visible: on a path hidden by includeScreens, a group or an entrance delay — or with hash routing or currentPath — it can report a false failure |
| V5 | User identity set |
| V6 | Route changes detected (needs two visited routes; reads the screen cache, so it fails when the dashboard turns localCache off, or right after EmbedLogout()) |
| V7 | Events accepted — sends a real custom_event named revrag_verify_integration |
| V8 | Capture healthy — fails only if capture throws, nothing actionable is captured, or the SDK's own UI leaks in; unlabeled controls and truncated containers are notes |
| V9 | Masking honored |
| V10 | The Revrag backend is reachable (it does not check the call server or the avatar host) |
- Each failing check carries a
remediationtelling you what to change. - Skipped checks are not failures and are not counted: V7 when no user id is stored; V9 when the page has no masked fields; V7 and V10 in offline mode.
A good first run
- Page load (with
window.__revragDebug = true) →[RevRag] …lines;isInitialized$emitstrue,error$staysnull. - Log in → your
USER_DATAis sent. - Navigate to an allowed path → the button appears;
agent_visiblefires. - Tap the button and talk → the microphone prompt (first time) →
microphone_permission_allowed→agent_conversation_started. - Navigate inside a
continuousgroup → the call keeps running. - Hang up →
agent_conversation_endedwithreason: 'manual_disconnect'. - Navigate to an excluded path → the button is gone, a live call ends, and the agent stops seeing the page.
Invalid patterns: a call before initialization; agent_visible never firing; two buttons.
10. Troubleshooting
Find your symptom below, then work through its causes and fixes.
The button never appears
Check these in order — the first three account for most cases.
- Did initialization succeed? Subscribe to
error$and catch theinitialize()promise.[RevRag] app_version is required…→ passappVersion.- Any other error → check the API key and
baseUrlbelong to the same environment, and that the dashboard has a config for thisappVersion.
- Did you add the stylesheet to
angular.json? Without it the button gets only a minimal fallback style and can look absent. Restartng serveafter the change. - Does the path match? Turn on debug logs and read the
[RevRag] scopelines, or logrouter.url. Matching is exact and case-sensitive; a trailing slash counts. - Is the path excluded?
excludeScreensbeats everything (unlessshowOnAllScreensisfalse), and covers every path below it. - Is an entrance delay running? A 3000 ms delay looks like "not working" for three seconds.
- Did you define a group? Group screens turn
includeScreensinto an allow-list (§4). - Is
sessionStorageblocked? Strict privacy settings and sandboxed iframes block it; the widget cannot render without it. - Is the provider in a routed component? Move it to the root component (5C).
NG05105: Unexpected synthetic property / the widget does not open
- Cause: animations are not enabled.
- Fix: add
provideAnimations()(standalone) orBrowserAnimationsModule(NgModule) — §1.
"Can't bind to … since it isn't a known property of 'revrag-embed-provider'"
- Cause: the component that uses the tag does not import the provider.
- Fix: add
EmbedProviderComponentto that standalone component'simports, orEmbedModuleto its NgModule.
NgModule app: NG6008 "Component AppComponent is standalone, and cannot be declared in an NgModule"
- Cause: on Angular 19 and newer, components are standalone unless they say otherwise.
- Fix: add
standalone: falseto the component's decorator (5B).
The widget appears unstyled
- Cause: the stylesheet is not in your build.
- Fix: add
node_modules/@revrag-ai/embed-angular/styles/widget.csstoangular.jsonstyles, then restartng serve.
The build fails: signal is not exported by @angular/core
- Cause: you are on Angular 15 or older.
- Fix: upgrade Angular to 16 or newer, or stay on
1.0.7(1.0.7 guide).
npm ERESOLVE on install
- Cause:
@angular/animationsor@angular/routerwas missing, so npm picked a newer major than the rest of your Angular packages. Or your Angular is older than 16. - Fix: install
@angular/animationsand@angular/routerat your@angular/coreversion first, then the SDK (§1). - More: the Dependency & Compatibility Guide.
TypeScript: Cannot find module '@revrag-ai/embed-core' (TS2307)
- Cause: you are on
1.0.7, whose type files import the unpublished core package. - Fix: upgrade to
2.0.0. (On1.0.7,skipLibCheck: truehides it.)
The call ends when the user navigates
- Cause: the button was hidden — and on the web, a hidden button ends the call (§4). Either:
- the next path is outside
includeScreensor excluded - the next path has an entrance delay
- the provider itself was destroyed (it sits in a routed component)
- the next path is outside
- Fix: put the journey in one
continuousgroup with no delay, and keep the provider in the root component.
Two buttons appear
- Cause: two buttons are mounted — two providers, or a provider and a
<revrag-embed-button>. - Fix: keep exactly one provider and no standalone button.
The call does not start
- No microphone prompt: the page is not a secure context (HTTPS), or it runs in an iframe without
allow="microphone". - The user denied the microphone: listen for
microphone_permission_deniedand tell them how to re-enable it in the site settings. app_user_id is required: you setenableTracker: false(or calledEmbedLogout()) and have not sentUSER_DATAyet.- Nothing in particular: turn on debug logs and read the
[RevRag] calllines; check your CSP allows the call server overwss:andhttps:.
startCall() logs "no EmbedProvider mounted"
- Cause: the button is not mounted on this path (hidden by visibility), so there is nothing to start a call from.
The avatar shows the default orb, not ours
- Cause: your avatar animation could not be fetched — your CSP
connect-srcor the host's CORS rules blocked it. - Fix: allow the avatar host in
connect-src, or ask Revrag to serve it from a CORS-enabled origin (CSP).
Agent events never fire
- Cause: they are only raised while a user id is stored.
- Fix: with
enableTracker: false, sendUSER_DATAfirst. - Also check that you compare
event.typeagainstAgentEventconstants.
Your UI does not update from an SDK callback
- Cause: the callback ran outside Angular's zone, so change detection does not see the change.
- Fix: wrap the state change in
NgZone.run(…), as in the widget card and the event logger.
Something else
- Turn on debug logs in
index.html(a flag set in the console does not survive the reload). - Reload.
- Reproduce.
- Send Revrag:
- the console output from page load
- the
verifyIntegration()report - the SDK version and your Angular version
- the browser and OS
- the list of paths your app reports
11. Known limitations
Each is a current behaviour, its effect, and what to do.
| Id | Limitation | What to do |
|---|---|---|
| W1 | Hiding the button ends a call. The call lives in the button, so an entrance delay or a path outside includeScreens ends it, not only excludeScreens | continuous groups without delays for journeys |
| W2 | After EmbedLogout(), calls need a new user id. The anonymous id is only created on a page load | Send USER_DATA after the next login, or reload to your login page (§2) |
| W3 | Call errors are not shown in the widget, and only one reaches your code. A denied microphone raises microphone_permission_denied | Handle it with your own message; for connection failures, treat "no agent_conversation_started within a few seconds of startCall()" as a failure |
| W4 | sessionStorage is required. Where it is blocked, the widget does not render | — |
| W5 | The provider has no inputs to turn off dragging or set the auto-collapse time. In 'fixed' mode the button is always draggable | Set the collapse time on the dashboard (collapsedView.collapsedTime) |
| W6 | trackerCallback and widgetClassName do nothing | Use embedEvent.addCallback; style the page around the widget instead |
| W7 | No automatic PII detection; unmasked text and input values are captured and cached off-call | Mask sensitive fields (data-revrag-mask), keep personal data out of URLs, and disclose to your security reviewer |
| W8 | An avatar animation that cannot be fetched shows the bundled orb, not your avatar | Serve the animation from a CORS-enabled origin your CSP allows |
| W9 | Debug logs are off by default, in development too | window.__revragDebug = true |
| W10 | Without a router and without currentPath, the path is read once and never updates | Pass currentPath (5E) |
| W11 | Zoneless Angular is untested | Keep zone.js |
- W3 — a failed connection (token, network, a room that drops before the agent joins) raises no event at all — the button simply returns to idle.
12. Checklist
Check off each item for your integration.
Install and serve
- Angular 16 or newer
-
@angular/animationsand@angular/routerinstalled at your@angular/coreversion, then@revrag-ai/embed-angular,livekit-clientandlottie-web -
node_modules/@revrag-ai/embed-angular/styles/widget.cssinangular.jsonstyles -
provideAnimations()orBrowserAnimationsModule - Served over HTTPS;
allow="microphone"on any iframe that hosts the app
Initialize and identify
-
EmbedInitService.initialize(apiKey, { appVersion })once, in the root component (browser only, with SSR) - API key and
baseUrlfrom the same environment; production omitsbaseUrl - Dashboard has a widget config (and avatar) for this exact
appVersion— re-checked on every release that bumps it -
error$logged and theinitialize()promise caught -
USER_DATAwith your stableapp_user_idsent after login, afterisInitialized$istrue - Sign-out calls
EmbedLogout()(§2)
Mount and visibility
- Exactly one
<revrag-embed-provider>, in the root component; no standalone<revrag-embed-button> - Router-less apps pass
currentPath— pathname only, no query, hash or identifiers - Visibility lists written against the paths your app actually reports
- Login, OTP, PIN and payment paths in
excludeScreens - Journeys that must keep a call alive are one
continuousgroup, with no entrance delay - Nudge and auto-call settings on the dashboard are what your product wants
Privacy and CSP
- Sensitive fields masked (
data-revrag-mask), or kept outsideactionRootor on excluded paths - Icon-only buttons and unlabeled inputs have an
aria-label— the agent finds controls by name - CSP allows the backend, the call server (
wss:andhttps:) and your avatar host (§1)
Verify and test
-
verifyIntegration()passes in development - A real call tested on desktop Chrome and iOS Safari, on the production build and CSP
Reference
Look up the public API, package entries, working examples and the other guides.
| Package | @revrag-ai/embed-angular — entries ., /diagnostics, /styles/widget.css |
| AI coding assistants | Angular Copilot integration · the package also ships node_modules/@revrag-ai/embed-angular/revrag-integrate.md |
| Working example | examples/angular in the SDK repository — Angular 19, standalone, four visibility patterns |
| Versions & pitfalls | Dependency & Compatibility Guide |
| More patterns | Advanced provider usage |
| Release notes | Angular SDK release notes |
| Older version | 1.0.7 guide |
| Other platforms | React · Android · React Native |
Public API
Everything you need. Anything not listed here or under Deprecated is internal and may change without notice.
import {
EmbedProviderComponent, // <revrag-embed-provider> — mount once (section 3)
EmbedButtonComponent, // <revrag-embed-button> — only without the provider
EmbedModule, // for NgModule apps (section 5B)
EmbedInitService, // initialize, isInitialized$, isLoading$, error$ (section 2)
EmbedCallService, // startCall, endCall, isCallActive (section 6)
EmbedLogout, // on sign-out (section 2)
ActionIntelligenceService, // drive Action Intelligence without the provider (advanced)
embedEvent, EventKeys, // send events (section 7)
expandWidget, collapseWidget, isWidgetExpanded, subscribeWidgetExpanded, // the card (section 6)
AgentEvent, embedOnAgent, embedOffAgent, // agent events (section 7)
MASK_ATTR, // 'data-revrag-mask'
} from '@revrag-ai/embed-angular';
import type {
EmbedButtonVisibilityConfig, EmbedButtonGroupConfig, EmbedButtonContinuity,
EmbedButtonDelayPolicy, EmbedButtonPosition,
SDKConfig, EventKey, CustomEventData, CustomEventPayload,
AgentEventPayload, AgentEventHandler, ActionScopeInputs,
} from '@revrag-ai/embed-angular';
import { verifyIntegration, integrationSnapshot } from '@revrag-ai/embed-angular/diagnostics';
import type { VerifyResult, VerifyCheck, VerifyOptions } from '@revrag-ai/embed-angular/diagnostics';embedEventexposes onlyevent,customEvent,addCallback,removeCallbackandgetStoredAppUserId.integrationSnapshot()returns the runtime state without running any checks.EmbedInitServicealso hasreset(),clearSessionData(),getStorageInfo()andsessionData$. You do not need them for a normal integration — and see On logout before using the first two.
Deprecated, still working (exported for apps written against 1.0.7; to be removed in a later major version): EmbedWidgetComponent, AnimatedButtonComponent, ExpandedViewComponent, CallTextPanelComponent, TooltipComponent, WaveformVisualizerComponent, TypewriterComponent, EmbedLiveKitService, InteractionWatcherService, PlatformService, WidgetConfigService, resolveColorPalette, resolveAnimationData, resolveWidgetPosition, deriveWidgetConfig, and the types WidgetConfig, WidgetSide, ResolvedWidgetPosition, EventPayload. See the release notes.