@oneuptime/common 10.5.3 → 10.5.6

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 (86) hide show
  1. package/Models/DatabaseModels/AlertEpisodeMember.ts +29 -0
  2. package/Models/DatabaseModels/AlertGroupingRule.ts +76 -0
  3. package/Models/DatabaseModels/GlobalConfig.ts +28 -0
  4. package/Models/DatabaseModels/IncidentEpisodeMember.ts +29 -0
  5. package/Models/DatabaseModels/IncidentGroupingRule.ts +76 -0
  6. package/Models/DatabaseModels/ProjectSmtpConfig.ts +62 -6
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.ts +37 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1779975064262-AddTransportTypeToProjectSmtpConfig.ts +37 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1779976190561-AddSmtpTransportTypeToGlobalConfig.ts +19 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1779980428744-MigrationName.ts +23 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  12. package/Server/Services/AlertEpisodeMemberService.ts +23 -0
  13. package/Server/Services/AlertGroupingEngineService.ts +83 -0
  14. package/Server/Services/IncidentEpisodeMemberService.ts +23 -0
  15. package/Server/Services/IncidentGroupingEngineService.ts +99 -0
  16. package/Server/Services/MailService.ts +15 -3
  17. package/Server/Services/ProjectSmtpConfigService.ts +30 -10
  18. package/Server/Services/StatusPagePrivateUserService.ts +1 -0
  19. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  20. package/Server/Services/UserNotificationSettingService.ts +12 -0
  21. package/Server/Utils/WhatsAppTemplateUtil.ts +4 -0
  22. package/Tests/Server/Services/AlertGroupingEngineService.test.ts +28 -0
  23. package/Tests/Server/Services/AlertGroupingRuleService.test.ts +14 -0
  24. package/Types/Email/EmailServer.ts +22 -4
  25. package/Types/Email/EmailTemplateType.ts +2 -0
  26. package/Types/Email/MailTransportType.ts +18 -0
  27. package/Types/NotificationSetting/NotificationSettingEventType.ts +2 -0
  28. package/Types/WhatsApp/WhatsAppTemplates.ts +10 -0
  29. package/UI/Components/Accordion/Accordion.tsx +12 -4
  30. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +30 -0
  31. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  32. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +78 -0
  33. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -1
  34. package/build/dist/Models/DatabaseModels/GlobalConfig.js +28 -0
  35. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  36. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +30 -0
  37. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  38. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +78 -0
  39. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  40. package/build/dist/Models/DatabaseModels/ProjectSmtpConfig.js +59 -6
  41. package/build/dist/Models/DatabaseModels/ProjectSmtpConfig.js.map +1 -1
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js +18 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js.map +1 -0
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779975064262-AddTransportTypeToProjectSmtpConfig.js +18 -0
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779975064262-AddTransportTypeToProjectSmtpConfig.js.map +1 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779976190561-AddSmtpTransportTypeToGlobalConfig.js +12 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779976190561-AddSmtpTransportTypeToGlobalConfig.js.map +1 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779980428744-MigrationName.js +14 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779980428744-MigrationName.js.map +1 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  51. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  52. package/build/dist/Server/Services/AlertEpisodeMemberService.js +20 -0
  53. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  54. package/build/dist/Server/Services/AlertGroupingEngineService.js +85 -0
  55. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  56. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +20 -0
  57. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  58. package/build/dist/Server/Services/IncidentGroupingEngineService.js +95 -0
  59. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  60. package/build/dist/Server/Services/MailService.js +15 -3
  61. package/build/dist/Server/Services/MailService.js.map +1 -1
  62. package/build/dist/Server/Services/ProjectSmtpConfigService.js +24 -8
  63. package/build/dist/Server/Services/ProjectSmtpConfigService.js.map +1 -1
  64. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
  65. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  66. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  67. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  68. package/build/dist/Server/Services/UserNotificationSettingService.js +2 -0
  69. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  70. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +2 -0
  71. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  72. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +21 -0
  73. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -1
  74. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +12 -0
  75. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -1
  76. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  77. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  78. package/build/dist/Types/Email/MailTransportType.js +19 -0
  79. package/build/dist/Types/Email/MailTransportType.js.map +1 -0
  80. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +2 -0
  81. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  82. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +6 -0
  83. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  84. package/build/dist/UI/Components/Accordion/Accordion.js +4 -4
  85. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  86. package/package.json +1 -1
@@ -626,6 +626,35 @@ export default class AlertEpisodeMember extends BaseModel {
626
626
  })
627
627
  public createdByUserId?: ObjectID = undefined;
628
628
 
629
+ @ColumnAccessControl({
630
+ create: [],
631
+ read: [
632
+ Permission.ProjectOwner,
633
+ Permission.ProjectAdmin,
634
+ Permission.ProjectMember,
635
+ Permission.Viewer,
636
+ Permission.AlertAdmin,
637
+ Permission.AlertMember,
638
+ Permission.AlertViewer,
639
+ Permission.ReadAlertEpisodeMember,
640
+ ],
641
+ update: [],
642
+ })
643
+ @TableColumn({
644
+ isDefaultValueColumn: true,
645
+ required: true,
646
+ type: TableColumnType.Boolean,
647
+ title: "Is Owner Notified of Alert Added to Episode",
648
+ description:
649
+ "Has the owner been notified that this alert was added to the episode?",
650
+ })
651
+ @Column({
652
+ type: ColumnType.Boolean,
653
+ nullable: false,
654
+ default: false,
655
+ })
656
+ public isOwnerNotifiedOfAlertAdded?: boolean = undefined;
657
+
629
658
  @ColumnAccessControl({
630
659
  create: [],
631
660
  read: [
@@ -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,
@@ -10,6 +10,7 @@ import TableColumn from "../../Types/Database/TableColumn";
10
10
  import TableColumnType from "../../Types/Database/TableColumnType";
11
11
  import TableMetadata from "../../Types/Database/TableMetadata";
12
12
  import Email from "../../Types/Email";
13
+ import MailTransportType from "../../Types/Email/MailTransportType";
13
14
  import OAuthProviderType from "../../Types/Email/OAuthProviderType";
14
15
  import SMTPAuthenticationType from "../../Types/Email/SMTPAuthenticationType";
15
16
  import IconProp from "../../Types/Icon/IconProp";
@@ -208,6 +209,33 @@ export default class GlobalConfig extends GlobalConfigModel {
208
209
  })
209
210
  public smtpFromName?: string = undefined;
210
211
 
212
+ /*
213
+ * Transport selection for the global SMTP. Defaults to SMTP for backwards
214
+ * compatibility. 'Microsoft Graph' bypasses SMTP entirely and posts to
215
+ * https://graph.microsoft.com/v1.0/users/{from}/sendMail — useful for tenants
216
+ * that have SMTP AUTH disabled.
217
+ */
218
+
219
+ @ColumnAccessControl({
220
+ create: [],
221
+ read: [],
222
+ update: [],
223
+ })
224
+ @TableColumn({
225
+ type: TableColumnType.ShortText,
226
+ title: "SMTP Transport",
227
+ description:
228
+ "How OneUptime delivers mail using the global SMTP config. 'SMTP' uses the host/port. 'Microsoft Graph' sends via the Microsoft Graph REST API.",
229
+ defaultValue: MailTransportType.SMTP,
230
+ })
231
+ @Column({
232
+ type: ColumnType.ShortText,
233
+ length: ColumnLength.ShortText,
234
+ nullable: true,
235
+ default: MailTransportType.SMTP,
236
+ })
237
+ public smtpTransportType?: MailTransportType = undefined;
238
+
211
239
  // SMTP OAuth 2.0 Settings.
212
240
 
213
241
  @ColumnAccessControl({
@@ -627,6 +627,35 @@ export default class IncidentEpisodeMember extends BaseModel {
627
627
  })
628
628
  public createdByUserId?: ObjectID = undefined;
629
629
 
630
+ @ColumnAccessControl({
631
+ create: [],
632
+ read: [
633
+ Permission.ProjectOwner,
634
+ Permission.ProjectAdmin,
635
+ Permission.ProjectMember,
636
+ Permission.Viewer,
637
+ Permission.IncidentAdmin,
638
+ Permission.IncidentMember,
639
+ Permission.IncidentViewer,
640
+ Permission.ReadIncidentEpisodeMember,
641
+ ],
642
+ update: [],
643
+ })
644
+ @TableColumn({
645
+ isDefaultValueColumn: true,
646
+ required: true,
647
+ type: TableColumnType.Boolean,
648
+ title: "Is Owner Notified of Incident Added to Episode",
649
+ description:
650
+ "Has the owner been notified that this incident was added to the episode?",
651
+ })
652
+ @Column({
653
+ type: ColumnType.Boolean,
654
+ nullable: false,
655
+ default: false,
656
+ })
657
+ public isOwnerNotifiedOfIncidentAdded?: boolean = undefined;
658
+
630
659
  @ColumnAccessControl({
631
660
  create: [],
632
661
  read: [
@@ -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,
@@ -17,6 +17,7 @@ import TableMetadata from "../../Types/Database/TableMetadata";
17
17
  import TenantColumn from "../../Types/Database/TenantColumn";
18
18
  import UniqueColumnBy from "../../Types/Database/UniqueColumnBy";
19
19
  import Email from "../../Types/Email";
20
+ import MailTransportType from "../../Types/Email/MailTransportType";
20
21
  import OAuthProviderType from "../../Types/Email/OAuthProviderType";
21
22
  import SMTPAuthenticationType from "../../Types/Email/SMTPAuthenticationType";
22
23
  import IconProp from "../../Types/Icon/IconProp";
@@ -478,12 +479,15 @@ export default class ProjectSmtpConfig extends BaseModel {
478
479
  ],
479
480
  })
480
481
  @TableColumn({
481
- required: true,
482
+ required: false,
482
483
  type: TableColumnType.ShortText,
484
+ title: "Hostname",
485
+ description:
486
+ "SMTP server hostname. Required when Transport is SMTP. Not used by HTTP-API transports like Microsoft Graph.",
483
487
  example: "smtp.gmail.com",
484
488
  })
485
489
  @Column({
486
- nullable: false,
490
+ nullable: true,
487
491
  type: ColumnType.ShortText,
488
492
  length: ColumnLength.ShortText,
489
493
  transformer: Hostname.getDatabaseTransformer(),
@@ -513,12 +517,15 @@ export default class ProjectSmtpConfig extends BaseModel {
513
517
  ],
514
518
  })
515
519
  @TableColumn({
516
- required: true,
520
+ required: false,
517
521
  type: TableColumnType.Number,
522
+ title: "Port",
523
+ description:
524
+ "SMTP server port. Required when Transport is SMTP. Not used by HTTP-API transports.",
518
525
  example: 587,
519
526
  })
520
527
  @Column({
521
- nullable: false,
528
+ nullable: true,
522
529
  type: ColumnType.Number,
523
530
  transformer: Port.getDatabaseTransformer(),
524
531
  })
@@ -616,18 +623,67 @@ export default class ProjectSmtpConfig extends BaseModel {
616
623
  ],
617
624
  })
618
625
  @TableColumn({
619
- required: true,
626
+ required: false,
620
627
  type: TableColumnType.Boolean,
628
+ title: "Use SSL / TLS",
629
+ description:
630
+ "Enable secure SMTP connection. Used only for SMTP transport — HTTP-API transports always use HTTPS.",
621
631
  defaultValue: true,
622
632
  example: true,
623
633
  })
624
634
  @Column({
625
- nullable: false,
635
+ nullable: true,
626
636
  type: ColumnType.Boolean,
627
637
  default: true,
628
638
  })
629
639
  public secure?: boolean = undefined;
630
640
 
641
+ /*
642
+ * Transport type selects how mail is actually delivered.
643
+ * SMTP (default) uses the host/port above. HTTP-API transports (e.g. Microsoft
644
+ * Graph) ignore host/port and call the provider's REST API directly using the
645
+ * OAuth credentials below.
646
+ */
647
+
648
+ @ColumnAccessControl({
649
+ create: [
650
+ Permission.ProjectOwner,
651
+ Permission.ProjectAdmin,
652
+ Permission.CreateProjectSMTPConfig,
653
+ ],
654
+ read: [
655
+ Permission.ProjectOwner,
656
+ Permission.ProjectAdmin,
657
+ Permission.ProjectMember,
658
+ Permission.Viewer,
659
+ Permission.SettingsAdmin,
660
+ Permission.SettingsMember,
661
+ Permission.SettingsViewer,
662
+ Permission.ReadProjectSMTPConfig,
663
+ ],
664
+ update: [
665
+ Permission.ProjectOwner,
666
+ Permission.ProjectAdmin,
667
+ Permission.EditProjectSMTPConfig,
668
+ ],
669
+ })
670
+ @TableColumn({
671
+ required: true,
672
+ type: TableColumnType.ShortText,
673
+ title: "Transport",
674
+ description:
675
+ "How OneUptime delivers mail for this config. 'SMTP' uses the hostname/port. 'Microsoft Graph' sends via the Microsoft Graph REST API — use this when your Microsoft 365 tenant has SMTP AUTH disabled.",
676
+ defaultValue: MailTransportType.SMTP,
677
+ example: "SMTP",
678
+ })
679
+ @Column({
680
+ nullable: false,
681
+ type: ColumnType.ShortText,
682
+ length: ColumnLength.ShortText,
683
+ default: MailTransportType.SMTP,
684
+ })
685
+ public transportType?: MailTransportType = undefined;
686
+
631
687
  // OAuth 2.0 Configuration Fields
632
688
 
633
689
  @ColumnAccessControl({
@@ -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
+ }
@@ -0,0 +1,37 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddTransportTypeToProjectSmtpConfig1779975064262
4
+ implements MigrationInterface
5
+ {
6
+ name = "AddTransportTypeToProjectSmtpConfig1779975064262";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "ProjectSMTPConfig" ADD "transportType" character varying(100) NOT NULL DEFAULT 'SMTP'`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "hostname" DROP NOT NULL`,
14
+ );
15
+ await queryRunner.query(
16
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "port" DROP NOT NULL`,
17
+ );
18
+ await queryRunner.query(
19
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "secure" DROP NOT NULL`,
20
+ );
21
+ }
22
+
23
+ public async down(queryRunner: QueryRunner): Promise<void> {
24
+ await queryRunner.query(
25
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "secure" SET NOT NULL`,
26
+ );
27
+ await queryRunner.query(
28
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "port" SET NOT NULL`,
29
+ );
30
+ await queryRunner.query(
31
+ `ALTER TABLE "ProjectSMTPConfig" ALTER COLUMN "hostname" SET NOT NULL`,
32
+ );
33
+ await queryRunner.query(
34
+ `ALTER TABLE "ProjectSMTPConfig" DROP COLUMN "transportType"`,
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddSmtpTransportTypeToGlobalConfig1779976190561
4
+ implements MigrationInterface
5
+ {
6
+ name = "AddSmtpTransportTypeToGlobalConfig1779976190561";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "GlobalConfig" ADD "smtpTransportType" character varying(100) DEFAULT 'SMTP'`,
11
+ );
12
+ }
13
+
14
+ public async down(queryRunner: QueryRunner): Promise<void> {
15
+ await queryRunner.query(
16
+ `ALTER TABLE "GlobalConfig" DROP COLUMN "smtpTransportType"`,
17
+ );
18
+ }
19
+ }
@@ -0,0 +1,23 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1779980428744 implements MigrationInterface {
4
+ public name = "MigrationName1779980428744";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "AlertEpisodeMember" ADD "isOwnerNotifiedOfAlertAdded" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "IncidentEpisodeMember" ADD "isOwnerNotifiedOfIncidentAdded" boolean NOT NULL DEFAULT false`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "IncidentEpisodeMember" DROP COLUMN "isOwnerNotifiedOfIncidentAdded"`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "AlertEpisodeMember" DROP COLUMN "isOwnerNotifiedOfAlertAdded"`,
21
+ );
22
+ }
23
+ }
@@ -356,6 +356,10 @@ 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";
360
+ import { AddTransportTypeToProjectSmtpConfig1779975064262 } from "./1779975064262-AddTransportTypeToProjectSmtpConfig";
361
+ import { AddSmtpTransportTypeToGlobalConfig1779976190561 } from "./1779976190561-AddSmtpTransportTypeToGlobalConfig";
362
+ import { MigrationName1779980428744 } from "./1779980428744-MigrationName";
359
363
 
360
364
  export default [
361
365
  InitialMigration,
@@ -716,4 +720,8 @@ export default [
716
720
  RenameStatusPageZhToZhCN1779827700000,
717
721
  MigrationName1779879993421,
718
722
  MigrationName1779882573463,
723
+ AddLabelGroupByToGroupingRules1779971548393,
724
+ AddTransportTypeToProjectSmtpConfig1779975064262,
725
+ AddSmtpTransportTypeToGlobalConfig1779976190561,
726
+ MigrationName1779980428744,
719
727
  ];
@@ -4,6 +4,7 @@ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
4
4
  import DatabaseService from "./DatabaseService";
5
5
  import BadDataException from "../../Types/Exception/BadDataException";
6
6
  import ObjectID from "../../Types/ObjectID";
7
+ import PositiveNumber from "../../Types/PositiveNumber";
7
8
  import Model from "../../Models/DatabaseModels/AlertEpisodeMember";
8
9
  import Alert from "../../Models/DatabaseModels/Alert";
9
10
  import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
@@ -62,6 +63,28 @@ export class Service extends DatabaseService<Model> {
62
63
  createBy.data.addedAt = OneUptimeDate.getCurrentDate();
63
64
  }
64
65
 
66
+ /*
67
+ * If this is the very first alert in the episode (the founder), the
68
+ * "episode created" notification already covers it. Mark the member as
69
+ * "already notified" so the AlertAdded-to-episode cron skips it. This
70
+ * avoids double-notifying owners when an episode is born with its first
71
+ * alert.
72
+ */
73
+ if (createBy.data.isOwnerNotifiedOfAlertAdded === undefined) {
74
+ const existingMemberCount: PositiveNumber = await this.countBy({
75
+ query: {
76
+ alertEpisodeId: createBy.data.alertEpisodeId,
77
+ },
78
+ props: {
79
+ isRoot: true,
80
+ },
81
+ });
82
+
83
+ if (existingMemberCount.toNumber() === 0) {
84
+ createBy.data.isOwnerNotifiedOfAlertAdded = true;
85
+ }
86
+ }
87
+
65
88
  return { createBy, carryForward: null };
66
89
  }
67
90
 
@@ -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