Custom frontend metrics ยถ

Faro can send custom numeric measurements from the browser to Loki. Use this to track application-specific metrics like load times for specific components, feature usage counts, or business-relevant durations.

Never put personal data in a custom measurement

Measurement values, event attributes, and everything you pass as context are written verbatim to shared observability storage (Loki) that every team on the platform can query. Treat a custom measurement like a public log line.

Never put a fรธdselsnummer, a name, an address, or any other personopplysning into a measurement value, a context_* field, an event attribute, or setUser. Use only opaque, non-identifying keys โ€” a hashed or otherwise non-reversible id instead of a raw user identifier, and a category instead of a person.

The ingestion pipeline scrubs 11-digit fรธdselsnummer as a defense-in-depth backstop, but it is regex-based and you must not rely on it: it cannot catch names, addresses, or free text, and it does not touch numeric measurement values at all.

Personvern: NAV-identer and names are never scrubbed

Real NAV personopplysninger have already leaked into shared Loki through custom telemetry. Beyond fรธdselsnummer, never put NAV-identer (Z-/NAV-numre), employee or citizen names, enhet-/office names, or case details into measurement values, context_*, event attributes, or setUser. Regex scrubbing cannot catch any of these โ€” only fรธdselsnummer is scrubbed, and only as a backstop. This is the same rule the @nais/apm Privacy section applies to setUser: prefer a hashed or opaque id.

Send a measurement ยถ

Use faro.api.pushMeasurement to send named numeric values:

js

Using `@nais/apm`? Reach `pushMeasurement` through `init()`

nais browser apps set up telemetry through @nais/apm, which deliberately does not wrap pushMeasurement / pushEvent. Its init() returns the underlying Faro instance โ€” call the raw Faro API on that value rather than importing a separate faro singleton:

js

@nais/apm's PII scrubbing runs inside its Faro beforeSend and only rewrites fรธdselsnummer, emails, and token URL parameters. It does not clean numeric measurement values and never catches NAV-identer or names, so custom measurements and events get no meaningful PII protection from the wrapper โ€” the warning above applies in full.

Manual custom spans aren't supported by @nais/apm either: browser tracing is automatic only, so there is no custom-span API to reach for.

You can send multiple values in a single measurement:

js

Add context ยถ

Attach string key-value pairs as context for filtering:

js

Query in Grafana ยถ

Custom measurements are stored in Loki as structured log lines with kind=measurement. Query them with LogQL.

Filter by type ยถ

logql

Extract numeric values ยถ

Use unwrap to turn a logfmt field into a numeric sample for aggregation:

logql

Percentiles ยถ

logql

Count occurrences ยถ

logql

Naming conventions ยถ

GuidelineExample
Use snake_case for value namessearch_results_ms, not searchResultsMs
Include units in the nameduration_ms, size_bytes
Use a descriptive type"checkout-timing", not "custom"
Keep context values low-cardinalityEndpoints, categories โ€” not UUIDs or user IDs

How it works ยถ

Plaintext

Measurements are sent to the Faro collector, converted to logfmt, and stored in Loki with the stream labels {app_name, kind, env}. The measurement values and context end up as logfmt fields in the log line, queryable with | logfmt and | unwrap.

No Prometheus metrics for custom measurements

Unlike Web Vitals (which are automatically extracted as Prometheus histograms), custom measurements are only stored in Loki. Use LogQL unwrap for numeric aggregation. This keeps the metric cardinality bounded while giving you full flexibility in what you measure.

Example: track component render time ยถ

tsx

Then query the P95 render time:

logql