Privacy: what we don't collect
Fastmon's privacy posture, line by line. Useful when your legal team asks.
Fastmon is built so you can monitor real-user performance without walking into a privacy minefield. The short version: no IPs on disk, no raw User-Agent, no fingerprinting, no cross-site tracking, EU-only infrastructure, zero cookies, and a storage-free default. This page is the long version: what each of those promises means in practice, what we do collect so the product works, and how that changes with the site's tracker settings.
What we never collect, whatever the settings
These rules hold regardless of how the site is configured:
| We do not collect… | What that means in practice |
|---|---|
| IP addresses | The edge strips the IP at the network boundary before any application code sees the request. The backend reads only a pre-derived country code (FM-Country, 2-letter ISO), never the IP itself. No IP is written to disk, ever. |
| Raw User-Agent string | The edge also strips the User-Agent header at the network boundary. The backend reads only pre-bucketed FM-Browser / FM-Browser-Version / FM-OS / FM-Device headers. The raw UA is never visible to the application or its logs. |
| Device or browser fingerprints | No canvas fingerprinting, no font enumeration, no userAgentData high-entropy hints, no battery / sensor / audio-context probing. |
| Cross-site identifiers | We mint nothing that survives a navigation across domains. A session on acme.com cannot be linked to one on other.com. |
| Third-party data | We don't enrich with data brokers, ad networks, or social platforms. There is no "audience" data. |
| Page content | We don't read DOM content, form values, scroll positions, or session-replay frames. There is no recording. The one carve-out is opt-in pagetype_ruleset, which reads a single body controller-class token to classify the page type; off by default. |
| Query-parameter values and URL fragments | The URL is cut to origin + path at the tracker. The #… fragment is always dropped, and query values are dropped. /orders/checkout?email=jane@x.com arrives as /orders/checkout?email under the default Standard preset (the name email kept via collect_query_keys, the value stripped), or as /orders/checkout on Minimal. A value is stored only for keys you explicitly allow-list in collect_query_values_for. |
| Personally identifiable information | Nothing the visitor types, clicks on, or has in their browser is captured. |
A note on cookies and storage
fastmon sets no cookies at all, in any configuration. The only thing the tracker can write to the visitor's device is a single sessionStorage entry (_fms), the session id, and only when collect_sessions is on. By default it writes nothing: the Standard preset new sites start on stores no session id, so no cookies, no localStorage, no sessionStorage, no IndexedDB, no Cache API.
Storage-free is about what the beacon keeps on the device, not about whether consent applies: reading the browser's performance APIs can be consent-relevant regardless. See Consent and the TDDDG.
When collect_sessions is on, whether the session id is written on page load or deferred until your CMP grants consent is the session_consent switch. The id lives in sessionStorage and is gone when the tab closes. There is no persistent identifier and no cross-page cookie anywhere in the design.
What we collect
Just enough to compute Web Vitals and per-page performance breakdowns. The exact field-by-field list is in The RUM Beacon; the categories:
- Page URL: origin + pathname, plus query-parameter names when
collect_query_keysis on (the default Standard preset). The hash fragment and query values are dropped at the tracker; a value is kept only for keys you allow-list (collect_query_values_for). - Web Vitals: LCP, INP, CLS, FCP, TTFB, and related timings.
- Navigation timing: nav type, TTFB, page-load timing.
- Resource timing: per-category byte sizes and counts (script, css, img, font, other).
- Errors: type and count always. Keeping the error message is opt-in (
collect_error_messages) and even then runs through an always-on scrubber that masks URLs, emails, numeric IDs, and quoted strings with*before transmission. Stack-frame detail is opt-in (collect_error_frames); with it off, frames collapse toward host-only, except your own first-party external scripts, which always keep the path and line/column so an own-code bug stays locatable. That location is identical for every visitor who hits the bug, so it identifies the error, not the person. - Environment: allowlisted browser family, OS family, device class (
mobile/tablet/desktop), viewport class, effective connection type, network round-trip and downlink estimates. The network values are real-time measurements the browser computes on read, not stored device or OS state. We don't readnavigator.deviceMemory,navigator.hardwareConcurrency, thesaveDataflag,prefers-color-scheme, orprefers-reduced-motionat all. - Geo: country only (2-letter ISO). Derived at the edge from the IP and forwarded as a request header; the IP itself never reaches the application.
- Campaign attribution:
utm_source/utm_medium/utm_campaign/utm_term/utm_contentand an ad-platform-labelclick_id_param(e.g."gclid") extracted from the page URL. Each capped at 64 chars. The click-ID value (Cj0KCQabc...) is intentionally discarded; only the label of which platform sent the click is stored. These are marketer-set tags on the customer's own outbound URLs, not visitor-derived state. - LCP image URL, LoAF script paths, third-party domain map: collected in every configuration, sanitized to origin + pathname at collection. These describe the customer's own page composition (see Site metadata vs. visitor data), not the visitor.
- Fetch/XHR aggregates: opt-in (
collect_fetch_xhr). Per-endpoint call counts, durations, and error codes for your own APIs. Paths are sanitized harder than page URLs (query, fragment, and id-like segments removed). - Session ID: only when
collect_sessionsis on (the Full preset, or after your CMP callsgrantConsent()). Lives insessionStorage, groups pageviews of the same session together, dies with the tab.
That's the whole payload. There's no opt-in we can offer to "unlock" more. We didn't build the more.
The edge-derived stitch identifier
For unique-visitor counts and per-visitor performance debugging without storing anything on the device, the edge can compute a one-way join key before the IP and User-Agent are destroyed:
stitch = HMAC-SHA256(rotating_salt, IP | User-Agent | collector_hash | domain)truncated to 128 bits (32 hex chars on the wire). It is stored whenever store_stitch is on, which includes the default Standard preset; the Minimal preset is the opt-down that leaves the row with no visitor identifier at all. Properties when it is on:
- Forward secrecy by construction. The salt is a 32-byte
crypto/randvalue held only in the active edge's RAM and rotated every 24 hours. It is never persisted, never distributed, never logged. An attacker who later gains row access cannot reversestitchback to (IP, UA): the salt that produced it is gone. - Bounded joinability, at most ~24h. Across a salt rotation the same visitor gets a new
stitchvalue. Cross-pageview linking cannot accumulate beyond a single salt window. - Per-domain, never cross-site. Both
collector_hashand the pagedomainare in the HMAC input, so the same (IP, UA) yields a differentstitchon every domain, including across the domains served by a single application. Nothing links a visitor across domains or across sites; the "no cross-site identifier" row above is unchanged.
Classify it honestly: while the salt lives, stitch is a pseudonymous visitor identifier and therefore personal data, processed under Art. 6 (1)(f) GDPR with a documented legitimate-interest assessment. Because it is on under the default Standard preset, name it in your privacy notice. If you'd rather store no visitor identifier at all, turning store_stitch off (the Minimal preset) keeps the row genuinely anonymous.
Turning identifiers off, or leaving them off
Two per-site switches decide whether any visitor identifier reaches storage. Under the default Standard preset store_stitch is on and collect_sessions is off; the Minimal preset turns both off.
store_stitch. On stores the edge-derivedstitchfor unique-visitor counts and visitor-level metrics (bounce rate, pages per visit, dwell time). Off leaves a row with no visitor identifier at all, fully anonymous. Every per-pageview signal (Web Vitals, errors, third-party, attribution) is unaffected either way.collect_sessions. On writes the tab-scopedsessionStoragesession id for cross-page journeys and funnels. Off means the served bundle never touchessessionStorageat all: the on-device write is skipped, not merely dropped server-side.
The two are independent: enable either, both, or neither.
Site metadata vs. visitor data
A handful of fields describe the customer's own site rather than the visitor: which third-party domains the page loads, the script / image / font byte breakdown, the LCP image origin, the LoAF script paths. These are identical across every visitor of the same page. They tell you which of your CDNs is slow; they do not identify anyone.
That makes them site configuration, not visitor data. We still treat them carefully (script URLs and LCP image URLs are sanitized to origin + pathname at collection), but they're not what the consent and linkability controls are about.
Personal data: what's ruled out, and what isn't
- No personal data, in every configuration. Web Vitals and all timing, resource / LoAF / third-party metadata, device / connection / viewport buckets, the 2-letter country, and error type + count. No IP, no raw User-Agent, no fingerprint is ever stored. On the Minimal preset the stored row carries no visitor identifier at all.
- Can't be fully ruled out; we minimize, and the residual is yours to control. The edge-derived
stitch(on under the Standard preset) is pseudonymous personal data by design. URL and fetch/XHR paths are sanitized (query, fragment and values stripped), but a segment you build from personal data (/orders/jane-doe) survives. Error messages (collect_error_messages) run through a scrubber that masks patterned PII but can't catch every free-text leak. And anything you opt into: an allow-listedcollect_query_values_forvalue, or thecollect_sessionsid. These last cases are covered by your responsibilities below.
What your settings change
The tracker settings decide linkability and content depth, not the privacy floor. The floor above (no IP storage, no raw User-Agent, no fingerprints, no cookies) holds in every configuration. What the settings move is whether a session id, error message, error frame detail, query keys, or fetch/XHR telemetry are present, and whether the edge stitch is stored. See Tracker settings for the per-setting breakdown, and presets for the three starting points.
Consent and the TDDDG
Whether embedding fastmon needs consent, who is responsible, and the two legal readings are covered in full on Tracker settings → Consent and the TDDDG.
Where the data lives
- EU only, no US jurisdiction. All data is processed and stored exclusively in the EU, on infrastructure operated in Germany. Every provider in the data path is a German or EU company: no Cloudflare, no AWS, no GCP, no Vercel, no US-owned subprocessor.
- Operator. Data is processed by fastmon, the company behind this product. The DPA lists every sub-processor and goes out on request.
- Retention. 90 days by default. Configurable per site (
data_retention_days). When retention runs out, rows get dropped. We don't archive them.
No US CLOUD Act exposure
Because neither fastmon nor any subprocessor falls under US law, the US CLOUD Act has no reach over your RUM data.
Comparison
If you're moving from a US analytics product, here's the relevant delta:
| Capability | fastmon | Typical US RUM provider |
|---|---|---|
| Cookies set | 0 | 1–N (analytics + ad partners) |
| IP stored | No | Often, sometimes truncated |
| Cross-site identity graph | No | Often, via SDK or pixel network |
| Session replay / DOM dump | No | Common opt-in feature |
| EU-only data path | Yes | Rare |
| Data leaves the EU | No | Often (US CLOUD Act) |
Your responsibilities
The tracker enforces the wire shape. Two things sit on your side of the line:
- No PII in URL paths. Query strings and hash fragments are stripped, but the pathname is preserved.
/orders/jane.doe@x.comwould carry the email into our pipeline. Use opaque IDs (UUIDs, numeric DB IDs) or put PII into query parameters that get stripped. - No PII in UTM parameters. UTM tags and click-ID labels land in the database verbatim from the marketer's outbound URLs. Don't embed visitor PII. Never construct campaign URLs like
?utm_term=jane@example.com&utm_content=user-12345for per-recipient tracking. Each field is server-side capped at 64 chars, but the value is not pattern-scrubbed. Ad-platform click-ID values are dropped by the tracker, so the per-recipient join risk applies only to the UTM fields. - Don't pass raw user input to
throw. The always-on scrubber catches patterned PII (URLs, emails, IDs), but free-form text (names, custom phrases) can slip through. Type the failure instead:throw new EmailDeliveryError({ code: "..." })rather thanthrow new Error("Failed to send to " + email). Only matters whencollect_error_messagesis on; otherwise the entire message is dropped.
What this means for you
- Consent is the operator's call, whatever the settings. Whether reading the browser's performance APIs needs consent depends on your deployment and your own assessment (see Consent and the TDDDG); if it does, you gate the beacon behind your CMP.
- Your data never leaves the EU. No US transfer to reason about in the first place.
- You can hand your team a one-page answer: point them at this page and our DPA, done.
- Want explicit opt-in before any session linkage? Turn on
collect_sessionswithsession_consentleft atoff, so the session id is written only after your CMP callsgrantConsent().
What's next
- Review the tracker settings and presets.
- Read about the beacon for the technical detail.
- Want the DPA? Email support@fastmon.eu.