@oneuptime/common 10.5.3 → 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/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.ts +37 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertGroupingEngineService.ts +83 -0
- package/Server/Services/IncidentGroupingEngineService.ts +99 -0
- package/Tests/Server/Services/AlertGroupingEngineService.test.ts +28 -0
- package/Tests/Server/Services/AlertGroupingRuleService.test.ts +14 -0
- package/UI/Components/Accordion/Accordion.tsx +12 -4
- 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/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 +2 -0
- 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/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/UI/Components/Accordion/Accordion.js +4 -4
- package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
- package/package.json +1 -1
|
@@ -811,6 +811,82 @@ export default class AlertGroupingRule extends BaseModel {
|
|
|
811
811
|
})
|
|
812
812
|
public groupByAlertTitle?: boolean = undefined;
|
|
813
813
|
|
|
814
|
+
@ColumnAccessControl({
|
|
815
|
+
create: [
|
|
816
|
+
Permission.ProjectOwner,
|
|
817
|
+
Permission.ProjectAdmin,
|
|
818
|
+
Permission.CreateAlertGroupingRule,
|
|
819
|
+
],
|
|
820
|
+
read: [
|
|
821
|
+
Permission.ProjectOwner,
|
|
822
|
+
Permission.ProjectAdmin,
|
|
823
|
+
Permission.ProjectMember,
|
|
824
|
+
Permission.Viewer,
|
|
825
|
+
Permission.AlertAdmin,
|
|
826
|
+
Permission.AlertMember,
|
|
827
|
+
Permission.AlertViewer,
|
|
828
|
+
Permission.ReadAlertGroupingRule,
|
|
829
|
+
],
|
|
830
|
+
update: [
|
|
831
|
+
Permission.ProjectOwner,
|
|
832
|
+
Permission.ProjectAdmin,
|
|
833
|
+
Permission.EditAlertGroupingRule,
|
|
834
|
+
],
|
|
835
|
+
})
|
|
836
|
+
@TableColumn({
|
|
837
|
+
required: false,
|
|
838
|
+
type: TableColumnType.Boolean,
|
|
839
|
+
title: "Group By Alert Labels",
|
|
840
|
+
description:
|
|
841
|
+
"When enabled, alerts with different sets of labels will be grouped into separate episodes (exact set match). When disabled, alert labels are ignored for grouping.",
|
|
842
|
+
defaultValue: false,
|
|
843
|
+
isDefaultValueColumn: true,
|
|
844
|
+
})
|
|
845
|
+
@Column({
|
|
846
|
+
type: ColumnType.Boolean,
|
|
847
|
+
nullable: false,
|
|
848
|
+
default: false,
|
|
849
|
+
})
|
|
850
|
+
public groupByAlertLabels?: boolean = undefined;
|
|
851
|
+
|
|
852
|
+
@ColumnAccessControl({
|
|
853
|
+
create: [
|
|
854
|
+
Permission.ProjectOwner,
|
|
855
|
+
Permission.ProjectAdmin,
|
|
856
|
+
Permission.CreateAlertGroupingRule,
|
|
857
|
+
],
|
|
858
|
+
read: [
|
|
859
|
+
Permission.ProjectOwner,
|
|
860
|
+
Permission.ProjectAdmin,
|
|
861
|
+
Permission.ProjectMember,
|
|
862
|
+
Permission.Viewer,
|
|
863
|
+
Permission.AlertAdmin,
|
|
864
|
+
Permission.AlertMember,
|
|
865
|
+
Permission.AlertViewer,
|
|
866
|
+
Permission.ReadAlertGroupingRule,
|
|
867
|
+
],
|
|
868
|
+
update: [
|
|
869
|
+
Permission.ProjectOwner,
|
|
870
|
+
Permission.ProjectAdmin,
|
|
871
|
+
Permission.EditAlertGroupingRule,
|
|
872
|
+
],
|
|
873
|
+
})
|
|
874
|
+
@TableColumn({
|
|
875
|
+
required: false,
|
|
876
|
+
type: TableColumnType.Boolean,
|
|
877
|
+
title: "Group By Monitor Labels",
|
|
878
|
+
description:
|
|
879
|
+
"When enabled, alerts whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
|
|
880
|
+
defaultValue: false,
|
|
881
|
+
isDefaultValueColumn: true,
|
|
882
|
+
})
|
|
883
|
+
@Column({
|
|
884
|
+
type: ColumnType.Boolean,
|
|
885
|
+
nullable: false,
|
|
886
|
+
default: false,
|
|
887
|
+
})
|
|
888
|
+
public groupByMonitorLabels?: boolean = undefined;
|
|
889
|
+
|
|
814
890
|
@ColumnAccessControl({
|
|
815
891
|
create: [
|
|
816
892
|
Permission.ProjectOwner,
|
|
@@ -815,6 +815,82 @@ export default class IncidentGroupingRule extends BaseModel {
|
|
|
815
815
|
})
|
|
816
816
|
public groupByIncidentTitle?: boolean = undefined;
|
|
817
817
|
|
|
818
|
+
@ColumnAccessControl({
|
|
819
|
+
create: [
|
|
820
|
+
Permission.ProjectOwner,
|
|
821
|
+
Permission.ProjectAdmin,
|
|
822
|
+
Permission.CreateIncidentGroupingRule,
|
|
823
|
+
],
|
|
824
|
+
read: [
|
|
825
|
+
Permission.ProjectOwner,
|
|
826
|
+
Permission.ProjectAdmin,
|
|
827
|
+
Permission.ProjectMember,
|
|
828
|
+
Permission.Viewer,
|
|
829
|
+
Permission.IncidentAdmin,
|
|
830
|
+
Permission.IncidentMember,
|
|
831
|
+
Permission.IncidentViewer,
|
|
832
|
+
Permission.ReadIncidentGroupingRule,
|
|
833
|
+
],
|
|
834
|
+
update: [
|
|
835
|
+
Permission.ProjectOwner,
|
|
836
|
+
Permission.ProjectAdmin,
|
|
837
|
+
Permission.EditIncidentGroupingRule,
|
|
838
|
+
],
|
|
839
|
+
})
|
|
840
|
+
@TableColumn({
|
|
841
|
+
required: false,
|
|
842
|
+
type: TableColumnType.Boolean,
|
|
843
|
+
title: "Group By Incident Labels",
|
|
844
|
+
description:
|
|
845
|
+
"When enabled, incidents with different sets of labels will be grouped into separate episodes (exact set match). When disabled, incident labels are ignored for grouping.",
|
|
846
|
+
defaultValue: false,
|
|
847
|
+
isDefaultValueColumn: true,
|
|
848
|
+
})
|
|
849
|
+
@Column({
|
|
850
|
+
type: ColumnType.Boolean,
|
|
851
|
+
nullable: false,
|
|
852
|
+
default: false,
|
|
853
|
+
})
|
|
854
|
+
public groupByIncidentLabels?: boolean = undefined;
|
|
855
|
+
|
|
856
|
+
@ColumnAccessControl({
|
|
857
|
+
create: [
|
|
858
|
+
Permission.ProjectOwner,
|
|
859
|
+
Permission.ProjectAdmin,
|
|
860
|
+
Permission.CreateIncidentGroupingRule,
|
|
861
|
+
],
|
|
862
|
+
read: [
|
|
863
|
+
Permission.ProjectOwner,
|
|
864
|
+
Permission.ProjectAdmin,
|
|
865
|
+
Permission.ProjectMember,
|
|
866
|
+
Permission.Viewer,
|
|
867
|
+
Permission.IncidentAdmin,
|
|
868
|
+
Permission.IncidentMember,
|
|
869
|
+
Permission.IncidentViewer,
|
|
870
|
+
Permission.ReadIncidentGroupingRule,
|
|
871
|
+
],
|
|
872
|
+
update: [
|
|
873
|
+
Permission.ProjectOwner,
|
|
874
|
+
Permission.ProjectAdmin,
|
|
875
|
+
Permission.EditIncidentGroupingRule,
|
|
876
|
+
],
|
|
877
|
+
})
|
|
878
|
+
@TableColumn({
|
|
879
|
+
required: false,
|
|
880
|
+
type: TableColumnType.Boolean,
|
|
881
|
+
title: "Group By Monitor Labels",
|
|
882
|
+
description:
|
|
883
|
+
"When enabled, incidents whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
|
|
884
|
+
defaultValue: false,
|
|
885
|
+
isDefaultValueColumn: true,
|
|
886
|
+
})
|
|
887
|
+
@Column({
|
|
888
|
+
type: ColumnType.Boolean,
|
|
889
|
+
nullable: false,
|
|
890
|
+
default: false,
|
|
891
|
+
})
|
|
892
|
+
public groupByMonitorLabels?: boolean = undefined;
|
|
893
|
+
|
|
818
894
|
@ColumnAccessControl({
|
|
819
895
|
create: [
|
|
820
896
|
Permission.ProjectOwner,
|
|
@@ -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
|
+
}
|
|
@@ -356,6 +356,7 @@ import { ExpandOwnerRuleInheritFlags1779823516881 } from "./1779823516881-Expand
|
|
|
356
356
|
import { RenameStatusPageZhToZhCN1779827700000 } from "./1779827700000-RenameStatusPageZhToZhCN";
|
|
357
357
|
import { MigrationName1779879993421 } from "./1779879993421-MigrationName";
|
|
358
358
|
import { MigrationName1779882573463 } from "./1779882573463-MigrationName";
|
|
359
|
+
import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-AddLabelGroupByToGroupingRules";
|
|
359
360
|
|
|
360
361
|
export default [
|
|
361
362
|
InitialMigration,
|
|
@@ -716,4 +717,5 @@ export default [
|
|
|
716
717
|
RenameStatusPageZhToZhCN1779827700000,
|
|
717
718
|
MigrationName1779879993421,
|
|
718
719
|
MigrationName1779882573463,
|
|
720
|
+
AddLabelGroupByToGroupingRules1779971548393,
|
|
719
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
|
|
@@ -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", () => {
|
|
@@ -75,7 +75,9 @@ 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 ${
|
|
79
81
|
isOpen ? "" : "hover:bg-gray-50/80"
|
|
80
82
|
}`}
|
|
81
83
|
role="button"
|
|
@@ -87,10 +89,16 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
87
89
|
}}
|
|
88
90
|
onKeyDown={handleKeyDown}
|
|
89
91
|
>
|
|
90
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
className={`flex ${
|
|
94
|
+
props.description ? "items-start" : "items-center"
|
|
95
|
+
} min-w-0 flex-1`}
|
|
96
|
+
>
|
|
91
97
|
{props.title && (
|
|
92
98
|
<div
|
|
93
|
-
className={`flex-shrink-0
|
|
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 ${
|
|
94
102
|
isOpen
|
|
95
103
|
? "bg-gray-900/5 text-gray-700"
|
|
96
104
|
: "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
|
|
@@ -126,7 +134,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
|
|
|
126
134
|
)}
|
|
127
135
|
</div>
|
|
128
136
|
{!isOpen && props.rightElement && (
|
|
129
|
-
<div className="flex-shrink-0
|
|
137
|
+
<div className="flex-shrink-0">{props.rightElement}</div>
|
|
130
138
|
)}
|
|
131
139
|
</div>
|
|
132
140
|
{isOpen && (
|
|
@@ -54,6 +54,8 @@ let AlertGroupingRule = class AlertGroupingRule extends BaseModel {
|
|
|
54
54
|
this.groupByMonitor = undefined;
|
|
55
55
|
this.groupBySeverity = undefined;
|
|
56
56
|
this.groupByAlertTitle = undefined;
|
|
57
|
+
this.groupByAlertLabels = undefined;
|
|
58
|
+
this.groupByMonitorLabels = undefined;
|
|
57
59
|
this.enableTimeWindow = undefined;
|
|
58
60
|
this.timeWindowMinutes = undefined;
|
|
59
61
|
this.groupByFields = undefined;
|
|
@@ -780,6 +782,82 @@ __decorate([
|
|
|
780
782
|
}),
|
|
781
783
|
__metadata("design:type", Boolean)
|
|
782
784
|
], AlertGroupingRule.prototype, "groupByAlertTitle", void 0);
|
|
785
|
+
__decorate([
|
|
786
|
+
ColumnAccessControl({
|
|
787
|
+
create: [
|
|
788
|
+
Permission.ProjectOwner,
|
|
789
|
+
Permission.ProjectAdmin,
|
|
790
|
+
Permission.CreateAlertGroupingRule,
|
|
791
|
+
],
|
|
792
|
+
read: [
|
|
793
|
+
Permission.ProjectOwner,
|
|
794
|
+
Permission.ProjectAdmin,
|
|
795
|
+
Permission.ProjectMember,
|
|
796
|
+
Permission.Viewer,
|
|
797
|
+
Permission.AlertAdmin,
|
|
798
|
+
Permission.AlertMember,
|
|
799
|
+
Permission.AlertViewer,
|
|
800
|
+
Permission.ReadAlertGroupingRule,
|
|
801
|
+
],
|
|
802
|
+
update: [
|
|
803
|
+
Permission.ProjectOwner,
|
|
804
|
+
Permission.ProjectAdmin,
|
|
805
|
+
Permission.EditAlertGroupingRule,
|
|
806
|
+
],
|
|
807
|
+
}),
|
|
808
|
+
TableColumn({
|
|
809
|
+
required: false,
|
|
810
|
+
type: TableColumnType.Boolean,
|
|
811
|
+
title: "Group By Alert Labels",
|
|
812
|
+
description: "When enabled, alerts with different sets of labels will be grouped into separate episodes (exact set match). When disabled, alert labels are ignored for grouping.",
|
|
813
|
+
defaultValue: false,
|
|
814
|
+
isDefaultValueColumn: true,
|
|
815
|
+
}),
|
|
816
|
+
Column({
|
|
817
|
+
type: ColumnType.Boolean,
|
|
818
|
+
nullable: false,
|
|
819
|
+
default: false,
|
|
820
|
+
}),
|
|
821
|
+
__metadata("design:type", Boolean)
|
|
822
|
+
], AlertGroupingRule.prototype, "groupByAlertLabels", void 0);
|
|
823
|
+
__decorate([
|
|
824
|
+
ColumnAccessControl({
|
|
825
|
+
create: [
|
|
826
|
+
Permission.ProjectOwner,
|
|
827
|
+
Permission.ProjectAdmin,
|
|
828
|
+
Permission.CreateAlertGroupingRule,
|
|
829
|
+
],
|
|
830
|
+
read: [
|
|
831
|
+
Permission.ProjectOwner,
|
|
832
|
+
Permission.ProjectAdmin,
|
|
833
|
+
Permission.ProjectMember,
|
|
834
|
+
Permission.Viewer,
|
|
835
|
+
Permission.AlertAdmin,
|
|
836
|
+
Permission.AlertMember,
|
|
837
|
+
Permission.AlertViewer,
|
|
838
|
+
Permission.ReadAlertGroupingRule,
|
|
839
|
+
],
|
|
840
|
+
update: [
|
|
841
|
+
Permission.ProjectOwner,
|
|
842
|
+
Permission.ProjectAdmin,
|
|
843
|
+
Permission.EditAlertGroupingRule,
|
|
844
|
+
],
|
|
845
|
+
}),
|
|
846
|
+
TableColumn({
|
|
847
|
+
required: false,
|
|
848
|
+
type: TableColumnType.Boolean,
|
|
849
|
+
title: "Group By Monitor Labels",
|
|
850
|
+
description: "When enabled, alerts whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
|
|
851
|
+
defaultValue: false,
|
|
852
|
+
isDefaultValueColumn: true,
|
|
853
|
+
}),
|
|
854
|
+
Column({
|
|
855
|
+
type: ColumnType.Boolean,
|
|
856
|
+
nullable: false,
|
|
857
|
+
default: false,
|
|
858
|
+
}),
|
|
859
|
+
__metadata("design:type", Boolean)
|
|
860
|
+
], AlertGroupingRule.prototype, "groupByMonitorLabels", void 0);
|
|
783
861
|
__decorate([
|
|
784
862
|
ColumnAccessControl({
|
|
785
863
|
create: [
|