The RUM Beacon
The small JavaScript file fastmon ships to your visitors. What it observes, when it sends, and what it never does.
The beacon is the JavaScript file fastmon serves at https://fastmon.site/s/{source_hash}.js. That's the only client-side code we ship, and the only thing your visitors load from us.
This page is the technical reference. For setup, head to Implementation; for the privacy posture, see Privacy: what we don't collect.
One bundle, two builds
There's a single beacon (standard.js). The server compiles it per site into one of two builds, decided by the collect_sessions setting:
| Build | When | Storage on device |
|---|---|---|
| Stateless | collect_sessions off | None; the sessionStorage write is compiled out entirely |
| Stateful | collect_sessions on | sessionStorage._fms only (the session id) |
fastmon sets no cookies at all, in either build. By default nothing is written to the device: only the Full preset (or turning on collect_sessions) writes the sessionStorage._fms session id. Whether that write lands on page load or waits for consent is the session_consent setting.
You don't pick the build in the <script> tag; the server resolves it from the site's settings and ships the right one. A change to a setting that affects the served bundle rolls the script cache automatically.
Why it matters
The beacon is the boundary between fastmon and the visitor: what it measures, when it flushes, and which identifiers it mints all happen in the browser. We keep that surface small and document it in full on this page, and the wire shape is enforced server-side, so what the beacon can and can't do stays auditable and easy to defend in a privacy review, independent of the script itself.
The shipped script is minified and obfuscated on purpose, not to hide what it does (that's all on this page) but so content and ad blockers can't pattern-match it by name and silently block it, which would leave holes in your monitoring.
What the beacon observes
| Source | What's captured |
|---|---|
PerformanceObserver | LCP, INP, CLS, FCP, paint entries, long animation frames, layout shifts. |
Navigation Timing | TTFB, redirect count, navigation type (navigate, reload, back_forward). |
Resource Timing | Per-resource size, duration, and category. Aggregated to per-domain rollups. |
error event | Type, count, and stack-frame hosts always; the path + line/column of your own first-party script frames too. The message, and the detail of inline / third-party frames, only when collect_error_messages / collect_error_frames are on, and only after an always-on PII scrubber masks URLs, emails, IDs, and quoted strings. |
unhandledrejection | Same shape as error. |
Page Visibility API | Time-to-hidden, total visible time. |
fetch / XMLHttpRequest | Per-endpoint Fetch/XHR aggregates: call counts, durations, duration buckets, and a status-error distribution, templated per endpoint (method + host + path, no query strings). Server-controlled per site (collect_fetch_xhr, default off). |
Server-Timing | The Server-Timing metrics your own backend reports (name and duration), first-party by construction. |
| Document & Window | Page URL (sanitized to origin + path), referrer, viewport size class. |
Geo data (the country) is not read in the browser. The edge derives it from the request IP and forwards a country code as a FM-Country header; the IP itself never reaches the application. Same for the bucketed browser / OS / device family, forwarded as FM-Browser / FM-Browser-Version / FM-OS / FM-Device. The edge also computes a one-way FM-Stitch header (HMAC-SHA256(rotating-24h-salt, IP | UA | collector_hash | domain) truncated to 128 bits) that the backend uses for bounded cross-pageview joins (stored only when store_stitch is on). See Privacy → The edge-derived stitch identifier for the legal framing. The beacon itself never asks for geolocation.
What the beacon does not do
- Read or write
localStorage/IndexedDB/Cache Storage, ever. - Set cookies. fastmon sets no cookies at all, in any configuration.
- Touch DOM content (no scroll-replay, no session-replay, no form capture). The one carve-out is opt-in
pagetype_ruleset, which reads a single body controller-class token to classify the page type, nothing else. - Read the visitor's media devices, sensors, or geolocation.
- Mint an on-device identifier that links page-views together, unless
collect_sessionsis on (then the tab-scoped session id). The per-pageviewpvidexists in every configuration and dies with the pageview.
The lifecycle of a pageview
page loads
├── beacon script downloads (async, cached, ETag-validated)
├── tracker registers PerformanceObserver and event listeners
├── visitor uses the page
│ └── tracker buffers events in memory
└── flush triggers (visibilitychange→hidden, pagehide, soft-nav,
error update, or terminate) cause a POST to /c/{collector_hash}
with a versioned payload (lifecycle marks each emission)The flush uses navigator.sendBeacon() when it's available, so the request survives even if the page is unloading. If sendBeacon isn't available, the tracker falls back to fetch with keepalive: true. If both fail (legacy browsers, very aggressive extension blocking), it falls back to a GET pixel at /c/{collector_hash}.gif?…. That URL serializes a safe subset of fields into the query string. Sensitive payloads (sid, referrer, errors, the large third-party, resource, and Long Animation Frame breakdowns) are excluded from the pixel fallback by name, so they never travel through any access-log surface.
The wire payload
The payload is JSON with deliberately short keys to keep the wire size down. The body is capped at 8 KiB. All fields are optional; a representative envelope looks like:
{
"sid": "01HZX...",
"pvid": "01HZY...",
"ver": 3,
"lcy": "hidden",
"pst": 1746604800000,
"cst": 1746604803456,
"u": "https://acme.com/pricing",
"r": "https://google.com/",
"b": "Chrome",
"v": "120",
"d": "desktop",
"o": "macOS",
"vc": "wide",
"c": "4g",
"ttfb": 320,
"fcp": 1100,
"lcp": 1820,
"inp": 140,
"cls": 0.05,
"errs": [ { "t": "TypeError", "n": 1, "m": "...", "fr": "..." } ],
"tv": "260508-a1b2c3d",
"dlv": "b"
}You don't usually need to read this; the dashboard and the Analytics API unpack it for you. It's documented here for transparency.
Key abbreviations:
sid: session id. Lives insessionStorage._fms. Only emitted whencollect_sessionsis on (on load, or aftergrantConsent(), persession_consent).pvid: per-pageview id. New on every pageview, not persisted.ver: monotonic version of this pageview's payload.lcy: lifecycle marker, one ofinit,loaded,error_update,fetch_xhr_update,softnav,hidden,frozen,terminated. (Cached older bundles send it as the longlifecyclekey, which the collector also accepts.)pst: page-started timestamp, visitor wall-clock (ms since epoch).cst: client-sent-at timestamp, visitor wall-clock at flush. Used server-side to detect clock skew against receive time.u,r: page URL and referrer, both pre-sanitized at the tracker to origin + pathname (query and hash dropped).ris classified server-side into bucketed source + medium and the raw value is dropped before any logging path. It never persists, in any mode.us,um,uc,ut,un,ci: campaign attribution (utm source / medium / campaign / term / content, and the click-ID platform label) extracted fromwindow.location.search. Each capped at 64 chars.ciis the platform label only ("gclid"/"fbclid"/ …); the click-ID value is dropped by the tracker. Emitted in every configuration, and omitted entirely when empty.b,v,d,o,vc,c: browser family, browser major, device class, OS family, viewport class, connection type. The first four come fromFM-*edge headers;vcandccome from the browser.rtt/dljoin the connection group as real-time RTT and downlink estimates.pre,ad,vd: prerender flag (Speculation Rules API), activation delta (ms, prerendered pages only), and visibility-dirty (was the tab backgrounded during the pageview).- Metric keys are short mnemonics:
ttfb,fcp,lcp,inp,cls, page load (plt), cache hit rate (chr), the per-type resource and third-party rollups, and the per-script Long Animation Frame aggregates. They were previously opaque numbered slots (t1…t36); cached older bundles still send those names, which the collector maps onto the same fields. errs: error entries (ttype,ncount,mmessage,frframes).tandnship in every configuration, and so does each frame's host (plus the path and line/column for frames in your own first-party external scripts). The messagemis opt-in viacollect_error_messages, and the path/line/column of inline and third-party frames viacollect_error_frames; even thenmruns through a scrubber that masks URLs, emails, IDs, and quoted strings.snl: Route Load for soft navigations (soft_nav_load): the max end-offset of the route's own same-site requests. A network proxy for SPA route-load, deliberately never merged into LCP/FCP;nullon hard loads and when the route fired no qualifying request.tv: tracker version, formatYYMMDD-<git-sha>[-dirty].dlv: delivery method (bforsendBeacon, empty forfetch).- Fetch/XHR and
Server-Timing: per-endpoint Fetch/XHR aggregates (call counts, durations, duration buckets, status-error counts) and the page'sServer-Timingmetrics. Both are templated and normalized server-side (the wire is forgeable), so paths carry no query strings or raw IDs.
Soft navigations (SPA route changes) produce no native paint or navigation-timing entries, so their loading vitals come from two places: the snl network proxy (every browser) and, on Chromium once its Soft Navigations API ships, real LCP/FCP written into the usual lcp/fcp keys and attributed to the route.
Cache and rollouts
The script is served with strong caching and an ETag. Two things invalidate the cache automatically:
- Setting change. Changing a setting that affects the served bundle (for example
collect_sessions) ships a new template version. - Beacon upgrade. Fastmon ships beacon versions (
source_version); changing a site's pinned version rolls the cache.
You don't need to redeploy your site to take a beacon upgrade. The browser picks up the new version on its next cache check.
Customer-facing JS API
The tracker reads a few namespace-stable globals off window.
Grant consent
When collect_sessions is on with session_consent left at off, the session-id write is deferred until your CMP grants consent:
// In your cookie banner's "Accept" callback, and on every page load
// while consent is granted (most consent managers do this for you):
window.fastmon && window.fastmon.grantConsent && window.fastmon.grantConsent();grantConsent() mints the sessionStorage._fms session id and unlocks cross-page linkage from that point on. It's idempotent, and there's no mid-session revoke: the id lives in sessionStorage and dies with the tab, so declining just means the write never happens.
Fine-tuning
By default the tracker keeps request volume low: init fires immediately as a pageview-exists marker, loaded follows once LCP is final, mid-page-view updates fire only on a meaningful signal (a new first-party endpoint, a failure, or a slow call), and a polling page's update beacons are capped. A typical pageview sends two to four beacons. For pages that need to change any of that, it reads a few window globals. Most sites never touch them; set a value before the <script> tag so the first beacon respects it.
| Global | Default | Effect |
|---|---|---|
__FASTMON_SAMPLE_RATE | 1 | Fraction of page-views that send beacons (0 to 1). 0 skips a page-view entirely. The decision is per page-view; a sampled page-view still sends all of its lifecycle beacons. |
__FASTMON_INIT_COALESCE_MS | 0 | Delay before the init beacon. At 0 (the default) init fires immediately, so a pageview that dies before any other beacon (a bot, a crash, an abrupt close) still registers. A positive value re-delays it. |
__FASTMON_LCP_FINAL_CAP_MS | 10000 | How long the tracker waits for LCP to settle before it sends loaded, when no interaction or tab-hide has finalized LCP first. 0 disables the cap. |
__FASTMON_UPDATE_MIN_AGE_MS | 10000 | Holds mid-page-view update beacons (errors, fetch/XHR) until the page is at least this old; a shorter visit ships them on the final beacon instead. 0 fires them immediately. |
__FASTMON_MAX_VERSION | 20 | Caps mid-page-view update beacons per page-view, so a polling page can't fire thousands. Lifecycle beacons are never capped, and the cumulative aggregates still land on the final beacon. 0 removes the cap. |
Tracker settings
What the beacon does is controlled per site by its tracker settings: whether a session id, error message, error frame detail, query keys, fetch/XHR telemetry, or the edge stitch are present. The three starting points are the presets (Minimal, Standard, Full). The privacy guarantees in Privacy hold under every configuration.
Rate limiting
The collector rate-limits per (collector_hash, client_ip). The IP is read at the edge for that bucket only; the application sees a pre-derived bucket key, not the raw IP, and the IP is not written to disk. It's defensive: a broken page or a synthetic load test shouldn't saturate ingestion for other tenants. Real traffic basically never hits it.
A rate-limited beacon gets a 429. The browser drops its buffer and carries on; nothing else breaks.
Payloads larger than 8 KiB return 413. If cst is more than 5 minutes off the server clock, the server falls back to its own receive time instead of rejecting; the beacon is still accepted.
Things worth knowing
- SPA route changes start a new pageview. The script stays loaded, but soft-navs (patched
history.pushState+popstate) mint a newpvid, reset the Web Vitals observers, and flush asoftnavbeacon for the previous pageview. So one tab can emit many pageviews. - Multiple flushes per pageview. The buffer flushes on lifecycle triggers (
init,loaded,error_update,softnav,hidden,frozen,terminated), not just on tab switch. Expect more than onePOST /c/per visit.
Related
- Implementation: how to embed the beacon.
- Tracker settings: what the beacon collects, per setting.
- Presets: the three starting points.
- Privacy: what we don't collect: what we guarantee.
- Architecture: what happens after the POST.