@oneuptime/common 12.0.18 → 12.0.20

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 (262) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  11. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  12. package/Server/API/UserSlackAPI.ts +125 -0
  13. package/Server/EnvironmentConfig.ts +29 -107
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  19. package/Server/Infrastructure/Queue.ts +7 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +171 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Response.ts +2 -2
  40. package/Server/Utils/StartServer.ts +2 -2
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  42. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  43. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  44. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  45. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  46. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  47. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  48. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  49. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  50. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  51. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  52. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  53. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  54. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  55. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  56. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  57. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  58. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  59. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  60. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  61. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  62. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  63. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  64. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  65. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  66. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  67. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  68. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  69. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  70. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  71. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  72. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  73. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  74. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  75. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  76. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  77. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  78. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  79. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  80. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  81. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  82. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  83. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  84. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  85. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  86. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  87. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  88. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  89. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  90. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  91. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  92. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  93. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  94. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  95. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  97. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  98. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  99. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  100. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  101. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  102. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  103. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  104. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  105. package/Types/Analytics/RevenueEvent.ts +1 -0
  106. package/Types/Marketing/MarketingEvent.ts +140 -0
  107. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  108. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  109. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  110. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  111. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  112. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  113. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  114. package/UI/Components/Detail/Detail.tsx +18 -1
  115. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  116. package/UI/Utils/PermissionGate.ts +56 -0
  117. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  118. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  119. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  121. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  123. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  125. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  126. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  127. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  129. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  131. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  133. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  134. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  135. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  136. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  137. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  138. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  139. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  140. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  141. package/build/dist/Server/EnvironmentConfig.js +25 -70
  142. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  152. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  153. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  154. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  155. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  156. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  157. package/build/dist/Server/Services/Index.js +6 -2
  158. package/build/dist/Server/Services/Index.js.map +1 -1
  159. package/build/dist/Server/Services/LlmLogService.js +25 -9
  160. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  161. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  162. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  163. package/build/dist/Server/Services/ProjectService.js +144 -0
  164. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  165. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  166. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  167. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  168. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  169. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  170. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  171. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  172. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  173. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  174. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  175. package/build/dist/Server/Services/UserService.js +26 -1
  176. package/build/dist/Server/Services/UserService.js.map +1 -1
  177. package/build/dist/Server/Services/UserSlackService.js +195 -0
  178. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  179. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  180. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  181. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  182. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  183. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  184. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  185. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  186. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  187. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  188. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  190. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  192. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  193. package/build/dist/Server/Utils/Response.js +2 -2
  194. package/build/dist/Server/Utils/Response.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +2 -2
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  198. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  199. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  200. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  201. package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
  202. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  203. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  204. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  205. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  206. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  207. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  208. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  209. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  210. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  211. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  212. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  213. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  214. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  215. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  216. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  217. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  218. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  219. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  220. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  221. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  222. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  223. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  224. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  225. package/package.json +1 -1
  226. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  227. package/Server/Services/MarketingConversionService.ts +0 -10
  228. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  229. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  230. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  231. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  232. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  233. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  234. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  235. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  236. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  237. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  238. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  239. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  240. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  241. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  242. package/Types/Marketing/MarketingConversion.ts +0 -53
  243. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  244. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  245. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  246. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  247. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  248. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  249. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  250. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  251. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  252. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  253. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  254. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  255. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  256. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  257. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  258. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  259. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  260. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  261. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  262. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,368 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ cleanup,
12
+ fireEvent,
13
+ render,
14
+ screen,
15
+ waitFor,
16
+ } from "@testing-library/react";
17
+ import * as React from "react";
18
+ import getJestMockFunction, { MockFunction } from "../../MockType";
19
+
20
+ /*
21
+ * The per-series investigate menu is the chart's bridge from "this series
22
+ * is spiking" to the signals that explain it: the chip's magnifier must
23
+ * open a menu whose pivots carry the series' group-by labels as REAL
24
+ * filters (logs/traces URLs), whose "Filter to this series" narrows the
25
+ * live explorer view, and whose resource entry lands on the host page the
26
+ * series is about. Rendering the real MetricCharts (not a stub) pins the
27
+ * whole chain: series naming → chip → menu → scoped URL.
28
+ */
29
+
30
+ /*
31
+ * ResponsiveContainer measures its parent, which is always 0x0 in jsdom,
32
+ * so the chart renders nothing without a fixed size.
33
+ */
34
+ jest.mock("recharts", () => {
35
+ const actual: Record<string, any> = jest.requireActual("recharts") as Record<
36
+ string,
37
+ any
38
+ >;
39
+ return {
40
+ ...actual,
41
+ ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
42
+ return React.cloneElement(children, {
43
+ width: 600,
44
+ height: 300,
45
+ } as Record<string, unknown>);
46
+ },
47
+ };
48
+ });
49
+
50
+ const getListMock: MockFunction = getJestMockFunction();
51
+ const fetchExemplarsMock: MockFunction = getJestMockFunction();
52
+ const showToastMock: MockFunction = getJestMockFunction();
53
+
54
+ /*
55
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
56
+ * compiled requires, so the mock variables above are still unassigned when
57
+ * the factory runs. Dereferencing them lazily, at call time, is what makes
58
+ * this work.
59
+ */
60
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
61
+ return {
62
+ __esModule: true,
63
+ default: {
64
+ getList: (...args: Array<any>) => {
65
+ return getListMock(...args);
66
+ },
67
+ },
68
+ };
69
+ });
70
+
71
+ jest.mock("../../../UI/Components/Toast/ToastInit", () => {
72
+ return {
73
+ __esModule: true,
74
+ ShowToastNotification: (...args: Array<any>) => {
75
+ return showToastMock(...args);
76
+ },
77
+ };
78
+ });
79
+
80
+ /*
81
+ * Keep the exemplar fetch (fired by MetricCharts whenever the view has a
82
+ * resolvable window) off the network; everything else on the Metrics util
83
+ * surface that MetricCharts touches is pure enough to stub inline.
84
+ */
85
+ jest.mock(
86
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
87
+ () => {
88
+ return {
89
+ __esModule: true,
90
+ default: {
91
+ fetchExemplars: (...args: Array<any>) => {
92
+ fetchExemplarsMock(...args);
93
+ /*
94
+ * Never resolves: a resolved exemplar set would setState after
95
+ * each test's assertions and trip React's act() warning; no
96
+ * test here cares about exemplar dots.
97
+ */
98
+ return new Promise(() => {
99
+ // Intentionally never settles.
100
+ });
101
+ },
102
+ setQueryTopNOverride: () => {
103
+ return undefined;
104
+ },
105
+ getQueryConfigTopNKey: (
106
+ _queryConfig: unknown,
107
+ index: number,
108
+ scope?: string,
109
+ ) => {
110
+ return `${scope || ""}:${index}`;
111
+ },
112
+ clearQueryTopNOverridesForScope: () => {
113
+ return undefined;
114
+ },
115
+ serializeAttributeFiltersForKey: (attributes: unknown) => {
116
+ return JSON.stringify(attributes || {});
117
+ },
118
+ },
119
+ DEFAULT_TOP_N_SERIES: 10,
120
+ SHOW_ALL_SERIES_TOP_N: 10_000,
121
+ sanitizeAttributeFilters: (attributes: unknown) => {
122
+ return attributes;
123
+ },
124
+ };
125
+ },
126
+ );
127
+
128
+ import MetricCharts from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts";
129
+ import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
130
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
131
+ import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
132
+ import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
133
+ import MetricViewData from "../../../Types/Metrics/MetricViewData";
134
+ import ObjectID from "../../../Types/ObjectID";
135
+ import Navigation from "../../../UI/Utils/Navigation";
136
+ import ProjectUtil from "../../../UI/Utils/Project";
137
+
138
+ const PROJECT_ID: ObjectID = new ObjectID(
139
+ "77777777-1111-4111-8111-777777777777",
140
+ );
141
+ const HOST_MODEL_ID: string = "88888888-2222-4222-8222-888888888888";
142
+
143
+ const WINDOW_START: Date = new Date("2026-08-20T10:00:00.000Z");
144
+ const WINDOW_END: Date = new Date("2026-08-20T11:00:00.000Z");
145
+
146
+ function buildQueryConfig(): MetricQueryConfigData {
147
+ return {
148
+ metricAliasData: { metricVariable: "a" },
149
+ metricQueryData: {
150
+ filterData: {
151
+ metricName: "cpu.usage",
152
+ attributes: {},
153
+ aggegationType: MetricsAggregationType.Avg,
154
+ },
155
+ groupByAttributeKeys: ["host.name"],
156
+ },
157
+ } as unknown as MetricQueryConfigData;
158
+ }
159
+
160
+ function buildViewData(): MetricViewData {
161
+ return {
162
+ queryConfigs: [buildQueryConfig()],
163
+ formulaConfigs: [],
164
+ startAndEndDate: new InBetween<Date>(WINDOW_START, WINDOW_END),
165
+ } as MetricViewData;
166
+ }
167
+
168
+ function buildResults(): Array<AggregatedResult> {
169
+ const timestamps: Array<string> = [
170
+ "2026-08-20T10:10:00.000Z",
171
+ "2026-08-20T10:20:00.000Z",
172
+ ];
173
+ const data: Array<Record<string, unknown>> = [];
174
+ for (const host of ["web-01", "web-02"]) {
175
+ for (const [index, timestamp] of timestamps.entries()) {
176
+ data.push({
177
+ timestamp: new Date(timestamp),
178
+ value: host === "web-01" ? 90 + index : 10 + index,
179
+ attributes: { "host.name": host },
180
+ });
181
+ }
182
+ }
183
+ return [{ data, truncated: false } as unknown as AggregatedResult];
184
+ }
185
+
186
+ let navigateSpy: ReturnType<typeof jest.spyOn>;
187
+
188
+ beforeEach(() => {
189
+ getListMock.mockReset();
190
+ fetchExemplarsMock.mockReset();
191
+ showToastMock.mockReset();
192
+ navigateSpy = jest.spyOn(Navigation, "navigate").mockImplementation(() => {
193
+ return undefined;
194
+ });
195
+ jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
196
+ });
197
+
198
+ afterEach(() => {
199
+ jest.restoreAllMocks();
200
+ cleanup();
201
+ });
202
+
203
+ function getNavigatedUrl(): string {
204
+ const lastCall: Array<unknown> | undefined =
205
+ navigateSpy.mock.calls[navigateSpy.mock.calls.length - 1];
206
+ return decodeURIComponent(String(lastCall?.[0]));
207
+ }
208
+
209
+ describe("per-series investigate menu", () => {
210
+ test("each series chip carries an investigate button that opens the menu", () => {
211
+ render(
212
+ <MetricCharts
213
+ metricViewData={buildViewData()}
214
+ metricResults={buildResults()}
215
+ metricTypes={[]}
216
+ />,
217
+ );
218
+
219
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
220
+
221
+ const menu: HTMLElement = screen.getByRole("menu", {
222
+ name: "Series investigation actions",
223
+ });
224
+ expect(menu).toBeInTheDocument();
225
+ expect(screen.getByText("View logs")).toBeInTheDocument();
226
+ expect(screen.getByText("View traces")).toBeInTheDocument();
227
+ expect(screen.getByText("View exceptions")).toBeInTheDocument();
228
+ // The menu header names the series and what it is scoped to.
229
+ expect(menu.textContent).toContain("host.name = web-01");
230
+ });
231
+
232
+ test("read-only hosts get an explorer deep link; read-write hosts filter in place", () => {
233
+ const onQueryConfigsChange: MockFunction = getJestMockFunction();
234
+
235
+ const { unmount } = render(
236
+ <MetricCharts
237
+ metricViewData={buildViewData()}
238
+ metricResults={buildResults()}
239
+ metricTypes={[]}
240
+ />,
241
+ );
242
+
243
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
244
+ expect(screen.getByText("Open in Metric Explorer")).toBeInTheDocument();
245
+ expect(screen.queryByText("Filter to this series")).toBeNull();
246
+ unmount();
247
+
248
+ render(
249
+ <MetricCharts
250
+ metricViewData={buildViewData()}
251
+ metricResults={buildResults()}
252
+ metricTypes={[]}
253
+ onQueryConfigsChange={(...args: Array<any>) => {
254
+ onQueryConfigsChange(...args);
255
+ }}
256
+ />,
257
+ );
258
+
259
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
260
+ expect(screen.queryByText("Open in Metric Explorer")).toBeNull();
261
+
262
+ fireEvent.click(screen.getByText("Filter to this series"));
263
+
264
+ expect(onQueryConfigsChange).toHaveBeenCalledTimes(1);
265
+ const narrowedConfigs: Array<MetricQueryConfigData> = onQueryConfigsChange
266
+ .mock.calls[0]?.[0] as Array<MetricQueryConfigData>;
267
+ expect(
268
+ (
269
+ narrowedConfigs[0]?.metricQueryData.filterData as Record<
270
+ string,
271
+ unknown
272
+ >
273
+ )["attributes"],
274
+ ).toEqual({ "host.name": "web-01" });
275
+ });
276
+
277
+ test("View logs navigates to the logs explorer scoped to the series and window", async () => {
278
+ render(
279
+ <MetricCharts
280
+ metricViewData={buildViewData()}
281
+ metricResults={buildResults()}
282
+ metricTypes={[]}
283
+ />,
284
+ );
285
+
286
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-02"));
287
+ fireEvent.click(screen.getByText("View logs"));
288
+
289
+ await waitFor(() => {
290
+ expect(navigateSpy).toHaveBeenCalled();
291
+ });
292
+
293
+ const url: string = getNavigatedUrl();
294
+ expect(url).toContain("/logs");
295
+ expect(url).toContain('["attributes.host.name",["web-02"]]');
296
+ expect(url).toContain("range=Custom");
297
+ expect(url).toContain("start=");
298
+ expect(url).toContain("end=");
299
+ });
300
+
301
+ test("View exceptions reports what the exceptions grammar cannot carry", async () => {
302
+ render(
303
+ <MetricCharts
304
+ metricViewData={buildViewData()}
305
+ metricResults={buildResults()}
306
+ metricTypes={[]}
307
+ />,
308
+ );
309
+
310
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
311
+ fireEvent.click(screen.getByText("View exceptions"));
312
+
313
+ await waitFor(() => {
314
+ expect(navigateSpy).toHaveBeenCalled();
315
+ });
316
+
317
+ const url: string = getNavigatedUrl();
318
+ expect(url).toContain("/exceptions");
319
+ expect(url).toContain("status=all");
320
+ // The host.name narrowing has no exceptions facet — the user is told.
321
+ expect(showToastMock).toHaveBeenCalled();
322
+ });
323
+
324
+ test("the host series jumps to the matching Host page", async () => {
325
+ getListMock.mockReturnValue(
326
+ Promise.resolve({ data: [{ _id: HOST_MODEL_ID }] }),
327
+ );
328
+
329
+ render(
330
+ <MetricCharts
331
+ metricViewData={buildViewData()}
332
+ metricResults={buildResults()}
333
+ metricTypes={[]}
334
+ />,
335
+ );
336
+
337
+ fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
338
+ fireEvent.click(screen.getByText('Open host "web-01"'));
339
+
340
+ await waitFor(() => {
341
+ expect(navigateSpy).toHaveBeenCalled();
342
+ });
343
+
344
+ const listArgs: Record<string, unknown> = getListMock.mock
345
+ .calls[0]?.[0] as Record<string, unknown>;
346
+ expect(listArgs["query"]).toEqual({
347
+ projectId: PROJECT_ID,
348
+ hostIdentifier: "web-01",
349
+ });
350
+ expect(getNavigatedUrl()).toContain(`/host/${HOST_MODEL_ID}`);
351
+ });
352
+
353
+ test("enableSeriesActions={false} renders plain chips with no investigate affordance", () => {
354
+ render(
355
+ <MetricCharts
356
+ metricViewData={buildViewData()}
357
+ metricResults={buildResults()}
358
+ metricTypes={[]}
359
+ enableSeriesActions={false}
360
+ />,
361
+ );
362
+
363
+ // The chips themselves still render…
364
+ expect(screen.getByText("host.name=web-01")).toBeInTheDocument();
365
+ // …but nothing offers navigation.
366
+ expect(screen.queryByLabelText("Investigate host.name=web-01")).toBeNull();
367
+ });
368
+ });
@@ -122,14 +122,18 @@ import CallMethods from "../../../../App/FeatureSet/Dashboard/src/Components/Not
122
122
  import PushMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/Push";
123
123
  import WhatsAppMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/WhatsApp";
124
124
  import TelegramMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/Telegram";
125
+ import SlackMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/Slack";
126
+ import MicrosoftTeamsMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/MicrosoftTeams";
125
127
  import WebhookMethods from "../../../../App/FeatureSet/Dashboard/src/Components/NotificationMethods/Webhook";
126
128
  import BaseModel, {
127
129
  DatabaseBaseModelType,
128
130
  } from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
129
131
  import UserCall from "../../../Models/DatabaseModels/UserCall";
130
132
  import UserEmail from "../../../Models/DatabaseModels/UserEmail";
133
+ import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
131
134
  import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
132
135
  import UserPush from "../../../Models/DatabaseModels/UserPush";
136
+ import UserSlack from "../../../Models/DatabaseModels/UserSlack";
133
137
  import UserSMS from "../../../Models/DatabaseModels/UserSMS";
134
138
  import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
135
139
  import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
@@ -1470,7 +1474,7 @@ describe("Escalation rules, wired: the delete confirmation says what it costs",
1470
1474
 
1471
1475
  /*
1472
1476
  * ---------------------------------------------------------------------------
1473
- * THE SEVEN METHOD TABLES, RENDERED FOR REAL.
1477
+ * THE NINE METHOD TABLES, RENDERED FOR REAL.
1474
1478
  * ---------------------------------------------------------------------------
1475
1479
  *
1476
1480
  * This block exists because the first cut of this feature shipped a correct,
@@ -1532,6 +1536,8 @@ const METHOD_ID_CALL: string = "33333333-9999-4999-8999-999999999999";
1532
1536
  const METHOD_ID_PUSH: string = "44444444-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
1533
1537
  const METHOD_ID_WHATSAPP: string = "55555555-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
1534
1538
  const METHOD_ID_TELEGRAM: string = "66666666-cccc-4ccc-8ccc-cccccccccccc";
1539
+ const METHOD_ID_SLACK: string = "88888888-eeee-4eee-8eee-eeeeeeeeeeee";
1540
+ const METHOD_ID_TEAMS: string = "99999999-ffff-4fff-8fff-ffffffffffff";
1535
1541
  const METHOD_ID_WEBHOOK: string = "77777777-dddd-4ddd-8ddd-dddddddddddd";
1536
1542
 
1537
1543
  const METHOD_SURFACES: Array<MethodSurface> = [
@@ -1624,6 +1630,36 @@ const METHOD_SURFACES: Array<MethodSurface> = [
1624
1630
  modalTitle: "Delete Telegram Account",
1625
1631
  methodLabel: "Telegram: @alexchen",
1626
1632
  },
1633
+ {
1634
+ name: "Slack",
1635
+ Component: SlackMethods,
1636
+ modelType: UserSlack,
1637
+ relationName: "userSlack",
1638
+ methodId: METHOD_ID_SLACK,
1639
+ buildRow: (): BaseModel => {
1640
+ return buildMethodRow(UserSlack, METHOD_ID_SLACK, {
1641
+ slackUserName: "alexchen",
1642
+ isVerified: true,
1643
+ });
1644
+ },
1645
+ modalTitle: "Delete Slack Account",
1646
+ methodLabel: "Slack: alexchen",
1647
+ },
1648
+ {
1649
+ name: "Microsoft Teams",
1650
+ Component: MicrosoftTeamsMethods,
1651
+ modelType: UserMicrosoftTeams,
1652
+ relationName: "userMicrosoftTeams",
1653
+ methodId: METHOD_ID_TEAMS,
1654
+ buildRow: (): BaseModel => {
1655
+ return buildMethodRow(UserMicrosoftTeams, METHOD_ID_TEAMS, {
1656
+ microsoftTeamsUserName: "Alex Example",
1657
+ isVerified: true,
1658
+ });
1659
+ },
1660
+ modalTitle: "Delete Microsoft Teams Account",
1661
+ methodLabel: "Microsoft Teams: Alex Example",
1662
+ },
1627
1663
  {
1628
1664
  name: "Webhook",
1629
1665
  Component: WebhookMethods,
@@ -38,7 +38,7 @@ import getJestMockFunction, { MockFunction } from "../../MockType";
38
38
  *
39
39
  * The fourth is where the "Notification Method" dropdown comes from, which is
40
40
  * now two different places depending on whose rules are on screen. For the
41
- * signed-in user it is still seven reads of the seven method models, and that
41
+ * signed-in user it is still nine reads of the nine method models, and that
42
42
  * path is the four settings pages every existing user relies on. For anybody
43
43
  * else those reads are REFUSED - the models are scoped to the person who owns
44
44
  * the device, because the columns behind them are the raw phone number, the
@@ -186,8 +186,10 @@ import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
186
186
  import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
187
187
  import UserCall from "../../../Models/DatabaseModels/UserCall";
188
188
  import UserEmail from "../../../Models/DatabaseModels/UserEmail";
189
+ import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
189
190
  import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
190
191
  import UserPush from "../../../Models/DatabaseModels/UserPush";
192
+ import UserSlack from "../../../Models/DatabaseModels/UserSlack";
191
193
  import UserSMS from "../../../Models/DatabaseModels/UserSMS";
192
194
  import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
193
195
  import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
@@ -199,7 +201,7 @@ import ObjectID from "../../../Types/ObjectID";
199
201
  import ProjectUtil from "../../../UI/Utils/Project";
200
202
 
201
203
  /*
202
- * The seven models whose rows only their owner may read. They are listed
204
+ * The nine models whose rows only their owner may read. They are listed
203
205
  * together because the assertion that matters is about the SET: an admin
204
206
  * surface that reads any one of them is reading a row it is not entitled to,
205
207
  * and which one hardly matters.
@@ -211,13 +213,15 @@ const NOTIFICATION_METHOD_MODELS: Array<unknown> = [
211
213
  UserPush,
212
214
  UserWhatsApp,
213
215
  UserTelegram,
216
+ UserSlack,
217
+ UserMicrosoftTeams,
214
218
  UserWebhook,
215
219
  ];
216
220
 
217
221
  /*
218
222
  * What the admin surface hands over in place of those reads: a channel name, the
219
223
  * id of the row, and a mask. Telegram is unverified on purpose - the self-serve
220
- * path filters `isVerified: true` in six of its seven queries, so a supplied
224
+ * path filters `isVerified: true` in eight of its nine queries, so a supplied
221
225
  * list that offered unverified methods would be the two paths disagreeing about
222
226
  * what is pickable.
223
227
  */
@@ -250,6 +254,8 @@ const METHOD_FOREIGN_KEYS: Array<keyof UserNotificationRule> = [
250
254
  "userPushId",
251
255
  "userWhatsAppId",
252
256
  "userTelegramId",
257
+ "userSlackId",
258
+ "userMicrosoftTeamsId",
253
259
  "userWebhookId",
254
260
  ];
255
261
 
@@ -343,7 +349,7 @@ const getMethodOptions: GetMethodOptions = (
343
349
  };
344
350
 
345
351
  /*
346
- * The seven RELATION spellings. Selecting any of these on a rule pulls columns
352
+ * The nine RELATION spellings. Selecting any of these on a rule pulls columns
347
353
  * out of the method models themselves - the raw address, the raw number, the
348
354
  * telegram handle - through a table an administrator is allowed to read.
349
355
  *
@@ -366,6 +372,8 @@ const METHOD_RELATIONS: Array<string> = [
366
372
  "userPush",
367
373
  "userWhatsApp",
368
374
  "userTelegram",
375
+ "userSlack",
376
+ "userMicrosoftTeams",
369
377
  "userWebhook",
370
378
  ];
371
379
 
@@ -719,7 +727,7 @@ describe("OnCallRulesTable", () => {
719
727
 
720
728
  /*
721
729
  * The whole point of the redesign, in one assertion. Not "reads them and
722
- * shows fewer columns" - does not read them. Each of these seven models is
730
+ * shows fewer columns" - does not read them. Each of these nine models is
723
731
  * scoped to the person who owns the device, and the columns behind them are
724
732
  * the raw phone number, the webhook bearer url, the push device token, the
725
733
  * telegram chat id and the verification code. A request here is a request
@@ -748,7 +756,7 @@ describe("OnCallRulesTable", () => {
748
756
  * The readiness payload the admin page sources these from is fetched
749
757
  * asynchronously and can fail outright, so an absent list is a real state
750
758
  * rather than a caller mistake. Treating it as "then fetch them yourself"
751
- * would fire seven refused requests and turn this component's error state on
759
+ * would fire nine refused requests and turn this component's error state on
752
760
  * - taking the rule repair away because a DIFFERENT read failed.
753
761
  */
754
762
  await renderForOtherUser(undefined);
@@ -786,7 +794,7 @@ describe("OnCallRulesTable", () => {
786
794
 
787
795
  /*
788
796
  * The unverified Telegram method is absent, which mirrors the
789
- * `isVerified: true` filter on six of the seven self-serve queries. A rule
797
+ * `isVerified: true` filter on eight of the nine self-serve queries. A rule
790
798
  * pointed at an unverified device is a rule that pages nothing.
791
799
  */
792
800
  expect(
@@ -805,7 +813,7 @@ describe("OnCallRulesTable", () => {
805
813
  });
806
814
 
807
815
  /*
808
- * The seven foreign keys are mutually exclusive and nothing validates that
816
+ * The nine foreign keys are mutually exclusive and nothing validates that
809
817
  * client-side, so this asserts the whole set: an SMS id written to
810
818
  * `userEmailId` would page an address rather than a phone, and would not
811
819
  * error anywhere on the way.
@@ -968,7 +976,7 @@ describe("OnCallRulesTable", () => {
968
976
  test("the signed-in user's own table still reads the method models itself", async () => {
969
977
  /*
970
978
  * The regression surface. Four settings pages every existing user already
971
- * relies on read their own seven models to fill this dropdown, and reading
979
+ * relies on read their own nine models to fill this dropdown, and reading
972
980
  * your own rows is exactly what those models allow. Nothing about the admin
973
981
  * surface is a reason to change it - and an unmasked label is more use to
974
982
  * somebody looking at their own phone number than a mask of it.
@@ -985,7 +993,7 @@ describe("OnCallRulesTable", () => {
985
993
  getQueriedUserIds().filter((userId: string | undefined) => {
986
994
  return userId === CURRENT_USER_ID_STRING;
987
995
  }),
988
- ).toHaveLength(7);
996
+ ).toHaveLength(9);
989
997
 
990
998
  expect(getMethodOptions(capturedTables[0]!)).toEqual([
991
999
  { label: `Email: ${OWN_EMAIL_ADDRESS}`, value: OWN_EMAIL_ID },
@@ -117,6 +117,30 @@ const NOT_READABLE_BY_THEIR_DOMAIN_ROLES: Array<string> = [
117
117
  "IncomingCallPolicyEscalationRule",
118
118
  ];
119
119
 
120
+ /*
121
+ * Columns inside an in-domain table whose read list deliberately stops short of
122
+ * the domain's full tier set, and why. The table-level list above is about
123
+ * whole tables a role holder is kept out of; this is the same idea one level
124
+ * down, for a table they are otherwise welcome in.
125
+ *
126
+ * Entries are the exact `<table>.<column> is missing <tier>` strings the sweep
127
+ * produces, so the list is self-policing: widen one of these read lists again
128
+ * and the entry stops being produced and the assertion fails.
129
+ *
130
+ * https://github.com/OneUptime/oneuptime/issues/3360 - Monitor's three secret
131
+ * keys are bearer credentials, not monitor data. Reading one is gated on being
132
+ * able to ROTATE it, which is what separates MonitorMember (may edit a monitor,
133
+ * so may see and reset its key) from MonitorViewer (read-only, so may not).
134
+ * Viewer used to be on these lists, which made "read-only" mean "can read a
135
+ * live credential for every agent in the project" -- the same reasoning that
136
+ * keeps MonitorSecret out of the table list above.
137
+ */
138
+ const COLUMNS_NOT_READABLE_BY_THEIR_DOMAIN_ROLES: Array<string> = [
139
+ "Monitor.incomingEmailSecretKey is missing MonitorViewer",
140
+ "Monitor.incomingRequestSecretKey is missing MonitorViewer",
141
+ "Monitor.serverMonitorSecretKey is missing MonitorViewer",
142
+ ];
143
+
120
144
  type ModelNameFunction = (modelType: ModelType) => string;
121
145
 
122
146
  const modelName: ModelNameFunction = (modelType: ModelType): string => {
@@ -317,7 +341,9 @@ describe("Domain role tier coverage", () => {
317
341
  }
318
342
  }
319
343
 
320
- expect(missing.sort()).toEqual([]);
344
+ expect(missing.sort()).toEqual(
345
+ COLUMNS_NOT_READABLE_BY_THEIR_DOMAIN_ROLES.slice().sort(),
346
+ );
321
347
  expect(checked).toBeGreaterThan(500);
322
348
  });
323
349
  });