feat(alerting): per-alert snooze and silent auto-resolve#2
Merged
sameerk27 merged 3 commits intoJun 23, 2026
Merged
Conversation
Per-alert snooze (4h / 24h / 7d presets) plus silent auto-resolve on metric
recovery. Snooze data lives on TriggeredAlert and composes with the existing
per-policy SuppressionMinutes.
Schema: adds SnoozedUntil, SnoozedBy, BelowThresholdStreakCount, and
LastEvaluatedAt columns to TriggeredAlerts via idempotent IF COL_LENGTH
ALTER blocks, mirroring the existing SourceFailureDetails pattern.
API: POST /api/triggered-alerts/{id}/snooze accepts an absolute timestamp
or a duration in hours; POST .../unsnooze clears the state. Snoozing a
terminal-state alert returns 400.
Evaluator: after the existing dispatch loop, scans non-terminal alerts,
increments or resets a per-alert streak counter on each evaluation, and
transitions to Status="auto_resolved" after AutoResolveDebounceCycles
(default 2) consecutive below-threshold observations. Resolution is silent
- no notification dispatch, no NotificationLog write. Manual resolve
remains the user-driven terminal state; the first transition wins.
Config: new Alerting section in appsettings.json; bound via the existing
IOptions pattern.
Adds M365SecurityDashboard.Api.Tests with seven focused tests for the new auto-resolve loop in AlertEvaluator: - Streak increments on a below-threshold observation - Auto-resolve after AutoResolveDebounceCycles consecutive observations - Streak resets to zero on any above-threshold observation - Debounce-of-one auto-resolves on the first observation - Terminal-state alerts (resolved, auto_resolved) are skipped - No notification dispatch on auto-resolve - AcknowledgedAt and AcknowledgedBy survive an auto-resolve transition Uses Microsoft.EntityFrameworkCore.InMemory for the test DbContext. NotificationSender is constructed but never invoked by the loop under test, so no HTTP/DPAPI dependencies are exercised.
Wires the Alert Center to the new per-alert snooze endpoints and renders
the new server-side fields (snoozedUntil, lastEvaluatedAt).
- TriggeredAlert interface extended with status union members
('snoozed' | 'auto_resolved') and the four new fields.
- acApi gains snooze(id, durationHours) and unsnooze(id).
- statusTone maps the new statuses to neutral (snoozed) and info
(auto_resolved) tones so they read distinctly from new / acknowledged.
- Status filter dropdown gets two new options so users can isolate
snoozed or auto-resolved rows.
- Action cell gains a snooze <select> with 4h / 24h / 7d presets and an
Unsnooze button that appears when snoozedUntil is in the future.
- Detail modal surfaces Snoozed until and Last evaluated rows when
populated.
- Mini-list and active-alerts table both render a muted 'snoozed until'
indicator so the state is visible at a glance.
Owner
|
Really clean contribution — the IF COL_LENGTH migration pattern means existing installs just pick this up at startup with no manual steps, and the 7 xUnit tests covering the debounce edge cases (streak reset, terminal-state skip, silent dispatch, AcknowledgedAt preservation) are exactly the kind of coverage this codebase needed. Made a few small fixes before merging: scoped the auto-resolve query to active policy IDs only to avoid loading orphaned rows every cycle, clamped durationHours to a 1-year max, and fixed a "Z until" copy artifact in the UI. Great work — hope to see more PRs from you. 🙌 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses alert fatigue from the same alert firing every collection cycle. Adds two complementary levers:
Per-alert snooze — silence a specific instance for 4h, 24h, or 7d. Compositional with the existing per-policy
SuppressionMinutes, which still blocks new alerts for a policy.Silent auto-resolve — when an open alert's underlying metric has been below its threshold for
AutoResolveDebounceCyclesconsecutive evaluation cycles, the alert transitions toauto_resolved. No notification is dispatched; noNotificationLogentry is written. Manual resolve still wins (first transition wins, terminal states cannot re-open).Changes
src/M365SecurityDashboard.Api/Data/AlertingSchema.cs— four newIF COL_LENGTHALTER blocks addSnoozedUntil,SnoozedBy,BelowThresholdStreakCount(NOT NULL DEFAULT 0),LastEvaluatedAttoTriggeredAlerts. Mirrors the existingSourceFailureDetailspattern, so installs created before this PR get the new columns at startup without a migration.src/M365SecurityDashboard.Api/Models/TriggeredAlert.cs— four new properties matching the schema.src/M365SecurityDashboard.Api/Models/AlertingOptions.cs(new) —AutoResolveDebounceCycles(default 2).src/M365SecurityDashboard.Api/Services/AlertEvaluator.cs— auto-resolve loop appended after the dispatch block; injectsIOptions<AlertingOptions>. Silent (no dispatch call, no log row).src/M365SecurityDashboard.Api/Program.cs— two new endpoints (POST /api/triggered-alerts/{id}/snooze,POST .../unsnooze);SnoozeRequestDTO at the bottom;AlertingOptionsbinding alongside the existingGraphOptionsbinding.src/M365SecurityDashboard.Api/appsettings.json— newAlerting:AutoResolveDebounceCyclessection.src/m365-security-dashboard-client/src/main.tsx—TriggeredAlertinterface gains the four new fields;acApigainssnooze/unsnooze;statusTonemaps the new statuses (snoozed → neutral, auto_resolved → info); status filter dropdown adds two options; action cell gains a snooze<select>with 4h/24h/7d presets and an Unsnooze button; detail modal surfacesSnoozed untilandLast evaluated; the active-alerts table and mini-list both render a muted "snoozed until" indicator.src/M365SecurityDashboard.Api.Tests/(new project) — 7 xUnit tests for the auto-resolve path. UsesMicrosoft.EntityFrameworkCore.InMemoryfor the testAppDbContext.NotificationSenderis constructed but never invoked by the loop under test, so no HTTP/DPAPI dependencies are exercised.Testing
Coverage of the new auto-resolve logic:
AutoResolveDebounceCyclesconsecutive observationsNotificationLogstable after transition)AcknowledgedAtandAcknowledgedBysurvive an auto-resolve transitionDeferred (not in this PR)
SnoozedBy(placeholder literal matches the existingAcknowledgedBy = "dashboard"pattern; auth lands separately)Verification before merging
npm run buildinsrc/m365-security-dashboard-client/is clean.dotnet buildis clean (0 warnings, 0 errors). Schema migration smoke test: start the API against an existing DB with pre-featureTriggeredAlertsrows; the fourIF COL_LENGTHblocks run without error and existing rows render with the new fields as NULL or 0. Manual smoke for the snooze/unsnooze endpoints is in the plan notes.