@skyhook-io/radar-app 1.13.4 → 1.14.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 (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,393 @@
1
+ import { displayKind, type IssueRecentChange } from "@skyhook-io/k8s-ui";
2
+ import type { ChartAnnotation } from "@skyhook-io/k8s-ui/components/charts";
3
+ import { apiVersionToGroup } from "../../utils/navigation";
4
+ import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
5
+ import type {
6
+ InvestigationEvidenceGroup,
7
+ InvestigationEvidenceObservation,
8
+ InvestigationMetricsEvidence,
9
+ InvestigationMetricsSelector,
10
+ } from "./investigationEvidence";
11
+
12
+ function metricUnitSuffix(metric: string): "bytes" | "seconds" | undefined {
13
+ // _sum is the sum of the observed values, so it measures what the histogram
14
+ // measures. _bucket and _count are counts of observations: only the `le`
15
+ // boundary carries the unit, and nothing here reads it, so a bucket series
16
+ // has no unit this can print.
17
+ if (metric.endsWith("_bucket") || metric.endsWith("_count")) return undefined;
18
+ let base = metric;
19
+ for (const suffix of ["_total", "_sum"]) {
20
+ if (base.endsWith(suffix)) base = base.slice(0, -suffix.length);
21
+ }
22
+ if (base.endsWith("_bytes")) return "bytes";
23
+ if (base.endsWith("_seconds")) return "seconds";
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * PromQL an axis label can survive. Anything outside this list leaves the axis
29
+ * unitless, because a unit is a claim about what the numbers mean and only the
30
+ * expression can support it.
31
+ *
32
+ * Aggregation operators and the `_over_time` functions that return one of the
33
+ * samples keep the metric's unit; so does the change in a counter. Everything
34
+ * else either produces a different quantity (a count, a ratio, a variance, a
35
+ * boolean) or is something this has not reasoned about, and both are better
36
+ * unlabelled than labelled wrong.
37
+ */
38
+ const UNIT_PRESERVING_FUNCTIONS = new Set([
39
+ "sum",
40
+ "min",
41
+ "max",
42
+ "avg",
43
+ "by",
44
+ "without",
45
+ "abs",
46
+ "ceil",
47
+ "floor",
48
+ "round",
49
+ "clamp",
50
+ "clamp_max",
51
+ "clamp_min",
52
+ "label_replace",
53
+ "label_join",
54
+ "topk",
55
+ "bottomk",
56
+ "sort",
57
+ "sort_desc",
58
+ "sort_by_label",
59
+ "sort_by_label_desc",
60
+ "quantile",
61
+ "min_over_time",
62
+ "max_over_time",
63
+ "avg_over_time",
64
+ "sum_over_time",
65
+ "last_over_time",
66
+ "quantile_over_time",
67
+ "increase",
68
+ "delta",
69
+ "idelta",
70
+ ]);
71
+
72
+ /** The functions that divide a quantity by time. */
73
+ const RATE_FUNCTIONS = new Set(["rate", "irate", "deriv"]);
74
+
75
+ /**
76
+ * Blanks string literals and comments in one left-to-right pass, so neither
77
+ * can be read as the other. Scanning for comments first lets a `#` inside a
78
+ * label value swallow the rest of the query, which once left `a_bytes{note="#"}
79
+ * > bool 0` looking like a plain metric and gave a dimensionless 0/1 a bytes
80
+ * axis. PromQL strings come in three quotes; only the first two take
81
+ * backslash escapes.
82
+ */
83
+ function withoutStringsAndComments(query: string): string {
84
+ let out = "";
85
+ let quote: string | undefined;
86
+ for (let index = 0; index < query.length; index += 1) {
87
+ const char = query[index];
88
+ if (quote) {
89
+ if (char === "\\" && quote !== "`") {
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (char === quote) quote = undefined;
94
+ continue;
95
+ }
96
+ if (char === '"' || char === "'" || char === "`") {
97
+ quote = char;
98
+ out += '""';
99
+ continue;
100
+ }
101
+ if (char === "#") {
102
+ while (index < query.length && query[index] !== "\n") index += 1;
103
+ out += " ";
104
+ continue;
105
+ }
106
+ out += char;
107
+ }
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * The unit is what every metric in the expression states through its name, as
113
+ * the producer's selector inventory lists them, and it only survives the
114
+ * operations that leave that meaning intact.
115
+ *
116
+ * `sum(x_bytes)` is bytes. `rate(x_bytes[5m])` is bytes per second, and a rate
117
+ * of seconds is a ratio with no unit worth printing. Everything else is barred
118
+ * rather than guessed: arithmetic and comparisons produce a quantity the
119
+ * metric's name no longer describes (`x_bytes > bool 0` is a 0 or a 1,
120
+ * `stdvar(x_bytes)` is bytes squared), a set operator mixes two expressions
121
+ * whose units may differ, `atan2` returns an angle, a second rate divides by
122
+ * time twice, and an unlisted function is simply unknown. Strings, comments, label matchers and
123
+ * range selectors are blanked before the expression is read, so nothing
124
+ * inside them can be mistaken for an operator or a call.
125
+ */
126
+ export function metricsUnitForExpression(
127
+ query: string,
128
+ selectors: readonly InvestigationMetricsSelector[],
129
+ ): string {
130
+ if (selectors.length === 0) return "";
131
+ const units = new Set(
132
+ selectors.map((selector) => metricUnitSuffix(selector.metric)),
133
+ );
134
+ if (units.size !== 1) return "";
135
+ const [unit] = units;
136
+ if (!unit) return "";
137
+ const expression = withoutStringsAndComments(query)
138
+ .replace(/\{[^{}]*\}/g, "")
139
+ .replace(/\[[^\]]*\]/g, "");
140
+ // Arithmetic, comparison and the set operators all produce something the
141
+ // metric's name no longer describes, or mix two things that disagree.
142
+ if (
143
+ /[-+/*%^]|[<>!=]=|[<>]|\b(?:bool|and|or|unless|atan2)\b/.test(expression)
144
+ ) {
145
+ return "";
146
+ }
147
+ // An aggregation may put its grouping clause before the parenthesis, as in
148
+ // `count by (pod) (...)`, so the name is not always adjacent to it.
149
+ const functions = [
150
+ ...expression.matchAll(
151
+ /([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:(?:by|without)\s*\([^()]*\)\s*)?\(/g,
152
+ ),
153
+ ]
154
+ .map((match) => match[1])
155
+ .filter((name) => !selectors.some((selector) => selector.metric === name));
156
+ const rates = functions.filter((name) => RATE_FUNCTIONS.has(name)).length;
157
+ if (rates > 1) return "";
158
+ for (const name of functions) {
159
+ if (RATE_FUNCTIONS.has(name)) continue;
160
+ if (!UNIT_PRESERVING_FUNCTIONS.has(name)) return "";
161
+ }
162
+ if (rates === 1) return unit === "bytes" ? "bytes/s" : "";
163
+ return unit;
164
+ }
165
+
166
+ export function metricsDomain(
167
+ data: Pick<InvestigationMetricsEvidence, "mode" | "start" | "end">,
168
+ ): { start: number; end: number } | undefined {
169
+ if (data.mode !== "range" || !data.start || !data.end) return undefined;
170
+ const start = Date.parse(data.start) / 1000;
171
+ const end = Date.parse(data.end) / 1000;
172
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
173
+ return undefined;
174
+ return { start, end };
175
+ }
176
+
177
+ function refKey(ref: {
178
+ kind: string;
179
+ group?: string;
180
+ namespace?: string;
181
+ name: string;
182
+ }): string {
183
+ return [
184
+ ref.kind.toLowerCase(),
185
+ (ref.group ?? "").toLowerCase(),
186
+ ref.namespace ?? "",
187
+ ref.name,
188
+ ].join("/");
189
+ }
190
+
191
+ function looseKey(ref: {
192
+ kind: string;
193
+ namespace?: string;
194
+ name: string;
195
+ }): string {
196
+ return [ref.kind.toLowerCase(), ref.namespace ?? "", ref.name].join("/");
197
+ }
198
+
199
+ /**
200
+ * A change names its resource exactly when it carries an apiVersion. Helm
201
+ * history rows and older stores omit it, so those match on kind, namespace
202
+ * and name alone, which is unambiguous only against the subject and its
203
+ * producer-established relatives.
204
+ */
205
+ function changeMatchesRelative(
206
+ change: IssueRecentChange,
207
+ strictKeys: ReadonlySet<string>,
208
+ looseKeys: ReadonlySet<string>,
209
+ ): boolean {
210
+ if (change.apiVersion) {
211
+ return strictKeys.has(
212
+ refKey({
213
+ kind: change.kind,
214
+ group: apiVersionToGroup(change.apiVersion),
215
+ namespace: change.namespace,
216
+ name: change.name,
217
+ }),
218
+ );
219
+ }
220
+ return looseKeys.has(looseKey(change));
221
+ }
222
+
223
+ function sameResource(
224
+ left: DiagnosisResourceRef,
225
+ right: DiagnosisResourceRef,
226
+ ): boolean {
227
+ return refKey(left) === refKey(right);
228
+ }
229
+
230
+ /**
231
+ * The resources a change may be recorded against and still count as a change
232
+ * to the chart's subject: the subject itself and the relatives producers named
233
+ * in the same turn (the configuration it consumes, and the pods and ReplicaSets
234
+ * diagnosis rows identified). Name prefixes never qualify.
235
+ */
236
+ function producerRelatives(
237
+ groups: readonly InvestigationEvidenceGroup[],
238
+ subject: DiagnosisResourceRef,
239
+ turnIndex: number,
240
+ ): DiagnosisResourceRef[] {
241
+ const relatives: DiagnosisResourceRef[] = [subject];
242
+ for (const group of groups) {
243
+ for (const observation of group.observations) {
244
+ if (
245
+ observation.source.turnIndex !== turnIndex ||
246
+ observation.relevance === "broader"
247
+ ) {
248
+ continue;
249
+ }
250
+ const { data } = observation;
251
+ switch (data.type) {
252
+ case "resource": {
253
+ const ref: DiagnosisResourceRef = {
254
+ kind: data.resource.kind,
255
+ group: apiVersionToGroup(data.resource.apiVersion) || undefined,
256
+ namespace: data.resource.metadata.namespace,
257
+ name: data.resource.metadata.name,
258
+ };
259
+ if (!sameResource(ref, subject)) break;
260
+ const uses = data.resourceContext?.uses;
261
+ relatives.push(...(uses?.configMaps ?? []), ...(uses?.secrets ?? []));
262
+ if (data.resourceContext?.owner) {
263
+ relatives.push(data.resourceContext.owner);
264
+ }
265
+ break;
266
+ }
267
+ case "startup":
268
+ if (data.subject) relatives.push(data.subject);
269
+ break;
270
+ case "logs":
271
+ if (data.namespace) {
272
+ relatives.push({
273
+ kind: "Pod",
274
+ namespace: data.namespace,
275
+ name: data.pod,
276
+ });
277
+ }
278
+ break;
279
+ case "crash":
280
+ if (data.namespace) {
281
+ for (const pod of data.crash.pods) {
282
+ relatives.push({
283
+ kind: "Pod",
284
+ namespace: data.namespace,
285
+ name: pod,
286
+ });
287
+ }
288
+ }
289
+ break;
290
+ default:
291
+ break;
292
+ }
293
+ }
294
+ }
295
+ return relatives;
296
+ }
297
+
298
+ /**
299
+ * Changes Radar recorded for the chart's subject inside the chart window,
300
+ * paired with whether that lookup was possible at all. An empty marker list
301
+ * means "none were recorded" only when `checked` is true; otherwise the chart
302
+ * had no subject to look changes up for, or the turn captured no changes to
303
+ * look through, and the chart must not claim a clean window.
304
+ */
305
+ export type MetricsChangeCoverage = {
306
+ markers: ChartAnnotation[];
307
+ /** False when no change lookup was possible, so an empty list proves nothing. */
308
+ checked: boolean;
309
+ };
310
+
311
+ export function metricsChangeCoverage(
312
+ groups: readonly InvestigationEvidenceGroup[],
313
+ observation: InvestigationEvidenceObservation,
314
+ ): MetricsChangeCoverage {
315
+ const { data } = observation;
316
+ // A broader chart's subject is a neighbour; the turn's non-broader relatives
317
+ // and changes all belong to the target, so marking them there would
318
+ // attribute the target's changes to the neighbour.
319
+ if (
320
+ data.type !== "metrics" ||
321
+ !data.subject ||
322
+ observation.relevance === "broader"
323
+ ) {
324
+ return { markers: [], checked: false };
325
+ }
326
+ const domain = metricsDomain(data);
327
+ if (!domain) return { markers: [], checked: false };
328
+ const turnIndex = observation.source.turnIndex;
329
+ const relatives = producerRelatives(groups, data.subject, turnIndex);
330
+ const strictKeys = new Set(relatives.map(refKey));
331
+ const looseKeys = new Set(relatives.map(looseKey));
332
+ const seen = new Set<string>();
333
+ const markers: ChartAnnotation[] = [];
334
+ let checked = false;
335
+ for (const group of groups) {
336
+ for (const candidate of group.observations) {
337
+ if (
338
+ candidate.source.turnIndex !== turnIndex ||
339
+ candidate.relevance === "broader"
340
+ ) {
341
+ continue;
342
+ }
343
+ // An authoritative empty change window is filed as a receipt, and counts
344
+ // as a check just as a change list does. A window the producer could not
345
+ // vouch for becomes a limitation instead, never a receipt, so it leaves
346
+ // `checked` false.
347
+ if (
348
+ candidate.data.type === "receipt" &&
349
+ candidate.data.checked === "changes"
350
+ ) {
351
+ checked = true;
352
+ continue;
353
+ }
354
+ if (candidate.data.type !== "changes") continue;
355
+ checked = true;
356
+ for (const change of candidate.data.changes) {
357
+ const timestamp = Date.parse(change.timestamp) / 1000;
358
+ if (
359
+ !Number.isFinite(timestamp) ||
360
+ timestamp < domain.start ||
361
+ timestamp > domain.end ||
362
+ !changeMatchesRelative(change, strictKeys, looseKeys)
363
+ ) {
364
+ continue;
365
+ }
366
+ const identity = `${change.apiVersion ?? ""}|${change.kind}|${change.namespace ?? ""}|${change.name}|${change.timestamp}`;
367
+ if (seen.has(identity)) continue;
368
+ seen.add(identity);
369
+ markers.push({
370
+ timestamp,
371
+ label: `${displayKind(change.kind)} ${change.name}`,
372
+ kind: "change",
373
+ });
374
+ }
375
+ }
376
+ }
377
+ return {
378
+ markers: markers.sort((left, right) => left.timestamp - right.timestamp),
379
+ checked,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Changes Radar recorded for the chart's subject inside the chart window.
385
+ * Callers that need to tell "none recorded" from "not looked up" want
386
+ * {@link metricsChangeCoverage} instead.
387
+ */
388
+ export function metricsChangeMarkers(
389
+ groups: readonly InvestigationEvidenceGroup[],
390
+ observation: InvestigationEvidenceObservation,
391
+ ): ChartAnnotation[] {
392
+ return metricsChangeCoverage(groups, observation).markers;
393
+ }
@@ -33,6 +33,8 @@ export function evidenceSourceExcerpt(
33
33
  return data.release.healthIssue;
34
34
  case "permissions":
35
35
  return data.accessCheck?.reason;
36
+ case "metrics":
37
+ return data.query;
36
38
  default:
37
39
  return undefined;
38
40
  }