New Incident Management + Host Exposure Scanning — Shipped April 12
Services Pricing Dashboard
← All help & documentation

Real-User Monitoring & Web Analytics

A 3 KB snippet that captures pageviews, Web Vitals, and custom events from real visitors — without cookies, fingerprinting, or shipping personal data.

What you get

One snippet covers three signals at the same time:

  • Web analytics — pageviews, sessions, bounce rate, referrers, UTM attribution, country, browser, device, and OS breakdowns.
  • Web Vitals (RUM) — real-user LCP, INP, CLS, TTFB, and FCP measurements, summarised at p75 with Good / Needs Improvement / Poor banding.
  • Custom events — named user actions you fire from your own JavaScript: signups, button clicks, plan upgrades, errors, anything you want to count.

The dashboard for all of this lives at /monitor/rum.

Install the snippet

  1. Go to /monitor/rum and create a site (give it a name and the domain you'll deploy on).
  2. Copy the install snippet shown on the site detail page.
  3. Paste it just before </head> on every page you want tracked.

It looks like this:

<script async src="https://servicealert.ai/sa.js"
        data-site-key="YOUR-SITE-KEY"></script>

That's it. The script is async, ~3 KB minified, has no dependencies, and starts sending pageviews immediately. SPA route changes (history.pushState) are detected automatically — no extra code needed.

Verify: open your site, then load /monitor/rum and look at the live pill on the site tile. It should show 1 within ~30 seconds.

Custom events

Pageviews come in automatically. Custom events are named actions you fire yourself, for things that don't have their own URL: button clicks, form submits, plan upgrades, errors, milestones.

API

window.sa('event', '<event_name>', { /* optional props */ });
  • event_name — required, a short string up to 128 chars (e.g. signup_click, checkout_complete).
  • props — optional object with up to ~20 small key/value pairs. Use it to slice events later (plan, variant, source, etc.).

Examples

// Fire on a signup button click
document.querySelector('#signup').addEventListener('click', () => {
  window.sa('event', 'signup_click', { plan: 'business' });
});

// Fire when a user completes the checkout flow
window.sa('event', 'checkout_complete', { revenue: 49, currency: 'USD' });

// Fire when a feature flag is gated
window.sa('event', 'paywall_hit', { feature: 'export_csv' });

// Fire on a JS error you've caught
window.sa('event', 'js_error', { message: err.message.slice(0, 80) });

What to instrument first

You don't need a long list. Start with the moments that matter to your business and add more as you learn. Common ones:

  • Top of funnelsignup_click, pricing_cta_click, video_play
  • Activationonboarding_step (with a step prop), first_action_done
  • Conversioncheckout_started, checkout_complete, plan_upgraded
  • Engagementexport_csv, share_clicked, search_submitted
  • Qualityjs_error, api_failed, slow_render
Naming: stick to snake_case. Same casing across the site means your top-events panel doesn't split SignupClick and signup_click into separate rows.

UTM tracking & referrer attribution

Any pageview whose URL has ?utm_source / ?utm_medium / ?utm_campaign query parameters is automatically attributed in the UTM Sources panel. No code change needed.

The Top Referrers panel uses the browser-supplied document.referrer hostname. Cross-domain entries become first-class rows; same-origin internal navigation is grouped as (direct).

AI-assistant traffic shows up here. ChatGPT and Microsoft Copilot append ?utm_source=chatgpt.com (or copilot.com) when they cite a URL, so AI-driven traffic naturally appears in your UTM panel without any extra setup.

Web Vitals (RUM)

The same snippet collects Core Web Vitals from real visitors using Google's web-vitals library:

  • LCP — Largest Contentful Paint. Good ≤ 2500 ms.
  • INP — Interaction to Next Paint. Good ≤ 200 ms.
  • CLS — Cumulative Layout Shift. Good ≤ 0.1.
  • TTFB — Time to First Byte. Good ≤ 800 ms.
  • FCP — First Contentful Paint. Good ≤ 1800 ms.

The site detail page shows the p75 of each metric across all visitors over the selected range, with Good / Needs Improvement / Poor banding using the standard web.dev thresholds. P75 (rather than mean or median) matches what Chrome's Core Web Vitals report grades against.

Web Vitals beacons are aggregated nightly from raw measurements into a daily rollup. Today's bar reflects events received so far in the current UTC day; trend bars older than that are stable.

Bot filtering & accuracy

The ingest endpoint drops beacons whose User-Agent matches a known bot signature before the row is written. This mirrors Umami's isbot() check and means your Visitors and Pageviews reflect humans, not crawlers. Categories filtered:

  • Search-engine crawlers — Googlebot, Bingbot, DuckDuckBot, YandexBot, Baidu, Sogou.
  • AI agents — GPTBot, ClaudeBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Bytespider, Anthropic, Cohere, Applebot, Amazonbot.
  • SEO crawlers — AhrefsBot, SemrushBot, MJ12bot, DotBot, BLEXBot, MegaIndex.
  • Social previewers — FacebookBot, LinkedInBot, Twitterbot, TelegramBot, WhatsApp, Slackbot, Discordbot.
  • Headless / scripted clients — HeadlessChrome, Puppeteer, Playwright, Selenium, PhantomJS, jsdom.
  • HTTP libraries — curl, wget, python-requests, aiohttp, httpx, axios, node-fetch, Go-http-client, java HTTP, Scrapy, HTTPie.
  • Uptime monitors — UptimeRobot, StatusCake, Pingdom, Nagios.
  • Security scanners — Nuclei, Nessus, Nikto, sqlmap, w3af.

Anything with a UA containing the substrings bot, crawl, spider, fetcher, or monitor is also dropped. AI-assistant referrals where the user is a real human (e.g. someone who clicked a ChatGPT citation in a normal browser) are not filtered — they still report a normal Chrome / Firefox / Safari UA and the pageview is counted, with the source attributed via ?utm_source=chatgpt.com.

Compared to Umami: our filter list is curated rather than the full ~3,000-entry isbot list, so it's tighter and faster. If you see specific bot UAs slip through, send them and we'll add them.

Privacy posture

The snippet is designed so you don't need a cookie banner.

  • No cookies set or read — client-side or server-side.
  • No fingerprinting — no canvas hashing, no font enumeration, no audio context, no WebGL probes.
  • No raw User-Agent shipped to your account — the server classifies it once and stores only coarse buckets like chrome, firefox, mobile, desktop.
  • No raw IP stored — IPs are mapped to a country code at the ingest edge and discarded; only the country is persisted.
  • Anonymous sessions — visitor sessions are attributed via a daily-rotating server-side salt the snippet never sees, so two visits separated by 24 hours can't be correlated even on the server.

This matches the privacy posture of Plausible, Fathom, and Umami. It's compatible with GDPR, CCPA, and TTDSG without explicit consent prompts in most jurisdictions, but always review your own legal context.

Reading the dashboard

The site detail page (/monitor/rum → click a tile) groups everything into one view:

  • Live pill — unique sessions with any beacon in the last 5 minutes.
  • Range toggleToday uses your local timezone (not UTC). 24h is rolling. 7d / 30d are calendar windows.
  • Pageviews / Visitors / Visits — visits are split by 30-minute inactivity gaps, matching the GA / Umami definition (a tab left open for 8 hours doesn't count as one giant visit).
  • Bounce rate — share of visits with exactly one pageview.
  • Avg visit time — mean time between first and last beacon within a visit. Bounces contribute 0, dragging the average down (matches Umami / GA).
  • Top pages / referrers / countries / browsers / devices — classic breakdowns; rolling totals over the selected range.
  • UTM Sources — only rows with at least one UTM parameter set; (none) renders for the missing pieces.
  • Custom Events — ranked by occurrences, with sessions in which the event fired at least once.

Troubleshooting

Pageviews aren't showing up

  1. Open DevTools → Network and reload your page. Filter for sa.js and send. You should see sa.js load (200) and one POST to /api/analytics/send (or /api/send) per pageview.
  2. If sa.js is missing, check that the snippet is in the rendered HTML (some CMSs strip <script> tags from drafts).
  3. If the POST returns 401, the data-site-key doesn't match a site you own. Double-check the key on /monitor/rum.

Pageviews show up locally but not in production

Some content blockers (uBlock + EasyPrivacy, AdGuard, Brave shields) match the analytics endpoint. The snippet sends beacons via fetch() with keepalive: true rather than navigator.sendBeacon specifically because the latter is classified as a $ping resource that EasyPrivacy blocks. Expect 5–15% of real visitors to be silently dropped by privacy extensions; that's normal and matches the industry baseline.

Custom events not appearing

Confirm the call is actually firing: in DevTools console, type window.sa — you should see a function. Then call it manually:

window.sa('event', 'manual_test')

The Custom Events panel buckets events server-side, so it can take 30–60s for a brand-new event name to appear in the list.

I'm running Umami today — can I migrate without changing my markup?

Yes. analytics.servicealert.ai/script.js is served by the same backend, so an existing Umami install whose DNS points analytics. at us starts using ServiceAlert RUM transparently. The site key replaces the Umami website ID via data-website-id.

Ready to instrument your site? Real-User Monitoring is included on all paid plans.

Open the RUM dashboard