feat(AutoResolve): implement resolve delay logic based on incident grouping rules

This commit is contained in:
Nawaz Dhandala
2026-02-09 21:49:49 +00:00
parent 09a6827709
commit 6dbcd69ecd

View File

@@ -1,11 +1,14 @@
import RunCron from "../../Utils/Cron";
import OneUptimeDate from "Common/Types/Date";
import { EVERY_MINUTE } from "Common/Utils/CronTime";
import IncidentEpisodeService from "Common/Server/Services/IncidentEpisodeService";
import IncidentEpisodeMemberService from "Common/Server/Services/IncidentEpisodeMemberService";
import IncidentStateService from "Common/Server/Services/IncidentStateService";
import IncidentGroupingRuleService from "Common/Server/Services/IncidentGroupingRuleService";
import IncidentService from "Common/Server/Services/IncidentService";
import logger from "Common/Server/Utils/Logger";
import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode";
import IncidentGroupingRule from "Common/Models/DatabaseModels/IncidentGroupingRule";
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
import Incident from "Common/Models/DatabaseModels/Incident";
import ObjectID from "Common/Types/ObjectID";
@@ -33,6 +36,7 @@ RunCron(
select: {
_id: true,
projectId: true,
incidentGroupingRuleId: true,
lastIncidentAddedAt: true,
},
props: {
@@ -71,6 +75,31 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
return;
}
// Get resolve delay from the grouping rule if exists and enabled
let resolveDelayMinutes: number = 0;
let enableResolveDelay: boolean = false;
if (episode.incidentGroupingRuleId) {
const rule: IncidentGroupingRule | null =
await IncidentGroupingRuleService.findOneById({
id: episode.incidentGroupingRuleId,
select: {
enableResolveDelay: true,
resolveDelayMinutes: true,
},
props: {
isRoot: true,
},
});
if (rule) {
enableResolveDelay = rule.enableResolveDelay || false;
if (enableResolveDelay && rule.resolveDelayMinutes) {
resolveDelayMinutes = rule.resolveDelayMinutes;
}
}
}
// Get all incidents in this episode
const incidentIds: ObjectID[] =
await IncidentEpisodeMemberService.getIncidentsInEpisode(episode.id);
@@ -108,6 +137,7 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
// Check if all incidents are in resolved state or higher
let allResolved: boolean = true;
let lastResolvedAt: Date | null = null;
for (const incidentId of incidentIds) {
const incident: Incident | null = await IncidentService.findOneById({
@@ -133,6 +163,13 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
allResolved = false;
break;
}
// Track the latest resolved time among incidents
if (incident.updatedAt) {
if (!lastResolvedAt || incident.updatedAt > lastResolvedAt) {
lastResolvedAt = incident.updatedAt;
}
}
}
if (!allResolved) {
@@ -142,6 +179,22 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
return;
}
// All incidents are resolved. Check if resolve delay has passed (only if enabled)
if (enableResolveDelay && resolveDelayMinutes > 0 && lastResolvedAt) {
const timeSinceLastResolved: number =
OneUptimeDate.getDifferenceInMinutes(
lastResolvedAt,
OneUptimeDate.getCurrentDate(),
);
if (timeSinceLastResolved < resolveDelayMinutes) {
logger.debug(
`IncidentEpisode:AutoResolve - Episode ${episode.id} waiting for resolve delay (${resolveDelayMinutes} minutes)`,
);
return;
}
}
// Resolve the episode
logger.info(
`IncidentEpisode:AutoResolve - Resolving episode ${episode.id} as all incidents are resolved`,