@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
@@ -0,0 +1,265 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import yaml from "js-yaml";
5
+
6
+ /*
7
+ * The cache and BullMQ queue backend runs Valkey, the BSD-licensed fork of
8
+ * Redis 7.2 that most of the original Redis contributors moved to after Redis
9
+ * 7.4 left the BSD licence. The engine, the compose service, the settings and
10
+ * the Kubernetes objects are all named for it.
11
+ *
12
+ * What these tests guard is the half of that rename nobody sees until an
13
+ * upgrade goes wrong: every OLD name still has to resolve. A self-hoster who
14
+ * never edits config.env, and an operator whose values.yaml still says `redis:`,
15
+ * must both keep running. So the compose file reads `${VALKEY_X:-${REDIS_X}}`,
16
+ * keeps `redis` as a network alias, and emits the deprecated REDIS_* mirrors for
17
+ * an app image pinned to a pre-rename release.
18
+ *
19
+ * The app-side half of the fallback is covered by
20
+ * Common/Tests/Server/EnvironmentConfigValkey.test.ts, the Helm-side half by
21
+ * HelmChart/Public/oneuptime/tests/valkey-legacy-values_test.yaml, and the
22
+ * config.env upgrade path by Tests/Ops/MergeEnvTemplateRenames.test.js.
23
+ */
24
+
25
+ const REPO_ROOT: string = path.resolve(__dirname, "../../../..");
26
+ const BASE_COMPOSE_PATH: string = path.join(
27
+ REPO_ROOT,
28
+ "docker-compose.base.yml",
29
+ );
30
+ const EXAMPLE_ENV_PATH: string = path.join(REPO_ROOT, "config.example.env");
31
+ const HELM_VALUES_PATH: string = path.join(
32
+ REPO_ROOT,
33
+ "HelmChart",
34
+ "Public",
35
+ "oneuptime",
36
+ "values.yaml",
37
+ );
38
+
39
+ // The three compose files an operator actually invokes; each extends the base.
40
+ const OVERLAY_COMPOSE_FILES: Array<string> = [
41
+ "docker-compose.yml",
42
+ "docker-compose.dev.yml",
43
+ "docker-compose.billing.yml",
44
+ ];
45
+
46
+ // Every cache setting, by the suffix the two spellings share.
47
+ const SETTING_SUFFIXES: Array<string> = [
48
+ "USERNAME",
49
+ "PASSWORD",
50
+ "HOST",
51
+ "PORT",
52
+ "DB",
53
+ "IP_FAMILY",
54
+ "TLS_CA",
55
+ "TLS_SENTINEL_MODE",
56
+ ];
57
+
58
+ interface ComposeService {
59
+ image?: string;
60
+ command?: string;
61
+ healthcheck?: { test?: Array<string> };
62
+ networks?: Record<string, { aliases?: Array<string> } | null>;
63
+ extends?: { file?: string; service?: string };
64
+ }
65
+
66
+ interface ComposeFile {
67
+ services: Record<string, ComposeService>;
68
+ "x-common-runtime-variables"?: Record<string, string>;
69
+ "x-common-depends-on"?: Record<string, { condition?: string }>;
70
+ }
71
+
72
+ function readCompose(filePath: string): ComposeFile {
73
+ return yaml.load(fs.readFileSync(filePath, "utf8")) as ComposeFile;
74
+ }
75
+
76
+ function readExampleEnvValue(name: string): string | undefined {
77
+ const source: string = fs.readFileSync(EXAMPLE_ENV_PATH, "utf8");
78
+ const match: RegExpMatchArray | null = source.match(
79
+ new RegExp(`^${name}=(.*)$`, "m"),
80
+ );
81
+
82
+ return match?.[1];
83
+ }
84
+
85
+ /* `${VALKEY_X:-${REDIS_X}}` -- the current name, falling back to the old one. */
86
+ function fallbackExpression(suffix: string): string {
87
+ return `\${VALKEY_${suffix}:-\${REDIS_${suffix}}}`;
88
+ }
89
+
90
+ /* A bare `REDIS_SOMETHING=` line in config.example.env. */
91
+ const LEGACY_ENV_ASSIGNMENT: RegExp = /^REDIS_[A-Z_]+=/;
92
+
93
+ describe("valkey cache and queue backend", () => {
94
+ const base: ComposeFile = readCompose(BASE_COMPOSE_PATH);
95
+
96
+ test("docker compose runs the valkey image and not redis", () => {
97
+ const service: ComposeService | undefined = base.services["valkey"];
98
+
99
+ expect(service).toBeDefined();
100
+ expect(service!.image).toMatch(/^valkey\/valkey:/);
101
+ expect(base.services["redis"]).toBeUndefined();
102
+ });
103
+
104
+ /*
105
+ * The valkey image ships redis-server/redis-cli symlinks, so leaving the old
106
+ * binary names in place would keep working today and break silently the day
107
+ * those symlinks go. Both the entrypoint and the healthcheck name the real
108
+ * binaries.
109
+ */
110
+ test("docker compose starts and probes valkey by its own binary names", () => {
111
+ const service: ComposeService = base.services["valkey"]!;
112
+
113
+ expect(service.command).toMatch(/^valkey-server /);
114
+ expect(service.command).not.toMatch(/redis-server/);
115
+ expect(service.healthcheck?.test?.[1]).toBe("valkey-cli");
116
+ });
117
+
118
+ /*
119
+ * The server, the healthcheck and the app must all resolve to ONE password.
120
+ * If any of the three read a different expression, the stack still starts --
121
+ * it just cannot authenticate, or worse, starts on a different secret than
122
+ * the one the operator set.
123
+ */
124
+ test("the server, its healthcheck and the app share one password expression", () => {
125
+ const service: ComposeService = base.services["valkey"]!;
126
+ const expression: string = fallbackExpression("PASSWORD");
127
+
128
+ expect(service.command).toContain(`--requirepass "${expression}"`);
129
+ expect(service.healthcheck?.test).toEqual([
130
+ "CMD",
131
+ "valkey-cli",
132
+ "-a",
133
+ expression,
134
+ "ping",
135
+ ]);
136
+ expect(base["x-common-runtime-variables"]!["VALKEY_PASSWORD"]).toBe(
137
+ expression,
138
+ );
139
+ });
140
+
141
+ /*
142
+ * The upgrade case: an existing config.env holds only REDIS_*, and
143
+ * `npm run update` deliberately does not rewrite it (see
144
+ * Tests/Ops/MergeEnvTemplateRenames.test.js). The `:-` fallback in the compose
145
+ * file is the only thing that carries those values onto the new names.
146
+ */
147
+ test.each(SETTING_SUFFIXES)(
148
+ "VALKEY_%s falls back to the deprecated REDIS_ name",
149
+ (suffix: string) => {
150
+ expect(base["x-common-runtime-variables"]![`VALKEY_${suffix}`]).toBe(
151
+ fallbackExpression(suffix),
152
+ );
153
+ },
154
+ );
155
+
156
+ /*
157
+ * And the mirrors going the other way: APP_TAG lets you pin an image from
158
+ * before the rename against this compose file, and such an image reads
159
+ * REDIS_* only. Both names must carry the identical expression -- a second
160
+ * source of truth here is how the app and the server end up on different
161
+ * credentials.
162
+ */
163
+ test.each(SETTING_SUFFIXES)(
164
+ "the deprecated REDIS_%s mirror carries the identical value",
165
+ (suffix: string) => {
166
+ const common: Record<string, string> =
167
+ base["x-common-runtime-variables"]!;
168
+
169
+ expect(common[`REDIS_${suffix}`]).toBe(common[`VALKEY_${suffix}`]);
170
+ },
171
+ );
172
+
173
+ /*
174
+ * Renaming the service moved the hostname. Every config.env written before
175
+ * this change still says REDIS_HOST=redis, and those files are not rewritten
176
+ * on upgrade -- the network alias is the only thing keeping them resolving.
177
+ */
178
+ test("the old redis hostname still resolves to the valkey service", () => {
179
+ const networks: Record<string, { aliases?: Array<string> } | null> =
180
+ base.services["valkey"]!.networks!;
181
+
182
+ expect(networks["oneuptime"]?.aliases).toContain("redis");
183
+ });
184
+
185
+ test("config.example.env ships the current names only", () => {
186
+ expect(readExampleEnvValue("VALKEY_HOST")).toBe("valkey");
187
+ expect(readExampleEnvValue("VALKEY_PORT")).toBe("6379");
188
+ expect(readExampleEnvValue("VALKEY_USERNAME")).toBe("default");
189
+ expect(readExampleEnvValue("VALKEY_DB")).toBe("0");
190
+
191
+ /*
192
+ * A leftover REDIS_* line would be actively harmful, not just untidy:
193
+ * Home/Scripts/Install.sh replaces each `please-change-this-to-random-value`
194
+ * occurrence with a SEPARATE freshly generated value, so a duplicated
195
+ * password line hands the server one secret and the app another.
196
+ */
197
+ const example: string = fs.readFileSync(EXAMPLE_ENV_PATH, "utf8");
198
+ const legacyLines: Array<string> = example
199
+ .split("\n")
200
+ .filter((line: string) => {
201
+ return LEGACY_ENV_ASSIGNMENT.test(line);
202
+ });
203
+
204
+ expect(legacyLines).toEqual([]);
205
+ });
206
+
207
+ test.each(OVERLAY_COMPOSE_FILES)(
208
+ "%s extends and waits on the valkey service",
209
+ (fileName: string) => {
210
+ const overlay: ComposeFile = readCompose(path.join(REPO_ROOT, fileName));
211
+
212
+ expect(overlay.services["valkey"]?.extends?.service).toBe("valkey");
213
+ expect(overlay.services["redis"]).toBeUndefined();
214
+
215
+ /*
216
+ * Every app container gates its start on this block, so a stale service
217
+ * name here means compose refuses to start the stack at all.
218
+ */
219
+ const dependsOn: Record<string, { condition?: string }> =
220
+ overlay["x-common-depends-on"]!;
221
+
222
+ expect(dependsOn["valkey"]?.condition).toBe("service_healthy");
223
+ expect(dependsOn["redis"]).toBeUndefined();
224
+ },
225
+ );
226
+
227
+ /*
228
+ * Compose and Helm claim in their comments to run the same engine version.
229
+ * Nothing enforces that but this: the two drift apart the first time only one
230
+ * of them is bumped, and the bug surfaces as behaviour that reproduces in
231
+ * compose but not in Kubernetes (or the reverse).
232
+ */
233
+ test("helm runs the same pinned valkey image as compose", () => {
234
+ const values: {
235
+ valkey: { image: { repository: string; tag: string } };
236
+ redis: Record<string, unknown>;
237
+ } = yaml.load(fs.readFileSync(HELM_VALUES_PATH, "utf8")) as {
238
+ valkey: { image: { repository: string; tag: string } };
239
+ redis: Record<string, unknown>;
240
+ };
241
+
242
+ expect(values.valkey.image.repository).toBe("valkey/valkey");
243
+ expect(`${values.valkey.image.repository}:${values.valkey.image.tag}`).toBe(
244
+ base.services["valkey"]!.image,
245
+ );
246
+
247
+ /*
248
+ * The deprecated `redis:` alias must stay EMPTY. The merge in
249
+ * `oneuptime.valkey` layers it on top of the real defaults, so anything
250
+ * shipped here would be impossible for an operator to override.
251
+ */
252
+ expect(values.redis).toEqual({});
253
+ });
254
+
255
+ /*
256
+ * A floating tag would let a major engine version arrive on an unrelated
257
+ * `docker compose pull` or `helm upgrade`. The pin is to a minor line, so
258
+ * patch releases (including CVE fixes) still flow in.
259
+ */
260
+ test("the valkey image tag is pinned to a version line", () => {
261
+ expect(base.services["valkey"]!.image).toMatch(
262
+ /^valkey\/valkey:\d+\.\d+(\.\d+)?-alpine$/,
263
+ );
264
+ });
265
+ });
@@ -0,0 +1,382 @@
1
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
2
+ import HttpMetricsMiddleware from "../../../Server/Middleware/HttpMetricsMiddleware";
3
+ import AppMetrics from "../../../Server/Utils/Telemetry/AppMetrics";
4
+ import { ExpressRequest, ExpressResponse } from "../../../Server/Utils/Express";
5
+
6
+ /*
7
+ * Every HTTP request the server handles passes through here, so two things
8
+ * about it are disproportionately important.
9
+ *
10
+ * THE IN-FLIGHT GAUGE MUST BALANCE. It is incremented on the way in and
11
+ * decremented on the way out, and Express fires BOTH `finish` and `close` for
12
+ * most responses. A recorder that ran twice would decrement twice, and because
13
+ * the gauge is cumulative the drift never washes out - it accumulates for the
14
+ * life of the process until "active requests" is a large negative number and
15
+ * every dashboard and alert built on it is worthless. `recorded` is the latch
16
+ * that prevents it, and nothing else checks that the latch works.
17
+ *
18
+ * THE ROUTE LABEL MUST STAY LOW-CARDINALITY. It is a metric dimension, so a
19
+ * raw URL would mint a new time series per distinct path - one scan of
20
+ * /admin/<random> is enough to blow up the metric store. Express only
21
+ * populates req.route once something MATCHED, so the unmatched case (which is
22
+ * exactly the scanning case) has to fall back to a constant.
23
+ */
24
+
25
+ interface RecordedCall {
26
+ value: number;
27
+ attributes: Record<string, string | number>;
28
+ }
29
+
30
+ interface Recorder {
31
+ counter: Array<RecordedCall>;
32
+ duration: Array<RecordedCall>;
33
+ inFlight: Array<RecordedCall>;
34
+ }
35
+
36
+ type InstallRecorderFunction = () => Recorder;
37
+
38
+ const installRecorder: InstallRecorderFunction = (): Recorder => {
39
+ const recorder: Recorder = { counter: [], duration: [], inFlight: [] };
40
+
41
+ jest.spyOn(AppMetrics, "getHttpRequestCounter").mockReturnValue({
42
+ add: (value: number, attributes: Record<string, string | number>): void => {
43
+ recorder.counter.push({ value: value, attributes: attributes });
44
+ },
45
+ } as never);
46
+
47
+ jest.spyOn(AppMetrics, "getHttpRequestDuration").mockReturnValue({
48
+ record: (
49
+ value: number,
50
+ attributes: Record<string, string | number>,
51
+ ): void => {
52
+ recorder.duration.push({ value: value, attributes: attributes });
53
+ },
54
+ } as never);
55
+
56
+ jest.spyOn(AppMetrics, "getHttpRequestsInFlight").mockReturnValue({
57
+ add: (value: number, attributes: Record<string, string | number>): void => {
58
+ recorder.inFlight.push({ value: value, attributes: attributes });
59
+ },
60
+ } as never);
61
+
62
+ return recorder;
63
+ };
64
+
65
+ interface FakeResponse {
66
+ statusCode: number;
67
+ listeners: Record<string, Array<() => void>>;
68
+ emit: (event: string) => void;
69
+ }
70
+
71
+ type BuildResponseFunction = (statusCode: number) => FakeResponse;
72
+
73
+ const buildResponse: BuildResponseFunction = (
74
+ statusCode: number,
75
+ ): FakeResponse => {
76
+ const listeners: Record<string, Array<() => void>> = {};
77
+
78
+ return {
79
+ statusCode: statusCode,
80
+ listeners: listeners,
81
+ emit: (event: string): void => {
82
+ for (const listener of listeners[event] ?? []) {
83
+ listener();
84
+ }
85
+ },
86
+ on: (event: string, listener: () => void): void => {
87
+ listeners[event] = listeners[event] ?? [];
88
+ listeners[event]!.push(listener);
89
+ },
90
+ } as unknown as FakeResponse;
91
+ };
92
+
93
+ type RunFunction = (data: {
94
+ method?: string | undefined;
95
+ route?: { path?: string } | undefined;
96
+ baseUrl?: string;
97
+ statusCode?: number;
98
+ emit?: Array<string>;
99
+ }) => { recorder: Recorder; nextCalled: number };
100
+
101
+ const run: RunFunction = (data: {
102
+ method?: string | undefined;
103
+ route?: { path?: string } | undefined;
104
+ baseUrl?: string;
105
+ statusCode?: number;
106
+ emit?: Array<string>;
107
+ }): { recorder: Recorder; nextCalled: number } => {
108
+ const recorder: Recorder = installRecorder();
109
+ const res: FakeResponse = buildResponse(data.statusCode ?? 200);
110
+
111
+ let nextCalled: number = 0;
112
+
113
+ const req: Record<string, unknown> = {
114
+ method: data.method,
115
+ route: data.route,
116
+ baseUrl: data.baseUrl,
117
+ };
118
+
119
+ HttpMetricsMiddleware(
120
+ req as unknown as ExpressRequest,
121
+ res as unknown as ExpressResponse,
122
+ ((): void => {
123
+ nextCalled++;
124
+ }) as never,
125
+ );
126
+
127
+ for (const event of data.emit ?? ["finish"]) {
128
+ res.emit(event);
129
+ }
130
+
131
+ return { recorder: recorder, nextCalled: nextCalled };
132
+ };
133
+
134
+ describe("the middleware stays out of the way of the request", () => {
135
+ afterEach(() => {
136
+ jest.restoreAllMocks();
137
+ });
138
+
139
+ test("calls next exactly once, immediately", () => {
140
+ expect(run({ method: "GET" }).nextCalled).toBe(1);
141
+ });
142
+
143
+ test("counts the request as in flight before the handler runs", () => {
144
+ const recorder: Recorder = installRecorder();
145
+ const res: FakeResponse = buildResponse(200);
146
+
147
+ HttpMetricsMiddleware(
148
+ { method: "GET" } as unknown as ExpressRequest,
149
+ res as unknown as ExpressResponse,
150
+ ((): void => {}) as never,
151
+ );
152
+
153
+ // Nothing has finished yet, so the only entry is the increment.
154
+ expect(recorder.inFlight).toEqual([
155
+ { value: 1, attributes: { "http.request.method": "GET" } },
156
+ ]);
157
+ expect(recorder.counter).toEqual([]);
158
+ });
159
+ });
160
+
161
+ describe("the in-flight gauge balances", () => {
162
+ afterEach(() => {
163
+ jest.restoreAllMocks();
164
+ });
165
+
166
+ test("adds one on the way in and takes one back on the way out", () => {
167
+ const recorder: Recorder = run({ method: "GET" }).recorder;
168
+
169
+ expect(
170
+ recorder.inFlight.map((call: RecordedCall) => {
171
+ return call.value;
172
+ }),
173
+ ).toEqual([1, -1]);
174
+ });
175
+
176
+ /*
177
+ * Express fires both events for an ordinary response. Recording on each
178
+ * would decrement twice, and because the gauge is cumulative that error
179
+ * never washes out - it accumulates for the life of the process.
180
+ */
181
+ test("records once even though both finish and close fire", () => {
182
+ const recorder: Recorder = run({
183
+ method: "GET",
184
+ emit: ["finish", "close"],
185
+ }).recorder;
186
+
187
+ expect(recorder.counter.length).toBe(1);
188
+ expect(recorder.duration.length).toBe(1);
189
+ expect(
190
+ recorder.inFlight.reduce((total: number, call: RecordedCall) => {
191
+ return total + call.value;
192
+ }, 0),
193
+ ).toBe(0);
194
+ });
195
+
196
+ test("records once when only close fires - a client that hung up", () => {
197
+ const recorder: Recorder = run({ method: "GET", emit: ["close"] }).recorder;
198
+
199
+ expect(recorder.counter.length).toBe(1);
200
+ expect(
201
+ recorder.inFlight.reduce((total: number, call: RecordedCall) => {
202
+ return total + call.value;
203
+ }, 0),
204
+ ).toBe(0);
205
+ });
206
+
207
+ test("stays balanced however many times the events are re-emitted", () => {
208
+ const recorder: Recorder = run({
209
+ method: "GET",
210
+ emit: ["finish", "close", "finish", "close"],
211
+ }).recorder;
212
+
213
+ expect(recorder.counter.length).toBe(1);
214
+ expect(
215
+ recorder.inFlight.reduce((total: number, call: RecordedCall) => {
216
+ return total + call.value;
217
+ }, 0),
218
+ ).toBe(0);
219
+ });
220
+ });
221
+
222
+ describe("the route label stays low-cardinality", () => {
223
+ afterEach(() => {
224
+ jest.restoreAllMocks();
225
+ });
226
+
227
+ test("uses the route TEMPLATE, never the resolved path", () => {
228
+ const recorder: Recorder = run({
229
+ method: "GET",
230
+ route: { path: "/users/:id" },
231
+ }).recorder;
232
+
233
+ expect(recorder.counter[0]!.attributes["http.route"]).toBe("/users/:id");
234
+ });
235
+
236
+ test("prefixes the router's mount point so two routers do not collide", () => {
237
+ const recorder: Recorder = run({
238
+ method: "GET",
239
+ route: { path: "/users/:id" },
240
+ baseUrl: "/api",
241
+ }).recorder;
242
+
243
+ expect(recorder.counter[0]!.attributes["http.route"]).toBe(
244
+ "/api/users/:id",
245
+ );
246
+ });
247
+
248
+ /*
249
+ * The unmatched case IS the scanning case. Labelling it with the raw URL
250
+ * would mint one time series per probe - which is the fastest way to lose a
251
+ * metric store to somebody else's port scanner.
252
+ */
253
+ test("labels an unmatched request with a constant, not its URL", () => {
254
+ const recorder: Recorder = run({ method: "GET" }).recorder;
255
+
256
+ expect(recorder.counter[0]!.attributes["http.route"]).toBe("unmatched");
257
+ });
258
+
259
+ test("labels a route object with no path as unmatched too", () => {
260
+ const recorder: Recorder = run({ method: "GET", route: {} }).recorder;
261
+
262
+ expect(recorder.counter[0]!.attributes["http.route"]).toBe("unmatched");
263
+ });
264
+
265
+ test("carries no high-cardinality identifiers at all", () => {
266
+ const recorder: Recorder = run({
267
+ method: "GET",
268
+ route: { path: "/users/:id" },
269
+ }).recorder;
270
+
271
+ expect(Object.keys(recorder.counter[0]!.attributes).sort()).toEqual([
272
+ "http.request.method",
273
+ "http.response.status_code",
274
+ "http.route",
275
+ "status_class",
276
+ ]);
277
+ });
278
+ });
279
+
280
+ describe("the labels it records", () => {
281
+ afterEach(() => {
282
+ jest.restoreAllMocks();
283
+ });
284
+
285
+ test("upper-cases the method", () => {
286
+ const recorder: Recorder = run({ method: "post" }).recorder;
287
+
288
+ expect(recorder.counter[0]!.attributes["http.request.method"]).toBe("POST");
289
+ });
290
+
291
+ test("labels a request with no method as UNKNOWN rather than blank", () => {
292
+ const recorder: Recorder = run({ method: undefined }).recorder;
293
+
294
+ expect(recorder.counter[0]!.attributes["http.request.method"]).toBe(
295
+ "UNKNOWN",
296
+ );
297
+ });
298
+
299
+ test("records the exact status code alongside its class", () => {
300
+ const recorder: Recorder = run({
301
+ method: "GET",
302
+ statusCode: 404,
303
+ }).recorder;
304
+
305
+ expect(recorder.counter[0]!.attributes["http.response.status_code"]).toBe(
306
+ 404,
307
+ );
308
+ expect(recorder.counter[0]!.attributes["status_class"]).toBe("4xx");
309
+ });
310
+
311
+ const STATUS_CLASSES: Array<[number, string]> = [
312
+ [200, "2xx"],
313
+ [201, "2xx"],
314
+ [301, "3xx"],
315
+ [400, "4xx"],
316
+ [401, "4xx"],
317
+ [500, "5xx"],
318
+ [503, "5xx"],
319
+ [100, "1xx"],
320
+ ];
321
+
322
+ test.each(STATUS_CLASSES)(
323
+ "buckets %i as %s",
324
+ (statusCode: number, expected: string) => {
325
+ const recorder: Recorder = run({
326
+ method: "GET",
327
+ statusCode: statusCode,
328
+ }).recorder;
329
+
330
+ expect(recorder.counter[0]!.attributes["status_class"]).toBe(expected);
331
+ },
332
+ );
333
+
334
+ /*
335
+ * A response that was never given a status - a socket that died before
336
+ * anything was written - must not produce a "0xx" bucket nobody can
337
+ * interpret.
338
+ */
339
+ test("buckets a status that never got set as unknown", () => {
340
+ const recorder: Recorder = run({ method: "GET", statusCode: 0 }).recorder;
341
+
342
+ expect(recorder.counter[0]!.attributes["status_class"]).toBe("unknown");
343
+ });
344
+
345
+ test("buckets an out-of-range status as unknown", () => {
346
+ const recorder: Recorder = run({ method: "GET", statusCode: 999 }).recorder;
347
+
348
+ expect(recorder.counter[0]!.attributes["status_class"]).toBe("unknown");
349
+ });
350
+
351
+ test("the duration and the count carry identical labels", () => {
352
+ const recorder: Recorder = run({
353
+ method: "GET",
354
+ route: { path: "/users/:id" },
355
+ statusCode: 201,
356
+ }).recorder;
357
+
358
+ expect(recorder.duration[0]!.attributes).toEqual(
359
+ recorder.counter[0]!.attributes,
360
+ );
361
+ });
362
+
363
+ test("records a duration in milliseconds, not nanoseconds", () => {
364
+ const recorder: Recorder = run({ method: "GET" }).recorder;
365
+ const durationMs: number = recorder.duration[0]!.value;
366
+
367
+ expect(durationMs).toBeGreaterThanOrEqual(0);
368
+ // A synchronous test cannot plausibly take a second.
369
+ expect(durationMs).toBeLessThan(1000);
370
+ });
371
+
372
+ test("the in-flight gauge is labelled by method only", () => {
373
+ const recorder: Recorder = run({
374
+ method: "GET",
375
+ route: { path: "/users/:id" },
376
+ }).recorder;
377
+
378
+ for (const call of recorder.inFlight) {
379
+ expect(Object.keys(call.attributes)).toEqual(["http.request.method"]);
380
+ }
381
+ });
382
+ });