@oneuptime/common 11.3.20 → 11.3.22

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 (212) 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/EnterpriseLicenseInstance.ts +173 -0
  14. package/Models/DatabaseModels/GlobalConfig.ts +19 -0
  15. package/Models/DatabaseModels/Incident.ts +100 -0
  16. package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
  17. package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
  18. package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
  19. package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
  20. package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
  21. package/Models/DatabaseModels/Index.ts +8 -0
  22. package/Models/DatabaseModels/Label.ts +2 -0
  23. package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
  24. package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
  25. package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
  26. package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
  27. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
  28. package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
  29. package/Server/API/DashboardAPI.ts +221 -0
  30. package/Server/API/EnterpriseLicenseAPI.ts +279 -12
  31. package/Server/API/GlobalConfigAPI.ts +81 -16
  32. package/Server/API/StatusPageAPI.ts +894 -791
  33. package/Server/EnvironmentConfig.ts +9 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  40. package/Server/Services/AlertReminderRuleService.ts +253 -0
  41. package/Server/Services/AlertService.ts +106 -0
  42. package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
  43. package/Server/Services/IncidentReminderRuleService.ts +256 -0
  44. package/Server/Services/IncidentService.ts +143 -0
  45. package/Server/Services/Index.ts +8 -0
  46. package/Server/Services/IoTDeviceService.ts +99 -4
  47. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
  48. package/Server/Services/ScheduledMaintenanceService.ts +127 -0
  49. package/Server/Services/StatusPageService.ts +104 -45
  50. package/Server/Services/TelemetryExceptionService.ts +123 -0
  51. package/Server/Services/UserNotificationSettingService.ts +24 -0
  52. package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
  53. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
  54. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
  55. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
  56. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
  57. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
  58. package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
  59. package/ServiceRoute.ts +4 -0
  60. package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
  62. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
  63. package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
  64. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
  65. package/Types/Email/EmailTemplateType.ts +3 -0
  66. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
  67. package/Types/Monitor/MonitorStep.ts +32 -0
  68. package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
  69. package/Types/Permission.ts +144 -0
  70. package/Types/Reminder/ReminderStopState.ts +6 -0
  71. package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
  72. package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
  73. package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
  74. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
  75. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
  76. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  77. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  78. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  79. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  80. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  81. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  83. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  85. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/Alert.js +103 -0
  87. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
  89. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
  91. package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
  92. package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
  93. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
  95. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
  97. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js +18 -0
  101. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Incident.js +103 -0
  103. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
  105. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
  107. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
  109. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
  111. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
  113. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  115. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  116. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  117. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
  119. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
  121. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
  123. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
  124. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
  125. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
  127. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
  129. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  130. package/build/dist/Server/API/DashboardAPI.js +166 -16
  131. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  132. package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
  133. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  134. package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
  135. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  136. package/build/dist/Server/API/StatusPageAPI.js +74 -31
  137. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  138. package/build/dist/Server/EnvironmentConfig.js +6 -0
  139. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  152. package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
  153. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
  154. package/build/dist/Server/Services/AlertService.js +87 -0
  155. package/build/dist/Server/Services/AlertService.js.map +1 -1
  156. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
  157. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
  158. package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
  159. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
  160. package/build/dist/Server/Services/IncidentService.js +116 -0
  161. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  162. package/build/dist/Server/Services/Index.js +8 -0
  163. package/build/dist/Server/Services/Index.js.map +1 -1
  164. package/build/dist/Server/Services/IoTDeviceService.js +66 -4
  165. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
  168. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
  169. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +91 -43
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
  173. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  174. package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
  175. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  176. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  177. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  178. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  180. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
  181. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
  183. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
  185. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  186. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
  187. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  188. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
  189. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  190. package/build/dist/ServiceRoute.js +1 -0
  191. package/build/dist/ServiceRoute.js.map +1 -1
  192. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  193. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  194. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
  195. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
  196. package/build/dist/Types/Monitor/MonitorStep.js +25 -0
  197. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  198. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  199. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  200. package/build/dist/Types/Permission.js +126 -0
  201. package/build/dist/Types/Permission.js.map +1 -1
  202. package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
  203. package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
  204. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
  205. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
  206. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
  207. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  208. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
  209. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  210. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
  211. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
  212. package/package.json +1 -1
@@ -56,6 +56,8 @@ import logger, { LogAttributes } from "../Utils/Logger";
56
56
  import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
57
57
  import IncidentFeedService from "./IncidentFeedService";
58
58
  import IncidentSlaService from "./IncidentSlaService";
59
+ import IncidentReminderRuleService from "./IncidentReminderRuleService";
60
+ import IncidentReminderRule from "../../Models/DatabaseModels/IncidentReminderRule";
59
61
  import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
60
62
  import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed";
61
63
  import IncidentGroupingEngineService from "./IncidentGroupingEngineService";
@@ -230,6 +232,64 @@ export class Service extends DatabaseService<Model> {
230
232
  return false;
231
233
  }
232
234
 
235
+ @CaptureSpan()
236
+ public async refreshReminderSchedule(data: {
237
+ incidentId: ObjectID;
238
+ projectId: ObjectID;
239
+ }): Promise<void> {
240
+ const incident: Model | null = await this.findOneById({
241
+ id: data.incidentId,
242
+ select: {
243
+ enableReminders: true,
244
+ incidentSeverityId: true,
245
+ labels: {
246
+ _id: true,
247
+ },
248
+ },
249
+ props: {
250
+ isRoot: true,
251
+ },
252
+ });
253
+
254
+ if (!incident) {
255
+ return;
256
+ }
257
+
258
+ let nextReminderNotificationAt: Date | null = null;
259
+
260
+ if (incident.enableReminders !== false) {
261
+ const matchingRule: IncidentReminderRule | null =
262
+ await IncidentReminderRuleService.findMatchingRule({
263
+ projectId: data.projectId,
264
+ incidentSeverityId: incident.incidentSeverityId,
265
+ labelIds: incident.labels?.map((label: Label) => {
266
+ return label.id!;
267
+ }),
268
+ });
269
+
270
+ if (
271
+ matchingRule &&
272
+ matchingRule.reminderIntervalInMinutes &&
273
+ !(await this.isIncidentResolved({ incidentId: data.incidentId }))
274
+ ) {
275
+ nextReminderNotificationAt = OneUptimeDate.addRemoveMinutes(
276
+ OneUptimeDate.getCurrentDate(),
277
+ matchingRule.reminderIntervalInMinutes,
278
+ );
279
+ }
280
+ }
281
+
282
+ await this.updateOneById({
283
+ id: data.incidentId,
284
+ data: {
285
+ nextReminderNotificationAt: nextReminderNotificationAt,
286
+ },
287
+ props: {
288
+ isRoot: true,
289
+ },
290
+ });
291
+ }
292
+
233
293
  @CaptureSpan()
234
294
  public async resolveIncident(
235
295
  incidentId: ObjectID,
@@ -1161,6 +1221,26 @@ export class Service extends DatabaseService<Model> {
1161
1221
  );
1162
1222
  }
1163
1223
  })
1224
+ .then(async () => {
1225
+ // Schedule reminder notifications for incident if a matching reminder rule exists
1226
+ try {
1227
+ if (createdItem.projectId && createdItem.id) {
1228
+ await this.refreshReminderSchedule({
1229
+ incidentId: createdItem.id,
1230
+ projectId: createdItem.projectId,
1231
+ });
1232
+ }
1233
+ } catch (error) {
1234
+ logger.error(
1235
+ `Reminder scheduling failed in IncidentService.onCreateSuccess: ${error}`,
1236
+ {
1237
+ projectId: createdItem.projectId?.toString(),
1238
+ incidentId: createdItem.id?.toString(),
1239
+ userId: createdItem.createdByUserId?.toString(),
1240
+ } as LogAttributes,
1241
+ );
1242
+ }
1243
+ })
1164
1244
  .catch((error: Error) => {
1165
1245
  logger.error(
1166
1246
  `Critical error in IncidentService sequential operations: ${error}`,
@@ -1728,6 +1808,29 @@ ${incident.remediationNotes || "No remediation notes provided."}
1728
1808
  });
1729
1809
  }
1730
1810
 
1811
+ // Re-evaluate reminder schedule when reminders are enabled or disabled for this incident
1812
+ if (
1813
+ Object.prototype.hasOwnProperty.call(
1814
+ updatedIncidentData,
1815
+ "enableReminders",
1816
+ )
1817
+ ) {
1818
+ try {
1819
+ await this.refreshReminderSchedule({
1820
+ incidentId: incidentId,
1821
+ projectId: projectId,
1822
+ });
1823
+ } catch (reminderError) {
1824
+ logger.error(
1825
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
1826
+ {
1827
+ projectId: projectId?.toString(),
1828
+ incidentId: incidentId?.toString(),
1829
+ } as LogAttributes,
1830
+ );
1831
+ }
1832
+ }
1833
+
1731
1834
  // emit postmortem completion time metric when postmortemPostedAt is set
1732
1835
  if (
1733
1836
  Object.prototype.hasOwnProperty.call(
@@ -1976,6 +2079,30 @@ ${labels
1976
2079
  }
1977
2080
  }
1978
2081
 
2082
+ /*
2083
+ * Re-match reminder rule on any labels change (including clearing all
2084
+ * labels), since labels can change which reminder rule matches.
2085
+ */
2086
+ if (
2087
+ updatedIncidentData.labels &&
2088
+ Array.isArray(updatedIncidentData.labels)
2089
+ ) {
2090
+ try {
2091
+ await this.refreshReminderSchedule({
2092
+ incidentId: incidentId,
2093
+ projectId: projectId,
2094
+ });
2095
+ } catch (reminderError) {
2096
+ logger.error(
2097
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
2098
+ {
2099
+ projectId: projectId?.toString(),
2100
+ incidentId: incidentId?.toString(),
2101
+ } as LogAttributes,
2102
+ );
2103
+ }
2104
+ }
2105
+
1979
2106
  if (
1980
2107
  updatedIncidentData.incidentSeverity &&
1981
2108
  (updatedIncidentData.incidentSeverity as any)._id
@@ -2017,6 +2144,22 @@ ${incidentSeverity.name}
2017
2144
  );
2018
2145
  }
2019
2146
 
2147
+ // Re-match reminder rule when severity changes
2148
+ try {
2149
+ await this.refreshReminderSchedule({
2150
+ incidentId: incidentId,
2151
+ projectId: projectId,
2152
+ });
2153
+ } catch (reminderError) {
2154
+ logger.error(
2155
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
2156
+ {
2157
+ projectId: projectId?.toString(),
2158
+ incidentId: incidentId?.toString(),
2159
+ } as LogAttributes,
2160
+ );
2161
+ }
2162
+
2020
2163
  // emit severity change metric
2021
2164
  try {
2022
2165
  const severityChangeMetric: MutableMetric = new MutableMetric();
@@ -113,6 +113,7 @@ import GlobalSsoProjectService from "./GlobalSsoProjectService";
113
113
  import GlobalOidcProjectService from "./GlobalOidcProjectService";
114
114
  import PromoCodeService from "./PromoCodeService";
115
115
  import EnterpriseLicenseService from "./EnterpriseLicenseService";
116
+ import EnterpriseLicenseInstanceService from "./EnterpriseLicenseInstanceService";
116
117
  import OpenSourceDeploymentService from "./OpenSourceDeploymentService";
117
118
  import ResellerPlanService from "./ResellerPlanService";
118
119
  import ResellerService from "./ResellerService";
@@ -225,6 +226,9 @@ import IncidentEpisodePublicNoteService from "./IncidentEpisodePublicNoteService
225
226
  import AlertGroupingRuleService from "./AlertGroupingRuleService";
226
227
  import IncidentSlaRuleService from "./IncidentSlaRuleService";
227
228
  import IncidentSlaService from "./IncidentSlaService";
229
+ import IncidentReminderRuleService from "./IncidentReminderRuleService";
230
+ import AlertReminderRuleService from "./AlertReminderRuleService";
231
+ import ScheduledMaintenanceReminderRuleService from "./ScheduledMaintenanceReminderRuleService";
228
232
 
229
233
  import TableViewService from "./TableViewService";
230
234
  import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
@@ -251,6 +255,7 @@ const services: Array<BaseService> = [
251
255
  AcmeCertificateService,
252
256
  PromoCodeService,
253
257
  EnterpriseLicenseService,
258
+ EnterpriseLicenseInstanceService,
254
259
  OpenSourceDeploymentService,
255
260
 
256
261
  ResellerService,
@@ -479,6 +484,9 @@ const services: Array<BaseService> = [
479
484
  AlertGroupingRuleService,
480
485
  IncidentSlaRuleService,
481
486
  IncidentSlaService,
487
+ IncidentReminderRuleService,
488
+ AlertReminderRuleService,
489
+ ScheduledMaintenanceReminderRuleService,
482
490
 
483
491
  TableViewService,
484
492
  MonitorTestService,
@@ -1,8 +1,10 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import Model from "../../Models/DatabaseModels/IoTDevice";
3
3
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
4
+ import ColumnLength from "../../Types/Database/ColumnLength";
4
5
  import ObjectID from "../../Types/ObjectID";
5
6
  import OneUptimeDate from "../../Types/Date";
7
+ import { clampIoTTimestamp } from "../Utils/Telemetry/IoTSnapshotScan";
6
8
  import logger from "../Utils/Logger";
7
9
 
8
10
  /*
@@ -57,6 +59,23 @@ export interface IoTInventorySummary {
57
59
  const UPSERT_BATCH_SIZE: number = 500;
58
60
  const STALE_DELETE_WARN_THRESHOLD: number = 100;
59
61
 
62
+ /*
63
+ * The identity/text columns are ShortText (100 chars). A single value
64
+ * over the column limit fails the whole multi-row INSERT chunk it rides
65
+ * in — truncate per value instead so one malformed device can never
66
+ * drop the other rows of its chunk.
67
+ */
68
+ function truncateShortText(value: string): string;
69
+ function truncateShortText(value: string | null): string | null;
70
+ function truncateShortText(value: string | null): string | null {
71
+ if (value === null) {
72
+ return value;
73
+ }
74
+ return value.length > ColumnLength.ShortText
75
+ ? value.substring(0, ColumnLength.ShortText)
76
+ : value;
77
+ }
78
+
60
79
  /*
61
80
  * Column order used by bulkUpsert() and its generated parameter tuples.
62
81
  * Keep this and the INSERT column list in perfect sync.
@@ -103,9 +122,52 @@ export class Service extends DatabaseService<Model> {
103
122
  return;
104
123
  }
105
124
 
125
+ /*
126
+ * Sanitize before building chunks: ShortText columns truncate to
127
+ * the column limit (one over-long device.id must not fail the
128
+ * whole 500-row chunk), lastSeenAt clamps so a future-skewed
129
+ * device clock can't wedge the `>=` dominance guard, and rows that
130
+ * collide on the conflict target after truncation dedupe to the
131
+ * newest observation (two VALUES rows hitting the same conflict
132
+ * target would abort the statement).
133
+ */
134
+ const dedupedByIdentity: Map<string, ParsedIoTDevice> = new Map();
135
+ for (const r of data.devices) {
136
+ const externalId: string = truncateShortText(r.externalId);
137
+ if (!externalId) {
138
+ continue;
139
+ }
140
+ if (externalId !== r.externalId) {
141
+ logger.warn(
142
+ `IoTDevice externalId exceeds ${ColumnLength.ShortText} chars; truncated to "${externalId}" (fleet ${data.iotFleetId.toString()}).`,
143
+ );
144
+ }
145
+ const device: ParsedIoTDevice = {
146
+ ...r,
147
+ kind: truncateShortText(r.kind),
148
+ externalId,
149
+ name: truncateShortText(r.name),
150
+ deviceType: truncateShortText(r.deviceType),
151
+ firmwareVersion: truncateShortText(r.firmwareVersion),
152
+ lastSeenAt: clampIoTTimestamp(r.lastSeenAt),
153
+ };
154
+ const key: string = `${device.kind}|${device.externalId}`;
155
+ const existing: ParsedIoTDevice | undefined = dedupedByIdentity.get(key);
156
+ if (!existing || device.lastSeenAt >= existing.lastSeenAt) {
157
+ dedupedByIdentity.set(key, device);
158
+ }
159
+ }
160
+
161
+ const devices: Array<ParsedIoTDevice> = Array.from(
162
+ dedupedByIdentity.values(),
163
+ );
164
+ if (devices.length === 0) {
165
+ return;
166
+ }
167
+
106
168
  // Chunk to keep individual statement parameter counts reasonable.
107
- for (let i: number = 0; i < data.devices.length; i += UPSERT_BATCH_SIZE) {
108
- const chunk: Array<ParsedIoTDevice> = data.devices.slice(
169
+ for (let i: number = 0; i < devices.length; i += UPSERT_BATCH_SIZE) {
170
+ const chunk: Array<ParsedIoTDevice> = devices.slice(
109
171
  i,
110
172
  i + UPSERT_BATCH_SIZE,
111
173
  );
@@ -181,8 +243,41 @@ export class Service extends DatabaseService<Model> {
181
243
  return;
182
244
  }
183
245
 
184
- for (let i: number = 0; i < data.metrics.length; i += UPSERT_BATCH_SIZE) {
185
- const chunk: Array<IoTDeviceLatestMetric> = data.metrics.slice(
246
+ /*
247
+ * Same sanitation as bulkUpsert — and the identity truncation MUST
248
+ * match it, or the mirror UPDATE would silently miss the row the
249
+ * upsert just wrote under the truncated id. Dedupe keeps the UPDATE
250
+ * deterministic when two rows collapse onto one identity.
251
+ */
252
+ const dedupedByIdentity: Map<string, IoTDeviceLatestMetric> = new Map();
253
+ for (const m of data.metrics) {
254
+ const externalId: string = truncateShortText(m.externalId);
255
+ if (!externalId) {
256
+ continue;
257
+ }
258
+ const metric: IoTDeviceLatestMetric = {
259
+ ...m,
260
+ kind: truncateShortText(m.kind),
261
+ externalId,
262
+ observedAt: clampIoTTimestamp(m.observedAt),
263
+ };
264
+ const key: string = `${metric.kind}|${metric.externalId}`;
265
+ const existing: IoTDeviceLatestMetric | undefined =
266
+ dedupedByIdentity.get(key);
267
+ if (!existing || metric.observedAt >= existing.observedAt) {
268
+ dedupedByIdentity.set(key, metric);
269
+ }
270
+ }
271
+
272
+ const metrics: Array<IoTDeviceLatestMetric> = Array.from(
273
+ dedupedByIdentity.values(),
274
+ );
275
+ if (metrics.length === 0) {
276
+ return;
277
+ }
278
+
279
+ for (let i: number = 0; i < metrics.length; i += UPSERT_BATCH_SIZE) {
280
+ const chunk: Array<IoTDeviceLatestMetric> = metrics.slice(
186
281
  i,
187
282
  i + UPSERT_BATCH_SIZE,
188
283
  );
@@ -0,0 +1,236 @@
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/ScheduledMaintenanceReminderRule";
6
+ import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
7
+ import Label from "../../Models/DatabaseModels/Label";
8
+ import ScheduledMaintenanceState from "../../Models/DatabaseModels/ScheduledMaintenanceState";
9
+ import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
10
+ import ScheduledMaintenanceStateService from "./ScheduledMaintenanceStateService";
11
+ import QueryHelper from "../Types/Database/QueryHelper";
12
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
13
+ import ObjectID from "../../Types/ObjectID";
14
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
15
+ import logger, { LogAttributes } from "../Utils/Logger";
16
+ import { IsBillingEnabled } from "../EnvironmentConfig";
17
+
18
+ export class Service extends DatabaseService<Model> {
19
+ public constructor() {
20
+ super(Model);
21
+ if (IsBillingEnabled) {
22
+ this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
23
+ }
24
+ }
25
+
26
+ @CaptureSpan()
27
+ protected override async onBeforeCreate(
28
+ createBy: CreateBy<Model>,
29
+ ): Promise<OnCreate<Model>> {
30
+ // Auto-assign order on creation
31
+ if (!createBy.data.order) {
32
+ const highestOrderRule: Model | null = await this.findOneBy({
33
+ query: {
34
+ projectId: createBy.data.projectId!,
35
+ },
36
+ select: {
37
+ order: true,
38
+ },
39
+ sort: {
40
+ order: SortOrder.Descending,
41
+ },
42
+ props: {
43
+ isRoot: true,
44
+ },
45
+ });
46
+
47
+ createBy.data.order = (highestOrderRule?.order || 0) + 1;
48
+ }
49
+
50
+ return {
51
+ createBy,
52
+ carryForward: null,
53
+ };
54
+ }
55
+
56
+ @CaptureSpan()
57
+ protected override async onCreateSuccess(
58
+ _onCreate: OnCreate<Model>,
59
+ createdItem: Model,
60
+ ): Promise<Model> {
61
+ if (createdItem.projectId) {
62
+ await this.refreshSchedulesForOpenScheduledMaintenances(
63
+ createdItem.projectId,
64
+ );
65
+ }
66
+
67
+ return createdItem;
68
+ }
69
+
70
+ @CaptureSpan()
71
+ protected override async onUpdateSuccess(
72
+ onUpdate: OnUpdate<Model>,
73
+ _updatedItemIds: Array<ObjectID>,
74
+ ): Promise<OnUpdate<Model>> {
75
+ const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
76
+ | ObjectID
77
+ | undefined;
78
+
79
+ if (projectId) {
80
+ await this.refreshSchedulesForOpenScheduledMaintenances(projectId);
81
+ }
82
+
83
+ return onUpdate;
84
+ }
85
+
86
+ @CaptureSpan()
87
+ protected override async onDeleteSuccess(
88
+ onDelete: OnDelete<Model>,
89
+ _itemIdsBeforeDelete: Array<ObjectID>,
90
+ ): Promise<OnDelete<Model>> {
91
+ const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
92
+ | ObjectID
93
+ | undefined;
94
+
95
+ if (projectId) {
96
+ await this.refreshSchedulesForOpenScheduledMaintenances(projectId);
97
+ }
98
+
99
+ return onDelete;
100
+ }
101
+
102
+ @CaptureSpan()
103
+ public async refreshSchedulesForOpenScheduledMaintenances(
104
+ projectId: ObjectID,
105
+ ): Promise<void> {
106
+ try {
107
+ const openStates: Array<ScheduledMaintenanceState> =
108
+ await ScheduledMaintenanceStateService.findBy({
109
+ query: {
110
+ projectId: projectId,
111
+ isResolvedState: false,
112
+ },
113
+ select: {
114
+ _id: true,
115
+ },
116
+ limit: LIMIT_PER_PROJECT,
117
+ skip: 0,
118
+ props: {
119
+ isRoot: true,
120
+ },
121
+ });
122
+
123
+ const openStateIds: Array<ObjectID> = openStates
124
+ .map((state: ScheduledMaintenanceState) => {
125
+ return state.id!;
126
+ })
127
+ .filter(Boolean);
128
+
129
+ if (openStateIds.length === 0) {
130
+ return;
131
+ }
132
+
133
+ const openScheduledMaintenances: Array<ScheduledMaintenance> =
134
+ await ScheduledMaintenanceService.findBy({
135
+ query: {
136
+ projectId: projectId,
137
+ currentScheduledMaintenanceStateId: QueryHelper.any(openStateIds),
138
+ },
139
+ select: {
140
+ _id: true,
141
+ },
142
+ limit: LIMIT_MAX,
143
+ skip: 0,
144
+ props: {
145
+ isRoot: true,
146
+ },
147
+ });
148
+
149
+ for (const scheduledMaintenance of openScheduledMaintenances) {
150
+ try {
151
+ await ScheduledMaintenanceService.refreshReminderSchedule({
152
+ scheduledMaintenanceId: scheduledMaintenance.id!,
153
+ projectId: projectId,
154
+ });
155
+ } catch (error) {
156
+ logger.error(
157
+ `Failed to refresh reminder schedule for scheduled maintenance ${scheduledMaintenance.id}: ${error}`,
158
+ { projectId: projectId?.toString() } as LogAttributes,
159
+ );
160
+ }
161
+ }
162
+ } catch (error) {
163
+ logger.error(
164
+ `Failed to refresh reminder schedules for open scheduled maintenances: ${error}`,
165
+ { projectId: projectId?.toString() } as LogAttributes,
166
+ );
167
+ }
168
+ }
169
+
170
+ @CaptureSpan()
171
+ public async findMatchingRule(data: {
172
+ projectId: ObjectID;
173
+ labelIds?: Array<ObjectID> | undefined;
174
+ }): Promise<Model | null> {
175
+ // Get all enabled rules sorted by order. First matching rule wins.
176
+ const rules: Array<Model> = await this.findBy({
177
+ query: {
178
+ projectId: data.projectId,
179
+ isEnabled: true,
180
+ },
181
+ sort: {
182
+ order: SortOrder.Ascending,
183
+ },
184
+ select: {
185
+ _id: true,
186
+ name: true,
187
+ order: true,
188
+ reminderIntervalInMinutes: true,
189
+ stopRemindersOnState: true,
190
+ remindWhileScheduled: true,
191
+ labels: {
192
+ _id: true,
193
+ },
194
+ },
195
+ limit: 100,
196
+ skip: 0,
197
+ props: {
198
+ isRoot: true,
199
+ },
200
+ });
201
+
202
+ for (const rule of rules) {
203
+ if (rule.labels && rule.labels.length > 0) {
204
+ if (!data.labelIds || data.labelIds.length === 0) {
205
+ continue;
206
+ }
207
+
208
+ const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
209
+ return label.id?.toString() || "";
210
+ });
211
+
212
+ const hasMatchingLabel: boolean = data.labelIds.some(
213
+ (labelId: ObjectID) => {
214
+ return ruleLabelIds.includes(labelId.toString());
215
+ },
216
+ );
217
+
218
+ if (!hasMatchingLabel) {
219
+ continue;
220
+ }
221
+ }
222
+
223
+ // Rule with no labels matches all scheduled maintenances.
224
+ logger.debug(
225
+ `Scheduled maintenance reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
226
+ { projectId: data.projectId?.toString() } as LogAttributes,
227
+ );
228
+
229
+ return rule;
230
+ }
231
+
232
+ return null;
233
+ }
234
+ }
235
+
236
+ export default new Service();