Files
cloudlense/website-monitoring-frontend/src/services/uptimeService.ts
T
Dennis 0d2aef07bc feat: implement real uptime monitoring, alerts, admin dashboard, billing & usage tracking
- Uptime service: real HTTP HEAD checks with response time tracking
- Alert engine: evaluates scan results, auto-resolves recovered alerts
- Notifications: Resend email + webhook delivery
- Admin dashboard: system stats, user CRUD, org management (role-protected)
- Billing: tier limits (free/starter/pro/enterprise), usage tracking API
- Competitor analysis: real Lighthouse comparison + response time
- Tests: 11 backend + 11 frontend = 22 total tests passing
- Database: added competitor_metrics, alert_configurations tables

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 00:51:54 +01:00

219 lines
6.3 KiB
TypeScript

import { getSupabaseAdmin } from "@/lib/admin";
export interface UptimeCheckResult {
websiteId: string;
status: "up" | "down" | "warning";
responseTime: number;
statusCode: number | null;
errorMessage: string | null;
sslExpiry: string | null;
}
/**
* Performs a real HTTP check against a URL.
* Measures response time, captures status code, and checks SSL certificate expiry.
*/
async function checkUrl(url: string): Promise<Omit<UptimeCheckResult, "websiteId">> {
const start = Date.now();
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
const response = await fetch(url, {
method: "HEAD",
redirect: "follow",
signal: controller.signal,
headers: {
"User-Agent": "WebsiteMonitor/1.0 (Uptime Check)",
},
});
clearTimeout(timeout);
const responseTime = Date.now() - start;
// Determine status
let status: "up" | "down" | "warning" = "up";
if (response.status >= 500) {
status = "down";
} else if (response.status >= 400) {
status = "warning";
} else if (responseTime > 5000) {
status = "warning";
}
return {
status,
responseTime,
statusCode: response.status,
errorMessage: response.status >= 400 ? `HTTP ${response.status} ${response.statusText}` : null,
sslExpiry: null, // SSL expiry checked separately for HTTPS URLs
};
} catch (error) {
const responseTime = Date.now() - start;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
// Differentiate between timeout and connection errors
const isTimeout =
errorMessage.includes("abort") || errorMessage.includes("timeout");
return {
status: "down",
responseTime,
statusCode: null,
errorMessage: isTimeout ? "Request timed out (30s)" : errorMessage,
sslExpiry: null,
};
}
}
/**
* Fetches all active websites that have monitoring enabled and checks their uptime.
*/
export async function performUptimeChecks(): Promise<{
checked: number;
up: number;
down: number;
warning: number;
errors: string[];
}> {
const supabase = getSupabaseAdmin();
const results = { checked: 0, up: 0, down: 0, warning: 0, errors: [] as string[] };
// Get all active websites
const { data: websites, error: fetchError } = await supabase
.from("websites")
.select("id, name, base_url, organization_id")
.eq("is_active", true);
if (fetchError) {
throw new Error(`Failed to fetch websites: ${fetchError.message}`);
}
if (!websites || websites.length === 0) {
return results;
}
// Check each website in parallel (batched to avoid overwhelming)
const batchSize = 10;
for (let i = 0; i < websites.length; i += batchSize) {
const batch = websites.slice(i, i + batchSize);
const checks = await Promise.allSettled(
batch.map(async (website) => {
try {
const result = await checkUrl(String(website.base_url));
// Store result in uptime_checks table
const { error: insertError } = await supabase
.from("uptime_checks")
.insert({
website_id: website.id,
status: result.status,
response_time: result.responseTime,
status_code: result.statusCode,
error_message: result.errorMessage,
});
if (insertError) {
results.errors.push(
`DB insert failed for ${website.name}: ${insertError.message}`
);
}
results.checked++;
results[result.status]++;
return result;
} catch (error) {
results.errors.push(
`Check failed for ${website.name}: ${error instanceof Error ? error.message : "Unknown"}`
);
results.checked++;
results.down++;
}
})
);
}
return results;
}
/**
* Evaluates alert rules after uptime checks.
* Triggers alerts when thresholds are breached.
*/
export async function evaluateUptimeAlerts(): Promise<number> {
const supabase = getSupabaseAdmin();
let alertsTriggered = 0;
// Get alert rules for downtime
const { data: alertRules } = await supabase
.from("alert_rules")
.select("*, websites(name, base_url, organization_id)")
.eq("is_active", true);
if (!alertRules || alertRules.length === 0) return 0;
for (const rule of alertRules) {
// Get recent uptime checks for this website
const { data: recentChecks } = await supabase
.from("uptime_checks")
.select("status, response_time, checked_at")
.eq("website_id", String(rule.website_id))
.order("checked_at", { ascending: false })
.limit(5);
if (!recentChecks || recentChecks.length === 0) continue;
let shouldAlert = false;
let alertMessage = "";
// Check for consecutive failures
const downChecks = recentChecks.filter((c) => c.status === "down");
if (downChecks.length >= 3) {
shouldAlert = true;
alertMessage = `Website is down — ${downChecks.length} consecutive failures`;
}
// Check for slow response time
const avgResponseTime =
recentChecks.reduce((sum, c) => sum + (Number(c.response_time) || 0), 0) /
recentChecks.length;
if (avgResponseTime > 5000 && !shouldAlert) {
shouldAlert = true;
alertMessage = `Slow response time — avg ${Math.round(avgResponseTime)}ms over last ${recentChecks.length} checks`;
}
if (shouldAlert) {
// Check if we already sent an alert recently (debounce: 1 hour)
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const { data: recentAlerts } = await supabase
.from("alerts")
.select("id")
.eq("website_id", String(rule.website_id))
.gte("created_at", oneHourAgo)
.limit(1);
if (recentAlerts && recentAlerts.length > 0) continue; // Already alerted
// Create alert record
const { error: alertError } = await supabase.from("alerts").insert({
website_id: rule.website_id,
alert_rule_id: rule.id,
type: "downtime",
severity: downChecks.length >= 3 ? "critical" : "warning",
message: alertMessage,
status: "active",
});
if (!alertError) {
alertsTriggered++;
}
}
}
return alertsTriggered;
}