@checkstack/anomaly-frontend 0.5.17 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # @checkstack/anomaly-frontend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5236e41: Scope anomaly rows by (check, environment), completing the deferred follow-up
8
+ from the per-environment work in #375 (which env-scoped only baselines).
9
+
10
+ Previously the `anomalies` table was cross-environment: the inline spike
11
+ detector and the drift evaluator located and created the open row by
12
+ `(systemId, configurationId, fieldPath, kind)` with no environment predicate.
13
+ When a `(system, configuration)` assignment fanned out to multiple environments,
14
+ a healthy value in environment A shared one row with an anomaly in environment B,
15
+ so one env could mask (or merge with) another.
16
+
17
+ - **Schema.** New nullable `anomalies.environment_id` column (migration
18
+ `0007_uneven_trauma.sql`, a single `ADD COLUMN`). No unique constraint is
19
+ added: the table intentionally allows multiple rows per identity tuple (a
20
+ `recovered` historical row plus a fresh active row), so uniqueness would break
21
+ the state machine.
22
+ - **Detection.** The spike detector (from the `checkCompleted` hook) and the
23
+ drift evaluator (from the analyzer's per-environment loop) now locate/create
24
+ the open row by `(systemId, configurationId, environmentId, fieldPath, kind)`,
25
+ matching `environment_id = <id>` when present or `IS NULL` for the env-less
26
+ slice - mirroring the per-environment baseline lookup.
27
+ - **Reads.** `getAnomalies` gains an optional `environmentId` tristate filter
28
+ (`undefined` = all envs, `null` = env-less slice, string = that env), and both
29
+ `AnomalyDto` and `getActiveSignalAnomalies` surface `environmentId`. The
30
+ system-detail widget renders an environment pill on env-scoped anomaly rows.
31
+ - **Notifications.** An env-scoped anomaly appends its environment id to the
32
+ collapse key, so two failing environments render as two independent cards
33
+ instead of collapsing into one. The env-less slice keeps the pre-feature
34
+ two-segment key. Mutes stay env-agnostic (per system / per field).
35
+
36
+ BREAKING (semantics, not types; BETA so minor only):
37
+
38
+ - **Anomaly row identity now includes `environmentId`.** For a fanned-out check,
39
+ an anomaly in one environment is a distinct row from another environment. Any
40
+ code that assumed a single anomaly row per `(system, config, field, kind)`
41
+ must account for the environment dimension.
42
+ - **`AnomalyDto` and `getActiveSignalAnomalies` rows carry a new
43
+ `environmentId: string | null` field**, and `getAnomalies` accepts a new
44
+ optional `environmentId` filter. Additive on the wire; consumers that reject
45
+ unknown fields should be updated.
46
+ - **Upgrade behaviour.** Existing rows backfill to `null` (the env-less slice)
47
+ and stay until they recover; the next detection tick opens fresh
48
+ per-environment rows for fanned-out checks. This mirrors how #375 handled
49
+ baselines.
50
+
51
+ State and scale: the anomaly state lives entirely in the shared `anomalies`
52
+ Postgres table. `environmentId` is just another column on the row, so every pod
53
+ reads the same per-`(system, config, env, field, kind)` state - no pod-local
54
+ state, and reads return the same answer on every pod. The baseline cache key
55
+ already carries the env segment (#375), so there is no cross-env cache shadowing.
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [52c55bf]
60
+ - Updated dependencies [5236e41]
61
+ - Updated dependencies [d1b71b6]
62
+ - Updated dependencies [0d912a3]
63
+ - Updated dependencies [a07b375]
64
+ - Updated dependencies [d9f4654]
65
+ - Updated dependencies [b45be8e]
66
+ - Updated dependencies [d9f4654]
67
+ - Updated dependencies [3420d24]
68
+ - Updated dependencies [dea02f0]
69
+ - Updated dependencies [21e0d88]
70
+ - Updated dependencies [52c55bf]
71
+ - Updated dependencies [935d34e]
72
+ - Updated dependencies [e430fbe]
73
+ - Updated dependencies [eab80e3]
74
+ - Updated dependencies [259b93c]
75
+ - Updated dependencies [53666a7]
76
+ - Updated dependencies [b3b547c]
77
+ - Updated dependencies [d2d49cf]
78
+ - Updated dependencies [0d912a3]
79
+ - Updated dependencies [0d912a3]
80
+ - Updated dependencies [692fa18]
81
+ - @checkstack/anomaly-common@1.6.0
82
+ - @checkstack/healthcheck-common@1.10.0
83
+ - @checkstack/healthcheck-frontend@0.27.0
84
+ - @checkstack/notification-common@1.5.0
85
+ - @checkstack/notification-frontend@0.7.0
86
+ - @checkstack/ui@1.22.0
87
+ - @checkstack/frontend-api@0.13.0
88
+ - @checkstack/common@0.19.0
89
+ - @checkstack/catalog-common@2.6.0
90
+ - @checkstack/signal-frontend@0.3.2
91
+
92
+ ## 0.5.18
93
+
94
+ ### Patch Changes
95
+
96
+ - Updated dependencies [baf9b6e]
97
+ - @checkstack/ui@1.21.0
98
+ - @checkstack/healthcheck-frontend@0.26.1
99
+ - @checkstack/notification-frontend@0.6.4
100
+
3
101
  ## 0.5.17
4
102
 
5
103
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/anomaly-frontend",
3
- "version": "0.5.17",
3
+ "version": "0.6.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -16,16 +16,16 @@
16
16
  "lint:code": "eslint . --max-warnings 0"
17
17
  },
18
18
  "dependencies": {
19
- "@checkstack/anomaly-common": "1.5.4",
20
- "@checkstack/catalog-common": "2.5.0",
21
- "@checkstack/common": "0.18.0",
22
- "@checkstack/frontend-api": "0.12.1",
23
- "@checkstack/healthcheck-common": "1.9.0",
24
- "@checkstack/healthcheck-frontend": "0.26.0",
25
- "@checkstack/notification-common": "1.4.2",
26
- "@checkstack/notification-frontend": "0.6.3",
27
- "@checkstack/signal-frontend": "0.3.1",
28
- "@checkstack/ui": "1.20.0",
19
+ "@checkstack/anomaly-common": "1.6.0",
20
+ "@checkstack/catalog-common": "2.6.0",
21
+ "@checkstack/common": "0.19.0",
22
+ "@checkstack/frontend-api": "0.13.0",
23
+ "@checkstack/healthcheck-common": "1.10.0",
24
+ "@checkstack/healthcheck-frontend": "0.27.0",
25
+ "@checkstack/notification-common": "1.5.0",
26
+ "@checkstack/notification-frontend": "0.7.0",
27
+ "@checkstack/signal-frontend": "0.3.2",
28
+ "@checkstack/ui": "1.22.0",
29
29
  "date-fns": "^4.4.0",
30
30
  "lucide-react": "^1.17.0",
31
31
  "react": "19.2.7",
@@ -33,7 +33,7 @@
33
33
  "zod": "^4.2.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@checkstack/scripts": "0.6.5",
36
+ "@checkstack/scripts": "0.7.0",
37
37
  "@checkstack/tsconfig": "0.0.7",
38
38
  "@types/react": "^19.0.0",
39
39
  "typescript": "^5.0.0"
@@ -47,6 +47,7 @@ export const AnomalySignalsFiller: React.FC<Props> = ({
47
47
  .map((a) => ({
48
48
  systemId: a.systemId,
49
49
  configurationId: a.configurationId,
50
+ environmentId: a.environmentId,
50
51
  fieldPath: a.fieldPath,
51
52
  startedAt: a.startedAt,
52
53
  state: a.state,
@@ -5,7 +5,7 @@ import {
5
5
  accessApiRef,
6
6
  type SlotContext,
7
7
  } from "@checkstack/frontend-api";
8
- import { SystemDetailsSlot } from "@checkstack/catalog-common";
8
+ import { SystemDetailsSlot, CatalogApi } from "@checkstack/catalog-common";
9
9
  import {
10
10
  AnomalyApi,
11
11
  anomalyAccess,
@@ -33,6 +33,7 @@ import {
33
33
  Bell,
34
34
  BellOff,
35
35
  EyeOff,
36
+ Layers,
36
37
  } from "lucide-react";
37
38
  import { formatDistanceToNow } from "date-fns";
38
39
  import { Link } from "react-router-dom";
@@ -123,6 +124,7 @@ function humanizeCollectorSource(strategyId: string, collectorId: string): strin
123
124
  function AnomalyRow({
124
125
  anomaly,
125
126
  systemId,
127
+ environmentLabel,
126
128
  isMuted,
127
129
  onToggleMute,
128
130
  isToggling,
@@ -133,6 +135,12 @@ function AnomalyRow({
133
135
  }: {
134
136
  anomaly: AnomalyDto;
135
137
  systemId: string;
138
+ /**
139
+ * Resolved environment label for an env-scoped anomaly (rendered as a pill
140
+ * next to the field name). `undefined` for the env-less slice / cross-env
141
+ * feed - no pill, matching the pre-per-env single-row look.
142
+ */
143
+ environmentLabel?: string;
136
144
  isMuted: boolean;
137
145
  onToggleMute: (fieldPath: string, isMuted: boolean) => void;
138
146
  isToggling: boolean;
@@ -198,6 +206,12 @@ function AnomalyRow({
198
206
  {parsed.source}
199
207
  </span>
200
208
  ) : undefined}
209
+ {environmentLabel ? (
210
+ <span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border/60 bg-surface-inset px-1.5 py-0.5 text-[10px] text-muted-foreground">
211
+ <Layers className="h-2.5 w-2.5" />
212
+ {environmentLabel}
213
+ </span>
214
+ ) : undefined}
201
215
  {isMuted && (
202
216
  <span className="text-[10px] text-muted-foreground bg-muted/60 px-1.5 py-0.5 rounded font-medium shrink-0 inline-flex items-center gap-0.5">
203
217
  <BellOff className="h-2.5 w-2.5" />
@@ -299,6 +313,7 @@ function AnomalyRow({
299
313
 
300
314
  export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
301
315
  const anomalyClient = usePluginClient(AnomalyApi);
316
+ const catalogClient = usePluginClient(CatalogApi);
302
317
  const toast = useToast();
303
318
  const accessApi = useApi(accessApiRef);
304
319
  // Muting notifications is a per-user preference any LOGGED-IN user may set
@@ -329,6 +344,19 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
329
344
  { staleTime: 30_000 },
330
345
  );
331
346
 
347
+ // Resolve env names for env-scoped anomaly rows (two envs of the same field
348
+ // now surface as two rows). Same query the healthcheck overview/drawer issue,
349
+ // so the cache entry is shared when those surfaces mount alongside this one.
350
+ const { data: systemEnvironments = [] } =
351
+ catalogClient.getSystemEnvironments.useQuery(
352
+ { systemId: system.id },
353
+ { staleTime: 30_000 },
354
+ );
355
+ const envNameById = React.useMemo(
356
+ () => new Map(systemEnvironments.map((e) => [e.id, e.name])),
357
+ [systemEnvironments],
358
+ );
359
+
332
360
  const mutedFields = React.useMemo(
333
361
  () => new Set(mutes.map((m) => m.fieldPath)),
334
362
  [mutes],
@@ -502,6 +530,12 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
502
530
  key={anomaly.id}
503
531
  anomaly={anomaly}
504
532
  systemId={system.id}
533
+ environmentLabel={
534
+ anomaly.environmentId
535
+ ? (envNameById.get(anomaly.environmentId) ??
536
+ anomaly.environmentId)
537
+ : undefined
538
+ }
505
539
  isMuted={isSystemMuted || mutedFields.has(anomaly.fieldPath)}
506
540
  onToggleMute={handleToggleMute}
507
541
  isToggling={isToggling}