Angular apps fail in non-obvious ways. Your server-side rendered pages might return 200 with a blank shell because a data-fetch timed out. Your backend API might be down while the Angular SPA happily serves from CDN cache. Your ErrorHandler might swallow exceptions silently. Vigilmon surfaces these failures before users report them. This guide covers adding health endpoints, HTTP monitors, heartbeat checks, and webhook alerts to an Angular 17+ application.
What You'll Build
- A
/api/healthendpoint in your Angular SSR Express server - Vigilmon HTTP monitors for the health endpoint and the SPA homepage
- A global Angular
ErrorHandlerthat fires a Vigilmon webhook on unhandled errors - Email and webhook alert channels
Prerequisites
- An Angular 17+ project (standalone component style)
- Angular SSR installed (
ng add @angular/ssr) — required for the server-side health endpoint - A free Vigilmon account
If your Angular app is a pure SPA without SSR, skip to Step 2 — you can still monitor the hosting URL and add the client-side error handler.
Step 1: Add a Health Endpoint to the Angular SSR Server
When you install @angular/ssr, Angular generates a server.ts file that bootstraps an Express server to handle SSR. You can add a health route before the Angular Universal handler — it runs on the same Node.js process and can check your app's real dependencies.
// server.ts
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express, { Request, Response } from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// ─── Health endpoint ─────────────────────────────────────────────────────
server.get('/api/health', async (_req: Request, res: Response) => {
const checks: Record<string, string> = {};
let status = 'ok';
// Example: check an external API your app depends on
try {
const response = await fetch('https://your-backend-api.com/ping', {
signal: AbortSignal.timeout(3000),
});
checks.api = response.ok ? 'ok' : `status_${response.status}`;
if (!response.ok) status = 'degraded';
} catch (err: any) {
checks.api = `error: ${err?.message ?? 'unreachable'}`;
status = 'degraded';
}
res
.status(status === 'ok' ? 200 : 503)
.set('Cache-Control', 'no-store')
.json({
status,
timestamp: new Date().toISOString(),
checks,
});
});
// ─────────────────────────────────────────────────────────────────────────
// Serve static files from /browser
server.get(
'**',
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
// Angular SSR handler (must come after the health route)
server.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();
Test it locally:
npm run build && node dist/your-app/server/server.mjs &
curl -s http://localhost:4000/api/health | jq
# {
# "status": "ok",
# "timestamp": "2025-06-29T10:00:00.000Z",
# "checks": { "api": "ok" }
# }
No external dependencies? Use a minimal health check that confirms the SSR process is alive:
server.get('/api/health', (_req, res) => {
res
.status(200)
.set('Cache-Control', 'no-store')
.json({ status: 'ok', timestamp: new Date().toISOString() });
});
Even this simple endpoint lets Vigilmon detect if your Node.js/SSR process has crashed or become unresponsive.
Step 2: Create Vigilmon HTTP Monitors
Log in to Vigilmon and set up two monitors.
Monitor 1: Health Endpoint
| Field | Value |
|---|---|
| URL | https://yourapp.com/api/health |
| Method | GET |
| Check interval | 60 seconds |
| Expected status | 200 |
| Timeout | 10 seconds |
Monitor 2: SPA Homepage
| Field | Value |
|---|---|
| URL | https://yourapp.com |
| Method | GET |
| Check interval | 5 minutes |
| Expected status | 200 |
| Keyword check | A string unique to a correctly rendered page (your app name, a navigation label, etc.) |
The keyword check guards against the case where Angular SSR returns 200 but the page body is empty or shows a render error. A pure HTTP status check would miss this — the keyword check won't.
Pure SPA / CDN-hosted apps: If you're deploying to Netlify, Vercel (static mode), Firebase Hosting, or similar, point Monitor 2 at the CDN URL and skip Monitor 1. Add a separate monitor for your backend API.
Step 3: Global Error Handler with Webhook Alerts
Angular's ErrorHandler class is the framework's central exception boundary. Every unhandled error — from component code, services, HTTP interceptors, and route guards — flows through it. Override it to fire a Vigilmon webhook whenever a user hits a crash.
Create the Custom Error Handler
// src/app/core/vigilmon-error-handler.ts
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable()
export class VigilmonErrorHandler implements ErrorHandler {
// Use a plain fetch fallback so errors during HttpClient bootstrap don't recurse
private readonly webhookUrl = environment.vigilmonWebhookUrl;
handleError(error: unknown): void {
const err = error instanceof Error ? error : new Error(String(error));
// Always log to console — never suppress errors silently
console.error('[VigilmonErrorHandler]', err);
if (!this.webhookUrl) return;
// Fire-and-forget; don't let the alert mechanism cause more errors
fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: err.message,
stack: err.stack?.slice(0, 500),
url: typeof window !== 'undefined' ? window.location.href : 'ssr',
timestamp: new Date().toISOString(),
}),
}).catch(() => {
// Silently absorb — the app is already in an error state
});
}
}
Register It in Your Application Config
// src/app/app.config.ts
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
import { VigilmonErrorHandler } from './core/vigilmon-error-handler';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
// Replace the default ErrorHandler with the Vigilmon-aware one
{ provide: ErrorHandler, useClass: VigilmonErrorHandler },
],
};
Add the Webhook URL to Environments
// src/environments/environment.ts (development)
export const environment = {
production: false,
vigilmonWebhookUrl: '', // Leave empty in dev to avoid noise
};
// src/environments/environment.prod.ts (production)
export const environment = {
production: true,
vigilmonWebhookUrl: 'https://vigilmon.online/api/webhooks/YOUR-UUID',
};
Get the webhook URL from Vigilmon → Alert Channels → Add Channel → Webhook.
Test the Error Handler
Throw a deliberate error in a component to confirm the webhook fires:
// Temporarily add to any component's ngOnInit
ngOnInit() {
throw new Error('Vigilmon webhook test — remove me');
}
Check your Vigilmon webhook log and confirm the payload arrives. Then remove the throw.
Step 4: HTTP Interceptor for API Failure Tracking (Optional)
If your Angular app calls a backend API, an HTTP interceptor can track systematic failures — like a spike in 503 responses — and ping Vigilmon:
// src/app/core/http-error.interceptor.ts
import {
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpErrorResponse,
} from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { environment } from '../../environments/environment';
let recentErrors = 0;
let resetTimer: ReturnType<typeof setTimeout> | null = null;
const ERROR_THRESHOLD = 5; // alert after 5 errors in a 60s window
export const httpErrorInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn,
) => {
return next(req).pipe(
tap({
error: (err: unknown) => {
if (!(err instanceof HttpErrorResponse)) return;
if (err.status < 500) return; // only server errors
recentErrors++;
if (!resetTimer) {
resetTimer = setTimeout(() => {
recentErrors = 0;
resetTimer = null;
}, 60_000);
}
if (recentErrors >= ERROR_THRESHOLD && environment.vigilmonWebhookUrl) {
fetch(environment.vigilmonWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: `Angular HTTP: ${recentErrors} server errors in 60s`,
latestStatus: err.status,
latestUrl: req.url,
timestamp: new Date().toISOString(),
}),
}).catch(() => {});
recentErrors = 0; // reset so it doesn't flood
}
},
}),
);
};
Register it:
// src/app/app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { httpErrorInterceptor } from './core/http-error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([httpErrorInterceptor])),
{ provide: ErrorHandler, useClass: VigilmonErrorHandler },
],
};
Step 5: Alert Routing in Vigilmon
Configure under Vigilmon → Alert Channels → Email. You'll receive alerts when:
/api/healthreturns non-200- The SPA homepage keyword check fails
- A webhook payload arrives from the error handler
Slack Webhook
- Create a Slack incoming webhook for your team channel
- Vigilmon → Alert Channels → Add Channel → Webhook → paste the Slack URL
- Assign the Slack channel to both HTTP monitors
Example Slack alert:
🔴 *yourapp.com/api/health* is DOWN
Status: 503 | checks.api: error: Connection refused
Duration: 4m 12s
Step 6: Test the Full Loop
- Kill the SSR process: stop your Node.js server temporarily — the
/api/healthmonitor should alert within 60 seconds. - Break an API dependency: return a 503 from your backend — the health check's
checks.apifield should flip to degraded and Vigilmon should alert. - Keyword check: temporarily change the homepage so the expected keyword is absent — the SSR monitor should alert.
- Trigger an unhandled error: throw inside a component — confirm the Vigilmon webhook fires and appears in your Slack channel.
- Recover: fix each issue and confirm Vigilmon sends "back online" notifications.
Production Checklist
- [ ]
/api/healthroute registered before the Angular Universal handler inserver.ts - [ ]
Cache-Control: no-storeset on the health response - [ ] Vigilmon health endpoint monitor running at 60s intervals
- [ ] Vigilmon SSR homepage monitor with keyword check
- [ ]
VigilmonErrorHandlerregistered inapp.config.ts - [ ]
vigilmonWebhookUrlset inenvironment.prod.ts - [ ] HTTP error interceptor registered (optional but recommended)
- [ ] Alert channels tested end-to-end before going to production
Wrapping Up
Your Angular app now has layered monitoring:
- Uptime: Vigilmon polls
/api/healthevery minute, checking real dependencies through the Express server - Rendering: a keyword check on the homepage confirms Angular SSR is producing real content
- Runtime errors: the custom
ErrorHandlerfires a webhook when users hit unhandled exceptions - API health: the HTTP interceptor alerts on systematic server-side failures
These layers work together: uptime confirms the server is alive, keyword check confirms it's rendering correctly, and error handler catches the cases that slip past both.
Sign up for Vigilmon — free tier includes multiple monitors with 1-minute checks, no credit card required.
Questions or Angular-specific edge cases? Drop them in the comments!