Ever wondered if your website is actually working for your users right now? Like, not just “is the server responding” but “can someone actually log in, click through pages, and do the things they’re supposed to do?” That’s where synthetic monitoring comes in, and it’s pretty neat.
What’s Synthetic Monitoring Anyway?
Think of synthetic monitoring as having a robot that pretends to be your user. Every few minutes, this robot visits your site, clicks around, fills out forms, and basically does all the things a real person would do. If something breaks, you know about it before your actual users start complaining. It’s like having a really dedicated QA person who never sleeps, never gets bored, and tests your site 24/7.
The “synthetic” part just means it’s simulated behavior rather than watching real users. You’re creating fake but realistic traffic to monitor how your application performs.
Enter Playwright and WorkAdventure’s Tool
Playwright is Microsoft’s end-to-end testing framework. It can control browsers (Chrome, Firefox, Safari) and interact with websites just like a human would. It’s powerful, fast, and surprisingly easy to use.
WorkAdventure took Playwright and wrapped it up in a Docker container that runs your tests continuously and exposes the results through handy HTTP endpoints. So instead of just running tests when you remember to, they run automatically and feed data to your monitoring tools.
Here’s what makes it cool:
- Runs your Playwright tests every 5 minutes (configurable)
- Exposes a
/metricsendpoint for Prometheus - Provides a
/healthcheckendpoint that returns HTTP 200 if tests pass, HTTP 500 if they fail - Shows a
/last-errorpage with the full Playwright report when things go wrong - Keeps the error report even after tests recover, so you can investigate what happened
Getting Started with Docker Compose
First things first, you need to write some Playwright tests. Don’t worry, they’re pretty straightforward. Here’s a simple example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { test, expect } from '@playwright/test'; test('homepage loads correctly', async ({ page }) => { await page.goto('https://example.com/'); await expect(page.getByText('Welcome')).toBeVisible(); }); test('can navigate to about page', async ({ page }) => { await page.goto('https://example.com/'); await page.click('a[href="/about"]'); await expect(page).toHaveURL(/.*about/); }); |
Save your tests in a directory called tests and make sure they end with .spec.ts or .spec.js.
Now here’s the basic Docker Compose setup:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
version: '3.8' services: playwright-monitoring: image: workadventure/playwright-synthetic-monitoring:latest container_name: playwright-monitoring volumes: - ./tests:/work/tests:ro ports: - "3000:3000" restart: unless-stopped |
This mounts your test directory as read-only into the container and exposes the web interface on port 3000.
Environment Variables Explained
Want to customize how it runs? Here are the environment variables you can play with:
MONITORING_INTERVAL
This controls how often your tests run. The default is 300 seconds (5 minutes).
|
1 2 3 4 |
environment: - MONITORING_INTERVAL=600 # Run tests every 10 minutes |
Important note: Make sure your tests can actually finish within this interval. If they take longer, they’ll be killed and marked as failed.
BASE_URL
If your tests use relative URLs or you want to test different environments easily, set a base URL:
|
1 2 3 4 |
environment: - BASE_URL=https://staging.yoursite.com |
Then in your tests, you can use relative paths:
|
1 2 3 |
await page.goto('/login'); // Goes to https://staging.yoursite.com/login |
PERSIST_TRACE_DIRECTORY
Want to keep traces of failed tests for later investigation? Set this to a directory name:
|
1 2 3 4 5 6 7 |
volumes: - ./tests:/work/tests:ro - ./traces:/work/traces environment: - PERSIST_TRACE_DIRECTORY=traces |
Failed test traces will be saved as ZIP files with timestamps like 2025-04-24T14:57:49.662Z.zip.
Complete Docker Compose Example
Here’s a more complete setup with all the bells and whistles:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
version: '3.8' services: playwright-monitoring: image: workadventure/playwright-synthetic-monitoring:latest container_name: playwright-monitoring volumes: - ./tests:/work/tests:ro - ./traces:/work/traces ports: - "3000:3000" environment: - MONITORING_INTERVAL=300 - BASE_URL=https://yoursite.com - PERSIST_TRACE_DIRECTORY=traces restart: unless-stopped healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/healthcheck"] interval: 60s timeout: 10s retries: 3 |
Using the Endpoints
Once it’s running, you’ve got several useful endpoints:
Main Dashboard: http://localhost:3000/
This shows the status of your last test run and links to all the other pages. It’s your monitoring home base.
Health Check: http://localhost:3000/healthcheck
This is what you hook up to UptimeRobot or whatever monitoring tool you use. It returns:
- HTTP 200 if all tests passed
- HTTP 500 if any test failed
Prometheus Metrics: http://localhost:3000/metrics
If you’re running Prometheus, point it here. You’ll get:
playwright_synthetic_monitoring_status: 1 if tests passed, 0 if failedplaywright_synthetic_monitoring_test_duration_seconds: How long the tests took
Last Error Report: http://localhost:3000/last-error
When tests fail, this shows the full Playwright HTML report. The cool thing is it stays available even after tests recover, so you can investigate what went wrong at 3 AM without having to recreate it.
Writing Good Synthetic Tests
Here are some tips for writing tests that’ll actually catch problems:
Test Critical User Journeys
Focus on the paths users actually take. Login, checkout, signup, whatever matters for your business.
|
1 2 3 4 5 6 7 8 9 |
test('user can complete signup', async ({ page }) => { await page.goto('/signup'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'SecurePass123!'); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/.*dashboard/); }); |
Keep Tests Fast
Remember, these run every few minutes. Keep each test under 30 seconds if possible.
Make Tests Resilient
Use good selectors that won’t break when someone changes a CSS class:
|
1 2 3 4 5 6 7 |
// Better await page.getByRole('button', { name: 'Submit' }).click(); // Instead of await page.click('.btn-primary-submit-form-v2'); |
Test Both Happy and Sad Paths
Don’t just test that things work. Test that errors show up correctly too:
|
1 2 3 4 5 6 7 8 9 |
test('shows error for invalid login', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'wrong@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); await expect(page.getByText('Invalid credentials')).toBeVisible(); }); |
Running It
Start everything up:
|
1 2 3 |
docker-compose up -d |
Check the logs to make sure it’s working:
|
1 2 3 |
docker-compose logs -f playwright-monitoring |
Open http://localhost:3000 in your browser and you should see the status page.
If you want to run tests once manually to see what happens:
|
1 2 3 |
docker-compose exec playwright-monitoring npm run test |
Integrating with Monitoring Tools
UptimeRobot
Create a new HTTP(s) monitor pointing to http://your-server:3000/healthcheck. Set it to check every 5 minutes (or whatever your MONITORING_INTERVAL is). UptimeRobot will alert you if it gets a 500 response.
Prometheus + Grafana
Add this to your prometheus.yml:
|
1 2 3 4 5 6 7 |
scrape_configs: - job_name: 'playwright-monitoring' scrape_interval: 60s static_configs: - targets: ['your-server:3000'] |
Then create Grafana dashboards using the metrics. You can track success rates, test duration, and set up alerts when tests fail.
Troubleshooting Common Issues
Tests are taking too long and timing out
Increase your MONITORING_INTERVAL or optimize your tests. Look for unnecessary waits and slow page loads.
Tests pass locally but fail in Docker
Timing issues are common. Add explicit waits for elements:
|
1 2 3 |
await page.waitForSelector('.important-element', { timeout: 10000 }); |
Container keeps restarting
Check the logs for errors. Make sure your test files are valid TypeScript/JavaScript and that they’re mounted correctly.
Useful Links
- GitHub Repository – The official repo with full documentation
- Playwright Documentation – Learn how to write better tests
- Playwright Best Practices – Tips for writing resilient tests
- Docker Hub Image – The container image
Thoughts
Synthetic monitoring is one of those things that seems like overkill until the one time it catches a critical bug before your users do. With WorkAdventure’s Playwright Synthetic Monitoring, you get a zero-config solution that just works. Write your tests, spin up the container, hook it to your monitoring stack, and forget about it.
The beauty of this setup is that it’s completely isolated in Docker, runs automatically, and integrates with all the standard monitoring tools you’re probably already using. Plus, Playwright tests are actually useful beyond just monitoring – you can run the same tests in your CI/CD pipeline, during development, or whenever you need them.
Now go write some tests and catch those bugs before your users do.
Some Playwright Test Examples
Here’s a collection of practical Playwright test examples you can use for synthetic monitoring. These cover common scenarios you’ll want to monitor on your websites and applications.
Basic Page Load Tests
Simple Homepage Check
|
1 2 3 4 5 6 7 8 9 |
import { test, expect } from '@playwright/test'; test('homepage loads successfully', async ({ page }) => { await page.goto('https://example.com/'); await expect(page).toHaveTitle(/Example Domain/); await expect(page.locator('h1')).toBeVisible(); }); |
Check Multiple Pages Load
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
test('critical pages are accessible', async ({ page }) => { const pages = [ '/', '/about', '/contact', '/pricing', '/docs' ]; for (const path of pages) { await page.goto(`https://example.com${path}`); await expect(page).not.toHaveTitle(/404|Error/); // Wait for page to be fully loaded await page.waitForLoadState('networkidle'); } }); |
Verify Page Speed
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('homepage loads within acceptable time', async ({ page }) => { const startTime = Date.now(); await page.goto('https://example.com/'); await page.waitForLoadState('domcontentloaded'); const loadTime = Date.now() - startTime; expect(loadTime).toBeLessThan(3000); // Should load in under 3 seconds }); |
Authentication Tests
Login Flow
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('user can log in successfully', async ({ page }) => { await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'SecurePassword123!'); await page.click('button[type="submit"]'); // Wait for redirect to dashboard await expect(page).toHaveURL(/.*dashboard/); // Verify user is logged in await expect(page.locator('text=Welcome back')).toBeVisible(); }); |
Failed Login Handling
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('shows error message for invalid credentials', async ({ page }) => { await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'wrong@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); // Should stay on login page await expect(page).toHaveURL(/.*login/); // Error message should appear await expect(page.locator('.error-message')).toContainText('Invalid credentials'); }); |
Session Persistence
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
test('user stays logged in after refresh', async ({ page }) => { // Login first await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'SecurePassword123!'); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/.*dashboard/); // Refresh the page await page.reload(); // User should still be logged in await expect(page).toHaveURL(/.*dashboard/); await expect(page.locator('text=Welcome back')).toBeVisible(); }); |
Form Submission Tests
Contact Form
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('contact form can be submitted', async ({ page }) => { await page.goto('https://example.com/contact'); await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', 'john@example.com'); await page.fill('textarea[name="message"]', 'This is a test message'); await page.check('input[name="newsletter"]'); // Optional checkbox await page.click('button[type="submit"]'); // Check for success message await expect(page.locator('.success-message')).toContainText('Thank you'); }); |
Form Validation
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('form shows validation errors', async ({ page }) => { await page.goto('https://example.com/contact'); // Try to submit without filling required fields await page.click('button[type="submit"]'); // Check for validation messages await expect(page.locator('input[name="email"]:invalid')).toBeVisible(); await expect(page.locator('.validation-error')).toContainText('required'); }); |
File Upload
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('file can be uploaded', async ({ page }) => { await page.goto('https://example.com/upload'); // Upload a file const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles('./test-file.pdf'); await page.click('button[type="submit"]'); // Verify upload success await expect(page.locator('.upload-success')).toBeVisible(); await expect(page.locator('.file-name')).toContainText('test-file.pdf'); }); |
E-commerce Tests
Add to Cart
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
test('product can be added to cart', async ({ page }) => { await page.goto('https://shop.example.com/product/123'); // Check initial cart count const cartBadge = page.locator('.cart-count'); const initialCount = await cartBadge.textContent(); await page.click('button:has-text("Add to Cart")'); // Wait for cart to update await page.waitForTimeout(1000); // Verify cart count increased const newCount = await cartBadge.textContent(); expect(parseInt(newCount || '0')).toBeGreaterThan(parseInt(initialCount || '0')); }); |
Checkout Flow
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
test('complete checkout process works', async ({ page }) => { // Add product to cart await page.goto('https://shop.example.com/product/123'); await page.click('button:has-text("Add to Cart")'); // Go to cart await page.click('a[href="/cart"]'); await expect(page.locator('.cart-item')).toBeVisible(); // Proceed to checkout await page.click('button:has-text("Checkout")'); // Fill shipping info await page.fill('input[name="firstName"]', 'John'); await page.fill('input[name="lastName"]', 'Doe'); await page.fill('input[name="address"]', '123 Main St'); await page.fill('input[name="city"]', 'Anytown'); await page.fill('input[name="zip"]', '12345'); await page.click('button:has-text("Continue")'); // Verify order summary page await expect(page).toHaveURL(/.*order-summary/); }); |
Search Functionality
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
test('product search returns results', async ({ page }) => { await page.goto('https://shop.example.com/'); await page.fill('input[name="search"]', 'laptop'); await page.press('input[name="search"]', 'Enter'); // Wait for results await page.waitForSelector('.search-results'); // Verify results are shown const resultCount = await page.locator('.product-card').count(); expect(resultCount).toBeGreaterThan(0); // Verify results are relevant await expect(page.locator('.product-title').first()).toContainText(/laptop/i); }); |
API Health Checks
REST API Endpoint
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('API endpoint returns valid data', async ({ page }) => { // Use page.request for API calls const response = await page.request.get('https://api.example.com/health'); expect(response.ok()).toBeTruthy(); expect(response.status()).toBe(200); const data = await response.json(); expect(data.status).toBe('healthy'); }); |
API with Authentication
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
test('authenticated API request works', async ({ page }) => { const response = await page.request.get('https://api.example.com/user/profile', { headers: { 'Authorization': 'Bearer YOUR_TEST_TOKEN', 'Content-Type': 'application/json' } }); expect(response.ok()).toBeTruthy(); const data = await response.json(); expect(data).toHaveProperty('email'); expect(data).toHaveProperty('name'); }); |
Navigation Tests
Menu Navigation
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('main navigation menu works', async ({ page }) => { await page.goto('https://example.com/'); // Click through main menu items await page.hover('nav a:has-text("Products")'); await page.click('nav a:has-text("Software")'); await expect(page).toHaveURL(/.*products\/software/); await expect(page.locator('h1')).toContainText('Software'); }); |
Breadcrumb Navigation
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
test('breadcrumb navigation is functional', async ({ page }) => { await page.goto('https://example.com/products/software/details'); // Verify breadcrumbs exist const breadcrumbs = page.locator('.breadcrumb'); await expect(breadcrumbs).toBeVisible(); // Click home in breadcrumbs await page.click('.breadcrumb a:has-text("Home")'); await expect(page).toHaveURL('https://example.com/'); }); |
Mobile Menu
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('mobile menu works on small screens', async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }); await page.goto('https://example.com/'); // Open mobile menu await page.click('button.mobile-menu-toggle'); await expect(page.locator('.mobile-menu')).toBeVisible(); // Click a menu item await page.click('.mobile-menu a:has-text("About")'); await expect(page).toHaveURL(/.*about/); }); |
Content Verification Tests
Check for Specific Text
|
1 2 3 4 5 6 7 8 9 10 |
test('page contains required content', async ({ page }) => { await page.goto('https://example.com/about'); // Check for key information await expect(page.locator('text=Our Mission')).toBeVisible(); await expect(page.locator('text=Founded in 2020')).toBeVisible(); await expect(page.locator('text=Contact Us')).toBeVisible(); }); |
Verify Images Load
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('all critical images are loaded', async ({ page }) => { await page.goto('https://example.com/'); const logo = page.locator('img.logo'); await expect(logo).toBeVisible(); // Check if image loaded successfully const isLoaded = await logo.evaluate((img: HTMLImageElement) => { return img.complete && img.naturalHeight > 0; }); expect(isLoaded).toBeTruthy(); }); |
Check Links Are Valid
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
test('footer links are not broken', async ({ page }) => { await page.goto('https://example.com/'); const footerLinks = await page.locator('footer a').all(); for (const link of footerLinks) { const href = await link.getAttribute('href'); if (href && href.startsWith('http')) { const response = await page.request.get(href); expect(response.status()).toBeLessThan(400); } } }); |
Multi-Step User Flows
Complete Registration Flow
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
test('new user registration process', async ({ page }) => { // Go to signup page await page.goto('https://example.com/signup'); // Fill registration form await page.fill('input[name="firstName"]', 'Jane'); await page.fill('input[name="lastName"]', 'Smith'); await page.fill('input[name="email"]', `test+${Date.now()}@example.com`); await page.fill('input[name="password"]', 'SecurePass123!'); await page.fill('input[name="confirmPassword"]', 'SecurePass123!'); await page.check('input[name="terms"]'); await page.click('button[type="submit"]'); // Should redirect to email verification page await expect(page).toHaveURL(/.*verify-email/); await expect(page.locator('text=Check your email')).toBeVisible(); }); |
Profile Update Flow
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
test('user can update profile information', async ({ page }) => { // Login first await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'SecurePassword123!'); await page.click('button[type="submit"]'); // Navigate to profile await page.click('a[href="/profile"]'); // Update information await page.fill('input[name="phone"]', '+1234567890'); await page.selectOption('select[name="country"]', 'US'); await page.click('button:has-text("Save Changes")'); // Verify success message await expect(page.locator('.success-message')).toContainText('Profile updated'); }); |
Error Handling Tests
404 Page Exists
|
1 2 3 4 5 6 7 8 9 10 11 |
test('404 page is shown for invalid URLs', async ({ page }) => { const response = await page.goto('https://example.com/this-page-does-not-exist'); expect(response?.status()).toBe(404); await expect(page.locator('h1')).toContainText(/404|Not Found/i); // Check that home link exists await expect(page.locator('a[href="/"]')).toBeVisible(); }); |
Handle Network Errors Gracefully
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
test('app shows error message when API fails', async ({ page }) => { // Intercept API call and make it fail await page.route('**/api/data', route => route.abort()); await page.goto('https://example.com/dashboard'); // Should show error message await expect(page.locator('.error-message')).toContainText(/unable to load|error/i); // Retry button should be available await expect(page.locator('button:has-text("Retry")')).toBeVisible(); }); |
Performance Tests
Check Page Weight
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
test('homepage is not too heavy', async ({ page }) => { let totalSize = 0; page.on('response', response => { const size = parseInt(response.headers()['content-length'] || '0'); totalSize += size; }); await page.goto('https://example.com/'); await page.waitForLoadState('networkidle'); const totalMB = totalSize / (1024 * 1024); expect(totalMB).toBeLessThan(5); // Less than 5MB }); |
Check Number of Requests
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('page makes reasonable number of requests', async ({ page }) => { let requestCount = 0; page.on('request', () => requestCount++); await page.goto('https://example.com/'); await page.waitForLoadState('networkidle'); expect(requestCount).toBeLessThan(50); // Fewer than 50 requests }); |
Accessibility Tests
Check for Alt Text on Images
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
test('images have alt text', async ({ page }) => { await page.goto('https://example.com/'); const images = await page.locator('img').all(); for (const img of images) { const alt = await img.getAttribute('alt'); expect(alt).toBeDefined(); expect(alt?.length).toBeGreaterThan(0); } }); |
Check Form Labels
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
test('form inputs have associated labels', async ({ page }) => { await page.goto('https://example.com/contact'); const inputs = await page.locator('input[type="text"], input[type="email"]').all(); for (const input of inputs) { const id = await input.getAttribute('id'); if (id) { const label = page.locator(`label[for="${id}"]`); await expect(label).toBeVisible(); } } }); |
Third-Party Integration Tests
Social Media Login
|
1 2 3 4 5 6 7 8 9 |
test('Google OAuth login option is available', async ({ page }) => { await page.goto('https://example.com/login'); const googleButton = page.locator('button:has-text("Sign in with Google")'); await expect(googleButton).toBeVisible(); await expect(googleButton).toBeEnabled(); }); |
Payment Integration
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('payment form loads Stripe elements', async ({ page }) => { await page.goto('https://example.com/checkout'); // Wait for Stripe to load await page.waitForSelector('iframe[name^="__privateStripeFrame"]'); // Verify Stripe iframe is present const stripeFrame = page.frameLocator('iframe[name^="__privateStripeFrame"]'); await expect(stripeFrame.locator('input[name="cardnumber"]')).toBeVisible(); }); |
Using Environment Variables
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// tests/config.spec.ts import { test, expect } from '@playwright/test'; // Use BASE_URL environment variable const baseUrl = process.env.BASE_URL || 'https://example.com'; test('homepage loads using BASE_URL', async ({ page }) => { await page.goto(baseUrl); await expect(page.locator('h1')).toBeVisible(); }); test('login with test credentials', async ({ page }) => { const testEmail = process.env.TEST_EMAIL || 'test@example.com'; const testPassword = process.env.TEST_PASSWORD || 'password'; await page.goto(`${baseUrl}/login`); await page.fill('input[name="email"]', testEmail); await page.fill('input[name="password"]', testPassword); await page.click('button[type="submit"]'); await expect(page).toHaveURL(new RegExp(`${baseUrl}/dashboard`)); }); |
Tips for Better Tests
Use Data Attributes for Stable Selectors
|
1 2 3 4 5 6 7 |
// Instead of CSS classes that might change await page.click('.btn-primary-large-submit'); // Use data attributes await page.click('[data-testid="submit-button"]'); |
Add Explicit Waits for Dynamic Content
|
1 2 3 4 5 6 7 8 9 10 11 |
test('wait for dynamic content', async ({ page }) => { await page.goto('https://example.com/dashboard'); // Wait for specific element await page.waitForSelector('[data-testid="user-profile"]', { timeout: 10000 }); // Or wait for network to be idle await page.waitForLoadState('networkidle'); }); |
Take Screenshots on Failure
|
1 2 3 4 5 6 7 8 9 10 11 12 |
test('important user flow', async ({ page }) => { try { await page.goto('https://example.com/checkout'); await page.click('button:has-text("Pay Now")'); await expect(page.locator('.success')).toBeVisible(); } catch (error) { await page.screenshot({ path: 'failure-screenshot.png', fullPage: true }); throw error; } }); |
Group Related Tests
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { test } from '@playwright/test'; test.describe('Authentication', () => { test('successful login', async ({ page }) => { // test code }); test('failed login', async ({ page }) => { // test code }); test('logout', async ({ page }) => { // test code }); }); test.describe('Shopping Cart', () => { test('add to cart', async ({ page }) => { // test code }); test('remove from cart', async ({ page }) => { // test code }); }); |
These examples should give you a solid foundation for monitoring all the critical parts of your web application. Pick the ones that match your use case and customize them for your specific needs.
FAQ
What is synthetic monitoring?
Synthetic monitoring is a proactive testing method that simulates user interactions with websites and applications using automated scripts. It runs scheduled tests from various global locations to measure performance, availability, and functionality before real users encounter issues.
How does synthetic monitoring differ from Real User Monitoring (RUM)?
Synthetic monitoring is proactive and uses scripted tests in controlled environments, while RUM passively collects data from actual users. Synthetic monitoring runs on predetermined schedules and can test even when no users are present, whereas RUM captures real user behavior and experiences. Both approaches complement each other for comprehensive monitoring.
What are the main benefits of synthetic monitoring?
Key benefits include proactive issue detection before users are affected, performance optimization through detailed metrics, consistent testing in controlled environments, 24/7 monitoring even during low traffic periods, ability to test from multiple global locations, and early detection of third-party service failures.
How often should synthetic monitoring tests run?
Test frequency depends on business criticality. High-priority transactions like checkout flows may run every 1-5 minutes, while lower-priority functions might run hourly or daily. The goal is to balance insight with system overhead and reduce mean time to detect (MTTD).
What types of tests can synthetic monitoring perform?
Synthetic monitoring can perform browser-based testing (simulating user clicks and navigation), API monitoring (testing endpoints and response times), transaction monitoring (testing complete user journeys like login and checkout), uptime checks, SSL certificate validation, and multi-location performance testing.
Can synthetic monitoring test applications in pre-production?
Yes, synthetic monitoring excels at pre-production testing. Unlike RUM which requires actual user traffic, synthetic monitoring can test staging environments, establish performance baselines, and catch issues before they reach production, making it invaluable for CI/CD pipelines.
What are common challenges with synthetic monitoring?
Common challenges include script maintenance when applications change frequently, alert fatigue from false positives, limited coverage of uncommon user paths, and the predictability of tests which may miss edge cases. Using resilient selectors, dynamic waits, and modularized scripts helps address these issues.
How do you reduce false alerts in synthetic monitoring?
Best practices include running concurrent monitoring from multiple locations to confirm true failures, setting alert thresholds to trigger only after multiple consecutive failures (e.g., 3 failures over 15 minutes), and aligning alerts with business impact to prioritize critical transactions.
What makes a good synthetic monitoring tool in 2025?
Essential features include browser-based testing capabilities, API monitoring, mobile app testing, custom script support, comprehensive alerting systems, global testing locations (230+ checkpoints), AI-powered anomaly detection, integration with CI/CD pipelines, detailed visualization and reporting, and historical data analysis.
Can synthetic monitoring help with SEO and Core Web Vitals?
Yes, synthetic monitoring tools can track Core Web Vitals (LCP, FID, CLS), Lighthouse scores, and page speed metrics that directly impact SEO rankings. They provide detailed performance recommendations and can alert you to regressions that might harm search rankings.
How does synthetic monitoring help with third-party service monitoring?
Synthetic monitoring can verify third-party SLAs, detect when external services (payment gateways, CDNs, analytics) fail or slow down, and measure their impact on your application. You can create tests that include or exclude third-party assets to quantify their performance impact.
What scripting languages are commonly used for synthetic monitoring?
Popular frameworks include Playwright and Puppeteer (JavaScript/TypeScript), Cypress (JavaScript), and Selenium. Modern tools like Checkly are built on Playwright, offering code-based test creation with ultimate flexibility for complex workflows.
How do you write maintainable synthetic monitoring scripts?
Best practices include using resilient selectors (data attributes instead of auto-generated IDs), incorporating intelligent dynamic waits instead of hardcoded timeouts, modularizing scripts into reusable components, and integrating script updates into your CI/CD pipeline for automatic maintenance.
Can synthetic monitoring simulate global user interactions?
Yes, synthetic monitoring tools run tests from multiple global locations (often 90+ countries with 800+ testing nodes) to identify regional issues, test geofencing, evaluate performance worldwide, and ensure consistent experiences for users in different geographic locations.
What role does AI play in modern synthetic monitoring?
AI capabilities enable automated root cause analysis, predictive performance analytics, anomaly detection without manual threshold configuration, self-healing tests that adapt to UI changes automatically, and predictive alerts that identify potential issues before they impact users.
How do you set up alerting for synthetic monitoring?
Define acceptable response times and availability benchmarks for each transaction, specify failure conditions (like 3 consecutive failures or 10-second delays), and route alerts to appropriate teams via integrations like Slack, email, or Teams. This ensures actionable alerts reach the right people quickly.
Should synthetic monitoring be used with other monitoring types?
Absolutely. A comprehensive monitoring strategy combines synthetic monitoring with RUM for complete visibility. Synthetic provides proactive testing and controlled environments, while RUM offers real user insights. Together, they provide a 360-degree view of application health and user experience.
How can synthetic monitoring improve competitive advantage?
Synthetic monitoring enables competitor benchmarking by testing their websites without code injection, helps prepare for traffic spikes (Black Friday, Cyber Monday), validates performance across different markets, and ensures your critical user journeys are faster and more reliable than competitors.
What is the difference between synthetic monitoring and journey monitoring?
They are essentially the same thing. Journey monitoring focuses on testing complete user paths (login to checkout), while synthetic monitoring is the broader term. The terminology “synthetic monitoring” is more commonly used across leading web monitoring solutions.
How do open-source synthetic monitoring tools compare to commercial ones?
Open-source tools like Playwright, Cypress, and Prometheus Blackbox Exporter offer ultimate flexibility and are completely free but require technical expertise, self-hosting, and manual integration with schedulers and alerting. Commercial tools provide user-friendly interfaces, managed infrastructure, global testing networks, and comprehensive support.
Example: Basic synthetic monitoring test script
Here’s a simple example using Playwright to test a login flow:
