Files
oneuptime/Common/Server/Services/WorkspaceNotificationSummaryService.ts
Nawaz Dhandala d9c7259356 Refactor code for improved readability and consistency
- Simplified arrow function syntax in MasterPassword.tsx and DashboardAPI.ts
- Consolidated logger.debug statements in PublicDashboard.ts and DashboardDomainAPI.ts
- Reformatted multi-line statements for better clarity in various files
- Updated migration files for consistent naming conventions and formatting
- Enhanced code structure in DashboardDomainService.ts and MonitorTelemetryMonitor.ts
- Incremented version number to 10.0.40
2026-03-26 16:41:11 +00:00

1465 lines
53 KiB
TypeScript

import ObjectID from "../../Types/ObjectID";
import DatabaseService from "./DatabaseService";
import WorkspaceNotificationSummary from "../../Models/DatabaseModels/WorkspaceNotificationSummary";
import WorkspaceNotificationSummaryType from "../../Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryType";
import WorkspaceNotificationSummaryItem from "../../Types/Workspace/NotificationSummary/WorkspaceNotificationSummaryItem";
import BadDataException from "../../Types/Exception/BadDataException";
import IncidentService from "./IncidentService";
import AlertService from "./AlertService";
import IncidentEpisodeService from "./IncidentEpisodeService";
import AlertEpisodeService from "./AlertEpisodeService";
import IncidentStateTimelineService from "./IncidentStateTimelineService";
import AlertStateTimelineService from "./AlertStateTimelineService";
import Incident from "../../Models/DatabaseModels/Incident";
import Alert from "../../Models/DatabaseModels/Alert";
import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTimeline";
import AlertStateTimeline from "../../Models/DatabaseModels/AlertStateTimeline";
import Label from "../../Models/DatabaseModels/Label";
import Monitor from "../../Models/DatabaseModels/Monitor";
import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
import WorkspaceNotificationStatus from "../../Types/Workspace/WorkspaceNotificationStatus";
import WorkspaceNotificationActionType from "../../Types/Workspace/WorkspaceNotificationActionType";
import logger from "../Utils/Logger";
import OneUptimeDate from "../../Types/Date";
import QueryHelper from "../Types/Database/QueryHelper";
import WorkspaceMessagePayload, {
WorkspaceMessageBlock,
WorkspacePayloadDivider,
WorkspacePayloadHeader,
WorkspacePayloadMarkdown,
} from "../../Types/Workspace/WorkspaceMessagePayload";
import WorkspaceUtil from "../Utils/Workspace/Workspace";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import URL from "../../Types/API/URL";
import DatabaseConfig from "../DatabaseConfig";
import NotificationRuleCondition, {
NotificationRuleConditionCheckOn,
} from "../../Types/Workspace/NotificationRules/NotificationRuleCondition";
import FilterCondition from "../../Types/Filter/FilterCondition";
import { WorkspaceNotificationRuleUtil } from "../../Types/Workspace/NotificationRules/NotificationRuleUtil";
import IncidentNotificationRule from "../../Types/Workspace/NotificationRules/NotificationRuleTypes/IncidentNotificationRule";
/*
* NOTE ON FORMATTING:
* WorkspacePayloadMarkdown text goes through SlackifyMarkdown which converts
* standard markdown to Slack mrkdwn. So we must use:
* **bold** (NOT *bold*)
* _italic_ (same in both)
* [text](url) (NOT <url|text>)
*/
interface TimelineData {
ackBy?: string | undefined;
resolvedBy?: string | undefined;
ackAt?: Date | undefined;
resolvedAt?: Date | undefined;
declaredAt?: Date | undefined;
}
export class Service extends DatabaseService<WorkspaceNotificationSummary> {
public constructor() {
super(WorkspaceNotificationSummary);
}
@CaptureSpan()
public async testSummary(data: {
summaryId: ObjectID;
projectId: ObjectID;
testByUserId: ObjectID;
props: DatabaseCommonInteractionProps;
}): Promise<void> {
await this.sendSummary({ summaryId: data.summaryId, isTest: true });
}
@CaptureSpan()
public async sendSummary(data: {
summaryId: ObjectID;
isTest?: boolean;
}): Promise<void> {
const summary: WorkspaceNotificationSummary | null = await this.findOneById(
{
id: data.summaryId,
select: {
projectId: true,
name: true,
workspaceType: true,
summaryType: true,
recurringInterval: true,
numberOfDaysOfData: true,
channelNames: true,
teamName: true,
summaryItems: true,
filters: true,
filterCondition: true,
},
props: {
isRoot: true,
},
},
);
if (!summary) {
throw new BadDataException("Summary not found");
}
if (!summary.projectId) {
throw new BadDataException("Summary project ID not found");
}
if (!summary.channelNames || summary.channelNames.length === 0) {
throw new BadDataException("No channel names configured for summary");
}
if (!summary.summaryItems || summary.summaryItems.length === 0) {
throw new BadDataException("No summary items selected");
}
const messageBlocks: Array<WorkspaceMessageBlock> =
await this.buildSummaryMessageBlocks({ summary });
const messagePayload: WorkspaceMessagePayload = {
_type: "WorkspaceMessagePayload",
channelNames: summary.channelNames,
channelIds: [],
messageBlocks: messageBlocks,
workspaceType: summary.workspaceType!,
teamId: summary.teamName || undefined,
};
try {
await WorkspaceUtil.postMessageToAllWorkspaceChannelsAsBot({
projectId: summary.projectId,
messagePayloadsByWorkspace: [messagePayload],
});
// Log successful send
for (const channelName of summary.channelNames) {
await WorkspaceNotificationLogService.createWorkspaceLog(
{
projectId: summary.projectId!,
workspaceType: summary.workspaceType!,
channelName: channelName,
actionType: WorkspaceNotificationActionType.SendMessage,
status: WorkspaceNotificationStatus.Success,
statusMessage: data.isTest
? "Test summary sent successfully"
: "Summary sent successfully",
message: `${summary.summaryType || ""} summary "${summary.name || "Untitled"}" sent to channel "${channelName}"`,
},
{ isRoot: true },
);
}
} catch (err) {
// Log failed send
for (const channelName of summary.channelNames) {
await WorkspaceNotificationLogService.createWorkspaceLog(
{
projectId: summary.projectId!,
workspaceType: summary.workspaceType!,
channelName: channelName,
actionType: WorkspaceNotificationActionType.SendMessage,
status: WorkspaceNotificationStatus.Error,
statusMessage: err instanceof Error ? err.message : "Unknown error",
message: `Failed to send ${summary.summaryType || ""} summary "${summary.name || "Untitled"}" to channel "${channelName}"`,
},
{ isRoot: true },
).catch((logErr: unknown) => {
logger.error(
"Failed to create workspace notification log for summary send failure",
);
logger.error(logErr);
});
}
throw err; // Re-throw so caller knows it failed
}
if (!data.isTest) {
await this.updateOneById({
id: data.summaryId,
data: {
lastSentAt: OneUptimeDate.getCurrentDate(),
},
props: {
isRoot: true,
},
});
}
}
// ───────────────────────── helpers ─────────────────────────
private static divider(): WorkspacePayloadDivider {
return { _type: "WorkspacePayloadDivider" };
}
private static header(text: string): WorkspacePayloadHeader {
return { _type: "WorkspacePayloadHeader", text };
}
private static md(text: string): WorkspacePayloadMarkdown {
return { _type: "WorkspacePayloadMarkdown", text };
}
private static bold(text: string): string {
return `**${text}**`;
}
private static link(url: string, text: string): string {
return `[${text}](${url})`;
}
private static formatDuration(totalMinutes: number): string {
if (totalMinutes < 1) {
return "< 1m";
}
const days: number = Math.floor(totalMinutes / 1440);
const hours: number = Math.floor((totalMinutes % 1440) / 60);
const mins: number = Math.round(totalMinutes % 60);
const parts: Array<string> = [];
if (days > 0) {
parts.push(`${days}d`);
}
if (hours > 0) {
parts.push(`${hours}h`);
}
if (mins > 0 || parts.length === 0) {
parts.push(`${mins}m`);
}
return parts.join(" ");
}
private static formatDate(date: Date): string {
return OneUptimeDate.getDateAsLocalFormattedString(date, true);
}
private static has(
items: Array<WorkspaceNotificationSummaryItem>,
item: WorkspaceNotificationSummaryItem,
): boolean {
if (items.includes(WorkspaceNotificationSummaryItem.All)) {
return true;
}
return items.includes(item);
}
// Check if an item matches the summary's filter conditions
private static matchesFilters(data: {
filters: Array<NotificationRuleCondition> | undefined;
filterCondition: FilterCondition | undefined;
values: {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
};
}): boolean {
if (!data.filters || data.filters.length === 0) {
return true; // no filters = include everything
}
const rule: IncidentNotificationRule = {
filters: data.filters,
filterCondition: data.filterCondition || FilterCondition.Any,
} as IncidentNotificationRule;
return WorkspaceNotificationRuleUtil.isRuleMatching({
notificationRule: rule,
values: data.values,
});
}
// Build values map for an incident
private static buildIncidentValues(incident: Incident): {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
} {
return {
[NotificationRuleConditionCheckOn.IncidentTitle]: incident.title || "",
[NotificationRuleConditionCheckOn.IncidentDescription]:
incident.description || "",
[NotificationRuleConditionCheckOn.IncidentSeverity]:
incident.incidentSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.IncidentState]:
incident.currentIncidentState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.IncidentLabels]:
incident.labels?.map((l: Label) => {
return l._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.Monitors]:
incident.monitors?.map((m: Monitor) => {
return m._id?.toString() || "";
}) || [],
// unused for incidents
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyName]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyDescription]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeLabels]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeLabels]: undefined,
};
}
// Build values map for an alert
private static buildAlertValues(alert: Alert): {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
} {
return {
[NotificationRuleConditionCheckOn.AlertTitle]: alert.title || "",
[NotificationRuleConditionCheckOn.AlertDescription]:
alert.description || "",
[NotificationRuleConditionCheckOn.AlertSeverity]:
alert.alertSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertState]:
alert.currentAlertState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertLabels]:
alert.labels?.map((l: Label) => {
return l._id?.toString() || "";
}) || [],
[NotificationRuleConditionCheckOn.Monitors]: alert.monitor?._id
? [alert.monitor._id.toString()]
: [],
// unused for alerts
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyName]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyDescription]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeLabels]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeLabels]: undefined,
};
}
// Build values map for an incident episode
private static buildIncidentEpisodeValues(episode: IncidentEpisode): {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
} {
return {
[NotificationRuleConditionCheckOn.IncidentEpisodeTitle]:
episode.title || "",
[NotificationRuleConditionCheckOn.IncidentEpisodeDescription]:
episode.description || "",
[NotificationRuleConditionCheckOn.IncidentEpisodeSeverity]:
episode.incidentSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.IncidentEpisodeState]:
episode.currentIncidentState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.IncidentEpisodeLabels]:
episode.labels?.map((l: Label) => {
return l._id?.toString() || "";
}) || [],
// unused for incident episodes
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.AlertEpisodeLabels]: undefined,
[NotificationRuleConditionCheckOn.Monitors]: undefined,
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyName]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyDescription]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyLabels]: undefined,
};
}
// Build values map for an alert episode
private static buildAlertEpisodeValues(episode: AlertEpisode): {
[key in NotificationRuleConditionCheckOn]:
| string
| Array<string>
| undefined;
} {
return {
[NotificationRuleConditionCheckOn.AlertEpisodeTitle]: episode.title || "",
[NotificationRuleConditionCheckOn.AlertEpisodeDescription]:
episode.description || "",
[NotificationRuleConditionCheckOn.AlertEpisodeSeverity]:
episode.alertSeverity?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertEpisodeState]:
episode.currentAlertState?._id?.toString() || "",
[NotificationRuleConditionCheckOn.AlertEpisodeLabels]:
episode.labels?.map((l: Label) => {
return l._id?.toString() || "";
}) || [],
// unused for alert episodes
[NotificationRuleConditionCheckOn.IncidentTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentState]: undefined,
[NotificationRuleConditionCheckOn.IncidentLabels]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeTitle]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeDescription]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeSeverity]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeState]: undefined,
[NotificationRuleConditionCheckOn.IncidentEpisodeLabels]: undefined,
[NotificationRuleConditionCheckOn.AlertTitle]: undefined,
[NotificationRuleConditionCheckOn.AlertDescription]: undefined,
[NotificationRuleConditionCheckOn.AlertSeverity]: undefined,
[NotificationRuleConditionCheckOn.AlertState]: undefined,
[NotificationRuleConditionCheckOn.AlertLabels]: undefined,
[NotificationRuleConditionCheckOn.Monitors]: undefined,
[NotificationRuleConditionCheckOn.MonitorName]: undefined,
[NotificationRuleConditionCheckOn.MonitorType]: undefined,
[NotificationRuleConditionCheckOn.MonitorStatus]: undefined,
[NotificationRuleConditionCheckOn.MonitorLabels]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceTitle]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceDescription]:
undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceState]: undefined,
[NotificationRuleConditionCheckOn.ScheduledMaintenanceLabels]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyName]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyDescription]: undefined,
[NotificationRuleConditionCheckOn.OnCallDutyPolicyLabels]: undefined,
};
}
// ───────────────────────── main builder ─────────────────────────
@CaptureSpan()
private async buildSummaryMessageBlocks(data: {
summary: WorkspaceNotificationSummary;
}): Promise<Array<WorkspaceMessageBlock>> {
const { summary } = data;
const blocks: Array<WorkspaceMessageBlock> = [];
const items: Array<WorkspaceNotificationSummaryItem> =
summary.summaryItems!;
const days: number = summary.numberOfDaysOfData || 7;
const type: WorkspaceNotificationSummaryType = summary.summaryType!;
const fromDate: Date = OneUptimeDate.addRemoveDays(
OneUptimeDate.getCurrentDate(),
-days,
);
const fromDateStr: string = Service.formatDate(fromDate);
const toDateStr: string = Service.formatDate(
OneUptimeDate.getCurrentDate(),
);
// Title
blocks.push(
Service.header(`${type} Summary — ${fromDateStr} to ${toDateStr}`),
);
blocks.push(
Service.md(
`_Reporting period: ${Service.bold(String(days))} day${days !== 1 ? "s" : ""}_`,
),
);
blocks.push(Service.divider());
// Build type-specific content
if (
type === WorkspaceNotificationSummaryType.Incident ||
type === WorkspaceNotificationSummaryType.IncidentEpisode
) {
await this.buildIncidentBlocks({
blocks,
items,
type,
fromDate,
projectId: summary.projectId!,
filters: summary.filters || undefined,
filterCondition: summary.filterCondition || undefined,
});
} else {
await this.buildAlertBlocks({
blocks,
items,
type,
fromDate,
projectId: summary.projectId!,
filters: summary.filters || undefined,
filterCondition: summary.filterCondition || undefined,
});
}
// Footer
blocks.push(Service.divider());
blocks.push(
Service.md(`_Sent by OneUptime • ${summary.name || "Untitled"}_`),
);
return blocks;
}
// ───────────────────────── Incidents ─────────────────────────
@CaptureSpan()
private async buildIncidentBlocks(data: {
blocks: Array<WorkspaceMessageBlock>;
items: Array<WorkspaceNotificationSummaryItem>;
type: WorkspaceNotificationSummaryType;
fromDate: Date;
projectId: ObjectID;
filters?: Array<NotificationRuleCondition> | undefined;
filterCondition?: FilterCondition | undefined;
}): Promise<void> {
if (data.type === WorkspaceNotificationSummaryType.IncidentEpisode) {
await this.buildIncidentEpisodeBlocks(data);
return;
}
const { blocks, items, fromDate, projectId } = data;
let incidents: Array<Incident> = await IncidentService.findAllBy({
query: {
projectId,
createdAt: QueryHelper.greaterThanEqualTo(fromDate),
},
select: {
_id: true,
title: true,
description: true,
incidentNumber: true,
incidentNumberWithPrefix: true,
incidentSeverity: { name: true, _id: true },
currentIncidentState: {
name: true,
_id: true,
isResolvedState: true,
isAcknowledgedState: true,
},
labels: { _id: true, name: true },
monitors: { name: true, _id: true },
createdAt: true,
declaredAt: true,
},
props: { isRoot: true },
});
// Apply filters
if (data.filters && data.filters.length > 0) {
incidents = incidents.filter((inc: Incident) => {
return Service.matchesFilters({
filters: data.filters,
filterCondition: data.filterCondition,
values: Service.buildIncidentValues(inc),
});
});
}
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
// Overview stats
if (Service.has(items, WorkspaceNotificationSummaryItem.TotalCount)) {
const resolved: number = incidents.filter((i: Incident) => {
return i.currentIncidentState?.isResolvedState;
}).length;
const open: number = incidents.length - resolved;
blocks.push(
Service.md(
`${Service.bold("Total:")} ${incidents.length} incident${incidents.length !== 1 ? "s" : ""} · ` +
`${Service.bold("Open:")} ${open} · ${Service.bold("Resolved:")} ${resolved}`,
),
);
}
// Severity breakdown
if (
Service.has(items, WorkspaceNotificationSummaryItem.SeverityBreakdown)
) {
const map: Map<string, number> = new Map();
for (const i of incidents) {
const s: string = i.incidentSeverity?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [sev, count] of map) {
parts.push(`${sev}: ${Service.bold(String(count))}`);
}
blocks.push(
Service.md(`${Service.bold("By Severity:")} ${parts.join(" · ")}`),
);
}
}
// State breakdown
if (Service.has(items, WorkspaceNotificationSummaryItem.StateBreakdown)) {
const map: Map<string, number> = new Map();
for (const i of incidents) {
const s: string = i.currentIncidentState?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [state, count] of map) {
parts.push(`${state}: ${Service.bold(String(count))}`);
}
blocks.push(
Service.md(`${Service.bold("By State:")} ${parts.join(" · ")}`),
);
}
}
// Timeline data for MTTA/MTTR/who
const needTimeline: boolean =
Service.has(items, WorkspaceNotificationSummaryItem.WhoAcknowledged) ||
Service.has(items, WorkspaceNotificationSummaryItem.WhoResolved) ||
Service.has(items, WorkspaceNotificationSummaryItem.TimeToAcknowledge) ||
Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve);
const tlMap: Map<string, TimelineData> = new Map();
if (needTimeline && incidents.length > 0) {
const ids: Array<ObjectID> = incidents
.filter((i: Incident) => {
return i._id;
})
.map((i: Incident) => {
return new ObjectID(i._id!.toString());
});
const timelines: Array<IncidentStateTimeline> =
await IncidentStateTimelineService.findAllBy({
query: {
projectId,
incidentId: QueryHelper.any(ids),
},
select: {
incidentId: true,
incidentState: {
isAcknowledgedState: true,
isResolvedState: true,
},
createdByUser: { name: true, email: true },
createdAt: true,
},
props: { isRoot: true },
});
for (const tl of timelines) {
const id: string = tl.incidentId?.toString() || "";
if (!tlMap.has(id)) {
tlMap.set(id, {});
}
const td: TimelineData = tlMap.get(id)!;
const userName: string =
tl.createdByUser?.name?.toString() ||
tl.createdByUser?.email?.toString() ||
"System";
if (tl.incidentState?.isAcknowledgedState && !td.ackAt) {
td.ackBy = userName;
td.ackAt = tl.createdAt;
}
if (tl.incidentState?.isResolvedState && !td.resolvedAt) {
td.resolvedBy = userName;
td.resolvedAt = tl.createdAt;
}
}
for (const inc of incidents) {
const id: string = inc._id?.toString() || "";
if (!tlMap.has(id)) {
tlMap.set(id, {});
}
tlMap.get(id)!.declaredAt = inc.declaredAt || inc.createdAt;
}
}
// MTTA
if (
Service.has(items, WorkspaceNotificationSummaryItem.TimeToAcknowledge)
) {
const { avg, count } = this.computeAvg(tlMap, "ack");
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTA (Mean Time to Acknowledge):")} ${Service.bold(Service.formatDuration(avg))} _(${count} acknowledged)_`
: `${Service.bold("MTTA (Mean Time to Acknowledge):")} _No incidents acknowledged_`,
),
);
}
// MTTR
if (Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve)) {
const { avg, count } = this.computeAvg(tlMap, "resolve");
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTR (Mean Time to Resolve):")} ${Service.bold(Service.formatDuration(avg))} _(${count} resolved)_`
: `${Service.bold("MTTR (Mean Time to Resolve):")} _No incidents resolved_`,
),
);
}
// Resources affected
if (
Service.has(items, WorkspaceNotificationSummaryItem.ResourcesAffected)
) {
const names: Set<string> = new Set();
for (const inc of incidents) {
if (inc.monitors) {
for (const m of inc.monitors) {
if (m.name) {
names.add(m.name);
}
}
}
}
if (names.size > 0) {
blocks.push(
Service.md(
`${Service.bold(`Resources Affected (${names.size}):`)} ${Array.from(names).join(", ")}`,
),
);
}
}
// Detailed list
if (Service.has(items, WorkspaceNotificationSummaryItem.ListWithLinks)) {
blocks.push(Service.divider());
if (incidents.length === 0) {
blocks.push(Service.md(`_No incidents reported in this period._`));
return;
}
for (const inc of incidents) {
const id: string = inc._id?.toString() || "";
const display: string =
inc.incidentNumberWithPrefix || `#${inc.incidentNumber || ""}`;
const linkUrl: string = URL.fromString(dashboardUrl.toString())
.addRoute(`/${projectId.toString()}/incidents/${id}`)
.toString();
const td: TimelineData | undefined = tlMap.get(id);
// Title line with link
let text: string = `${Service.bold(Service.link(linkUrl, `${display}${inc.title || "Untitled"}`))}`;
// Meta line
const meta: Array<string> = [];
if (inc.incidentSeverity?.name) {
meta.push(`Severity: ${Service.bold(inc.incidentSeverity.name)}`);
}
if (inc.currentIncidentState?.name) {
meta.push(`State: ${Service.bold(inc.currentIncidentState.name)}`);
}
if (inc.declaredAt) {
meta.push(`Declared: ${Service.formatDate(inc.declaredAt)}`);
}
if (meta.length > 0) {
text += `\n${meta.join(" · ")}`;
}
// Ack & resolve line
const ackResolve: Array<string> = [];
if (
Service.has(items, WorkspaceNotificationSummaryItem.WhoAcknowledged)
) {
if (td?.ackBy && td?.ackAt) {
ackResolve.push(
`Ack: ${Service.bold(td.ackBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || inc.createdAt!, td.ackAt))}`,
);
} else if (td?.resolvedBy && td?.resolvedAt) {
// If not explicitly acknowledged but resolved, ack time = resolve time
ackResolve.push(
`Ack: ${Service.bold(td.resolvedBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || inc.createdAt!, td.resolvedAt))}`,
);
} else {
ackResolve.push(`_Not yet acknowledged_`);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.WhoResolved)) {
if (td?.resolvedBy && td?.resolvedAt) {
ackResolve.push(
`Resolved: ${Service.bold(td.resolvedBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || inc.createdAt!, td.resolvedAt))}`,
);
} else if (!inc.currentIncidentState?.isResolvedState) {
ackResolve.push(`_Not yet resolved_`);
}
}
if (ackResolve.length > 0) {
text += `\n${ackResolve.join(" · ")}`;
}
blocks.push(Service.md(text));
}
}
}
// ───────────────────────── Incident Episodes ─────────────────────────
@CaptureSpan()
private async buildIncidentEpisodeBlocks(data: {
blocks: Array<WorkspaceMessageBlock>;
items: Array<WorkspaceNotificationSummaryItem>;
fromDate: Date;
projectId: ObjectID;
filters?: Array<NotificationRuleCondition> | undefined;
filterCondition?: FilterCondition | undefined;
}): Promise<void> {
const { blocks, items, fromDate, projectId } = data;
let episodes: Array<IncidentEpisode> =
await IncidentEpisodeService.findAllBy({
query: {
projectId,
createdAt: QueryHelper.greaterThanEqualTo(fromDate),
},
select: {
_id: true,
title: true,
description: true,
incidentSeverity: { name: true, _id: true },
currentIncidentState: {
name: true,
_id: true,
isResolvedState: true,
},
labels: { _id: true, name: true },
createdAt: true,
resolvedAt: true,
},
props: { isRoot: true },
});
// Apply filters
if (data.filters && data.filters.length > 0) {
episodes = episodes.filter((ep: IncidentEpisode) => {
return Service.matchesFilters({
filters: data.filters,
filterCondition: data.filterCondition,
values: Service.buildIncidentEpisodeValues(ep),
});
});
}
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
if (Service.has(items, WorkspaceNotificationSummaryItem.TotalCount)) {
const resolved: number = episodes.filter((e: IncidentEpisode) => {
return e.currentIncidentState?.isResolvedState;
}).length;
blocks.push(
Service.md(
`${Service.bold("Total:")} ${episodes.length} episode${episodes.length !== 1 ? "s" : ""} · ` +
`${Service.bold("Open:")} ${episodes.length - resolved} · ${Service.bold("Resolved:")} ${resolved}`,
),
);
}
if (
Service.has(items, WorkspaceNotificationSummaryItem.SeverityBreakdown)
) {
const map: Map<string, number> = new Map();
for (const e of episodes) {
const s: string = e.incidentSeverity?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [sev, c] of map) {
parts.push(`${sev}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By Severity:")} ${parts.join(" · ")}`),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.StateBreakdown)) {
const map: Map<string, number> = new Map();
for (const e of episodes) {
const s: string = e.currentIncidentState?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [state, c] of map) {
parts.push(`${state}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By State:")} ${parts.join(" · ")}`),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve)) {
let total: number = 0;
let count: number = 0;
for (const e of episodes) {
if (e.resolvedAt && e.createdAt) {
total += OneUptimeDate.getMinutesBetweenTwoDates(
e.createdAt,
e.resolvedAt,
);
count++;
}
}
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTR (Mean Time to Resolve):")} ${Service.bold(Service.formatDuration(Math.round(total / count)))} _(${count} resolved)_`
: `${Service.bold("MTTR (Mean Time to Resolve):")} _No episodes resolved_`,
),
);
}
if (Service.has(items, WorkspaceNotificationSummaryItem.ListWithLinks)) {
blocks.push(Service.divider());
if (episodes.length === 0) {
blocks.push(Service.md(`_No incident episodes in this period._`));
return;
}
for (const ep of episodes) {
const id: string = ep._id?.toString() || "";
const linkUrl: string = URL.fromString(dashboardUrl.toString())
.addRoute(`/${projectId.toString()}/incidents/episodes/${id}`)
.toString();
let text: string = `${Service.bold(Service.link(linkUrl, ep.title || "Untitled Episode"))}`;
const meta: Array<string> = [];
if (ep.incidentSeverity?.name) {
meta.push(`Severity: ${Service.bold(ep.incidentSeverity.name)}`);
}
if (ep.currentIncidentState?.name) {
meta.push(`State: ${Service.bold(ep.currentIncidentState.name)}`);
}
if (ep.createdAt) {
meta.push(`Created: ${Service.formatDate(ep.createdAt)}`);
}
if (ep.resolvedAt && ep.createdAt) {
meta.push(
`Resolved in ${Service.bold(Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(ep.createdAt, ep.resolvedAt)))}`,
);
}
if (meta.length > 0) {
text += `\n${meta.join(" · ")}`;
}
blocks.push(Service.md(text));
}
}
}
// ───────────────────────── Alerts ─────────────────────────
@CaptureSpan()
private async buildAlertBlocks(data: {
blocks: Array<WorkspaceMessageBlock>;
items: Array<WorkspaceNotificationSummaryItem>;
type: WorkspaceNotificationSummaryType;
fromDate: Date;
projectId: ObjectID;
filters?: Array<NotificationRuleCondition> | undefined;
filterCondition?: FilterCondition | undefined;
}): Promise<void> {
if (data.type === WorkspaceNotificationSummaryType.AlertEpisode) {
await this.buildAlertEpisodeBlocks(data);
return;
}
const { blocks, items, fromDate, projectId } = data;
let alerts: Array<Alert> = await AlertService.findAllBy({
query: {
projectId,
createdAt: QueryHelper.greaterThanEqualTo(fromDate),
},
select: {
_id: true,
title: true,
description: true,
alertNumber: true,
alertNumberWithPrefix: true,
alertSeverity: { name: true, _id: true },
currentAlertState: {
name: true,
_id: true,
isResolvedState: true,
isAcknowledgedState: true,
},
labels: { _id: true, name: true },
monitor: { name: true, _id: true },
createdAt: true,
},
props: { isRoot: true },
});
// Apply filters
if (data.filters && data.filters.length > 0) {
alerts = alerts.filter((alert: Alert) => {
return Service.matchesFilters({
filters: data.filters,
filterCondition: data.filterCondition,
values: Service.buildAlertValues(alert),
});
});
}
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
if (Service.has(items, WorkspaceNotificationSummaryItem.TotalCount)) {
const resolved: number = alerts.filter((a: Alert) => {
return a.currentAlertState?.isResolvedState;
}).length;
blocks.push(
Service.md(
`${Service.bold("Total:")} ${alerts.length} alert${alerts.length !== 1 ? "s" : ""} · ` +
`${Service.bold("Open:")} ${alerts.length - resolved} · ${Service.bold("Resolved:")} ${resolved}`,
),
);
}
if (
Service.has(items, WorkspaceNotificationSummaryItem.SeverityBreakdown)
) {
const map: Map<string, number> = new Map();
for (const a of alerts) {
const s: string = a.alertSeverity?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [sev, c] of map) {
parts.push(`${sev}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By Severity:")} ${parts.join(" · ")}`),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.StateBreakdown)) {
const map: Map<string, number> = new Map();
for (const a of alerts) {
const s: string = a.currentAlertState?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [state, c] of map) {
parts.push(`${state}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By State:")} ${parts.join(" · ")}`),
);
}
}
// Timeline data
const needTimeline: boolean =
Service.has(items, WorkspaceNotificationSummaryItem.WhoAcknowledged) ||
Service.has(items, WorkspaceNotificationSummaryItem.WhoResolved) ||
Service.has(items, WorkspaceNotificationSummaryItem.TimeToAcknowledge) ||
Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve);
const tlMap: Map<string, TimelineData> = new Map();
if (needTimeline && alerts.length > 0) {
const ids: Array<ObjectID> = alerts
.filter((a: Alert) => {
return a._id;
})
.map((a: Alert) => {
return new ObjectID(a._id!.toString());
});
const timelines: Array<AlertStateTimeline> =
await AlertStateTimelineService.findAllBy({
query: { projectId, alertId: QueryHelper.any(ids) },
select: {
alertId: true,
alertState: {
isAcknowledgedState: true,
isResolvedState: true,
},
createdByUser: { name: true, email: true },
createdAt: true,
},
props: { isRoot: true },
});
for (const tl of timelines) {
const id: string = tl.alertId?.toString() || "";
if (!tlMap.has(id)) {
tlMap.set(id, {});
}
const td: TimelineData = tlMap.get(id)!;
const userName: string =
tl.createdByUser?.name?.toString() ||
tl.createdByUser?.email?.toString() ||
"System";
if (tl.alertState?.isAcknowledgedState && !td.ackAt) {
td.ackBy = userName;
td.ackAt = tl.createdAt;
}
if (tl.alertState?.isResolvedState && !td.resolvedAt) {
td.resolvedBy = userName;
td.resolvedAt = tl.createdAt;
}
}
for (const a of alerts) {
const id: string = a._id?.toString() || "";
if (!tlMap.has(id)) {
tlMap.set(id, {});
}
tlMap.get(id)!.declaredAt = a.createdAt;
}
}
if (
Service.has(items, WorkspaceNotificationSummaryItem.TimeToAcknowledge)
) {
const { avg, count } = this.computeAvg(tlMap, "ack");
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTA (Mean Time to Acknowledge):")} ${Service.bold(Service.formatDuration(avg))} _(${count} acknowledged)_`
: `${Service.bold("MTTA (Mean Time to Acknowledge):")} _No alerts acknowledged_`,
),
);
}
if (Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve)) {
const { avg, count } = this.computeAvg(tlMap, "resolve");
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTR (Mean Time to Resolve):")} ${Service.bold(Service.formatDuration(avg))} _(${count} resolved)_`
: `${Service.bold("MTTR (Mean Time to Resolve):")} _No alerts resolved_`,
),
);
}
if (
Service.has(items, WorkspaceNotificationSummaryItem.ResourcesAffected)
) {
const names: Set<string> = new Set();
for (const a of alerts) {
if (a.monitor?.name) {
names.add(a.monitor.name);
}
}
if (names.size > 0) {
blocks.push(
Service.md(
`${Service.bold(`Resources Affected (${names.size}):`)} ${Array.from(names).join(", ")}`,
),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.ListWithLinks)) {
blocks.push(Service.divider());
if (alerts.length === 0) {
blocks.push(Service.md(`_No alerts reported in this period._`));
return;
}
for (const a of alerts) {
const id: string = a._id?.toString() || "";
const display: string =
a.alertNumberWithPrefix || `#${a.alertNumber || ""}`;
const linkUrl: string = URL.fromString(dashboardUrl.toString())
.addRoute(`/${projectId.toString()}/alerts/${id}`)
.toString();
const td: TimelineData | undefined = tlMap.get(id);
let text: string = `${Service.bold(Service.link(linkUrl, `${display}${a.title || "Untitled"}`))}`;
const meta: Array<string> = [];
if (a.alertSeverity?.name) {
meta.push(`Severity: ${Service.bold(a.alertSeverity.name)}`);
}
if (a.currentAlertState?.name) {
meta.push(`State: ${Service.bold(a.currentAlertState.name)}`);
}
if (a.createdAt) {
meta.push(`Created: ${Service.formatDate(a.createdAt)}`);
}
if (meta.length > 0) {
text += `\n${meta.join(" · ")}`;
}
const ackResolve: Array<string> = [];
if (
Service.has(items, WorkspaceNotificationSummaryItem.WhoAcknowledged)
) {
if (td?.ackBy && td?.ackAt) {
ackResolve.push(
`Ack: ${Service.bold(td.ackBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || a.createdAt!, td.ackAt))}`,
);
} else if (td?.resolvedBy && td?.resolvedAt) {
// If not explicitly acknowledged but resolved, ack time = resolve time
ackResolve.push(
`Ack: ${Service.bold(td.resolvedBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || a.createdAt!, td.resolvedAt))}`,
);
} else {
ackResolve.push(`_Not yet acknowledged_`);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.WhoResolved)) {
if (td?.resolvedBy && td?.resolvedAt) {
ackResolve.push(
`Resolved: ${Service.bold(td.resolvedBy)} in ${Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(td.declaredAt || a.createdAt!, td.resolvedAt))}`,
);
} else if (!a.currentAlertState?.isResolvedState) {
ackResolve.push(`_Not yet resolved_`);
}
}
if (ackResolve.length > 0) {
text += `\n${ackResolve.join(" · ")}`;
}
blocks.push(Service.md(text));
}
}
}
// ───────────────────────── Alert Episodes ─────────────────────────
@CaptureSpan()
private async buildAlertEpisodeBlocks(data: {
blocks: Array<WorkspaceMessageBlock>;
items: Array<WorkspaceNotificationSummaryItem>;
fromDate: Date;
projectId: ObjectID;
filters?: Array<NotificationRuleCondition> | undefined;
filterCondition?: FilterCondition | undefined;
}): Promise<void> {
const { blocks, items, fromDate, projectId } = data;
let episodes: Array<AlertEpisode> = await AlertEpisodeService.findAllBy({
query: {
projectId,
createdAt: QueryHelper.greaterThanEqualTo(fromDate),
},
select: {
_id: true,
title: true,
description: true,
alertSeverity: { name: true, _id: true },
currentAlertState: { name: true, _id: true, isResolvedState: true },
labels: { _id: true, name: true },
createdAt: true,
resolvedAt: true,
},
props: { isRoot: true },
});
// Apply filters
if (data.filters && data.filters.length > 0) {
episodes = episodes.filter((ep: AlertEpisode) => {
return Service.matchesFilters({
filters: data.filters,
filterCondition: data.filterCondition,
values: Service.buildAlertEpisodeValues(ep),
});
});
}
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
if (Service.has(items, WorkspaceNotificationSummaryItem.TotalCount)) {
const resolved: number = episodes.filter((e: AlertEpisode) => {
return e.currentAlertState?.isResolvedState;
}).length;
blocks.push(
Service.md(
`${Service.bold("Total:")} ${episodes.length} episode${episodes.length !== 1 ? "s" : ""} · ` +
`${Service.bold("Open:")} ${episodes.length - resolved} · ${Service.bold("Resolved:")} ${resolved}`,
),
);
}
if (
Service.has(items, WorkspaceNotificationSummaryItem.SeverityBreakdown)
) {
const map: Map<string, number> = new Map();
for (const e of episodes) {
const s: string = e.alertSeverity?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [sev, c] of map) {
parts.push(`${sev}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By Severity:")} ${parts.join(" · ")}`),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.StateBreakdown)) {
const map: Map<string, number> = new Map();
for (const e of episodes) {
const s: string = e.currentAlertState?.name || "Unknown";
map.set(s, (map.get(s) || 0) + 1);
}
if (map.size > 0) {
const parts: Array<string> = [];
for (const [state, c] of map) {
parts.push(`${state}: ${Service.bold(String(c))}`);
}
blocks.push(
Service.md(`${Service.bold("By State:")} ${parts.join(" · ")}`),
);
}
}
if (Service.has(items, WorkspaceNotificationSummaryItem.TimeToResolve)) {
let total: number = 0;
let count: number = 0;
for (const e of episodes) {
if (e.resolvedAt && e.createdAt) {
total += OneUptimeDate.getMinutesBetweenTwoDates(
e.createdAt,
e.resolvedAt,
);
count++;
}
}
blocks.push(
Service.md(
count > 0
? `${Service.bold("MTTR (Mean Time to Resolve):")} ${Service.bold(Service.formatDuration(Math.round(total / count)))} _(${count} resolved)_`
: `${Service.bold("MTTR (Mean Time to Resolve):")} _No episodes resolved_`,
),
);
}
if (Service.has(items, WorkspaceNotificationSummaryItem.ListWithLinks)) {
blocks.push(Service.divider());
if (episodes.length === 0) {
blocks.push(Service.md(`_No alert episodes in this period._`));
return;
}
for (const ep of episodes) {
const id: string = ep._id?.toString() || "";
const linkUrl: string = URL.fromString(dashboardUrl.toString())
.addRoute(`/${projectId.toString()}/alerts/episodes/${id}`)
.toString();
let text: string = `${Service.bold(Service.link(linkUrl, ep.title || "Untitled Episode"))}`;
const meta: Array<string> = [];
if (ep.alertSeverity?.name) {
meta.push(`Severity: ${Service.bold(ep.alertSeverity.name)}`);
}
if (ep.currentAlertState?.name) {
meta.push(`State: ${Service.bold(ep.currentAlertState.name)}`);
}
if (ep.createdAt) {
meta.push(`Created: ${Service.formatDate(ep.createdAt)}`);
}
if (ep.resolvedAt && ep.createdAt) {
meta.push(
`Resolved in ${Service.bold(Service.formatDuration(OneUptimeDate.getMinutesBetweenTwoDates(ep.createdAt, ep.resolvedAt)))}`,
);
}
if (meta.length > 0) {
text += `\n${meta.join(" · ")}`;
}
blocks.push(Service.md(text));
}
}
}
// ───────────────────────── Utilities ─────────────────────────
private computeAvg(
tlMap: Map<string, TimelineData>,
kind: "ack" | "resolve",
): { avg: number; count: number } {
let total: number = 0;
let count: number = 0;
for (const [, td] of tlMap) {
// For ack: if not explicitly acknowledged but resolved, use resolve time as ack time
const eventTime: Date | undefined =
kind === "ack" ? td.ackAt || td.resolvedAt : td.resolvedAt;
if (eventTime && td.declaredAt) {
total += OneUptimeDate.getMinutesBetweenTwoDates(
td.declaredAt,
eventTime,
);
count++;
}
}
return { avg: count > 0 ? Math.round(total / count) : 0, count };
}
}
export default new Service();