@oneuptime/common 10.0.73 → 10.0.78

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.
Files changed (26) hide show
  1. package/Server/Services/IncidentService.ts +48 -0
  2. package/Server/Utils/Monitor/MonitorAlert.ts +0 -78
  3. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +24 -0
  4. package/Server/Utils/Monitor/MonitorIncident.ts +0 -78
  5. package/Types/Dashboard/DashboardLanguage.ts +30 -0
  6. package/Types/Icon/IconProp.ts +1 -0
  7. package/Types/Monitor/MonitorCriteriaInstance.ts +14 -0
  8. package/Types/Monitor/MonitorEvaluationSummary.ts +2 -0
  9. package/UI/Components/Icon/Icon.tsx +8 -0
  10. package/build/dist/Server/Services/IncidentService.js +44 -0
  11. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  12. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +0 -67
  13. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  14. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +26 -6
  15. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  16. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +0 -67
  17. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  18. package/build/dist/Types/Dashboard/DashboardLanguage.js +22 -0
  19. package/build/dist/Types/Dashboard/DashboardLanguage.js.map +1 -0
  20. package/build/dist/Types/Icon/IconProp.js +1 -0
  21. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  22. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
  23. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  24. package/build/dist/UI/Components/Icon/Icon.js +3 -0
  25. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  26. package/package.json +1 -1
@@ -1932,6 +1932,41 @@ ${incidentSeverity.name}
1932
1932
  return incidentCount.toNumber() > 0;
1933
1933
  }
1934
1934
 
1935
+ @CaptureSpan()
1936
+ public async doesMonitorHaveActiveIncidents(
1937
+ monitorId: ObjectID,
1938
+ projectId: ObjectID,
1939
+ ): Promise<boolean> {
1940
+ const resolvedState: IncidentState | null =
1941
+ await IncidentStateService.findOneBy({
1942
+ query: {
1943
+ projectId: projectId,
1944
+ isResolvedState: true,
1945
+ },
1946
+ props: {
1947
+ isRoot: true,
1948
+ },
1949
+ select: {
1950
+ _id: true,
1951
+ order: true,
1952
+ },
1953
+ });
1954
+
1955
+ const incidentCount: PositiveNumber = await this.countBy({
1956
+ query: {
1957
+ monitors: QueryHelper.inRelationArray([monitorId]),
1958
+ currentIncidentState: {
1959
+ order: QueryHelper.lessThan(resolvedState?.order as number),
1960
+ },
1961
+ },
1962
+ props: {
1963
+ isRoot: true,
1964
+ },
1965
+ });
1966
+
1967
+ return incidentCount.toNumber() > 0;
1968
+ }
1969
+
1935
1970
  @CaptureSpan()
1936
1971
  public async markMonitorsActiveForMonitoring(
1937
1972
  projectId: ObjectID,
@@ -1980,6 +2015,19 @@ ${incidentSeverity.name}
1980
2015
  },
1981
2016
  });
1982
2017
 
2018
+ /*
2019
+ * Don't flip the monitor to operational while other incidents
2020
+ * are still open on it — e.g. a metric monitor with group-by
2021
+ * may have one incident per series, and resolving one series
2022
+ * shouldn't claim the whole monitor is healthy.
2023
+ */
2024
+ const hasOtherActiveIncidents: boolean =
2025
+ await this.doesMonitorHaveActiveIncidents(monitor.id!, projectId!);
2026
+
2027
+ if (hasOtherActiveIncidents) {
2028
+ continue;
2029
+ }
2030
+
1983
2031
  const latestState: MonitorStatusTimeline | null =
1984
2032
  await MonitorStatusTimelineService.findOneBy({
1985
2033
  query: {
@@ -16,9 +16,7 @@ import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
16
16
  import { DisableAutomaticAlertCreation } from "../../EnvironmentConfig";
17
17
  import AlertService from "../../Services/AlertService";
18
18
  import AlertSeverityService from "../../Services/AlertSeverityService";
19
- import AlertStateService from "../../Services/AlertStateService";
20
19
  import AlertStateTimelineService from "../../Services/AlertStateTimelineService";
21
- import AlertState from "../../../Models/DatabaseModels/AlertState";
22
20
  import logger, { LogAttributes } from "../Logger";
23
21
  import CaptureSpan from "../Telemetry/CaptureSpan";
24
22
  import DataToProcess from "./DataToProcess";
@@ -372,82 +370,6 @@ export default class MonitorAlert {
372
370
  input.openAlert.projectId!,
373
371
  );
374
372
 
375
- /*
376
- * Skip the Resolved insert if the alert's timeline is already at or past
377
- * the Resolved state in the project's workflow order. Two cases:
378
- * 1. Latest timeline state is Resolved but Alert.currentAlertStateId is
379
- * stuck on an earlier state (partial-failure from a prior resolve).
380
- * Re-inserting Resolved would throw "Alert state cannot be same as
381
- * previous state" from AlertStateTimelineService.onBeforeCreate.
382
- * 2. The project defines a custom state after Resolved (e.g. Closed) and
383
- * the alert has moved into it. Inserting Resolved would throw
384
- * "cannot transition to Resolved from Closed because Resolved is
385
- * before Closed in the order of alert states."
386
- * Either failure bubbles up through ingest workers and causes monitors to
387
- * flap. Reconcile Alert.currentAlertStateId if out of sync with the
388
- * timeline, then return.
389
- */
390
- const [resolvedState, latestTimeline]: [
391
- AlertState | null,
392
- AlertStateTimeline | null,
393
- ] = await Promise.all([
394
- AlertStateService.findOneBy({
395
- query: {
396
- _id: resolvedStateId.toString(),
397
- },
398
- select: {
399
- order: true,
400
- },
401
- props: {
402
- isRoot: true,
403
- },
404
- }),
405
- AlertStateTimelineService.findOneBy({
406
- query: {
407
- alertId: input.openAlert.id!,
408
- },
409
- sort: {
410
- startsAt: SortOrder.Descending,
411
- },
412
- select: {
413
- alertStateId: true,
414
- alertState: {
415
- order: true,
416
- },
417
- },
418
- props: {
419
- isRoot: true,
420
- },
421
- }),
422
- ]);
423
-
424
- const latestOrder: number | undefined | null =
425
- latestTimeline?.alertState?.order;
426
- const resolvedOrder: number | undefined | null = resolvedState?.order;
427
-
428
- if (
429
- latestTimeline?.alertStateId &&
430
- typeof latestOrder === "number" &&
431
- typeof resolvedOrder === "number" &&
432
- latestOrder >= resolvedOrder
433
- ) {
434
- if (
435
- input.openAlert.currentAlertStateId?.toString() !==
436
- latestTimeline.alertStateId.toString()
437
- ) {
438
- await AlertService.updateOneById({
439
- id: input.openAlert.id!,
440
- data: {
441
- currentAlertStateId: latestTimeline.alertStateId,
442
- },
443
- props: {
444
- isRoot: true,
445
- },
446
- });
447
- }
448
- return;
449
- }
450
-
451
373
  const alertStateTimeline: AlertStateTimeline = new AlertStateTimeline();
452
374
  alertStateTimeline.alertId = input.openAlert.id!;
453
375
  alertStateTimeline.alertStateId = resolvedStateId;
@@ -78,6 +78,30 @@ export default class MonitorCriteriaEvaluator {
78
78
  }
79
79
 
80
80
  for (const criteriaInstance of criteria.data.monitorCriteriaInstanceArray) {
81
+ /*
82
+ * Record disabled criteria in the summary (so the user can see why
83
+ * nothing happened) but skip evaluation, status changes, incidents,
84
+ * and alerts.
85
+ */
86
+ if (criteriaInstance.data?.isEnabled === false) {
87
+ const skipReason: string =
88
+ "This criteria is disabled and was not evaluated.";
89
+ const skippedCriteriaResult: MonitorEvaluationCriteriaResult = {
90
+ criteriaId: criteriaInstance.data?.id,
91
+ criteriaName: criteriaInstance.data?.name,
92
+ filterCondition:
93
+ criteriaInstance.data?.filterCondition || FilterCondition.All,
94
+ met: false,
95
+ message: skipReason,
96
+ filters: [],
97
+ skipped: true,
98
+ skipReason: skipReason,
99
+ };
100
+
101
+ input.evaluationSummary.criteriaResults.push(skippedCriteriaResult);
102
+ continue;
103
+ }
104
+
81
105
  const criteriaResult: MonitorEvaluationCriteriaResult = {
82
106
  criteriaId: criteriaInstance.data?.id,
83
107
  criteriaName: criteriaInstance.data?.name,
@@ -17,10 +17,8 @@ import { TelemetryQuery } from "../../../Types/Telemetry/TelemetryQuery";
17
17
  import { DisableAutomaticIncidentCreation } from "../../EnvironmentConfig";
18
18
  import IncidentService from "../../Services/IncidentService";
19
19
  import IncidentSeverityService from "../../Services/IncidentSeverityService";
20
- import IncidentStateService from "../../Services/IncidentStateService";
21
20
  import IncidentStateTimelineService from "../../Services/IncidentStateTimelineService";
22
21
  import IncidentMemberService from "../../Services/IncidentMemberService";
23
- import IncidentState from "../../../Models/DatabaseModels/IncidentState";
24
22
  import logger, { LogAttributes } from "../Logger";
25
23
  import CaptureSpan from "../Telemetry/CaptureSpan";
26
24
  import DataToProcess from "./DataToProcess";
@@ -468,82 +466,6 @@ export default class MonitorIncident {
468
466
  input.openIncident.projectId!,
469
467
  );
470
468
 
471
- /*
472
- * Skip the Resolved insert if the incident's timeline is already at or
473
- * past the Resolved state in the project's workflow order. Two cases:
474
- * 1. Latest timeline state is Resolved but Incident.currentIncidentStateId
475
- * is stuck on an earlier state (partial-failure from a prior resolve).
476
- * Re-inserting Resolved would throw "state cannot be same as previous"
477
- * from IncidentStateTimelineService.onBeforeCreate.
478
- * 2. The project defines a custom state after Resolved (e.g. Closed) and
479
- * the incident has moved into it. Inserting Resolved would throw
480
- * "cannot transition to Resolved from Closed because Resolved is
481
- * before Closed in the order of incident states."
482
- * Either failure bubbles up through ingest workers and causes monitors to
483
- * flap. Reconcile Incident.currentIncidentStateId if out of sync with the
484
- * timeline, then return.
485
- */
486
- const [resolvedState, latestTimeline]: [
487
- IncidentState | null,
488
- IncidentStateTimeline | null,
489
- ] = await Promise.all([
490
- IncidentStateService.findOneBy({
491
- query: {
492
- _id: resolvedStateId.toString(),
493
- },
494
- select: {
495
- order: true,
496
- },
497
- props: {
498
- isRoot: true,
499
- },
500
- }),
501
- IncidentStateTimelineService.findOneBy({
502
- query: {
503
- incidentId: input.openIncident.id!,
504
- },
505
- sort: {
506
- startsAt: SortOrder.Descending,
507
- },
508
- select: {
509
- incidentStateId: true,
510
- incidentState: {
511
- order: true,
512
- },
513
- },
514
- props: {
515
- isRoot: true,
516
- },
517
- }),
518
- ]);
519
-
520
- const latestOrder: number | undefined | null =
521
- latestTimeline?.incidentState?.order;
522
- const resolvedOrder: number | undefined | null = resolvedState?.order;
523
-
524
- if (
525
- latestTimeline?.incidentStateId &&
526
- typeof latestOrder === "number" &&
527
- typeof resolvedOrder === "number" &&
528
- latestOrder >= resolvedOrder
529
- ) {
530
- if (
531
- input.openIncident.currentIncidentStateId?.toString() !==
532
- latestTimeline.incidentStateId.toString()
533
- ) {
534
- await IncidentService.updateOneById({
535
- id: input.openIncident.id!,
536
- data: {
537
- currentIncidentStateId: latestTimeline.incidentStateId,
538
- },
539
- props: {
540
- isRoot: true,
541
- },
542
- });
543
- }
544
- return;
545
- }
546
-
547
469
  const incidentStateTimeline: IncidentStateTimeline =
548
470
  new IncidentStateTimeline();
549
471
  incidentStateTimeline.incidentId = input.openIncident.id!;
@@ -0,0 +1,30 @@
1
+ export interface DashboardLanguage {
2
+ code: string;
3
+ nativeName: string;
4
+ englishName: string;
5
+ }
6
+
7
+ export const DEFAULT_DASHBOARD_LANGUAGE: string = "en";
8
+
9
+ export const SUPPORTED_DASHBOARD_LANGUAGES: Array<DashboardLanguage> = [
10
+ { code: "en", nativeName: "English", englishName: "English" },
11
+ { code: "de", nativeName: "Deutsch", englishName: "German" },
12
+ { code: "fr", nativeName: "Français", englishName: "French" },
13
+ { code: "es", nativeName: "Español", englishName: "Spanish" },
14
+ { code: "it", nativeName: "Italiano", englishName: "Italian" },
15
+ { code: "pt", nativeName: "Português", englishName: "Portuguese" },
16
+ { code: "nl", nativeName: "Nederlands", englishName: "Dutch" },
17
+ { code: "da", nativeName: "Dansk", englishName: "Danish" },
18
+ { code: "no", nativeName: "Norsk", englishName: "Norwegian" },
19
+ { code: "sv", nativeName: "Svenska", englishName: "Swedish" },
20
+ { code: "ru", nativeName: "Русский", englishName: "Russian" },
21
+ { code: "ja", nativeName: "日本語", englishName: "Japanese" },
22
+ { code: "ko", nativeName: "한국어", englishName: "Korean" },
23
+ { code: "zh", nativeName: "中文", englishName: "Chinese" },
24
+ { code: "hi", nativeName: "हिन्दी", englishName: "Hindi" },
25
+ ];
26
+
27
+ export const SUPPORTED_DASHBOARD_LANGUAGE_CODES: Array<string> =
28
+ SUPPORTED_DASHBOARD_LANGUAGES.map((language: DashboardLanguage) => {
29
+ return language.code;
30
+ });
@@ -75,6 +75,7 @@ enum IconProp {
75
75
  Time = "Time",
76
76
  Terminal = "Terminal",
77
77
  Drag = "Drag",
78
+ GripVertical = "GripVertical",
78
79
  Error = "Error",
79
80
  Code = "Code",
80
81
  Report = "Report",
@@ -30,6 +30,7 @@ export interface MonitorCriteriaInstanceType {
30
30
  changeMonitorStatus?: boolean | undefined;
31
31
  createIncidents?: boolean | undefined;
32
32
  createAlerts?: boolean | undefined;
33
+ isEnabled?: boolean | undefined;
33
34
  id: string;
34
35
  }
35
36
 
@@ -52,6 +53,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
52
53
  createIncidents: false,
53
54
  createAlerts: false,
54
55
  changeMonitorStatus: false,
56
+ isEnabled: true,
55
57
  incidents: [],
56
58
  alerts: [],
57
59
  name: "",
@@ -1375,6 +1377,14 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1375
1377
  return this;
1376
1378
  }
1377
1379
 
1380
+ public setIsEnabled(isEnabled: boolean | undefined): MonitorCriteriaInstance {
1381
+ if (this.data) {
1382
+ this.data.isEnabled = isEnabled;
1383
+ }
1384
+
1385
+ return this;
1386
+ }
1387
+
1378
1388
  public override toJSON(): JSONObject {
1379
1389
  if (!this.data) {
1380
1390
  return MonitorCriteriaInstance.getNewMonitorCriteriaInstanceAsJSON();
@@ -1392,6 +1402,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1392
1402
  createAlerts: this.data.createAlerts,
1393
1403
  changeMonitorStatus: this.data.changeMonitorStatus,
1394
1404
  createIncidents: this.data.createIncidents,
1405
+ isEnabled: this.data.isEnabled,
1395
1406
  name: this.data.name,
1396
1407
  description: this.data.description,
1397
1408
  } as any,
@@ -1499,6 +1510,8 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1499
1510
  changeMonitorStatus: (json["changeMonitorStatus"] as boolean) || false,
1500
1511
  createIncidents: (json["createIncidents"] as boolean) || false,
1501
1512
  createAlerts: (json["createAlerts"] as boolean) || false,
1513
+ isEnabled:
1514
+ json["isEnabled"] === undefined ? true : (json["isEnabled"] as boolean),
1502
1515
  filters: filters as any,
1503
1516
  incidents: incidents as any,
1504
1517
  alerts: alerts as any,
@@ -1524,6 +1537,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1524
1537
  changeMonitorStatus: Zod.boolean().optional(),
1525
1538
  createIncidents: Zod.boolean().optional(),
1526
1539
  createAlerts: Zod.boolean().optional(),
1540
+ isEnabled: Zod.boolean().optional(),
1527
1541
  }).openapi({
1528
1542
  type: "object",
1529
1543
  example: {
@@ -28,6 +28,8 @@ export interface MonitorEvaluationCriteriaResult {
28
28
  met: boolean;
29
29
  message: string;
30
30
  filters: Array<MonitorEvaluationFilterResult>;
31
+ skipped?: boolean | undefined;
32
+ skipReason?: string | undefined;
31
33
  }
32
34
 
33
35
  export interface MonitorEvaluationEvent {
@@ -1243,6 +1243,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
1243
1243
  d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z"
1244
1244
  />,
1245
1245
  );
1246
+ } else if (icon === IconProp.GripVertical) {
1247
+ return getSvgWrapper(
1248
+ <path
1249
+ fill="currentColor"
1250
+ stroke="none"
1251
+ d="M9 5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM9 12a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM9 19a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM18 5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM18 12a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM18 19a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"
1252
+ />,
1253
+ );
1246
1254
  } else if (icon === IconProp.Graph) {
1247
1255
  return getSvgWrapper(
1248
1256
  <path
@@ -1427,6 +1427,33 @@ ${incidentSeverity.name}
1427
1427
  });
1428
1428
  return incidentCount.toNumber() > 0;
1429
1429
  }
1430
+ async doesMonitorHaveActiveIncidents(monitorId, projectId) {
1431
+ const resolvedState = await IncidentStateService.findOneBy({
1432
+ query: {
1433
+ projectId: projectId,
1434
+ isResolvedState: true,
1435
+ },
1436
+ props: {
1437
+ isRoot: true,
1438
+ },
1439
+ select: {
1440
+ _id: true,
1441
+ order: true,
1442
+ },
1443
+ });
1444
+ const incidentCount = await this.countBy({
1445
+ query: {
1446
+ monitors: QueryHelper.inRelationArray([monitorId]),
1447
+ currentIncidentState: {
1448
+ order: QueryHelper.lessThan(resolvedState === null || resolvedState === void 0 ? void 0 : resolvedState.order),
1449
+ },
1450
+ },
1451
+ props: {
1452
+ isRoot: true,
1453
+ },
1454
+ });
1455
+ return incidentCount.toNumber() > 0;
1456
+ }
1430
1457
  async markMonitorsActiveForMonitoring(projectId, monitors, startsAt) {
1431
1458
  // resolve all the monitors.
1432
1459
  var _a;
@@ -1460,6 +1487,16 @@ ${incidentSeverity.name}
1460
1487
  isRoot: true,
1461
1488
  },
1462
1489
  });
1490
+ /*
1491
+ * Don't flip the monitor to operational while other incidents
1492
+ * are still open on it — e.g. a metric monitor with group-by
1493
+ * may have one incident per series, and resolving one series
1494
+ * shouldn't claim the whole monitor is healthy.
1495
+ */
1496
+ const hasOtherActiveIncidents = await this.doesMonitorHaveActiveIncidents(monitor.id, projectId);
1497
+ if (hasOtherActiveIncidents) {
1498
+ continue;
1499
+ }
1463
1500
  const latestState = await MonitorStatusTimelineService.findOneBy({
1464
1501
  query: {
1465
1502
  monitorId: monitor.id,
@@ -2214,6 +2251,13 @@ __decorate([
2214
2251
  ObjectID]),
2215
2252
  __metadata("design:returntype", Promise)
2216
2253
  ], Service.prototype, "doesMonitorHasMoreActiveManualIncidents", null);
2254
+ __decorate([
2255
+ CaptureSpan(),
2256
+ __metadata("design:type", Function),
2257
+ __metadata("design:paramtypes", [ObjectID,
2258
+ ObjectID]),
2259
+ __metadata("design:returntype", Promise)
2260
+ ], Service.prototype, "doesMonitorHaveActiveIncidents", null);
2217
2261
  __decorate([
2218
2262
  CaptureSpan(),
2219
2263
  __metadata("design:type", Function),