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.
Send a measurement ΒΆ
Use faro.api.pushMeasurement to send named numeric values:
import { faro } from '@grafana/faro-web-sdk';
faro.api.pushMeasurement({
type: 'custom',
values: {
search_results_ms: 342,
},
});You can send multiple values in a single measurement:
faro.api.pushMeasurement({
type: 'checkout-timing',
values: {
payment_form_render_ms: 120,
total_checkout_ms: 4500,
},
});Add context ΒΆ
Attach string key-value pairs as context for filtering:
faro.api.pushMeasurement(
{
type: 'api-call',
values: {
duration_ms: 230,
},
},
{
context: {
endpoint: '/api/soknader',
method: 'POST',
},
},
);Query in Grafana ΒΆ
Custom measurements are stored in Loki as structured log lines with kind=measurement. Query them with LogQL.
Filter by type ΒΆ
{app_name="my-app", kind="measurement"} | logfmt | type="custom"Extract numeric values ΒΆ
Use unwrap to turn a logfmt field into a numeric sample for aggregation:
avg_over_time(
{app_name="my-app", kind="measurement"}
| logfmt
| type="api-call"
| unwrap duration_ms [5m]
) by (context_endpoint)Percentiles ΒΆ
quantile_over_time(0.95,
{app_name="my-app", kind="measurement"}
| logfmt
| type="checkout-timing"
| unwrap total_checkout_ms [5m]
)Count occurrences ΒΆ
count_over_time(
{app_name="my-app", kind="measurement"}
| logfmt
| type="custom"
[5m]
)Naming conventions ΒΆ
| Guideline | Example |
|---|---|
| Use snake_case for value names | search_results_ms, not searchResultsMs |
| Include units in the name | duration_ms, size_bytes |
Use a descriptive type | "checkout-timing", not "custom" |
| Keep context values low-cardinality | Endpoints, categories β not UUIDs or user IDs |
How it works ΒΆ
Browser Alloy Loki
β β β
β pushMeasurement(...) β β
ββββββββββββββββββββββββββββΆβ β
β β logfmt log line β
β β kind=measurement β
β β type=checkout-timingβ
β β total_checkout_ms=4500
β ββββββββββββββββββββββββΆβ
β β β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 ΒΆ
import { faro } from '@grafana/faro-web-sdk';
import { useEffect, useRef } from 'react';
function ExpensiveComponent() {
const startRef = useRef(performance.now());
useEffect(() => {
const renderTime = performance.now() - startRef.current;
faro.api.pushMeasurement(
{
type: 'component-render',
values: {
render_ms: Math.round(renderTime),
},
},
{
context: {
component: 'ExpensiveComponent',
},
},
);
}, []);
return <div>...</div>;
}Then query the P95 render time:
quantile_over_time(0.95,
{app_name="my-app", kind="measurement"}
| logfmt
| type="component-render"
| context_component="ExpensiveComponent"
| unwrap render_ms [5m]
)