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-sdkIf you want browser tracing (connects frontend spans with backend traces), also install:
npm install @grafana/faro-web-tracingInitialize 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.external.prod.ci-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(),
],
});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.jsThis 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',
namespace: 'my-team',
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: [].
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:
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.
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.ci-nais.cloud.nais.io/ https://telemetry.external.dev.ci-nais.cloud.nais.io/;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, 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:
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 ¶
- Deploy your application
- Open it in a browser and interact with it
- Open your app in Grafana APM and go to the Frontend tab to see Core Web Vitals
- Or query Loki directly in Grafana Explore:
{app_name="my-app"} | logfmtReal-world examples ¶
These navikt repositories use Faro with React Router:
navikt/dp-brukerdialog-frontend— includes trace propagation to backend APIsnavikt/bidrag-bidragskalkulator-ui— uses@grafana/faro-reactwith router integration