Skip to content

Set up Faro

Add Grafana Faro 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

npm install @grafana/faro-web-sdk

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

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.

Initialize Faro

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

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

const faro = initializeFaro({
  url: 'https://telemetry.nav.no/collect',
  paused: window.location.hostname === 'localhost',
  app: {
    name: 'my-app',   // required β€” identifies your app in Grafana
    version: '1.0.0',  // optional β€” useful for comparing behavior across deploys
  },
  instrumentations: [
    ...getWebInstrumentations(),
  ],
});

Auto-configuration

Instead of hardcoding the collector URL, you can let the platform generate it for you. See auto-configuration below or the reference page for details.

Setting app.version

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

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

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:

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

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

Step 2: Import and use it

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.config.js
export default {
  build: {
    rollupOptions: {
      external: ['./nais.js'],
    },
  },
};
// webpack.config.js
module.exports = {
  externals: {
    './nais.js': 'excludedFile',
  },
};
// 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:

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

Stack traces from pushed errors are automatically deobfuscated if sourcemaps are available.

React error boundaries

Use <FaroErrorBoundary> from @grafana/faro-react to catch React rendering errors. Without this, errors that happen during rendering are silently lost.

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 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:

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:

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:

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

Content Security Policy (CSP)

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

connect-src 'self' https://telemetry.nav.no https://telemetry.ekstern.dev.nav.no;

Without this, the browser blocks Faro's requests to the collector. See Troubleshooting 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, tokens, passwords, or other PII to the console
  • Watch URLs β€” query parameters and path segments may contain identifiers
  • Watch form input β€” don't send user input as custom events without redacting

Use the beforeSend hook to filter or redact telemetry:

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 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:
{app_name="my-app"} | logfmt

Real-world examples

These navikt repositories use Faro with React Router:

Next steps