# Set up Faro

Add [Grafana Faro](https://www.npmjs.com/package/@grafana/faro-web-sdk) to a frontend application running on Nais.

## Prerequisites

- A frontend application deployed on Nais (GCP only; on-premises is not supported)
- Node.js and npm

## Install

```sh
npm install @grafana/faro-web-sdk
```

If you want browser tracing (connects frontend spans with backend traces), also install:

```sh
npm install @grafana/faro-web-tracing
```

> **Bundle size**
> `@grafana/faro-web-tracing` adds ~500kB to your JavaScript bundle. Only include it if you need [trace propagation](/observability/frontend/how-to/trace-propagation.md).

## Initialize Faro

Initialize Faro as early as possible in your application so it captures all errors and page loads.

```js
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';

const faro = initializeFaro({
  url: 'https://telemetry.external.prod.dev-nais.cloud.nais.io/collect',
  paused: window.location.hostname === 'localhost',
  app: {
    name: 'my-app',       // required — must match metadata.name in nais.yaml
    namespace: 'my-team', // required — must match metadata.namespace in nais.yaml
    version: '1.0.0',     // optional — useful for comparing behavior across deploys
  },
  instrumentations: [
    ...getWebInstrumentations(),
  ],
});
```

> **Use the same name and namespace as your Nais app**
> `app.name` and `app.namespace` must match `metadata.name` and `metadata.namespace` in your `nais.yaml`. Nais APM uses these fields to group frontend telemetry with your app — if they don't match, your frontend data appears as a separate service in the APM service list.

> **Auto-configuration**
> Instead of hardcoding the collector URL, you can let the platform generate it for you. See [auto-configuration](#auto-configuration) below or the [reference page](/observability/frontend/reference/auto-configuration.md) for details.

### Setting `app.version`

Setting a version lets you filter and compare metrics across deploys in Grafana. If you use [auto-configuration](#auto-configuration), the version is extracted from your container image tag automatically.

For manual setup, inject the commit SHA from your CI pipeline:

```js
app: {
  name: 'my-app',
  version: process.env.COMMIT_SHA || 'local',
}
```

## Auto-configuration

The platform can generate the collector URL and app metadata for you. This is the recommended approach for static frontends (nginx, CDN) since the URL is set per cluster — no separate config for dev and prod.

Add this to your `nais.yaml`:

```yaml
spec:
  frontend:
    generatedConfig:
      mountPath: /usr/share/nginx/html/js/nais.js
```

This generates a JavaScript file at the specified path containing the collector URL, your app name (from `metadata.name`), and version (from your image tag). The environment variable `NAIS_FRONTEND_TELEMETRY_COLLECTOR_URL` is also set in your pod.

See the [auto-configuration reference](/observability/frontend/reference/auto-configuration.md) for the full list of generated values.

### Step 1: Create a local `nais.js` fallback

Create a `nais.js` file for local development. Nais replaces this file at deploy time with the real values.

```js
export default {
  telemetryCollectorURL: 'http://localhost:12347/collect',
  app: {
    name: 'my-app',
    namespace: 'my-team',
    version: 'local',
  },
};
```

### Step 2: Import and use it

```js
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import nais from './nais.js';

const faro = initializeFaro({
  url: nais.telemetryCollectorURL,
  app: nais.app,
  instrumentations: [
    ...getWebInstrumentations(),
  ],
});
```

### Step 3: Exclude `nais.js` from your bundler

The local fallback file must not be bundled into your production build. Exclude it in your bundler config:

**Vite / Rollup**

```js
// vite.config.js
export default {
  build: {
    rollupOptions: {
      external: ['./nais.js'],
    },
  },
};
```

**Webpack**

```js
// webpack.config.js
module.exports = {
  externals: {
    './nais.js': 'excludedFile',
  },
};
```

**Next.js**

```js
// next.config.js
module.exports = {
  webpack: (config) => {
    config.externals.push('./nais.js');
    return config;
  },
};
```

## Capture exceptions

Console errors are captured automatically. To get full stack traces for caught exceptions, push them to Faro:

```js
try {
  riskyOperation();
} catch (error) {
  faro.api.pushError(error);
}
```

Stack traces from pushed errors are [automatically deobfuscated](/observability/frontend/how-to/sourcemaps.md) if sourcemaps are available.

### React error boundaries

Use `<FaroErrorBoundary>` from [`@grafana/faro-react`](https://www.npmjs.com/package/@grafana/faro-react) to catch React rendering errors. Without this, errors that happen during rendering are silently lost.

```tsx
import { FaroErrorBoundary } from '@grafana/faro-react';

function App() {
  return (
    <FaroErrorBoundary fallback={<p>Something went wrong</p>}>
      <MyComponent />
    </FaroErrorBoundary>
  );
}
```

For Next.js, see the [dedicated error boundary pattern](/observability/frontend/how-to/setup-nextjs.md#error-boundaries) using `error.tsx`.

## Performance tuning

Faro generates a lot of data by default. Use these options to control the volume:

### Session sampling

Only instrument a percentage of user sessions:

```js
initializeFaro({
  // ... other options
  sessionTracking: {
    samplingRate: 0.5, // instrument 50% of sessions
  },
});
```

### Disable console capture

If your app is verbose with console output, disable automatic capture:

```js
initializeFaro({
  // ... other options
  instrumentations: [
    ...getWebInstrumentations({
      captureConsole: false,
    }),
  ],
});
```

### Filter console levels

By default, Faro ignores `console.debug`, `console.trace`, and `console.log`. To change which levels are captured, use the top-level `consoleInstrumentation` config:

```js
import { LogLevel } from '@grafana/faro-web-sdk';

initializeFaro({
  // ... other options
  consoleInstrumentation: {
    disabledLevels: [LogLevel.DEBUG, LogLevel.TRACE], // capture log, info, warn, error
  },
});
```

To capture all levels, pass an empty array: `disabledLevels: []`.

## Group dynamic URLs in Per-Page Performance

If your app has dynamic URL segments (IDs, UUIDs), the APM dashboard's "Per-Page Performance" table shows one row per unique URL instead of grouping them by route. Use `generatePageId` to normalize dynamic paths into a single route:

```js
initializeFaro({
  // ... other options
  pageTracking: {
    generatePageId: (location) =>
      location.pathname
        .replace(/\/sak\/[^/]+\/[^/]+/, '/sak/{saksid}/{behandlingsreferanse}')
        .replace(/\/person\/[^/]+/, '/person/{fnr}'),
  },
});
```

The function receives the browser `Location` object and returns a string that identifies the "page route". All events from URLs matching the same pattern are grouped together in the dashboard.

> **Use your router's route definitions**
> If you use React Router or Next.js, consider deriving the page ID from your route config rather than writing regex patterns manually. For example, with React Router v6:

> ```js
> import { matchRoutes } from 'react-router-dom';
> import { routes } from './routes';

> pageTracking: {
>   generatePageId: (location) => {
>     const matches = matchRoutes(routes, location.pathname);
>     return matches?.at(-1)?.route.path ?? location.pathname;
>   },
> },
> ```

## Content Security Policy (CSP)

If your application uses a Content Security Policy, add the collector endpoint to `connect-src`:

```
connect-src 'self' https://telemetry.external.prod.dev-nais.cloud.nais.io/ https://telemetry.external.dev.dev-nais.cloud.nais.io/;
```

Without this, the browser blocks Faro's requests to the collector. See [Troubleshooting](/observability/frontend/reference/troubleshooting.md) for more details.

## Privacy and sensitive data

Faro captures console output, errors, and HTTP request URLs automatically. Make sure you don't leak sensitive data:

- **Never log** fødselsnummer, emails, names, tokens, passwords, or any other personal identifier to the console or into telemetry. Nav operates on identities but must not log them.
- **Use opaque identifiers** — when you need to correlate a user or session, use a hashed or otherwise opaque id, never a fødselsnummer, email, or other ident.
- **Watch URLs** — query parameters and path segments may contain identifiers or tokens.
- **Watch form input** — don't send user input as custom events without redacting.

Use the `beforeSend` hook to filter or redact telemetry:

```js
initializeFaro({
  // ... other options
  beforeSend: (item) => {
    // Strip query parameters from page URLs (may contain tokens, codes, identifiers)
    if (item.meta?.page?.url) {
      try {
        const url = new URL(item.meta.page.url);
        url.search = '';
        item.meta.page.url = url.toString();
      } catch { /* ignore malformed URLs */ }
    }

    // Drop items that may contain fødselsnummer (11-digit pattern)
    const payload = JSON.stringify(item);
    if (/\d{11}/.test(payload)) {
      return null;
    }

    return item;
  },
});
```

## Local development

Set `paused: window.location.hostname === 'localhost'` (shown in the init example above) to skip telemetry during local development.

For a full local observability stack, check out the [tracing demo repository](https://github.com/nais/tracing-demo) and run `docker-compose up`.

## Verify it works

1. Deploy your application
2. Open it in a browser and interact with it
3. Open your app in Grafana APM and go to the **Frontend** tab to see Core Web Vitals
4. Or query Loki directly in [Grafana Explore](https://grafana.dev-nais.cloud.nais.io/explore):

```logql
{app_name="my-app"} | logfmt
```

## Real-world examples

These `navikt` repositories use Faro with React Router:

- [`navikt/dp-brukerdialog-frontend`](https://github.com/navikt/dp-brukerdialog-frontend) — includes trace propagation to backend APIs
- [`navikt/bidrag-bidragskalkulator-ui`](https://github.com/navikt/bidrag-bidragskalkulator-ui) — uses `@grafana/faro-react` with router integration

## Next steps

- [Set up Faro with Next.js App Router](/observability/frontend/how-to/setup-nextjs.md)
- [Connect frontend traces to backend spans](/observability/frontend/how-to/trace-propagation.md)
- [Learn how sourcemap deobfuscation works](/observability/frontend/how-to/sourcemaps.md)
