@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
@@ -15,6 +15,17 @@ import AIAgentTaskTelemetryExceptionService from "./AIAgentTaskTelemetryExceptio
15
15
  import QueryHelper from "../Types/Database/QueryHelper";
16
16
  import ModelPermission from "../Types/Database/Permissions/Index";
17
17
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
18
+ import Query from "../Types/Database/Query";
19
+ import Includes from "../../Types/BaseDatabase/Includes";
20
+ import logger from "../Utils/Logger";
21
+
22
+ /*
23
+ * Hard cap on the fingerprint NOT IN list handed to the ClickHouse count
24
+ * query. ClickHouse receives query params via the request URI (default
25
+ * http_max_uri_size 1 MiB ≈ 14k sha256 fingerprints); 5k keeps a wide
26
+ * safety margin while covering all realistic projects.
27
+ */
28
+ export const EXCLUDED_FINGERPRINT_LIMIT: number = 5000;
18
29
 
19
30
  export interface CreateAIAgentTaskForExceptionParams {
20
31
  telemetryExceptionId: ObjectID;
@@ -263,6 +274,118 @@ export class Service extends DatabaseService<Model> {
263
274
  };
264
275
  }
265
276
 
277
+ /**
278
+ * Fingerprints of exception groups marked resolved and/or archived —
279
+ * used by the exception monitor to exclude occurrences of those groups
280
+ * from the exception count. Resolution state lives on this Postgres
281
+ * model per (projectId, primaryEntityId, fingerprint) group, while
282
+ * occurrences live in ClickHouse; fingerprints are the join key.
283
+ * Fingerprints hash projectId + primaryEntityId into the digest, so a
284
+ * fingerprint never collides across services and excluding by
285
+ * fingerprint alone is exact.
286
+ *
287
+ * The result is capped at EXCLUDED_FINGERPRINT_LIMIT, keeping the most
288
+ * recently seen groups: the ClickHouse client ships query params in the
289
+ * request URI, so an unbounded NOT IN list would blow past ClickHouse's
290
+ * http_max_uri_size (~1 MiB, roughly 14k fingerprints) and hard-fail the
291
+ * monitor's count query. Overflow groups simply stay counted (fail-open:
292
+ * better a stale incident than a silently swallowed one), and the
293
+ * lastSeenAt-descending order means what gets dropped is the groups
294
+ * least likely to have occurrences in the monitor window.
295
+ *
296
+ * Deliberately NO lastSeenAt lower bound: ExceptionInstance.time is the
297
+ * client-reported occurrence timestamp while lastSeenAt is server-side
298
+ * ingest time, so any window-derived cutoff breaks under client clock
299
+ * skew (and under monitor configs with no time window at all).
300
+ */
301
+ @CaptureSpan()
302
+ public async getResolvedOrArchivedFingerprints(data: {
303
+ projectId: ObjectID;
304
+ telemetryServiceIds?: Array<ObjectID> | undefined;
305
+ resolved: boolean;
306
+ archived: boolean;
307
+ }): Promise<Array<string>> {
308
+ const statusFilters: Array<Query<Model>> = [];
309
+
310
+ if (data.resolved) {
311
+ statusFilters.push({ isResolved: true });
312
+ }
313
+
314
+ if (data.archived) {
315
+ statusFilters.push({ isArchived: true });
316
+ }
317
+
318
+ if (statusFilters.length === 0) {
319
+ return [];
320
+ }
321
+
322
+ // resolved-OR-archived needs two indexed lookups — findBy has no OR.
323
+ const resultsPerStatus: Array<Array<Model>> = await Promise.all(
324
+ statusFilters.map(
325
+ async (statusFilter: Query<Model>): Promise<Array<Model>> => {
326
+ const query: Query<Model> = {
327
+ projectId: data.projectId,
328
+ ...statusFilter,
329
+ };
330
+
331
+ if (data.telemetryServiceIds && data.telemetryServiceIds.length > 0) {
332
+ query.primaryEntityId = new Includes(data.telemetryServiceIds);
333
+ }
334
+
335
+ /*
336
+ * Per-status limit equals the final cap: the merged most-recent
337
+ * EXCLUDED_FINGERPRINT_LIMIT groups are necessarily within the
338
+ * top EXCLUDED_FINGERPRINT_LIMIT of their own status query.
339
+ */
340
+ return this.findBy({
341
+ query: query,
342
+ select: {
343
+ fingerprint: true,
344
+ lastSeenAt: true,
345
+ },
346
+ sort: {
347
+ lastSeenAt: SortOrder.Descending,
348
+ },
349
+ limit: EXCLUDED_FINGERPRINT_LIMIT,
350
+ skip: 0,
351
+ props: {
352
+ isRoot: true,
353
+ },
354
+ });
355
+ },
356
+ ),
357
+ );
358
+
359
+ // Merge the status lists most-recently-seen first, dedupe, then cap.
360
+ const merged: Array<Model> = resultsPerStatus
361
+ .flat()
362
+ .sort((a: Model, b: Model): number => {
363
+ return (b.lastSeenAt?.getTime() || 0) - (a.lastSeenAt?.getTime() || 0);
364
+ });
365
+
366
+ const fingerprints: Set<string> = new Set<string>();
367
+ let truncated: boolean = false;
368
+
369
+ for (const exception of merged) {
370
+ if (!exception.fingerprint) {
371
+ continue;
372
+ }
373
+ if (fingerprints.size >= EXCLUDED_FINGERPRINT_LIMIT) {
374
+ truncated = true;
375
+ break;
376
+ }
377
+ fingerprints.add(exception.fingerprint);
378
+ }
379
+
380
+ if (truncated) {
381
+ logger.warn(
382
+ `TelemetryException fingerprint exclusion list for project ${data.projectId.toString()} exceeds ${EXCLUDED_FINGERPRINT_LIMIT}; least recently seen resolved/archived exception groups will still be counted by exception monitors.`,
383
+ );
384
+ }
385
+
386
+ return Array.from(fingerprints);
387
+ }
388
+
266
389
  @CaptureSpan()
267
390
  private async aggregateUnresolvedByService(
268
391
  projectId: ObjectID,
@@ -580,6 +580,18 @@ export class Service extends DatabaseService<UserNotificationSetting> {
580
580
  await this.addAlertNotificationSettings(userId, projectId);
581
581
  await this.addAlertEpisodeNotificationSettings(userId, projectId);
582
582
  await this.addIncidentEpisodeNotificationSettings(userId, projectId);
583
+ await this.addScheduledMaintenanceNotificationSettings(userId, projectId);
584
+ }
585
+
586
+ private async addScheduledMaintenanceNotificationSettings(
587
+ userId: ObjectID,
588
+ projectId: ObjectID,
589
+ ): Promise<void> {
590
+ await this.addNotificationSettingIfNotExists(
591
+ userId,
592
+ projectId,
593
+ NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_REMINDER_OWNER_NOTIFICATION,
594
+ );
583
595
  }
584
596
 
585
597
  private async addProbeOwnerNotificationSettings(
@@ -614,6 +626,12 @@ export class Service extends DatabaseService<UserNotificationSetting> {
614
626
  projectId,
615
627
  NotificationSettingEventType.SEND_INCIDENT_STATE_CHANGED_OWNER_NOTIFICATION,
616
628
  );
629
+
630
+ await this.addNotificationSettingIfNotExists(
631
+ userId,
632
+ projectId,
633
+ NotificationSettingEventType.SEND_INCIDENT_REMINDER_OWNER_NOTIFICATION,
634
+ );
617
635
  }
618
636
 
619
637
  private async addMonitorNotificationSettings(
@@ -689,6 +707,12 @@ export class Service extends DatabaseService<UserNotificationSetting> {
689
707
  projectId,
690
708
  NotificationSettingEventType.SEND_ALERT_STATE_CHANGED_OWNER_NOTIFICATION,
691
709
  );
710
+
711
+ await this.addNotificationSettingIfNotExists(
712
+ userId,
713
+ projectId,
714
+ NotificationSettingEventType.SEND_ALERT_REMINDER_OWNER_NOTIFICATION,
715
+ );
692
716
  }
693
717
 
694
718
  private async addAlertEpisodeNotificationSettings(
@@ -5,6 +5,7 @@ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
5
5
  import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
6
6
  import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
7
7
  import Includes from "../../../Types/BaseDatabase/Includes";
8
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
8
9
  import LessThan from "../../../Types/BaseDatabase/LessThan";
9
10
  import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
10
11
  import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
@@ -108,7 +109,7 @@ export class Statement implements BaseQueryParams {
108
109
  v.value instanceof NotEqual
109
110
  ) {
110
111
  finalValue = v.value.value;
111
- } else if (v.value instanceof Includes) {
112
+ } else if (v.value instanceof Includes || v.value instanceof IncludesNone) {
112
113
  if (
113
114
  v.type === TableColumnType.Text ||
114
115
  v.type === TableColumnType.ObjectID
@@ -198,9 +199,12 @@ export class Statement implements BaseQueryParams {
198
199
  [TableColumnType.UInt64]: "UInt64",
199
200
  };
200
201
 
201
- if ((statementParam as StatementParameter).value instanceof Includes) {
202
+ if (
203
+ (statementParam as StatementParameter).value instanceof Includes ||
204
+ (statementParam as StatementParameter).value instanceof IncludesNone
205
+ ) {
202
206
  const includesValues: Array<string | number | ObjectID> = (
203
- (statementParam as StatementParameter).value as Includes
207
+ (statementParam as StatementParameter).value as Includes | IncludesNone
204
208
  ).values as Array<string | number | ObjectID>;
205
209
 
206
210
  const isNumberArray: boolean = includesValues.every(
@@ -675,6 +675,41 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
675
675
  type: tableColumn.type,
676
676
  }}`,
677
677
  );
678
+ } else if (
679
+ value instanceof IncludesNone &&
680
+ tableColumn.type === TableColumnType.ArrayText
681
+ ) {
682
+ /*
683
+ * Array(String) exclusion — the negation of the Includes/hasAny
684
+ * branch above. An empty IncludesNone excludes nothing, so it
685
+ * drops to no predicate (mirrors the empty-Includes behavior).
686
+ */
687
+ const arrayExcludeValues: Array<string> =
688
+ ((value as IncludesNone).values as Array<string>) || [];
689
+ if (arrayExcludeValues.length > 0) {
690
+ whereStatement.append(
691
+ SQL`AND NOT hasAny(${key}, ${{
692
+ value: arrayExcludeValues,
693
+ type: TableColumnType.ArrayText,
694
+ }})`,
695
+ );
696
+ }
697
+ } else if (value instanceof IncludesNone) {
698
+ /*
699
+ * Scalar exclusion ("is none of"): `col NOT IN (v1, v2, ...)`.
700
+ * An empty IncludesNone means "exclude nothing" — skip the
701
+ * predicate entirely rather than emitting `NOT IN ()`.
702
+ */
703
+ const excludeValues: Array<string | ObjectID | number> =
704
+ ((value as IncludesNone).values as Array<string | ObjectID>) || [];
705
+ if (excludeValues.length > 0) {
706
+ whereStatement.append(
707
+ SQL`AND ${key} NOT IN ${{
708
+ value: value,
709
+ type: tableColumn.type,
710
+ }}`,
711
+ );
712
+ }
678
713
  } else if (value instanceof IsNull) {
679
714
  if (tableColumn.type === TableColumnType.Text) {
680
715
  whereStatement.append(SQL`AND (${key} IS NULL OR ${key} = '')`);
@@ -0,0 +1,28 @@
1
+ export default class EventLoop {
2
+ /**
3
+ * Yields control back to the Node.js event loop, resuming on a later
4
+ * "check" phase tick.
5
+ *
6
+ * Use this to break up CPU-bound synchronous work — e.g. transforming
7
+ * tens of thousands of OTLP datapoints / spans / log records inside a
8
+ * single ingest job — so the process can service pending I/O between
9
+ * chunks. Most importantly, this lets the Kubernetes liveness and
10
+ * readiness probe handlers (/status/live, /status/ready) run. Without a
11
+ * periodic yield a large batch pins the single-threaded event loop for
12
+ * seconds at a time, the probe HTTP handler never gets scheduled, the
13
+ * kubelet marks the probe as failed, and the pod is restarted.
14
+ *
15
+ * IMPORTANT: this is implemented with setImmediate, NOT
16
+ * `await Promise.resolve()`. Awaiting a resolved promise only drains the
17
+ * microtask queue; the event loop never advances to its poll phase, so
18
+ * queued I/O (including probe requests) stays starved no matter how often
19
+ * you await. setImmediate schedules a macrotask in the check phase, which
20
+ * runs only after the poll phase has had a chance to process I/O — so a
21
+ * loop that periodically awaits this genuinely unblocks the event loop.
22
+ */
23
+ public static async yieldToEventLoop(): Promise<void> {
24
+ return new Promise<void>((resolve: () => void) => {
25
+ setImmediate(resolve);
26
+ });
27
+ }
28
+ }
@@ -35,6 +35,8 @@ const templateDashboardLinkVariableMap: Partial<
35
35
  [WhatsAppTemplateIds.IncidentStateChangedOwnerNotification]: "incident_link",
36
36
  [WhatsAppTemplateIds.IncidentOwnerAddedNotification]: "incident_link",
37
37
  [WhatsAppTemplateIds.IncidentMemberAddedNotification]: "incident_link",
38
+ [WhatsAppTemplateIds.IncidentReminderOwnerNotification]: "incident_link",
39
+ [WhatsAppTemplateIds.AlertReminderOwnerNotification]: "alert_link",
38
40
  [WhatsAppTemplateIds.MonitorOwnerAddedNotification]: "monitor_link",
39
41
  [WhatsAppTemplateIds.MonitorCreatedOwnerNotification]: "monitor_link",
40
42
  [WhatsAppTemplateIds.MonitorStatusChangedOwnerNotification]: "monitor_link",
@@ -48,6 +50,8 @@ const templateDashboardLinkVariableMap: Partial<
48
50
  "maintenance_link",
49
51
  [WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification]:
50
52
  "maintenance_link",
53
+ [WhatsAppTemplateIds.ScheduledMaintenanceReminderOwnerNotification]:
54
+ "event_link",
51
55
  [WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification]:
52
56
  "status_page_link",
53
57
  [WhatsAppTemplateIds.StatusPageCreatedOwnerNotification]: "status_page_link",
@@ -78,6 +82,10 @@ const templateIdByEventType: Record<
78
82
  WhatsAppTemplateIds.IncidentOwnerAddedNotification,
79
83
  [NotificationSettingEventType.SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION]:
80
84
  WhatsAppTemplateIds.IncidentMemberAddedNotification,
85
+ [NotificationSettingEventType.SEND_INCIDENT_REMINDER_OWNER_NOTIFICATION]:
86
+ WhatsAppTemplateIds.IncidentReminderOwnerNotification,
87
+ [NotificationSettingEventType.SEND_ALERT_REMINDER_OWNER_NOTIFICATION]:
88
+ WhatsAppTemplateIds.AlertReminderOwnerNotification,
81
89
  [NotificationSettingEventType.SEND_ALERT_CREATED_OWNER_NOTIFICATION]:
82
90
  WhatsAppTemplateIds.AlertCreatedOwnerNotification,
83
91
  [NotificationSettingEventType.SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION]:
@@ -124,6 +132,8 @@ const templateIdByEventType: Record<
124
132
  WhatsAppTemplateIds.ScheduledMaintenanceOwnerAddedNotification,
125
133
  [NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION]:
126
134
  WhatsAppTemplateIds.ScheduledMaintenanceStateChangedOwnerNotification,
135
+ [NotificationSettingEventType.SEND_SCHEDULED_MAINTENANCE_REMINDER_OWNER_NOTIFICATION]:
136
+ WhatsAppTemplateIds.ScheduledMaintenanceReminderOwnerNotification,
127
137
  [NotificationSettingEventType.SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION]:
128
138
  WhatsAppTemplateIds.StatusPageAnnouncementCreatedOwnerNotification,
129
139
  [NotificationSettingEventType.SEND_STATUS_PAGE_CREATED_OWNER_NOTIFICATION]:
@@ -672,6 +672,55 @@ describe("StatementGenerator", () => {
672
672
  expect(statement.query).toBe("");
673
673
  expect(statement.query_params).toStrictEqual({});
674
674
  });
675
+
676
+ test("emits NOT hasAny(...) for IncludesNone on an Array(String) column", () => {
677
+ const statement: Statement = arrayGenerator.toWhereStatement({
678
+ entityKeys: new IncludesNone(["210dac24142f1baa"]),
679
+ } as any);
680
+ expect(statement.query).toBe(
681
+ "AND NOT hasAny({p0:Identifier}, {p1:Array(String)})",
682
+ );
683
+ expect(statement.query_params).toStrictEqual({
684
+ p0: "entityKeys",
685
+ p1: ["210dac24142f1baa"],
686
+ });
687
+ });
688
+
689
+ test("drops empty IncludesNone instead of NOT hasAny(col, [])", () => {
690
+ const statement: Statement = arrayGenerator.toWhereStatement({
691
+ entityKeys: new IncludesNone([]),
692
+ } as any);
693
+ expect(statement.query).toBe("");
694
+ expect(statement.query_params).toStrictEqual({});
695
+ });
696
+ });
697
+
698
+ describe("scalar IncludesNone exclusion", () => {
699
+ /*
700
+ * "is none of" on a scalar column compiles to `col NOT IN (...)` —
701
+ * the exception monitor uses this to exclude occurrences of
702
+ * resolved/archived exception groups by fingerprint.
703
+ */
704
+ test("emits NOT IN (...) for IncludesNone on a Text column", () => {
705
+ const statement: Statement = generator.toWhereStatement({
706
+ column_1: new IncludesNone(["<fingerprint-1>", "<fingerprint-2>"]),
707
+ } as any);
708
+ expect(statement.query).toBe(
709
+ "AND {p0:Identifier} NOT IN {p1:Array(String)}",
710
+ );
711
+ expect(statement.query_params).toStrictEqual({
712
+ p0: "column_1",
713
+ p1: ["<fingerprint-1>", "<fingerprint-2>"],
714
+ });
715
+ });
716
+
717
+ test("drops empty IncludesNone instead of producing NOT IN ()", () => {
718
+ const statement: Statement = generator.toWhereStatement({
719
+ column_1: new IncludesNone([]),
720
+ } as any);
721
+ expect(statement.query).toBe("");
722
+ expect(statement.query_params).toStrictEqual({});
723
+ });
675
724
  });
676
725
 
677
726
  describe("entityScope synthetic key", () => {
@@ -37,11 +37,13 @@ enum EmailTemplateType {
37
37
  IncidentOwnerNotePosted = "IncidentOwnerNotePosted.hbs",
38
38
  IncidentOwnerResourceCreated = "IncidentOwnerResourceCreated.hbs",
39
39
  IncidentMemberAdded = "IncidentMemberAdded.hbs",
40
+ IncidentOwnerUnresolvedReminder = "IncidentOwnerUnresolvedReminder.hbs",
40
41
 
41
42
  AlertOwnerAdded = "AlertOwnerAdded.hbs",
42
43
  AlertOwnerStateChanged = "AlertOwnerStateChanged.hbs",
43
44
  AlertOwnerNotePosted = "AlertOwnerNotePosted.hbs",
44
45
  AlertOwnerResourceCreated = "AlertOwnerResourceCreated.hbs",
46
+ AlertOwnerUnresolvedReminder = "AlertOwnerUnresolvedReminder.hbs",
45
47
 
46
48
  AlertEpisodeOwnerAdded = "AlertEpisodeOwnerAdded.hbs",
47
49
  AlertEpisodeOwnerStateChanged = "AlertEpisodeOwnerStateChanged.hbs",
@@ -63,6 +65,7 @@ enum EmailTemplateType {
63
65
  ScheduledMaintenanceOwnerAdded = "ScheduledMaintenanceOwnerAdded.hbs",
64
66
  ScheduledMaintenanceOwnerStateChanged = "ScheduledMaintenanceOwnerStateChanged.hbs",
65
67
  ScheduledMaintenanceOwnerResourceCreated = "ScheduledMaintenanceOwnerResourceCreated.hbs",
68
+ ScheduledMaintenanceOwnerUnresolvedReminder = "ScheduledMaintenanceOwnerUnresolvedReminder.hbs",
66
69
  StatusPageOwnerResourceCreated = "StatusPageOwnerResourceCreated.hbs",
67
70
  StatusPageOwnerAdded = "StatusPageOwnerAdded.hbs",
68
71
  StatusPageOwnerAnnouncementPosted = "StatusPageOwnerAnnouncementPosted.hbs",
@@ -5,6 +5,7 @@ enum NotificationSettingEventType {
5
5
  SEND_INCIDENT_STATE_CHANGED_OWNER_NOTIFICATION = "Send incident state changed notification when I am the owner of the incident",
6
6
  SEND_INCIDENT_OWNER_ADDED_NOTIFICATION = "Send notification when I am added as a owner to the incident",
7
7
  SEND_INCIDENT_MEMBER_ADDED_NOTIFICATION = "Send notification when I am assigned to an incident as a member",
8
+ SEND_INCIDENT_REMINDER_OWNER_NOTIFICATION = "Send reminder notification when an incident I own is still open",
8
9
 
9
10
  // Alerts
10
11
 
@@ -12,6 +13,7 @@ enum NotificationSettingEventType {
12
13
  SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION = "Send alert note posted notification when I am the owner of the alert",
13
14
  SEND_ALERT_STATE_CHANGED_OWNER_NOTIFICATION = "Send alert state changed notification when I am the owner of the alert",
14
15
  SEND_ALERT_OWNER_ADDED_NOTIFICATION = "Send notification when I am added as a owner to the alert",
16
+ SEND_ALERT_REMINDER_OWNER_NOTIFICATION = "Send reminder notification when an alert I own is still open",
15
17
 
16
18
  // Alert Episodes
17
19
 
@@ -41,6 +43,7 @@ enum NotificationSettingEventType {
41
43
  SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = "Send event note posted notification when I am the owner of the event",
42
44
  SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = "Send notification when I am added as a owner to the event",
43
45
  SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = "Send event state changed notification when I am the owner of the event",
46
+ SEND_SCHEDULED_MAINTENANCE_REMINDER_OWNER_NOTIFICATION = "Send reminder notification when a scheduled maintenance event I own is still not complete",
44
47
 
45
48
  // Status Page
46
49
  SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION = "Send status page announcement created notification when I am the owner of the status page",
@@ -421,6 +421,12 @@ enum Permission {
421
421
  EditScheduledMaintenanceLabelRule = "EditScheduledMaintenanceLabelRule",
422
422
  ReadScheduledMaintenanceLabelRule = "ReadScheduledMaintenanceLabelRule",
423
423
 
424
+ // Scheduled Maintenance Reminder Rule Permissions
425
+ CreateScheduledMaintenanceReminderRule = "CreateScheduledMaintenanceReminderRule",
426
+ DeleteScheduledMaintenanceReminderRule = "DeleteScheduledMaintenanceReminderRule",
427
+ EditScheduledMaintenanceReminderRule = "EditScheduledMaintenanceReminderRule",
428
+ ReadScheduledMaintenanceReminderRule = "ReadScheduledMaintenanceReminderRule",
429
+
424
430
  CreateStatusPageOwnerTeam = "CreateStatusPageOwnerTeam",
425
431
  DeleteStatusPageOwnerTeam = "DeleteStatusPageOwnerTeam",
426
432
  EditStatusPageOwnerTeam = "EditStatusPageOwnerTeam",
@@ -1407,6 +1413,12 @@ enum Permission {
1407
1413
  EditAlertGroupingRule = "EditAlertGroupingRule",
1408
1414
  ReadAlertGroupingRule = "ReadAlertGroupingRule",
1409
1415
 
1416
+ // Alert Reminder Rule Permissions
1417
+ CreateAlertReminderRule = "CreateAlertReminderRule",
1418
+ DeleteAlertReminderRule = "DeleteAlertReminderRule",
1419
+ EditAlertReminderRule = "EditAlertReminderRule",
1420
+ ReadAlertReminderRule = "ReadAlertReminderRule",
1421
+
1410
1422
  // Alert On-Call Rule Permissions
1411
1423
  CreateAlertOnCallRule = "CreateAlertOnCallRule",
1412
1424
  DeleteAlertOnCallRule = "DeleteAlertOnCallRule",
@@ -1587,6 +1599,12 @@ enum Permission {
1587
1599
  EditIncidentSlaRule = "EditIncidentSlaRule",
1588
1600
  ReadIncidentSlaRule = "ReadIncidentSlaRule",
1589
1601
 
1602
+ // Incident Reminder Rule Permissions
1603
+ CreateIncidentReminderRule = "CreateIncidentReminderRule",
1604
+ DeleteIncidentReminderRule = "DeleteIncidentReminderRule",
1605
+ EditIncidentReminderRule = "EditIncidentReminderRule",
1606
+ ReadIncidentReminderRule = "ReadIncidentReminderRule",
1607
+
1590
1608
  // Incident SLA Permissions
1591
1609
  CreateIncidentSla = "CreateIncidentSla",
1592
1610
  DeleteIncidentSla = "DeleteIncidentSla",
@@ -11996,6 +12014,48 @@ export class PermissionHelper {
11996
12014
  group: PermissionGroup.ScheduledMaintenance,
11997
12015
  },
11998
12016
 
12017
+ // Scheduled Maintenance Reminder Rule Permissions
12018
+ {
12019
+ permission: Permission.CreateScheduledMaintenanceReminderRule,
12020
+ title: "Create Scheduled Maintenance Reminder Rule",
12021
+ description:
12022
+ "This permission can create Scheduled Maintenance Reminder Rules in this project.",
12023
+ isAssignableToTenant: true,
12024
+ isAccessControlPermission: false,
12025
+ isRolePermission: false,
12026
+ group: PermissionGroup.ScheduledMaintenance,
12027
+ },
12028
+ {
12029
+ permission: Permission.DeleteScheduledMaintenanceReminderRule,
12030
+ title: "Delete Scheduled Maintenance Reminder Rule",
12031
+ description:
12032
+ "This permission can delete Scheduled Maintenance Reminder Rules of this project.",
12033
+ isAssignableToTenant: true,
12034
+ isAccessControlPermission: false,
12035
+ isRolePermission: false,
12036
+ group: PermissionGroup.ScheduledMaintenance,
12037
+ },
12038
+ {
12039
+ permission: Permission.EditScheduledMaintenanceReminderRule,
12040
+ title: "Edit Scheduled Maintenance Reminder Rule",
12041
+ description:
12042
+ "This permission can edit Scheduled Maintenance Reminder Rules of this project.",
12043
+ isAssignableToTenant: true,
12044
+ isAccessControlPermission: false,
12045
+ isRolePermission: false,
12046
+ group: PermissionGroup.ScheduledMaintenance,
12047
+ },
12048
+ {
12049
+ permission: Permission.ReadScheduledMaintenanceReminderRule,
12050
+ title: "Read Scheduled Maintenance Reminder Rule",
12051
+ description:
12052
+ "This permission can read Scheduled Maintenance Reminder Rules of this project.",
12053
+ isAssignableToTenant: true,
12054
+ isAccessControlPermission: false,
12055
+ isRolePermission: false,
12056
+ group: PermissionGroup.ScheduledMaintenance,
12057
+ },
12058
+
11999
12059
  // Alert Episode Permissions
12000
12060
  {
12001
12061
  permission: Permission.CreateAlertEpisode,
@@ -12120,6 +12180,48 @@ export class PermissionHelper {
12120
12180
  group: PermissionGroup.Alert,
12121
12181
  },
12122
12182
 
12183
+ // Alert Reminder Rule Permissions
12184
+ {
12185
+ permission: Permission.CreateAlertReminderRule,
12186
+ title: "Create Alert Reminder Rule",
12187
+ description:
12188
+ "This permission can create Alert Reminder Rules in this project.",
12189
+ isAssignableToTenant: true,
12190
+ isAccessControlPermission: false,
12191
+ isRolePermission: false,
12192
+ group: PermissionGroup.Alert,
12193
+ },
12194
+ {
12195
+ permission: Permission.DeleteAlertReminderRule,
12196
+ title: "Delete Alert Reminder Rule",
12197
+ description:
12198
+ "This permission can delete Alert Reminder Rules of this project.",
12199
+ isAssignableToTenant: true,
12200
+ isAccessControlPermission: false,
12201
+ isRolePermission: false,
12202
+ group: PermissionGroup.Alert,
12203
+ },
12204
+ {
12205
+ permission: Permission.EditAlertReminderRule,
12206
+ title: "Edit Alert Reminder Rule",
12207
+ description:
12208
+ "This permission can edit Alert Reminder Rules of this project.",
12209
+ isAssignableToTenant: true,
12210
+ isAccessControlPermission: false,
12211
+ isRolePermission: false,
12212
+ group: PermissionGroup.Alert,
12213
+ },
12214
+ {
12215
+ permission: Permission.ReadAlertReminderRule,
12216
+ title: "Read Alert Reminder Rule",
12217
+ description:
12218
+ "This permission can read Alert Reminder Rules of this project.",
12219
+ isAssignableToTenant: true,
12220
+ isAccessControlPermission: false,
12221
+ isRolePermission: false,
12222
+ group: PermissionGroup.Alert,
12223
+ },
12224
+
12123
12225
  // Alert On-Call Rule Permissions
12124
12226
  {
12125
12227
  permission: Permission.CreateAlertOnCallRule,
@@ -13402,6 +13504,48 @@ export class PermissionHelper {
13402
13504
  group: PermissionGroup.Incident,
13403
13505
  },
13404
13506
 
13507
+ // Incident Reminder Rule Permissions
13508
+ {
13509
+ permission: Permission.CreateIncidentReminderRule,
13510
+ title: "Create Incident Reminder Rule",
13511
+ description:
13512
+ "This permission can create Incident Reminder Rules in this project.",
13513
+ isAssignableToTenant: true,
13514
+ isAccessControlPermission: false,
13515
+ isRolePermission: false,
13516
+ group: PermissionGroup.Incident,
13517
+ },
13518
+ {
13519
+ permission: Permission.DeleteIncidentReminderRule,
13520
+ title: "Delete Incident Reminder Rule",
13521
+ description:
13522
+ "This permission can delete Incident Reminder Rules of this project.",
13523
+ isAssignableToTenant: true,
13524
+ isAccessControlPermission: false,
13525
+ isRolePermission: false,
13526
+ group: PermissionGroup.Incident,
13527
+ },
13528
+ {
13529
+ permission: Permission.EditIncidentReminderRule,
13530
+ title: "Edit Incident Reminder Rule",
13531
+ description:
13532
+ "This permission can edit Incident Reminder Rules of this project.",
13533
+ isAssignableToTenant: true,
13534
+ isAccessControlPermission: false,
13535
+ isRolePermission: false,
13536
+ group: PermissionGroup.Incident,
13537
+ },
13538
+ {
13539
+ permission: Permission.ReadIncidentReminderRule,
13540
+ title: "Read Incident Reminder Rule",
13541
+ description:
13542
+ "This permission can read Incident Reminder Rules of this project.",
13543
+ isAssignableToTenant: true,
13544
+ isAccessControlPermission: false,
13545
+ isRolePermission: false,
13546
+ group: PermissionGroup.Incident,
13547
+ },
13548
+
13405
13549
  // Incident SLA Permissions
13406
13550
  {
13407
13551
  permission: Permission.CreateIncidentSla,
@@ -0,0 +1,6 @@
1
+ enum ReminderStopState {
2
+ Acknowledged = "Acknowledged",
3
+ Resolved = "Resolved",
4
+ }
5
+
6
+ export default ReminderStopState;
@@ -0,0 +1,6 @@
1
+ enum ScheduledMaintenanceReminderStopState {
2
+ Ongoing = "Ongoing",
3
+ Completed = "Completed",
4
+ }
5
+
6
+ export default ScheduledMaintenanceReminderStopState;