mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add support for alert episode handling in user on-call logs and notifications
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user