@oneuptime/common 12.0.5 → 12.0.6

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 (528) hide show
  1. package/Models/DatabaseModels/AIRun.ts +33 -0
  2. package/Models/DatabaseModels/DataSource.ts +744 -0
  3. package/Models/DatabaseModels/Index.ts +3 -0
  4. package/Models/DatabaseModels/ProjectCallSMSConfig.ts +10 -2
  5. package/Models/DatabaseModels/User.ts +2 -0
  6. package/Models/DatabaseModels/UserOnCallLog.ts +2 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +2 -0
  8. package/Models/DatabaseModels/UserSession.ts +2 -0
  9. package/Models/DatabaseModels/UserTotpAuth.ts +6 -4
  10. package/Models/DatabaseModels/UserWebAuthn.ts +5 -3
  11. package/Server/API/AIInsightAPI.ts +47 -4
  12. package/Server/API/AIInvestigationAPI.ts +25 -0
  13. package/Server/API/DashboardAPI.ts +176 -492
  14. package/Server/API/DataSourceAPI.ts +268 -0
  15. package/Server/API/StatusPageAPI.ts +11 -1
  16. package/Server/API/UserAPI.ts +119 -0
  17. package/Server/Infrastructure/ClickhouseConfig.ts +59 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.ts +39 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.ts +24 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.ts +76 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  22. package/Server/Infrastructure/Queue.ts +26 -0
  23. package/Server/Infrastructure/QueueWorker.ts +17 -0
  24. package/Server/Middleware/TelemetryIngest.ts +8 -3
  25. package/Server/Services/AIInsightService.ts +132 -12
  26. package/Server/Services/AIRunService.ts +34 -0
  27. package/Server/Services/AIService.ts +17 -2
  28. package/Server/Services/AlertService.ts +38 -32
  29. package/Server/Services/AnalyticsDatabaseService.ts +8 -4
  30. package/Server/Services/DataSourceService.ts +200 -0
  31. package/Server/Services/HostService.ts +92 -0
  32. package/Server/Services/IncidentService.ts +45 -0
  33. package/Server/Services/Index.ts +2 -0
  34. package/Server/Services/MonitorService.ts +49 -0
  35. package/Server/Services/MonitorStatusService.ts +166 -0
  36. package/Server/Services/OpenTelemetryIngestService.ts +255 -184
  37. package/Server/Services/RumSessionPinService.ts +7 -21
  38. package/Server/Services/StatusPageSubscriberService.ts +13 -0
  39. package/Server/Services/UserService.ts +280 -9
  40. package/Server/Static/Vendor/README.md +62 -0
  41. package/Server/Static/Vendor/fonts/InterVariable.woff2 +0 -0
  42. package/Server/Static/Vendor/highlight/highlight.min.js +1244 -0
  43. package/Server/Static/Vendor/highlight/languages/apache.min.js +15 -0
  44. package/Server/Static/Vendor/highlight/languages/bash.min.js +21 -0
  45. package/Server/Static/Vendor/highlight/languages/c.min.js +41 -0
  46. package/Server/Static/Vendor/highlight/languages/cpp.min.js +47 -0
  47. package/Server/Static/Vendor/highlight/languages/csharp.min.js +49 -0
  48. package/Server/Static/Vendor/highlight/languages/css.min.js +31 -0
  49. package/Server/Static/Vendor/highlight/languages/diff.min.js +9 -0
  50. package/Server/Static/Vendor/highlight/languages/dns.min.js +11 -0
  51. package/Server/Static/Vendor/highlight/languages/dockerfile.min.js +8 -0
  52. package/Server/Static/Vendor/highlight/languages/go.min.js +20 -0
  53. package/Server/Static/Vendor/highlight/languages/graphql.min.js +12 -0
  54. package/Server/Static/Vendor/highlight/languages/http.min.js +14 -0
  55. package/Server/Static/Vendor/highlight/languages/ini.min.js +15 -0
  56. package/Server/Static/Vendor/highlight/languages/java.min.js +38 -0
  57. package/Server/Static/Vendor/highlight/languages/javascript.min.js +81 -0
  58. package/Server/Static/Vendor/highlight/languages/json.min.js +8 -0
  59. package/Server/Static/Vendor/highlight/languages/kotlin.min.js +46 -0
  60. package/Server/Static/Vendor/highlight/languages/lua.min.js +15 -0
  61. package/Server/Static/Vendor/highlight/languages/makefile.min.js +14 -0
  62. package/Server/Static/Vendor/highlight/languages/nginx.min.js +21 -0
  63. package/Server/Static/Vendor/highlight/languages/perl.min.js +41 -0
  64. package/Server/Static/Vendor/highlight/languages/php.min.js +59 -0
  65. package/Server/Static/Vendor/highlight/languages/powershell.min.js +39 -0
  66. package/Server/Static/Vendor/highlight/languages/python.min.js +42 -0
  67. package/Server/Static/Vendor/highlight/languages/r.min.js +26 -0
  68. package/Server/Static/Vendor/highlight/languages/ruby.min.js +54 -0
  69. package/Server/Static/Vendor/highlight/languages/rust.min.js +27 -0
  70. package/Server/Static/Vendor/highlight/languages/scala.min.js +28 -0
  71. package/Server/Static/Vendor/highlight/languages/sql.min.js +21 -0
  72. package/Server/Static/Vendor/highlight/languages/swift.min.js +68 -0
  73. package/Server/Static/Vendor/highlight/languages/typescript.min.js +99 -0
  74. package/Server/Static/Vendor/highlight/languages/xml.min.js +29 -0
  75. package/Server/Static/Vendor/highlight/languages/yaml.min.js +28 -0
  76. package/Server/Static/Vendor/highlight/styles/github-dark.min.css +10 -0
  77. package/Server/Static/Vendor/highlight/styles/vs2015.min.css +1 -0
  78. package/Server/Static/Vendor/tailwind/tailwind-3.4.5.js +21028 -0
  79. package/Server/Types/Database/Permissions/BasePermission.ts +7 -0
  80. package/Server/Types/Database/Permissions/DeletePermission.ts +2 -0
  81. package/Server/Types/Database/Permissions/ReadPermission.ts +3 -0
  82. package/Server/Types/Database/Permissions/TenantPermission.ts +101 -16
  83. package/Server/Utils/AI/CodeFix/FixRunBudget.ts +2 -2
  84. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +90 -11
  85. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +1 -0
  86. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +1 -0
  87. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.ts +125 -0
  88. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +25 -15
  89. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +256 -57
  90. package/Server/Utils/AI/SRE/Insights/InsightStore.ts +76 -18
  91. package/Server/Utils/AI/SRE/InvestigationTldr.ts +352 -0
  92. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +7 -2
  93. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +1793 -0
  94. package/Server/Utils/Dashboard/PublicDashboardViewConfig.ts +65 -0
  95. package/Server/Utils/DataSource/ConnectorRegistry.ts +53 -0
  96. package/Server/Utils/DataSource/Connectors/ClickHouseConnector.ts +338 -0
  97. package/Server/Utils/DataSource/Connectors/ElasticsearchConnector.ts +717 -0
  98. package/Server/Utils/DataSource/Connectors/LokiConnector.ts +606 -0
  99. package/Server/Utils/DataSource/Connectors/MySqlConnector.ts +307 -0
  100. package/Server/Utils/DataSource/Connectors/PostgresConnector.ts +260 -0
  101. package/Server/Utils/DataSource/Connectors/PrometheusConnector.ts +555 -0
  102. package/Server/Utils/DataSource/Connectors/RestApiConnector.ts +597 -0
  103. package/Server/Utils/DataSource/Connectors/SqlServerConnector.ts +318 -0
  104. package/Server/Utils/DataSource/EgressGuard.ts +446 -0
  105. package/Server/Utils/DataSource/HttpFetch.ts +221 -0
  106. package/Server/Utils/DataSource/SqlQueryGuard.ts +383 -0
  107. package/Server/Utils/DataSource/SqlResultShaper.ts +372 -0
  108. package/Server/Utils/DataSource/Types.ts +93 -0
  109. package/Server/Utils/Database/PostgresErrorTranslator.ts +172 -17
  110. package/Server/Utils/InProcessMemo.ts +141 -0
  111. package/Server/Utils/InstanceHealth/TelemetryIngestion.ts +628 -0
  112. package/Server/Utils/Monitor/MonitorMetricUtil.ts +58 -0
  113. package/Server/Utils/Monitor/MonitorResource.ts +14 -0
  114. package/Server/Utils/Response.ts +12 -1
  115. package/Server/Utils/SSRFProtection.ts +237 -0
  116. package/Server/Utils/StartServer.ts +8 -0
  117. package/Server/Utils/StatusPageSubscriberWebhook.ts +30 -0
  118. package/Server/Utils/Telemetry/CaptureSpan.ts +30 -18
  119. package/Server/Utils/Telemetry/IngestionTimestamp.ts +75 -0
  120. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +99 -7
  121. package/Server/Utils/Telemetry/Telemetry.ts +112 -1
  122. package/Server/Utils/Telemetry.ts +104 -3
  123. package/Server/Utils/VendorAssets.ts +165 -0
  124. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +28 -5
  125. package/Server/Utils/Workspace/Slack/Slack.ts +5 -0
  126. package/Server/Views/Partials/Head.ejs +21 -7
  127. package/Tests/App/AdminDashboard/AdminProjectsIdFilter.test.tsx +207 -0
  128. package/Tests/App/AdminDashboard/AdminUsersIdFilter.test.tsx +216 -0
  129. package/Tests/App/Dashboard/AIInsightViewActions.test.tsx +422 -0
  130. package/Tests/App/Dashboard/AddWidgetModal.test.tsx +10 -3
  131. package/Tests/App/Dashboard/AlertsSideMenu.test.tsx +387 -0
  132. package/Tests/App/Dashboard/ChatActivityFeed.test.tsx +199 -0
  133. package/Tests/App/Dashboard/DataSourceChartSyntheticConfig.test.tsx +736 -0
  134. package/Tests/App/Dashboard/DataSourceQueryEditor.test.tsx +868 -0
  135. package/Tests/App/Dashboard/DataSourceWidgetCatalog.test.ts +440 -0
  136. package/Tests/App/Dashboard/DataSourceWidgetFetching.test.tsx +2109 -0
  137. package/Tests/App/Dashboard/DataSourceWidgetViews.test.tsx +726 -0
  138. package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +447 -0
  139. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +514 -3
  140. package/Tests/App/Dashboard/ProductMenuBreadcrumbs.test.tsx +391 -0
  141. package/Tests/App/Dashboard/ScheduledMaintenanceSideMenu.test.tsx +326 -0
  142. package/Tests/App/Dashboard/SideMenuHarness.tsx +187 -0
  143. package/Tests/App/Dashboard/WidgetCatalog.test.ts +6 -1
  144. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +1858 -0
  145. package/Tests/Models/DatabaseModels/DataSourceModelSecrets.test.ts +70 -0
  146. package/Tests/Server/API/AIInvestigationAPI.test.ts +187 -0
  147. package/Tests/Server/API/DashboardPublicDataSourceStrip.test.ts +686 -0
  148. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +585 -823
  149. package/Tests/Server/API/DataSourceAPIRoutes.test.ts +486 -0
  150. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +291 -0
  151. package/Tests/Server/API/UserAuthenticationAPI.test.ts +641 -0
  152. package/Tests/Server/Infrastructure/ClickhouseKeepAliveOptions.test.ts +441 -0
  153. package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +140 -0
  154. package/Tests/Server/Services/AIInsightService.test.ts +266 -11
  155. package/Tests/Server/Services/AIRunAnalysisTldrPersistence.test.ts +160 -0
  156. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +4 -0
  157. package/Tests/Server/Services/AlertMetricResourceAttributes.test.ts +352 -0
  158. package/Tests/Server/Services/DataSourceServiceSettings.test.ts +244 -0
  159. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +397 -0
  160. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +41 -2
  161. package/Tests/Server/Services/IncidentMetricResourceAttributes.test.ts +408 -0
  162. package/Tests/Server/Services/MonitorStatusServiceDeleteReferenceCleanup.test.ts +351 -0
  163. package/Tests/Server/Services/MonitorStatusServiceValidation.test.ts +171 -0
  164. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +398 -0
  165. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +19 -0
  166. package/Tests/Server/Services/ProjectCallSMSConfigCredentialUniqueness.test.ts +305 -0
  167. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +36 -1
  168. package/Tests/Server/Services/RumSessionPinUniqueViolationRecovery.test.ts +163 -0
  169. package/Tests/Server/Services/UserAuthenticationService.test.ts +973 -0
  170. package/Tests/Server/Types/Database/ObjectIdColumnQuery.test.ts +262 -0
  171. package/Tests/Server/Types/Database/Permissions/TenantPermission.test.ts +358 -0
  172. package/Tests/Server/Utils/AI/FixRunBudget.test.ts +2 -2
  173. package/Tests/Server/Utils/AI/Insights/ExceptionSpikeDetector.test.ts +6 -1
  174. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +121 -4
  175. package/Tests/Server/Utils/AI/Insights/NewExceptionDetector.test.ts +361 -10
  176. package/Tests/Server/Utils/AI/InvestigationSettlement.test.ts +292 -0
  177. package/Tests/Server/Utils/AI/InvestigationTldr.test.ts +623 -0
  178. package/Tests/Server/Utils/AI/InvestigationTldrWiring.test.ts +98 -0
  179. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +857 -0
  180. package/Tests/Server/Utils/Dashboard/PublicDashboardViewConfig.test.ts +480 -0
  181. package/Tests/Server/Utils/DataSource/ClickHouseConnector.test.ts +759 -0
  182. package/Tests/Server/Utils/DataSource/ConnectorRegistry.test.ts +37 -0
  183. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +645 -0
  184. package/Tests/Server/Utils/DataSource/ElasticsearchConnector.test.ts +1206 -0
  185. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +616 -0
  186. package/Tests/Server/Utils/DataSource/LokiConnector.test.ts +970 -0
  187. package/Tests/Server/Utils/DataSource/MySqlConnector.test.ts +935 -0
  188. package/Tests/Server/Utils/DataSource/PostgresConnector.test.ts +854 -0
  189. package/Tests/Server/Utils/DataSource/PrometheusConnector.test.ts +881 -0
  190. package/Tests/Server/Utils/DataSource/RestApiConnector.test.ts +1020 -0
  191. package/Tests/Server/Utils/DataSource/SqlQueryGuard.test.ts +425 -0
  192. package/Tests/Server/Utils/DataSource/SqlResultShaper.test.ts +555 -0
  193. package/Tests/Server/Utils/DataSource/SqlServerConnector.test.ts +934 -0
  194. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +228 -7
  195. package/Tests/Server/Utils/InProcessMemo.test.ts +292 -0
  196. package/Tests/Server/Utils/InstanceHealth/TelemetryIngestion.test.ts +960 -0
  197. package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +103 -0
  198. package/Tests/Server/Utils/Monitor/MonitorMetricResourceAttributes.test.ts +336 -0
  199. package/Tests/Server/Utils/OfflineAssetHygiene.test.ts +521 -0
  200. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +143 -0
  201. package/Tests/Server/Utils/SSRFProtection.test.ts +193 -0
  202. package/Tests/Server/Utils/StatusPageSubscriberWebhook.test.ts +101 -0
  203. package/Tests/Server/Utils/TailwindBuildCopy.test.ts +225 -0
  204. package/Tests/Server/Utils/Telemetry/ResourceHeartbeatL1Memo.test.ts +355 -0
  205. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +128 -0
  206. package/Tests/Server/Utils/Telemetry/TelemetryUtilAttributeKeys.test.ts +381 -0
  207. package/Tests/Server/Utils/Telemetry.test.ts +87 -0
  208. package/Tests/Server/Utils/VendorAssetIntegrity.test.ts +343 -0
  209. package/Tests/Server/Utils/VendorAssets.test.ts +671 -0
  210. package/Tests/Server/Views/OfflineRender.test.ts +297 -0
  211. package/Tests/Types/Dashboard/DataSourceComponentTypes.test.ts +618 -0
  212. package/Tests/Types/DateClickhouseFormat.test.ts +192 -0
  213. package/Tests/Types/Password.test.ts +363 -0
  214. package/Tests/UI/Components/ConfirmModal.test.tsx +7 -0
  215. package/Tests/UI/Components/ErrorBoundary.test.tsx +53 -0
  216. package/Tests/UI/Components/Modal.test.tsx +281 -1
  217. package/Tests/UI/Components/ObjectIdFilterPipeline.test.tsx +396 -0
  218. package/Tests/UI/Components/StatusPage/ResourceGroupNavigator.test.tsx +1147 -0
  219. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +116 -1
  220. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  221. package/Tests/Utils/Analytics.test.ts +23 -0
  222. package/Tests/Utils/Dashboard/Components/DashboardDataSourceComponents.test.ts +640 -0
  223. package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +138 -0
  224. package/Tests/Utils/DataSource/DataSourceValueReducer.test.ts +526 -0
  225. package/Tests/Utils/Metrics/MetricResourceAttributeUtil.test.ts +691 -0
  226. package/Tests/Utils/StatusPage/GroupHierarchyView.test.ts +1452 -0
  227. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +931 -0
  228. package/Tests/Utils/Telemetry/CaptureSpanExportGating.test.ts +288 -0
  229. package/Tests/Utils/Telemetry/IngestionTimestamp.test.ts +170 -0
  230. package/Types/AI/AIInsightEvidence.ts +6 -0
  231. package/Types/Analytics/RevenueEvent.ts +31 -0
  232. package/Types/Dashboard/DashboardComponentType.ts +31 -0
  233. package/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.ts +27 -0
  234. package/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.ts +32 -0
  235. package/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.ts +30 -0
  236. package/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.ts +55 -0
  237. package/Types/DataSource/DataSourceLimits.ts +62 -0
  238. package/Types/DataSource/DataSourceQueryConfig.ts +41 -0
  239. package/Types/DataSource/DataSourceTableResult.ts +38 -0
  240. package/Types/DataSource/DataSourceType.ts +213 -0
  241. package/Types/Date.ts +43 -3
  242. package/Types/Password.ts +64 -0
  243. package/Types/Permission.ts +46 -0
  244. package/Types/UserAuthenticationStatus.ts +34 -0
  245. package/UI/Components/Button/Button.tsx +1 -1
  246. package/UI/Components/ErrorBoundary.tsx +459 -108
  247. package/UI/Components/Icon/Icon.tsx +10 -9
  248. package/UI/Components/Modal/ConfirmModal.tsx +6 -1
  249. package/UI/Components/Modal/Modal.tsx +267 -35
  250. package/UI/Components/Modal/ModalFooter.tsx +6 -1
  251. package/UI/Components/MoreMenu/MoreMenu.tsx +19 -4
  252. package/UI/Components/MoreMenu/MoreMenuItem.tsx +6 -1
  253. package/UI/Components/StatusPage/ResourceGrid.tsx +314 -0
  254. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +808 -0
  255. package/UI/Components/StatusPage/ResourceList.tsx +570 -0
  256. package/UI/Styles/Theme.css +18 -0
  257. package/UI/Utils/API/RequestOptions.ts +7 -0
  258. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +1 -0
  259. package/UI/Utils/ModelAPI/ModelAPI.ts +1 -0
  260. package/UI/Utils/TestDataSource.ts +61 -0
  261. package/UI/esbuild-config.js +45 -0
  262. package/Utils/Analytics.ts +15 -0
  263. package/Utils/Dashboard/Components/DashboardDataSourceChartComponent.ts +88 -0
  264. package/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.ts +123 -0
  265. package/Utils/Dashboard/Components/DashboardDataSourceShared.ts +16 -0
  266. package/Utils/Dashboard/Components/DashboardDataSourceTableComponent.ts +72 -0
  267. package/Utils/Dashboard/Components/DashboardDataSourceValueComponent.ts +126 -0
  268. package/Utils/Dashboard/Components/Index.ts +28 -0
  269. package/Utils/DataSource/DataSourceQueryText.ts +94 -0
  270. package/Utils/DataSource/DataSourceValueReducer.ts +156 -0
  271. package/Utils/Metrics/MetricResourceAttributeUtil.ts +380 -0
  272. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +8 -6
  273. package/Utils/StatusPage/GroupHierarchyView.ts +931 -0
  274. package/Utils/StatusPage/ResourceExplorer.ts +853 -0
  275. package/build/dist/Models/DatabaseModels/AIRun.js +34 -0
  276. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  277. package/build/dist/Models/DatabaseModels/DataSource.js +774 -0
  278. package/build/dist/Models/DatabaseModels/DataSource.js.map +1 -0
  279. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  280. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  281. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js +14 -4
  282. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js.map +1 -1
  283. package/build/dist/Models/DatabaseModels/User.js +2 -0
  284. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  285. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +2 -0
  286. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  287. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +2 -0
  288. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  289. package/build/dist/Models/DatabaseModels/UserSession.js +2 -0
  290. package/build/dist/Models/DatabaseModels/UserSession.js.map +1 -1
  291. package/build/dist/Models/DatabaseModels/UserTotpAuth.js +6 -4
  292. package/build/dist/Models/DatabaseModels/UserTotpAuth.js.map +1 -1
  293. package/build/dist/Models/DatabaseModels/UserWebAuthn.js +5 -3
  294. package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
  295. package/build/dist/Server/API/AIInsightAPI.js +34 -4
  296. package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
  297. package/build/dist/Server/API/AIInvestigationAPI.js +23 -0
  298. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  299. package/build/dist/Server/API/DashboardAPI.js +119 -433
  300. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  301. package/build/dist/Server/API/DataSourceAPI.js +173 -0
  302. package/build/dist/Server/API/DataSourceAPI.js.map +1 -0
  303. package/build/dist/Server/API/StatusPageAPI.js +11 -1
  304. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  305. package/build/dist/Server/API/UserAPI.js +94 -2
  306. package/build/dist/Server/API/UserAPI.js.map +1 -1
  307. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +49 -0
  308. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  309. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.js +20 -0
  310. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.js.map +1 -0
  311. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.js +19 -0
  312. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.js.map +1 -0
  313. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.js +64 -0
  314. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.js.map +1 -0
  315. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  316. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  317. package/build/dist/Server/Infrastructure/Queue.js +27 -0
  318. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  319. package/build/dist/Server/Infrastructure/QueueWorker.js +13 -0
  320. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  321. package/build/dist/Server/Middleware/TelemetryIngest.js +8 -3
  322. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  323. package/build/dist/Server/Services/AIInsightService.js +110 -10
  324. package/build/dist/Server/Services/AIInsightService.js.map +1 -1
  325. package/build/dist/Server/Services/AIRunService.js +34 -0
  326. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  327. package/build/dist/Server/Services/AIService.js +16 -2
  328. package/build/dist/Server/Services/AIService.js.map +1 -1
  329. package/build/dist/Server/Services/AlertService.js +28 -35
  330. package/build/dist/Server/Services/AlertService.js.map +1 -1
  331. package/build/dist/Server/Services/AnalyticsDatabaseService.js +8 -4
  332. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  333. package/build/dist/Server/Services/DataSourceService.js +159 -0
  334. package/build/dist/Server/Services/DataSourceService.js.map +1 -0
  335. package/build/dist/Server/Services/HostService.js +74 -0
  336. package/build/dist/Server/Services/HostService.js.map +1 -1
  337. package/build/dist/Server/Services/IncidentService.js +35 -24
  338. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  339. package/build/dist/Server/Services/Index.js +2 -0
  340. package/build/dist/Server/Services/Index.js.map +1 -1
  341. package/build/dist/Server/Services/MonitorService.js +45 -0
  342. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  343. package/build/dist/Server/Services/MonitorStatusService.js +129 -0
  344. package/build/dist/Server/Services/MonitorStatusService.js.map +1 -1
  345. package/build/dist/Server/Services/OpenTelemetryIngestService.js +222 -159
  346. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  347. package/build/dist/Server/Services/RumSessionPinService.js +7 -15
  348. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  349. package/build/dist/Server/Services/StatusPageSubscriberService.js +10 -0
  350. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  351. package/build/dist/Server/Services/UserService.js +241 -10
  352. package/build/dist/Server/Services/UserService.js.map +1 -1
  353. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +6 -0
  354. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/DeletePermission.js +1 -0
  356. package/build/dist/Server/Types/Database/Permissions/DeletePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +2 -0
  358. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +68 -12
  360. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  361. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js +2 -2
  362. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js.map +1 -1
  363. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +68 -8
  364. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  365. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +1 -0
  366. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +1 -0
  368. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  369. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.js +97 -0
  370. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.js.map +1 -0
  371. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +17 -15
  372. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  373. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +178 -54
  374. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  375. package/build/dist/Server/Utils/AI/SRE/Insights/InsightStore.js +65 -16
  376. package/build/dist/Server/Utils/AI/SRE/Insights/InsightStore.js.map +1 -1
  377. package/build/dist/Server/Utils/AI/SRE/InvestigationTldr.js +303 -0
  378. package/build/dist/Server/Utils/AI/SRE/InvestigationTldr.js.map +1 -0
  379. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -1
  380. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +1261 -0
  381. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -0
  382. package/build/dist/Server/Utils/Dashboard/PublicDashboardViewConfig.js +47 -0
  383. package/build/dist/Server/Utils/Dashboard/PublicDashboardViewConfig.js.map +1 -0
  384. package/build/dist/Server/Utils/DataSource/ConnectorRegistry.js +42 -0
  385. package/build/dist/Server/Utils/DataSource/ConnectorRegistry.js.map +1 -0
  386. package/build/dist/Server/Utils/DataSource/Connectors/ClickHouseConnector.js +187 -0
  387. package/build/dist/Server/Utils/DataSource/Connectors/ClickHouseConnector.js.map +1 -0
  388. package/build/dist/Server/Utils/DataSource/Connectors/ElasticsearchConnector.js +493 -0
  389. package/build/dist/Server/Utils/DataSource/Connectors/ElasticsearchConnector.js.map +1 -0
  390. package/build/dist/Server/Utils/DataSource/Connectors/LokiConnector.js +399 -0
  391. package/build/dist/Server/Utils/DataSource/Connectors/LokiConnector.js.map +1 -0
  392. package/build/dist/Server/Utils/DataSource/Connectors/MySqlConnector.js +195 -0
  393. package/build/dist/Server/Utils/DataSource/Connectors/MySqlConnector.js.map +1 -0
  394. package/build/dist/Server/Utils/DataSource/Connectors/PostgresConnector.js +149 -0
  395. package/build/dist/Server/Utils/DataSource/Connectors/PostgresConnector.js.map +1 -0
  396. package/build/dist/Server/Utils/DataSource/Connectors/PrometheusConnector.js +362 -0
  397. package/build/dist/Server/Utils/DataSource/Connectors/PrometheusConnector.js.map +1 -0
  398. package/build/dist/Server/Utils/DataSource/Connectors/RestApiConnector.js +342 -0
  399. package/build/dist/Server/Utils/DataSource/Connectors/RestApiConnector.js.map +1 -0
  400. package/build/dist/Server/Utils/DataSource/Connectors/SqlServerConnector.js +186 -0
  401. package/build/dist/Server/Utils/DataSource/Connectors/SqlServerConnector.js.map +1 -0
  402. package/build/dist/Server/Utils/DataSource/EgressGuard.js +305 -0
  403. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -0
  404. package/build/dist/Server/Utils/DataSource/HttpFetch.js +134 -0
  405. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -0
  406. package/build/dist/Server/Utils/DataSource/SqlQueryGuard.js +331 -0
  407. package/build/dist/Server/Utils/DataSource/SqlQueryGuard.js.map +1 -0
  408. package/build/dist/Server/Utils/DataSource/SqlResultShaper.js +280 -0
  409. package/build/dist/Server/Utils/DataSource/SqlResultShaper.js.map +1 -0
  410. package/build/dist/Server/Utils/DataSource/Types.js +2 -0
  411. package/build/dist/Server/Utils/DataSource/Types.js.map +1 -0
  412. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +103 -9
  413. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -1
  414. package/build/dist/Server/Utils/InProcessMemo.js +113 -0
  415. package/build/dist/Server/Utils/InProcessMemo.js.map +1 -0
  416. package/build/dist/Server/Utils/InstanceHealth/TelemetryIngestion.js +406 -0
  417. package/build/dist/Server/Utils/InstanceHealth/TelemetryIngestion.js.map +1 -0
  418. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +34 -0
  419. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  420. package/build/dist/Server/Utils/Monitor/MonitorResource.js +14 -0
  421. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  422. package/build/dist/Server/Utils/Response.js +9 -1
  423. package/build/dist/Server/Utils/Response.js.map +1 -1
  424. package/build/dist/Server/Utils/SSRFProtection.js +192 -0
  425. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -0
  426. package/build/dist/Server/Utils/StartServer.js +7 -0
  427. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  428. package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js +24 -0
  429. package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js.map +1 -1
  430. package/build/dist/Server/Utils/Telemetry/CaptureSpan.js +25 -11
  431. package/build/dist/Server/Utils/Telemetry/CaptureSpan.js.map +1 -1
  432. package/build/dist/Server/Utils/Telemetry/IngestionTimestamp.js +32 -0
  433. package/build/dist/Server/Utils/Telemetry/IngestionTimestamp.js.map +1 -0
  434. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +92 -5
  435. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -1
  436. package/build/dist/Server/Utils/Telemetry/Telemetry.js +92 -6
  437. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  438. package/build/dist/Server/Utils/Telemetry.js +93 -6
  439. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  440. package/build/dist/Server/Utils/VendorAssets.js +121 -0
  441. package/build/dist/Server/Utils/VendorAssets.js.map +1 -0
  442. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -4
  443. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  444. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +5 -0
  445. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  446. package/build/dist/Types/Analytics/RevenueEvent.js +26 -0
  447. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -0
  448. package/build/dist/Types/Dashboard/DashboardComponentType.js +27 -0
  449. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  450. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.js +2 -0
  451. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.js.map +1 -0
  452. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.js +2 -0
  453. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.js.map +1 -0
  454. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.js +2 -0
  455. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.js.map +1 -0
  456. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.js +16 -0
  457. package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.js.map +1 -0
  458. package/build/dist/Types/DataSource/DataSourceLimits.js +47 -0
  459. package/build/dist/Types/DataSource/DataSourceLimits.js.map +1 -0
  460. package/build/dist/Types/DataSource/DataSourceQueryConfig.js +10 -0
  461. package/build/dist/Types/DataSource/DataSourceQueryConfig.js.map +1 -0
  462. package/build/dist/Types/DataSource/DataSourceTableResult.js +14 -0
  463. package/build/dist/Types/DataSource/DataSourceTableResult.js.map +1 -0
  464. package/build/dist/Types/DataSource/DataSourceType.js +183 -0
  465. package/build/dist/Types/DataSource/DataSourceType.js.map +1 -0
  466. package/build/dist/Types/Date.js +38 -3
  467. package/build/dist/Types/Date.js.map +1 -1
  468. package/build/dist/Types/Password.js +56 -0
  469. package/build/dist/Types/Password.js.map +1 -0
  470. package/build/dist/Types/Permission.js +42 -0
  471. package/build/dist/Types/Permission.js.map +1 -1
  472. package/build/dist/Types/UserAuthenticationStatus.js +2 -0
  473. package/build/dist/Types/UserAuthenticationStatus.js.map +1 -0
  474. package/build/dist/UI/Components/ErrorBoundary.js +388 -72
  475. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  476. package/build/dist/UI/Components/Icon/Icon.js +10 -4
  477. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  478. package/build/dist/UI/Components/Modal/ConfirmModal.js +1 -1
  479. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  480. package/build/dist/UI/Components/Modal/Modal.js +178 -19
  481. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  482. package/build/dist/UI/Components/Modal/ModalFooter.js +3 -1
  483. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  484. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +9 -6
  485. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  486. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +7 -1
  487. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  488. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +119 -0
  489. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -0
  490. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +354 -0
  491. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -0
  492. package/build/dist/UI/Components/StatusPage/ResourceList.js +203 -0
  493. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -0
  494. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +1 -6
  495. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  496. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +3 -8
  497. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  498. package/build/dist/UI/Utils/TestDataSource.js +39 -0
  499. package/build/dist/UI/Utils/TestDataSource.js.map +1 -0
  500. package/build/dist/Utils/Analytics.js +4 -0
  501. package/build/dist/Utils/Analytics.js.map +1 -1
  502. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceChartComponent.js +79 -0
  503. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceChartComponent.js.map +1 -0
  504. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.js +111 -0
  505. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.js.map +1 -0
  506. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceShared.js +15 -0
  507. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceShared.js.map +1 -0
  508. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceTableComponent.js +62 -0
  509. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceTableComponent.js.map +1 -0
  510. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceValueComponent.js +112 -0
  511. package/build/dist/Utils/Dashboard/Components/DashboardDataSourceValueComponent.js.map +1 -0
  512. package/build/dist/Utils/Dashboard/Components/Index.js +16 -0
  513. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  514. package/build/dist/Utils/DataSource/DataSourceQueryText.js +64 -0
  515. package/build/dist/Utils/DataSource/DataSourceQueryText.js.map +1 -0
  516. package/build/dist/Utils/DataSource/DataSourceValueReducer.js +124 -0
  517. package/build/dist/Utils/DataSource/DataSourceValueReducer.js.map +1 -0
  518. package/build/dist/Utils/Metrics/MetricResourceAttributeUtil.js +281 -0
  519. package/build/dist/Utils/Metrics/MetricResourceAttributeUtil.js.map +1 -0
  520. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +8 -6
  521. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
  522. package/build/dist/Utils/StatusPage/GroupHierarchyView.js +560 -0
  523. package/build/dist/Utils/StatusPage/GroupHierarchyView.js.map +1 -0
  524. package/build/dist/Utils/StatusPage/ResourceExplorer.js +519 -0
  525. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -0
  526. package/jest.config.json +1 -0
  527. package/package.json +5 -1
  528. package/tsconfig.json +12 -10
@@ -0,0 +1,1206 @@
1
+ import ElasticsearchConnector, {
2
+ applyElasticsearchTimeMacros,
3
+ buildElasticsearchSearchUrl,
4
+ parseHitsToTable,
5
+ parseQueryDsl,
6
+ parseTimeSeriesAggregations,
7
+ } from "../../../../Server/Utils/DataSource/Connectors/ElasticsearchConnector";
8
+ import DataSourceEgressGuard from "../../../../Server/Utils/DataSource/EgressGuard";
9
+ import DataSourceHttpFetch, {
10
+ DataSourceHttpRequest,
11
+ DataSourceHttpResponse,
12
+ } from "../../../../Server/Utils/DataSource/HttpFetch";
13
+ import {
14
+ DataSourceConnectionSettings,
15
+ DataSourceQueryWindow,
16
+ } from "../../../../Server/Utils/DataSource/Types";
17
+ import AggregatedResult from "../../../../Types/BaseDatabase/AggregatedResult";
18
+ import {
19
+ DATA_SOURCE_CONNECT_TIMEOUT_IN_MS,
20
+ DATA_SOURCE_MAX_TABLE_ROWS,
21
+ DATA_SOURCE_QUERY_TIMEOUT_IN_MS,
22
+ } from "../../../../Types/DataSource/DataSourceLimits";
23
+ import DataSourceTableResult, {
24
+ DataSourceTableColumn,
25
+ DataSourceTableColumnType,
26
+ } from "../../../../Types/DataSource/DataSourceTableResult";
27
+ import DataSourceType from "../../../../Types/DataSource/DataSourceType";
28
+ import BadDataException from "../../../../Types/Exception/BadDataException";
29
+ import { afterEach, describe, expect, test } from "@jest/globals";
30
+
31
+ const SECRET_PASSWORD: string = "sup3r-secret-pass";
32
+ const SECRET_TOKEN: string = "secret-api-token-value";
33
+
34
+ /*
35
+ * Fixed window every test queries over:
36
+ * 2026-01-01T00:00:00Z ... 2026-01-02T00:00:00Z.
37
+ */
38
+ const START_DATE: Date = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
39
+ const END_DATE: Date = new Date(Date.UTC(2026, 0, 2, 0, 0, 0));
40
+ const START_EPOCH_MS: number = START_DATE.getTime();
41
+ const END_EPOCH_MS: number = END_DATE.getTime();
42
+
43
+ const HOUR_MS: number = 60 * 60 * 1000;
44
+
45
+ function makeWindow(): DataSourceQueryWindow {
46
+ return { startDate: START_DATE, endDate: END_DATE };
47
+ }
48
+
49
+ function makeSettings(
50
+ overrides?: Partial<DataSourceConnectionSettings>,
51
+ ): DataSourceConnectionSettings {
52
+ return {
53
+ dataSourceType: DataSourceType.Elasticsearch,
54
+ url: "https://elastic.example.com:9200",
55
+ username: "readonly_user",
56
+ password: SECRET_PASSWORD,
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ function jsonResponse(
62
+ payload: unknown,
63
+ statusCode: number = 200,
64
+ ): DataSourceHttpResponse {
65
+ return {
66
+ statusCode: statusCode,
67
+ bodyText: JSON.stringify(payload),
68
+ bodyJson: payload,
69
+ };
70
+ }
71
+
72
+ function mockGuard(): jest.SpyInstance {
73
+ return jest
74
+ .spyOn(DataSourceEgressGuard, "assertHostnameAllowed")
75
+ .mockResolvedValue([{ address: "203.0.113.10", family: 4 }]);
76
+ }
77
+
78
+ function mockFetch(response: DataSourceHttpResponse): jest.SpyInstance {
79
+ return jest.spyOn(DataSourceHttpFetch, "fetch").mockResolvedValue(response);
80
+ }
81
+
82
+ function getRequest(
83
+ spy: jest.SpyInstance,
84
+ callIndex: number = 0,
85
+ ): DataSourceHttpRequest {
86
+ return spy.mock.calls[callIndex]![0] as DataSourceHttpRequest;
87
+ }
88
+
89
+ function getRequestBody(spy: jest.SpyInstance): Record<string, unknown> {
90
+ return JSON.parse(getRequest(spy).body as string) as Record<string, unknown>;
91
+ }
92
+
93
+ const HISTOGRAM_QUERY: string =
94
+ '{ "query": { "match_all": {} }, "aggs": { "over_time": { "date_histogram": { "field": "@timestamp", "fixed_interval": "1h" } } } }';
95
+
96
+ /*
97
+ * ES 8-shaped date_histogram response: buckets carry both the epoch-ms key
98
+ * and key_as_string, plus a metric sub-aggregation.
99
+ */
100
+ function histogramResponse(): unknown {
101
+ return {
102
+ took: 3,
103
+ timed_out: false,
104
+ hits: { total: { value: 12, relation: "eq" }, hits: [] },
105
+ aggregations: {
106
+ over_time: {
107
+ buckets: [
108
+ {
109
+ key_as_string: "2026-01-01T00:00:00.000Z",
110
+ key: START_EPOCH_MS,
111
+ doc_count: 5,
112
+ avg_latency: { value: 12.5 },
113
+ },
114
+ {
115
+ key_as_string: "2026-01-01T01:00:00.000Z",
116
+ key: START_EPOCH_MS + HOUR_MS,
117
+ doc_count: 7,
118
+ avg_latency: { value: 15 },
119
+ },
120
+ ],
121
+ },
122
+ },
123
+ };
124
+ }
125
+
126
+ function termsResponse(): unknown {
127
+ return {
128
+ aggregations: {
129
+ by_service: {
130
+ doc_count_error_upper_bound: 0,
131
+ sum_other_doc_count: 0,
132
+ buckets: [
133
+ {
134
+ key: "api",
135
+ doc_count: 10,
136
+ over_time: {
137
+ buckets: [
138
+ { key: START_EPOCH_MS, doc_count: 4 },
139
+ { key: START_EPOCH_MS + HOUR_MS, doc_count: 6 },
140
+ ],
141
+ },
142
+ },
143
+ {
144
+ key: "worker",
145
+ doc_count: 6,
146
+ over_time: {
147
+ buckets: [{ key: START_EPOCH_MS, doc_count: 6 }],
148
+ },
149
+ },
150
+ ],
151
+ },
152
+ },
153
+ };
154
+ }
155
+
156
+ function makeHit(
157
+ id: string,
158
+ source: Record<string, unknown> | undefined,
159
+ ): Record<string, unknown> {
160
+ return {
161
+ _index: "logs-2026.01.01",
162
+ _id: id,
163
+ _score: 1,
164
+ _source: source,
165
+ };
166
+ }
167
+
168
+ function hitsResponse(
169
+ hits: Array<Record<string, unknown>>,
170
+ total: unknown,
171
+ ): unknown {
172
+ return { hits: { total: total, hits: hits } };
173
+ }
174
+
175
+ afterEach(() => {
176
+ jest.restoreAllMocks();
177
+ });
178
+
179
+ describe("applyElasticsearchTimeMacros", () => {
180
+ test("replaces ms macros with epoch numbers and plain macros with unquoted ISO strings", () => {
181
+ const substituted: string = applyElasticsearchTimeMacros(
182
+ '{"gte": $__startTimeMs, "lte": $__endTimeMs, "from": "$__startTime", "to": "$__endTime"}',
183
+ START_DATE,
184
+ END_DATE,
185
+ );
186
+
187
+ expect(substituted).toBe(
188
+ `{"gte": ${START_EPOCH_MS}, "lte": ${END_EPOCH_MS}, "from": "2026-01-01T00:00:00.000Z", "to": "2026-01-02T00:00:00.000Z"}`,
189
+ );
190
+ });
191
+
192
+ test("replaces every occurrence and leaves unrelated text untouched", () => {
193
+ const substituted: string = applyElasticsearchTimeMacros(
194
+ "$__startTime $__startTime keep-this $__endTimeMs",
195
+ START_DATE,
196
+ END_DATE,
197
+ );
198
+
199
+ expect(substituted).toBe(
200
+ `2026-01-01T00:00:00.000Z 2026-01-01T00:00:00.000Z keep-this ${END_EPOCH_MS}`,
201
+ );
202
+ });
203
+ });
204
+
205
+ describe("parseQueryDsl", () => {
206
+ test("parses a Query DSL body with macros inside existing string quotes", () => {
207
+ const body: Record<string, unknown> = parseQueryDsl(
208
+ '{ "query": { "range": { "@timestamp": { "gte": "$__startTime", "lte": "$__endTime" } } } }',
209
+ START_DATE,
210
+ END_DATE,
211
+ );
212
+
213
+ const query: Record<string, unknown> = body["query"] as Record<
214
+ string,
215
+ unknown
216
+ >;
217
+ const range: Record<string, unknown> = query["range"] as Record<
218
+ string,
219
+ unknown
220
+ >;
221
+ const timestampFilter: Record<string, unknown> = range[
222
+ "@timestamp"
223
+ ] as Record<string, unknown>;
224
+
225
+ expect(timestampFilter["gte"]).toBe("2026-01-01T00:00:00.000Z");
226
+ expect(timestampFilter["lte"]).toBe("2026-01-02T00:00:00.000Z");
227
+ });
228
+
229
+ test("throws a BadDataException naming the parse problem for invalid JSON", () => {
230
+ expect(() => {
231
+ return parseQueryDsl('{ "query": ', START_DATE, END_DATE);
232
+ }).toThrow(BadDataException);
233
+ expect(() => {
234
+ return parseQueryDsl('{ "query": ', START_DATE, END_DATE);
235
+ }).toThrow(/Query is not valid JSON/);
236
+ });
237
+
238
+ test("throws when the query is empty or whitespace", () => {
239
+ expect(() => {
240
+ return parseQueryDsl("", START_DATE, END_DATE);
241
+ }).toThrow(/Query is required/);
242
+ expect(() => {
243
+ return parseQueryDsl(" ", START_DATE, END_DATE);
244
+ }).toThrow(/Query is required/);
245
+ });
246
+
247
+ test("throws when the query is a JSON array or primitive", () => {
248
+ expect(() => {
249
+ return parseQueryDsl("[1, 2]", START_DATE, END_DATE);
250
+ }).toThrow(/must be a JSON object/);
251
+ expect(() => {
252
+ return parseQueryDsl("42", START_DATE, END_DATE);
253
+ }).toThrow(/must be a JSON object/);
254
+ });
255
+ });
256
+
257
+ describe("buildElasticsearchSearchUrl", () => {
258
+ test("defaults the index to * and trims trailing slashes from the base URL", () => {
259
+ const url: string = buildElasticsearchSearchUrl(
260
+ makeSettings({ url: "https://elastic.example.com:9200///" }),
261
+ );
262
+
263
+ expect(url).toBe("https://elastic.example.com:9200/*/_search");
264
+ });
265
+
266
+ test("URL-encodes the configured index into the path", () => {
267
+ const url: string = buildElasticsearchSearchUrl(
268
+ makeSettings({
269
+ additionalOptions: { elasticsearchIndex: "logs/2026 team" },
270
+ }),
271
+ );
272
+
273
+ expect(url).toBe(
274
+ "https://elastic.example.com:9200/logs%2F2026%20team/_search",
275
+ );
276
+ });
277
+
278
+ test("keeps wildcard index patterns usable after encoding", () => {
279
+ const url: string = buildElasticsearchSearchUrl(
280
+ makeSettings({
281
+ additionalOptions: { elasticsearchIndex: "logs-*" },
282
+ }),
283
+ );
284
+
285
+ expect(url).toBe("https://elastic.example.com:9200/logs-*/_search");
286
+ });
287
+
288
+ test("throws when the settings have no URL", () => {
289
+ expect(() => {
290
+ return buildElasticsearchSearchUrl(makeSettings({ url: undefined }));
291
+ }).toThrow(/missing a URL/);
292
+ });
293
+ });
294
+
295
+ describe("parseTimeSeriesAggregations", () => {
296
+ test("parses a single date_histogram using the first numeric sub-aggregation value", () => {
297
+ const result: AggregatedResult =
298
+ parseTimeSeriesAggregations(histogramResponse());
299
+
300
+ expect(result.truncated).toBe(false);
301
+ expect(result.data).toHaveLength(2);
302
+ expect(result.data[0]!.timestamp).toEqual(new Date(START_EPOCH_MS));
303
+ expect(result.data[0]!.value).toBe(12.5);
304
+ expect(result.data[1]!.timestamp).toEqual(
305
+ new Date(START_EPOCH_MS + HOUR_MS),
306
+ );
307
+ expect(result.data[1]!.value).toBe(15);
308
+ expect(result.data[0]!["attributes"]).toBeUndefined();
309
+ });
310
+
311
+ test("falls back to doc_count when no sub-aggregation owns a numeric value", () => {
312
+ const body: unknown = {
313
+ aggregations: {
314
+ per_hour: {
315
+ buckets: [
316
+ { key: START_EPOCH_MS, doc_count: 9 },
317
+ { key: START_EPOCH_MS + HOUR_MS, doc_count: 3 },
318
+ ],
319
+ },
320
+ },
321
+ };
322
+
323
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
324
+
325
+ expect(result.data).toHaveLength(2);
326
+ expect(result.data[0]!.value).toBe(9);
327
+ expect(result.data[1]!.value).toBe(3);
328
+ });
329
+
330
+ test("coerces string-number sub-aggregation values", () => {
331
+ const body: unknown = {
332
+ aggregations: {
333
+ per_hour: {
334
+ buckets: [
335
+ { key: START_EPOCH_MS, doc_count: 2, p99: { value: "42.5" } },
336
+ ],
337
+ },
338
+ },
339
+ };
340
+
341
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
342
+
343
+ expect(result.data).toHaveLength(1);
344
+ expect(result.data[0]!.value).toBe(42.5);
345
+ });
346
+
347
+ test("falls back to doc_count when the sub-aggregation value is null", () => {
348
+ const body: unknown = {
349
+ aggregations: {
350
+ per_hour: {
351
+ buckets: [
352
+ { key: START_EPOCH_MS, doc_count: 4, avg_ms: { value: null } },
353
+ ],
354
+ },
355
+ },
356
+ };
357
+
358
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
359
+
360
+ expect(result.data).toHaveLength(1);
361
+ expect(result.data[0]!.value).toBe(4);
362
+ });
363
+
364
+ test("parses timestamps from key_as_string when the numeric key is absent", () => {
365
+ const body: unknown = {
366
+ aggregations: {
367
+ per_hour: {
368
+ buckets: [
369
+ { key_as_string: "2026-01-01T05:00:00.000Z", doc_count: 8 },
370
+ ],
371
+ },
372
+ },
373
+ };
374
+
375
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
376
+
377
+ expect(result.data).toHaveLength(1);
378
+ expect(result.data[0]!.timestamp).toEqual(
379
+ new Date("2026-01-01T05:00:00.000Z"),
380
+ );
381
+ expect(result.data[0]!.value).toBe(8);
382
+ });
383
+
384
+ test("skips malformed bucket entries instead of failing the chart", () => {
385
+ const body: unknown = {
386
+ aggregations: {
387
+ per_hour: {
388
+ buckets: [
389
+ null,
390
+ "garbage",
391
+ { key: START_EPOCH_MS, doc_count: 1 },
392
+ { doc_count: 2 },
393
+ ],
394
+ },
395
+ },
396
+ };
397
+
398
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
399
+
400
+ expect(result.data).toHaveLength(1);
401
+ expect(result.data[0]!.value).toBe(1);
402
+ });
403
+
404
+ test("builds one series per term bucket with attributes keyed by the terms agg name", () => {
405
+ const result: AggregatedResult =
406
+ parseTimeSeriesAggregations(termsResponse());
407
+
408
+ expect(result.truncated).toBe(false);
409
+ expect(result.data).toHaveLength(3);
410
+ expect(result.data[0]!["attributes"]).toEqual({ by_service: "api" });
411
+ expect(result.data[0]!.value).toBe(4);
412
+ expect(result.data[1]!["attributes"]).toEqual({ by_service: "api" });
413
+ expect(result.data[2]!["attributes"]).toEqual({ by_service: "worker" });
414
+ expect(result.data[2]!.value).toBe(6);
415
+ });
416
+
417
+ test("labels term series from key_as_string when present (boolean terms)", () => {
418
+ const body: unknown = {
419
+ aggregations: {
420
+ by_success: {
421
+ buckets: [
422
+ {
423
+ key: 1,
424
+ key_as_string: "true",
425
+ doc_count: 5,
426
+ over_time: {
427
+ buckets: [{ key: START_EPOCH_MS, doc_count: 5 }],
428
+ },
429
+ },
430
+ ],
431
+ },
432
+ },
433
+ };
434
+
435
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
436
+
437
+ expect(result.data).toHaveLength(1);
438
+ expect(result.data[0]!["attributes"]).toEqual({ by_success: "true" });
439
+ });
440
+
441
+ test("stringifies numeric term keys for series labels", () => {
442
+ const body: unknown = {
443
+ aggregations: {
444
+ by_status: {
445
+ buckets: [
446
+ {
447
+ key: 500,
448
+ doc_count: 2,
449
+ over_time: {
450
+ buckets: [{ key: START_EPOCH_MS, doc_count: 2 }],
451
+ },
452
+ },
453
+ ],
454
+ },
455
+ },
456
+ };
457
+
458
+ const result: AggregatedResult = parseTimeSeriesAggregations(body);
459
+
460
+ expect(result.data[0]!["attributes"]).toEqual({ by_status: "500" });
461
+ });
462
+
463
+ test("caps the number of series and flags truncation", () => {
464
+ const body: unknown = {
465
+ aggregations: {
466
+ by_service: {
467
+ buckets: [
468
+ {
469
+ key: "a",
470
+ over_time: { buckets: [{ key: START_EPOCH_MS, doc_count: 1 }] },
471
+ },
472
+ {
473
+ key: "b",
474
+ over_time: { buckets: [{ key: START_EPOCH_MS, doc_count: 2 }] },
475
+ },
476
+ {
477
+ key: "c",
478
+ over_time: { buckets: [{ key: START_EPOCH_MS, doc_count: 3 }] },
479
+ },
480
+ ],
481
+ },
482
+ },
483
+ };
484
+
485
+ const result: AggregatedResult = parseTimeSeriesAggregations(body, {
486
+ maxSeries: 2,
487
+ });
488
+
489
+ expect(result.truncated).toBe(true);
490
+ expect(result.data).toHaveLength(2);
491
+ const labels: Array<string> = result.data.map(
492
+ (point: Record<string, unknown>) => {
493
+ return (point["attributes"] as Record<string, string>)["by_service"]!;
494
+ },
495
+ );
496
+ expect(labels).toEqual(["a", "b"]);
497
+ });
498
+
499
+ test("caps total points across term series and flags truncation", () => {
500
+ const result: AggregatedResult = parseTimeSeriesAggregations(
501
+ termsResponse(),
502
+ { maxPoints: 2 },
503
+ );
504
+
505
+ expect(result.truncated).toBe(true);
506
+ expect(result.data).toHaveLength(2);
507
+ });
508
+
509
+ test("caps points in a single date_histogram and flags truncation", () => {
510
+ const buckets: Array<Record<string, unknown>> = [];
511
+ for (let index: number = 0; index < 5; index++) {
512
+ buckets.push({ key: START_EPOCH_MS + index * HOUR_MS, doc_count: index });
513
+ }
514
+
515
+ const result: AggregatedResult = parseTimeSeriesAggregations(
516
+ { aggregations: { per_hour: { buckets: buckets } } },
517
+ { maxPoints: 3 },
518
+ );
519
+
520
+ expect(result.truncated).toBe(true);
521
+ expect(result.data).toHaveLength(3);
522
+ });
523
+
524
+ test("returns an empty result for an empty bucket list", () => {
525
+ const result: AggregatedResult = parseTimeSeriesAggregations({
526
+ aggregations: { per_hour: { buckets: [] } },
527
+ });
528
+
529
+ expect(result.data).toEqual([]);
530
+ expect(result.truncated).toBe(false);
531
+ });
532
+
533
+ test("throws when the response has no aggregations", () => {
534
+ expect(() => {
535
+ return parseTimeSeriesAggregations({ hits: { hits: [] } });
536
+ }).toThrow(/date_histogram/);
537
+ });
538
+
539
+ test("throws when aggregations contain no bucket aggregation", () => {
540
+ expect(() => {
541
+ return parseTimeSeriesAggregations({
542
+ aggregations: { avg_latency: { value: 42 } },
543
+ });
544
+ }).toThrow(/date_histogram/);
545
+ });
546
+
547
+ test("throws when bucket keys are not timestamps (plain terms agg)", () => {
548
+ expect(() => {
549
+ return parseTimeSeriesAggregations({
550
+ aggregations: {
551
+ by_host: {
552
+ buckets: [
553
+ { key: "host-1", doc_count: 4 },
554
+ { key: "host-2", doc_count: 2 },
555
+ ],
556
+ },
557
+ },
558
+ });
559
+ }).toThrow(/does not look like a date_histogram/);
560
+ });
561
+
562
+ test("throws for a non-object response body", () => {
563
+ expect(() => {
564
+ return parseTimeSeriesAggregations([1, 2, 3]);
565
+ }).toThrow(/unexpected response shape/);
566
+ expect(() => {
567
+ return parseTimeSeriesAggregations("not-json-object");
568
+ }).toThrow(/unexpected response shape/);
569
+ });
570
+ });
571
+
572
+ describe("parseHitsToTable", () => {
573
+ test("builds _id and _index columns plus the union of _source keys", () => {
574
+ const result: DataSourceTableResult = parseHitsToTable(
575
+ hitsResponse(
576
+ [
577
+ makeHit("doc-1", {
578
+ message: "error occurred",
579
+ level: "error",
580
+ status: 500,
581
+ }),
582
+ makeHit("doc-2", { message: "all good", region: "eu-west-1" }),
583
+ ],
584
+ { value: 2, relation: "eq" },
585
+ ),
586
+ );
587
+
588
+ const keys: Array<string> = result.columns.map(
589
+ (column: DataSourceTableColumn) => {
590
+ return column.key;
591
+ },
592
+ );
593
+ expect(keys).toEqual([
594
+ "_id",
595
+ "_index",
596
+ "message",
597
+ "level",
598
+ "status",
599
+ "region",
600
+ ]);
601
+
602
+ expect(result.rows).toHaveLength(2);
603
+ expect(result.rows[0]!["_id"]).toBe("doc-1");
604
+ expect(result.rows[0]!["_index"]).toBe("logs-2026.01.01");
605
+ expect(result.rows[0]!["status"]).toBe(500);
606
+ expect(result.rows[0]!["region"]).toBeNull();
607
+ expect(result.rows[1]!["level"]).toBeNull();
608
+ expect(result.rows[1]!["region"]).toBe("eu-west-1");
609
+ expect(result.truncated).toBe(false);
610
+ });
611
+
612
+ test("sniffs column types from cell values", () => {
613
+ const result: DataSourceTableResult = parseHitsToTable(
614
+ hitsResponse([makeHit("doc-1", { count: 3, ok: true, note: "text" })], {
615
+ value: 1,
616
+ relation: "eq",
617
+ }),
618
+ );
619
+
620
+ const typeByKey: Record<string, DataSourceTableColumnType> = {};
621
+ for (const column of result.columns) {
622
+ typeByKey[column.key] = column.type;
623
+ }
624
+ expect(typeByKey["count"]).toBe(DataSourceTableColumnType.Number);
625
+ expect(typeByKey["ok"]).toBe(DataSourceTableColumnType.Boolean);
626
+ expect(typeByKey["note"]).toBe(DataSourceTableColumnType.Text);
627
+ expect(typeByKey["_id"]).toBe(DataSourceTableColumnType.Text);
628
+ });
629
+
630
+ test("serializes nested _source objects to JSON strings", () => {
631
+ const result: DataSourceTableResult = parseHitsToTable(
632
+ hitsResponse(
633
+ [makeHit("doc-1", { context: { user: "u-1", attempts: 2 } })],
634
+ { value: 1, relation: "eq" },
635
+ ),
636
+ );
637
+
638
+ expect(result.rows[0]!["context"]).toBe('{"user":"u-1","attempts":2}');
639
+ });
640
+
641
+ test("caps the column set at 50 columns", () => {
642
+ const wideSource: Record<string, unknown> = {};
643
+ for (let index: number = 0; index < 60; index++) {
644
+ wideSource[`field_${index}`] = index;
645
+ }
646
+
647
+ const result: DataSourceTableResult = parseHitsToTable(
648
+ hitsResponse([makeHit("doc-1", wideSource)], {
649
+ value: 1,
650
+ relation: "eq",
651
+ }),
652
+ );
653
+
654
+ expect(result.columns).toHaveLength(50);
655
+ const keys: Array<string> = result.columns.map(
656
+ (column: DataSourceTableColumn) => {
657
+ return column.key;
658
+ },
659
+ );
660
+ expect(keys).toContain("_id");
661
+ expect(keys).toContain("_index");
662
+ expect(keys).toContain("field_47");
663
+ expect(keys).not.toContain("field_48");
664
+ });
665
+
666
+ test("handles hits without _source", () => {
667
+ const result: DataSourceTableResult = parseHitsToTable(
668
+ hitsResponse([makeHit("doc-1", undefined)], {
669
+ value: 1,
670
+ relation: "eq",
671
+ }),
672
+ );
673
+
674
+ expect(result.rows).toHaveLength(1);
675
+ expect(result.rows[0]!["_id"]).toBe("doc-1");
676
+ expect(
677
+ result.columns.map((column: DataSourceTableColumn) => {
678
+ return column.key;
679
+ }),
680
+ ).toEqual(["_id", "_index"]);
681
+ });
682
+
683
+ test("returns an empty result for zero hits", () => {
684
+ const result: DataSourceTableResult = parseHitsToTable(
685
+ hitsResponse([], { value: 0, relation: "eq" }),
686
+ );
687
+
688
+ expect(result.columns).toEqual([]);
689
+ expect(result.rows).toEqual([]);
690
+ expect(result.truncated).toBe(false);
691
+ });
692
+
693
+ test("clamps rows beyond maxRows and flags truncation", () => {
694
+ const result: DataSourceTableResult = parseHitsToTable(
695
+ hitsResponse(
696
+ [
697
+ makeHit("doc-1", { n: 1 }),
698
+ makeHit("doc-2", { n: 2 }),
699
+ makeHit("doc-3", { n: 3 }),
700
+ ],
701
+ { value: 3, relation: "eq" },
702
+ ),
703
+ { maxRows: 2 },
704
+ );
705
+
706
+ expect(result.rows).toHaveLength(2);
707
+ expect(result.truncated).toBe(true);
708
+ });
709
+
710
+ test("flags truncation when a full page has more matching documents (ES 7/8 total object)", () => {
711
+ const result: DataSourceTableResult = parseHitsToTable(
712
+ hitsResponse(
713
+ [
714
+ makeHit("doc-1", { n: 1 }),
715
+ makeHit("doc-2", { n: 2 }),
716
+ makeHit("doc-3", { n: 3 }),
717
+ ],
718
+ { value: 10, relation: "gte" },
719
+ ),
720
+ { maxRows: 3 },
721
+ );
722
+
723
+ expect(result.rows).toHaveLength(3);
724
+ expect(result.truncated).toBe(true);
725
+ });
726
+
727
+ test("flags truncation with an ES 6-style numeric total", () => {
728
+ const result: DataSourceTableResult = parseHitsToTable(
729
+ hitsResponse([makeHit("doc-1", { n: 1 })], 10),
730
+ { maxRows: 1 },
731
+ );
732
+
733
+ expect(result.truncated).toBe(true);
734
+ });
735
+
736
+ test("does not flag truncation when the page is not full", () => {
737
+ const result: DataSourceTableResult = parseHitsToTable(
738
+ hitsResponse([makeHit("doc-1", { n: 1 })], {
739
+ value: 10,
740
+ relation: "eq",
741
+ }),
742
+ { maxRows: 5 },
743
+ );
744
+
745
+ expect(result.truncated).toBe(false);
746
+ });
747
+
748
+ test("throws when the response is missing hits.hits", () => {
749
+ expect(() => {
750
+ return parseHitsToTable({ took: 1 });
751
+ }).toThrow(/missing hits\.hits/);
752
+ expect(() => {
753
+ return parseHitsToTable({ hits: { total: 1 } });
754
+ }).toThrow(/missing hits\.hits/);
755
+ });
756
+
757
+ test("throws for a non-object response body", () => {
758
+ expect(() => {
759
+ return parseHitsToTable("nope");
760
+ }).toThrow(/unexpected response shape/);
761
+ });
762
+
763
+ test("skips malformed hit entries", () => {
764
+ const result: DataSourceTableResult = parseHitsToTable(
765
+ hitsResponse(
766
+ [null as unknown as Record<string, unknown>, makeHit("doc-1", {})],
767
+ { value: 2, relation: "eq" },
768
+ ),
769
+ );
770
+
771
+ expect(result.rows).toHaveLength(1);
772
+ expect(result.rows[0]!["_id"]).toBe("doc-1");
773
+ });
774
+ });
775
+
776
+ describe("ElasticsearchConnector.testConnection", () => {
777
+ test("passes on a 2xx JSON cluster banner and sends auth + connect timeout", async () => {
778
+ const guardSpy: jest.SpyInstance = mockGuard();
779
+ const fetchSpy: jest.SpyInstance = mockFetch(
780
+ jsonResponse({ cluster_name: "prod", version: { number: "8.13.0" } }),
781
+ );
782
+
783
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
784
+ await connector.testConnection(makeSettings());
785
+
786
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
787
+ const request: DataSourceHttpRequest = getRequest(fetchSpy);
788
+ expect(request.method).toBe("GET");
789
+ expect(request.url).toBe("https://elastic.example.com:9200/");
790
+ expect(request.timeoutInMs).toBe(DATA_SOURCE_CONNECT_TIMEOUT_IN_MS);
791
+
792
+ const expectedBasic: string = `Basic ${Buffer.from(
793
+ `readonly_user:${SECRET_PASSWORD}`,
794
+ ).toString("base64")}`;
795
+ expect(request.headers!["Authorization"]).toBe(expectedBasic);
796
+
797
+ // The egress guard must have validated the host before the fetch.
798
+ expect(guardSpy).toHaveBeenCalledWith("elastic.example.com", undefined);
799
+ expect(guardSpy.mock.invocationCallOrder[0]!).toBeLessThan(
800
+ fetchSpy.mock.invocationCallOrder[0]!,
801
+ );
802
+ });
803
+
804
+ test("sends a Bearer token when an API token is configured", async () => {
805
+ mockGuard();
806
+ const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({ ok: true }));
807
+
808
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
809
+ await connector.testConnection(
810
+ makeSettings({
811
+ username: undefined,
812
+ password: undefined,
813
+ apiToken: SECRET_TOKEN,
814
+ }),
815
+ );
816
+
817
+ expect(getRequest(fetchSpy).headers!["Authorization"]).toBe(
818
+ `Bearer ${SECRET_TOKEN}`,
819
+ );
820
+ });
821
+
822
+ test("lets a custom Authorization header win (ApiKey scheme)", async () => {
823
+ mockGuard();
824
+ const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({ ok: true }));
825
+
826
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
827
+ await connector.testConnection(
828
+ makeSettings({
829
+ customHeaders: { Authorization: "ApiKey encoded-key==" },
830
+ }),
831
+ );
832
+
833
+ expect(getRequest(fetchSpy).headers!["Authorization"]).toBe(
834
+ "ApiKey encoded-key==",
835
+ );
836
+ });
837
+
838
+ test("surfaces HTTP 401 as a clear credential failure without leaking secrets", async () => {
839
+ mockGuard();
840
+ jest
841
+ .spyOn(DataSourceHttpFetch, "fetch")
842
+ .mockRejectedValue(
843
+ new BadDataException(
844
+ "Data source responded with HTTP 401: security_exception missing authentication credentials",
845
+ ),
846
+ );
847
+
848
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
849
+ const error: Error = await connector
850
+ .testConnection(makeSettings())
851
+ .then(() => {
852
+ return new Error("expected rejection");
853
+ })
854
+ .catch((caught: Error) => {
855
+ return caught;
856
+ });
857
+
858
+ expect(error).toBeInstanceOf(BadDataException);
859
+ expect(error.message).toContain("401");
860
+ expect(error.message).toContain("credentials");
861
+ expect(error.message).not.toContain(SECRET_PASSWORD);
862
+ });
863
+
864
+ test("surfaces HTTP 403 as a permission failure", async () => {
865
+ mockGuard();
866
+ jest
867
+ .spyOn(DataSourceHttpFetch, "fetch")
868
+ .mockRejectedValue(
869
+ new BadDataException("Data source responded with HTTP 403: forbidden"),
870
+ );
871
+
872
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
873
+ await expect(connector.testConnection(makeSettings())).rejects.toThrow(
874
+ /HTTP 403 Forbidden/,
875
+ );
876
+ });
877
+
878
+ test("rejects a 2xx response that is not JSON", async () => {
879
+ mockGuard();
880
+ mockFetch({
881
+ statusCode: 200,
882
+ bodyText: "<html>login page</html>",
883
+ bodyJson: undefined,
884
+ });
885
+
886
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
887
+ await expect(connector.testConnection(makeSettings())).rejects.toThrow(
888
+ /did not return the JSON cluster banner/,
889
+ );
890
+ });
891
+
892
+ test("throws before fetching when no URL is configured", async () => {
893
+ mockGuard();
894
+ const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({ ok: true }));
895
+
896
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
897
+ await expect(
898
+ connector.testConnection(makeSettings({ url: undefined })),
899
+ ).rejects.toThrow(/missing a URL/);
900
+ expect(fetchSpy).not.toHaveBeenCalled();
901
+ });
902
+
903
+ test("does not fetch when the egress guard blocks the host", async () => {
904
+ jest
905
+ .spyOn(DataSourceEgressGuard, "assertHostnameAllowed")
906
+ .mockRejectedValue(
907
+ new BadDataException(
908
+ "Data source host elastic.example.com resolves to 127.0.0.1, which is not allowed: loopback address.",
909
+ ),
910
+ );
911
+ const fetchSpy: jest.SpyInstance = jest.spyOn(DataSourceHttpFetch, "fetch");
912
+
913
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
914
+ await expect(connector.testConnection(makeSettings())).rejects.toThrow(
915
+ /not allowed/,
916
+ );
917
+ expect(fetchSpy).not.toHaveBeenCalled();
918
+ });
919
+ });
920
+
921
+ describe("ElasticsearchConnector.queryTimeSeries", () => {
922
+ test("POSTs the macro-substituted body to the encoded index with size forced to 0", async () => {
923
+ const guardSpy: jest.SpyInstance = mockGuard();
924
+ const fetchSpy: jest.SpyInstance = mockFetch(
925
+ jsonResponse(histogramResponse()),
926
+ );
927
+
928
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
929
+ const query: string =
930
+ '{ "size": 500, "query": { "range": { "@timestamp": { "gte": "$__startTime", "lte": "$__endTime" } } }, "aggs": { "over_time": { "date_histogram": { "field": "@timestamp", "fixed_interval": "1h" } } } }';
931
+
932
+ const result: AggregatedResult = await connector.queryTimeSeries(
933
+ makeSettings({
934
+ additionalOptions: { elasticsearchIndex: "logs-*" },
935
+ }),
936
+ query,
937
+ makeWindow(),
938
+ );
939
+
940
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
941
+ const request: DataSourceHttpRequest = getRequest(fetchSpy);
942
+ expect(request.method).toBe("POST");
943
+ expect(request.url).toBe("https://elastic.example.com:9200/logs-*/_search");
944
+ expect(request.timeoutInMs).toBe(DATA_SOURCE_QUERY_TIMEOUT_IN_MS);
945
+
946
+ const body: Record<string, unknown> = getRequestBody(fetchSpy);
947
+ expect(body["size"]).toBe(0);
948
+ const queryClause: Record<string, unknown> = body["query"] as Record<
949
+ string,
950
+ unknown
951
+ >;
952
+ const range: Record<string, unknown> = queryClause["range"] as Record<
953
+ string,
954
+ unknown
955
+ >;
956
+ const timestampFilter: Record<string, unknown> = range[
957
+ "@timestamp"
958
+ ] as Record<string, unknown>;
959
+ expect(timestampFilter["gte"]).toBe("2026-01-01T00:00:00.000Z");
960
+ expect(timestampFilter["lte"]).toBe("2026-01-02T00:00:00.000Z");
961
+
962
+ expect(guardSpy.mock.invocationCallOrder[0]!).toBeLessThan(
963
+ fetchSpy.mock.invocationCallOrder[0]!,
964
+ );
965
+
966
+ expect(result.data).toHaveLength(2);
967
+ expect(result.data[0]!.value).toBe(12.5);
968
+ });
969
+
970
+ test("substitutes epoch-ms macros as JSON numbers", async () => {
971
+ mockGuard();
972
+ const fetchSpy: jest.SpyInstance = mockFetch(
973
+ jsonResponse(histogramResponse()),
974
+ );
975
+
976
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
977
+ await connector.queryTimeSeries(
978
+ makeSettings(),
979
+ '{ "query": { "range": { "ts": { "gte": $__startTimeMs, "lte": $__endTimeMs } } }, "aggs": { "h": { "date_histogram": { "field": "ts", "fixed_interval": "1h" } } } }',
980
+ makeWindow(),
981
+ );
982
+
983
+ const body: Record<string, unknown> = getRequestBody(fetchSpy);
984
+ const queryClause: Record<string, unknown> = body["query"] as Record<
985
+ string,
986
+ unknown
987
+ >;
988
+ const range: Record<string, unknown> = queryClause["range"] as Record<
989
+ string,
990
+ unknown
991
+ >;
992
+ const timestampFilter: Record<string, unknown> = range["ts"] as Record<
993
+ string,
994
+ unknown
995
+ >;
996
+ expect(timestampFilter["gte"]).toBe(START_EPOCH_MS);
997
+ expect(timestampFilter["lte"]).toBe(END_EPOCH_MS);
998
+ });
999
+
1000
+ test("throws for invalid query JSON without calling the network", async () => {
1001
+ mockGuard();
1002
+ const fetchSpy: jest.SpyInstance = mockFetch(
1003
+ jsonResponse(histogramResponse()),
1004
+ );
1005
+
1006
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1007
+ await expect(
1008
+ connector.queryTimeSeries(makeSettings(), "{ nope", makeWindow()),
1009
+ ).rejects.toThrow(/not valid JSON/);
1010
+ expect(fetchSpy).not.toHaveBeenCalled();
1011
+ });
1012
+
1013
+ test("scrubs secrets from transport errors", async () => {
1014
+ mockGuard();
1015
+ jest
1016
+ .spyOn(DataSourceHttpFetch, "fetch")
1017
+ .mockRejectedValue(
1018
+ new Error(`connect failed: password ${SECRET_PASSWORD} rejected`),
1019
+ );
1020
+
1021
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1022
+ const error: Error = await connector
1023
+ .queryTimeSeries(makeSettings(), HISTOGRAM_QUERY, makeWindow())
1024
+ .then(() => {
1025
+ return new Error("expected rejection");
1026
+ })
1027
+ .catch((caught: Error) => {
1028
+ return caught;
1029
+ });
1030
+
1031
+ expect(error).toBeInstanceOf(BadDataException);
1032
+ expect(error.message).not.toContain(SECRET_PASSWORD);
1033
+ expect(error.message).toContain("***");
1034
+ });
1035
+
1036
+ test("surfaces the server's error body for failed queries without secrets", async () => {
1037
+ mockGuard();
1038
+ jest
1039
+ .spyOn(DataSourceHttpFetch, "fetch")
1040
+ .mockRejectedValue(
1041
+ new BadDataException(
1042
+ "Data source responded with HTTP 400: parsing_exception at line 1",
1043
+ ),
1044
+ );
1045
+
1046
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1047
+ const error: Error = await connector
1048
+ .queryTimeSeries(makeSettings(), HISTOGRAM_QUERY, makeWindow())
1049
+ .then(() => {
1050
+ return new Error("expected rejection");
1051
+ })
1052
+ .catch((caught: Error) => {
1053
+ return caught;
1054
+ });
1055
+
1056
+ expect(error).toBeInstanceOf(BadDataException);
1057
+ expect(error.message).toContain("parsing_exception");
1058
+ expect(error.message).not.toContain(SECRET_PASSWORD);
1059
+ });
1060
+
1061
+ test("rejects a non-JSON search response", async () => {
1062
+ mockGuard();
1063
+ mockFetch({
1064
+ statusCode: 200,
1065
+ bodyText: "<html>proxy error</html>",
1066
+ bodyJson: undefined,
1067
+ });
1068
+
1069
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1070
+ await expect(
1071
+ connector.queryTimeSeries(makeSettings(), HISTOGRAM_QUERY, makeWindow()),
1072
+ ).rejects.toThrow(/non-JSON response/);
1073
+ });
1074
+
1075
+ test("uses the default * index when none is configured", async () => {
1076
+ mockGuard();
1077
+ const fetchSpy: jest.SpyInstance = mockFetch(
1078
+ jsonResponse(histogramResponse()),
1079
+ );
1080
+
1081
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1082
+ await connector.queryTimeSeries(
1083
+ makeSettings(),
1084
+ HISTOGRAM_QUERY,
1085
+ makeWindow(),
1086
+ );
1087
+
1088
+ expect(getRequest(fetchSpy).url).toBe(
1089
+ "https://elastic.example.com:9200/*/_search",
1090
+ );
1091
+ });
1092
+ });
1093
+
1094
+ describe("ElasticsearchConnector.queryTable", () => {
1095
+ const MATCH_ALL_QUERY: string = '{ "query": { "match_all": {} } }';
1096
+
1097
+ test("defaults size to the table row cap and parses hit rows", async () => {
1098
+ const guardSpy: jest.SpyInstance = mockGuard();
1099
+ const fetchSpy: jest.SpyInstance = mockFetch(
1100
+ jsonResponse(
1101
+ hitsResponse([makeHit("doc-1", { message: "hello", status: 200 })], {
1102
+ value: 1,
1103
+ relation: "eq",
1104
+ }),
1105
+ ),
1106
+ );
1107
+
1108
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1109
+ const result: DataSourceTableResult = await connector.queryTable(
1110
+ makeSettings(),
1111
+ MATCH_ALL_QUERY,
1112
+ makeWindow(),
1113
+ );
1114
+
1115
+ const body: Record<string, unknown> = getRequestBody(fetchSpy);
1116
+ expect(body["size"]).toBe(DATA_SOURCE_MAX_TABLE_ROWS);
1117
+ expect(getRequest(fetchSpy).timeoutInMs).toBe(
1118
+ DATA_SOURCE_QUERY_TIMEOUT_IN_MS,
1119
+ );
1120
+
1121
+ expect(guardSpy.mock.invocationCallOrder[0]!).toBeLessThan(
1122
+ fetchSpy.mock.invocationCallOrder[0]!,
1123
+ );
1124
+
1125
+ expect(result.rows).toHaveLength(1);
1126
+ expect(result.rows[0]!["message"]).toBe("hello");
1127
+ expect(result.rows[0]!["status"]).toBe(200);
1128
+ });
1129
+
1130
+ test("keeps a smaller user-provided size", async () => {
1131
+ mockGuard();
1132
+ const fetchSpy: jest.SpyInstance = mockFetch(
1133
+ jsonResponse(hitsResponse([], { value: 0, relation: "eq" })),
1134
+ );
1135
+
1136
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1137
+ await connector.queryTable(
1138
+ makeSettings(),
1139
+ '{ "size": 5, "query": { "match_all": {} } }',
1140
+ makeWindow(),
1141
+ );
1142
+
1143
+ expect(getRequestBody(fetchSpy)["size"]).toBe(5);
1144
+ });
1145
+
1146
+ test("clamps an oversized user-provided size to the cap", async () => {
1147
+ mockGuard();
1148
+ const fetchSpy: jest.SpyInstance = mockFetch(
1149
+ jsonResponse(hitsResponse([], { value: 0, relation: "eq" })),
1150
+ );
1151
+
1152
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1153
+ await connector.queryTable(
1154
+ makeSettings(),
1155
+ '{ "size": 999999, "query": { "match_all": {} } }',
1156
+ makeWindow(),
1157
+ );
1158
+
1159
+ expect(getRequestBody(fetchSpy)["size"]).toBe(DATA_SOURCE_MAX_TABLE_ROWS);
1160
+ });
1161
+
1162
+ test("replaces an invalid size value with the cap", async () => {
1163
+ mockGuard();
1164
+ const fetchSpy: jest.SpyInstance = mockFetch(
1165
+ jsonResponse(hitsResponse([], { value: 0, relation: "eq" })),
1166
+ );
1167
+
1168
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1169
+ await connector.queryTable(
1170
+ makeSettings(),
1171
+ '{ "size": -3, "query": { "match_all": {} } }',
1172
+ makeWindow(),
1173
+ );
1174
+
1175
+ expect(getRequestBody(fetchSpy)["size"]).toBe(DATA_SOURCE_MAX_TABLE_ROWS);
1176
+ });
1177
+
1178
+ test("propagates a missing hits envelope as a BadDataException", async () => {
1179
+ mockGuard();
1180
+ mockFetch(jsonResponse({ took: 2 }));
1181
+
1182
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1183
+ await expect(
1184
+ connector.queryTable(makeSettings(), MATCH_ALL_QUERY, makeWindow()),
1185
+ ).rejects.toThrow(/missing hits\.hits/);
1186
+ });
1187
+
1188
+ test("rejects a non-JSON response", async () => {
1189
+ mockGuard();
1190
+ mockFetch({
1191
+ statusCode: 200,
1192
+ bodyText: "plain text",
1193
+ bodyJson: undefined,
1194
+ });
1195
+
1196
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1197
+ await expect(
1198
+ connector.queryTable(makeSettings(), MATCH_ALL_QUERY, makeWindow()),
1199
+ ).rejects.toThrow(/non-JSON response/);
1200
+ });
1201
+
1202
+ test("exposes the connector type for the registry", () => {
1203
+ const connector: ElasticsearchConnector = new ElasticsearchConnector();
1204
+ expect(connector.dataSourceType).toBe(DataSourceType.Elasticsearch);
1205
+ });
1206
+ });