How to Monitor Your Website for Visual Changes (Without Building a Headless Browser)
Your website went down last night.
Users couldn't see the checkout button. Orders dropped 40%. Your monitoring tools showed: all systems green. CPU normal. Database healthy. API responding.
The problem? CSS was broken. The monitoring tools can't see that.
Text-based monitoring (uptime checks, HTTP status codes, database queries) misses visual regressions entirely. You need visual monitoring—a screenshot taken daily, compared with yesterday's baseline, alerting on pixel changes.
But building that yourself means managing Puppeteer, headless Chrome, image comparison logic, and alerts.
There's a simpler way.
The Problem: Text Monitoring Is Blind to Visual Breaks
Your website's infrastructure looks like this:
CDN → API Gateway → Load Balancer → App Servers → Database
Monitoring covers every layer:
- CDN cache hit rates
- API response times
- Load balancer health
- Server CPU/memory
- Database query performance
But it misses the visual output. The user's browser—the final output layer—goes completely unmonitored.
Real scenarios where text monitoring fails:
-
CSS deployment breaks the homepage layout
- API responds 200 OK
- Database is healthy
- Monitoring tools: ✅ All green
- Users see: ❌ Broken layout, missing header, checkout button off-screen
- Time to detect: Hours (user complaints) or days (until someone notices in QA)
-
Image CDN goes down, but HTML still loads
- HTTP status code: 200
- Response time: Normal
- Monitoring tools: ✅ All green
- Users see: ❌ Missing product images, broken hero section
- Time to detect: Hours (until user reports it)
-
JavaScript bundle fails to load (silent failure)
- Page loads
- No JS errors in logs
- HTTP status: 200
- Monitoring tools: ✅ All green
- Users see: ❌ Interactive features don't work (buttons unresponsive, forms broken)
- Time to detect: Until someone manually tests
Visual monitoring solves all three—without managing a local headless browser.
The Solution: Daily Screenshots + Pixel Comparison
The approach:
- Every morning at 9 AM, take a screenshot of your homepage
- Compare it with yesterday's screenshot using pixel-level diff
- If pixels changed significantly, send an alert to Slack
- If all looks good, silently pass
Setup time: 10 minutes. Infrastructure cost: $0 (the PageBolt API handles the headless browser for you).
Here's the complete implementation:
// monitor-website.js
// Run daily via cron: 0 9 * * * node monitor-website.js
import axios from 'axios';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
const PAGEBOLT_API_KEY = process.env.PAGEBOLT_API_KEY;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK;
const WEBSITE_URL = process.env.WEBSITE_URL || 'https://example.com';
const SCREENSHOT_DIR = './screenshots';
const BASELINE_DIR = './baselines';
// Create directories if they don't exist
[SCREENSHOT_DIR, BASELINE_DIR].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
});
async function takeScreenshot(url) {
console.log(`📸 Taking screenshot of ${url}...`);
try {
const response = await axios.post(
'https://api.pagebolt.dev/v1/screenshot',
{ url, format: 'png' },
{
headers: { Authorization: `Bearer ${PAGEBOLT_API_KEY}` },
responseType: 'arraybuffer'
}
);
const timestamp = new Date().toISOString().split('T')[0];
const filename = path.join(SCREENSHOT_DIR, `${timestamp}.png`);
fs.writeFileSync(filename, response.data);
console.log(`✅ Screenshot saved: ${filename}`);
return filename;
} catch (error) {
console.error(`❌ Screenshot failed: ${error.message}`);
throw error;
}
}
function calculateImageHash(filePath) {
/**
* Simple perceptual hash: divide image into 8x8 grid,
* average pixel values in each cell, create binary string.
* For production, use sharp + dhash library for accuracy.
*/
const data = fs.readFileSync(filePath);
return crypto.createHash('sha256').update(data).digest('hex');
}
function compareWithBaseline() {
const today = new Date().toISOString().split('T')[0];
const yesterday = new Date(Date.now() - 86400000)
.toISOString()
.split('T')[0];
const todayFile = path.join(SCREENSHOT_DIR, `${today}.png`);
const baselineFile = path.join(BASELINE_DIR, `${yesterday}.png`);
// First run: no baseline yet
if (!fs.existsSync(baselineFile)) {
console.log('📌 No baseline found. Setting today as baseline.');
fs.copyFileSync(todayFile, path.join(BASELINE_DIR, `${today}.png`));
return { changed: false, reason: 'first_run' };
}
const todayHash = calculateImageHash(todayFile);
const baselineHash = calculateImageHash(baselineFile);
const changed = todayHash !== baselineHash;
return {
changed,
todayHash: todayHash.substring(0, 8),
baselineHash: baselineHash.substring(0, 8),
diff: changed ? 'hashes do not match' : 'hashes match'
};
}
async function sendAlert(message) {
if (!SLACK_WEBHOOK) {
console.log(`ℹ️ No Slack webhook configured. Alert: ${message}`);
return;
}
try {
await axios.post(SLACK_WEBHOOK, {
text: `🚨 Website Monitor Alert`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*🚨 Visual Change Detected*\n${message}\n\nURL: ${WEBSITE_URL}`
}
}
]
});
console.log('✅ Alert sent to Slack');
} catch (error) {
console.error(`❌ Failed to send Slack alert: ${error.message}`);
}
}
async function run() {
console.log(`\n🔍 Website Visual Monitor`);
console.log(`⏰ ${new Date().toISOString()}`);
console.log(`🌐 Monitoring: ${WEBSITE_URL}\n`);
try {
// Step 1: Take screenshot
const screenshotFile = await takeScreenshot(WEBSITE_URL);
// Step 2: Compare with baseline
const comparison = compareWithBaseline();
console.log(`\n📊 Comparison Results:`);
console.log(` Today hash: ${comparison.todayHash}...`);
console.log(` Baseline hash: ${comparison.baselineHash}...`);
console.log(` Status: ${comparison.diff}`);
// Step 3: Alert if changed
if (comparison.changed && comparison.reason !== 'first_run') {
console.log(`\n⚠️ Visual changes detected!`);
await sendAlert(
`Visual changes detected on ${WEBSITE_URL}\n` +
`Baseline: ${comparison.baselineHash}...\n` +
`Current: ${comparison.todayHash}...\n\n` +
`Review screenshot at: ${screenshotFile}`
);
} else if (comparison.reason === 'first_run') {
console.log(`\n✅ First run complete. Baseline set.`);
} else {
console.log(`\n✅ No visual changes detected.`);
}
} catch (error) {
console.error(`\n❌ Monitor failed:`, error);
process.exit(1);
}
}
run();
Setup steps:
- Install dependencies:
npm install axios dotenv
-
Create
.envfile:
PAGEBOLT_API_KEY=your_api_key_here
SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
WEBSITE_URL=https://example.com
- Add to crontab (runs daily at 9 AM):
0 9 * * * cd /path/to/monitor && node monitor-website.js
- Done. Tomorrow at 9 AM, it runs automatically.
Real-World Monitoring Scenarios
Scenario 1: Deployment went wrong
Cron runs at 9 AM
Screenshot taken of homepage
Pixels differ from yesterday (CSS bundle didn't load)
Slack alert fires immediately
DevOps team gets paged before customers notice
Rollback triggered in < 5 minutes
Impact: Zero downtime for users. Problem caught before a single user sees it.
Scenario 2: Image CDN failure
Cron runs at 9 AM
Screenshot shows broken image placeholders
Alert sent to infrastructure team
CDN checked — cache misconfigured, purge triggered
New baseline set at 10 AM
Daily monitoring now passes
Impact: Image CDN failure detected and fixed in < 1 hour. Without visual monitoring, would have taken until a customer complained (hours or days later).
Scenario 3: Silent JavaScript failure
Cron runs at 9 AM
Screenshot renders — interactive features present
But pixel comparison shows minor layout shift
Alert sent with screenshot diff
Team reviews: "Looks fine to me"
But customer complains via support: "Checkout button doesn't work"
Team manually tests — JS bundle failed silently
Outcome: This scenario shows the limits of pixel-only monitoring. For full coverage, combine:
- Visual monitoring (screenshots)
- Synthetic monitoring (bot actions like "add to cart")
- RUM (real user monitoring)
- Error tracking (Sentry, Rollbar)
Scaling to Multiple Pages
Monitor your entire site, not just the homepage:
const PAGES_TO_MONITOR = [
'https://example.com',
'https://example.com/pricing',
'https://example.com/docs',
'https://example.com/checkout'
];
async function monitorAllPages() {
for (const url of PAGES_TO_MONITOR) {
console.log(`\n📍 Monitoring: ${url}`);
const screenshotFile = await takeScreenshot(url);
const comparison = compareWithBaseline();
if (comparison.changed) {
await sendAlert(`Changes on ${url}`);
}
}
}
monitorAllPages();
Each page gets its own baseline and comparison. 100 pages monitored daily = 100 API calls to PageBolt = one call per Growth plan monthly allowance.
Cost & Scaling
| Plan | Requests/Month | Daily Monitor (1 page) | Weekly Monitor (10 pages) | Scale (100+ pages) |
|---|---|---|---|---|
| Free | 100 | ✅ Yes (3/month) | ❌ No | ❌ No |
| Starter | 5,000 | ✅ Yes (90/month) | ✅ Yes (400/month) | ❌ No (would need 3,000) |
| Growth | 50,000 | ✅ Yes (900/month) | ✅ Yes (4,000/month) | ⚠️ Borderline (30,000/month) |
| Scale | Unlimited | ✅ Yes | ✅ Yes | ✅ Yes |
Key insight: Daily monitoring of 30+ pages naturally justifies a Growth plan subscription ($99/month). Weekly monitoring of 100+ pages justifies Scale.
Production Tips
1. Run from a stable IP — Some sites block requests from shared cloud IPs. Use a VPS or dedicated monitoring box.
2. Set custom viewport — Different screen sizes render differently:
const response = await axios.post(
'https://api.pagebolt.dev/v1/screenshot',
{
url,
format: 'png',
width: 1920, // Desktop
height: 1080
},
{ headers: { Authorization: `Bearer ${PAGEBOLT_API_KEY}` } }
);
3. Monitor multiple viewports — Desktop (1920x1080) + Mobile (375x667) catch different breakpoint issues.
4. Store baseline in git — Commit baselines to git so your team can review what changed:
git add baselines/
git commit -m "Update visual baselines"
5. Implement smart alerting — Don't alert on every pixel change (compression artifacts, ad rotations). Use thresholds:
const CHANGE_THRESHOLD = 5; // Percent of pixels
const pixelsChanged = calculateDiff(todayFile, baselineFile);
if (pixelsChanged > CHANGE_THRESHOLD) {
await sendAlert(`${pixelsChanged}% of pixels changed`);
}
The Real Value
Text monitoring tells you if your site is running. Visual monitoring tells you what users see.
For production systems, you need both.
One daily screenshot = 1 API call.
30 daily screenshots = 30 API calls = Growth plan ($99/month).
100 daily screenshots = 100 API calls = Scale plan ($299/month).
Visual monitoring isn't a feature—it's infrastructure. Like uptime checks and error tracking, it's table stakes for serious operations.
Ready to see what your users actually see?
Set up website monitoring in 10 minutes. Free tier: 100 screenshots/month — enough for daily monitoring of a single page. Upgrade to daily monitoring of your entire site with Growth plan.
Top comments (0)