diff --git a/server/private/lib/alerts/sendAlertWebhook.ts b/server/private/lib/alerts/sendAlertWebhook.ts index 5f309d577..5656026bc 100644 --- a/server/private/lib/alerts/sendAlertWebhook.ts +++ b/server/private/lib/alerts/sendAlertWebhook.ts @@ -15,6 +15,8 @@ import logger from "@server/logger"; import { AlertContext, WebhookAlertConfig } from "@server/routers/alertRule/types"; const REQUEST_TIMEOUT_MS = 15_000; +const MAX_RETRIES = 3; +const RETRY_BASE_DELAY_MS = 500; /** * Sends a single webhook POST for an alert event. @@ -49,45 +51,70 @@ export async function sendAlertWebhook( const body = JSON.stringify(payload); const headers = buildHeaders(webhookConfig); - const controller = new AbortController(); - const timeoutHandle = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + let lastError: Error | undefined; - let response: Response; - try { - response = await fetch(url, { - method: webhookConfig.method ?? "POST", - headers, - body, - signal: controller.signal - }); - } catch (err: unknown) { - const isAbort = err instanceof Error && err.name === "AbortError"; - if (isAbort) { - throw new Error( - `Alert webhook: request to "${url}" timed out after ${REQUEST_TIMEOUT_MS} ms` - ); - } - const msg = err instanceof Error ? err.message : String(err); - throw new Error(`Alert webhook: request to "${url}" failed – ${msg}`); - } finally { - clearTimeout(timeoutHandle); - } + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + const controller = new AbortController(); + const timeoutHandle = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); - if (!response.ok) { - let snippet = ""; + let response: Response; try { - const text = await response.text(); - snippet = text.slice(0, 300); - } catch { - // best-effort + response = await fetch(url, { + method: webhookConfig.method ?? "POST", + headers, + body, + signal: controller.signal + }); + } catch (err: unknown) { + clearTimeout(timeoutHandle); + const isAbort = err instanceof Error && err.name === "AbortError"; + if (isAbort) { + lastError = new Error( + `Alert webhook: request to "${url}" timed out after ${REQUEST_TIMEOUT_MS} ms` + ); + } else { + const msg = err instanceof Error ? err.message : String(err); + lastError = new Error(`Alert webhook: request to "${url}" failed – ${msg}`); + } + if (attempt < MAX_RETRIES) { + const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1); + logger.warn( + `Alert webhook: attempt ${attempt}/${MAX_RETRIES} failed – retrying in ${delay} ms. ${lastError.message}` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + continue; + } finally { + clearTimeout(timeoutHandle); } - throw new Error( - `Alert webhook: server at "${url}" returned HTTP ${response.status} ${response.statusText}` + - (snippet ? ` – ${snippet}` : "") - ); + + if (!response.ok) { + let snippet = ""; + try { + const text = await response.text(); + snippet = text.slice(0, 300); + } catch { + // best-effort + } + lastError = new Error( + `Alert webhook: server at "${url}" returned HTTP ${response.status} ${response.statusText}` + + (snippet ? ` – ${snippet}` : "") + ); + if (attempt < MAX_RETRIES) { + const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1); + logger.warn( + `Alert webhook: attempt ${attempt}/${MAX_RETRIES} failed – retrying in ${delay} ms. ${lastError.message}` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + continue; + } + + logger.debug(`Alert webhook sent successfully to "${url}" for event "${context.eventType}" (attempt ${attempt}/${MAX_RETRIES})`); + return; } - logger.debug(`Alert webhook sent successfully to "${url}" for event "${context.eventType}"`); + throw lastError ?? new Error(`Alert webhook: all ${MAX_RETRIES} attempts failed for "${url}"`); } // --------------------------------------------------------------------------- diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index f4c4d72ef..f8bb6cf5e 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -113,7 +113,7 @@ export default function GeneralPage() { return ( - {site?.siteId && site?.orgId && ( + {site?.siteId && site?.orgId && site.type != "local" && ( {t("uptime30d")}, cell: ({ row }) => { const originalRow = row.original; + if (originalRow.type == "local") { + return -; + } return ( ); diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx index 32db6beb7..7daeb8731 100644 --- a/src/components/SupporterStatus.tsx +++ b/src/components/SupporterStatus.tsx @@ -230,7 +230,7 @@ export default function SupporterStatus({

Business & Enterprise Users: For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}