From 6dbcd69ecd4eba78420bc065b1758a17468defbe Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 9 Feb 2026 21:49:49 +0000 Subject: [PATCH] feat(AutoResolve): implement resolve delay logic based on incident grouping rules --- Worker/Jobs/IncidentEpisode/AutoResolve.ts | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Worker/Jobs/IncidentEpisode/AutoResolve.ts b/Worker/Jobs/IncidentEpisode/AutoResolve.ts index 728bb0641f..8e4bbc0659 100644 --- a/Worker/Jobs/IncidentEpisode/AutoResolve.ts +++ b/Worker/Jobs/IncidentEpisode/AutoResolve.ts @@ -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`,