How to Monitor SaaS Dashboards with Automated Screenshots
SaaS dashboards change — sometimes intentionally (new features, redesigns), sometimes unexpectedly (broken widgets, missing data, layout shifts). Visual monitoring catches problems that uptime checks miss: a dashboard can return HTTP 200 with a blank chart or a broken table, and your users are the first to notice.
A screenshot API lets you capture scheduled visual snapshots of any dashboard, compare them over time, and alert when something looks wrong — without writing a custom browser automation stack.
Core Pattern: Scheduled Dashboard Capture
import hashlib
import os
import time
import requests
from datetime import datetime, timezone
def capture_dashboard(url: str, output_dir: str, api_key: str, label: str = None) -> dict:
"""Capture a screenshot of a dashboard page and store with timestamp."""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
slug = label or hashlib.md5(url.encode()).hexdigest()[:8]
os.makedirs(output_dir, exist_ok=True)
filename = f"{slug}-{timestamp}.png"
output_path = os.path.join(output_dir, filename)
resp = requests.get(
"https://hermesforge.dev/api/screenshot",
params={
"url": url,
"width": 1920,
"height": 1080,
"format": "png",
"full_page": "false", # Viewport-only for dashboards — charts above the fold
"wait_for": "networkidle",
"key": api_key
},
timeout=45
)
if resp.status_code == 200:
with open(output_path, "wb") as f:
f.write(resp.content)
return {"label": slug, "timestamp": timestamp, "path": output_path, "status": "ok", "size": len(resp.content)}
return {"label": slug, "timestamp": timestamp, "status": "failed", "http_status": resp.status_code}
DASHBOARDS = [
{"label": "analytics-overview", "url": "https://app.example.com/dashboard"},
{"label": "revenue-metrics", "url": "https://app.example.com/revenue"},
{"label": "support-queue", "url": "https://app.example.com/support"},
]
api_key = "YOUR_API_KEY"
for dashboard in DASHBOARDS:
result = capture_dashboard(dashboard["url"], f"snapshots/{dashboard['label']}", api_key, dashboard["label"])
print(f"{result['status']}: {dashboard['label']}")
time.sleep(2)
Use Case 1: Visual Regression Detection
Compare each new capture against the previous one to detect unexpected changes:
import os
from pathlib import Path
def get_latest_two(snapshot_dir: str) -> tuple[str | None, str | None]:
"""Return the two most recent snapshots in a directory."""
snapshots = sorted(Path(snapshot_dir).glob("*.png"), key=os.path.getmtime)
if len(snapshots) >= 2:
return str(snapshots[-2]), str(snapshots[-1])
elif len(snapshots) == 1:
return None, str(snapshots[0])
return None, None
def file_size_diff_percent(path_a: str, path_b: str) -> float:
"""Quick proxy for visual change: file size difference as percentage."""
size_a = os.path.getsize(path_a)
size_b = os.path.getsize(path_b)
if size_a == 0:
return 100.0
return abs(size_b - size_a) / size_a * 100
# After capturing new snapshots, check for regressions
CHANGE_THRESHOLD_PERCENT = 15.0 # Flag if file size changes >15%
for dashboard in DASHBOARDS:
snapshot_dir = f"snapshots/{dashboard['label']}"
prev, curr = get_latest_two(snapshot_dir)
if prev and curr:
diff = file_size_diff_percent(prev, curr)
if diff > CHANGE_THRESHOLD_PERCENT:
print(f"ALERT: {dashboard['label']} changed by {diff:.1f}% — manual review needed")
print(f" Previous: {prev}")
print(f" Current: {curr}")
else:
print(f"OK: {dashboard['label']} ({diff:.1f}% change)")
File size difference is a fast proxy for visual change — a blank chart or missing widget produces a meaningfully different file size than a fully rendered one. For higher fidelity, use a pixel-diff library like pixelmatch (Node.js) or Pillow + numpy (Python) to compare images directly.
Use Case 2: Stakeholder Reporting
Capture weekly dashboard screenshots to embed in status reports, avoiding the need for stakeholders to log in:
from datetime import datetime
def generate_weekly_report_captures(dashboards: list, api_key: str, report_dir: str):
"""Capture a set of dashboard screenshots for weekly stakeholder report."""
week = datetime.now(timezone.utc).strftime("%Y-W%V")
output_dir = os.path.join(report_dir, week)
os.makedirs(output_dir, exist_ok=True)
report_assets = []
for dashboard in dashboards:
resp = requests.get(
"https://hermesforge.dev/api/screenshot",
params={
"url": dashboard["url"],
"width": 1920,
"format": "png",
"full_page": "false",
"wait_for": "networkidle",
"key": api_key
},
timeout=45
)
if resp.status_code == 200:
filename = f"{dashboard['label']}.png"
path = os.path.join(output_dir, filename)
with open(path, "wb") as f:
f.write(resp.content)
report_assets.append({"label": dashboard["label"], "path": path})
print(f"Captured: {dashboard['label']} → {path}")
time.sleep(2)
return report_assets
assets = generate_weekly_report_captures(DASHBOARDS, api_key, "reports")
print(f"\nWeekly report assets: {len(assets)} dashboards captured")
Use Case 3: Uptime Page Monitoring
Track your own status page or third-party service status pages visually:
STATUS_PAGES = [
{"label": "our-status", "url": "https://status.yourapp.com"},
{"label": "stripe-status", "url": "https://status.stripe.com"},
{"label": "aws-status", "url": "https://health.aws.amazon.com/health/status"},
]
def monitor_status_pages(pages: list, api_key: str):
for page in pages:
result = capture_dashboard(page["url"], f"status-monitoring/{page['label']}", api_key, page["label"])
if result["status"] == "ok":
print(f"Captured: {page['label']} ({result['size']:,} bytes)")
else:
print(f"FAILED: {page['label']} — HTTP {result.get('http_status', 'unknown')}")
time.sleep(2)
A status page that normally renders at 800KB and suddenly captures at 200KB is showing an incident banner — visually detectable without parsing the page content.
Scheduling with Cron
For hourly dashboard monitoring, add to crontab:
0 * * * * python3 /home/youruser/monitor_dashboards.py >> /var/log/dashboard-monitor.log 2>&1
For daily stakeholder report captures:
0 7 * * 1 python3 /home/youruser/weekly_report.py >> /var/log/weekly-report.log 2>&1
Rate Limit Planning
| Monitoring frequency | Dashboards | Daily captures | Recommended tier |
|---|---|---|---|
| Hourly | 3 dashboards | 72/day | Starter ($4/30-day, 200/day) |
| Every 4h | 5 dashboards | 30/day | Free (50/day) |
| Daily | 10 dashboards | 10/day | Free (50/day) |
| Hourly | 20 dashboards | 480/day | Pro ($9/30-day, 1000/day) |
Handling Auth-Protected Dashboards
Most production dashboards require authentication. If your dashboard has a public shareable link (Grafana, Datadog, Metabase, Looker all support this), use that URL directly — no auth required.
For dashboards without public share links, consider: - Embedding a public read-only view: Create a limited public view and screenshot that - Pre-rendering as static HTML: Export the chart data and render a static version at a public URL before screenshotting
The screenshot API captures what an unauthenticated browser would see. If the dashboard requires login, the screenshot will show the login page, not the dashboard — use shareable links where available.
Hermesforge Screenshot API handles JavaScript-heavy dashboards with wait_for=networkidle. Get your API key — 50 screenshots/day free.