Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

151 lines
4.2 KiB
JavaScript

// API Health Check Endpoint - Neon Database
// Monitors critical API endpoints and logs results
// Called by Vercel Cron every 15 minutes
import { neon } from '@neondatabase/serverless';
function getNeonClient() {
const connectionString = process.env.NEON_DATABASE_URL;
if (!connectionString) {
throw new Error('NEON_DATABASE_URL not configured');
}
return neon(connectionString);
}
const ENDPOINTS_TO_CHECK = [
{ url: 'https://www.aitmpl.com/api/track-download-supabase', method: 'OPTIONS' },
{ url: 'https://www.aitmpl.com/api/track-command-usage', method: 'OPTIONS' },
{ url: 'https://www.aitmpl.com/api/track-website-events', method: 'OPTIONS' }
];
const TIMEOUT_MS = 10000;
async function checkEndpoint(endpoint) {
const startTime = Date.now();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
const response = await fetch(endpoint.url, {
method: endpoint.method,
signal: controller.signal
});
clearTimeout(timeoutId);
const responseTimeMs = Date.now() - startTime;
return {
endpoint: endpoint.url,
method: endpoint.method,
statusCode: response.status,
responseTimeMs,
errorMessage: null
};
} catch (error) {
const responseTimeMs = Date.now() - startTime;
return {
endpoint: endpoint.url,
method: endpoint.method,
statusCode: 0,
responseTimeMs,
errorMessage: error.name === 'AbortError' ? 'Timeout' : error.message
};
}
}
async function sendDiscordAlert(failures) {
const webhookUrl = process.env.DISCORD_WEBHOOK_URL_CHANGELOG;
if (!webhookUrl || failures.length === 0) return;
const failureList = failures.map(f =>
`- \`${f.endpoint}\`: ${f.errorMessage || `HTTP ${f.statusCode}`} (${f.responseTimeMs}ms)`
).join('\n');
try {
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
embeds: [{
title: 'API Health Alert',
description: `The following endpoints are experiencing issues:\n${failureList}`,
color: 0xff4444,
timestamp: new Date().toISOString()
}]
})
});
} catch (error) {
console.error('Failed to send Discord alert:', error.message);
}
}
export default async function handler(req, res) {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.status(200).json({ ok: true });
}
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed', allowed: ['GET'] });
}
try {
// Check all endpoints in parallel
const results = await Promise.all(ENDPOINTS_TO_CHECK.map(checkEndpoint));
const sql = getNeonClient();
// Log each result
for (const result of results) {
await sql`
INSERT INTO api_health_logs (
endpoint, method, status_code, response_time_ms, error_message
) VALUES (
${result.endpoint},
${result.method},
${result.statusCode},
${result.responseTimeMs},
${result.errorMessage}
)
`;
}
// Identify failures (status >= 500, status 0 for network errors, or timeout > 10s)
const failures = results.filter(r =>
r.statusCode === 0 || r.statusCode >= 500 || r.responseTimeMs >= TIMEOUT_MS
);
// Send Discord alert if there are failures
if (failures.length > 0) {
await sendDiscordAlert(failures);
}
const allHealthy = failures.length === 0;
res.status(200).json({
success: true,
healthy: allHealthy,
results: results.map(r => ({
endpoint: r.endpoint,
status: r.statusCode,
responseTime: r.responseTimeMs,
error: r.errorMessage
})),
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Health check error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Health check failed',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
}