These docs are a work in progress: some pages may still be incomplete or contain inaccuracies.
fastmon Docs
Concepts

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:

BuildWhenStorage on device
Statelesscollect_sessions offNone; the sessionStorage write is compiled out entirely
Statefulcollect_sessions onsessionStorage._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

SourceWhat's captured
PerformanceObserverLCP, INP, CLS, FCP, paint entries, long animation frames, layout shifts.
Navigation TimingTTFB, redirect count, navigation type (navigate, reload, back_forward).
Resource TimingPer-resource size, duration, and category. Aggregated to per-domain rollups.
error eventType, 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.
unhandledrejectionSame shape as error.
Page Visibility APITime-to-hidden, total visible time.
fetch / XMLHttpRequestPer-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-TimingThe Server-Timing metrics your own backend reports (name and duration), first-party by construction.
Document & WindowPage 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_sessions is on (then the tab-scoped session id). The per-pageview pvid exists 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 in sessionStorage._fms. Only emitted when collect_sessions is on (on load, or after grantConsent(), per session_consent).
  • pvid: per-pageview id. New on every pageview, not persisted.
  • ver: monotonic version of this pageview's payload.
  • lcy: lifecycle marker, one of init, loaded, error_update, fetch_xhr_update, softnav, hidden, frozen, terminated. (Cached older bundles send it as the long lifecycle key, 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). r is 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 from window.location.search. Each capped at 64 chars. ci is 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 from FM-* edge headers; vc and c come from the browser. rtt / dl join 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 (t1t36); cached older bundles still send those names, which the collector maps onto the same fields.
  • errs: error entries (t type, n count, m message, fr frames). t and n ship 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 message m is opt-in via collect_error_messages, and the path/line/column of inline and third-party frames via collect_error_frames; even then m runs 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; null on hard loads and when the route fired no qualifying request.
  • tv: tracker version, format YYMMDD-<git-sha>[-dirty].
  • dlv: delivery method (b for sendBeacon, empty for fetch).
  • Fetch/XHR and Server-Timing: per-endpoint Fetch/XHR aggregates (call counts, durations, duration buckets, status-error counts) and the page's Server-Timing metrics. 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.

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.

GlobalDefaultEffect
__FASTMON_SAMPLE_RATE1Fraction 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_MS0Delay 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_MS10000How 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_MS10000Holds 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_VERSION20Caps 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 new pvid, reset the Web Vitals observers, and flush a softnav beacon 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 one POST /c/ per visit.

On this page