feat: Implement Incident Episode services, routes, and notification templates

This commit is contained in:
Nawaz Dhandala
2026-01-28 18:07:36 +00:00
parent 9fd781c083
commit d523ae822d
9 changed files with 350 additions and 0 deletions

View File

@@ -128,6 +128,30 @@ import AlertEpisodeOwnerUserService, {
import AlertEpisodeStateTimelineService, {
Service as AlertEpisodeStateTimelineServiceType,
} from "Common/Server/Services/AlertEpisodeStateTimelineService";
// IncidentEpisode Services
import IncidentEpisodeService, {
Service as IncidentEpisodeServiceType,
} from "Common/Server/Services/IncidentEpisodeService";
import IncidentEpisodeFeedService, {
Service as IncidentEpisodeFeedServiceType,
} from "Common/Server/Services/IncidentEpisodeFeedService";
import IncidentEpisodeInternalNoteService, {
Service as IncidentEpisodeInternalNoteServiceType,
} from "Common/Server/Services/IncidentEpisodeInternalNoteService";
import IncidentEpisodeMemberService, {
Service as IncidentEpisodeMemberServiceType,
} from "Common/Server/Services/IncidentEpisodeMemberService";
import IncidentEpisodeOwnerTeamService, {
Service as IncidentEpisodeOwnerTeamServiceType,
} from "Common/Server/Services/IncidentEpisodeOwnerTeamService";
import IncidentEpisodeOwnerUserService, {
Service as IncidentEpisodeOwnerUserServiceType,
} from "Common/Server/Services/IncidentEpisodeOwnerUserService";
import IncidentEpisodeStateTimelineService, {
Service as IncidentEpisodeStateTimelineServiceType,
} from "Common/Server/Services/IncidentEpisodeStateTimelineService";
import AlertGroupingRuleService, {
Service as AlertGroupingRuleServiceType,
} from "Common/Server/Services/AlertGroupingRuleService";
@@ -458,6 +482,15 @@ import AlertEpisodeOwnerUser from "Common/Models/DatabaseModels/AlertEpisodeOwne
import AlertEpisodeStateTimeline from "Common/Models/DatabaseModels/AlertEpisodeStateTimeline";
import AlertGroupingRule from "Common/Models/DatabaseModels/AlertGroupingRule";
// IncidentEpisode Models
import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode";
import IncidentEpisodeFeed from "Common/Models/DatabaseModels/IncidentEpisodeFeed";
import IncidentEpisodeInternalNote from "Common/Models/DatabaseModels/IncidentEpisodeInternalNote";
import IncidentEpisodeMember from "Common/Models/DatabaseModels/IncidentEpisodeMember";
import IncidentEpisodeOwnerTeam from "Common/Models/DatabaseModels/IncidentEpisodeOwnerTeam";
import IncidentEpisodeOwnerUser from "Common/Models/DatabaseModels/IncidentEpisodeOwnerUser";
import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline";
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPostmortemTemplate";
@@ -1001,6 +1034,66 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
// IncidentEpisode Routes
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisode, IncidentEpisodeServiceType>(
IncidentEpisode,
IncidentEpisodeService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeFeed, IncidentEpisodeFeedServiceType>(
IncidentEpisodeFeed,
IncidentEpisodeFeedService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeInternalNote,
IncidentEpisodeInternalNoteServiceType
>(IncidentEpisodeInternalNote, IncidentEpisodeInternalNoteService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeMember, IncidentEpisodeMemberServiceType>(
IncidentEpisodeMember,
IncidentEpisodeMemberService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeOwnerTeam, IncidentEpisodeOwnerTeamServiceType>(
IncidentEpisodeOwnerTeam,
IncidentEpisodeOwnerTeamService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeOwnerUser, IncidentEpisodeOwnerUserServiceType>(
IncidentEpisodeOwnerUser,
IncidentEpisodeOwnerUserService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeStateTimeline,
IncidentEpisodeStateTimelineServiceType
>(
IncidentEpisodeStateTimeline,
IncidentEpisodeStateTimelineService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<AlertGroupingRule, AlertGroupingRuleServiceType>(

View File

@@ -0,0 +1,66 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " incidentEpisodeTitle) }}
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident Episode Title:" text=incidentEpisodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentEpisodeSeverity }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=incidentEpisodeDescription }}
{{> DetailBoxEnd this }}
{{#if incidentsList}}
{{> TitleBlock title=(concat "Incidents in this Episode (" incidentsCount ")") }}
<!-- Incidents List Container -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td style="border: 0; margin: 0; padding: 0;">
{{{incidentsList}}}
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<!-- /Incidents List Container -->
{{/if}}
{{> InfoBlock info="ACTION REQUIRED: Please acknowledge this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=acknowledgeIncidentEpisodeLink buttonText="Acknowledge Incident Episode"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=acknowledgeIncidentEpisodeLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> TitleBlock title="Why am I receiving this email?"}}
{{> InfoBlock info="You are receiving this email because you are a member of the team that is responsible for this incident episode or you are currently on-call."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,30 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="You have been added as the owner of this incident episode."}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{> DetailBoxField title="Description: " text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,37 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="A new note has been posted on this incident episode."}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{#if isPrivateNote}}
{{> DetailBoxField title="Private Note: " text=note }}
{{else}}
{{> DetailBoxField title="Public Note: " text=note }}
{{/if}}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,35 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Episode Created By: " text=declaredBy }}
{{> DetailBoxField title="Episode Created At: " text=declaredAt }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{> DetailBoxField title="Description: " text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,37 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="Incident episode state has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> StateTransition this}}
{{#ifNotCond previousStateDurationText ""}}
{{> DetailBoxField title="Duration in Previous State:" text=previousStateDurationText }}
{{/ifNotCond}}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
{{> DetailBoxField title="Severity:" text=episodeSeverity }}
{{> DetailBoxField title="Description:" text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -32,6 +32,7 @@ import SlackAuthAction, {
import SlackIncidentActions from "../Utils/Workspace/Slack/Actions/Incident";
import SlackAlertActions from "../Utils/Workspace/Slack/Actions/Alert";
import SlackAlertEpisodeActions from "../Utils/Workspace/Slack/Actions/AlertEpisode";
import SlackIncidentEpisodeActions from "../Utils/Workspace/Slack/Actions/IncidentEpisode";
import SlackScheduledMaintenanceActions from "../Utils/Workspace/Slack/Actions/ScheduledMaintenance";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import SlackMonitorActions from "../Utils/Workspace/Slack/Actions/Monitor";
@@ -647,6 +648,19 @@ export default class SlackAPI {
});
}
if (
SlackIncidentEpisodeActions.isIncidentEpisodeAction({
actionType: action.actionType,
})
) {
return SlackIncidentEpisodeActions.handleIncidentEpisodeAction({
slackRequest: authResult,
action: action,
req: req,
res: res,
});
}
if (
SlackMonitorActions.isMonitorAction({
actionType: action.actionType,
@@ -837,6 +851,13 @@ export default class SlackAPI {
logger.error(err);
}
try {
await SlackIncidentEpisodeActions.handleEmojiReaction(reactionData);
} catch (err) {
logger.error("Error handling incident episode emoji reaction:");
logger.error(err);
}
try {
await SlackScheduledMaintenanceActions.handleEmojiReaction(
reactionData,

View File

@@ -264,6 +264,7 @@ ${onCallPolicy.description || "No description provided."}
triggeredByIncidentId?: ObjectID | undefined;
triggeredByAlertId?: ObjectID | undefined;
triggeredByAlertEpisodeId?: ObjectID | undefined;
triggeredByIncidentEpisodeId?: ObjectID | undefined;
userNotificationEventType: UserNotificationEventType;
},
): Promise<void> {
@@ -299,6 +300,16 @@ ${onCallPolicy.description || "No description provided."}
);
}
if (
UserNotificationEventType.IncidentEpisodeCreated ===
options.userNotificationEventType &&
!options.triggeredByIncidentEpisodeId
) {
throw new BadDataException(
"triggeredByIncidentEpisodeId is required when userNotificationEventType is IncidentEpisodeCreated",
);
}
const policy: OnCallDutyPolicy | null = await this.findOneById({
id: policyId,
select: {
@@ -338,6 +349,10 @@ ${onCallPolicy.description || "No description provided."}
log.triggeredByAlertEpisodeId = options.triggeredByAlertEpisodeId;
}
if (options.triggeredByIncidentEpisodeId) {
log.triggeredByIncidentEpisodeId = options.triggeredByIncidentEpisodeId;
}
await OnCallDutyPolicyExecutionLogService.create({
data: log,
props: {

View File

@@ -86,6 +86,7 @@ import {
} from "./Actions/ActionTypes";
import MicrosoftTeamsAlertActions from "./Actions/Alert";
import MicrosoftTeamsAlertEpisodeActions from "./Actions/AlertEpisode";
import MicrosoftTeamsIncidentEpisodeActions from "./Actions/IncidentEpisode";
import MicrosoftTeamsMonitorActions from "./Actions/Monitor";
import MicrosoftTeamsScheduledMaintenanceActions from "./Actions/ScheduledMaintenance";
import MicrosoftTeamsOnCallDutyActions from "./Actions/OnCallDutyPolicy";
@@ -2535,6 +2536,21 @@ All monitoring checks are passing normally.`;
return;
}
// Handle incident episode actions
if (
MicrosoftTeamsIncidentEpisodeActions.isIncidentEpisodeAction({ actionType })
) {
await MicrosoftTeamsIncidentEpisodeActions.handleBotIncidentEpisodeAction({
actionType,
actionValue,
value,
projectId,
oneUptimeUserId,
turnContext: data.turnContext,
});
return;
}
// Handle monitor actions
if (MicrosoftTeamsMonitorActions.isMonitorAction({ actionType })) {
await MicrosoftTeamsMonitorActions.handleBotMonitorAction({