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-tracingBundle 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.external.prod.test-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 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.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.
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:
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.test-nais.cloud.nais.io/ https://telemetry.external.dev.test-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, 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 ¶
- 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 APIs -
navikt/bidrag-bidragskalkulator-ui— uses@grafana/faro-reactwith router integration