@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
@@ -20,6 +20,8 @@ import SnmpVendorTemplateUtil, {
20
20
  import NetworkDeviceMonitoringMethod from "../../../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
21
21
  import { NetworkDevicePollMode } from "../../../../Server/Utils/Monitor/NetworkDeviceHydrationUtil";
22
22
  import logger from "../../../../Server/Utils/Logger";
23
+ import { JSONObject } from "../../../../Types/JSON";
24
+ import { FindOperator } from "typeorm";
23
25
 
24
26
  /*
25
27
  * NetworkInventoryUtil.updateFromWalk is the single writer that keeps the
@@ -47,6 +49,8 @@ let deviceUpdateSpy: jest.SpyInstance;
47
49
  let interfaceFindSpy: jest.SpyInstance;
48
50
  let interfaceUpsertSpy: jest.SpyInstance;
49
51
  let endpointUpsertSpy: jest.SpyInstance;
52
+ let deviceLookupSpy: jest.SpyInstance;
53
+ let macWriteSpy: jest.SpyInstance;
50
54
 
51
55
  function mockServices(
52
56
  existingInterfaces: Array<NetworkInterface> = [],
@@ -119,6 +123,19 @@ function mockServices(
119
123
  endpointUpsertSpy = jest
120
124
  .spyOn(NetworkEndpointService, "upsertDiscoveredEndpoints")
121
125
  .mockResolvedValue(undefined as never);
126
+ /*
127
+ * Device MAC learning follows the endpoint upsert on any walk that
128
+ * carried ARP bindings: one findBy for the devices registered at the
129
+ * bound addresses, then a hook-free compare-and-set write per device the
130
+ * planner picked. Stubbed here so the ARP cases never reach a database;
131
+ * the learning block seeds findBy with real rows where it matters.
132
+ */
133
+ deviceLookupSpy = jest
134
+ .spyOn(NetworkDeviceService, "findBy")
135
+ .mockResolvedValue([]);
136
+ macWriteSpy = jest
137
+ .spyOn(NetworkDeviceService, "updateColumnsByIdWithoutHooks")
138
+ .mockResolvedValue(undefined);
122
139
  }
123
140
 
124
141
  function deviceUpdatePayload(): DeviceUpdatePayload {
@@ -1101,6 +1118,306 @@ describe("NetworkInventoryUtil.updateFromWalk — endpoint discovery", () => {
1101
1118
  });
1102
1119
  });
1103
1120
 
1121
+ /*
1122
+ * A walked router's ARP table binds the address a managed device is
1123
+ * registered at to the MAC the switches learned - the one fact a ping-only
1124
+ * register or handset never reports about itself. After the endpoint
1125
+ * inventory is written, the walk hands those bindings to
1126
+ * NetworkDeviceMacLearningUtil, which stamps the MAC on the device row while
1127
+ * its column is still empty. The planner's rules are pinned in
1128
+ * Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts and the database contract
1129
+ * in Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts; what is
1130
+ * pinned HERE is the hand-off from the walk: when learning runs, what it is
1131
+ * scoped by, and that a failure in it stays in it.
1132
+ */
1133
+ describe("NetworkInventoryUtil.updateFromWalk — device MAC learning", () => {
1134
+ const SITE_ID: ObjectID = new ObjectID(
1135
+ "5a5a5a5a-0000-4000-8000-00000000000a",
1136
+ );
1137
+ const OTHER_SITE_ID: ObjectID = new ObjectID(
1138
+ "5b5b5b5b-0000-4000-8000-00000000000b",
1139
+ );
1140
+ const REGISTER_ID: string = "8f2c1f0e-0000-4000-8000-0000000000bb";
1141
+ const HANDSET_ID: string = "8f2c1f0e-0000-4000-8000-0000000000cc";
1142
+ const REGISTER_IP: string = "10.0.0.5";
1143
+ const REGISTER_MAC: string = "aa:bb:cc:00:11:22";
1144
+
1145
+ // A managed, ping-only device registered at the address the router bound.
1146
+ function registeredDevice(
1147
+ id: string,
1148
+ overrides?: { siteId?: ObjectID; macAddress?: string },
1149
+ ): NetworkDevice {
1150
+ const row: NetworkDevice = new NetworkDevice();
1151
+ row.id = new ObjectID(id);
1152
+ row.hostname = REGISTER_IP;
1153
+ if (overrides?.siteId) {
1154
+ row.siteId = overrides.siteId;
1155
+ }
1156
+ if (overrides?.macAddress !== undefined) {
1157
+ row.macAddress = overrides.macAddress;
1158
+ }
1159
+ return row;
1160
+ }
1161
+
1162
+ // The ARP table spelling differs from the column's: learning normalizes.
1163
+ function arpWalk(): Partial<SnmpMonitorResponse> {
1164
+ return {
1165
+ arpEntries: [
1166
+ {
1167
+ ipAddress: REGISTER_IP,
1168
+ macAddress: "AA-BB-CC-00-11-22",
1169
+ interfaceIndex: 3,
1170
+ entryType: "dynamic",
1171
+ },
1172
+ ],
1173
+ };
1174
+ }
1175
+
1176
+ /*
1177
+ * The addresses the device lookup asked about. QueryHelper.any renders
1178
+ * them into a TypeORM Raw operator with a RANDOM parameter name, so the
1179
+ * list is recovered from the operator's parameters.
1180
+ */
1181
+ function addressesLookedUp(): Array<string> {
1182
+ const query: JSONObject = (deviceLookupSpy.mock.calls[0]![0] as JSONObject)[
1183
+ "query"
1184
+ ] as JSONObject;
1185
+ const operator: FindOperator<string> = query[
1186
+ "hostname"
1187
+ ] as unknown as FindOperator<string>;
1188
+ const parameters: Record<string, unknown> =
1189
+ (operator.objectLiteralParameters || {}) as Record<string, unknown>;
1190
+ const values: Array<unknown> =
1191
+ (Object.values(parameters)[0] as Array<unknown>) || [];
1192
+ return values.map((value: unknown): string => {
1193
+ return String(value);
1194
+ });
1195
+ }
1196
+
1197
+ function spyError(): jest.SpyInstance {
1198
+ return jest.spyOn(logger, "error").mockImplementation(() => {
1199
+ return undefined;
1200
+ });
1201
+ }
1202
+
1203
+ test("a walk carrying an ARP entry looks up the devices at the bound address, in the walked device's project", async () => {
1204
+ mockServices([], { siteId: SITE_ID });
1205
+ deviceLookupSpy.mockResolvedValue([
1206
+ registeredDevice(REGISTER_ID, { siteId: SITE_ID }),
1207
+ ]);
1208
+
1209
+ await runWalk(arpWalk());
1210
+
1211
+ expect(deviceLookupSpy).toHaveBeenCalledTimes(1);
1212
+
1213
+ const query: JSONObject = (deviceLookupSpy.mock.calls[0]![0] as JSONObject)[
1214
+ "query"
1215
+ ] as JSONObject;
1216
+
1217
+ expect((query["projectId"] as ObjectID).toString()).toBe(PROJECT_ID);
1218
+ expect(addressesLookedUp()).toEqual([REGISTER_IP]);
1219
+ });
1220
+
1221
+ test("a device with an empty MAC in the router's site is stamped through the hook-free compare-and-set write", async () => {
1222
+ mockServices([], { siteId: SITE_ID });
1223
+ deviceLookupSpy.mockResolvedValue([
1224
+ registeredDevice(REGISTER_ID, { siteId: SITE_ID }),
1225
+ ]);
1226
+
1227
+ await runWalk(arpWalk());
1228
+
1229
+ expect(macWriteSpy).toHaveBeenCalledTimes(1);
1230
+
1231
+ const write: JSONObject = macWriteSpy.mock.calls[0]![0] as JSONObject;
1232
+
1233
+ expect((write["id"] as ObjectID).toString()).toBe(REGISTER_ID);
1234
+ expect(write["data"]).toEqual({
1235
+ macAddress: REGISTER_MAC,
1236
+ isMacAddressLearned: true,
1237
+ });
1238
+ expect(write["expectedData"]).toEqual({
1239
+ macAddress: null,
1240
+ deletedAt: null,
1241
+ });
1242
+ // The walked router's own row is untouched by learning.
1243
+ expect(deviceUpdateSpy).toHaveBeenCalledTimes(1);
1244
+ expect(deviceUpdatePayload()).not.toHaveProperty("macAddress");
1245
+ });
1246
+
1247
+ test("learning runs after the endpoint inventory has been written", async () => {
1248
+ mockServices([], { siteId: SITE_ID });
1249
+
1250
+ await runWalk(arpWalk());
1251
+
1252
+ expect(endpointUpsertSpy).toHaveBeenCalledTimes(1);
1253
+ expect(deviceLookupSpy).toHaveBeenCalledTimes(1);
1254
+ expect(endpointUpsertSpy.mock.invocationCallOrder[0]!).toBeLessThan(
1255
+ deviceLookupSpy.mock.invocationCallOrder[0]!,
1256
+ );
1257
+ });
1258
+
1259
+ test("a walk with FDB entries only never looks devices up", async () => {
1260
+ mockServices([], { siteId: SITE_ID });
1261
+
1262
+ await runWalk({
1263
+ interfaces: [walkedInterface({ interfaceIndex: 5, name: "Gi0/5" })],
1264
+ fdbEntries: [
1265
+ {
1266
+ macAddress: REGISTER_MAC,
1267
+ bridgePort: 5,
1268
+ interfaceIndex: 5,
1269
+ status: "learned",
1270
+ },
1271
+ ],
1272
+ });
1273
+
1274
+ // The attachment was still written; only the ARP half was absent.
1275
+ expect(endpointUpsertSpy).toHaveBeenCalledTimes(1);
1276
+ expect(deviceLookupSpy).not.toHaveBeenCalled();
1277
+ expect(macWriteSpy).not.toHaveBeenCalled();
1278
+ });
1279
+
1280
+ test("a walk without endpoint arrays at all never looks devices up", async () => {
1281
+ mockServices([], { siteId: SITE_ID });
1282
+
1283
+ await runWalk({ interfaces: [walkedInterface()] });
1284
+
1285
+ expect(deviceLookupSpy).not.toHaveBeenCalled();
1286
+ expect(macWriteSpy).not.toHaveBeenCalled();
1287
+ });
1288
+
1289
+ /*
1290
+ * Learning keys off the bindings the attachment pass KEPT, not the raw
1291
+ * ARP rows: an entry for the router's own interface MAC is dropped there
1292
+ * and must not come back as a lookup here.
1293
+ */
1294
+ test("an ARP entry the attachment pass discards never reaches learning", async () => {
1295
+ mockServices([], { siteId: SITE_ID });
1296
+
1297
+ await runWalk({
1298
+ interfaces: [
1299
+ walkedInterface({ interfaceIndex: 3, macAddress: REGISTER_MAC }),
1300
+ ],
1301
+ ...arpWalk(),
1302
+ });
1303
+
1304
+ expect(endpointUpsertSpy).not.toHaveBeenCalled();
1305
+ expect(deviceLookupSpy).not.toHaveBeenCalled();
1306
+ expect(macWriteSpy).not.toHaveBeenCalled();
1307
+ });
1308
+
1309
+ test("a failure in the learning step is logged and does not prevent the walk from completing", async () => {
1310
+ mockServices([], { siteId: SITE_ID });
1311
+ deviceLookupSpy.mockRejectedValue(new Error("connection reset"));
1312
+ const error: jest.SpyInstance = spyError();
1313
+
1314
+ const snmpResponse: SnmpMonitorResponse = await runWalk(arpWalk());
1315
+
1316
+ // Nothing escaped, and everything before the learning step landed.
1317
+ expect(snmpResponse).toBeDefined();
1318
+ expect(deviceUpdateSpy).toHaveBeenCalledTimes(1);
1319
+ expect(endpointUpsertSpy).toHaveBeenCalledTimes(1);
1320
+ expect(macWriteSpy).not.toHaveBeenCalled();
1321
+
1322
+ const messages: Array<string> = error.mock.calls.map(
1323
+ (call: Array<unknown>) => {
1324
+ return String(call[0]);
1325
+ },
1326
+ );
1327
+
1328
+ /*
1329
+ * Its OWN catch, naming the device - never the outer "failed to update
1330
+ * network inventory" line, which would read as the endpoint upsert
1331
+ * having failed too.
1332
+ */
1333
+ expect(
1334
+ messages.some((message: string) => {
1335
+ return message.includes("MAC") && message.includes(DEVICE_ID);
1336
+ }),
1337
+ ).toBe(true);
1338
+ expect(
1339
+ messages.some((message: string) => {
1340
+ return message.includes("Failed to update network inventory");
1341
+ }),
1342
+ ).toBe(false);
1343
+ });
1344
+
1345
+ test("a failing MAC write is contained the same way", async () => {
1346
+ mockServices([], { siteId: SITE_ID });
1347
+ deviceLookupSpy.mockResolvedValue([
1348
+ registeredDevice(REGISTER_ID, { siteId: SITE_ID }),
1349
+ ]);
1350
+ macWriteSpy.mockRejectedValue(new Error("deadlock detected"));
1351
+ const error: jest.SpyInstance = spyError();
1352
+
1353
+ await expect(runWalk(arpWalk())).resolves.toBeDefined();
1354
+
1355
+ expect(endpointUpsertSpy).toHaveBeenCalledTimes(1);
1356
+ expect(error).toHaveBeenCalled();
1357
+ });
1358
+
1359
+ test("the observing device's site scopes the match: a device in another site is not stamped", async () => {
1360
+ mockServices([], { siteId: SITE_ID });
1361
+ deviceLookupSpy.mockResolvedValue([
1362
+ registeredDevice(REGISTER_ID, { siteId: OTHER_SITE_ID }),
1363
+ ]);
1364
+
1365
+ await runWalk(arpWalk());
1366
+
1367
+ // Looked up - the read is project-wide by design - but refused.
1368
+ expect(deviceLookupSpy).toHaveBeenCalledTimes(1);
1369
+ expect(macWriteSpy).not.toHaveBeenCalled();
1370
+ });
1371
+
1372
+ test("an observing device with no site stamps only a device with no site", async () => {
1373
+ // The default device carries no siteId: a project without sites.
1374
+ mockServices();
1375
+ const siteless: NetworkDevice = registeredDevice(HANDSET_ID);
1376
+ deviceLookupSpy.mockResolvedValue([
1377
+ registeredDevice(REGISTER_ID, { siteId: SITE_ID }),
1378
+ ]);
1379
+
1380
+ await runWalk(arpWalk());
1381
+ expect(macWriteSpy).not.toHaveBeenCalled();
1382
+
1383
+ deviceLookupSpy.mockResolvedValue([siteless]);
1384
+
1385
+ await runWalk(arpWalk());
1386
+ expect(macWriteSpy).toHaveBeenCalledTimes(1);
1387
+ expect(
1388
+ (
1389
+ (macWriteSpy.mock.calls[0]![0] as JSONObject)["id"] as ObjectID
1390
+ ).toString(),
1391
+ ).toBe(HANDSET_ID);
1392
+ });
1393
+
1394
+ test("a device that already carries a MAC is left alone", async () => {
1395
+ mockServices([], { siteId: SITE_ID });
1396
+ deviceLookupSpy.mockResolvedValue([
1397
+ registeredDevice(REGISTER_ID, {
1398
+ siteId: SITE_ID,
1399
+ macAddress: "00:11:22:33:44:55",
1400
+ }),
1401
+ ]);
1402
+
1403
+ await runWalk(arpWalk());
1404
+
1405
+ expect(deviceLookupSpy).toHaveBeenCalledTimes(1);
1406
+ expect(macWriteSpy).not.toHaveBeenCalled();
1407
+ });
1408
+
1409
+ test("selects siteId on the device read, or learning could never scope by site", async () => {
1410
+ mockServices();
1411
+
1412
+ await runWalk();
1413
+
1414
+ const findArgs: { select?: Record<string, boolean> } = deviceFindSpy.mock
1415
+ .calls[0]![0] as unknown as { select?: Record<string, boolean> };
1416
+
1417
+ expect(findArgs.select?.["siteId"]).toBe(true);
1418
+ });
1419
+ });
1420
+
1104
1421
  describe("NetworkInventoryUtil.updateFromWalk — vendor health template auto-apply", () => {
1105
1422
  /*
1106
1423
  * The automatic counterpart of the dashboard's vendor-template banner:
@@ -0,0 +1,281 @@
1
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
2
+ import type { Context } from "@opentelemetry/api";
3
+ import type { ReadableSpan, Span } from "@opentelemetry/sdk-trace-base";
4
+ import ContextSpanProcessor from "../../../../Server/Utils/Telemetry/ContextSpanProcessor";
5
+ import TelemetryContext from "../../../../Server/Utils/Telemetry/TelemetryContext";
6
+
7
+ /*
8
+ * This processor runs on the creation of EVERY span in every OneUptime
9
+ * process. That is what makes it useful - it is how projectId and userId
10
+ * reach the ~1958 attribute-less @CaptureSpan spans without touching any of
11
+ * those call sites - and it is also what makes it dangerous.
12
+ *
13
+ * Two properties follow.
14
+ *
15
+ * IT MUST NEVER THROW. onStart sits inside the OpenTelemetry SDK's tracer,
16
+ * on the hot path of every traced operation in the product. An exception
17
+ * escaping it does not lose a span, it fails the OPERATION the span was
18
+ * describing - telemetry taking down the thing it was watching. The
19
+ * swallow-everything catch is the point of the file, so the tests drive it
20
+ * with a context that throws and a span that throws.
21
+ *
22
+ * IT MUST NOT WRITE undefined. A context attribute that is absent has to
23
+ * stay absent rather than being stamped as a literal `undefined`, which is
24
+ * both a wasted attribute slot on every span and a value that reads, on a
25
+ * dashboard, as though the identifier were known to be empty.
26
+ */
27
+
28
+ interface RecordedAttribute {
29
+ key: string;
30
+ value: unknown;
31
+ }
32
+
33
+ interface FakeSpan {
34
+ attributes: Array<RecordedAttribute>;
35
+ }
36
+
37
+ type BuildSpanFunction = (options?: { throwOnSet?: boolean }) => {
38
+ span: Span;
39
+ recorded: Array<RecordedAttribute>;
40
+ };
41
+
42
+ const buildSpan: BuildSpanFunction = (options?: {
43
+ throwOnSet?: boolean;
44
+ }): { span: Span; recorded: Array<RecordedAttribute> } => {
45
+ const recorded: Array<RecordedAttribute> = [];
46
+
47
+ const span: FakeSpan & {
48
+ setAttribute: (key: string, value: unknown) => void;
49
+ } = {
50
+ attributes: recorded,
51
+ setAttribute: (key: string, value: unknown): void => {
52
+ if (options?.throwOnSet) {
53
+ throw new Error("span is already ended");
54
+ }
55
+ recorded.push({ key: key, value: value });
56
+ },
57
+ };
58
+
59
+ return { span: span as unknown as Span, recorded: recorded };
60
+ };
61
+
62
+ const EMPTY_CONTEXT: Context = {} as Context;
63
+
64
+ type StartInContextFunction = (
65
+ attributes: Record<string, string | number | boolean>,
66
+ options?: { throwOnSet?: boolean },
67
+ ) => Array<RecordedAttribute>;
68
+
69
+ const startInContext: StartInContextFunction = (
70
+ attributes: Record<string, string | number | boolean>,
71
+ options?: { throwOnSet?: boolean },
72
+ ): Array<RecordedAttribute> => {
73
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
74
+ buildSpan(options);
75
+
76
+ TelemetryContext.runWithContext(attributes, (): void => {
77
+ new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
78
+ });
79
+
80
+ return built.recorded;
81
+ };
82
+
83
+ describe("the ambient context is stamped onto every span", () => {
84
+ afterEach(() => {
85
+ jest.restoreAllMocks();
86
+ });
87
+
88
+ test("copies the tenant identifiers of the enclosing scope", () => {
89
+ const recorded: Array<RecordedAttribute> = startInContext({
90
+ projectId: "project-1",
91
+ userId: "user-1",
92
+ });
93
+
94
+ expect(recorded).toEqual(
95
+ expect.arrayContaining([
96
+ { key: "projectId", value: "project-1" },
97
+ { key: "userId", value: "user-1" },
98
+ ]),
99
+ );
100
+ });
101
+
102
+ test("copies numbers and booleans without coercing them to strings", () => {
103
+ const recorded: Array<RecordedAttribute> = startInContext({
104
+ retryCount: 3,
105
+ isRetry: true,
106
+ });
107
+
108
+ expect(recorded).toEqual(
109
+ expect.arrayContaining([
110
+ { key: "retryCount", value: 3 },
111
+ { key: "isRetry", value: true },
112
+ ]),
113
+ );
114
+ });
115
+
116
+ /*
117
+ * Zero and false are real values. A truthiness check instead of an
118
+ * undefined check would silently drop exactly the attributes an operator
119
+ * most wants to filter on - "the runs where retryCount is 0".
120
+ */
121
+ test("copies zero and false, which a truthiness check would have dropped", () => {
122
+ const recorded: Array<RecordedAttribute> = startInContext({
123
+ retryCount: 0,
124
+ isRetry: false,
125
+ emptyLabel: "",
126
+ });
127
+
128
+ expect(recorded).toEqual(
129
+ expect.arrayContaining([
130
+ { key: "retryCount", value: 0 },
131
+ { key: "isRetry", value: false },
132
+ { key: "emptyLabel", value: "" },
133
+ ]),
134
+ );
135
+ });
136
+
137
+ test("writes nothing at all when there is no ambient scope", () => {
138
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
139
+ buildSpan();
140
+
141
+ new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
142
+
143
+ expect(built.recorded).toEqual([]);
144
+ });
145
+
146
+ test("writes nothing when the scope is empty", () => {
147
+ expect(startInContext({})).toEqual([]);
148
+ });
149
+
150
+ /*
151
+ * An absent identifier must stay absent. Stamping a literal `undefined`
152
+ * wastes an attribute slot on every span and, on a dashboard, reads as
153
+ * though the identifier were known to be empty.
154
+ */
155
+ test("never stamps an undefined value", () => {
156
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
157
+ buildSpan();
158
+
159
+ jest.spyOn(TelemetryContext, "getAttributes").mockReturnValue({
160
+ projectId: "project-1",
161
+ monitorId: undefined,
162
+ incidentId: null,
163
+ } as unknown as Record<string, string | number | boolean>);
164
+
165
+ new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
166
+
167
+ expect(
168
+ built.recorded.map((attribute: RecordedAttribute) => {
169
+ return attribute.key;
170
+ }),
171
+ ).toEqual(["projectId"]);
172
+ });
173
+
174
+ test("a nested scope stamps the identifiers of both levels", () => {
175
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
176
+ buildSpan();
177
+
178
+ TelemetryContext.runWithContext({ projectId: "project-1" }, (): void => {
179
+ TelemetryContext.runWithContext({ monitorId: "monitor-1" }, (): void => {
180
+ new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
181
+ });
182
+ });
183
+
184
+ expect(built.recorded).toEqual(
185
+ expect.arrayContaining([
186
+ { key: "projectId", value: "project-1" },
187
+ { key: "monitorId", value: "monitor-1" },
188
+ ]),
189
+ );
190
+ });
191
+
192
+ test("the scope does not leak out of the run it was seeded for", () => {
193
+ TelemetryContext.runWithContext({ projectId: "project-1" }, (): void => {
194
+ // seeded and immediately left
195
+ });
196
+
197
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
198
+ buildSpan();
199
+
200
+ new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
201
+
202
+ expect(built.recorded).toEqual([]);
203
+ });
204
+ });
205
+
206
+ describe("telemetry never takes down the operation it is watching", () => {
207
+ afterEach(() => {
208
+ jest.restoreAllMocks();
209
+ });
210
+
211
+ /*
212
+ * onStart runs inside the SDK's tracer, on the hot path of every traced
213
+ * operation. An exception escaping it does not lose a span - it fails the
214
+ * work the span was describing.
215
+ */
216
+ test("swallows a span that refuses attributes", () => {
217
+ expect(() => {
218
+ return startInContext({ projectId: "project-1" }, { throwOnSet: true });
219
+ }).not.toThrow();
220
+ });
221
+
222
+ test("swallows a context that throws while being read", () => {
223
+ jest.spyOn(TelemetryContext, "getAttributes").mockImplementation(() => {
224
+ throw new Error("async local storage is unhappy");
225
+ });
226
+
227
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
228
+ buildSpan();
229
+
230
+ expect(() => {
231
+ return new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
232
+ }).not.toThrow();
233
+ });
234
+
235
+ test("swallows a context that is not an object at all", () => {
236
+ jest
237
+ .spyOn(TelemetryContext, "getAttributes")
238
+ .mockReturnValue(null as unknown as Record<string, string>);
239
+
240
+ const built: { span: Span; recorded: Array<RecordedAttribute> } =
241
+ buildSpan();
242
+
243
+ expect(() => {
244
+ return new ContextSpanProcessor().onStart(built.span, EMPTY_CONTEXT);
245
+ }).not.toThrow();
246
+ });
247
+ });
248
+
249
+ describe("the rest of the SpanProcessor contract", () => {
250
+ /*
251
+ * Enrichment happens entirely at creation, so onEnd is deliberately inert.
252
+ * Doing anything to an ended span here would be both useless and a way to
253
+ * throw on the export path.
254
+ */
255
+ test("onEnd does nothing and does not throw", () => {
256
+ expect(() => {
257
+ return new ContextSpanProcessor().onEnd({} as ReadableSpan);
258
+ }).not.toThrow();
259
+ });
260
+
261
+ test("shutdown resolves, so the SDK's teardown is never blocked", async () => {
262
+ await expect(
263
+ new ContextSpanProcessor().shutdown(),
264
+ ).resolves.toBeUndefined();
265
+ });
266
+
267
+ test("forceFlush resolves, so a flush before exit never hangs", async () => {
268
+ await expect(
269
+ new ContextSpanProcessor().forceFlush(),
270
+ ).resolves.toBeUndefined();
271
+ });
272
+
273
+ test("shutdown and forceFlush stay resolvable after an enrichment failure", async () => {
274
+ const processor: ContextSpanProcessor = new ContextSpanProcessor();
275
+
276
+ processor.onStart(buildSpan({ throwOnSet: true }).span, EMPTY_CONTEXT);
277
+
278
+ await expect(processor.forceFlush()).resolves.toBeUndefined();
279
+ await expect(processor.shutdown()).resolves.toBeUndefined();
280
+ });
281
+ });