Back to Blog
How to Monitor Your Website for Visual Changes (Without Building a Headless Browser)

How to Monitor Your Website for Visual Changes (Without Building a Headless Browser)

March 24, 20269 min read

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

Enter fullscreen mode Exit fullscreen mode

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:

  1. 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)
  2. 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)
  3. 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:

  1. Every morning at 9 AM, take a screenshot of your homepage
  2. Compare it with yesterday's screenshot using pixel-level diff
  3. If pixels changed significantly, send an alert to Slack
  4. 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();

Enter fullscreen mode Exit fullscreen mode

Setup steps:

  1. Install dependencies:
   npm install axios dotenv

Enter fullscreen mode Exit fullscreen mode

  1. Create .env file:
   PAGEBOLT_API_KEY=your_api_key_here
   SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
   WEBSITE_URL=https://example.com

Enter fullscreen mode Exit fullscreen mode

  1. Add to crontab (runs daily at 9 AM):
   0 9 * * * cd /path/to/monitor && node monitor-website.js

Enter fullscreen mode Exit fullscreen mode

  1. 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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}` } }
   );

Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

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`);
   }

Enter fullscreen mode Exit fullscreen mode


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.

Get started free →


Source: Dev.to

Related Posts