Merge pull request #3995 from bluewave-labs/fix/decision-refactor

fix: convert decision objecty into a state object
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-09-25 11:13:45 -07:00
commit 1a287be323
15 files changed
+149 -271

No files matched your search

+20 -30
View File
@@ -54,45 +54,35 @@ export class IncidentService implements IIncidentService {
}
handleIncident = async (monitor: Monitor, decision: MonitorActionDecision, check: Check): Promise<Incident | null> => {
if (!decision.shouldCreateIncident && !decision.shouldResolveIncident) {
return null;
}
const { transition } = decision;
if (transition === null) return null;
const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
if (decision.shouldCreateIncident) {
if (activeIncident) {
return activeIncident;
} else {
let statusCode = check.statusCode;
let message: string | undefined;
// For threshold breaches, use 9999 status code and build descriptive message
if (decision.incidentReason === "threshold_breach") {
statusCode = 9999;
message = this.notificationMessageBuilder.buildThresholdBreachMessage(monitor, check, decision.thresholdBreaches);
}
const incident = {
switch (transition) {
case "status_down":
case "threshold_breach": {
if (activeIncident) return activeIncident;
// A threshold breach has no HTTP status
const isBreach = transition === "threshold_breach";
return await this.incidentsRepository.create({
monitorId: monitor.id,
teamId: monitor.teamId,
startTime: Date.now().toString(),
status: true,
statusCode,
message,
};
return await this.incidentsRepository.create(incident);
statusCode: isBreach ? 9999 : check.statusCode,
message: isBreach ? this.notificationMessageBuilder.buildThresholdBreachMessage(monitor, check, decision.thresholdBreaches) : undefined,
});
}
case "status_up":
case "threshold_resolved": {
if (!activeIncident) return null;
activeIncident.status = false;
activeIncident.endTime = Date.now().toString();
activeIncident.resolutionType = "automatic";
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
}
}
if (!decision.shouldResolveIncident || !activeIncident) {
return null;
}
activeIncident.status = false;
activeIncident.endTime = Date.now().toString();
activeIncident.resolutionType = "automatic";
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
};
resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string) => {
@@ -39,24 +39,21 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
clientHost,
metadata: {
teamId: monitor.teamId,
notificationReason: decision.notificationReason,
},
};
}
private determineNotificationType(decision: MonitorActionDecision, monitor: Monitor): NotificationType {
// Down status has highest priority (critical)
if (monitor.status === "down") {
return "monitor_down";
}
const isDocker = monitor.type === "docker";
switch (decision.notificationReason) {
switch (decision.transition) {
case "status_down":
return "monitor_down";
case "threshold_breach":
return isDocker ? "container_breach" : "threshold_breach";
case "threshold_resolved":
return isDocker ? "container_resolved" : "threshold_resolved";
default:
case "status_up":
case null:
return "monitor_up";
}
}
@@ -115,9 +115,7 @@ export class NotificationsService implements INotificationsService {
};
handleNotifications = async (monitor: Monitor, check: Check, decision: MonitorActionDecision) => {
if (!decision.shouldSendNotification) {
return false;
}
if (decision.transition === null) return false;
// Send notifications based on decision
return await this.sendNotifications(monitor, check, decision);
@@ -1,5 +1,4 @@
import type { HardwareMetricKey } from "@/domain/monitors/monitor.type.js";
import { MonitorActionDecision } from "@/worker/worker.interface.js";
export const NotificationChannels = [
"email",
@@ -120,6 +119,5 @@ export interface NotificationMessage {
clientHost: string;
metadata: {
teamId: string;
notificationReason: MonitorActionDecision["notificationReason"];
};
}
+5 -5
View File
@@ -6,12 +6,12 @@ import type { QueueMode } from "@/domain/app-settings/app-settings.type.js";
export type JobHandler = (job: Job) => Promise<void>;
export type JobHandlers = Record<JobType, JobHandler>;
export const MonitorTransitions = ["status_down", "status_up", "threshold_breach", "threshold_resolved"] as const;
export type MonitorTransition = (typeof MonitorTransitions)[number];
export interface MonitorActionDecision {
shouldCreateIncident: boolean;
shouldResolveIncident: boolean;
shouldSendNotification: boolean;
incidentReason: "status_down" | "threshold_breach" | null;
notificationReason: "status_change" | "threshold_breach" | "threshold_resolved" | null;
transition: MonitorTransition | null;
thresholdBreaches?: HardwareBreaches;
}
+16 -31
View File
@@ -20,13 +20,7 @@ import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js";
import { JobHandler, MonitorActionDecision, MonitorEvaluation } from "@/worker/worker.interface.js";
const SERVICE_NAME = "WorkerPipeline";
const NO_ACTION: MonitorActionDecision = {
shouldCreateIncident: false,
shouldResolveIncident: false,
shouldSendNotification: false,
incidentReason: null,
notificationReason: null,
};
// The three entry points: the queue calls the two handlers, the buffer calls ingestChecks.
// produce and evaluateCheck stay public on the class so tests can drive one stage without the queue or the buffer.
export interface IWorkerPipeline {
@@ -272,7 +266,7 @@ export class WorkerPipeline implements IWorkerPipeline {
return {
monitor,
check,
decision: { ...NO_ACTION },
decision: { transition: null },
};
};
@@ -288,30 +282,21 @@ export class WorkerPipeline implements IWorkerPipeline {
};
private decide = (statusChange: StatusChangeResult): MonitorActionDecision => {
const { monitor, statusChanged, prevStatus } = statusChange;
const decision: MonitorActionDecision = { ...NO_ACTION };
if (!statusChanged) return decision;
const { monitor, statusChanged, prevStatus, thresholdBreaches } = statusChange;
if (!statusChanged) return { transition: null };
if (monitor.status === "down") {
decision.shouldCreateIncident = true;
decision.shouldSendNotification = true;
decision.incidentReason = "status_down";
decision.notificationReason = "status_change";
} else if (monitor.status === "breached") {
if (statusChange.thresholdBreaches) decision.thresholdBreaches = statusChange.thresholdBreaches;
decision.shouldCreateIncident = true;
decision.shouldSendNotification = true;
decision.incidentReason = "threshold_breach";
decision.notificationReason = "threshold_breach";
} else if (monitor.status === "up" && prevStatus === "breached") {
decision.shouldResolveIncident = true;
decision.shouldSendNotification = true;
decision.notificationReason = "threshold_resolved";
} else if (monitor.status === "up" && prevStatus === "down") {
decision.shouldResolveIncident = true;
decision.shouldSendNotification = true;
decision.notificationReason = "status_change";
if (monitor.status === "down") return { transition: "status_down" };
if (monitor.status === "breached") {
return thresholdBreaches
? // Hardware
{ transition: "threshold_breach", thresholdBreaches }
: // Docker
{ transition: "threshold_breach" };
}
return decision;
if (monitor.status === "up" && prevStatus === "breached") return { transition: "threshold_resolved" };
if (monitor.status === "up" && prevStatus === "down") return { transition: "status_up" };
return { transition: null };
};
}