From 3ff028f741abfeb408ed292c775d7debf6a445d6 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:58:22 -0400 Subject: [PATCH] Fix webhook alert recording: eliminate duplicate entries, track webhook delivery Dashboard: move RecordAlert back inside SMTP block to prevent duplicate "tray" entries when email is disabled. Add separate "webhook" record when webhook notifications are delivered. Lite: capture webhook send result and reflect in notification_type column ("webhook" or "email+webhook") in DuckDB alert log. Both: change TrySendWebhookAlertsAsync return type to Task so callers can act on delivery status. Follows up on #725. Co-Authored-By: Claude Opus 4.6 (1M context) --- Dashboard/Services/EmailAlertService.cs | 17 +++++++++-------- Dashboard/Services/WebhookAlertService.cs | 7 +++++-- Lite/Services/EmailAlertService.cs | 10 +++++++++- Lite/Services/WebhookAlertService.cs | 7 +++++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Dashboard/Services/EmailAlertService.cs b/Dashboard/Services/EmailAlertService.cs index cd96afab..7b1afd84 100644 --- a/Dashboard/Services/EmailAlertService.cs +++ b/Dashboard/Services/EmailAlertService.cs @@ -79,9 +79,6 @@ public async Task TrySendAlertEmailAsync( try { var prefs = _preferencesService.GetPreferences(); - string? sendError = null; - bool sent = false; - string notificationType = "tray"; /* Attempt email delivery if SMTP is fully configured */ if (prefs.SmtpEnabled && @@ -95,7 +92,8 @@ public async Task TrySendAlertEmailAsync( if (!withinCooldown) { - notificationType = "email"; + bool sent = false; + string? sendError = null; var subject = $"[SQL Monitor Alert] {metricName} on {serverName}"; var (htmlBody, plainTextBody) = EmailTemplateBuilder.BuildAlertEmail( metricName, serverName, currentValue, thresholdValue, prefs.EmailCooldownMinutes, context); @@ -130,18 +128,21 @@ public async Task TrySendAlertEmailAsync( Logger.Error($"ALERT EMAIL STILL FAILING: {_consecutiveFailures} consecutive failures. Last error: {ex.Message}"); } } + + RecordAlert(serverId, serverName, metricName, currentValue, thresholdValue, sent, "email", sendError); } } - /* Log the alert attempt */ - RecordAlert(serverId, serverName, metricName, currentValue, thresholdValue, sent, notificationType, sendError); - /* Send webhook notifications (Teams / Slack) — independent of email */ var webhookService = WebhookAlertService.Current; if (webhookService != null) { - await webhookService.TrySendWebhookAlertsAsync( + var webhookSent = await webhookService.TrySendWebhookAlertsAsync( metricName, serverName, currentValue, thresholdValue, serverId, context); + if (webhookSent) + { + RecordAlert(serverId, serverName, metricName, currentValue, thresholdValue, true, "webhook"); + } } } catch (Exception ex) diff --git a/Dashboard/Services/WebhookAlertService.cs b/Dashboard/Services/WebhookAlertService.cs index d6340cfb..f9cb68b2 100644 --- a/Dashboard/Services/WebhookAlertService.cs +++ b/Dashboard/Services/WebhookAlertService.cs @@ -46,7 +46,7 @@ public WebhookAlertService(UserPreferencesService preferencesService) /// Sends webhook alerts to all configured channels (Teams and/or Slack). /// Respects the email cooldown setting for throttling. Never throws. /// - public async Task TrySendWebhookAlertsAsync( + public async Task TrySendWebhookAlertsAsync( string metricName, string serverName, string currentValue, @@ -62,7 +62,7 @@ public async Task TrySendWebhookAlertsAsync( if (_cooldowns.TryGetValue(cooldownKey, out var lastSent) && DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(prefs.EmailCooldownMinutes)) { - return; + return false; } bool sent = false; @@ -81,10 +81,13 @@ public async Task TrySendWebhookAlertsAsync( { _cooldowns[cooldownKey] = DateTime.UtcNow; } + + return sent; } catch (Exception ex) { Logger.Error($"TrySendWebhookAlertsAsync outer error: {ex.Message}"); + return false; } } diff --git a/Lite/Services/EmailAlertService.cs b/Lite/Services/EmailAlertService.cs index b22da7fe..c895b9f8 100644 --- a/Lite/Services/EmailAlertService.cs +++ b/Lite/Services/EmailAlertService.cs @@ -112,12 +112,20 @@ public async Task TrySendAlertEmailAsync( } /* Send webhook notifications (Teams / Slack) alongside email */ + bool webhookSent = false; if (!muted) { - await _webhookAlertService.TrySendWebhookAlertsAsync( + webhookSent = await _webhookAlertService.TrySendWebhookAlertsAsync( metricName, serverName, currentValue, thresholdValue, serverId, context); } + /* Reflect webhook delivery in notification type */ + if (webhookSent) + { + notificationType = notificationType == "email" ? "email+webhook" : "webhook"; + sent = true; + } + /* Always log the alert to DuckDB, regardless of email status */ var logCurrent = numericCurrentValue ?? (double.TryParse(currentValue.TrimEnd('%'), out var cv) ? cv : 0); diff --git a/Lite/Services/WebhookAlertService.cs b/Lite/Services/WebhookAlertService.cs index afc2d35e..7e54ea58 100644 --- a/Lite/Services/WebhookAlertService.cs +++ b/Lite/Services/WebhookAlertService.cs @@ -37,7 +37,7 @@ public class WebhookAlertService /// Sends webhook alerts to all configured channels (Teams and/or Slack). /// Respects the email cooldown setting for throttling. Never throws. /// - public async Task TrySendWebhookAlertsAsync( + public async Task TrySendWebhookAlertsAsync( string metricName, string serverName, string currentValue, @@ -51,7 +51,7 @@ public async Task TrySendWebhookAlertsAsync( if (_cooldowns.TryGetValue(cooldownKey, out var lastSent) && DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(App.EmailCooldownMinutes)) { - return; + return false; } bool sent = false; @@ -70,10 +70,13 @@ public async Task TrySendWebhookAlertsAsync( { _cooldowns[cooldownKey] = DateTime.UtcNow; } + + return sent; } catch (Exception ex) { AppLogger.Error("Webhook", $"TrySendWebhookAlertsAsync outer error: {ex.Message}"); + return false; } }