Website monitoring
for developers
Playwright-based checks with retries, screenshots, alerts, and HTML reports. Zero config to get started.
Up and running in minutes
# Install
npm install -D @alosha/monitor
# monitor.config.ts
export default {
checks: [
{ name: 'Homepage', url: 'https://yoursite.com', interval: '5m' },
{ name: 'API', url: 'https://api.yoursite.com/health', interval: '1m' },
],
notify: { slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL } }
}
# Run once
npx monitor run
# Or watch continuously
npx monitor watchEverything you need to ship with confidence
- Real browser checksUses Playwright under the hood — catches JS errors, redirects, and anything a headless Chrome would catch.
- Automatic retriesConfigurable retry attempts before marking a check as failed. No false alarms from flaky networks.
- Screenshots on failureAutomatically saves a full-page screenshot whenever a check fails. Know exactly what went wrong.
- Multi-channel alertsEmail, Slack, Discord, or any webhook. Get notified where your team already lives.
- Built-in schedulerRun `monitor watch` and each check fires on its own interval — no cron, no external scheduler needed.
- HTML reportsAuto-generated report after every run. Open it in your browser, share it with your team.
The "Datadog tax": what hosted synthetics really cost
Per-run pricing punishes you for monitoring well
Hosted synthetic monitoring meters every browser run, so the exact levers that improve reliability — higher frequency, more regions, deeper journeys — are the levers that inflate the bill.
The cost of the hosted-SaaS approach
- Frequency = cost
Moving a check from 5-minute to 1-minute intervals multiplies its billed runs by five. Better detection time, five times the bill.
- Per-region multiplier
Each location you check from re-bills the same run, so multi-region coverage scales cost linearly with geography.
- Per-step browser pricing
Browser tests are billed per scripted step, so a realistic login-to-checkout journey costs more than a single page load.
With Monitor: Monitor runs as a devDependency in your CI or on a small box you already own. Frequency, regions and journey length change how thorough you are — not what you pay. The marginal cost of one more check run is zero.
Your checks and history live in someone else’s product
Synthetic tests authored in a vendor’s recorder — plus the failure history and screenshots — are trapped there. Leaving means re-writing every journey and losing the timeline, which is exactly what keeps the renewal quote climbing.
The cost of the hosted-SaaS approach
- Trapped scripts
Journeys built in a proprietary recorder don’t export into anything you can run yourself.
- History stays behind
Run history and failure screenshots live in the vendor; churning the subscription loses the record.
- Switching cost
Migrating providers means re-authoring every check in a new DSL — so the renewal always wins the build-vs-buy argument.
With Monitor: Checks are plain TypeScript in your repo, versioned alongside the app they test. Results, screenshots and HTML reports are written to your own storage. There is nothing to export and no renewal holding your monitoring hostage.
What 5 browser checks cost per month
Same workload — 5 checks at 5-minute intervals — priced three ways. Hosted figures use Datadog’s published list rate of $12 per 1,000 browser-test runs (~8,640 runs per check per month).
~129,600 browser runs/mo × $0.012
~43,200 browser runs/mo × $0.012
flat — runs, regions and journeys don’t change it
Production recipes
Fail your CI build when a critical user journey breaks
The problem: A deploy can pass unit tests but still break login or checkout in a real browser — and you find out from a customer.
import { run } from '@alosha/monitor'
const report = await run({
checks: [{
name: 'Login flow',
url: 'https://app.example.com/login',
steps: [
{ action: 'fill', selector: '#email', value: process.env.TEST_EMAIL! },
{ action: 'fill', selector: '#password', value: process.env.TEST_PASS! },
{ action: 'click', selector: 'button[type=submit]' },
{ action: 'waitForURL', value: '**/dashboard' }
],
assertions: [{ type: 'visible', selector: '[data-test=user-menu]' }]
}]
})
// Break the build if any check failed.
if (report.failed > 0) process.exit(1)Why it works: run() drives a real Chromium session through the journey and returns a structured RunReport, so a single exit-code check turns "does login still work?" into a CI gate that blocks the deploy before users ever see the break.
Run synthetic checks on a schedule with no server to babysit
The problem: You want real browser checks every few minutes, but you don’t want to own a box, a cron daemon, or a per-run monitoring bill to get them.
# .github/workflows/synthetics.yml
name: Synthetics
on:
schedule:
- cron: '*/15 * * * *' # every 15 minutes
workflow_dispatch: # ...and on demand
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx playwright install --with-deps chromium
# 'monitor run' exits 1 on failure — the run goes red AND
# the Slack notifier in monitor.config.ts fires.
- run: npx monitor run
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Why it works: GitHub schedules and runs the job for you — no server, no cron daemon, no hosted bill. monitor run exits non-zero when any check fails, so the workflow goes red and your config’s Slack notifier alerts at the same moment. (GitHub cron is best-effort, so treat very short intervals as approximate.)
Get a Slack alert with a screenshot the moment a page goes down
The problem: Uptime pings tell you a URL returns 200, not that the page actually rendered — and they rarely show you what the user saw.
import { watch } from '@alosha/monitor'
// Runs continuously, firing each check on its own interval.
await watch({
checks: [
{ name: 'Homepage', url: 'https://example.com', interval: '1m', maxResponseTimeMs: 2000 },
{ name: 'Checkout', url: 'https://example.com/checkout', interval: '5m' }
],
notify: { slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL! } }
})Why it works: watch() schedules each check independently and, on failure, captures a full-page screenshot before posting to Slack — so your first signal of an outage is an alert with visual proof, not a support ticket.
Page on-call through PagerDuty when a critical check fails
The problem: A failed checkout at 3am needs to wake someone. Monitor ships Slack, Discord, email and webhook notifiers — but no native PagerDuty integration.
import { run } from '@alosha/monitor'
const report = await run({
checks: [
{ name: 'Checkout', url: 'https://app.example.com/checkout', maxResponseTimeMs: 3000 }
]
})
// No built-in PagerDuty notifier — page on-call yourself from the
// structured report via the Events API v2.
const down = report.results.filter((r) => r.status === 'fail')
await Promise.all(down.map((r) =>
fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: 'trigger',
dedup_key: `monitor-${r.name}`, // collapse repeats into one incident
payload: {
summary: `${r.name} is DOWN — ${r.error ?? 'check failed'}`,
source: r.url,
severity: 'critical'
}
})
})
))
if (report.failed > 0) process.exit(1)Why it works: run() returns every check as a structured CheckResult, so mapping failures onto PagerDuty’s Events API v2 is a few lines — a trigger event plus a stable dedup_key that collapses repeat alerts into a single incident. You get real on-call escalation without waiting for a built-in integration.
Built to pass a dependency review
| Metric / concern | What ships |
|---|---|
| Check engine | Playwright (Chromium) |
| Data isolation | Runs in your CI / infra |
| Type safety | Ships .d.ts (ESM + CJS) |
| Install model | devDependency + CLI |
| Alerting | Slack · Discord · email · webhook |
| Cost model | Flat · $0 per run |
| Licensing | MIT |
Start monitoring in minutes
- Custom checks, assertions or notifier integrations on request
- Help wiring Monitor into your CI/CD pipeline
- Priority bug fixes and answers straight from the maintainer