Monitoring Web Applications: RUM and Synthetic Monitoring
In this tutorial, you'll learn about Monitoring Web Applications: RUM and Synthetic Monitoring. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
What You Will Learn
This tutorial teaches you how to monitor web application performance using Real User Monitoring (RUM) for actual visitor data and synthetic monitoring for proactive testing, covering Core Web Vitals, page load metrics, and API performance.
Why It Matters
Users judge your application by how fast it loads and responds. A one-second delay in page load time can reduce conversions by 7%. Without web monitoring, you are unaware of performance problems that directly impact user experience and revenue.
Real-World Use
The Doda Browser team uses RUM to track Core Web Vitals across all browser versions. When a new release caused a 20% regression in First Input Delay, the RUM data caught it within hours of the rollout, and the team reverted the offending change before the weekly release cycle.
Web application monitoring splits into two categories: Real User Monitoring (RUM) captures data from actual users loading your site in their browsers. Synthetic monitoring runs scripted tests from controlled environments to proactively detect issues before users do. Both are essential for a complete picture.
Prerequisites
- A web application to monitor (your own or a test site)
- Node.js 18+ installed
- A Linux server or local development machine
- Basic knowledge of HTML and JavaScript
Step-by-Step Tutorial
Step 1: Set Up a Simple Web Application
Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DodaTech Demo App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Welcome to DodaTech</h1>
<button id="load-data">Load Data</button>
<div id="content"></div>
<script src="app.js"></script>
</body>
</html>
Create app.js:
document.getElementById("load-data").addEventListener("click", async () => {
const start = performance.now();
const response = await fetch("/api/data");
const data = await response.json();
const duration = performance.now() - start;
document.getElementById("content").textContent =
`Loaded ${data.items} items in ${duration.toFixed(0)}ms`;
});
Step 2: Add RUM with the Performance API
// Send performance data to your analytics endpoint
function sendPerformanceMetrics() {
if (!performance.getEntriesByType) return;
const navigation = performance.getEntriesByType("navigation")[0];
const paint = performance.getEntriesByType("paint");
const metrics = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ttfb: navigation.responseStart - navigation.requestStart,
dom_load: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
full_load: navigation.loadEventEnd - navigation.loadEventStart,
fcp: paint.find(p => p.name === "first-contentful-paint")?.startTime,
};
navigator.sendBeacon("/api/rum", JSON.stringify(metrics));
}
window.addEventListener("load", sendPerformanceMetrics);
Step 3: Collect Web Vitals with the web-vitals Library
npm install web-vitals
import { onCLS, onFID, onLCP, onINP } from "web-vitals";
function sendToAnalytics({ name, value, rating }) {
const body = JSON.stringify({ name, value, rating, url: location.href });
navigator.sendBeacon("/api/vitals", body);
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
Step 4: Create a RUM Collection Endpoint
from flask import Flask, request
import json
import time
app = Flask(__name__)
@app.route("/api/rum", methods=["POST"])
def collect_rum():
data = request.get_json()
data["timestamp"] = time.time()
print(f"RUM data: {json.dumps(data)}")
return {"status": "ok"}
@app.route("/api/vitals", methods=["POST"])
def collect_vitals():
data = request.get_json()
data["timestamp"] = time.time()
print(f"Web Vital: {json.dumps(data)}")
return {"status": "ok"}
if __name__ == "__main__":
app.run(port=5000)
Step 5: Set Up Synthetic Monitoring with Playwright
npm init -y
npm install @playwright/test
npx playwright install chromium
Create synthetic-test.js:
const { chromium } = require("@playwright/test");
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const start = Date.now();
await page.goto("https://your-app.dodatech.com", { waitUntil: "networkidle" });
const loadTime = Date.now() - start;
const metrics = await page.evaluate(() => ({
lcp: performance.getEntriesByType("paint")
.find(p => p.name === "first-contentful-paint")?.startTime,
cls: performance.getEntriesByType("layout-shift")
?.reduce((sum, e) => sum + e.value, 0),
}));
console.log({ loadTime, ...metrics });
await browser.close();
})();
Step 6: Run Lighthouse CI for Performance Scoring
npm install -g @lhci/cli
Create .lighthouserc.json:
{
"ci": {
"collect": {
"url": ["https://your-app.dodatech.com"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["warn", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}
lhci autorun
Expected output: A Lighthouse report with performance, Accessibility, best practices, and SEO scores.
Step 7: Set Up API Monitoring
const https = require("https");
function checkEndpoint(url, expectedStatus = 200) {
const start = Date.now();
https.get(url, (res) => {
const duration = Date.now() - start;
const status = res.statusCode === expectedStatus ? "pass" : "fail";
console.log(JSON.stringify({
url, status, duration_ms: duration, timestamp: new Date().toISOString()
}));
});
}
setInterval(() => {
checkEndpoint("https://api.dodatech.com/health");
checkEndpoint("https://api.dodatech.com/v1/users");
}, 60000);
Step 8: Alert on Web Performance Degradation
# RUM: FCP above 2.5 seconds
avg by (page) (rum_first_contentful_paint) > 2500
# Synthetic: Lighthouse performance score below 85
lighthouse_performance_score < 85
# Synthetic: API response time above 500ms
avg by (endpoint) (synthetic_api_duration_ms) > 500
Learning Path
flowchart LR
A[Web App Monitoring] --> B[Real User Monitoring]
A --> C[Synthetic Monitoring]
B --> D[Core Web Vitals]
B --> E[Page Load Metrics]
B --> F[API Performance]
C --> G[Playwright Tests]
C --> H[Lighthouse CI]
C --> I[API Health Checks]
D --> J[Dashboards & Alerts]
G --> J
H --> J
style A fill:#4a90d9,color:#fff
style J fill:#e67e22,color:#fff
Common Errors
RUM data is blocked by ad blockers -- Ad blockers may block analytics endpoints. Use
/api/rumpaths that do not match common block patterns.Web Vitals library returns null for some metrics -- The browser does not support certain metrics or the interaction did not occur. Only LCP is guaranteed; CLS and INP require user interaction.
Playwright test times out -- The page does not reach
networkidledue to persistent connections. Change wait strategy todomcontentloaded.Lighthouse CI reports lower scores than local tests -- The CI environment has fewer resources. Use the same hardware class as your production deployment for consistent results.
CORS errors from RUM endpoints -- The endpoint does not have proper CORS headers. Add
Access-Control-Allow-Origin: *to the collection endpoint.Synthetic tests fail due to dynamic content -- The test expects specific text that changed after a deployment. Use stable selectors instead of text content.
Performance metrics show high variance -- Network conditions and device capabilities vary. Collect enough samples (100+) before drawing conclusions.
Practice Questions
What is the difference between RUM and synthetic monitoring? Answer: RUM captures data from real users in production. Synthetic monitoring runs controlled tests from test environments to detect issues before users do.
What are Core Web Vitals? Answer: LCP (Largest Contentful Paint), FID (First Input Delay), and CLS (Cumulative Layout Shift) -- Google metrics that measure loading, interactivity, and visual stability.
How does the Performance API help with RUM? Answer: It provides timing data for DNS, TCP, TTFB, DOM loading, and paint events through
performance.getEntriesByType().What is the purpose of
navigator.sendBeacon()in RUM? Answer: It sends data to the server asynchronously without delaying page unload, ensuring metrics are captured even when the user navigates away.How does Lighthouse CI integrate into a deployment pipeline? Answer: It runs Lighthouse audits as part of CI, asserting performance budgets and preventing regressions before they reach production.
Challenge
Build a complete web monitoring stack for a single-page application. Add RUM instrumentation that captures LCP, CLS, and INP metrics and sends them to a collection endpoint. Deploy a synthetic monitoring script using Playwright that navigates through the three most important user flows (login, search, checkout) and reports timing for each step. Configure Lighthouse CI to run on every deployment and enforce a performance budget of 90+. Create a Grafana dashboard that shows: RUM FCP trend, synthetic page load time, Lighthouse score trend, and API error rate. Set up alerts for FCP exceeding 2.5 seconds and Lighthouse score dropping below 85.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro