@oneuptime/common 11.3.21 → 11.3.23

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 (151) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
  2. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  3. package/Models/AnalyticsModels/Log.ts +1 -0
  4. package/Models/AnalyticsModels/Metric.ts +1 -0
  5. package/Models/AnalyticsModels/MonitorLog.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +1 -0
  7. package/Models/DatabaseModels/Alert.ts +100 -0
  8. package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
  9. package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
  10. package/Models/DatabaseModels/AlertSeverity.ts +2 -0
  11. package/Models/DatabaseModels/AlertState.ts +2 -0
  12. package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
  13. package/Models/DatabaseModels/Incident.ts +100 -0
  14. package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
  15. package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
  16. package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
  17. package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
  18. package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
  19. package/Models/DatabaseModels/Index.ts +6 -0
  20. package/Models/DatabaseModels/Label.ts +2 -0
  21. package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
  22. package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
  23. package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
  24. package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
  25. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
  26. package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  32. package/Server/Services/AlertReminderRuleService.ts +253 -0
  33. package/Server/Services/AlertService.ts +106 -0
  34. package/Server/Services/IncidentReminderRuleService.ts +256 -0
  35. package/Server/Services/IncidentService.ts +143 -0
  36. package/Server/Services/Index.ts +6 -0
  37. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
  38. package/Server/Services/ScheduledMaintenanceService.ts +127 -0
  39. package/Server/Services/TelemetryExceptionService.ts +123 -0
  40. package/Server/Services/UserNotificationSettingService.ts +24 -0
  41. package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
  42. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
  43. package/Server/Utils/EventLoop.ts +28 -0
  44. package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
  45. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
  46. package/Types/Email/EmailTemplateType.ts +3 -0
  47. package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
  48. package/Types/Permission.ts +144 -0
  49. package/Types/Reminder/ReminderStopState.ts +6 -0
  50. package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
  51. package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
  52. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
  53. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  54. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  55. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  56. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  57. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  58. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  59. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  60. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  61. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  62. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/Alert.js +103 -0
  64. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
  66. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
  68. package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
  69. package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
  70. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  71. package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
  72. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  73. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
  74. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  75. package/build/dist/Models/DatabaseModels/Incident.js +103 -0
  76. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  77. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
  78. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  79. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
  80. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  81. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
  82. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
  83. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
  84. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  85. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
  86. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  87. package/build/dist/Models/DatabaseModels/Index.js +6 -0
  88. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  90. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
  92. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
  94. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
  96. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
  97. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
  98. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
  100. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
  102. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
  114. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
  115. package/build/dist/Server/Services/AlertService.js +87 -0
  116. package/build/dist/Server/Services/AlertService.js.map +1 -1
  117. package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
  118. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
  119. package/build/dist/Server/Services/IncidentService.js +116 -0
  120. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  121. package/build/dist/Server/Services/Index.js +6 -0
  122. package/build/dist/Server/Services/Index.js.map +1 -1
  123. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
  124. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
  125. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
  126. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  127. package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
  128. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  129. package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
  130. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  131. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  132. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  133. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
  134. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  135. package/build/dist/Server/Utils/EventLoop.js +29 -0
  136. package/build/dist/Server/Utils/EventLoop.js.map +1 -0
  137. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
  138. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  139. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  140. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  141. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  142. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  143. package/build/dist/Types/Permission.js +126 -0
  144. package/build/dist/Types/Permission.js.map +1 -1
  145. package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
  146. package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
  147. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
  148. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
  149. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
  150. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  151. package/package.json +1 -1
@@ -0,0 +1,253 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
4
+ import DatabaseService from "./DatabaseService";
5
+ import Model from "../../Models/DatabaseModels/AlertReminderRule";
6
+ import Alert from "../../Models/DatabaseModels/Alert";
7
+ import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
8
+ import AlertState from "../../Models/DatabaseModels/AlertState";
9
+ import Label from "../../Models/DatabaseModels/Label";
10
+ import AlertService from "./AlertService";
11
+ import AlertStateService from "./AlertStateService";
12
+ import QueryHelper from "../Types/Database/QueryHelper";
13
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
14
+ import ObjectID from "../../Types/ObjectID";
15
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
16
+ import logger, { LogAttributes } from "../Utils/Logger";
17
+ import { IsBillingEnabled } from "../EnvironmentConfig";
18
+
19
+ export class Service extends DatabaseService<Model> {
20
+ public constructor() {
21
+ super(Model);
22
+ if (IsBillingEnabled) {
23
+ this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
24
+ }
25
+ }
26
+
27
+ @CaptureSpan()
28
+ protected override async onBeforeCreate(
29
+ createBy: CreateBy<Model>,
30
+ ): Promise<OnCreate<Model>> {
31
+ // Auto-assign order on creation
32
+ if (!createBy.data.order) {
33
+ const highestOrderRule: Model | null = await this.findOneBy({
34
+ query: {
35
+ projectId: createBy.data.projectId!,
36
+ },
37
+ select: {
38
+ order: true,
39
+ },
40
+ sort: {
41
+ order: SortOrder.Descending,
42
+ },
43
+ props: {
44
+ isRoot: true,
45
+ },
46
+ });
47
+
48
+ createBy.data.order = (highestOrderRule?.order || 0) + 1;
49
+ }
50
+
51
+ return {
52
+ createBy,
53
+ carryForward: null,
54
+ };
55
+ }
56
+
57
+ @CaptureSpan()
58
+ protected override async onCreateSuccess(
59
+ _onCreate: OnCreate<Model>,
60
+ createdItem: Model,
61
+ ): Promise<Model> {
62
+ if (createdItem.projectId) {
63
+ await this.refreshSchedulesForOpenAlerts(createdItem.projectId);
64
+ }
65
+
66
+ return createdItem;
67
+ }
68
+
69
+ @CaptureSpan()
70
+ protected override async onUpdateSuccess(
71
+ onUpdate: OnUpdate<Model>,
72
+ _updatedItemIds: Array<ObjectID>,
73
+ ): Promise<OnUpdate<Model>> {
74
+ const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
75
+ | ObjectID
76
+ | undefined;
77
+
78
+ if (projectId) {
79
+ await this.refreshSchedulesForOpenAlerts(projectId);
80
+ }
81
+
82
+ return onUpdate;
83
+ }
84
+
85
+ @CaptureSpan()
86
+ protected override async onDeleteSuccess(
87
+ onDelete: OnDelete<Model>,
88
+ _itemIdsBeforeDelete: Array<ObjectID>,
89
+ ): Promise<OnDelete<Model>> {
90
+ const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
91
+ | ObjectID
92
+ | undefined;
93
+
94
+ if (projectId) {
95
+ await this.refreshSchedulesForOpenAlerts(projectId);
96
+ }
97
+
98
+ return onDelete;
99
+ }
100
+
101
+ @CaptureSpan()
102
+ public async refreshSchedulesForOpenAlerts(
103
+ projectId: ObjectID,
104
+ ): Promise<void> {
105
+ try {
106
+ const unresolvedStates: Array<AlertState> =
107
+ await AlertStateService.findBy({
108
+ query: {
109
+ projectId: projectId,
110
+ isResolvedState: false,
111
+ },
112
+ select: {
113
+ _id: true,
114
+ },
115
+ limit: LIMIT_PER_PROJECT,
116
+ skip: 0,
117
+ props: {
118
+ isRoot: true,
119
+ },
120
+ });
121
+
122
+ const unresolvedStateIds: Array<ObjectID> = unresolvedStates
123
+ .map((state: AlertState) => {
124
+ return state.id!;
125
+ })
126
+ .filter(Boolean);
127
+
128
+ if (unresolvedStateIds.length === 0) {
129
+ return;
130
+ }
131
+
132
+ const openAlerts: Array<Alert> = await AlertService.findBy({
133
+ query: {
134
+ projectId: projectId,
135
+ currentAlertStateId: QueryHelper.any(unresolvedStateIds),
136
+ },
137
+ select: {
138
+ _id: true,
139
+ },
140
+ limit: LIMIT_MAX,
141
+ skip: 0,
142
+ props: {
143
+ isRoot: true,
144
+ },
145
+ });
146
+
147
+ for (const alert of openAlerts) {
148
+ try {
149
+ await AlertService.refreshReminderSchedule({
150
+ alertId: alert.id!,
151
+ projectId: projectId,
152
+ });
153
+ } catch (error) {
154
+ logger.error(
155
+ `Failed to refresh reminder schedule for alert ${alert.id}: ${error}`,
156
+ { projectId: projectId?.toString() } as LogAttributes,
157
+ );
158
+ }
159
+ }
160
+ } catch (error) {
161
+ logger.error(
162
+ `Failed to refresh reminder schedules for open alerts: ${error}`,
163
+ { projectId: projectId?.toString() } as LogAttributes,
164
+ );
165
+ }
166
+ }
167
+
168
+ @CaptureSpan()
169
+ public async findMatchingRule(data: {
170
+ projectId: ObjectID;
171
+ alertSeverityId?: ObjectID | undefined;
172
+ labelIds?: Array<ObjectID> | undefined;
173
+ }): Promise<Model | null> {
174
+ // Get all enabled rules sorted by order. First matching rule wins.
175
+ const rules: Array<Model> = await this.findBy({
176
+ query: {
177
+ projectId: data.projectId,
178
+ isEnabled: true,
179
+ },
180
+ sort: {
181
+ order: SortOrder.Ascending,
182
+ },
183
+ select: {
184
+ _id: true,
185
+ name: true,
186
+ order: true,
187
+ reminderIntervalInMinutes: true,
188
+ stopRemindersOnState: true,
189
+ alertSeverities: {
190
+ _id: true,
191
+ },
192
+ labels: {
193
+ _id: true,
194
+ },
195
+ },
196
+ limit: 100,
197
+ skip: 0,
198
+ props: {
199
+ isRoot: true,
200
+ },
201
+ });
202
+
203
+ for (const rule of rules) {
204
+ if (rule.alertSeverities && rule.alertSeverities.length > 0) {
205
+ if (!data.alertSeverityId) {
206
+ continue;
207
+ }
208
+
209
+ const severityIds: Array<string> = rule.alertSeverities.map(
210
+ (severity: AlertSeverity) => {
211
+ return severity.id?.toString() || "";
212
+ },
213
+ );
214
+
215
+ if (!severityIds.includes(data.alertSeverityId.toString())) {
216
+ continue;
217
+ }
218
+ }
219
+
220
+ if (rule.labels && rule.labels.length > 0) {
221
+ if (!data.labelIds || data.labelIds.length === 0) {
222
+ continue;
223
+ }
224
+
225
+ const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
226
+ return label.id?.toString() || "";
227
+ });
228
+
229
+ const hasMatchingLabel: boolean = data.labelIds.some(
230
+ (labelId: ObjectID) => {
231
+ return ruleLabelIds.includes(labelId.toString());
232
+ },
233
+ );
234
+
235
+ if (!hasMatchingLabel) {
236
+ continue;
237
+ }
238
+ }
239
+
240
+ // Rule with no severities and no labels matches all alerts.
241
+ logger.debug(
242
+ `Alert reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
243
+ { projectId: data.projectId?.toString() } as LogAttributes,
244
+ );
245
+
246
+ return rule;
247
+ }
248
+
249
+ return null;
250
+ }
251
+ }
252
+
253
+ export default new Service();
@@ -50,6 +50,8 @@ import Label from "../../Models/DatabaseModels/Label";
50
50
  import LabelService from "./LabelService";
51
51
  import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
52
52
  import AlertSeverityService from "./AlertSeverityService";
53
+ import AlertReminderRuleService from "./AlertReminderRuleService";
54
+ import AlertReminderRule from "../../Models/DatabaseModels/AlertReminderRule";
53
55
  import WorkspaceType from "../../Types/Workspace/WorkspaceType";
54
56
  import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
55
57
  import AlertWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/Alert";
@@ -121,6 +123,64 @@ export class Service extends DatabaseService<Model> {
121
123
  return false;
122
124
  }
123
125
 
126
+ @CaptureSpan()
127
+ public async refreshReminderSchedule(data: {
128
+ alertId: ObjectID;
129
+ projectId: ObjectID;
130
+ }): Promise<void> {
131
+ const alert: Model | null = await this.findOneById({
132
+ id: data.alertId,
133
+ select: {
134
+ enableReminders: true,
135
+ alertSeverityId: true,
136
+ labels: {
137
+ _id: true,
138
+ },
139
+ },
140
+ props: {
141
+ isRoot: true,
142
+ },
143
+ });
144
+
145
+ if (!alert) {
146
+ return;
147
+ }
148
+
149
+ let nextReminderNotificationAt: Date | null = null;
150
+
151
+ if (alert.enableReminders !== false) {
152
+ const matchingRule: AlertReminderRule | null =
153
+ await AlertReminderRuleService.findMatchingRule({
154
+ projectId: data.projectId,
155
+ alertSeverityId: alert.alertSeverityId,
156
+ labelIds: alert.labels?.map((label: Label) => {
157
+ return label.id!;
158
+ }),
159
+ });
160
+
161
+ if (
162
+ matchingRule &&
163
+ matchingRule.reminderIntervalInMinutes &&
164
+ !(await this.isAlertResolved({ alertId: data.alertId }))
165
+ ) {
166
+ nextReminderNotificationAt = OneUptimeDate.addRemoveMinutes(
167
+ OneUptimeDate.getCurrentDate(),
168
+ matchingRule.reminderIntervalInMinutes,
169
+ );
170
+ }
171
+ }
172
+
173
+ await this.updateOneById({
174
+ id: data.alertId,
175
+ data: {
176
+ nextReminderNotificationAt: nextReminderNotificationAt,
177
+ },
178
+ props: {
179
+ isRoot: true,
180
+ },
181
+ });
182
+ }
183
+
124
184
  @CaptureSpan()
125
185
  public async acknowledgeAlert(
126
186
  alertId: ObjectID,
@@ -479,6 +539,25 @@ export class Service extends DatabaseService<Model> {
479
539
  return Promise.resolve();
480
540
  }
481
541
  })
542
+ .then(async () => {
543
+ // Schedule reminder notifications for alert if a matching reminder rule exists
544
+ try {
545
+ if (createdItem.projectId && createdItem.id) {
546
+ await this.refreshReminderSchedule({
547
+ alertId: createdItem.id,
548
+ projectId: createdItem.projectId,
549
+ });
550
+ }
551
+ } catch (error) {
552
+ logger.error(
553
+ `Reminder scheduling failed in AlertService.onCreateSuccess: ${error}`,
554
+ {
555
+ projectId: createdItem.projectId?.toString(),
556
+ alertId: createdItem.id?.toString(),
557
+ } as LogAttributes,
558
+ );
559
+ }
560
+ })
482
561
  .catch((error: Error) => {
483
562
  logger.error(
484
563
  `Critical error in AlertService sequential operations: ${error}`,
@@ -1093,6 +1172,33 @@ ${alertSeverity.name}
1093
1172
  }
1094
1173
  }
1095
1174
 
1175
+ // Re-evaluate reminder schedule when severity or labels change or reminders are toggled
1176
+ if (
1177
+ (onUpdate.updateBy.data.alertSeverity &&
1178
+ (onUpdate.updateBy.data.alertSeverity as any)._id) ||
1179
+ (onUpdate.updateBy.data.labels &&
1180
+ Array.isArray(onUpdate.updateBy.data.labels)) ||
1181
+ Object.prototype.hasOwnProperty.call(
1182
+ onUpdate.updateBy.data,
1183
+ "enableReminders",
1184
+ )
1185
+ ) {
1186
+ try {
1187
+ await this.refreshReminderSchedule({
1188
+ alertId: alertId,
1189
+ projectId: projectId,
1190
+ });
1191
+ } catch (reminderError) {
1192
+ logger.error(
1193
+ `Reminder rescheduling failed in AlertService.onUpdateSuccess: ${reminderError}`,
1194
+ {
1195
+ projectId: projectId?.toString(),
1196
+ alertId: alertId?.toString(),
1197
+ } as LogAttributes,
1198
+ );
1199
+ }
1200
+ }
1201
+
1096
1202
  if (shouldAddAlertFeed) {
1097
1203
  await AlertFeedService.createAlertFeedItem({
1098
1204
  alertId: alertId,
@@ -0,0 +1,256 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
4
+ import DatabaseService from "./DatabaseService";
5
+ import Model from "../../Models/DatabaseModels/IncidentReminderRule";
6
+ import Incident from "../../Models/DatabaseModels/Incident";
7
+ import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
8
+ import Label from "../../Models/DatabaseModels/Label";
9
+ import IncidentState from "../../Models/DatabaseModels/IncidentState";
10
+ import IncidentService from "./IncidentService";
11
+ import IncidentStateService from "./IncidentStateService";
12
+ import QueryHelper from "../Types/Database/QueryHelper";
13
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
14
+ import ObjectID from "../../Types/ObjectID";
15
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
16
+ import logger, { LogAttributes } from "../Utils/Logger";
17
+ import { IsBillingEnabled } from "../EnvironmentConfig";
18
+
19
+ export class Service extends DatabaseService<Model> {
20
+ public constructor() {
21
+ super(Model);
22
+ if (IsBillingEnabled) {
23
+ this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
24
+ }
25
+ }
26
+
27
+ @CaptureSpan()
28
+ protected override async onBeforeCreate(
29
+ createBy: CreateBy<Model>,
30
+ ): Promise<OnCreate<Model>> {
31
+ // Auto-assign order on creation
32
+ if (!createBy.data.order) {
33
+ const highestOrderRule: Model | null = await this.findOneBy({
34
+ query: {
35
+ projectId: createBy.data.projectId!,
36
+ },
37
+ select: {
38
+ order: true,
39
+ },
40
+ sort: {
41
+ order: SortOrder.Descending,
42
+ },
43
+ props: {
44
+ isRoot: true,
45
+ },
46
+ });
47
+
48
+ createBy.data.order = (highestOrderRule?.order || 0) + 1;
49
+ }
50
+
51
+ return {
52
+ createBy,
53
+ carryForward: null,
54
+ };
55
+ }
56
+
57
+ @CaptureSpan()
58
+ protected override async onCreateSuccess(
59
+ _onCreate: OnCreate<Model>,
60
+ createdItem: Model,
61
+ ): Promise<Model> {
62
+ if (createdItem.projectId) {
63
+ await this.refreshSchedulesForOpenIncidents(createdItem.projectId);
64
+ }
65
+
66
+ return createdItem;
67
+ }
68
+
69
+ @CaptureSpan()
70
+ protected override async onUpdateSuccess(
71
+ onUpdate: OnUpdate<Model>,
72
+ _updatedItemIds: Array<ObjectID>,
73
+ ): Promise<OnUpdate<Model>> {
74
+ const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
75
+ | ObjectID
76
+ | undefined;
77
+
78
+ if (projectId) {
79
+ await this.refreshSchedulesForOpenIncidents(projectId);
80
+ }
81
+
82
+ return onUpdate;
83
+ }
84
+
85
+ @CaptureSpan()
86
+ protected override async onDeleteSuccess(
87
+ onDelete: OnDelete<Model>,
88
+ _itemIdsBeforeDelete: Array<ObjectID>,
89
+ ): Promise<OnDelete<Model>> {
90
+ const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
91
+ | ObjectID
92
+ | undefined;
93
+
94
+ if (projectId) {
95
+ await this.refreshSchedulesForOpenIncidents(projectId);
96
+ }
97
+
98
+ return onDelete;
99
+ }
100
+
101
+ @CaptureSpan()
102
+ public async refreshSchedulesForOpenIncidents(
103
+ projectId: ObjectID,
104
+ ): Promise<void> {
105
+ try {
106
+ const unresolvedStates: Array<IncidentState> =
107
+ await IncidentStateService.findBy({
108
+ query: {
109
+ projectId: projectId,
110
+ isResolvedState: false,
111
+ },
112
+ select: {
113
+ _id: true,
114
+ },
115
+ limit: LIMIT_PER_PROJECT,
116
+ skip: 0,
117
+ props: {
118
+ isRoot: true,
119
+ },
120
+ });
121
+
122
+ const unresolvedStateIds: Array<ObjectID> = unresolvedStates
123
+ .map((state: IncidentState) => {
124
+ return state.id!;
125
+ })
126
+ .filter(Boolean);
127
+
128
+ if (unresolvedStateIds.length === 0) {
129
+ return;
130
+ }
131
+
132
+ const openIncidents: Array<Incident> = await IncidentService.findBy({
133
+ query: {
134
+ projectId: projectId,
135
+ currentIncidentStateId: QueryHelper.any(unresolvedStateIds),
136
+ },
137
+ select: {
138
+ _id: true,
139
+ },
140
+ limit: LIMIT_MAX,
141
+ skip: 0,
142
+ props: {
143
+ isRoot: true,
144
+ },
145
+ });
146
+
147
+ for (const incident of openIncidents) {
148
+ try {
149
+ await IncidentService.refreshReminderSchedule({
150
+ incidentId: incident.id!,
151
+ projectId: projectId,
152
+ });
153
+ } catch (error) {
154
+ logger.error(
155
+ `Failed to refresh reminder schedule for incident ${incident.id}: ${error}`,
156
+ { projectId: projectId?.toString() } as LogAttributes,
157
+ );
158
+ }
159
+ }
160
+ } catch (error) {
161
+ logger.error(
162
+ `Failed to refresh reminder schedules for open incidents: ${error}`,
163
+ { projectId: projectId?.toString() } as LogAttributes,
164
+ );
165
+ }
166
+ }
167
+
168
+ @CaptureSpan()
169
+ public async findMatchingRule(data: {
170
+ projectId: ObjectID;
171
+ incidentSeverityId?: ObjectID | undefined;
172
+ labelIds?: Array<ObjectID> | undefined;
173
+ }): Promise<Model | null> {
174
+ // Get all enabled rules sorted by order. First matching rule wins.
175
+ const rules: Array<Model> = await this.findBy({
176
+ query: {
177
+ projectId: data.projectId,
178
+ isEnabled: true,
179
+ },
180
+ sort: {
181
+ order: SortOrder.Ascending,
182
+ },
183
+ select: {
184
+ _id: true,
185
+ name: true,
186
+ order: true,
187
+ reminderIntervalInMinutes: true,
188
+ stopRemindersOnState: true,
189
+ incidentSeverities: {
190
+ _id: true,
191
+ },
192
+ labels: {
193
+ _id: true,
194
+ },
195
+ },
196
+ limit: 100,
197
+ skip: 0,
198
+ props: {
199
+ isRoot: true,
200
+ },
201
+ });
202
+
203
+ for (const rule of rules) {
204
+ if (rule.incidentSeverities && rule.incidentSeverities.length > 0) {
205
+ if (!data.incidentSeverityId) {
206
+ continue;
207
+ }
208
+
209
+ const severityIds: Array<string> = rule.incidentSeverities.map(
210
+ (severity: IncidentSeverity) => {
211
+ return severity.id?.toString() || "";
212
+ },
213
+ );
214
+
215
+ if (!severityIds.includes(data.incidentSeverityId.toString())) {
216
+ continue;
217
+ }
218
+ }
219
+
220
+ if (rule.labels && rule.labels.length > 0) {
221
+ if (!data.labelIds || data.labelIds.length === 0) {
222
+ continue;
223
+ }
224
+
225
+ const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
226
+ return label.id?.toString() || "";
227
+ });
228
+
229
+ const hasMatchingLabel: boolean = data.labelIds.some(
230
+ (labelId: ObjectID) => {
231
+ return ruleLabelIds.includes(labelId.toString());
232
+ },
233
+ );
234
+
235
+ if (!hasMatchingLabel) {
236
+ continue;
237
+ }
238
+ }
239
+
240
+ /*
241
+ * Rule matched: (no severities OR severity matches) AND
242
+ * (no labels OR labels intersect). A rule with neither matches all.
243
+ */
244
+ logger.debug(
245
+ `Incident reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
246
+ { projectId: data.projectId?.toString() } as LogAttributes,
247
+ );
248
+
249
+ return rule;
250
+ }
251
+
252
+ return null;
253
+ }
254
+ }
255
+
256
+ export default new Service();