[glm-grade=A] fix(monitoring): DC-090 outage incidents follow displayed hysteresis status

checkForIncidents compared raw probe transitions while the dashboard badge
(DC-086) follows post-hysteresis displayed status. A single raw down blip
between two ups opened AND resolved a critical outage incident; a suppressed
up blip during a real outage resolved it early. Incidents now open/resolve
on displayed-vs-displayed transitions; previousDisplayed=null keeps legacy
raw semantics for direct callers. 6 new parity tests + legacy checkService
test moved to a 4-probe chain. Suite 2616/2616 (110).

Verdict: urn:ump:yc5rdlnmnmhch5audc5fifgbt6d7moi2stqfs5vidsbh6x6zkvgq
This commit is contained in:
Hermes
2026-08-22 16:52:53 -07:00
parent dd1110ef52
commit 88f1d4a414
3 changed files with 218 additions and 6 deletions
+24 -5
View File
@@ -199,8 +199,9 @@ class HealthChecker extends EventEmitter {
}
const previousStatus = this.currentStatus.get(serviceId);
const previousDisplayed = this.displayedStatus.get(serviceId);
this.recordStatus(serviceId, status);
this.checkForIncidents(serviceId, status, config, previousStatus);
this.checkForIncidents(serviceId, status, config, previousStatus, previousDisplayed);
return status;
} catch (error) {
@@ -223,8 +224,9 @@ class HealthChecker extends EventEmitter {
this.consecutiveFailures.set(serviceId, (this.consecutiveFailures.get(serviceId) || 0) + 1);
const previousStatus = this.currentStatus.get(serviceId);
const previousDisplayed = this.displayedStatus.get(serviceId);
this.recordStatus(serviceId, status);
this.checkForIncidents(serviceId, status, config, previousStatus);
this.checkForIncidents(serviceId, status, config, previousStatus, previousDisplayed);
return status;
}
@@ -453,10 +455,27 @@ class HealthChecker extends EventEmitter {
/**
* Check for incidents (downtime, slow response, etc.)
*/
checkForIncidents(serviceId, status, config, previous = this.currentStatus.get(serviceId)) {
checkForIncidents(serviceId, status, config, previous = this.currentStatus.get(serviceId), previousDisplayed = null) {
// Check for status change (up -> down or down -> up)
if (previous && previous.status !== status.status) {
// DC-090: outage incidents follow the DISPLAYED (post-hysteresis) status —
// the same signal that flips the dashboard badge. A single raw "down"
// blip that hysteresis suppresses must not open a critical outage
// incident (and a suppressed blip must not resolve a real one). When the
// caller supplies the pre-probe displayed state (checkService always
// does), transitions are evaluated displayed-vs-displayed using the
// post-recordStatus state in this.displayedStatus. Direct callers with
// no hysteresis state (previousDisplayed === null) keep the legacy
// raw-probe transition semantics.
if (previousDisplayed) {
const displayed = this.displayedStatus.get(serviceId);
if (displayed && displayed.status !== previousDisplayed.status) {
if (displayed.status === 'down') {
this.createIncident(serviceId, 'outage', 'Service is down', displayed);
} else if (displayed.status === 'up') {
this.resolveIncident(serviceId, 'outage', displayed);
}
}
} else if (previous && previous.status !== status.status) {
if (status.status === 'down') {
this.createIncident(serviceId, 'outage', 'Service is down', status);
} else if (status.status === 'up') {