Synthetic Monitoring
Overview
Set up synthetic monitoring to automatically simulate real user journeys, API workflows, and critical business transactions to detect issues and validate performance.
When to Use
- End-to-end workflow validation
- API flow testing
- User journey simulation
- Transaction monitoring
- Critical path validation
Instructions
1. Synthetic Tests with Playwright
// synthetic-tests.js
const { chromium } = require("playwright");
class SyntheticMonitor {
constructor(config = {}) {
this.baseUrl = config.baseUrl || "https://app.example.com";
this.timeout = config.timeout || 30000;
}
async testUserFlow() {
const browser = await chromium.launch();
const page = await browser.newPage();
const metrics = { steps: {} };
const startTime = Date.now();
try {
// Step 1: Navigate to login
let stepStart = Date.now();
await page.goto(`${this.baseUrl}/login`, { waitUntil: "networkidle" });
metrics.steps.navigation = Date.now() - stepStart;
// Step 2: Perform login
stepStart = Date.now();
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: "networkidle" });
metrics.steps.login = Date.now() - stepStart;
// Step 3: Navigate to dashboard
stepStart = Date.now();
await page.goto(`${this.baseUrl}/dashboard`, {
waitUntil: "networkidle",
});
metrics.steps.dashboard = Date.now() - stepStart;
// Step 4: Search for products
stepStart = Date.now();
await page.fill('input[placeholder="Search products"]', "laptop");
await page.waitForSelector(".product-list");
metrics.steps.search = Date.now() - stepStart;
// Step 5: Add to cart
stepStart = Date.now();
const firstProduct = await page.$(".product-item");
if (firstProduct) {
await firstProduct.click();
await page.click('button:has-text("Add to Cart")');
await page.waitForSelector('[data-testid="cart-count"]');
}
metrics.steps.addToCart = Date.now() - stepStart;
metrics.totalTime = Date.now() - startTime;
metrics.status = "success";
} catch (error) {
metrics.status = "failed";
metrics.error = error.message;
metrics.totalTime = Date.now() - startTime;
} finally {
await browser.close();
}
return metrics;
}
async testMobileUserFlow() {
const browser = await chromium.launch();
const context = await browser.createBrowserContext({
...chromium.devices["iPhone 12"],
});
const page = await context.newPage();
try {
const metrics = { device: "iPhone 12", steps: {} };
const startTime = Date.now();
let stepStart = Date.now();
await page.goto(this.baseUrl, { waitUntil: "networkidle" });
metrics.steps.navigation = Date.now() - stepStart;
const viewport = page.viewportSize();
metrics.viewport = viewport;
stepStart = Date.now();
await page.click(".menu-toggle");
await page.waitForSelector(".mobile-menu.open");
metrics.steps.mobileInteraction = Date.now() - stepStart;
metrics.totalTime = Date.now() - startTime;
metrics.status = "success";
return metrics;
} catch (error) {
return { status: "failed", error: error.message, device: "iPhone 12" };
} finally {
await browser.close();
}
}
async testWithPerformanceMetrics() {
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto(this.baseUrl, { waitUntil: "networkidle" });
const perfMetrics = JSON.parse(
await page.evaluate(() => JSON.stringify(window.performance.timing)),
);
const metrics = {
navigationTiming: {
domInteractive:
perfMetrics.domInteractive - perfMetrics.navigationStart,
domComplete: perfMetrics.domComplete - perfMetrics.navigationStart,
loadComplete: perfMetrics.loadEventEnd - perfMetrics.navigationStart,
},
status: "success",
};
return metrics;
} catch (error) {
return { status: "failed", error: error.message };
} finally {
await browser.close();
}
}
async recordMetrics(testName, metrics) {
try {
await axios.post("http://monitoring-service/synthetic-results", {
testName,
timestamp: new Date(),
metrics,
passed: metrics.status === "success",
});
} catch (error) {
console.error("Failed to record metrics:", error);
}
}
}
module.exports = SyntheticMonitor;
2. API Synthetic Tests
// api-synthetic-tests.js
const axios = require("axios");
class APISyntheticTests {
constructor(config = {}) {
this.baseUrl = config.baseUrl || "https://api.example.com";
this.client = axios.create({ baseURL: this.baseUrl });
}
async testAuthenticationFlow() {
const results = { steps: {}, status: "success" };
try {
const registerStart = Date.now();
const registerRes = await this.client.post("/auth/register", {
email: `test-${Date.now()}@example.com`,
password: "Test@123456",
});
results.steps.register = Date.now() - registerStart;
if (registerRes.status !== 201) throw new Error("Registration failed");
const loginStart = Date.now();
const loginRes = await this.client.post("/auth/login", {
email: registerRes.data.email,
password: "Test@123456",
});
results.steps.login = Date.now() - loginStart;
const token = loginRes.data.token;
const authStart = Date.now();
await this.client.get("/api/profile", {
headers: { Authorization: `Bearer ${token}` },
});
results.steps.authenticatedRequest = Date.now() - authStart;
const logoutStart = Date.now();
await this.client.post(
"/auth/logout",
{},
{
headers: { Authorization: `Bearer ${token}` },
},
);
results.steps.logout = Date.now() - logoutStart;
return results;
} catch (error) {
results.status = "failed";
results.error = error.message;
return results;
}
}
async testTransactionFlow() {
const results = { steps: {}, status: "success" };
try {
const orderStart = Date.now();
const orderRes = await this.client.post(
"/api/orders",
{
items: [{ sku: "ITEM-001", quantity: 2 }],
},
{
headers: { "X-Idempotency-Key": `order-${Date.now()}` },
},
);
results.steps.createOrder = Date.now() - orderStart;
const getStart = Date.now();
const getRes = await this.client.get(`/api/orders/${orderRes.data.id}`);
results.steps.getOrder = Date.now() - getStart;
const paymentStart = Date.now();
await this.client.post(`/api/orders/${orderRes.data.id}/payment`, {
method: "credit_card",
amount: getRes.data.total,
});
results.steps.processPayment = Date.now() - paymentStart;
return results;
} catch (error) {
results.status = "failed";
results.error = error.message;
return results;
}
}
async testUnderLoad(concurrentUsers = 10, duration = 60000) {
const startTime = Date.now();
const results = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
p95ResponseTime: 0,
};
const responseTimes = [];
const makeRequest = async () => {
const reqStart = Date.now();
try {
await this.client.get("/api/health");
results.successfulRequests++;
responseTimes.push(Date.now() - reqStart);
} catch {
results.failedRequests++;
}
results.totalRequests++;
};
const userSimulations = Array(concurrentUsers)
.fill(null)
.map(async () => {
while (Date.now() - startTime < duration) {
await makeRequest();
await new Promise((r) => setTimeout(r, Math.random() * 1000));
}
});
await Promise.all(userSimulations);
responseTimes.sort((a, b) => a - b);
results.averageResponseTime =
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
results.p95ResponseTime =
responseTimes[Math.floor(responseTimes.length * 0.95)];
return results;
}
}
module.exports = APISyntheticTests;
3. Scheduled Synthetic Monitoring
// scheduled-monitor.js
const cron = require("node-cron");
const SyntheticMonitor = require("./synthetic-tests");
const APISyntheticTests = require("./api-synthetic-tests");
const axios = require("axios");
class ScheduledSyntheticMonitor {
constructor(config = {}) {
this.eMonitor = new SyntheticMonitor(config);
this.apiTests = new APISyntheticTests(config);
this.alertThreshold = config.alertThreshold || 5000;
}
start() {
cron.schedule("*/5 * * * *", () => this.runE2ETests());
cron.schedule("*/2 * * * *", () => this.runAPITests());
cron.schedule("0 * * * *", () => this.runLoadTest());
}
async runE2ETests() {
try {
const metrics = await this.eMonitor.testUserFlow();
await this.recordResults("e2e-user-flow", metrics);
if (metrics.totalTime > this.alertThreshold) {
await this.sendAlert("e2e-user-flow", metrics);
}
} catch (error) {
console.error("E2E test failed:", error);
}
}
async runAPITests() {
try {
const authMetrics = await this.apiTests.testAuthenticationFlow();
const transactionMetrics = await this.apiTests.testTransactionFlow();
await this.recordResults("api-auth-flow", authMetrics);
await this.recordResults("api-transaction-flow", transactionMetrics);
if (
authMetrics.status === "failed" ||
transactionMetrics.status === "failed"
) {
await this.sendAlert("api-tests", { authMetrics, transactionMetrics });
}
} catch (error) {
console.error("API test failed:", error);
}
}
async runLoadTest() {
try {
const results = await this.apiTests.testUnderLoad(10, 30000);
await this.recordResults("load-test", results);
if (results.failedRequests > 0) {
await this.sendAlert("load-test", results);
}
} catch (error) {
console.error("Load test failed:", error);
}
}
async recordResults(testName, metrics) {
try {
await axios.post("http://monitoring-service/synthetic-results", {
testName,
timestamp: new Date(),
metrics,
});
console.log(`Recorded: ${testName}`, metrics);
} catch (error) {
console.error("Failed to record results:", error);
}
}
async sendAlert(testName, metrics) {
try {
await axios.post("http://alerting-service/alerts", {
type: "synthetic_monitoring",
testName,
severity: "warning",
message: `Synthetic test '${testName}' has issues`,
metrics,
timestamp: new Date(),
});
console.log(`Alert sent for ${testName}`);
} catch (error) {
console.error("Failed to send alert:", error);
}
}
}
module.exports = ScheduledSyntheticMonitor;
Best Practices
✅ DO
- Test critical user journeys
- Simulate real browser conditions
- Monitor from multiple locations
- Track response times
- Alert on test failures
- Rotate test data
- Test mobile and desktop
- Include error scenarios
❌ DON'T
- Test with production data
- Reuse test accounts
- Skip timeout configurations
- Ignore test maintenance
- Test too frequently
- Hard-code credentials
- Ignore geographic variations
- Test only happy paths
Key Metrics
- Response time
- Success rate
- Availability
- Core Web Vitals
- Error rate