@oneuptime/common 10.5.2 → 10.5.4
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/AlertGroupingRule.ts +76 -0
- package/Models/DatabaseModels/IncidentGroupingRule.ts +76 -0
- package/Models/DatabaseModels/StatusPageGroup.ts +212 -0
- package/Models/DatabaseModels/StatusPageResource.ts +86 -0
- package/Server/API/StatusPageAPI.ts +15 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.ts +61 -13
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.ts +65 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.ts +37 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -3
- package/Server/Services/AlertGroupingEngineService.ts +83 -0
- package/Server/Services/IncidentGroupingEngineService.ts +99 -0
- package/Server/Services/StatusPageService.ts +5 -0
- package/Tests/Server/Services/AlertGroupingEngineService.test.ts +28 -0
- package/Tests/Server/Services/AlertGroupingRuleService.test.ts +14 -0
- package/Types/Monitor/MonitorStep.ts +85 -0
- package/Types/StatusPage/StatusPageGroupViewMode.ts +6 -0
- package/UI/Components/Accordion/Accordion.tsx +40 -26
- package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +78 -0
- package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +78 -0
- package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageGroup.js +217 -0
- package/build/dist/Models/DatabaseModels/StatusPageGroup.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +88 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +15 -0
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js +29 -5
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertGroupingEngineService.js +85 -0
- package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +95 -0
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +5 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +21 -0
- package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -1
- package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +12 -0
- package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +59 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js +7 -0
- package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js.map +1 -0
- package/build/dist/UI/Components/Accordion/Accordion.js +11 -11
- package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.ts +0 -115
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js.map +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class AddLabelGroupByToGroupingRules1779971548393
|
|
4
|
+
implements MigrationInterface
|
|
5
|
+
{
|
|
6
|
+
public name: string = "AddLabelGroupByToGroupingRules1779971548393";
|
|
7
|
+
|
|
8
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
9
|
+
await queryRunner.query(
|
|
10
|
+
`ALTER TABLE "IncidentGroupingRule" ADD "groupByIncidentLabels" boolean NOT NULL DEFAULT false`,
|
|
11
|
+
);
|
|
12
|
+
await queryRunner.query(
|
|
13
|
+
`ALTER TABLE "IncidentGroupingRule" ADD "groupByMonitorLabels" boolean NOT NULL DEFAULT false`,
|
|
14
|
+
);
|
|
15
|
+
await queryRunner.query(
|
|
16
|
+
`ALTER TABLE "AlertGroupingRule" ADD "groupByAlertLabels" boolean NOT NULL DEFAULT false`,
|
|
17
|
+
);
|
|
18
|
+
await queryRunner.query(
|
|
19
|
+
`ALTER TABLE "AlertGroupingRule" ADD "groupByMonitorLabels" boolean NOT NULL DEFAULT false`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
24
|
+
await queryRunner.query(
|
|
25
|
+
`ALTER TABLE "AlertGroupingRule" DROP COLUMN "groupByMonitorLabels"`,
|
|
26
|
+
);
|
|
27
|
+
await queryRunner.query(
|
|
28
|
+
`ALTER TABLE "AlertGroupingRule" DROP COLUMN "groupByAlertLabels"`,
|
|
29
|
+
);
|
|
30
|
+
await queryRunner.query(
|
|
31
|
+
`ALTER TABLE "IncidentGroupingRule" DROP COLUMN "groupByMonitorLabels"`,
|
|
32
|
+
);
|
|
33
|
+
await queryRunner.query(
|
|
34
|
+
`ALTER TABLE "IncidentGroupingRule" DROP COLUMN "groupByIncidentLabels"`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -355,7 +355,9 @@ import { MigrationName1779790539196 } from "./1779790539196-MigrationName";
|
|
|
355
355
|
import { ExpandOwnerRuleInheritFlags1779823516881 } from "./1779823516881-ExpandOwnerRuleInheritFlags";
|
|
356
356
|
import { RenameStatusPageZhToZhCN1779827700000 } from "./1779827700000-RenameStatusPageZhToZhCN";
|
|
357
357
|
import { MigrationName1779879993421 } from "./1779879993421-MigrationName";
|
|
358
|
-
import {
|
|
358
|
+
import { MigrationName1779882573463 } from "./1779882573463-MigrationName";
|
|
359
|
+
import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-AddLabelGroupByToGroupingRules";
|
|
360
|
+
|
|
359
361
|
export default [
|
|
360
362
|
InitialMigration,
|
|
361
363
|
MigrationName1717678334852,
|
|
@@ -713,6 +715,7 @@ export default [
|
|
|
713
715
|
MigrationName1779790539196,
|
|
714
716
|
ExpandOwnerRuleInheritFlags1779823516881,
|
|
715
717
|
RenameStatusPageZhToZhCN1779827700000,
|
|
716
|
-
|
|
717
|
-
|
|
718
|
+
MigrationName1779879993421,
|
|
719
|
+
MigrationName1779882573463,
|
|
720
|
+
AddLabelGroupByToGroupingRules1779971548393,
|
|
718
721
|
];
|
|
@@ -16,6 +16,7 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
|
16
16
|
import OneUptimeDate from "../../Types/Date";
|
|
17
17
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
18
18
|
import AlertGroupingRuleService from "./AlertGroupingRuleService";
|
|
19
|
+
import AlertService from "./AlertService";
|
|
19
20
|
import AlertEpisodeService from "./AlertEpisodeService";
|
|
20
21
|
import AlertEpisodeMemberService from "./AlertEpisodeMemberService";
|
|
21
22
|
import AlertEpisodeOwnerUserService from "./AlertEpisodeOwnerUserService";
|
|
@@ -91,6 +92,8 @@ class AlertGroupingEngineServiceClass {
|
|
|
91
92
|
groupByMonitor: true,
|
|
92
93
|
groupBySeverity: true,
|
|
93
94
|
groupByAlertTitle: true,
|
|
95
|
+
groupByAlertLabels: true,
|
|
96
|
+
groupByMonitorLabels: true,
|
|
94
97
|
// Time settings
|
|
95
98
|
enableTimeWindow: true,
|
|
96
99
|
timeWindowMinutes: true,
|
|
@@ -555,10 +558,84 @@ class AlertGroupingEngineServiceClass {
|
|
|
555
558
|
parts.push(`title:${normalizedTitle}`);
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
// Group by alert labels (exact set match) - only if explicitly enabled
|
|
562
|
+
if (rule.groupByAlertLabels && alert.id) {
|
|
563
|
+
const alertLabels: Array<Label> = await this.getAlertLabels(alert);
|
|
564
|
+
const sortedLabelIds: Array<string> = alertLabels
|
|
565
|
+
.map((l: Label) => {
|
|
566
|
+
return l.id?.toString() || "";
|
|
567
|
+
})
|
|
568
|
+
.filter((id: string) => {
|
|
569
|
+
return id.length > 0;
|
|
570
|
+
})
|
|
571
|
+
.sort();
|
|
572
|
+
parts.push(`alertLabels:${sortedLabelIds.join(",")}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Group by monitor labels (exact set match) - only if explicitly enabled
|
|
576
|
+
if (rule.groupByMonitorLabels && alert.monitorId) {
|
|
577
|
+
const monitorLabels: Array<Label> = await this.getMonitorLabels(
|
|
578
|
+
alert.monitorId,
|
|
579
|
+
);
|
|
580
|
+
const sortedLabelIds: Array<string> = monitorLabels
|
|
581
|
+
.map((l: Label) => {
|
|
582
|
+
return l.id?.toString() || "";
|
|
583
|
+
})
|
|
584
|
+
.filter((id: string) => {
|
|
585
|
+
return id.length > 0;
|
|
586
|
+
})
|
|
587
|
+
.sort();
|
|
588
|
+
parts.push(`monitorLabels:${sortedLabelIds.join(",")}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
558
591
|
// If no group by options are enabled, all matching alerts go into a single episode
|
|
559
592
|
return parts.join("|") || "default";
|
|
560
593
|
}
|
|
561
594
|
|
|
595
|
+
@CaptureSpan()
|
|
596
|
+
private async getAlertLabels(alert: Alert): Promise<Array<Label>> {
|
|
597
|
+
// If labels are already loaded on the alert, use them
|
|
598
|
+
if (alert.labels && Array.isArray(alert.labels)) {
|
|
599
|
+
return alert.labels;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!alert.id) {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Re-load alert with labels
|
|
607
|
+
const reloadedAlert: Alert | null = await AlertService.findOneById({
|
|
608
|
+
id: alert.id,
|
|
609
|
+
select: {
|
|
610
|
+
labels: {
|
|
611
|
+
_id: true,
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
props: {
|
|
615
|
+
isRoot: true,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return reloadedAlert?.labels || [];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@CaptureSpan()
|
|
623
|
+
private async getMonitorLabels(monitorId: ObjectID): Promise<Array<Label>> {
|
|
624
|
+
const monitor: Monitor | null = await MonitorService.findOneById({
|
|
625
|
+
id: monitorId,
|
|
626
|
+
select: {
|
|
627
|
+
labels: {
|
|
628
|
+
_id: true,
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
props: {
|
|
632
|
+
isRoot: true,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return monitor?.labels || [];
|
|
637
|
+
}
|
|
638
|
+
|
|
562
639
|
@CaptureSpan()
|
|
563
640
|
private async findMatchingActiveEpisode(
|
|
564
641
|
projectId: ObjectID,
|
|
@@ -793,6 +870,12 @@ class AlertGroupingEngineServiceClass {
|
|
|
793
870
|
if (rule.groupByAlertTitle) {
|
|
794
871
|
groupByParts.push("Alert Title");
|
|
795
872
|
}
|
|
873
|
+
if (rule.groupByAlertLabels) {
|
|
874
|
+
groupByParts.push("Alert Labels");
|
|
875
|
+
}
|
|
876
|
+
if (rule.groupByMonitorLabels) {
|
|
877
|
+
groupByParts.push("Monitor Labels");
|
|
878
|
+
}
|
|
796
879
|
|
|
797
880
|
const groupByDescription: string =
|
|
798
881
|
groupByParts.length > 0
|
|
@@ -17,6 +17,7 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
|
17
17
|
import OneUptimeDate from "../../Types/Date";
|
|
18
18
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
19
19
|
import IncidentGroupingRuleService from "./IncidentGroupingRuleService";
|
|
20
|
+
import IncidentService from "./IncidentService";
|
|
20
21
|
import IncidentEpisodeService from "./IncidentEpisodeService";
|
|
21
22
|
import IncidentEpisodeMemberService from "./IncidentEpisodeMemberService";
|
|
22
23
|
import IncidentEpisodeOwnerUserService from "./IncidentEpisodeOwnerUserService";
|
|
@@ -93,6 +94,8 @@ class IncidentGroupingEngineServiceClass {
|
|
|
93
94
|
groupByMonitor: true,
|
|
94
95
|
groupBySeverity: true,
|
|
95
96
|
groupByIncidentTitle: true,
|
|
97
|
+
groupByIncidentLabels: true,
|
|
98
|
+
groupByMonitorLabels: true,
|
|
96
99
|
// Time settings
|
|
97
100
|
enableTimeWindow: true,
|
|
98
101
|
timeWindowMinutes: true,
|
|
@@ -613,10 +616,100 @@ class IncidentGroupingEngineServiceClass {
|
|
|
613
616
|
parts.push(`title:${normalizedTitle}`);
|
|
614
617
|
}
|
|
615
618
|
|
|
619
|
+
// Group by incident labels (exact set match) - only if explicitly enabled
|
|
620
|
+
if (rule.groupByIncidentLabels && incident.id) {
|
|
621
|
+
const incidentLabels: Array<Label> =
|
|
622
|
+
await this.getIncidentLabels(incident);
|
|
623
|
+
const sortedLabelIds: Array<string> = incidentLabels
|
|
624
|
+
.map((l: Label) => {
|
|
625
|
+
return l.id?.toString() || "";
|
|
626
|
+
})
|
|
627
|
+
.filter((id: string) => {
|
|
628
|
+
return id.length > 0;
|
|
629
|
+
})
|
|
630
|
+
.sort();
|
|
631
|
+
parts.push(`incidentLabels:${sortedLabelIds.join(",")}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/*
|
|
635
|
+
* Group by monitor labels (exact set match) - only if explicitly enabled
|
|
636
|
+
* Incidents can be associated with multiple monitors; we union and dedupe labels across all of them.
|
|
637
|
+
*/
|
|
638
|
+
if (
|
|
639
|
+
rule.groupByMonitorLabels &&
|
|
640
|
+
incident.monitors &&
|
|
641
|
+
incident.monitors.length > 0
|
|
642
|
+
) {
|
|
643
|
+
const monitorLabelIdSet: Set<string> = new Set<string>();
|
|
644
|
+
for (const incidentMonitor of incident.monitors) {
|
|
645
|
+
if (!incidentMonitor || !incidentMonitor.id) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const monitorLabels: Array<Label> = await this.getMonitorLabels(
|
|
649
|
+
incidentMonitor.id,
|
|
650
|
+
);
|
|
651
|
+
for (const label of monitorLabels) {
|
|
652
|
+
const labelIdStr: string = label.id?.toString() || "";
|
|
653
|
+
if (labelIdStr.length > 0) {
|
|
654
|
+
monitorLabelIdSet.add(labelIdStr);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const sortedLabelIds: Array<string> =
|
|
659
|
+
Array.from(monitorLabelIdSet).sort();
|
|
660
|
+
parts.push(`monitorLabels:${sortedLabelIds.join(",")}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
616
663
|
// If no group by options are enabled, all matching incidents go into a single episode
|
|
617
664
|
return parts.join("|") || "default";
|
|
618
665
|
}
|
|
619
666
|
|
|
667
|
+
@CaptureSpan()
|
|
668
|
+
private async getIncidentLabels(incident: Incident): Promise<Array<Label>> {
|
|
669
|
+
// If labels are already loaded on the incident, use them
|
|
670
|
+
if (incident.labels && Array.isArray(incident.labels)) {
|
|
671
|
+
return incident.labels;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!incident.id) {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Re-load incident with labels
|
|
679
|
+
const reloadedIncident: Incident | null = await IncidentService.findOneById(
|
|
680
|
+
{
|
|
681
|
+
id: incident.id,
|
|
682
|
+
select: {
|
|
683
|
+
labels: {
|
|
684
|
+
_id: true,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
props: {
|
|
688
|
+
isRoot: true,
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
return reloadedIncident?.labels || [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@CaptureSpan()
|
|
697
|
+
private async getMonitorLabels(monitorId: ObjectID): Promise<Array<Label>> {
|
|
698
|
+
const monitor: Monitor | null = await MonitorService.findOneById({
|
|
699
|
+
id: monitorId,
|
|
700
|
+
select: {
|
|
701
|
+
labels: {
|
|
702
|
+
_id: true,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
props: {
|
|
706
|
+
isRoot: true,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
return monitor?.labels || [];
|
|
711
|
+
}
|
|
712
|
+
|
|
620
713
|
@CaptureSpan()
|
|
621
714
|
private async findMatchingActiveEpisode(
|
|
622
715
|
projectId: ObjectID,
|
|
@@ -887,6 +980,12 @@ class IncidentGroupingEngineServiceClass {
|
|
|
887
980
|
if (rule.groupByIncidentTitle) {
|
|
888
981
|
groupByParts.push("Incident Title");
|
|
889
982
|
}
|
|
983
|
+
if (rule.groupByIncidentLabels) {
|
|
984
|
+
groupByParts.push("Incident Labels");
|
|
985
|
+
}
|
|
986
|
+
if (rule.groupByMonitorLabels) {
|
|
987
|
+
groupByParts.push("Monitor Labels");
|
|
988
|
+
}
|
|
890
989
|
|
|
891
990
|
const groupByDescription: string =
|
|
892
991
|
groupByParts.length > 0
|
|
@@ -1247,11 +1247,16 @@ export class Service extends DatabaseService<StatusPage> {
|
|
|
1247
1247
|
statusPageGroupId: true,
|
|
1248
1248
|
statusPageGroup: {
|
|
1249
1249
|
name: true,
|
|
1250
|
+
viewMode: true,
|
|
1251
|
+
rowAxisLabel: true,
|
|
1252
|
+
columnAxisLabel: true,
|
|
1250
1253
|
},
|
|
1251
1254
|
monitorId: true,
|
|
1252
1255
|
displayTooltip: true,
|
|
1253
1256
|
displayDescription: true,
|
|
1254
1257
|
displayName: true,
|
|
1258
|
+
rowAxisValue: true,
|
|
1259
|
+
columnAxisValue: true,
|
|
1255
1260
|
monitor: {
|
|
1256
1261
|
_id: true,
|
|
1257
1262
|
currentMonitorStatusId: true,
|
|
@@ -232,6 +232,34 @@ describe("AlertGroupingEngineService Models", () => {
|
|
|
232
232
|
);
|
|
233
233
|
expect(groupingKey).toContain(`title:${mockAlert.title}`);
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
test("should produce identical label key regardless of label order (exact set match)", () => {
|
|
237
|
+
// Simulates buildGroupingKey's label-sorting logic.
|
|
238
|
+
const labelIdsA: string[] = ["lbl-c", "lbl-a", "lbl-b"];
|
|
239
|
+
const labelIdsB: string[] = ["lbl-b", "lbl-c", "lbl-a"];
|
|
240
|
+
|
|
241
|
+
const keyA: string = `alertLabels:${[...labelIdsA].sort().join(",")}`;
|
|
242
|
+
const keyB: string = `alertLabels:${[...labelIdsB].sort().join(",")}`;
|
|
243
|
+
|
|
244
|
+
expect(keyA).toBe(keyB);
|
|
245
|
+
expect(keyA).toBe("alertLabels:lbl-a,lbl-b,lbl-c");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("should produce different label keys for different label sets (exact set match)", () => {
|
|
249
|
+
// Alerts with [A,B] and [A,C] must NOT group together under exact set match.
|
|
250
|
+
const keyAB: string = `alertLabels:${["lbl-a", "lbl-b"].sort().join(",")}`;
|
|
251
|
+
const keyAC: string = `alertLabels:${["lbl-a", "lbl-c"].sort().join(",")}`;
|
|
252
|
+
|
|
253
|
+
expect(keyAB).not.toBe(keyAC);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("should emit empty label key when alert has no labels", () => {
|
|
257
|
+
// Alerts with no labels should produce the same (empty) label key.
|
|
258
|
+
const labelIds: string[] = [];
|
|
259
|
+
const key: string = `alertLabels:${[...labelIds].sort().join(",")}`;
|
|
260
|
+
|
|
261
|
+
expect(key).toBe("alertLabels:");
|
|
262
|
+
});
|
|
235
263
|
});
|
|
236
264
|
|
|
237
265
|
describe("Time Window Configuration", () => {
|
|
@@ -143,14 +143,28 @@ describe("AlertGroupingRule Model", () => {
|
|
|
143
143
|
expect(rule.groupByAlertTitle).toBe(true);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("should set and get groupByAlertLabels correctly", () => {
|
|
147
|
+
rule.groupByAlertLabels = true;
|
|
148
|
+
expect(rule.groupByAlertLabels).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("should set and get groupByMonitorLabels correctly", () => {
|
|
152
|
+
rule.groupByMonitorLabels = true;
|
|
153
|
+
expect(rule.groupByMonitorLabels).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
146
156
|
test("should handle all groupBy options as false", () => {
|
|
147
157
|
rule.groupByMonitor = false;
|
|
148
158
|
rule.groupBySeverity = false;
|
|
149
159
|
rule.groupByAlertTitle = false;
|
|
160
|
+
rule.groupByAlertLabels = false;
|
|
161
|
+
rule.groupByMonitorLabels = false;
|
|
150
162
|
|
|
151
163
|
expect(rule.groupByMonitor).toBe(false);
|
|
152
164
|
expect(rule.groupBySeverity).toBe(false);
|
|
153
165
|
expect(rule.groupByAlertTitle).toBe(false);
|
|
166
|
+
expect(rule.groupByAlertLabels).toBe(false);
|
|
167
|
+
expect(rule.groupByMonitorLabels).toBe(false);
|
|
154
168
|
});
|
|
155
169
|
|
|
156
170
|
test("should handle combination of groupBy options", () => {
|
|
@@ -52,6 +52,39 @@ import MonitorStepDockerMonitor, {
|
|
|
52
52
|
} from "./MonitorStepDockerMonitor";
|
|
53
53
|
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
|
|
54
54
|
|
|
55
|
+
/*
|
|
56
|
+
* Caps and defaults for per-step request timeout and retry settings.
|
|
57
|
+
* Users may lower these via the UI; values higher than the cap are clamped.
|
|
58
|
+
*/
|
|
59
|
+
export const MAX_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000; // 60 seconds
|
|
60
|
+
export const DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000;
|
|
61
|
+
export const MAX_MONITOR_RETRY_COUNT: number = 3;
|
|
62
|
+
export const DEFAULT_MONITOR_RETRY_COUNT: number = 3;
|
|
63
|
+
|
|
64
|
+
export const clampMonitorRequestTimeoutInMs: (value: number) => number = (
|
|
65
|
+
value: number,
|
|
66
|
+
): number => {
|
|
67
|
+
if (!value || value <= 0) {
|
|
68
|
+
return DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS;
|
|
69
|
+
}
|
|
70
|
+
if (value > MAX_MONITOR_REQUEST_TIMEOUT_IN_MS) {
|
|
71
|
+
return MAX_MONITOR_REQUEST_TIMEOUT_IN_MS;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const clampMonitorRetryCount: (value: number) => number = (
|
|
77
|
+
value: number,
|
|
78
|
+
): number => {
|
|
79
|
+
if (value === undefined || value === null || isNaN(value) || value < 0) {
|
|
80
|
+
return DEFAULT_MONITOR_RETRY_COUNT;
|
|
81
|
+
}
|
|
82
|
+
if (value > MAX_MONITOR_RETRY_COUNT) {
|
|
83
|
+
return MAX_MONITOR_RETRY_COUNT;
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
};
|
|
87
|
+
|
|
55
88
|
export interface MonitorStepType {
|
|
56
89
|
id: string;
|
|
57
90
|
monitorDestination?: URL | IP | Hostname | undefined;
|
|
@@ -88,6 +121,19 @@ export interface MonitorStepType {
|
|
|
88
121
|
// retry count for synthetic monitors - number of times to retry on error
|
|
89
122
|
retryCountOnError?: number | undefined;
|
|
90
123
|
|
|
124
|
+
/*
|
|
125
|
+
* Per-step request timeout in milliseconds for probe-based monitors
|
|
126
|
+
* (Website, API, Ping, IP, Port, SSLCertificate). Defaults to and is
|
|
127
|
+
* capped at 60000 ms (60 seconds).
|
|
128
|
+
*/
|
|
129
|
+
requestTimeoutInMs?: number | undefined;
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* Per-step retry count for probe-based monitors when a check fails.
|
|
133
|
+
* Defaults to and is capped at 3.
|
|
134
|
+
*/
|
|
135
|
+
retryCount?: number | undefined;
|
|
136
|
+
|
|
91
137
|
// Log monitor type.
|
|
92
138
|
logMonitor?: MonitorStepLogMonitor | undefined;
|
|
93
139
|
|
|
@@ -148,6 +194,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
148
194
|
screenSizeTypes: undefined,
|
|
149
195
|
browserTypes: undefined,
|
|
150
196
|
retryCountOnError: undefined,
|
|
197
|
+
requestTimeoutInMs: undefined,
|
|
198
|
+
retryCount: undefined,
|
|
151
199
|
logMonitor: undefined,
|
|
152
200
|
traceMonitor: undefined,
|
|
153
201
|
metricMonitor: undefined,
|
|
@@ -190,6 +238,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
190
238
|
screenSizeTypes: undefined,
|
|
191
239
|
browserTypes: undefined,
|
|
192
240
|
retryCountOnError: undefined,
|
|
241
|
+
requestTimeoutInMs: undefined,
|
|
242
|
+
retryCount: undefined,
|
|
193
243
|
logMonitor: undefined,
|
|
194
244
|
traceMonitor: undefined,
|
|
195
245
|
metricMonitor: undefined,
|
|
@@ -294,6 +344,27 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
294
344
|
return this;
|
|
295
345
|
}
|
|
296
346
|
|
|
347
|
+
public setRequestTimeoutInMs(
|
|
348
|
+
requestTimeoutInMs: number | undefined,
|
|
349
|
+
): MonitorStep {
|
|
350
|
+
if (requestTimeoutInMs === undefined) {
|
|
351
|
+
this.data!.requestTimeoutInMs = undefined;
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
this.data!.requestTimeoutInMs =
|
|
355
|
+
clampMonitorRequestTimeoutInMs(requestTimeoutInMs);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public setRetryCount(retryCount: number | undefined): MonitorStep {
|
|
360
|
+
if (retryCount === undefined) {
|
|
361
|
+
this.data!.retryCount = undefined;
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
this.data!.retryCount = clampMonitorRetryCount(retryCount);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
297
368
|
public setLogMonitor(logMonitor: MonitorStepLogMonitor): MonitorStep {
|
|
298
369
|
this.data!.logMonitor = logMonitor;
|
|
299
370
|
return this;
|
|
@@ -400,6 +471,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
400
471
|
screenSizeTypes: undefined,
|
|
401
472
|
browserTypes: undefined,
|
|
402
473
|
retryCountOnError: undefined,
|
|
474
|
+
requestTimeoutInMs: undefined,
|
|
475
|
+
retryCount: undefined,
|
|
403
476
|
logMonitor: undefined,
|
|
404
477
|
exceptionMonitor: undefined,
|
|
405
478
|
kubernetesMonitor: undefined,
|
|
@@ -597,6 +670,11 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
597
670
|
screenSizeTypes: this.data.screenSizeTypes || undefined,
|
|
598
671
|
browserTypes: this.data.browserTypes || undefined,
|
|
599
672
|
retryCountOnError: this.data.retryCountOnError || undefined,
|
|
673
|
+
requestTimeoutInMs: this.data.requestTimeoutInMs || undefined,
|
|
674
|
+
retryCount:
|
|
675
|
+
this.data.retryCount === undefined
|
|
676
|
+
? undefined
|
|
677
|
+
: this.data.retryCount,
|
|
600
678
|
logMonitor: this.data.logMonitor
|
|
601
679
|
? MonitorStepLogMonitorUtil.toJSON(
|
|
602
680
|
this.data.logMonitor || MonitorStepLogMonitorUtil.getDefault(),
|
|
@@ -745,6 +823,11 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
745
823
|
(json["screenSizeTypes"] as Array<ScreenSizeType>) || undefined,
|
|
746
824
|
browserTypes: (json["browserTypes"] as Array<BrowserType>) || undefined,
|
|
747
825
|
retryCountOnError: (json["retryCountOnError"] as number) || undefined,
|
|
826
|
+
requestTimeoutInMs: (json["requestTimeoutInMs"] as number) || undefined,
|
|
827
|
+
retryCount:
|
|
828
|
+
json["retryCount"] === undefined || json["retryCount"] === null
|
|
829
|
+
? undefined
|
|
830
|
+
: (json["retryCount"] as number),
|
|
748
831
|
logMonitor: json["logMonitor"]
|
|
749
832
|
? (json["logMonitor"] as JSONObject)
|
|
750
833
|
: undefined,
|
|
@@ -806,6 +889,8 @@ export default class MonitorStep extends DatabaseProperty {
|
|
|
806
889
|
screenSizeTypes: Zod.any().optional(),
|
|
807
890
|
browserTypes: Zod.any().optional(),
|
|
808
891
|
retryCountOnError: Zod.number().optional(),
|
|
892
|
+
requestTimeoutInMs: Zod.number().optional(),
|
|
893
|
+
retryCount: Zod.number().optional(),
|
|
809
894
|
logMonitor: Zod.any().optional(),
|
|
810
895
|
traceMonitor: Zod.any().optional(),
|
|
811
896
|
metricMonitor: Zod.any().optional(),
|
|
@@ -75,7 +75,11 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
75
75
|
<div className={className}>
|
|
76
76
|
<div>
|
|
77
77
|
<div
|
|
78
|
-
className={`flex justify-between
|
|
78
|
+
className={`flex justify-between ${
|
|
79
|
+
props.description ? "items-start" : "items-center"
|
|
80
|
+
} gap-3 cursor-pointer group/accordion-header rounded-lg -mx-2 px-2 py-2 transition-colors ${
|
|
81
|
+
isOpen ? "" : "hover:bg-gray-50/80"
|
|
82
|
+
}`}
|
|
79
83
|
role="button"
|
|
80
84
|
tabIndex={0}
|
|
81
85
|
aria-expanded={isOpen}
|
|
@@ -85,43 +89,53 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
85
89
|
}}
|
|
86
90
|
onKeyDown={handleKeyDown}
|
|
87
91
|
>
|
|
88
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
className={`flex ${
|
|
94
|
+
props.description ? "items-start" : "items-center"
|
|
95
|
+
} min-w-0 flex-1`}
|
|
96
|
+
>
|
|
89
97
|
{props.title && (
|
|
90
|
-
<div
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
<div
|
|
99
|
+
className={`flex-shrink-0 ${
|
|
100
|
+
props.description ? "mt-0.5" : ""
|
|
101
|
+
} flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 ${
|
|
102
|
+
isOpen
|
|
103
|
+
? "bg-gray-900/5 text-gray-700"
|
|
104
|
+
: "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
|
|
105
|
+
}`}
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
>
|
|
108
|
+
<Icon
|
|
109
|
+
className={`h-3.5 w-3.5 transition-transform duration-200 ease-out ${
|
|
110
|
+
isOpen ? "rotate-90" : ""
|
|
111
|
+
}`}
|
|
112
|
+
icon={IconProp.ChevronRight}
|
|
113
|
+
thick={ThickProp.Thick}
|
|
114
|
+
/>
|
|
105
115
|
</div>
|
|
106
116
|
)}
|
|
107
117
|
{props.title && (
|
|
108
118
|
<div
|
|
109
|
-
className={`ml-
|
|
119
|
+
className={`ml-2.5 min-w-0 flex-1 ${
|
|
110
120
|
props.onClick ? "cursor-pointer" : ""
|
|
111
121
|
}`}
|
|
112
122
|
>
|
|
113
|
-
<div
|
|
114
|
-
{props.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{props.description && (
|
|
118
|
-
<MarkdownViewer text={props.description || ""} />
|
|
119
|
-
)}
|
|
123
|
+
<div
|
|
124
|
+
className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
|
|
125
|
+
>
|
|
126
|
+
{props.title}
|
|
120
127
|
</div>
|
|
128
|
+
{props.description && (
|
|
129
|
+
<div className="mt-1 text-sm text-gray-500 leading-relaxed">
|
|
130
|
+
<MarkdownViewer text={props.description} />
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
121
133
|
</div>
|
|
122
134
|
)}
|
|
123
135
|
</div>
|
|
124
|
-
{!isOpen &&
|
|
136
|
+
{!isOpen && props.rightElement && (
|
|
137
|
+
<div className="flex-shrink-0">{props.rightElement}</div>
|
|
138
|
+
)}
|
|
125
139
|
</div>
|
|
126
140
|
{isOpen && (
|
|
127
141
|
<div
|