@oneuptime/common 11.7.1 → 11.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DashboardDomain.ts +1 -0
- package/Models/DatabaseModels/Incident.ts +2 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Monitor.ts +1 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/NetworkSite.ts +1 -0
- package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
- package/Models/DatabaseModels/Probe.ts +13 -13
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
- package/Models/DatabaseModels/StatusPage.ts +1 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
- package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
- package/Server/API/BaseAPI.ts +17 -1
- package/Server/API/CommonAPI.ts +32 -0
- package/Server/API/MicrosoftTeamsAPI.ts +99 -2
- package/Server/API/ProbeAPI.ts +2 -0
- package/Server/API/SlackAPI.ts +62 -0
- package/Server/API/StatusPageAPI.ts +157 -192
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DatabaseService.ts +11 -3
- package/Server/Services/MonitorGroupService.ts +145 -0
- package/Server/Services/MonitorService.ts +231 -30
- package/Server/Services/ProbeService.ts +319 -16
- package/Server/Services/StatusPageService.ts +72 -42
- package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
- package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
- package/Server/Utils/Monitor/MonitorResource.ts +88 -35
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
- package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
- package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
- package/Tests/Server/API/ProbeAPI.test.ts +6 -0
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
- package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
- package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
- package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
- package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
- package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
- package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
- package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
- package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
- package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
- package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
- package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
- package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
- package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
- package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
- package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
- package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
- package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
- package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
- package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
- package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
- package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
- package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
- package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
- package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
- package/Tests/UI/Utils/Project.test.ts +809 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
- package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
- package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
- package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
- package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
- package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
- package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
- package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
- package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
- package/UI/Components/Forms/ModelForm.tsx +41 -18
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
- package/UI/Components/ModelTable/Column.ts +12 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/Table/Table.tsx +52 -13
- package/UI/Components/Table/Types/Column.ts +18 -0
- package/UI/Utils/Project.ts +214 -18
- package/UI/Utils/TableColumnsToCsv.ts +86 -6
- package/UI/esbuild-config.js +34 -0
- package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +2 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Probe.js +13 -13
- package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +21 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProbeAPI.js +2 -0
- package/build/dist/Server/API/ProbeAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +44 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +98 -152
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MonitorGroupService.js +118 -0
- package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +198 -24
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +238 -9
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +46 -39
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
- package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +31 -11
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +161 -15
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
- package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MonitorStatusTimelineUtil.updateMonitorStatusTimeline runs on EVERY probe /
|
|
3
|
+
* ingest result whose criteria matched — for a healthy fleet that is almost
|
|
4
|
+
* every check, and each call used to issue a sorted findOneBy against
|
|
5
|
+
* MonitorStatusTimeline (ORDER BY startsAt DESC on one of the largest tables)
|
|
6
|
+
* just to conclude "status unchanged, nothing to do".
|
|
7
|
+
*
|
|
8
|
+
* The optimization under test adds a steady-state fast-path BEFORE that
|
|
9
|
+
* SELECT: when the criteria wants to change the status
|
|
10
|
+
* (changeMonitorStatus + monitorStatusId set) and the target status already
|
|
11
|
+
* equals Monitor.currentMonitorStatusId, return null with NO query at all.
|
|
12
|
+
* Monitor.currentMonitorStatusId is kept in lockstep with the latest timeline
|
|
13
|
+
* row by MonitorStatusTimelineService, and onBeforeCreate still dedupes as
|
|
14
|
+
* the concurrency backstop — so skipping the SELECT is behavior-preserving.
|
|
15
|
+
*
|
|
16
|
+
* The same change also fixed a long-standing comparison bug: the second
|
|
17
|
+
* shouldUpdateStatus condition used to compare the criteria's monitorStatusId
|
|
18
|
+
* to the timeline row's PK id (which never matches a status id), and now
|
|
19
|
+
* compares it to lastMonitorStatusTimeline.monitorStatusId.
|
|
20
|
+
*
|
|
21
|
+
* This suite pins:
|
|
22
|
+
* - the fast-path returns null with zero service calls,
|
|
23
|
+
* - the fast-path requires changeMonitorStatus (legacy read path preserved),
|
|
24
|
+
* - the same-as-last-status inner check still prevents duplicate rows when
|
|
25
|
+
* currentMonitorStatusId is out of sync,
|
|
26
|
+
* - a real status change still creates a correctly-populated timeline row,
|
|
27
|
+
* - first-row creation (no timeline history) still works,
|
|
28
|
+
* - the two exact-message swallow paths (dedupe race + lock refusal)
|
|
29
|
+
* return null while every other create error propagates,
|
|
30
|
+
* - the fixed monitorStatusId-vs-monitorStatusId comparison.
|
|
31
|
+
*
|
|
32
|
+
* MonitorStatusTimelineService is mocked at the module boundary — the real
|
|
33
|
+
* module pulls in the whole database stack. No Postgres, no Redis.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const mockFindOneBy: jest.Mock = jest.fn();
|
|
37
|
+
const mockCreate: jest.Mock = jest.fn();
|
|
38
|
+
|
|
39
|
+
jest.mock("../../../Server/Services/MonitorStatusTimelineService", () => {
|
|
40
|
+
return {
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: {
|
|
43
|
+
findOneBy: (...args: Array<unknown>) => {
|
|
44
|
+
return mockFindOneBy(...args);
|
|
45
|
+
},
|
|
46
|
+
create: (...args: Array<unknown>) => {
|
|
47
|
+
return mockCreate(...args);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
/*
|
|
51
|
+
* Same literal values as the real service module. The test imports these
|
|
52
|
+
* from the mocked module below, so the swallow-path assertions use the
|
|
53
|
+
* exact strings the util itself sees at runtime.
|
|
54
|
+
*/
|
|
55
|
+
MONITOR_STATUS_SAME_AS_PREVIOUS_ERROR_MESSAGE:
|
|
56
|
+
"Monitor Status cannot be same as previous status.",
|
|
57
|
+
MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE:
|
|
58
|
+
"Could not acquire the monitor status timeline lock for this monitor.",
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
63
|
+
return {
|
|
64
|
+
__esModule: true,
|
|
65
|
+
default: {
|
|
66
|
+
debug: jest.fn(),
|
|
67
|
+
info: jest.fn(),
|
|
68
|
+
warn: jest.fn(),
|
|
69
|
+
error: jest.fn(),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
import MonitorStatusTimelineUtil from "../../../Server/Utils/Monitor/MonitorStatusTimeline";
|
|
75
|
+
import {
|
|
76
|
+
MONITOR_STATUS_SAME_AS_PREVIOUS_ERROR_MESSAGE,
|
|
77
|
+
MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE,
|
|
78
|
+
} from "../../../Server/Services/MonitorStatusTimelineService";
|
|
79
|
+
import DataToProcess from "../../../Server/Utils/Monitor/DataToProcess";
|
|
80
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
81
|
+
import MonitorStatusTimeline from "../../../Models/DatabaseModels/MonitorStatusTimeline";
|
|
82
|
+
import MonitorCriteriaInstance from "../../../Types/Monitor/MonitorCriteriaInstance";
|
|
83
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
84
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
85
|
+
import ServerException from "../../../Types/Exception/ServerException";
|
|
86
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
87
|
+
import { describe, expect, test, beforeEach } from "@jest/globals";
|
|
88
|
+
|
|
89
|
+
const MONITOR_ID: ObjectID = new ObjectID(
|
|
90
|
+
"11111111-1111-4111-8111-111111111111",
|
|
91
|
+
);
|
|
92
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
93
|
+
"22222222-2222-4222-8222-222222222222",
|
|
94
|
+
);
|
|
95
|
+
const ONLINE_STATUS_ID: ObjectID = new ObjectID(
|
|
96
|
+
"33333333-3333-4333-8333-333333333333",
|
|
97
|
+
);
|
|
98
|
+
const OFFLINE_STATUS_ID: ObjectID = new ObjectID(
|
|
99
|
+
"44444444-4444-4444-8444-444444444444",
|
|
100
|
+
);
|
|
101
|
+
const TIMELINE_ROW_ID: ObjectID = new ObjectID(
|
|
102
|
+
"55555555-5555-4555-8555-555555555555",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
type MakeMonitorFunction = (
|
|
106
|
+
currentMonitorStatusId: ObjectID | undefined,
|
|
107
|
+
) => Monitor;
|
|
108
|
+
|
|
109
|
+
const makeMonitor: MakeMonitorFunction = (
|
|
110
|
+
currentMonitorStatusId: ObjectID | undefined,
|
|
111
|
+
): Monitor => {
|
|
112
|
+
const monitor: Monitor = new Monitor();
|
|
113
|
+
monitor.id = MONITOR_ID;
|
|
114
|
+
monitor.projectId = PROJECT_ID;
|
|
115
|
+
if (currentMonitorStatusId) {
|
|
116
|
+
monitor.currentMonitorStatusId = currentMonitorStatusId;
|
|
117
|
+
}
|
|
118
|
+
return monitor;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type MakeCriteriaFunction = (input: {
|
|
122
|
+
changeMonitorStatus: boolean;
|
|
123
|
+
monitorStatusId: ObjectID | undefined;
|
|
124
|
+
}) => MonitorCriteriaInstance;
|
|
125
|
+
|
|
126
|
+
const makeCriteria: MakeCriteriaFunction = (input: {
|
|
127
|
+
changeMonitorStatus: boolean;
|
|
128
|
+
monitorStatusId: ObjectID | undefined;
|
|
129
|
+
}): MonitorCriteriaInstance => {
|
|
130
|
+
const criteria: MonitorCriteriaInstance = new MonitorCriteriaInstance();
|
|
131
|
+
criteria.data!.changeMonitorStatus = input.changeMonitorStatus;
|
|
132
|
+
criteria.data!.monitorStatusId = input.monitorStatusId;
|
|
133
|
+
return criteria;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type MakeTimelineRowFunction = (input: {
|
|
137
|
+
id: ObjectID;
|
|
138
|
+
monitorStatusId: ObjectID;
|
|
139
|
+
}) => MonitorStatusTimeline;
|
|
140
|
+
|
|
141
|
+
const makeTimelineRow: MakeTimelineRowFunction = (input: {
|
|
142
|
+
id: ObjectID;
|
|
143
|
+
monitorStatusId: ObjectID;
|
|
144
|
+
}): MonitorStatusTimeline => {
|
|
145
|
+
const row: MonitorStatusTimeline = new MonitorStatusTimeline();
|
|
146
|
+
row.id = input.id;
|
|
147
|
+
row.monitorStatusId = input.monitorStatusId;
|
|
148
|
+
return row;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const dataToProcess: DataToProcess = {
|
|
152
|
+
monitorId: MONITOR_ID,
|
|
153
|
+
projectId: PROJECT_ID,
|
|
154
|
+
isOnline: true,
|
|
155
|
+
} as unknown as DataToProcess;
|
|
156
|
+
|
|
157
|
+
const ROOT_CAUSE: string = "Monitor is online because criteria matched.";
|
|
158
|
+
|
|
159
|
+
type RunFunction = (input: {
|
|
160
|
+
criteriaInstance: MonitorCriteriaInstance;
|
|
161
|
+
monitor: Monitor;
|
|
162
|
+
}) => Promise<MonitorStatusTimeline | null>;
|
|
163
|
+
|
|
164
|
+
const run: RunFunction = (input: {
|
|
165
|
+
criteriaInstance: MonitorCriteriaInstance;
|
|
166
|
+
monitor: Monitor;
|
|
167
|
+
}): Promise<MonitorStatusTimeline | null> => {
|
|
168
|
+
return MonitorStatusTimelineUtil.updateMonitorStatusTimeline({
|
|
169
|
+
criteriaInstance: input.criteriaInstance,
|
|
170
|
+
monitor: input.monitor,
|
|
171
|
+
dataToProcess: dataToProcess,
|
|
172
|
+
rootCause: ROOT_CAUSE,
|
|
173
|
+
props: {},
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
describe("MonitorStatusTimelineUtil.updateMonitorStatusTimeline steady-state fast-path", () => {
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
mockFindOneBy.mockReset();
|
|
180
|
+
mockCreate.mockReset();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("returns null with ZERO queries when target status already equals currentMonitorStatusId", async () => {
|
|
184
|
+
/*
|
|
185
|
+
* The hot path this whole optimization exists for: healthy check, online
|
|
186
|
+
* criteria matched, monitor already online. Before the change this issued
|
|
187
|
+
* a sorted SELECT per check result; now it must touch the service not at
|
|
188
|
+
* all.
|
|
189
|
+
*/
|
|
190
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
191
|
+
criteriaInstance: makeCriteria({
|
|
192
|
+
changeMonitorStatus: true,
|
|
193
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
194
|
+
}),
|
|
195
|
+
monitor: makeMonitor(ONLINE_STATUS_ID),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result).toBeNull();
|
|
199
|
+
expect(mockFindOneBy).not.toHaveBeenCalled();
|
|
200
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("does NOT fast-path when changeMonitorStatus is falsy — legacy read path preserved", async () => {
|
|
204
|
+
/*
|
|
205
|
+
* A criteria that does not change the status must keep its pre-existing
|
|
206
|
+
* behavior (the timeline SELECT still runs, nothing is created). The
|
|
207
|
+
* fast-path condition deliberately requires changeMonitorStatus so it
|
|
208
|
+
* only skips work the function would provably not do.
|
|
209
|
+
*/
|
|
210
|
+
mockFindOneBy.mockResolvedValue(
|
|
211
|
+
makeTimelineRow({
|
|
212
|
+
id: TIMELINE_ROW_ID,
|
|
213
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
218
|
+
criteriaInstance: makeCriteria({
|
|
219
|
+
changeMonitorStatus: false,
|
|
220
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
221
|
+
}),
|
|
222
|
+
monitor: makeMonitor(ONLINE_STATUS_ID),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result).toBeNull();
|
|
226
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
227
|
+
|
|
228
|
+
// The legacy path issues exactly the sorted latest-row lookup.
|
|
229
|
+
expect(mockFindOneBy).toHaveBeenCalledTimes(1);
|
|
230
|
+
expect(mockFindOneBy).toHaveBeenCalledWith({
|
|
231
|
+
query: {
|
|
232
|
+
monitorId: MONITOR_ID,
|
|
233
|
+
projectId: PROJECT_ID,
|
|
234
|
+
},
|
|
235
|
+
select: {
|
|
236
|
+
_id: true,
|
|
237
|
+
monitorStatusId: true,
|
|
238
|
+
},
|
|
239
|
+
sort: {
|
|
240
|
+
startsAt: SortOrder.Descending,
|
|
241
|
+
},
|
|
242
|
+
props: {
|
|
243
|
+
isRoot: true,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("returns null without create when the LAST TIMELINE ROW already has the target status (currentMonitorStatusId out of sync)", async () => {
|
|
249
|
+
/*
|
|
250
|
+
* The timeline is the source of truth. If currentMonitorStatusId lags
|
|
251
|
+
* behind (so the fast-path does not trigger), the same-as-last-status
|
|
252
|
+
* check on the fetched row must still prevent a duplicate row.
|
|
253
|
+
*/
|
|
254
|
+
mockFindOneBy.mockResolvedValue(
|
|
255
|
+
makeTimelineRow({
|
|
256
|
+
id: TIMELINE_ROW_ID,
|
|
257
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
262
|
+
criteriaInstance: makeCriteria({
|
|
263
|
+
changeMonitorStatus: true,
|
|
264
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
265
|
+
}),
|
|
266
|
+
// out of sync: monitor still points at the old status.
|
|
267
|
+
monitor: makeMonitor(OFFLINE_STATUS_ID),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result).toBeNull();
|
|
271
|
+
expect(mockFindOneBy).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("creates a fully-populated timeline row on a real status change", async () => {
|
|
276
|
+
mockFindOneBy.mockResolvedValue(
|
|
277
|
+
makeTimelineRow({
|
|
278
|
+
id: TIMELINE_ROW_ID,
|
|
279
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const createdRow: MonitorStatusTimeline = makeTimelineRow({
|
|
284
|
+
id: ObjectID.generate(),
|
|
285
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
286
|
+
});
|
|
287
|
+
mockCreate.mockResolvedValue(createdRow);
|
|
288
|
+
|
|
289
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
290
|
+
criteriaInstance: makeCriteria({
|
|
291
|
+
changeMonitorStatus: true,
|
|
292
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
293
|
+
}),
|
|
294
|
+
monitor: makeMonitor(OFFLINE_STATUS_ID),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(result).toBe(createdRow);
|
|
298
|
+
expect(mockFindOneBy).toHaveBeenCalledTimes(1);
|
|
299
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
300
|
+
|
|
301
|
+
const createArg: {
|
|
302
|
+
data: MonitorStatusTimeline;
|
|
303
|
+
props: { isRoot: boolean };
|
|
304
|
+
} = mockCreate.mock.calls[0]![0] as {
|
|
305
|
+
data: MonitorStatusTimeline;
|
|
306
|
+
props: { isRoot: boolean };
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
expect(createArg.props).toEqual({ isRoot: true });
|
|
310
|
+
expect(createArg.data.monitorId?.toString()).toBe(MONITOR_ID.toString());
|
|
311
|
+
expect(createArg.data.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
312
|
+
expect(createArg.data.monitorStatusId?.toString()).toBe(
|
|
313
|
+
ONLINE_STATUS_ID.toString(),
|
|
314
|
+
);
|
|
315
|
+
expect(createArg.data.rootCause).toBe(ROOT_CAUSE);
|
|
316
|
+
expect(createArg.data.statusChangeLog).toEqual(
|
|
317
|
+
JSON.parse(JSON.stringify(dataToProcess)),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("creates the first timeline row when the monitor has no history at all", async () => {
|
|
322
|
+
/*
|
|
323
|
+
* Brand-new monitor: no timeline rows and no currentMonitorStatusId, so
|
|
324
|
+
* the fast-path must not trigger and the first row must still be written.
|
|
325
|
+
*/
|
|
326
|
+
mockFindOneBy.mockResolvedValue(null);
|
|
327
|
+
|
|
328
|
+
const createdRow: MonitorStatusTimeline = makeTimelineRow({
|
|
329
|
+
id: ObjectID.generate(),
|
|
330
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
331
|
+
});
|
|
332
|
+
mockCreate.mockResolvedValue(createdRow);
|
|
333
|
+
|
|
334
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
335
|
+
criteriaInstance: makeCriteria({
|
|
336
|
+
changeMonitorStatus: true,
|
|
337
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
338
|
+
}),
|
|
339
|
+
monitor: makeMonitor(undefined),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result).toBe(createdRow);
|
|
343
|
+
expect(mockFindOneBy).toHaveBeenCalledTimes(1);
|
|
344
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("create error handling", () => {
|
|
348
|
+
beforeEach(() => {
|
|
349
|
+
// A genuine status change so the create path is reached every time.
|
|
350
|
+
mockFindOneBy.mockResolvedValue(
|
|
351
|
+
makeTimelineRow({
|
|
352
|
+
id: TIMELINE_ROW_ID,
|
|
353
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const runStatusChange: () => Promise<MonitorStatusTimeline | null> =
|
|
359
|
+
(): Promise<MonitorStatusTimeline | null> => {
|
|
360
|
+
return run({
|
|
361
|
+
criteriaInstance: makeCriteria({
|
|
362
|
+
changeMonitorStatus: true,
|
|
363
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
364
|
+
}),
|
|
365
|
+
monitor: makeMonitor(OFFLINE_STATUS_ID),
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
test("swallows the exact same-as-previous dedupe race and returns null", async () => {
|
|
370
|
+
/*
|
|
371
|
+
* Loser of a concurrent-probe race: onBeforeCreate's dedupe throws
|
|
372
|
+
* this exact BadDataException. Idempotent no-op — must not fail the
|
|
373
|
+
* ingest run.
|
|
374
|
+
*/
|
|
375
|
+
mockCreate.mockRejectedValue(
|
|
376
|
+
new BadDataException(MONITOR_STATUS_SAME_AS_PREVIOUS_ERROR_MESSAGE),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
await expect(runStatusChange()).resolves.toBeNull();
|
|
380
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("swallows the exact lock-refusal ServerException and returns null", async () => {
|
|
384
|
+
/*
|
|
385
|
+
* Fail-closed mutex: the create was refused rather than performed
|
|
386
|
+
* unlocked. Skipping is recoverable (next probe result retries), so
|
|
387
|
+
* the util logs and returns null.
|
|
388
|
+
*/
|
|
389
|
+
mockCreate.mockRejectedValue(
|
|
390
|
+
new ServerException(MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await expect(runStatusChange()).resolves.toBeNull();
|
|
394
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("propagates a BadDataException with any OTHER message", async () => {
|
|
398
|
+
const otherError: BadDataException = new BadDataException(
|
|
399
|
+
"Some unrelated validation failure.",
|
|
400
|
+
);
|
|
401
|
+
mockCreate.mockRejectedValue(otherError);
|
|
402
|
+
|
|
403
|
+
await expect(runStatusChange()).rejects.toBe(otherError);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("propagates non-matching error types", async () => {
|
|
407
|
+
const dbError: Error = new Error("connection reset by peer");
|
|
408
|
+
mockCreate.mockRejectedValue(dbError);
|
|
409
|
+
|
|
410
|
+
await expect(runStatusChange()).rejects.toBe(dbError);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("comparison-bug pin: last row's PK id differs from its monitorStatusId — still no duplicate create", async () => {
|
|
415
|
+
/*
|
|
416
|
+
* Before this branch, the second shouldUpdateStatus condition compared
|
|
417
|
+
* the criteria's target monitorStatusId to lastMonitorStatusTimeline.id
|
|
418
|
+
* (the row's PK) — two different UUID namespaces that never match — so
|
|
419
|
+
* that condition was ALWAYS true whenever changeMonitorStatus was set,
|
|
420
|
+
* and only the inner same-as-last-status check (line ~121) saved the day
|
|
421
|
+
* by returning null before the INSERT.
|
|
422
|
+
*
|
|
423
|
+
* The fixed code compares against lastMonitorStatusTimeline
|
|
424
|
+
* .monitorStatusId, so with target === last.monitorStatusId (and id
|
|
425
|
+
* deliberately different, and currentMonitorStatusId out of sync so the
|
|
426
|
+
* fast-path stays out of the way) shouldUpdateStatus is reached only via
|
|
427
|
+
* the third condition and the outcome must be: exactly one SELECT, zero
|
|
428
|
+
* creates, null. If someone reverts the comparison to `.id`, the outcome
|
|
429
|
+
* is unchanged only because of the inner backstop — this test plus the
|
|
430
|
+
* out-of-sync test above pin that the backstop and the fixed condition
|
|
431
|
+
* agree on the row's monitorStatusId, never its PK.
|
|
432
|
+
*/
|
|
433
|
+
mockFindOneBy.mockResolvedValue(
|
|
434
|
+
makeTimelineRow({
|
|
435
|
+
// PK deliberately NOT equal to the status id it points at.
|
|
436
|
+
id: TIMELINE_ROW_ID,
|
|
437
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const result: MonitorStatusTimeline | null = await run({
|
|
442
|
+
criteriaInstance: makeCriteria({
|
|
443
|
+
changeMonitorStatus: true,
|
|
444
|
+
monitorStatusId: ONLINE_STATUS_ID,
|
|
445
|
+
}),
|
|
446
|
+
monitor: makeMonitor(OFFLINE_STATUS_ID),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result).toBeNull();
|
|
450
|
+
expect(mockFindOneBy).toHaveBeenCalledTimes(1);
|
|
451
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
});
|