mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 08:28:48 -05:00
purge incident module
This commit is contained in:
@@ -21,12 +21,12 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { fetchIncidentById, resolveIncident } = useFetchIncidents();
|
||||
const [incident, setIncident] = useState(null);
|
||||
const [incidentData, setIncidentData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const isActive = incident?.status === true;
|
||||
const duration = useGetIncidentsDuration(incident || null, isActive);
|
||||
const isActive = incidentData?.incident?.status === true;
|
||||
const duration = useGetIncidentsDuration(incidentData?.incident || null, isActive);
|
||||
|
||||
const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main;
|
||||
const toCapitalLetter = (text) =>
|
||||
@@ -75,7 +75,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
useEffect(() => {
|
||||
const loadIncident = async () => {
|
||||
if (!incidentId || !open) {
|
||||
setIncident(null);
|
||||
setIncidentData(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -83,10 +83,10 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const incidentData = await fetchIncidentById(incidentId);
|
||||
setIncident(incidentData);
|
||||
setIncidentData(incidentData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching incident:", error);
|
||||
setIncident(null);
|
||||
setIncidentData(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -107,11 +107,13 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
if (!incidentId) return;
|
||||
const updatedIncident = await fetchIncidentById(incidentId);
|
||||
if (updatedIncident) {
|
||||
setIncident(updatedIncident);
|
||||
setIncidentData(updatedIncident);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
console.log(incidentData);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<GenericFallback
|
||||
@@ -121,7 +123,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!incident) {
|
||||
if (!incidentData) {
|
||||
return (
|
||||
<GenericFallback
|
||||
isLoading={false}
|
||||
@@ -188,7 +190,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
variant="body1"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{incident?.monitorId?.name || t("incidentsPage.unknownMonitor")}
|
||||
{incidentData?.monitor?.name || t("incidentsPage.unknownMonitor")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -207,7 +209,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
variant="body1"
|
||||
sx={{ flex: 1, wordBreak: "break-word", fontFamily: "monospace" }}
|
||||
>
|
||||
{incident?.monitorId?.url || "-"}
|
||||
{incidentData?.monitor?.url || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -247,7 +249,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
label={t("incidentsPage.startedAt")}
|
||||
value={
|
||||
formatDateWithTz(
|
||||
incident?.startTime,
|
||||
incidentData?.incident?.startTime,
|
||||
"D MMM YYYY, h:mm A",
|
||||
uiTimezone
|
||||
) || "-"
|
||||
@@ -259,7 +261,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
label={t("incidentsPage.endedAt")}
|
||||
value={
|
||||
formatDateWithTz(
|
||||
incident?.endTime,
|
||||
incidentData?.incident?.endTime,
|
||||
"D MMM YYYY, h:mm A",
|
||||
uiTimezone
|
||||
) || "-"
|
||||
@@ -303,26 +305,28 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.statusCode")}
|
||||
value={incident?.statusCode ?? "-"}
|
||||
value={incidentData?.incident?.statusCode ?? "-"}
|
||||
/>
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.message")}
|
||||
value={incident?.message ?? "-"}
|
||||
value={incidentData?.incident?.message ?? "-"}
|
||||
/>
|
||||
|
||||
{!isActive && (
|
||||
<>
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.resolutionMethod")}
|
||||
value={toCapitalLetter(incident?.resolutionType) || "-"}
|
||||
value={
|
||||
toCapitalLetter(incidentData?.incident?.resolutionType) || "-"
|
||||
}
|
||||
/>
|
||||
{incident?.resolutionType === "manual" && (
|
||||
{incidentData?.incident?.resolutionType === "manual" && (
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.comment")}
|
||||
value={
|
||||
incident?.comment?.trim()
|
||||
? incident.comment
|
||||
incidentData?.incident.comment?.trim()
|
||||
? incidentData.incident.comment
|
||||
: t("incidentsPage.noCommentProvided")
|
||||
}
|
||||
/>
|
||||
@@ -353,15 +357,15 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
>
|
||||
{renderContent()}
|
||||
{!isActive &&
|
||||
incident?.resolutionType === "manual" &&
|
||||
incident?.resolvedBy?.email && (
|
||||
incidentData?.incident?.resolutionType === "manual" &&
|
||||
incidentData?.user?.email && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.resolvedBy")}: {incident.resolvedBy.email}
|
||||
{t("incidentsPage.resolvedBy")}: {incidentData.user.email}
|
||||
</Typography>
|
||||
)}
|
||||
<Stack
|
||||
|
||||
@@ -47,17 +47,12 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
// Models
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import User from "../db/models/User.js";
|
||||
import InviteToken from "../db/models/Invite.js";
|
||||
import Team from "../db/models/Team.js";
|
||||
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
|
||||
import MonitorStats from "../db/models/MonitorStats.js";
|
||||
import NotificationModel from "../db/models/Notification.js";
|
||||
import RecoveryToken from "../db/models/RecoveryToken.js";
|
||||
import Incident from "../db/models/Incident.js";
|
||||
|
||||
import IncidentModule from "../db/modules/incidentModule.js";
|
||||
|
||||
// repositories
|
||||
import {
|
||||
@@ -135,12 +130,10 @@ export const initializeServices = async ({
|
||||
settingsRepository: ISettingsRepository;
|
||||
}): Promise<InitializedServices> => {
|
||||
// Create DB
|
||||
const incidentModule = new IncidentModule({ logger, Incident, Monitor, User });
|
||||
|
||||
const db = new MongoDB({
|
||||
logger,
|
||||
envSettings,
|
||||
incidentModule,
|
||||
});
|
||||
|
||||
await db.connect();
|
||||
@@ -175,10 +168,11 @@ export const initializeServices = async ({
|
||||
const errorService = new ErrorService();
|
||||
|
||||
const incidentService = new IncidentService({
|
||||
db,
|
||||
logger,
|
||||
errorService,
|
||||
incidentsRepository,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
});
|
||||
|
||||
const checkService = new CheckService({
|
||||
|
||||
@@ -66,10 +66,13 @@ class IncidentController {
|
||||
|
||||
getIncidentById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const incident = await this.incidentService.getIncidentById({
|
||||
incidentId: req?.params?.incidentId,
|
||||
teamId: req?.user?.teamId,
|
||||
});
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const incidentId = req.params.incidentId;
|
||||
if (!incidentId) {
|
||||
throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 });
|
||||
}
|
||||
|
||||
const incident = await this.incidentService.getIncidentById(incidentId, teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -4,10 +4,9 @@ import { runMigrations } from "./migration/index.js";
|
||||
class MongoDB {
|
||||
static SERVICE_NAME = "MongoDB";
|
||||
|
||||
constructor({ logger, envSettings, incidentModule }) {
|
||||
constructor({ logger, envSettings }) {
|
||||
this.logger = logger;
|
||||
this.envSettings = envSettings;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
const SERVICE_NAME = "incidentModule";
|
||||
|
||||
const dateRangeLookup = {
|
||||
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
|
||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
all: undefined,
|
||||
};
|
||||
|
||||
class IncidentModule {
|
||||
constructor({ logger, Incident, Monitor, User }) {
|
||||
this.logger = logger;
|
||||
this.Incident = Incident;
|
||||
this.Monitor = Monitor;
|
||||
this.User = User;
|
||||
}
|
||||
|
||||
createIncident = async (incidentData) => {
|
||||
try {
|
||||
const incident = await this.Incident.create(incidentData);
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createIncidents = async (incidents) => {
|
||||
try {
|
||||
await this.Incident.insertMany(incidents, { ordered: false });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createIncidents";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getActiveIncidentByMonitor = async (monitorId) => {
|
||||
try {
|
||||
return await this.Incident.findOne({ monitorId: new ObjectId(monitorId), status: true });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getActiveIncidentByMonitor";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getActiveIncidentsByMonitors = async (monitorIds) => {
|
||||
try {
|
||||
if (!monitorIds || monitorIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const objectIds = monitorIds.map((id) => new ObjectId(id));
|
||||
const incidents = await this.Incident.find({
|
||||
monitorId: { $in: objectIds },
|
||||
status: true,
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
incidents.forEach((incident) => {
|
||||
const monitorIdStr = incident.monitorId.toString();
|
||||
map.set(monitorIdStr, incident);
|
||||
});
|
||||
|
||||
return map;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getActiveIncidentsByMonitors";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getLastManuallyResolvedIncident = async (monitorId) => {
|
||||
try {
|
||||
return await this.Incident.findOne({
|
||||
monitorId: new ObjectId(monitorId),
|
||||
status: false,
|
||||
resolutionType: "manual",
|
||||
})
|
||||
.sort({ endTime: -1 })
|
||||
.limit(1);
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getLastManuallyResolvedIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentById = async (incidentId) => {
|
||||
try {
|
||||
return await this.Incident.findById(incidentId).populate("monitorId", "name type url").populate("resolvedBy", "name email");
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
resolveIncident = async (incidentId, { resolutionType = "automatic", resolvedBy = null, comment = null, endTime = new Date() } = {}) => {
|
||||
try {
|
||||
const update = {
|
||||
status: false,
|
||||
endTime,
|
||||
resolutionType,
|
||||
resolvedBy,
|
||||
...(comment !== null && { comment }),
|
||||
};
|
||||
const incident = await this.Incident.findOneAndUpdate({ _id: new ObjectId(incidentId) }, { $set: update }, { new: true });
|
||||
|
||||
if (!incident) {
|
||||
throw new Error("Incident not found");
|
||||
}
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "resolveIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
addCheckToIncident = async (incidentId, checkId) => {
|
||||
try {
|
||||
const incident = await this.Incident.findOneAndUpdate(
|
||||
{ _id: new ObjectId(incidentId) },
|
||||
{ $addToSet: { checks: new ObjectId(checkId) } },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!incident) {
|
||||
throw new Error("Incident not found");
|
||||
}
|
||||
|
||||
return incident;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "addCheckToIncident";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
addChecksToIncidentsBatch = async (operations) => {
|
||||
try {
|
||||
if (!operations || operations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bulkOps = operations.map((op) => ({
|
||||
updateOne: {
|
||||
filter: { _id: new ObjectId(op.incidentId) },
|
||||
update: { $addToSet: { checks: new ObjectId(op.checkId) } },
|
||||
},
|
||||
}));
|
||||
|
||||
await this.Incident.bulkWrite(bulkOps, { ordered: false });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "addChecksToIncidentsBatch";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get incidents by team with filtering
|
||||
*
|
||||
* STATUS PARAMETER CONTRACT (IMPORTANT - Frontend dependency):
|
||||
* This method has an implicit contract with frontend's useFetchIncidents hook:
|
||||
*
|
||||
* - status === undefined: Frontend uses this to represent "all incidents" (filter === "all")
|
||||
* Backend applies $or logic: { status: true } OR { status: false, endTime: >= dateRange }
|
||||
* This shows all active incidents + resolved incidents within the date range
|
||||
*
|
||||
* - status === true: Frontend uses for "active incidents" (filter === "active")
|
||||
* Backend shows all active incidents (no date filter applied)
|
||||
* Active incidents are currently happening, so date range doesn't apply
|
||||
*
|
||||
* - status === false: Frontend uses for "resolved incidents" (filter === "resolved")
|
||||
* Backend filters by endTime >= dateRange
|
||||
* Only shows incidents that were resolved within the specified date range
|
||||
*
|
||||
* WARNING: Changing this logic will break frontend filtering behavior.
|
||||
* The frontend depends on undefined status triggering the $or logic for "all incidents".
|
||||
* This contract must be maintained when modifying either frontend or backend code.
|
||||
*
|
||||
* @param {Object} params - Query parameters
|
||||
* @param {string} params.teamId - Team ID
|
||||
* @param {string} [params.sortOrder] - Sort order (asc/desc)
|
||||
* @param {string} [params.dateRange] - Date range filter
|
||||
* @param {number} [params.page] - Page number
|
||||
* @param {number} [params.rowsPerPage] - Rows per page
|
||||
* @param {boolean|undefined} [params.status] - Status filter. undefined = all, true = active, false = resolved
|
||||
* @param {string} [params.monitorId] - Monitor ID filter
|
||||
* @param {string} [params.resolutionType] - Resolution type filter
|
||||
*/
|
||||
getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType }) => {
|
||||
try {
|
||||
page = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||
rowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||
|
||||
let statusBoolean = undefined;
|
||||
if (status !== undefined && status !== null) {
|
||||
if (typeof status === "string") {
|
||||
statusBoolean = status === "true";
|
||||
} else if (typeof status === "boolean") {
|
||||
statusBoolean = status;
|
||||
}
|
||||
}
|
||||
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
...(statusBoolean !== undefined && { status: statusBoolean }),
|
||||
...(monitorId && { monitorId: new ObjectId(monitorId) }),
|
||||
...(resolutionType && { resolutionType }),
|
||||
};
|
||||
|
||||
// Date range filter logic (see contract documentation above):
|
||||
// - Active incidents (statusBoolean === true): always show (no date filter) - they're currently happening
|
||||
// - Resolved incidents (statusBoolean === false): filter by endTime (when they were resolved)
|
||||
// - All incidents (statusBoolean === undefined): show all active + resolved in the range using $or
|
||||
if (dateRangeLookup[dateRange]) {
|
||||
const dateThreshold = dateRangeLookup[dateRange];
|
||||
if (statusBoolean === true) {
|
||||
// Active incidents: show all active incidents regardless of when they started
|
||||
// No date filter applied
|
||||
} else if (statusBoolean === false) {
|
||||
// Resolved incidents: only show if resolved in the range
|
||||
matchStage.endTime = { $gte: dateThreshold };
|
||||
} else {
|
||||
// All incidents: show all active + resolved incidents in the range
|
||||
// This is the critical contract: undefined status triggers $or logic
|
||||
matchStage.$or = [
|
||||
{ status: true }, // All active incidents
|
||||
{ status: false, endTime: { $gte: dateThreshold } }, // Resolved in range
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
const skip = page * rowsPerPage;
|
||||
|
||||
const incidents = await this.Incident.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { startTime: sortOrder } },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitorId",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$facet: {
|
||||
summary: [{ $count: "incidentsCount" }],
|
||||
incidents: [{ $skip: skip }, { $limit: rowsPerPage }],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
incidentsCount: {
|
||||
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
|
||||
},
|
||||
incidents: {
|
||||
$map: {
|
||||
input: { $ifNull: ["$incidents", []] },
|
||||
as: "incident",
|
||||
in: {
|
||||
$mergeObjects: [
|
||||
"$$incident",
|
||||
{
|
||||
monitorName: {
|
||||
$let: {
|
||||
vars: {
|
||||
monitor: { $arrayElemAt: ["$$incident.monitor", 0] },
|
||||
},
|
||||
in: {
|
||||
$ifNull: ["$$monitor.name", null],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return incidents[0];
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentsByTeam";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async ({ teamId, limit = 10 }) => {
|
||||
try {
|
||||
const matchStage = {
|
||||
teamId: new ObjectId(teamId),
|
||||
};
|
||||
|
||||
// Get basic counts and resolution types
|
||||
const countsPipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 },
|
||||
manualResolutions: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
automaticResolutions: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const counts = await this.Incident.aggregate(countsPipeline);
|
||||
|
||||
// Calculate totals
|
||||
let total = 0;
|
||||
let active = 0;
|
||||
let resolved = 0;
|
||||
let manual = 0;
|
||||
let automatic = 0;
|
||||
|
||||
counts.forEach((item) => {
|
||||
total += item.count;
|
||||
if (item._id === true) {
|
||||
active = item.count;
|
||||
}
|
||||
if (item._id === false) {
|
||||
resolved = item.count;
|
||||
}
|
||||
manual += item.manualResolutions;
|
||||
automatic += item.automaticResolutions;
|
||||
});
|
||||
|
||||
// Calculate average resolution time (in milliseconds)
|
||||
const resolutionTimePipeline = [
|
||||
{ $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } },
|
||||
{
|
||||
$project: {
|
||||
resolutionTime: {
|
||||
$subtract: ["$endTime", "$startTime"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResolutionTime: { $avg: "$resolutionTime" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const resolutionTimeResult = await this.Incident.aggregate(resolutionTimePipeline);
|
||||
const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0;
|
||||
const avgResolutionTimeHours = avgResolutionTimeMs / (1000 * 60 * 60); // Convert to hours
|
||||
|
||||
// Get monitor with most incidents
|
||||
const monitorPipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: "$monitorId",
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 1 },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "_id",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
monitorId: "$_id",
|
||||
count: 1,
|
||||
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const monitorResult = await this.Incident.aggregate(monitorPipeline);
|
||||
const topMonitor = monitorResult[0] || null;
|
||||
|
||||
// Get latest incidents
|
||||
const latestIncidentsPipeline = [
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: Math.max(1, parseInt(limit) || 10) },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitorId",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
monitorId: 1,
|
||||
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
|
||||
status: 1,
|
||||
startTime: 1,
|
||||
endTime: 1,
|
||||
resolutionType: 1,
|
||||
message: 1,
|
||||
statusCode: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const latestIncidents = await this.Incident.aggregate(latestIncidentsPipeline);
|
||||
|
||||
return {
|
||||
totalActive: active,
|
||||
avgResolutionTimeHours: Math.round(avgResolutionTimeHours * 100) / 100, // Round to 2 decimal places
|
||||
topMonitor: topMonitor
|
||||
? {
|
||||
monitorId: topMonitor.monitorId,
|
||||
monitorName: topMonitor.monitorName,
|
||||
incidentCount: topMonitor.count,
|
||||
}
|
||||
: null,
|
||||
total: total,
|
||||
totalManualResolutions: manual,
|
||||
totalAutomaticResolutions: automatic,
|
||||
latestIncidents: latestIncidents,
|
||||
};
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getIncidentSummary";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteIncidentsByMonitor = async (monitorId) => {
|
||||
try {
|
||||
const result = await this.Incident.deleteMany({ monitorId: new ObjectId(monitorId) });
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteIncidentsByMonitor";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteIncidentsByTeamId = async (teamId) => {
|
||||
try {
|
||||
const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 });
|
||||
const monitorIds = teamMonitors.map((monitor) => monitor._id);
|
||||
const deleteResult = await this.Incident.deleteMany({ monitorId: { $in: monitorIds } });
|
||||
return deleteResult.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteIncidentsByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default IncidentModule;
|
||||
@@ -4,6 +4,7 @@ export interface IIncidentsRepository {
|
||||
// create
|
||||
create(incident: Partial<Incident>): Promise<Incident>;
|
||||
// fetch
|
||||
findById(incidentId: string, teamId: string): Promise<Incident>;
|
||||
findActiveByIncidentId(incidentId: string, teamId: string): Promise<Incident | null>;
|
||||
findActiveByMonitorId(monitorId: string, teamId: string): Promise<Incident | null>;
|
||||
findByTeamId(
|
||||
|
||||
@@ -79,6 +79,17 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
||||
return this.toEntity(newIncident);
|
||||
}
|
||||
|
||||
findById = async (incidentId: string, teamId: string): Promise<Incident> => {
|
||||
const incident = await IncidentModel.findOne({
|
||||
_id: new mongoose.Types.ObjectId(incidentId),
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
});
|
||||
if (!incident) {
|
||||
throw new AppError({ message: `Incident with id ${incidentId} not found`, status: 404 });
|
||||
}
|
||||
return this.toEntity(incident);
|
||||
};
|
||||
|
||||
findActiveByIncidentId = async (incidentId: string, teamId: string): Promise<Incident | null> => {
|
||||
const incident = await IncidentModel.findOne({
|
||||
_id: new mongoose.Types.ObjectId(incidentId),
|
||||
|
||||
@@ -2,7 +2,7 @@ const SERVICE_NAME = "incidentService";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import type { IIncidentsRepository } from "@/repositories/index.js";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import type { Incident } from "@/types/index.js";
|
||||
|
||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
@@ -17,27 +17,30 @@ const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
class IncidentService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private errorService: any;
|
||||
private: any;
|
||||
private incidentsRepository: IIncidentsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private usersRepository: IUsersRepository;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
logger,
|
||||
errorService,
|
||||
incidentsRepository,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
errorService: any;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
usersRepository: IUsersRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.usersRepository = usersRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -193,27 +196,15 @@ class IncidentService {
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentById = async ({ incidentId, teamId }: { incidentId: string; teamId: string }) => {
|
||||
getIncidentById = async (incidentId: string, teamId: string) => {
|
||||
try {
|
||||
if (!incidentId) {
|
||||
throw this.errorService.createBadRequestError("No incident ID in request");
|
||||
const incident = await this.incidentsRepository.findById(incidentId, teamId);
|
||||
const monitor = await this.monitorsRepository.findById(incident.monitorId, teamId);
|
||||
let user = null;
|
||||
if (incident.resolvedBy) {
|
||||
user = await this.usersRepository.findById(incident.resolvedBy);
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const incident = await this.db.incidentModule.getIncidentById(incidentId);
|
||||
|
||||
if (!incident) {
|
||||
throw this.errorService.createNotFoundError("Incident not found");
|
||||
}
|
||||
|
||||
if (!incident.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
return incident;
|
||||
return { incident, monitor, user };
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
|
||||
Reference in New Issue
Block a user