tutorial

Monitoring Jest Test Pipelines with Vigilmon: Catch Silent Test Suite Failures

Use Vigilmon heartbeat monitors to detect when your Jest test suites stop running — scheduled test regressions, stuck CI jobs, and pipelines that silently go dark.

Your Jest test suite ran green this morning. Or did it? The CI step that was supposed to run it was skipped three days ago when someone accidentally mis-scoped a path filter. The suite hasn't run since, your coverage reports are stale, and nobody knows because Jest reports zero failures when it runs zero tests.

This is the silent test-suite failure problem. HTTP monitors can't catch it — there's nothing to ping. The fix is heartbeat monitoring: your Jest run pings a unique URL after every successful suite execution, and if Vigilmon doesn't receive that ping within the expected window, you get alerted.

This tutorial shows you how to instrument Jest test pipelines with Vigilmon heartbeat monitors so you know when tests stop running, not just when they fail.

What You'll Cover

  • Heartbeat monitoring for Jest test suites
  • Detecting suites that stop running entirely
  • Alerting on long-running or hung Jest processes
  • Multi-environment test monitoring (unit, integration, e2e)
  • Integrating with CI/CD pipelines (GitHub Actions, GitLab CI)

Prerequisites


The Problem: What Jest's Built-In Reporting Misses

Jest gives you pass/fail counts, coverage reports, and timing information — but only when it runs. It won't alert you when:

  • A CI path filter silently excludes your test directory
  • A --testPathPattern flag accidentally matches nothing (zero tests run, exit code 0)
  • A scheduled test job is disabled or never triggered
  • Jest hangs on an open handle and the CI runner times out without marking the job failed
  • A --bail flag causes early exit after one failure, leaving most tests un-run

Vigilmon's heartbeat pattern closes these gaps by requiring an explicit "I finished successfully" signal from outside Jest itself.


Step 1: Create a Heartbeat Monitor in Vigilmon

  1. Log in to Vigilmon and click New Monitor → Heartbeat.
  2. Name it something specific: Jest Unit Tests — main or Jest E2E — nightly.
  3. Set the Expected interval to match your test schedule. For a test suite that runs on every push, use 2 hours (gives CI queue time a buffer). For nightly suites, use 25 hours.
  4. Set the Grace period to 30 minutes for push-triggered suites, or 1 hour for scheduled suites.
  5. Save and copy the Ping URL — it looks like https://vigilmon.online/api/heartbeat/<unique-id>.

Step 2: Add a Global Setup File to Ping After Tests Pass

Jest's globalTeardown hook runs after all test suites complete. Use it to send the heartbeat ping on success.

Create jest.teardown.js in your project root:

// jest.teardown.js
const https = require("https");

module.exports = async function globalTeardown() {
  const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL;

  if (!heartbeatUrl) {
    return; // skip in local dev if the env var isn't set
  }

  await new Promise((resolve, reject) => {
    const url = new URL(heartbeatUrl);
    const req = https.request(
      {
        hostname: url.hostname,
        path: url.pathname + url.search,
        method: "POST",
        timeout: 10000,
      },
      (res) => {
        res.resume();
        resolve();
      }
    );
    req.on("error", reject);
    req.on("timeout", () => {
      req.destroy();
      reject(new Error("Heartbeat ping timed out"));
    });
    req.end();
  });
};

Register it in your Jest config (jest.config.js or package.json):

// jest.config.js
module.exports = {
  globalTeardown: "./jest.teardown.js",
  // ... rest of your config
};

Or in package.json:

{
  "jest": {
    "globalTeardown": "./jest.teardown.js"
  }
}

Important: globalTeardown only runs when all test suites pass (exit code 0). If any suite fails, Jest exits with a non-zero code and skips teardown — exactly the behaviour you want. No ping → Vigilmon alerts you.


Step 3: Integrate with GitHub Actions

# .github/workflows/test.yml
name: Jest Tests

on:
  push:
    branches: [main, develop]
  schedule:
    - cron: "0 3 * * *"  # nightly 03:00 UTC

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run Jest
        env:
          VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_JEST_HEARTBEAT_URL }}
        run: npx jest --ci --coverage
        # globalTeardown fires here on success and pings Vigilmon

Store the heartbeat URL in GitHub repo → Settings → Secrets and variables → Actions → New repository secret:

  • Name: VIGILMON_JEST_HEARTBEAT_URL
  • Value: ping URL from Step 1

If you prefer to handle the ping at the workflow level rather than inside Jest:

      - name: Run Jest
        run: npx jest --ci --coverage

      - name: Ping Vigilmon heartbeat
        if: success()
        run: |
          curl -fsS -X POST "${{ secrets.VIGILMON_JEST_HEARTBEAT_URL }}" \
            --max-time 10 \
            --retry 3 \
            --retry-delay 2

Use the globalTeardown approach for more granular control (e.g., multiple jobs sharing one heartbeat). Use the workflow step approach when you want the CI configuration to own the monitoring signal.


Step 4: Integrate with GitLab CI

# .gitlab-ci.yml
test:jest:
  image: node:20-alpine
  stage: test
  script:
    - npm ci
    - VIGILMON_HEARTBEAT_URL=$VIGILMON_JEST_HEARTBEAT_URL npx jest --ci --coverage
  only:
    - main
    - schedules

Set VIGILMON_JEST_HEARTBEAT_URL in GitLab → Settings → CI/CD → Variables.

For the ping-in-after-script approach:

test:jest:
  image: node:20-alpine
  stage: test
  script:
    - npm ci
    - npx jest --ci --coverage
  after_script:
    - |
      if [ "$CI_JOB_STATUS" = "success" ]; then
        curl -fsS -X POST "$VIGILMON_JEST_HEARTBEAT_URL" --max-time 10 --retry 3
      fi

Step 5: Monitor Multiple Test Suites Separately

Large projects often split tests into unit, integration, and end-to-end suites with different run schedules. Create one heartbeat monitor per suite:

| Suite | Jest config | Monitor name | Interval | |---|---|---|---| | Unit tests | jest.unit.config.js | Jest Unit — push | 2 h | | Integration tests | jest.integration.config.js | Jest Integration — push | 4 h | | E2E tests | jest.e2e.config.js | Jest E2E — nightly | 25 h |

In each config, point to a different teardown file with its own VIGILMON_HEARTBEAT_URL:

# .github/workflows/test.yml (multi-suite)
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npx jest --config jest.unit.config.js --ci
        env:
          VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_JEST_UNIT_HEARTBEAT_URL }}

  integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npx jest --config jest.integration.config.js --ci
        env:
          VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_JEST_INTEGRATION_HEARTBEAT_URL }}

Step 6: Detect Open Handle Hangs

Jest's --detectOpenHandles flag helps find resources that keep the process alive after tests finish. But if Jest hangs indefinitely, no teardown runs and no ping reaches Vigilmon — which is exactly the alert you want.

Add a CI-level timeout as a safety net:

      - name: Run Jest
        timeout-minutes: 15
        env:
          VIGILMON_HEARTBEAT_URL: ${{ secrets.VIGILMON_JEST_HEARTBEAT_URL }}
        run: npx jest --ci --forceExit

--forceExit forces Jest to exit after all tests complete, even with open handles. Combined with the Vigilmon heartbeat, you'll know if the forced exit happened without a full successful run (because the teardown will have already fired, or not, before forceExit kicks in).


Step 7: Alert Channels

In Vigilmon, go to Notifications → New Channel and configure:

  • Email — immediate alert when a heartbeat is missed
  • Slack webhook — ping your #dev or #ci-alerts channel

When a Jest suite stops pinging:

🔴 MISSED HEARTBEAT: Jest Unit — push
Last ping: 3 hours ago
Expected interval: 2 hours

When it recovers:

✅ HEARTBEAT RECOVERED: Jest Unit — push
Gap: 3 hours

What You're Now Catching

| Silent failure mode | How Vigilmon detects it | |---|---| | Path filter excludes test directory — 0 tests run | Jest exits 0, teardown fires or doesn't — depends on your check | | --testPathPattern matches nothing | Exit code 0, no teardown (add a test-count check) | | CI job skipped by branch filter | No ping within interval → alert | | Jest hangs on open handle | No teardown → no ping → alert after grace period | | Nightly suite accidentally disabled | No ping → alert after interval | | Tests fail and job exits non-zero | Teardown skipped → no ping → alert |


Bonus: Fail on Zero Tests

Add a check to catch the edge case where Jest exits 0 but ran nothing:

{
  "scripts": {
    "test": "jest --ci && node -e \"const r=require('./coverage/coverage-summary.json');if(!r.total.lines.total)process.exit(1)\""
  }
}

Or use Jest's --passWithNoTests flag in reverse — omit it from your CI command so that zero matched tests causes a non-zero exit.


Jest is thorough — when it runs. Vigilmon's heartbeat monitors give you the external verification that it's actually running on schedule, not just passing when someone remembers to trigger it.

Start monitoring your Jest pipelines today — register free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →