@oneuptime/common 14.0.2 → 14.0.3
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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { JSONObject, JSONValue } from "../../Types/JSON";
|
|
2
|
+
import ObjectID from "../../Types/ObjectID";
|
|
3
|
+
import { MAX_ALERTS_PER_INCIDENT_LINK_ACTION } from "../../Types/Incident/IncidentAlertLink";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Builds the "declare incident" form prefill from the alerts an incident is
|
|
7
|
+
* being declared from. Kept free of React and of the database models so the
|
|
8
|
+
* rules - which alert wins the title, how an alert severity maps onto an
|
|
9
|
+
* incident severity, what gets unioned - can be pinned by plain unit tests.
|
|
10
|
+
* The create-incident page converts its fetched models into these shapes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* A severity (alert or incident) as far as the mapping cares. */
|
|
14
|
+
export interface SeverityForMapping {
|
|
15
|
+
id: string;
|
|
16
|
+
name?: string | undefined;
|
|
17
|
+
order?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* An affected resource in the shape the incident form's resource picker
|
|
22
|
+
* takes: `{ _id, name }`, so it renders real names on first paint.
|
|
23
|
+
*/
|
|
24
|
+
export interface NamedResource {
|
|
25
|
+
_id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AlertForIncidentPrefill {
|
|
30
|
+
id: string;
|
|
31
|
+
title?: string | undefined;
|
|
32
|
+
description?: string | undefined;
|
|
33
|
+
alertNumber?: number | undefined;
|
|
34
|
+
alertNumberWithPrefix?: string | undefined;
|
|
35
|
+
alertSeverityId?: string | undefined;
|
|
36
|
+
monitor?: NamedResource | undefined;
|
|
37
|
+
hosts?: Array<NamedResource> | undefined;
|
|
38
|
+
kubernetesClusters?: Array<NamedResource> | undefined;
|
|
39
|
+
dockerHosts?: Array<NamedResource> | undefined;
|
|
40
|
+
podmanHosts?: Array<NamedResource> | undefined;
|
|
41
|
+
services?: Array<NamedResource> | undefined;
|
|
42
|
+
labelIds?: Array<string> | undefined;
|
|
43
|
+
isPrivate?: boolean | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IncidentPrefillFromAlerts {
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
incidentSeverityId?: string | undefined;
|
|
50
|
+
monitors: Array<NamedResource>;
|
|
51
|
+
hosts: Array<NamedResource>;
|
|
52
|
+
kubernetesClusters: Array<NamedResource>;
|
|
53
|
+
dockerHosts: Array<NamedResource>;
|
|
54
|
+
podmanHosts: Array<NamedResource>;
|
|
55
|
+
services: Array<NamedResource>;
|
|
56
|
+
labelIds: Array<string>;
|
|
57
|
+
/*
|
|
58
|
+
* True when any of the alerts is private. A private incident is visible
|
|
59
|
+
* only to its owners (and project admins), so when it is declared the
|
|
60
|
+
* server adds the alerts' owners as its owners, and the create page says
|
|
61
|
+
* so before the user declares it.
|
|
62
|
+
*/
|
|
63
|
+
isPrivate: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BuildIncidentPrefillInput {
|
|
67
|
+
alerts: Array<AlertForIncidentPrefill>;
|
|
68
|
+
alertSeverities: Array<SeverityForMapping>;
|
|
69
|
+
incidentSeverities: Array<SeverityForMapping>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ParsedAlertIds {
|
|
73
|
+
alertIds: Array<string>;
|
|
74
|
+
/* More ids were asked for than one declaration may link. */
|
|
75
|
+
wasTruncated: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* The resource lists the incident form supports besides monitors (which come
|
|
80
|
+
* from each alert's single `monitor`). Exported so the create page and the
|
|
81
|
+
* tests walk the same list.
|
|
82
|
+
*/
|
|
83
|
+
export const INCIDENT_PREFILL_RESOURCE_KEYS: Array<
|
|
84
|
+
"hosts" | "kubernetesClusters" | "dockerHosts" | "podmanHosts" | "services"
|
|
85
|
+
> = ["hosts", "kubernetesClusters", "dockerHosts", "podmanHosts", "services"];
|
|
86
|
+
|
|
87
|
+
type IncidentPrefillListKey =
|
|
88
|
+
| "monitors"
|
|
89
|
+
| "hosts"
|
|
90
|
+
| "kubernetesClusters"
|
|
91
|
+
| "dockerHosts"
|
|
92
|
+
| "podmanHosts"
|
|
93
|
+
| "services";
|
|
94
|
+
|
|
95
|
+
const INCIDENT_PREFILL_LIST_KEYS: Array<IncidentPrefillListKey> = [
|
|
96
|
+
"monitors",
|
|
97
|
+
...INCIDENT_PREFILL_RESOURCE_KEYS,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
export default class IncidentFromAlerts {
|
|
101
|
+
/*
|
|
102
|
+
* Reads the create page's `alertIds` query parameter: comma separated,
|
|
103
|
+
* whitespace tolerant, invalid ids dropped, duplicates removed (first one
|
|
104
|
+
* wins), capped at MAX_ALERTS_PER_INCIDENT_LINK_ACTION.
|
|
105
|
+
*/
|
|
106
|
+
public static parseAlertIdsQueryParam(
|
|
107
|
+
value: string | null | undefined,
|
|
108
|
+
): ParsedAlertIds {
|
|
109
|
+
if (!value) {
|
|
110
|
+
return { alertIds: [], wasTruncated: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const alertIds: Array<string> = [];
|
|
114
|
+
|
|
115
|
+
for (const rawId of value.split(",")) {
|
|
116
|
+
const id: string = rawId.trim();
|
|
117
|
+
|
|
118
|
+
if (!id || !ObjectID.isValidUUID(id) || alertIds.includes(id)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
alertIds.push(id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
alertIds: alertIds.slice(0, MAX_ALERTS_PER_INCIDENT_LINK_ACTION),
|
|
127
|
+
wasTruncated: alertIds.length > MAX_ALERTS_PER_INCIDENT_LINK_ACTION,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Severities ordered most severe first: ascending `order`, the way the
|
|
133
|
+
* project's severity settings list them. A severity without an order sorts
|
|
134
|
+
* after every ordered one; ties keep their input order.
|
|
135
|
+
*/
|
|
136
|
+
public static sortSeveritiesByOrder<T extends SeverityForMapping>(
|
|
137
|
+
severities: Array<T>,
|
|
138
|
+
): Array<T> {
|
|
139
|
+
return severities
|
|
140
|
+
.map((severity: T, index: number) => {
|
|
141
|
+
return { severity, index };
|
|
142
|
+
})
|
|
143
|
+
.sort(
|
|
144
|
+
(
|
|
145
|
+
a: { severity: T; index: number },
|
|
146
|
+
b: { severity: T; index: number },
|
|
147
|
+
): number => {
|
|
148
|
+
const aHasOrder: boolean = typeof a.severity.order === "number";
|
|
149
|
+
const bHasOrder: boolean = typeof b.severity.order === "number";
|
|
150
|
+
|
|
151
|
+
if (aHasOrder && bHasOrder) {
|
|
152
|
+
const difference: number =
|
|
153
|
+
(a.severity.order as number) - (b.severity.order as number);
|
|
154
|
+
|
|
155
|
+
if (difference !== 0) {
|
|
156
|
+
return difference;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return a.index - b.index;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (aHasOrder !== bHasOrder) {
|
|
163
|
+
return aHasOrder ? -1 : 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return a.index - b.index;
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
.map((entry: { severity: T; index: number }): T => {
|
|
170
|
+
return entry.severity;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* 0 for the most severe severity, 1 for the next and so on; null when the
|
|
176
|
+
* id is not one of the given severities.
|
|
177
|
+
*/
|
|
178
|
+
public static getSeverityRank(
|
|
179
|
+
severityId: string | undefined | null,
|
|
180
|
+
severities: Array<SeverityForMapping>,
|
|
181
|
+
): number | null {
|
|
182
|
+
if (!severityId) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rank: number = this.sortSeveritiesByOrder(severities).findIndex(
|
|
187
|
+
(severity: SeverityForMapping) => {
|
|
188
|
+
return severity.id === severityId;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return rank < 0 ? null : rank;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
* The alert whose severity ranks highest. Alerts without a known severity
|
|
197
|
+
* never win over one with a severity; ties go to the alert listed first.
|
|
198
|
+
* With no known severity anywhere, the first alert.
|
|
199
|
+
*/
|
|
200
|
+
public static getMostSevereAlert(
|
|
201
|
+
alerts: Array<AlertForIncidentPrefill>,
|
|
202
|
+
alertSeverities: Array<SeverityForMapping>,
|
|
203
|
+
): AlertForIncidentPrefill | null {
|
|
204
|
+
let mostSevere: AlertForIncidentPrefill | null = null;
|
|
205
|
+
let mostSevereRank: number | null = null;
|
|
206
|
+
|
|
207
|
+
for (const alert of alerts) {
|
|
208
|
+
const rank: number | null = this.getSeverityRank(
|
|
209
|
+
alert.alertSeverityId,
|
|
210
|
+
alertSeverities,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (!mostSevere) {
|
|
214
|
+
mostSevere = alert;
|
|
215
|
+
mostSevereRank = rank;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (rank === null) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (mostSevereRank === null || rank < mostSevereRank) {
|
|
224
|
+
mostSevere = alert;
|
|
225
|
+
mostSevereRank = rank;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return mostSevere;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* The incident severity an alert severity corresponds to. An incident
|
|
234
|
+
* severity with the same name (ignoring case and surrounding spaces) wins;
|
|
235
|
+
* otherwise the alert severity's rank among the alert severities picks the
|
|
236
|
+
* incident severity at the same rank, clamped to the least severe one when
|
|
237
|
+
* the project has fewer incident severities than alert severities.
|
|
238
|
+
*/
|
|
239
|
+
public static mapAlertSeverityToIncidentSeverity(data: {
|
|
240
|
+
alertSeverityId: string | undefined | null;
|
|
241
|
+
alertSeverities: Array<SeverityForMapping>;
|
|
242
|
+
incidentSeverities: Array<SeverityForMapping>;
|
|
243
|
+
}): string | null {
|
|
244
|
+
if (!data.alertSeverityId || data.incidentSeverities.length === 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const alertSeverity: SeverityForMapping | undefined =
|
|
249
|
+
data.alertSeverities.find((severity: SeverityForMapping) => {
|
|
250
|
+
return severity.id === data.alertSeverityId;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (!alertSeverity) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const alertSeverityName: string = (alertSeverity.name || "")
|
|
258
|
+
.trim()
|
|
259
|
+
.toLowerCase();
|
|
260
|
+
|
|
261
|
+
if (alertSeverityName) {
|
|
262
|
+
const sameName: SeverityForMapping | undefined =
|
|
263
|
+
data.incidentSeverities.find((severity: SeverityForMapping) => {
|
|
264
|
+
return (
|
|
265
|
+
(severity.name || "").trim().toLowerCase() === alertSeverityName
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (sameName) {
|
|
270
|
+
return sameName.id;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const rank: number | null = this.getSeverityRank(
|
|
275
|
+
alertSeverity.id,
|
|
276
|
+
data.alertSeverities,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (rank === null) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sortedIncidentSeverities: Array<SeverityForMapping> =
|
|
284
|
+
this.sortSeveritiesByOrder(data.incidentSeverities);
|
|
285
|
+
|
|
286
|
+
const target: SeverityForMapping | undefined =
|
|
287
|
+
sortedIncidentSeverities[
|
|
288
|
+
Math.min(rank, sortedIncidentSeverities.length - 1)
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
return target ? target.id : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* "Alert #42" / "Alert ALT-42", or "Alert" when the number is unknown. */
|
|
295
|
+
public static getAlertReference(alert: AlertForIncidentPrefill): string {
|
|
296
|
+
if (alert.alertNumberWithPrefix) {
|
|
297
|
+
return `Alert ${alert.alertNumberWithPrefix}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof alert.alertNumber === "number") {
|
|
301
|
+
return `Alert #${alert.alertNumber}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return "Alert";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public static buildIncidentPrefill(
|
|
308
|
+
input: BuildIncidentPrefillInput,
|
|
309
|
+
): IncidentPrefillFromAlerts {
|
|
310
|
+
const alerts: Array<AlertForIncidentPrefill> = input.alerts;
|
|
311
|
+
|
|
312
|
+
const prefill: IncidentPrefillFromAlerts = {
|
|
313
|
+
title: "",
|
|
314
|
+
description: "",
|
|
315
|
+
incidentSeverityId: undefined,
|
|
316
|
+
monitors: [],
|
|
317
|
+
hosts: [],
|
|
318
|
+
kubernetesClusters: [],
|
|
319
|
+
dockerHosts: [],
|
|
320
|
+
podmanHosts: [],
|
|
321
|
+
services: [],
|
|
322
|
+
labelIds: [],
|
|
323
|
+
isPrivate: false,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (alerts.length === 0) {
|
|
327
|
+
return prefill;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mostSevere: AlertForIncidentPrefill | null = this.getMostSevereAlert(
|
|
331
|
+
alerts,
|
|
332
|
+
input.alertSeverities,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (alerts.length === 1) {
|
|
336
|
+
prefill.title = alerts[0]!.title || "";
|
|
337
|
+
prefill.description = alerts[0]!.description || "";
|
|
338
|
+
} else {
|
|
339
|
+
prefill.title = mostSevere?.title || "";
|
|
340
|
+
prefill.description = alerts
|
|
341
|
+
.map((alert: AlertForIncidentPrefill): string => {
|
|
342
|
+
return `- ${this.getAlertReference(alert)}: ${alert.title || ""}`.trimEnd();
|
|
343
|
+
})
|
|
344
|
+
.join("\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
prefill.incidentSeverityId =
|
|
348
|
+
this.mapAlertSeverityToIncidentSeverity({
|
|
349
|
+
alertSeverityId: mostSevere?.alertSeverityId,
|
|
350
|
+
alertSeverities: input.alertSeverities,
|
|
351
|
+
incidentSeverities: input.incidentSeverities,
|
|
352
|
+
}) || undefined;
|
|
353
|
+
|
|
354
|
+
for (const alert of alerts) {
|
|
355
|
+
if (alert.monitor) {
|
|
356
|
+
this.addResource(prefill.monitors, alert.monitor);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const key of INCIDENT_PREFILL_RESOURCE_KEYS) {
|
|
360
|
+
for (const resource of alert[key] || []) {
|
|
361
|
+
this.addResource(prefill[key], resource);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const labelId of alert.labelIds || []) {
|
|
366
|
+
if (labelId && !prefill.labelIds.includes(labelId)) {
|
|
367
|
+
prefill.labelIds.push(labelId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (alert.isPrivate) {
|
|
372
|
+
prefill.isPrivate = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return prefill;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/*
|
|
380
|
+
* Folds the prefill into the form's initial values without undoing what is
|
|
381
|
+
* already there (an incident template, say): title, description and
|
|
382
|
+
* severity only fill blanks, resource and label lists are unioned, and a
|
|
383
|
+
* private incident stays private. On-call policies are never touched -
|
|
384
|
+
* declaring an incident from alerts must not page anyone the alerts did
|
|
385
|
+
* not already page.
|
|
386
|
+
*/
|
|
387
|
+
public static applyPrefillToInitialValues(
|
|
388
|
+
initialValues: JSONObject,
|
|
389
|
+
prefill: IncidentPrefillFromAlerts,
|
|
390
|
+
): JSONObject {
|
|
391
|
+
const merged: JSONObject = { ...initialValues };
|
|
392
|
+
|
|
393
|
+
if (!this.hasText(merged["title"]) && prefill.title) {
|
|
394
|
+
merged["title"] = prefill.title;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!this.hasText(merged["description"]) && prefill.description) {
|
|
398
|
+
merged["description"] = prefill.description;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
!this.hasText(merged["incidentSeverity"]) &&
|
|
403
|
+
prefill.incidentSeverityId
|
|
404
|
+
) {
|
|
405
|
+
merged["incidentSeverity"] = prefill.incidentSeverityId;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const key of INCIDENT_PREFILL_LIST_KEYS) {
|
|
409
|
+
const resources: Array<NamedResource> = [];
|
|
410
|
+
|
|
411
|
+
for (const existing of this.asArray(merged[key])) {
|
|
412
|
+
const resource: NamedResource | null = this.toNamedResource(existing);
|
|
413
|
+
|
|
414
|
+
if (resource) {
|
|
415
|
+
this.addResource(resources, resource);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const resource of prefill[key]) {
|
|
420
|
+
this.addResource(resources, resource);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (resources.length > 0) {
|
|
424
|
+
merged[key] = resources as unknown as JSONValue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const labelIds: Array<string> = [];
|
|
429
|
+
|
|
430
|
+
for (const existing of this.asArray(merged["labels"])) {
|
|
431
|
+
const id: string | null = this.toId(existing);
|
|
432
|
+
|
|
433
|
+
if (id && !labelIds.includes(id)) {
|
|
434
|
+
labelIds.push(id);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const labelId of prefill.labelIds) {
|
|
439
|
+
if (!labelIds.includes(labelId)) {
|
|
440
|
+
labelIds.push(labelId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (labelIds.length > 0) {
|
|
445
|
+
merged["labels"] = labelIds;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (prefill.isPrivate) {
|
|
449
|
+
merged["isPrivate"] = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return merged;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private static addResource(
|
|
456
|
+
resources: Array<NamedResource>,
|
|
457
|
+
resource: NamedResource,
|
|
458
|
+
): void {
|
|
459
|
+
if (!resource._id) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const alreadyAdded: boolean = resources.some((existing: NamedResource) => {
|
|
464
|
+
return existing._id === resource._id;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (!alreadyAdded) {
|
|
468
|
+
resources.push({ _id: resource._id, name: resource.name || "" });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static hasText(value: JSONValue | undefined): boolean {
|
|
473
|
+
if (value === null || value === undefined) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return value.toString().trim().length > 0;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private static asArray(value: JSONValue | undefined): Array<JSONValue> {
|
|
481
|
+
return Array.isArray(value) ? (value as Array<JSONValue>) : [];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private static toId(value: JSONValue): string | null {
|
|
485
|
+
if (typeof value === "string") {
|
|
486
|
+
return value || null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (value && typeof value === "object") {
|
|
490
|
+
const id: JSONValue | undefined =
|
|
491
|
+
(value as JSONObject)["_id"] ?? (value as JSONObject)["id"];
|
|
492
|
+
|
|
493
|
+
if (id !== null && id !== undefined) {
|
|
494
|
+
const idString: string = id.toString();
|
|
495
|
+
return idString || null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private static toNamedResource(value: JSONValue): NamedResource | null {
|
|
503
|
+
const id: string | null = this.toId(value);
|
|
504
|
+
|
|
505
|
+
if (!id) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const name: JSONValue | undefined =
|
|
510
|
+
value && typeof value === "object"
|
|
511
|
+
? (value as JSONObject)["name"]
|
|
512
|
+
: undefined;
|
|
513
|
+
|
|
514
|
+
return { _id: id, name: typeof name === "string" ? name : "" };
|
|
515
|
+
}
|
|
516
|
+
}
|