@oneuptime/common 14.0.6 → 14.0.7

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 (127) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  10. package/Server/Services/UserNotificationRuleService.ts +55 -53
  11. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  12. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  13. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  14. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  15. package/Server/Utils/Response.ts +31 -0
  16. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  17. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  18. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  19. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  20. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  21. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  22. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  23. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  24. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  25. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  26. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  27. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  28. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  29. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  30. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  31. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  32. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  33. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  34. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  35. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  36. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  37. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  38. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  39. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  40. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  41. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  42. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  43. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  44. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  45. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  46. package/Tests/Server/Services/DomainService.test.ts +525 -0
  47. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  48. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  49. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  50. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  51. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  52. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  53. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  54. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  55. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  56. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  57. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  58. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  59. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  60. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  61. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  62. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  63. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  64. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  65. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  66. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  67. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  68. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  69. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  70. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  71. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  72. package/Types/Incident/IncidentAlertLink.ts +19 -0
  73. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  74. package/Types/Topology/TopologyApi.ts +428 -0
  75. package/Types/Topology/TopologyTypeRules.ts +191 -0
  76. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  77. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  78. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  83. package/build/dist/Server/Services/AlertService.js +21 -33
  84. package/build/dist/Server/Services/AlertService.js.map +1 -1
  85. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  86. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  87. package/build/dist/Server/Services/IncidentService.js +96 -42
  88. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  89. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  90. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  91. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  92. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  93. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  94. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  95. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  96. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  97. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  98. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  99. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  100. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  101. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  102. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  103. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  104. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  105. package/build/dist/Server/Utils/Response.js +30 -0
  106. package/build/dist/Server/Utils/Response.js.map +1 -1
  107. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  108. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  109. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  110. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  111. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  112. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  113. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  114. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  115. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  116. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  117. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  118. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  119. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  120. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  121. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  122. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  123. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  124. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  125. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  126. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  127. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { compareCodePoints } from "../../../Types/Topology/TopologyTypeRules";
3
+
4
+ /*
5
+ * compareCodePoints is how the Dashboard breaks the ties the server breaks
6
+ * with COLLATE "C" (code-point order on a UTF-8 database). It must equal
7
+ * JavaScript's `<` for every string made of characters up to U+FFFF, and
8
+ * order characters beyond U+FFFF (surrogate pairs) after U+E000-U+FFFF,
9
+ * which `<` does not.
10
+ */
11
+
12
+ function sign(value: number): number {
13
+ return value < 0 ? -1 : value > 0 ? 1 : 0;
14
+ }
15
+
16
+ /* An independent reference: compare the strings' code points one by one. */
17
+ function referenceCompare(a: string, b: string): number {
18
+ const left: Array<number> = Array.from(a).map((char: string): number => {
19
+ return char.codePointAt(0)!;
20
+ });
21
+ const right: Array<number> = Array.from(b).map((char: string): number => {
22
+ return char.codePointAt(0)!;
23
+ });
24
+ const length: number = Math.min(left.length, right.length);
25
+ for (let index: number = 0; index < length; index++) {
26
+ if (left[index] !== right[index]) {
27
+ return left[index]! < right[index]! ? -1 : 1;
28
+ }
29
+ }
30
+ return sign(left.length - right.length);
31
+ }
32
+
33
+ function codeUnitCompare(a: string, b: string): number {
34
+ return a < b ? -1 : a > b ? 1 : 0;
35
+ }
36
+
37
+ /* A small deterministic generator, so a failure names its inputs. */
38
+ function makeRandom(seed: number): () => number {
39
+ let state: number = seed >>> 0;
40
+ return (): number => {
41
+ state = (state * 1664525 + 1013904223) >>> 0;
42
+ return state / 4294967296;
43
+ };
44
+ }
45
+
46
+ /* Characters from the ranges where the two orders meet or part ways. */
47
+ const ALPHABET: Array<string> = [
48
+ "a",
49
+ "b",
50
+ "z",
51
+ "0",
52
+ "-",
53
+ "é",
54
+ "中",
55
+ "퟿",
56
+ "",
57
+ "",
58
+ "fi",
59
+ "�",
60
+ "￿",
61
+ "\u{10000}",
62
+ "\u{1F600}",
63
+ "\u{1F601}",
64
+ "\u{10FFFF}",
65
+ ];
66
+
67
+ function randomString(random: () => number, alphabet: Array<string>): string {
68
+ const length: number = Math.floor(random() * 5);
69
+ let out: string = "";
70
+ for (let index: number = 0; index < length; index++) {
71
+ out += alphabet[Math.floor(random() * alphabet.length)]!;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ describe("compareCodePoints", () => {
77
+ test("equal strings compare equal", () => {
78
+ expect(compareCodePoints("", "")).toBe(0);
79
+ expect(compareCodePoints("node-a", "node-a")).toBe(0);
80
+ expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(0);
81
+ });
82
+
83
+ test("inside the Basic Multilingual Plane it orders exactly like `<`", () => {
84
+ const pairs: Array<[string, string]> = [
85
+ ["a", "b"],
86
+ ["node-a", "node-b"],
87
+ ["node", "node-a"],
88
+ ["", "a"],
89
+ ["Z", "a"],
90
+ ["a", "é"],
91
+ ["퟿", ""],
92
+ ["", "�"],
93
+ ["�", "￿"],
94
+ ["0123456789abcdef", "0123456789abcdf0"],
95
+ ];
96
+ for (const [a, b] of pairs) {
97
+ expect(compareCodePoints(a, b)).toBe(-1);
98
+ expect(compareCodePoints(b, a)).toBe(1);
99
+ expect(sign(compareCodePoints(a, b))).toBe(codeUnitCompare(a, b));
100
+ }
101
+ });
102
+
103
+ test("a character beyond U+FFFF sorts after U+E000..U+FFFF, where `<` puts it before", () => {
104
+ for (const high of ["", "", "�", "￿"]) {
105
+ for (const astral of ["\u{10000}", "\u{1F600}", "\u{10FFFF}"]) {
106
+ expect(compareCodePoints(high, astral)).toBe(-1);
107
+ expect(compareCodePoints(astral, high)).toBe(1);
108
+ expect(compareCodePoints(`node-${high}`, `node-${astral}`)).toBe(-1);
109
+ expect(compareCodePoints(`node-${astral}x`, `node-${high}`)).toBe(1);
110
+ /* The order `<` gives, which is what this exists to correct. */
111
+ expect(codeUnitCompare(high, astral)).toBe(1);
112
+ }
113
+ }
114
+ });
115
+
116
+ test("a character beyond U+FFFF still sorts after everything below U+D800", () => {
117
+ expect(compareCodePoints("퟿", "\u{10000}")).toBe(-1);
118
+ expect(compareCodePoints("z", "\u{1F600}")).toBe(-1);
119
+ expect(compareCodePoints("\u{1F600}", "z")).toBe(1);
120
+ });
121
+
122
+ test("two characters beyond U+FFFF compare by code point, high or low half", () => {
123
+ /* Different high surrogates (U+10000 is D800 DC00, U+1F600 is D83D DE00). */
124
+ expect(compareCodePoints("\u{10000}", "\u{1F600}")).toBe(-1);
125
+ /* Same high surrogate, different low surrogates. */
126
+ expect(compareCodePoints("\u{1F600}", "\u{1F601}")).toBe(-1);
127
+ expect(compareCodePoints("x\u{1F601}", "x\u{1F600}")).toBe(1);
128
+ });
129
+
130
+ test("a prefix sorts first", () => {
131
+ expect(compareCodePoints("node", "node-\u{1F600}")).toBe(-1);
132
+ expect(compareCodePoints("node-\u{1F600}", "node")).toBe(1);
133
+ expect(compareCodePoints("\u{1F600}", "\u{1F600}")).toBe(-1);
134
+ });
135
+
136
+ test("agrees with a code-point reference on thousands of mixed strings", () => {
137
+ const random: () => number = makeRandom(3973);
138
+ let disagreementsWithCodeUnits: number = 0;
139
+ for (let round: number = 0; round < 20000; round++) {
140
+ const a: string = randomString(random, ALPHABET);
141
+ const b: string = randomString(random, ALPHABET);
142
+ const expected: number = referenceCompare(a, b);
143
+ if (sign(compareCodePoints(a, b)) !== expected) {
144
+ throw new Error(
145
+ `compareCodePoints(${JSON.stringify(a)}, ${JSON.stringify(b)}) should be ${expected}`,
146
+ );
147
+ }
148
+ if (codeUnitCompare(a, b) !== expected) {
149
+ disagreementsWithCodeUnits++;
150
+ }
151
+ }
152
+ /* The inputs really did reach the cases where `<` is wrong. */
153
+ expect(disagreementsWithCodeUnits).toBeGreaterThan(100);
154
+ });
155
+
156
+ test('sorts a list the way COLLATE "C" would', () => {
157
+ const keys: Array<string> = [
158
+ "node-\u{1F600}",
159
+ "node-�",
160
+ "node-b",
161
+ "node-",
162
+ "node-a",
163
+ "node-\u{10000}",
164
+ "node",
165
+ ];
166
+ expect([...keys].sort(compareCodePoints)).toEqual([
167
+ "node",
168
+ "node-a",
169
+ "node-b",
170
+ "node-",
171
+ "node-�",
172
+ "node-\u{10000}",
173
+ "node-\u{1F600}",
174
+ ]);
175
+ });
176
+ });
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "@jest/globals";
2
2
  import IncidentFromAlerts, {
3
+ AlertForAcknowledgement,
3
4
  AlertForIncidentPrefill,
5
+ AlertsToAcknowledge,
6
+ AlertStateForAcknowledgement,
4
7
  INCIDENT_PREFILL_RESOURCE_KEYS,
5
8
  IncidentPrefillFromAlerts,
6
9
  NamedResource,
@@ -694,3 +697,302 @@ describe("IncidentFromAlerts", () => {
694
697
  });
695
698
  });
696
699
  });
700
+
701
+ /*
702
+ * Which alerts the create page offers to acknowledge as the incident is
703
+ * declared. States are compared by order, as the server does: anything at or
704
+ * after the project's Acknowledged state (Resolved, or a custom state in
705
+ * between) has been acknowledged already. An alert whose state cannot be
706
+ * placed is offered - the server skips it if it turns out to be acknowledged.
707
+ */
708
+ describe("IncidentFromAlerts.getAlertsToAcknowledge", () => {
709
+ const CREATED: AlertStateForAcknowledgement = {
710
+ id: "state-created",
711
+ order: 1,
712
+ isAcknowledgedState: false,
713
+ };
714
+ const ACKNOWLEDGED: AlertStateForAcknowledgement = {
715
+ id: "state-acknowledged",
716
+ order: 2,
717
+ isAcknowledgedState: true,
718
+ };
719
+ const RESOLVED: AlertStateForAcknowledgement = {
720
+ id: "state-resolved",
721
+ order: 3,
722
+ isAcknowledgedState: false,
723
+ };
724
+
725
+ const DEFAULT_ALERT_STATES: Array<AlertStateForAcknowledgement> = [
726
+ CREATED,
727
+ ACKNOWLEDGED,
728
+ RESOLVED,
729
+ ];
730
+
731
+ type AlertInStateFunction = (
732
+ id: string,
733
+ currentAlertStateId?: string | undefined,
734
+ ) => AlertForAcknowledgement;
735
+
736
+ const alertIn: AlertInStateFunction = (
737
+ id: string,
738
+ currentAlertStateId?: string | undefined,
739
+ ): AlertForAcknowledgement => {
740
+ return { id: id, currentAlertStateId: currentAlertStateId };
741
+ };
742
+
743
+ type GetFunction = (
744
+ alerts: Array<AlertForAcknowledgement>,
745
+ alertStates?: Array<AlertStateForAcknowledgement>,
746
+ ) => AlertsToAcknowledge | null;
747
+
748
+ const get: GetFunction = (
749
+ alerts: Array<AlertForAcknowledgement>,
750
+ alertStates?: Array<AlertStateForAcknowledgement>,
751
+ ): AlertsToAcknowledge | null => {
752
+ return IncidentFromAlerts.getAlertsToAcknowledge({
753
+ alerts: alerts,
754
+ alertStates: alertStates || DEFAULT_ALERT_STATES,
755
+ });
756
+ };
757
+
758
+ describe("when nothing could be acknowledged", () => {
759
+ test("is null when the project has no alert states at all", () => {
760
+ expect(get([alertIn("a1", CREATED.id)], [])).toBeNull();
761
+ });
762
+
763
+ test("is null when no state is the Acknowledged state", () => {
764
+ expect(
765
+ get(
766
+ [alertIn("a1", CREATED.id)],
767
+ [CREATED, { ...ACKNOWLEDGED, isAcknowledgedState: false }, RESOLVED],
768
+ ),
769
+ ).toBeNull();
770
+ });
771
+
772
+ test("is null when the Acknowledged flag is missing rather than false", () => {
773
+ expect(
774
+ get(
775
+ [alertIn("a1", CREATED.id)],
776
+ [
777
+ CREATED,
778
+ { id: ACKNOWLEDGED.id, order: ACKNOWLEDGED.order },
779
+ RESOLVED,
780
+ ],
781
+ ),
782
+ ).toBeNull();
783
+ });
784
+
785
+ test("is null when the Acknowledged state has no order to compare by", () => {
786
+ expect(
787
+ get(
788
+ [alertIn("a1", CREATED.id)],
789
+ [CREATED, { ...ACKNOWLEDGED, order: undefined }, RESOLVED],
790
+ ),
791
+ ).toBeNull();
792
+ });
793
+
794
+ test("is null without an Acknowledged state even when there are no alerts", () => {
795
+ expect(get([], [CREATED, RESOLVED])).toBeNull();
796
+ });
797
+ });
798
+
799
+ describe("comparing by order", () => {
800
+ test("offers an alert whose state comes before Acknowledged", () => {
801
+ expect(get([alertIn("a1", CREATED.id)])).toEqual({
802
+ alertIds: ["a1"],
803
+ alreadyAcknowledgedCount: 0,
804
+ });
805
+ });
806
+
807
+ test("leaves out an alert that is in the Acknowledged state itself", () => {
808
+ expect(get([alertIn("a1", ACKNOWLEDGED.id)])).toEqual({
809
+ alertIds: [],
810
+ alreadyAcknowledgedCount: 1,
811
+ });
812
+ });
813
+
814
+ test("leaves out an alert that is resolved", () => {
815
+ expect(get([alertIn("a1", RESOLVED.id)])).toEqual({
816
+ alertIds: [],
817
+ alreadyAcknowledgedCount: 1,
818
+ });
819
+ });
820
+
821
+ test("treats a custom state between Acknowledged and Resolved as acknowledged already", () => {
822
+ const investigating: AlertStateForAcknowledgement = {
823
+ id: "state-investigating",
824
+ order: 3,
825
+ };
826
+ const resolved: AlertStateForAcknowledgement = {
827
+ id: "state-resolved",
828
+ order: 4,
829
+ };
830
+
831
+ expect(
832
+ get(
833
+ [alertIn("a1", investigating.id), alertIn("a2", resolved.id)],
834
+ [CREATED, ACKNOWLEDGED, investigating, resolved],
835
+ ),
836
+ ).toEqual({ alertIds: [], alreadyAcknowledgedCount: 2 });
837
+ });
838
+
839
+ test("offers an alert in a custom state before Acknowledged", () => {
840
+ const triaged: AlertStateForAcknowledgement = {
841
+ id: "state-triaged",
842
+ order: 2,
843
+ };
844
+ const acknowledged: AlertStateForAcknowledgement = {
845
+ id: "state-acknowledged",
846
+ order: 3,
847
+ isAcknowledgedState: true,
848
+ };
849
+
850
+ expect(
851
+ get(
852
+ [alertIn("a1", triaged.id), alertIn("a2", CREATED.id)],
853
+ [CREATED, triaged, acknowledged, { ...RESOLVED, order: 4 }],
854
+ ),
855
+ ).toEqual({ alertIds: ["a1", "a2"], alreadyAcknowledgedCount: 0 });
856
+ });
857
+
858
+ test("compares by order, not by where the state sits in the list", () => {
859
+ // Listed newest-first: the list position must not decide anything.
860
+ const reversed: Array<AlertStateForAcknowledgement> = [
861
+ RESOLVED,
862
+ ACKNOWLEDGED,
863
+ CREATED,
864
+ ];
865
+
866
+ expect(
867
+ get(
868
+ [
869
+ alertIn("a1", RESOLVED.id),
870
+ alertIn("a2", CREATED.id),
871
+ alertIn("a3", ACKNOWLEDGED.id),
872
+ ],
873
+ reversed,
874
+ ),
875
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
876
+ });
877
+
878
+ test("an Acknowledged order of 0 is still an order", () => {
879
+ const acknowledgedAtZero: AlertStateForAcknowledgement = {
880
+ id: "state-acknowledged",
881
+ order: 0,
882
+ isAcknowledgedState: true,
883
+ };
884
+ const before: AlertStateForAcknowledgement = {
885
+ id: "state-before",
886
+ order: -1,
887
+ };
888
+
889
+ expect(
890
+ get(
891
+ [
892
+ alertIn("a1", acknowledgedAtZero.id),
893
+ alertIn("a2", before.id),
894
+ alertIn("a3", RESOLVED.id),
895
+ ],
896
+ [before, acknowledgedAtZero, RESOLVED],
897
+ ),
898
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 2 });
899
+ });
900
+ });
901
+
902
+ describe("alerts whose state cannot be placed", () => {
903
+ test("offers an alert whose current state is not one of the project's states", () => {
904
+ expect(get([alertIn("a1", "state-from-another-project")])).toEqual({
905
+ alertIds: ["a1"],
906
+ alreadyAcknowledgedCount: 0,
907
+ });
908
+ });
909
+
910
+ test("offers an alert with no current state id", () => {
911
+ expect(get([alertIn("a1", undefined), alertIn("a2", "")])).toEqual({
912
+ alertIds: ["a1", "a2"],
913
+ alreadyAcknowledgedCount: 0,
914
+ });
915
+ });
916
+
917
+ test("offers an alert whose current state has no order", () => {
918
+ const unordered: AlertStateForAcknowledgement = {
919
+ id: "state-unordered",
920
+ };
921
+
922
+ expect(
923
+ get(
924
+ [alertIn("a1", unordered.id)],
925
+ [CREATED, ACKNOWLEDGED, RESOLVED, unordered],
926
+ ),
927
+ ).toEqual({ alertIds: ["a1"], alreadyAcknowledgedCount: 0 });
928
+ });
929
+ });
930
+
931
+ test("matches state ids case-insensitively and ignoring surrounding spaces", () => {
932
+ const upperAcknowledged: AlertStateForAcknowledgement = {
933
+ id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000002",
934
+ order: 2,
935
+ isAcknowledgedState: true,
936
+ };
937
+ const upperCreated: AlertStateForAcknowledgement = {
938
+ id: "AAAAAAAA-AAAA-4AAA-8AAA-000000000001",
939
+ order: 1,
940
+ };
941
+
942
+ expect(
943
+ get(
944
+ [
945
+ alertIn("a1", " aaaaaaaa-aaaa-4aaa-8aaa-000000000002 "),
946
+ alertIn("a2", "aaaaaaaa-aaaa-4aaa-8aaa-000000000001"),
947
+ ],
948
+ [upperCreated, upperAcknowledged],
949
+ ),
950
+ ).toEqual({ alertIds: ["a2"], alreadyAcknowledgedCount: 1 });
951
+ });
952
+
953
+ test("keeps the alerts in the order they were given and counts the rest", () => {
954
+ const result: AlertsToAcknowledge | null = get([
955
+ alertIn("a5", CREATED.id),
956
+ alertIn("a1", ACKNOWLEDGED.id),
957
+ alertIn("a4", undefined),
958
+ alertIn("a2", RESOLVED.id),
959
+ alertIn("a3", CREATED.id),
960
+ alertIn("a6", RESOLVED.id),
961
+ ]);
962
+
963
+ expect(result).toEqual({
964
+ alertIds: ["a5", "a4", "a3"],
965
+ alreadyAcknowledgedCount: 3,
966
+ });
967
+ });
968
+
969
+ test("returns the alert ids exactly as given", () => {
970
+ expect(get([alertIn("ALERT-One", CREATED.id)])!.alertIds).toEqual([
971
+ "ALERT-One",
972
+ ]);
973
+ });
974
+
975
+ test("is empty, not null, when there are no alerts", () => {
976
+ expect(get([])).toEqual({ alertIds: [], alreadyAcknowledgedCount: 0 });
977
+ });
978
+
979
+ test("does not change its input", () => {
980
+ const alerts: Array<AlertForAcknowledgement> = [
981
+ alertIn("a1", CREATED.id),
982
+ alertIn("a2", ACKNOWLEDGED.id),
983
+ ];
984
+ const alertStates: Array<AlertStateForAcknowledgement> = [
985
+ RESOLVED,
986
+ CREATED,
987
+ ACKNOWLEDGED,
988
+ ];
989
+
990
+ get(alerts, alertStates);
991
+
992
+ expect(alerts).toEqual([
993
+ { id: "a1", currentAlertStateId: CREATED.id },
994
+ { id: "a2", currentAlertStateId: ACKNOWLEDGED.id },
995
+ ]);
996
+ expect(alertStates).toEqual([RESOLVED, CREATED, ACKNOWLEDGED]);
997
+ });
998
+ });
@@ -12,6 +12,25 @@
12
12
  */
13
13
  export const INCIDENT_ALERT_IDS_TO_LINK_KEY: string = "alertIdsToLink";
14
14
 
15
+ /*
16
+ * Declaring an incident from alerts does not, on its own, change the alerts:
17
+ * they keep escalating until somebody acknowledges them. Sending
18
+ * `miscDataProps[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY] = true` along with
19
+ * INCIDENT_ALERT_IDS_TO_LINK_KEY also acknowledges them, as the declaring
20
+ * user, once they are linked - which is what stops their on-call escalation.
21
+ * Alerts already acknowledged (or past it) are left as they are, and the
22
+ * caller must be allowed to change the state of every alert that is not.
23
+ * Off unless asked for.
24
+ *
25
+ * One exception: when the incident is declared straight into an
26
+ * acknowledged or resolved state and the project's linked-alert switches
27
+ * already move linked alerts along with it, those alerts are left to the
28
+ * switches (one writer per alert) - acknowledged or resolved as the switch
29
+ * does it, not credited to the declaring user.
30
+ */
31
+ export const INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY: string =
32
+ "acknowledgeAlertsToLink";
33
+
15
34
  /*
16
35
  * The create-incident page reads the alerts to declare an incident from out of
17
36
  * this query string parameter, as a comma separated list of alert ids.
@@ -54,3 +54,29 @@ export const getInventoryLivenessState: (
54
54
 
55
55
  return InventoryLiveness.Stale;
56
56
  };
57
+
58
+ /**
59
+ * `getInventoryLivenessState` as a Postgres expression over the row aliased
60
+ * `tableAlias`, which must already be quoted (`"InventoryItem"`).
61
+ *
62
+ * The one SQL definition of the rule: the `inventoryStatus` virtual column
63
+ * selects and filters with it, and the Inventory Overview counts "Gone
64
+ * Quiet" with it, so the number on that tile and the rows the Stale facet
65
+ * returns cannot disagree about what stale means.
66
+ *
67
+ * Measured against the database clock. The function above floors elapsed
68
+ * minutes before applying inclusive thresholds — 30m59s is still Live and
69
+ * 24h00m59s is still Recent — and strict comparisons against the following
70
+ * minute preserve exactly those boundaries.
71
+ */
72
+ export const getInventoryLivenessSql: (tableAlias: string) => string = (
73
+ tableAlias: string,
74
+ ): string => {
75
+ return `CASE
76
+ WHEN ${tableAlias}."source" IS DISTINCT FROM '${EntitySource.Discovered}' THEN '${InventoryLiveness.NotTracked}'
77
+ WHEN ${tableAlias}."lastSeenAt" IS NULL THEN '${InventoryLiveness.Never}'
78
+ WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_LIVE_WINDOW_MINUTES + 1} minutes' THEN '${InventoryLiveness.Live}'
79
+ WHEN ${tableAlias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_STALE_AFTER_MINUTES + 1} minutes' THEN '${InventoryLiveness.Recent}'
80
+ ELSE '${InventoryLiveness.Stale}'
81
+ END`;
82
+ };