@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -119,7 +119,15 @@ export default class Markdown {
119
119
  return htmlBody;
120
120
  }
121
121
 
122
- private static escapeHtml(text: string): string {
122
+ /**
123
+ * Escape the five characters that can break out of HTML text or an
124
+ * attribute value.
125
+ *
126
+ * Public because it is not only marked's business: code that hand-builds
127
+ * an email fragment by string interpolation needs the same escaping, and
128
+ * a second implementation is a second thing to get wrong.
129
+ */
130
+ public static escapeHtml(text: string): string {
123
131
  return text
124
132
  .replace(/&/g, "&")
125
133
  .replace(/</g, "&lt;")
@@ -128,6 +136,31 @@ export default class Markdown {
128
136
  .replace(/'/g, "&#39;");
129
137
  }
130
138
 
139
+ /*
140
+ * The email renderer.
141
+ *
142
+ * Everything here is an INLINE style, and that is not a preference.
143
+ * MailService compiles the Handlebars template and sends it — there is no
144
+ * CSS inlining step anywhere in the pipeline — and Gmail strips <style>
145
+ * blocks from the document head, so a class name emitted here reaches the
146
+ * reader as nothing at all. That is why the Docs/Blog renderers' Tailwind
147
+ * classes cannot be copied even though the override shape can.
148
+ *
149
+ * Until this existed the renderer was a bare `new Renderer()`. Alert and
150
+ * incident root causes carry a GitHub-flavoured table of breaching
151
+ * samples, and marked emitted it as a naked <table> with no borders, no
152
+ * padding and no alignment: every row ran into the next, which is
153
+ * precisely the part of the email an on-call engineer reads first.
154
+ *
155
+ * Outlook renders through Word, which ignores border-radius, box-shadow
156
+ * and — importantly — overflow, so the scrolling wrapper the Docs and
157
+ * Blog renderers use does not transfer. The table has to FIT instead:
158
+ * the DetailBox card is 472px wide with 28px of padding either side,
159
+ * leaving roughly 416px for a root-cause table that routinely runs to
160
+ * four or more columns. Hence `width:100%` with `table-layout:auto` and
161
+ * `word-break:break-word` on the cells — a long metric name wraps inside
162
+ * its column rather than pushing the table past the body width.
163
+ */
131
164
  private static getEmailRenderer(): Renderer {
132
165
  if (this.emailRenderer !== null) {
133
166
  return this.emailRenderer;
@@ -135,6 +168,65 @@ export default class Markdown {
135
168
 
136
169
  const renderer: Renderer = new Renderer();
137
170
 
171
+ const cellFont: string =
172
+ "font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;font-size:13px;line-height:20px;";
173
+
174
+ renderer.table = function (header: string, body: string): string {
175
+ return (
176
+ `<table cellpadding="0" cellspacing="0" border="0" width="100%" ` +
177
+ `style="border-collapse:collapse;width:100%;table-layout:auto;` +
178
+ `margin:12px 0;border:1px solid #e2e8f0;">` +
179
+ `<thead>${header}</thead><tbody>${body}</tbody></table>`
180
+ );
181
+ };
182
+
183
+ renderer.tablerow = function (content: string): string {
184
+ return `<tr>${content}</tr>`;
185
+ };
186
+
187
+ renderer.tablecell = function (
188
+ content: string,
189
+ flags: { header?: boolean; align?: string | null },
190
+ ): string {
191
+ const tag: string = flags.header ? "th" : "td";
192
+ const align: string = flags.align ? `text-align:${flags.align};` : "";
193
+
194
+ /*
195
+ * A header cell gets the tinted band and the heavier bottom rule; a
196
+ * body cell gets a hairline so consecutive rows stay separable. Both
197
+ * keep the same padding so the columns line up.
198
+ */
199
+ const tone: string = flags.header
200
+ ? "background-color:#f8fafc;color:#475569;font-weight:600;border-bottom:1px solid #cbd5e1;"
201
+ : "color:#1e293b;font-weight:400;border-bottom:1px solid #f1f5f9;";
202
+
203
+ return (
204
+ `<${tag} style="padding:8px 10px;${cellFont}${tone}` +
205
+ `word-break:break-word;${align || "text-align:left;"}">` +
206
+ `${content}</${tag}>`
207
+ );
208
+ };
209
+
210
+ /*
211
+ * The root cause backticks metric names, aliases and timestamps. Left
212
+ * bare they were indistinguishable from the prose around them.
213
+ *
214
+ * `code` arrives ALREADY escaped — marked escapes codespan text before
215
+ * it reaches the renderer — so escaping again here would turn a typed
216
+ * "<img>" into the literal "&lt;img&gt;" on screen rather than the
217
+ * "<img>" the author wrote. (The Docs and BlogValidation renderers do
218
+ * escape a second time; that is a separate defect in those surfaces,
219
+ * not a pattern to copy.)
220
+ */
221
+ renderer.codespan = function (code: string): string {
222
+ return (
223
+ `<code style="background-color:#f1f5f9;color:#0f172a;` +
224
+ `padding:1px 5px;border-radius:4px;` +
225
+ `font-family:'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;` +
226
+ `font-size:12px;">${code}</code>`
227
+ );
228
+ };
229
+
138
230
  this.emailRenderer = renderer;
139
231
 
140
232
  return renderer;
@@ -21,6 +21,7 @@ import DashboardModelQueryInterpolation, {
21
21
  AttributeToColumnMap,
22
22
  } from "../../../Utils/Dashboard/ModelQueryVariableInterpolation";
23
23
  import DashboardVariableInterpolation from "../../../Utils/Dashboard/VariableInterpolation";
24
+ import DashboardLabelVariable from "../../../Utils/Dashboard/LabelVariable";
24
25
 
25
26
  export interface PublicDashboardResourceListPolicyResult {
26
27
  resourceType: string;
@@ -180,6 +181,7 @@ export default class PublicDashboardResourceListPolicy {
180
181
  componentType: componentType as DashboardComponentType,
181
182
  argumentsObject,
182
183
  requestedQuery: data.requestedQuery,
184
+ variables,
183
185
  });
184
186
 
185
187
  let query: Record<string, unknown> = draft.query;
@@ -505,8 +507,9 @@ export default class PublicDashboardResourceListPolicy {
505
507
  componentType: DashboardComponentType;
506
508
  argumentsObject: Record<string, unknown>;
507
509
  requestedQuery: unknown;
510
+ variables: Array<DashboardVariable>;
508
511
  }): PolicyDraft {
509
- const { componentType, argumentsObject, requestedQuery } = data;
512
+ const { componentType, argumentsObject, requestedQuery, variables } = data;
510
513
 
511
514
  switch (componentType) {
512
515
  case DashboardComponentType.IncidentList:
@@ -520,6 +523,7 @@ export default class PublicDashboardResourceListPolicy {
520
523
  case DashboardComponentType.MonitorList:
521
524
  return PublicDashboardResourceListPolicy.buildMonitorPolicy(
522
525
  argumentsObject,
526
+ variables,
523
527
  );
524
528
  case DashboardComponentType.NetworkMap:
525
529
  return PublicDashboardResourceListPolicy.buildNetworkMapPolicy(
@@ -814,6 +818,7 @@ export default class PublicDashboardResourceListPolicy {
814
818
 
815
819
  private static buildMonitorPolicy(
816
820
  argumentsObject: Record<string, unknown>,
821
+ variables: Array<DashboardVariable>,
817
822
  ): PolicyDraft {
818
823
  const query: Record<string, unknown> = {};
819
824
  const statusFilter: string | undefined =
@@ -841,12 +846,22 @@ export default class PublicDashboardResourceListPolicy {
841
846
  argumentsObject,
842
847
  argumentKey: "monitorTypes",
843
848
  });
844
- PublicDashboardResourceListPolicy.addIncludesFromArgument({
845
- query,
846
- queryKey: "labels",
847
- argumentsObject,
848
- argumentKey: "labelIds",
849
- });
849
+ const labels: ReturnType<typeof DashboardLabelVariable.getFilter> =
850
+ DashboardLabelVariable.getFilter({
851
+ labelIds: PublicDashboardResourceListPolicy.optionalStringArray(
852
+ argumentsObject,
853
+ "labelIds",
854
+ ),
855
+ labelVariableId: PublicDashboardResourceListPolicy.optionalString(
856
+ argumentsObject,
857
+ "labelVariableId",
858
+ false,
859
+ ),
860
+ variables,
861
+ });
862
+ if (labels) {
863
+ query["labels"] = labels;
864
+ }
850
865
 
851
866
  return PublicDashboardResourceListPolicy.listDraft({
852
867
  resourceType: "monitor",
@@ -1653,19 +1668,19 @@ export default class PublicDashboardResourceListPolicy {
1653
1668
  storedValue,
1654
1669
  "Stored dashboard variable",
1655
1670
  );
1656
- if (stored["type"] !== DashboardVariableType.TelemetryAttribute) {
1671
+ const type: unknown = stored["type"];
1672
+ if (
1673
+ type !== DashboardVariableType.TelemetryAttribute &&
1674
+ type !== DashboardVariableType.ProjectLabel
1675
+ ) {
1657
1676
  continue;
1658
1677
  }
1659
1678
 
1660
1679
  const id: unknown = stored["id"];
1661
- const attributeKey: unknown = stored["attributeKey"];
1662
1680
  if (
1663
1681
  typeof id !== "string" ||
1664
1682
  id.length === 0 ||
1665
- id.length > MAX_VARIABLE_ID_LENGTH ||
1666
- typeof attributeKey !== "string" ||
1667
- attributeKey.trim().length === 0 ||
1668
- attributeKey.length > MAX_VARIABLE_VALUE_LENGTH
1683
+ id.length > MAX_VARIABLE_ID_LENGTH
1669
1684
  ) {
1670
1685
  throw new BadDataException("Stored dashboard variable is malformed.");
1671
1686
  }
@@ -1700,11 +1715,10 @@ export default class PublicDashboardResourceListPolicy {
1700
1715
  const name: unknown = stored["name"];
1701
1716
  const storedIsMultiSelect: boolean = isMultiSelect === true;
1702
1717
 
1703
- variables.push({
1718
+ const variable: DashboardVariable = {
1704
1719
  id,
1705
1720
  name: typeof name === "string" ? name : "",
1706
- type: DashboardVariableType.TelemetryAttribute,
1707
- attributeKey: attributeKey.trim(),
1721
+ type,
1708
1722
  isMultiSelect: storedIsMultiSelect,
1709
1723
  defaultValue:
1710
1724
  typeof defaultValue === "string" ? defaultValue : undefined,
@@ -1716,12 +1730,63 @@ export default class PublicDashboardResourceListPolicy {
1716
1730
  storedIsMultiSelect && Array.isArray(selectedValues)
1717
1731
  ? (selectedValues as Array<string>)
1718
1732
  : undefined,
1719
- });
1733
+ };
1734
+
1735
+ if (type === DashboardVariableType.ProjectLabel) {
1736
+ variable.labelOptions =
1737
+ PublicDashboardResourceListPolicy.readLabelOptions(stored);
1738
+ } else {
1739
+ const attributeKey: unknown = stored["attributeKey"];
1740
+ if (
1741
+ typeof attributeKey !== "string" ||
1742
+ attributeKey.trim().length === 0 ||
1743
+ attributeKey.length > MAX_VARIABLE_VALUE_LENGTH
1744
+ ) {
1745
+ throw new BadDataException("Stored dashboard variable is malformed.");
1746
+ }
1747
+ variable.attributeKey = attributeKey.trim();
1748
+ }
1749
+
1750
+ variables.push(variable);
1720
1751
  }
1721
1752
 
1722
1753
  return variables;
1723
1754
  }
1724
1755
 
1756
+ private static readLabelOptions(
1757
+ stored: Record<string, unknown>,
1758
+ ): NonNullable<DashboardVariable["labelOptions"]> {
1759
+ const value: unknown = stored["labelOptions"];
1760
+ if (!Array.isArray(value)) {
1761
+ throw new BadDataException(
1762
+ "Stored dashboard label options are malformed.",
1763
+ );
1764
+ }
1765
+
1766
+ return value.map((entry: unknown): { label: string; value: string } => {
1767
+ const option: Record<string, unknown> =
1768
+ PublicDashboardResourceListPolicy.requireObject(
1769
+ entry,
1770
+ "Stored dashboard label option",
1771
+ );
1772
+ const label: unknown = option["label"];
1773
+ const labelId: unknown = option["value"];
1774
+ if (
1775
+ typeof label !== "string" ||
1776
+ label.length === 0 ||
1777
+ label.length > MAX_VARIABLE_VALUE_LENGTH ||
1778
+ typeof labelId !== "string" ||
1779
+ labelId.length === 0 ||
1780
+ labelId.length > MAX_VARIABLE_VALUE_LENGTH
1781
+ ) {
1782
+ throw new BadDataException(
1783
+ "Stored dashboard label options are malformed.",
1784
+ );
1785
+ }
1786
+ return { label, value: labelId };
1787
+ });
1788
+ }
1789
+
1725
1790
  private static validateRequestedVariableSelections(
1726
1791
  requestedVariables: unknown,
1727
1792
  ): Map<string, Record<string, unknown>> {
@@ -751,9 +751,18 @@ export default class EmailRollupFlushRunner {
751
751
  const projectHomeLink: string = URL.fromString(dashboardUrl.toString())
752
752
  .addRoute(`/${bucket.projectId.toString()}/home`)
753
753
  .toString();
754
+ /*
755
+ * Email Preferences, not Notification Settings: this footer exists to
756
+ * offer the reader a way out of BATCHING, and the rollup switch lives on
757
+ * Email Preferences. Notification Settings is one link further on, from
758
+ * the page this lands on. The segment is duplicated from the Dashboard's
759
+ * UserSettingsRoutePath for the reason OnCallSetupReminderService gives -
760
+ * Common/Server cannot import App sources - so a route rename there has
761
+ * to be repeated here.
762
+ */
754
763
  const preferencesLink: string = URL.fromString(dashboardUrl.toString())
755
764
  .addRoute(
756
- `/${bucket.projectId.toString()}/user-settings/notification-settings`,
765
+ `/${bucket.projectId.toString()}/user-settings/email-preferences`,
757
766
  )
758
767
  .toString();
759
768
 
@@ -629,8 +629,8 @@ export const buildRollupEmail: BuildRollupEmailFunction = (
629
629
  */
630
630
  const preferencesText: string =
631
631
  "OneUptime groups owner notifications into a single email when several arrive at once for the same project. " +
632
- "To go back to one email per notification, sent the moment it happens, turn off email rollup for this project in your notification settings. " +
633
- "The same page is where you choose which notifications you receive, or turn email off entirely:";
632
+ "To go back to one email per notification, sent the moment it happens, turn off email rollup for this project in your email preferences. " +
633
+ "The same page can also switch off routine emails in bulk, and it links on to the per-event settings:";
634
634
 
635
635
  /*
636
636
  * ONE FLAT ARRAY, WITH THE SECTION HEADING CARRIED ON ITS FIRST ROW.
@@ -7,6 +7,7 @@ import {
7
7
  } from "../../../../Types/Monitor/CriteriaFilter";
8
8
  import Typeof from "../../../../Types/Typeof";
9
9
  import CaptureSpan from "../../Telemetry/CaptureSpan";
10
+ import MetricValueFormatter from "../../../../Utils/Monitor/MetricValueFormatter";
10
11
 
11
12
  export default class CompareCriteria {
12
13
  /*
@@ -474,6 +475,15 @@ export default class CompareCriteria {
474
475
  criteriaFilter: CriteriaFilter;
475
476
  metricDisplayName?: string | undefined;
476
477
  unit?: string | undefined;
478
+ /**
479
+ * The metric NAME, when there is a real one. Distinct from
480
+ * metricDisplayName, which is the formula expression for a formula
481
+ * criteria and is only ever used as a label: this one feeds
482
+ * ValueFormatter.isFractionMetric, which would read the trailing
483
+ * `_ratio` of a formula like `a / b_ratio` as a signal to multiply
484
+ * the value by 100. Callers pass it only for plain metric criteria.
485
+ */
486
+ metricName?: string | undefined;
477
487
  }): string | null {
478
488
  if (data.value === null || data.value === undefined) {
479
489
  return null;
@@ -499,6 +509,7 @@ export default class CompareCriteria {
499
509
  criteriaFilter: data.criteriaFilter,
500
510
  metricDisplayName: data.metricDisplayName,
501
511
  unit: data.unit,
512
+ metricName: data.metricName,
502
513
  });
503
514
  }
504
515
 
@@ -521,6 +532,7 @@ export default class CompareCriteria {
521
532
  criteriaFilter: data.criteriaFilter,
522
533
  metricDisplayName: data.metricDisplayName,
523
534
  unit: data.unit,
535
+ metricName: data.metricName,
524
536
  });
525
537
  }
526
538
 
@@ -543,6 +555,7 @@ export default class CompareCriteria {
543
555
  criteriaFilter: data.criteriaFilter,
544
556
  metricDisplayName: data.metricDisplayName,
545
557
  unit: data.unit,
558
+ metricName: data.metricName,
546
559
  });
547
560
  }
548
561
 
@@ -565,6 +578,7 @@ export default class CompareCriteria {
565
578
  criteriaFilter: data.criteriaFilter,
566
579
  metricDisplayName: data.metricDisplayName,
567
580
  unit: data.unit,
581
+ metricName: data.metricName,
568
582
  });
569
583
  }
570
584
 
@@ -587,6 +601,7 @@ export default class CompareCriteria {
587
601
  criteriaFilter: data.criteriaFilter,
588
602
  metricDisplayName: data.metricDisplayName,
589
603
  unit: data.unit,
604
+ metricName: data.metricName,
590
605
  });
591
606
  }
592
607
 
@@ -609,6 +624,7 @@ export default class CompareCriteria {
609
624
  criteriaFilter: data.criteriaFilter,
610
625
  metricDisplayName: data.metricDisplayName,
611
626
  unit: data.unit,
627
+ metricName: data.metricName,
612
628
  });
613
629
  }
614
630
 
@@ -625,6 +641,15 @@ export default class CompareCriteria {
625
641
  criteriaFilter: CriteriaFilter;
626
642
  metricDisplayName?: string | undefined;
627
643
  unit?: string | undefined;
644
+ /**
645
+ * The metric NAME, when there is a real one. Distinct from
646
+ * metricDisplayName, which is the formula expression for a formula
647
+ * criteria and is only ever used as a label: this one feeds
648
+ * ValueFormatter.isFractionMetric, which would read the trailing
649
+ * `_ratio` of a formula like `a / b_ratio` as a signal to multiply
650
+ * the value by 100. Callers pass it only for plain metric criteria.
651
+ */
652
+ metricName?: string | undefined;
628
653
  }): string {
629
654
  // CPU Percent over the last 5 minutes is 10 which is less than the threshold of 20
630
655
  let message: string = "";
@@ -695,30 +720,36 @@ export default class CompareCriteria {
695
720
  }
696
721
 
697
722
  /*
698
- * Suppress the units that read as noise or as a wrong number
699
- * next to a value. OTel's dimensionless "1" marks ratio metrics
700
- * whose samples are fractions in [0, 1]; rendering "0.06 1" is both
701
- * ugly and misleading (it is not 1% — it is 6%), and a CLS breach
702
- * read "is 0.31 1 which is greater than or equal to 0.25 1". UCUM
703
- * annotation-only units ("{cpu}", "{packets}", "{errors}") are
704
- * descriptive braces, not a unit a reader should be shown. Every
705
- * real unit passes through unchanged, so "%" stays "%" and "ms"
706
- * stays "ms".
723
+ * A metric value is written the way the dashboard writes it
724
+ * "1.07 GB", "1.5 sec", "25.34%" rather than as raw digits with a
725
+ * raw UCUM code glued on. Only two of the ~35 callers pass a unit at
726
+ * all (MetricMonitorCriteria and DatabaseMonitorCriteria); everything
727
+ * else carries its unit inside the CheckOn label ("Response Time (in
728
+ * ms)") and must keep its exact digits, so an absent unit leaves this
729
+ * sentence byte-for-byte as it was.
707
730
  *
708
- * The dashboard side already applies the "1" half of this rule in
709
- * MonitorCriteriaObservationBuilder.normalizeDisplayUnit, and
710
- * ValueFormatter applies both halves when it formats a value. The
711
- * email path never picked either up, so the same evaluation rendered
712
- * worse in the alert than on the dashboard.
731
+ * MetricValueFormatter also subsumes the unit-suppression rules this
732
+ * block used to spell out by hand: OTel's dimensionless "1" (which
733
+ * made a CLS breach read "is 0.31 1 which is greater than or equal to
734
+ * 0.25 1") and UCUM's annotation-only "{cpu}" / "{packets}" both
735
+ * disappear, and a fraction metric still carrying "1" is rendered as
736
+ * the percentage it means.
737
+ *
738
+ * The unit rides each NUMBER instead of the sentence, because
739
+ * auto-scaling can land two samples in the same sentence on different
740
+ * scales — "is 900 KB, 1.2 MB" — which one trailing suffix cannot
741
+ * express.
713
742
  */
714
- const trimmedUnit: string = (data.unit || "").trim();
715
- const annotationOnlyUnitPattern: RegExp = /^\{[^{}]*\}$/;
716
- const unitSuffix: string =
717
- trimmedUnit &&
718
- trimmedUnit !== "1" &&
719
- !annotationOnlyUnitPattern.test(trimmedUnit)
720
- ? ` ${trimmedUnit}`
721
- : "";
743
+ const withUnit: ((value: number) => string) | undefined =
744
+ MetricValueFormatter.hasDisplayableUnit(data.unit, data.metricName)
745
+ ? (value: number): string => {
746
+ return MetricValueFormatter.format({
747
+ value: value,
748
+ unit: data.unit,
749
+ metricName: data.metricName,
750
+ });
751
+ }
752
+ : undefined;
722
753
 
723
754
  if (
724
755
  data.criteriaFilter.filterType !== FilterType.True &&
@@ -750,10 +781,12 @@ export default class CompareCriteria {
750
781
  evaluationType: evaluationType,
751
782
  });
752
783
 
753
- const formattedValues: string =
754
- CompareCriteria.formatCriteriaValues(reportedValues);
784
+ const formattedValues: string = CompareCriteria.formatCriteriaValues(
785
+ reportedValues,
786
+ withUnit,
787
+ );
755
788
 
756
- message += ` is ${formattedValues}${unitSuffix}`;
789
+ message += ` is ${formattedValues}`;
757
790
 
758
791
  message += " which is";
759
792
 
@@ -778,22 +811,22 @@ export default class CompareCriteria {
778
811
 
779
812
  switch (data.criteriaFilter.filterType) {
780
813
  case FilterType.GreaterThan:
781
- message += ` greater than ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
814
+ message += ` greater than ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
782
815
  break;
783
816
  case FilterType.GreaterThanOrEqualTo:
784
- message += ` greater than or equal to ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
817
+ message += ` greater than or equal to ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
785
818
  break;
786
819
  case FilterType.LessThan:
787
- message += ` less than ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
820
+ message += ` less than ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
788
821
  break;
789
822
  case FilterType.LessThanOrEqualTo:
790
- message += ` less than or equal to ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
823
+ message += ` less than or equal to ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
791
824
  break;
792
825
  case FilterType.NotEqualTo:
793
- message += ` not equal to ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
826
+ message += ` not equal to ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
794
827
  break;
795
828
  case FilterType.EqualTo:
796
- message += ` equal to ${CompareCriteria.formatSingleValue(data.threshold)}${unitSuffix}. `;
829
+ message += ` equal to ${CompareCriteria.formatSingleValue(data.threshold, withUnit)}. `;
797
830
  break;
798
831
  case FilterType.Contains:
799
832
  message += ` contains ${CompareCriteria.formatSingleValue(data.threshold)}. `;
@@ -918,8 +951,16 @@ export default class CompareCriteria {
918
951
  }
919
952
  }
920
953
 
954
+ /*
955
+ * `withUnit`, when supplied, is how a NUMBER is rendered — unit
956
+ * included. It is threaded down rather than applied to the finished
957
+ * string because the string is not always one number: a large window
958
+ * collapses to "N samples between X and Y", and both X and Y need the
959
+ * unit of their own scale.
960
+ */
921
961
  private static formatCriteriaValues(
922
962
  values: Array<number | boolean> | number | boolean | string,
963
+ withUnit?: ((value: number) => string) | undefined,
923
964
  ): string {
924
965
  if (Array.isArray(values)) {
925
966
  /*
@@ -934,7 +975,7 @@ export default class CompareCriteria {
934
975
  if (values.length <= MAX_INLINE) {
935
976
  return values
936
977
  .map((value: number | boolean) => {
937
- return CompareCriteria.formatSingleValue(value);
978
+ return CompareCriteria.formatSingleValue(value, withUnit);
938
979
  })
939
980
  .join(", ");
940
981
  }
@@ -950,24 +991,26 @@ export default class CompareCriteria {
950
991
  const max: number = Math.max(...numericValues);
951
992
  return `${numericValues.length} samples between ${CompareCriteria.formatSingleValue(
952
993
  min,
953
- )} and ${CompareCriteria.formatSingleValue(max)}`;
994
+ withUnit,
995
+ )} and ${CompareCriteria.formatSingleValue(max, withUnit)}`;
954
996
  }
955
997
 
956
998
  // Fall back to a truncated list when not all values are numeric
957
999
  const head: string = values
958
1000
  .slice(0, MAX_INLINE)
959
1001
  .map((value: number | boolean) => {
960
- return CompareCriteria.formatSingleValue(value);
1002
+ return CompareCriteria.formatSingleValue(value, withUnit);
961
1003
  })
962
1004
  .join(", ");
963
1005
  return `${head}, … (${values.length} values total)`;
964
1006
  }
965
1007
 
966
- return CompareCriteria.formatSingleValue(values);
1008
+ return CompareCriteria.formatSingleValue(values, withUnit);
967
1009
  }
968
1010
 
969
1011
  private static formatSingleValue(
970
1012
  value: number | boolean | string | null | undefined,
1013
+ withUnit?: ((value: number) => string) | undefined,
971
1014
  ): string {
972
1015
  if (value === null || value === undefined) {
973
1016
  return "unknown";
@@ -976,6 +1019,10 @@ export default class CompareCriteria {
976
1019
  if (typeof value === Typeof.Number) {
977
1020
  const numericValue: number = value as number;
978
1021
 
1022
+ if (withUnit) {
1023
+ return withUnit(numericValue);
1024
+ }
1025
+
979
1026
  if (Number.isInteger(numericValue)) {
980
1027
  return numericValue.toString();
981
1028
  }
@@ -26,6 +26,7 @@ import {
26
26
  } from "../../../../Types/Monitor/CriteriaFilter";
27
27
  import CaptureSpan from "../../Telemetry/CaptureSpan";
28
28
  import MetricUnitUtil from "../../../../Utils/MetricUnitUtil";
29
+ import MetricValueFormatter from "../../../../Utils/Monitor/MetricValueFormatter";
29
30
  import MetricFormulaEvaluator from "../../../../Utils/Metrics/MetricFormulaEvaluator";
30
31
  import MetricBaselineService, {
31
32
  BaselineSummary,
@@ -369,6 +370,15 @@ export default class MetricMonitorCriteria {
369
370
  criteriaFilter: input.criteriaFilter,
370
371
  metricDisplayName: metricContext.metricName,
371
372
  unit: displayUnit,
373
+ /*
374
+ * Only a plain metric criteria has a real metric NAME —
375
+ * metricContext.metricName is the formula expression when the
376
+ * criteria targets a formula, and a formula ending in `_ratio`
377
+ * would trip the fraction heuristic into a 100× render.
378
+ */
379
+ metricName: metricContext.isFormula
380
+ ? undefined
381
+ : metricContext.metricName,
372
382
  });
373
383
 
374
384
  if (!comparisonMessage) {
@@ -989,13 +999,29 @@ export default class MetricMonitorCriteria {
989
999
  : "below";
990
1000
 
991
1001
  const sigmaAbs: number = Math.abs(firstBreach.sigma);
992
- const unitSuffix: string = metricContext.unit
993
- ? ` ${metricContext.unit}`
994
- : "";
1002
+
1003
+ /*
1004
+ * Same rendering as the breaching-samples table and the comparison
1005
+ * sentence — an anomaly on a memory metric reads "1.07 GB", not
1006
+ * "1073741824.000 By". σ is a count of standard deviations, not a
1007
+ * quantity in the metric's unit, so it stays a bare number; the
1008
+ * standard deviation itself is in the metric's unit and does not.
1009
+ */
1010
+ const inMetricUnit: (value: number) => string = (value: number): string => {
1011
+ return MetricValueFormatter.format({
1012
+ value: value,
1013
+ unit: metricContext.unit,
1014
+ // Formula expressions are not metric names — see the guard above.
1015
+ metricName: metricContext.isFormula
1016
+ ? undefined
1017
+ : metricContext.metricName,
1018
+ });
1019
+ };
1020
+
995
1021
  const rootCause: string =
996
- `${metricContext.metricName} value ${firstBreach.sample.value.toFixed(3)}${unitSuffix} ` +
1022
+ `${metricContext.metricName} value ${inMetricUnit(firstBreach.sample.value)} ` +
997
1023
  `is ${sigmaAbs.toFixed(2)}σ ${direction} the same-hour baseline ` +
998
- `(mean ${baseline.mean.toFixed(3)}${unitSuffix}, σ ${baseline.stddev.toFixed(3)}${unitSuffix}, ` +
1024
+ `(mean ${inMetricUnit(baseline.mean)}, σ ${inMetricUnit(baseline.stddev)}, ` +
999
1025
  `${baseline.sampleCount} samples over ${baseline.windowDays} days, sensitivity ${sensitivity}).`;
1000
1026
 
1001
1027
  return {