feat: add support for alert episode handling in user on-call logs and notifications

This commit is contained in:
Nawaz Dhandala
2026-01-26 23:06:20 +00:00
parent 54da185280
commit a808913049
7 changed files with 667 additions and 5 deletions

View File

@@ -28,6 +28,7 @@ import Permission from "../../Types/Permission";
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
import UserNotificationExecutionStatus from "../../Types/UserNotification/UserNotificationExecutionStatus";
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
import AlertEpisode from "./AlertEpisode";
import Alert from "./Alert";
@EnableDocumentation()
@@ -433,6 +434,53 @@ export default class UserOnCallLog extends BaseModel {
})
public triggeredByAlertId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "triggeredByAlertEpisodeId",
type: TableColumnType.Entity,
modelType: AlertEpisode,
title: "Triggered By Alert Episode",
description:
"Relation to Alert Episode which triggered this on-call duty policy.",
})
@ManyToOne(
() => {
return AlertEpisode;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "triggeredByAlertEpisodeId" })
public triggeredByAlertEpisode?: AlertEpisode = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Triggered By Alert Episode ID",
required: false,
description:
"ID of the Alert Episode which triggered this on-call escalation policy.",
example: "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public triggeredByAlertEpisodeId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],

View File

@@ -1,5 +1,6 @@
import Incident from "./Incident";
import Alert from "./Alert";
import AlertEpisode from "./AlertEpisode";
import OnCallDutyPolicy from "./OnCallDutyPolicy";
import OnCallDutyPolicyEscalationRule from "./OnCallDutyPolicyEscalationRule";
import OnCallDutyPolicyExecutionLog from "./OnCallDutyPolicyExecutionLog";
@@ -389,6 +390,52 @@ export default class UserOnCallLogTimeline extends BaseModel {
})
public triggeredByAlertId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "triggeredByAlertEpisodeId",
type: TableColumnType.Entity,
modelType: AlertEpisode,
title: "Alert Episode",
description: "Relation to Alert Episode Resource in which this object belongs",
})
@ManyToOne(
() => {
return AlertEpisode;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "triggeredByAlertEpisodeId" })
public triggeredByAlertEpisode?: AlertEpisode = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Alert Episode ID",
description: "ID of your OneUptime Alert Episode in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public triggeredByAlertEpisodeId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.CurrentUser],

View File

@@ -322,6 +322,8 @@ export class Service extends DatabaseService<Model> {
userNotificationEventType: options.userNotificationEventType!,
triggeredByIncidentId: options.triggeredByIncidentId || undefined,
triggeredByAlertId: options.triggeredByAlertId || undefined,
triggeredByAlertEpisodeId:
options.triggeredByAlertEpisodeId || undefined,
onCallPolicyExecutionLogId: options.onCallPolicyExecutionLogId,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: ruleId,

View File

@@ -25,6 +25,7 @@ export interface PushNotificationOptions {
// Optional relations for richer logging
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
alertEpisodeId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;

View File

@@ -51,6 +51,8 @@ import Alert from "../../Models/DatabaseModels/Alert";
import AlertService from "./AlertService";
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
import AlertSeverityService from "./AlertSeverityService";
import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
import AlertEpisodeService from "./AlertEpisodeService";
import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule";
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
import PushNotificationService from "./PushNotificationService";
@@ -73,6 +75,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
triggeredByIncidentId?: ObjectID | undefined;
triggeredByAlertId?: ObjectID | undefined;
triggeredByAlertEpisodeId?: ObjectID | undefined;
userNotificationEventType: UserNotificationEventType;
onCallPolicyExecutionLogId?: ObjectID | undefined;
onCallPolicyId: ObjectID | undefined;
@@ -201,6 +204,10 @@ export class Service extends DatabaseService<Model> {
logTimelineItem.triggeredByAlertId = options.triggeredByAlertId;
}
if (options.triggeredByAlertEpisodeId) {
logTimelineItem.triggeredByAlertEpisodeId = options.triggeredByAlertEpisodeId;
}
if (options.onCallDutyPolicyExecutionLogTimelineId) {
logTimelineItem.onCallDutyPolicyExecutionLogTimelineId =
options.onCallDutyPolicyExecutionLogTimelineId;
@@ -210,6 +217,7 @@ export class Service extends DatabaseService<Model> {
let incident: Incident | null = null;
let alert: Alert | null = null;
let alertEpisode: AlertEpisode | null = null;
if (
options.userNotificationEventType ===
@@ -270,8 +278,37 @@ export class Service extends DatabaseService<Model> {
});
}
if (!incident && !alert) {
throw new BadDataException("Incident or Alert not found.");
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
options.triggeredByAlertEpisodeId
) {
alertEpisode = await AlertEpisodeService.findOneById({
id: options.triggeredByAlertEpisodeId!,
props: {
isRoot: true,
},
select: {
_id: true,
title: true,
description: true,
projectId: true,
project: {
name: true,
},
currentAlertState: {
name: true,
},
alertSeverity: {
name: true,
},
episodeNumber: true,
},
});
}
if (!incident && !alert && !alertEpisode) {
throw new BadDataException("Incident, Alert, or Alert Episode not found.");
}
if (
@@ -384,6 +421,56 @@ export class Service extends DatabaseService<Model> {
});
});
}
// send email for alert episode
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
alertEpisode
) {
logTimelineItem.status = UserNotificationStatus.Sending;
logTimelineItem.statusMessage = `Sending email to ${notificationRuleItem.userEmail?.email.toString()}`;
logTimelineItem.userEmailId = notificationRuleItem.userEmail.id!;
const updatedLog: UserOnCallLogTimeline =
await UserOnCallLogTimelineService.create({
data: logTimelineItem,
props: {
isRoot: true,
},
});
const emailMessage: EmailMessage =
await this.generateEmailTemplateForAlertEpisodeCreated(
notificationRuleItem.userEmail?.email,
alertEpisode,
updatedLog.id!,
);
MailService.sendMail(emailMessage, {
userOnCallLogTimelineId: updatedLog.id!,
projectId: options.projectId,
alertEpisodeId: alertEpisode.id!,
userId: notificationRuleItem.userId!,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
teamId: options.userBelongsToTeamId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
}).catch(async (err: Error) => {
await UserOnCallLogTimelineService.updateOneById({
id: updatedLog.id!,
data: {
status: UserNotificationStatus.Error,
statusMessage: err.message || "Error sending email.",
},
props: {
isRoot: true,
},
});
});
}
}
// if you have an email but is not verified, then create a log.
@@ -512,6 +599,56 @@ export class Service extends DatabaseService<Model> {
});
});
}
// send sms for alert episode
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
alertEpisode
) {
logTimelineItem.status = UserNotificationStatus.Sending;
logTimelineItem.statusMessage = `Sending SMS to ${notificationRuleItem.userSms?.phone.toString()}.`;
logTimelineItem.userSmsId = notificationRuleItem.userSms.id!;
const updatedLog: UserOnCallLogTimeline =
await UserOnCallLogTimelineService.create({
data: logTimelineItem,
props: {
isRoot: true,
},
});
const smsMessage: SMS =
await this.generateSmsTemplateForAlertEpisodeCreated(
notificationRuleItem.userSms.phone,
alertEpisode,
updatedLog.id!,
);
SmsService.sendSms(smsMessage, {
projectId: alertEpisode.projectId,
userOnCallLogTimelineId: updatedLog.id!,
alertEpisodeId: alertEpisode.id!,
userId: notificationRuleItem.userId!,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
teamId: options.userBelongsToTeamId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
}).catch(async (err: Error) => {
await UserOnCallLogTimelineService.updateOneById({
id: updatedLog.id!,
data: {
status: UserNotificationStatus.Error,
statusMessage: err.message || "Error sending SMS.",
},
props: {
isRoot: true,
},
});
});
}
}
if (
@@ -631,6 +768,56 @@ export class Service extends DatabaseService<Model> {
});
});
}
// send WhatsApp for alert episode
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
alertEpisode
) {
logTimelineItem.status = UserNotificationStatus.Sending;
logTimelineItem.statusMessage = `Sending WhatsApp message to ${notificationRuleItem.userWhatsApp?.phone.toString()}.`;
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id!;
const updatedLog: UserOnCallLogTimeline =
await UserOnCallLogTimelineService.create({
data: logTimelineItem,
props: {
isRoot: true,
},
});
const whatsAppMessage: WhatsAppMessage =
await this.generateWhatsAppTemplateForAlertEpisodeCreated(
notificationRuleItem.userWhatsApp.phone,
alertEpisode,
updatedLog.id!,
);
WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
projectId: alertEpisode.projectId,
alertEpisodeId: alertEpisode.id!,
userOnCallLogTimelineId: updatedLog.id!,
userId: notificationRuleItem.userId!,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
teamId: options.userBelongsToTeamId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
}).catch(async (err: Error) => {
await UserOnCallLogTimelineService.updateOneById({
id: updatedLog.id!,
data: {
status: UserNotificationStatus.Error,
statusMessage: err.message || "Error sending WhatsApp message.",
},
props: {
isRoot: true,
},
});
});
}
}
if (
@@ -758,6 +945,56 @@ export class Service extends DatabaseService<Model> {
});
});
}
// send call for alert episode
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
alertEpisode
) {
logTimelineItem.status = UserNotificationStatus.Sending;
logTimelineItem.statusMessage = `Making a call to ${notificationRuleItem.userCall?.phone.toString()}.`;
logTimelineItem.userCallId = notificationRuleItem.userCall.id!;
const updatedLog: UserOnCallLogTimeline =
await UserOnCallLogTimelineService.create({
data: logTimelineItem,
props: {
isRoot: true,
},
});
const callRequest: CallRequest =
await this.generateCallTemplateForAlertEpisodeCreated(
notificationRuleItem.userCall?.phone,
alertEpisode,
updatedLog.id!,
);
CallService.makeCall(callRequest, {
projectId: alertEpisode.projectId,
userOnCallLogTimelineId: updatedLog.id!,
alertEpisodeId: alertEpisode.id!,
userId: notificationRuleItem.userId!,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
teamId: options.userBelongsToTeamId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
}).catch(async (err: Error) => {
await UserOnCallLogTimelineService.updateOneById({
id: updatedLog.id!,
data: {
status: UserNotificationStatus.Error,
statusMessage: err.message || "Error making call.",
},
props: {
isRoot: true,
},
});
});
}
}
if (
@@ -922,6 +1159,75 @@ export class Service extends DatabaseService<Model> {
});
});
}
// send push notification for alert episode
if (
options.userNotificationEventType ===
UserNotificationEventType.AlertEpisodeCreated &&
alertEpisode
) {
logTimelineItem.status = UserNotificationStatus.Sending;
logTimelineItem.statusMessage = `Sending push notification to device.`;
logTimelineItem.userPushId = notificationRuleItem.userPush.id!;
const updatedLog: UserOnCallLogTimeline =
await UserOnCallLogTimelineService.create({
data: logTimelineItem,
props: {
isRoot: true,
},
});
const pushMessage: PushNotificationMessage =
PushNotificationUtil.createAlertEpisodeCreatedNotification({
alertEpisodeTitle: alertEpisode.title!,
projectName: alertEpisode.project?.name || "OneUptime",
alertEpisodeViewLink: (
await AlertEpisodeService.getEpisodeLinkInDashboard(
alertEpisode.projectId!,
alertEpisode.id!,
)
).toString(),
});
PushNotificationService.sendPushNotification(
{
devices: [
{
token: notificationRuleItem.userPush.deviceToken!,
...(notificationRuleItem.userPush.deviceName && {
name: notificationRuleItem.userPush.deviceName,
}),
},
],
message: pushMessage,
deviceType: notificationRuleItem.userPush.deviceType!,
},
{
projectId: options.projectId,
userOnCallLogTimelineId: updatedLog.id!,
alertEpisodeId: alertEpisode.id!,
userId: notificationRuleItem.userId!,
onCallPolicyId: options.onCallPolicyId,
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
teamId: options.userBelongsToTeamId,
onCallDutyPolicyExecutionLogTimelineId:
options.onCallDutyPolicyExecutionLogTimelineId,
onCallScheduleId: options.onCallScheduleId,
},
).catch(async (err: Error) => {
await UserOnCallLogTimelineService.updateOneById({
id: updatedLog.id!,
data: {
status: UserNotificationStatus.Error,
statusMessage: err.message || "Error sending push notification.",
},
props: {
isRoot: true,
},
});
});
}
}
if (
@@ -1043,6 +1349,57 @@ export class Service extends DatabaseService<Model> {
return callRequest;
}
@CaptureSpan()
public async generateCallTemplateForAlertEpisodeCreated(
to: Phone,
alertEpisode: AlertEpisode,
userOnCallLogTimelineId: ObjectID,
): Promise<CallRequest> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const callRequest: CallRequest = {
to: to,
data: [
{
sayMessage: "This is a call from OneUptime",
},
{
sayMessage: "A new alert episode has been created",
},
{
sayMessage: alertEpisode.title!,
},
{
introMessage: "To acknowledge this alert episode press 1",
numDigits: 1,
timeoutInSeconds: 10,
noInputMessage: "You have not entered any input. Good bye",
onInputCallRequest: {
"1": {
sayMessage: "You have acknowledged this alert episode. Good bye",
},
default: {
sayMessage: "Invalid input. Good bye",
},
},
responseUrl: new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute(
"/call/gather-input/" + userOnCallLogTimelineId.toString(),
),
),
},
],
};
return callRequest;
}
@CaptureSpan()
public async generateSmsTemplateForAlertCreated(
to: Phone,
@@ -1109,6 +1466,39 @@ export class Service extends DatabaseService<Model> {
return sms;
}
@CaptureSpan()
public async generateSmsTemplateForAlertEpisodeCreated(
to: Phone,
alertEpisode: AlertEpisode,
userOnCallLogTimelineId: ObjectID,
): Promise<SMS> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const shortUrl: ShortLink = await ShortLinkService.saveShortLinkFor(
new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute("/acknowledge-page/" + userOnCallLogTimelineId.toString()),
),
);
const url: URL = await ShortLinkService.getShortenedUrl(shortUrl);
const episodeIdentifier: string =
alertEpisode.episodeNumber !== undefined
? `#${alertEpisode.episodeNumber} (${alertEpisode.title || "Alert Episode"})`
: alertEpisode.title || "Alert Episode";
const sms: SMS = {
to,
message: `This is a message from OneUptime. A new alert episode has been created: ${episodeIdentifier}. To acknowledge this alert episode, please click on the following link ${url.toString()}`,
};
return sms;
}
@CaptureSpan()
public async generateWhatsAppTemplateForAlertCreated(
to: Phone,
@@ -1223,6 +1613,65 @@ export class Service extends DatabaseService<Model> {
};
}
@CaptureSpan()
public async generateWhatsAppTemplateForAlertEpisodeCreated(
to: Phone,
alertEpisode: AlertEpisode,
userOnCallLogTimelineId: ObjectID,
): Promise<WhatsAppMessage> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const acknowledgeShortLink: ShortLink =
await ShortLinkService.saveShortLinkFor(
new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute(
"/acknowledge-page/" + userOnCallLogTimelineId.toString(),
),
),
);
const acknowledgeUrl: URL =
await ShortLinkService.getShortenedUrl(acknowledgeShortLink);
const episodeLinkOnDashboard: string =
alertEpisode.projectId && alertEpisode.id
? (
await AlertEpisodeService.getEpisodeLinkInDashboard(
alertEpisode.projectId,
alertEpisode.id,
)
).toString()
: acknowledgeUrl.toString();
// Use AlertCreated template as fallback since alert episode is similar to alert
const templateKey: WhatsAppTemplateId = WhatsAppTemplateIds.AlertCreated;
const templateVariables: Record<string, string> = {
project_name: alertEpisode.project?.name || "OneUptime",
alert_title: alertEpisode.title || "",
acknowledge_url: acknowledgeUrl.toString(),
alert_number:
alertEpisode.episodeNumber !== undefined
? alertEpisode.episodeNumber.toString()
: "",
alert_link: episodeLinkOnDashboard,
};
const body: string = renderWhatsAppTemplate(templateKey, templateVariables);
return {
to,
body,
templateKey,
templateVariables,
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
};
}
@CaptureSpan()
public async generateEmailTemplateForAlertCreated(
to: Email,
@@ -1308,6 +1757,49 @@ export class Service extends DatabaseService<Model> {
return emailMessage;
}
@CaptureSpan()
public async generateEmailTemplateForAlertEpisodeCreated(
to: Email,
alertEpisode: AlertEpisode,
userOnCallLogTimelineId: ObjectID,
): Promise<EmailMessage> {
const host: Hostname = await DatabaseConfig.getHost();
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
const vars: Dictionary<string> = {
alertTitle: alertEpisode.title!,
projectName: alertEpisode.project!.name!,
currentState: alertEpisode.currentAlertState!.name!,
alertDescription: await Markdown.convertToHTML(
alertEpisode.description! || "",
MarkdownContentType.Email,
),
alertSeverity: alertEpisode.alertSeverity!.name!,
alertViewLink: (
await AlertEpisodeService.getEpisodeLinkInDashboard(
alertEpisode.projectId!,
alertEpisode.id!,
)
).toString(),
acknowledgeAlertLink: new URL(
httpProtocol,
host,
new Route(AppApiRoute.toString())
.addRoute(new UserOnCallLogTimeline().crudApiPath!)
.addRoute("/acknowledge-page/" + userOnCallLogTimelineId.toString()),
).toString(),
};
const emailMessage: EmailMessage = {
toEmail: to!,
templateType: EmailTemplateType.AcknowledgeAlert,
vars: vars,
subject: "ACTION REQUIRED: Alert Episode created - " + alertEpisode.title!,
};
return emailMessage;
}
@CaptureSpan()
public async startUserNotificationRulesExecution(
userId: ObjectID,
@@ -1315,6 +1807,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
triggeredByIncidentId?: ObjectID | undefined;
triggeredByAlertId?: ObjectID | undefined;
triggeredByAlertEpisodeId?: ObjectID | undefined;
userNotificationEventType: UserNotificationEventType;
onCallPolicyExecutionLogId?: ObjectID | undefined;
onCallPolicyId: ObjectID | undefined;
@@ -1339,6 +1832,10 @@ export class Service extends DatabaseService<Model> {
userOnCallLog.triggeredByAlertId = options.triggeredByAlertId;
}
if (options.triggeredByAlertEpisodeId) {
userOnCallLog.triggeredByAlertEpisodeId = options.triggeredByAlertEpisodeId;
}
userOnCallLog.userNotificationEventType = options.userNotificationEventType;
if (options.onCallPolicyExecutionLogId) {

View File

@@ -120,6 +120,28 @@ export default class PushNotificationUtil {
});
}
public static createAlertEpisodeCreatedNotification(params: {
alertEpisodeTitle: string;
projectName: string;
alertEpisodeViewLink: string;
}): PushNotificationMessage {
const { alertEpisodeTitle, projectName, alertEpisodeViewLink } = params;
return PushNotificationUtil.applyDefaults({
title: `New Alert Episode: ${alertEpisodeTitle}`,
body: `A new alert episode has been created in ${projectName}. Click to view details.`,
clickAction: alertEpisodeViewLink,
url: alertEpisodeViewLink,
tag: "alert-episode-created",
requireInteraction: true,
data: {
type: "alert-episode-created",
alertEpisodeTitle: alertEpisodeTitle,
projectName: projectName,
url: alertEpisodeViewLink,
},
});
}
public static createMonitorStatusChangedNotification(params: {
monitorName: string;
projectName: string;

View File

@@ -13,6 +13,8 @@ import UserNotificationRule from "Common/Models/DatabaseModels/UserNotificationR
import UserOnCallLog from "Common/Models/DatabaseModels/UserOnCallLog";
import Alert from "Common/Models/DatabaseModels/Alert";
import AlertService from "Common/Server/Services/AlertService";
import AlertEpisode from "Common/Models/DatabaseModels/AlertEpisode";
import AlertEpisodeService from "Common/Server/Services/AlertEpisodeService";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
RunCron(
@@ -36,6 +38,7 @@ RunCron(
userNotificationEventType: true,
triggeredByIncidentId: true,
triggeredByAlertId: true,
triggeredByAlertEpisodeId: true,
onCallDutyPolicyEscalationRuleId: true,
onCallDutyPolicyExecutionLogTimelineId: true,
onCallDutyPolicyExecutionLogId: true,
@@ -72,6 +75,7 @@ const executePendingNotificationLog: ExecutePendingNotificationLogFunction =
let incident: Incident | null = null;
let alert: Alert | null = null;
let alertEpisode: AlertEpisode | null = null;
if (pendingNotificationLog.triggeredByIncidentId) {
incident = await IncidentService.findOneById({
@@ -97,8 +101,20 @@ const executePendingNotificationLog: ExecutePendingNotificationLogFunction =
});
}
if (!incident && !alert) {
throw new Error("Incident or Alert not found.");
if (pendingNotificationLog.triggeredByAlertEpisodeId) {
alertEpisode = await AlertEpisodeService.findOneById({
id: pendingNotificationLog.triggeredByAlertEpisodeId!,
props: {
isRoot: true,
},
select: {
alertSeverityId: true,
},
});
}
if (!incident && !alert && !alertEpisode) {
throw new Error("Incident, Alert, or Alert Episode not found.");
}
if (incident) {
@@ -147,6 +163,30 @@ const executePendingNotificationLog: ExecutePendingNotificationLogFunction =
}
}
if (alertEpisode) {
// check if the alert episode is acknowledged.
const isAcknowledged: boolean =
await AlertEpisodeService.isEpisodeAcknowledged({
episodeId: pendingNotificationLog.triggeredByAlertEpisodeId!,
});
if (isAcknowledged) {
// then mark this policy as executed.
await UserOnCallLogService.updateOneById({
id: pendingNotificationLog.id!,
data: {
status: UserNotificationExecutionStatus.Completed,
statusMessage:
"Execution completed because alert episode is acknowledged.",
},
props: {
isRoot: true,
},
});
return;
}
}
const notificationRules: Array<UserNotificationRule> =
await UserNotificationRuleService.findBy({
query: {
@@ -154,7 +194,10 @@ const executePendingNotificationLog: ExecutePendingNotificationLogFunction =
userId: pendingNotificationLog.userId!,
ruleType: ruleType,
incidentSeverityId: incident?.incidentSeverityId || undefined,
alertSeverityId: alert?.alertSeverityId || undefined,
alertSeverityId:
alert?.alertSeverityId ||
alertEpisode?.alertSeverityId ||
undefined,
},
select: {
_id: true,
@@ -202,6 +245,8 @@ const executePendingNotificationLog: ExecutePendingNotificationLogFunction =
projectId: pendingNotificationLog.projectId!,
triggeredByIncidentId: pendingNotificationLog.triggeredByIncidentId,
triggeredByAlertId: pendingNotificationLog.triggeredByAlertId,
triggeredByAlertEpisodeId:
pendingNotificationLog.triggeredByAlertEpisodeId,
userNotificationEventType:
pendingNotificationLog.userNotificationEventType!,
onCallPolicyExecutionLogId: