# Add a feedback widget to an Angular app

> Add the widget script to src/index.html with defer. Create a small service that pushes identify onto the widget queue, then call it from an effect in the root component whenever the signed-in user changes. Angular Router moves between pages without reloading the document, so the widget keeps working.

Source: https://escutaproduto.com/resources/feedback-widget-angular
Last updated: 2026-10-09

## Add the script to src/index.html

An Angular CLI project has one HTML file that the browser loads first, at `src/index.html`. Add the widget to its `head` with `defer`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My Angular app</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <script src="https://escutaproduto.com/widget.js" data-key="pk_your_product_key" defer></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
```

The script loads once with the page, so you do not add it to any component. A component that adds the tag would insert it again each time the component mounts, which is the wrong pattern for a page-wide widget. The `data-key` is your public product key, which can only create feedback.

The `base href` line is required by the Angular Router to build URLs correctly, and it is already in a default project. Leave it in place, because the feedback item records the page URL that the router produced.

## Create a service that identifies the user

Put the widget calls in one injectable service. Use the `DOCUMENT` token from `@angular/common` rather than the global `window`, because it works the same way during server rendering:

```ts
// src/app/feedback-identity.service.ts
import { Injectable, inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";

export type FeedbackUser = { id: string; email: string; name?: string };

type EscutaWindow = Window & { EscutaProduto?: { q?: unknown[][] } };

@Injectable({ providedIn: "root" })
export class FeedbackIdentityService {
  private readonly view = inject(DOCUMENT).defaultView as EscutaWindow | null;

  identify(user: FeedbackUser): void {
    if (!this.view) return;
    this.view.EscutaProduto = this.view.EscutaProduto || { q: [] };
    (this.view.EscutaProduto.q ||= []).push([
      "identify",
      [{ id: user.id, email: user.email, name: user.name }],
    ]);
  }
}
```

The service pushes onto the queue the widget reads when it loads. If the widget has not loaded yet, the call still works, because the queue holds the data until it does. The object you pass is copied field by field, so a name with quotes or angle brackets stays as plain data.

## Call identify from the root component

Your auth service already knows the current user. Read it in an `effect` in the root component, so identify runs when the session loads and again whenever the user changes:

```ts
// src/app/app.component.ts
import { Component, effect, inject } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { AuthService } from "./auth.service";
import { FeedbackIdentityService } from "./feedback-identity.service";

@Component({
  selector: "app-root",
  imports: [RouterOutlet],
  template: `<router-outlet />`,
})
export class AppComponent {
  constructor() {
    const auth = inject(AuthService);
    const feedback = inject(FeedbackIdentityService);

    effect(() => {
      const user = auth.currentUser();
      if (user) feedback.identify(user);
    });
  }
}
```

This handles two cases in one place. A user who is already signed in when the app starts gets identified as soon as the session restores. A user who signs in later gets identified when the signal updates. Keep the payload to the fields you would show the person, because the identify call runs in the browser.

Zone-based change detection does not need extra handling for the queue push itself, because pushing an array triggers nothing. If you notice change detection running on every widget event in the Angular DevTools profiler, wrap the call in `NgZone.runOutsideAngular`. Most apps never need it.

## Angular Router navigation and the widget

The Angular Router uses the History API. It swaps the routed component and updates the address without reloading the document, so the script in `index.html` stays loaded. The Feedback button is present on every route, and nothing needs to re-run on navigation.

Each feedback item stores the URL the router produced, so a message from `/orders/1042/refund` shows the path in the inbox. That is useful when a bug depends on one screen. Pair it with the browser name the widget saves, and most layout issues can be reproduced from the item alone.

## Open the form from an Angular template

A template can open the form with a `data-escuta-open` attribute on any element. Angular passes static attributes through unchanged:

```html
<button type="button" data-escuta-open="bug">Report a bug</button>
<button type="button" data-escuta-open="idea">Suggest a feature</button>
```

Valid types are `bug`, `idea`, `praise` and `other`. Add `data-trigger="none"` to the script tag if you want only your own buttons. Use the attribute rather than a `(click)` handler, because the widget then owns focus handling and the Escape key, and you do not need a custom dialog.

## Angular server-side rendering and the window guard

With Angular SSR, the component tree renders on the server first. The `DOCUMENT` token exists there, but its `defaultView` is null, and that is what the guard in the service checks. The identify call therefore does nothing during server rendering and runs only in the browser.

The `effect` in the root component can also run on the server in some setups. The guard makes that harmless. Do not read `window` directly in components, because that throws during server rendering.

## Content Security Policy for an Angular build

Angular CLI builds load their code from external bundles, and the default setup needs no inline scripts for the app. The widget needs its host in two directives:

```text
script-src 'self' https://escutaproduto.com;
connect-src 'self' https://escutaproduto.com;
```

Angular may inject inline styles for component styling. If a strict `style-src` blocks them, Angular reads a nonce from the `CSP_NONCE` injection token, which you provide on the server. Check the Angular security guide for your version, because the setup changed across releases. The [CSP guide for widgets](/resources/content-security-policy-widgets) covers reading a blocked request in the browser console.

## Check the Angular install in your Escuta Produto inbox

1. Add `http://localhost:4200` (the default Angular dev server URL) to the allowed origins of your product.
2. Sign in, open the Feedback button and send a test bug from a page that needs a user.
3. In the inbox, check that the item shows the name and email from the signal, along with the page URL.
4. Navigate with a routerLink to a second page, send another item and confirm the page URL changed.

If identity is missing, log the value of the auth signal in the root effect. A null value means the session has not loaded yet, which the effect handles on the next change.

For the complete option list, read the [widget reference](/docs/widget). The [React Router guide](/resources/feedback-widget-react-router) shows the same root-level pattern, and the [Vue guide](/resources/feedback-widget-vue) covers a composable version.

## Frequently asked questions

### Where do I put the feedback widget script in an Angular app?

In the head of src/index.html, with the defer attribute. Angular CLI builds serve that file as the entry point, so the script loads once and stays loaded while the Angular Router changes pages.

### How do I identify the logged-in user in Angular?

Create a service that pushes identify onto the widget queue, then call it from an effect in the root component that reads the current user signal. The effect runs whenever the user changes, including on app start when a session already exists.

### Does the Angular Router reload the feedback widget?

No. The router uses the History API and renders the next component without reloading the document. The widget stays loaded, so the Feedback button is available on every route.

### Does the widget work with Angular server-side rendering?

Yes, with a guard. The identify service reads the window through the DOCUMENT token and returns early when there is no default view, which is the case during server rendering. The script itself only runs in the browser.
