Repository navigation
Stop analysis-finding alerts delivering their Diagnosis facts twice - #3317
Merged
Merged
Conversation
FindingMessageFormatter is a third producer of (Context, DetailText) pairs. Its DetailText and its context's Diagnosis item render the same values under different labels, so the textual equality in AlertDetailText.ProseForDelivery cannot see the duplication and every channel prints those facts twice. The prose is suppressed at DELIVERY only. FindingAlert now carries the producer's own declaration, and the two senders route DeliveredProse to the channels while the alert-history row keeps DetailText in full - config_alert_log.detail_text is what the MCP reader, the triage endpoint and the Viewer's detail pane render, and the input AlertMuteContext.PopulateFromDetailText parses for the mute pre-fill.
… the row Occurrence counts rather than absences. Asserting only that the prose is gone would pass equally if the structured context had been dropped instead, which is the opposite defect and the more expensive one - the context carries the advice, the remediation T-SQL and the drill-down that the prose does not. Each count is paired with the same builder fed DetailText, the argument #3297 shipped, which must come back with the facts twice. Both directions, or the fix is indistinguishable from reverting #3297 for this seam: a producer that declares its prose independent still has it delivered, and the persisted detail text is a frozen literal on both SKUs. Driven through the real AnalysisNotificationService, not a hand-built FindingAlert - the declaration under test is the producer's.
… number #3313 is an open issue about webhook batch re-rendering. These comments were referencing a number that was never read, only constructed.
Same trap this change removes from AlertDetailText's doc: a count in prose is a partial list with a number welded on, and the list beside it is already the claim.
|
Reviewed the diff (C# only — no T-SQL touched, so the T-SQL style rules don't apply here). Correctness / parity: Verified end to end and it holds up.
Security: No new input handling, no secrets, no file/process/network surface beyond the existing test-only loopback TCP/SMTP fixtures used to observe wire bytes — fine for test code. Style/process: New file No findings to flag — this looks correct and keeps Lite/Darling in parity. |
This was referenced Sep 11, 2026
pull Bot
pushed a commit
to ehtick/PerformanceMonitor
that referenced
this pull request
Sep 18, 2026
erikdarlingdata#3317 merged the fix; lane PRs do not touch CHANGELOG.md. Named by the PR that shipped the regression rather than a guessed issue number, matching the fixing lane's own choice.
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.
Analysis-finding alerts render their Diagnosis facts twice on every channel. This is the open review thread on #3302 (
PerformanceMonitor.Notifications/AlertDetailText.cs), settled.The regression
#3302 threaded each alert's prose
DetailTextthrough to all five delivery channels — the #3296 field defect was that it reached none. It gates delivery onAlertDetailText.ProseForDelivery, which suppresses the prose when it is textually equal to the flattening of the alert's own structured context.FindingMessageFormatteris a third producer of(Context, DetailText)pairs and nothing in #3302 touched it.BuildContextputs story / severity / notify threshold / confidence / fact count / database / window into aDiagnosisitem;DetailTextformats the same values with different labels (Facts in chainvsFacts, one combined severity line vs two fields) and a different window separator. Different text, so the equality gate never fires, so both are delivered — on the largest alert category there is (CPU spikes, plan regressions, index and RCSI findings), on both SKUs, on email, Teams, Slack, PagerDuty and the generic webhook.What this does, and the seam it rests on
The prose is suppressed at delivery. The persisted value is untouched.
That distinction is the whole point.
FindingMessageFormatter.DetailTextis alsoconfig_alert_log.detail_textfor every analysis row: the MCP alert reader, the triage endpoint, the Viewer's detail pane and the Dashboard's no-channel tray row all render it, andAlertMuteContext.PopulateFromDetailTextparses it for the mute pre-fill. The alternative repair — makeDetailTextthe flattening of its own context, as the thirteenAlertEnginefire sites do — would change that column for the largest alert category, gaining theDiagnosisheading, a separateNotify thresholdfield, the advice headline, the remediation-T-SQL item's heading and every drill-down item's fields.The seam exists, at two separate call sites in each sender, and it is not a new one:
deprecated/Dashboard'sSendFindingAlertAsynchas always omitted the detail from its delivery call while passing it to its tray row. Lite and Darling each callEmailSendCore.TrySendAsync(detailText:)for delivery andIAlertHistoryStore.RecordAlertAsync(... DetailText ...)for the row.FindingAlertnow carries the producer's own declaration —DeliverDetailText, withDeliveredProseresolving it once so the two senders cannot disagree — and the senders routeDeliveredProseto the channels andDetailTextto the row. Keyed on producer rather than on text, which is what the equality gate cannot do here.Not widened to "suppress when the context's facts all appear in the prose": that also drops the #2109 AG alerts'
SET HADR RESUMEremedy, the one case the gate was deliberately built not to drop.Lite's shell shares one path with the engine, so it takes a
deliverProseflag defaulting to true. The two failure directions are not symmetric — delivering a redundant paragraph is cosmetic, withholding one discards an operator's only copy of the remedy, which is #3296 — so a caller that never considers it delivers the prose.Also in here:
AlertDetailText's doc saidAlertEnginebuilds its detail text that way "at eleven fire sites". #3302 took that to thirteen. The numeral is gone rather than corrected — no test derives it, so it is a partial list with a number welded on.Verification
Lite.TestsandDarling.Testsarenet10.0-windowsand cannot run on macOS, so the ACTUAL committed test sources were compiled into twonet10.0console harnesses over an xunit shim and RUN.Lite/Services/EmailAlertService.csis compiled in as the real product source rather than shimmed — only its project is Windows-targeted — because it is the subject of two of the pins.All three measured harness traps are controlled, not avoided. The shim's
--self-checkcarries 6 controls that must pass (Assert.Equalon distinct-but-equalList<T>, arrays and nested collections; anasync Tasksucceeding after two awaits; a two-case theory) and 5 that must fail, including a failure raised after an await and one thrown below the first await in a nested async call. All 6 green, all 5 red.BaseOutputPathsits inside the worktree soDocCommentHygieneTests' walk-up findsPerformanceMonitor.sln; all 77 of its cases ran and passed, which is how the new doc comments and crefs are checked.Lite.Tests(8 real test files + the realEmailAlertService.cs)Darling.Tests(AlertDeliveryChannelTests,CustomAlertDisplayNameTests,DocCommentHygieneTests,CSharpSourceWalkerTests)The SKIP is
TheViewerRow_RendersThroughTheSharedDescriber, whose subject is the WPFViewerAlertRow; it is reported SKIP rather than PASS because a shimmed subject would make the result the harness's. CI runs it for real.The pins
Both directions, or the fix is indistinguishable from reverting #3302 for this seam.
AnalysisNotificationService, not a hand-builtFindingAlert: the declaration under test is the producer's. On the Darling side it is the whole path,DarlingFindingAlertSenderthrough the send core, the fan-out and each payload builder, read off the bytes that left the process via the loopback TCP and SMTP sinks.DetailText, the argument Deliver the alert's detail text on every channel #3302 shipped, which must come back with the facts twice.config_alert_log.detail_textis unchanged — a frozen list of lines on both SKUs, not a re-derivation of the code it pins. Darling.Tests cannot seeFindingMessageFormatterat all (Notifications grantsInternalsVisibleToto Lite and Dashboard, not there), which makes that limitation useful.Mutations
Committed before every one; each applied, run, reverted, and the revert verified by blob hash against
HEADplus a whole-tree clean check. Every run goes through a driver that BUILDS first — a source-only restore leaves the previous mutation's DLL inbin, and the previous lane lost a round to reds that were its harness's stale state.DeliveredProsenever suppressesDeliveredProsealways suppressesDetailTextinstead ofDeliveredProsedeliverProsedeliverProse: falsedeliverProse: trueBuildContextdrops theDatabasefield (the opposite defect)DetailTextbecomes the flattening of its own context (the declined approach)ProseForDeliveryalways returns nullMutation 12 is what shows the control halves are load-bearing rather than decoration, and mutation 10 that the "once" claim discriminates which rendering survived.
TheSurvivingRendering_IsTheStructuredContext_NotTheProsestays green under 12, correctly — that one is about which copy remains, not how many.Mechanics
Fresh worktree off
origin/dev;origin/devmerged IN at2a52dd5b(no overlap with any file here), never rebased, never force-pushed. Solution builds with-p:EnableWindowsTargeting=true, 0 warnings.global.jsonuntouched — 10.0.302 is installed and the pin edit is a stale ritual.packages.lock.jsonchurn from the macOS restore reverted. Named paths only, nevergit add -A. NoCHANGELOG.mdedit.One correction of my own, recorded because the artefact is durable: the branch was first pushed as
fix/3313-..., a number I constructed as "next" rather than read. #3313 is an open issue about a webhook batch re-rendering already-delivered incidents. Branch renamed, the wrong remote ref deleted, and five code comments that referenced it rewritten to name #3302 instead. No issue is filed for this regression — it is carried here.CHANGELOG entry text (for the coordinator, not committed)