DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to get a screenshot in Slack when your website looks broken

How to Get a Screenshot in Slack When Your Website Looks Broken

Uptime monitors like Pingdom and Better Uptime check that your server responds with a 200. They don't check whether the page actually renders correctly — a React app that crashes on load, a blank white screen from a failed API call, or a CSS file that didn't deploy all return HTTP 200.

Here's how to add a screenshot to every alert: when your monitor fires, capture what the page actually looks like and attach it to the Slack message.

The pattern

monitor fires webhook → take screenshot → upload to Slack → post alert with image
Enter fullscreen mode Exit fullscreen mode

Receive the webhook and post with screenshot

import express from "express";
import { WebClient } from "@slack/web-api";
import fs from "fs/promises";
import path from "path";
import os from "os";

const app = express();
app.use(express.json());

const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
const ALERT_CHANNEL = process.env.SLACK_ALERT_CHANNEL; // e.g. "#incidents"

app.post("/webhooks/uptime", async (req, res) => {
  const { url, status, message } = req.body;
  res.json({ ok: true }); // acknowledge immediately

  await alertWithScreenshot({ url, status, message });
});

async function alertWithScreenshot({ url, status, message }) {
  // 1. Screenshot the broken page
  const screenshotRes = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: {
      "x-api-key": process.env.PAGEBOLT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url,
      fullPage: false,
      blockBanners: true,
    }),
  });

  const imageBuffer = Buffer.from(await screenshotRes.arrayBuffer());

  // 2. Upload image to Slack
  const tmpFile = path.join(os.tmpdir(), `alert-${Date.now()}.png`);
  await fs.writeFile(tmpFile, imageBuffer);

  const upload = await slack.filesUploadV2({
    channel_id: ALERT_CHANNEL,
    file: tmpFile,
    filename: "screenshot.png",
    initial_comment: [
      `🚨 *Site alert* — ${message || `Status: ${status}`}`,
      `URL: ${url}`,
      `Time: ${new Date().toUTCString()}`,
    ].join("\n"),
  });

  await fs.unlink(tmpFile);
  console.log(`Alert posted for ${url}: ${upload.ok}`);
}
Enter fullscreen mode Exit fullscreen mode

Stand-alone monitor (no external uptime service)

Run this on a cron to check your own pages and alert if anything looks wrong:

// monitor.js
import cron from "node-cron";

const PAGES = [
  { name: "Home", url: "https://yourapp.com" },
  { name: "Pricing", url: "https://yourapp.com/pricing" },
  { name: "Login", url: "https://yourapp.com/login" },
  { name: "App dashboard", url: "https://app.yourapp.com/dashboard" },
];

// Check every 5 minutes
cron.schedule("*/5 * * * *", async () => {
  for (const page of PAGES) {
    await checkPage(page);
  }
});

async function checkPage({ name, url }) {
  try {
    const start = Date.now();
    const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
    const ms = Date.now() - start;

    if (!res.ok) {
      await alertWithScreenshot({
        url,
        status: res.status,
        message: `${name} returned HTTP ${res.status} (${ms}ms)`,
      });
      return;
    }

    // Optional: check page content (detect blank screens)
    const text = await res.text();
    if (text.length < 500 || text.includes("Application Error")) {
      await alertWithScreenshot({
        url,
        status: res.status,
        message: `${name} returned 200 but content looks wrong (${text.length} chars)`,
      });
    }
  } catch (err) {
    await alertWithScreenshot({
      url,
      status: 0,
      message: `${name} unreachable: ${err.message}`,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Detect visual changes (not just HTTP errors)

Compare the current screenshot to a baseline — alert if it looks different:

import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";

async function checkVisualChange({ name, url, baselinePath }) {
  const current = await takeScreenshot(url);

  let baseline;
  try {
    baseline = await fs.readFile(baselinePath);
  } catch {
    // No baseline yet — save current as baseline
    await fs.writeFile(baselinePath, current);
    console.log(`Baseline saved for ${name}`);
    return;
  }

  const img1 = PNG.sync.read(baseline);
  const img2 = PNG.sync.read(current);
  const { width, height } = img1;
  const diff = new PNG({ width, height });

  const changed = pixelmatch(img1.data, img2.data, diff.data, width, height, {
    threshold: 0.1,
  });

  const diffPct = (changed / (width * height)) * 100;

  if (diffPct > 5) {
    // More than 5% of pixels changed
    await slack.chat.postMessage({
      channel: ALERT_CHANNEL,
      text: `⚠️ *Visual change detected* on ${name} (${diffPct.toFixed(1)}% of pixels changed)`,
      attachments: [{ text: url }],
    });
    // Optionally update baseline after alerting
  }
}
Enter fullscreen mode Exit fullscreen mode

GitHub Actions — check production after deploy

Add a post-deploy check as the final CI step:

  smoke-test:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Visual smoke test
        env:
          PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          SLACK_ALERT_CHANNEL: "#deployments"
        run: node scripts/smoke-test.js
Enter fullscreen mode Exit fullscreen mode
// scripts/smoke-test.js
const PAGES = ["https://yourapp.com", "https://yourapp.com/pricing"];

for (const url of PAGES) {
  const res = await fetch("https://pagebolt.dev/api/v1/screenshot", {
    method: "POST",
    headers: { "x-api-key": process.env.PAGEBOLT_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ url, fullPage: false, blockBanners: true }),
  });

  const image = Buffer.from(await res.arrayBuffer());

  // Post screenshot to Slack as confirmation the deploy looks good
  await postToSlack({
    text: `✅ Deploy smoke test — ${url}`,
    image,
  });
}
Enter fullscreen mode Exit fullscreen mode

A successful deploy posts a "looks good" screenshot. A failed deploy posts the broken state. Either way, your team sees exactly what the page looks like the moment the deploy completes.


Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

Top comments (0)