@checkstack/anomaly-frontend 0.5.18 → 0.6.1
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,136 @@
|
|
|
1
1
|
# @checkstack/anomaly-frontend
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0cac684: Align the health-check run-history gates end to end. The history surfaces had a
|
|
8
|
+
three-way drift: the route allowed `configuration.read`, the page required
|
|
9
|
+
manage capability, and the procedures required the standalone
|
|
10
|
+
`healthcheck.details` rule - so global read-rule holders reached a page that
|
|
11
|
+
denied them, and team-scoped managers passed the page gate but got 403s from
|
|
12
|
+
every data call.
|
|
13
|
+
|
|
14
|
+
Detailed run history is now a MANAGER surface everywhere, with system owners
|
|
15
|
+
included: access requires global `configuration.manage`, a team manage grant
|
|
16
|
+
on the CONFIGURATION, or manage access to the SYSTEM - a system's owning team
|
|
17
|
+
sees every run of that system, whoever owns the configuration.
|
|
18
|
+
|
|
19
|
+
- Routes, pages, drawer links, and the anomaly/health signals gate on the
|
|
20
|
+
manage capability (with `catalog.system` as the parent type); the drawer and
|
|
21
|
+
chart hook check the caller's grant on the specific configuration OR system.
|
|
22
|
+
- All three history procedures (`getDetailedHistory`,
|
|
23
|
+
`getDetailedAggregatedHistory`, `getRunById`) are authorized in the handler
|
|
24
|
+
via a shared fail-closed module (`history-access.ts`) - the triple-OR is not
|
|
25
|
+
expressible with the declarative instanceAccess modes. `getRunById`
|
|
26
|
+
authorizes against the fetched run's own configuration/system, and answers
|
|
27
|
+
`undefined` for unauthorized callers so run ids don't leak existence.
|
|
28
|
+
- The feed (`getDetailedHistory`) scopes team callers to runs of their
|
|
29
|
+
configurations UNION runs of their systems, with correct pagination totals.
|
|
30
|
+
|
|
31
|
+
BREAKING CHANGES:
|
|
32
|
+
|
|
33
|
+
- The standalone `healthcheck.details` access rule is REMOVED. Roles that held
|
|
34
|
+
`details` without `configuration.manage` lose access to detailed run data;
|
|
35
|
+
grant them the manage rule (or a team grant on the configuration/system)
|
|
36
|
+
instead. Stale role rows referencing the removed rule are inert.
|
|
37
|
+
- `getDetailedAggregatedHistory` is `authenticated` (was `public`); anonymous
|
|
38
|
+
callers could never pass its access rule anyway.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [0cac684]
|
|
41
|
+
- @checkstack/healthcheck-common@1.11.0
|
|
42
|
+
- @checkstack/healthcheck-frontend@0.28.0
|
|
43
|
+
- @checkstack/notification-frontend@0.7.1
|
|
44
|
+
|
|
45
|
+
## 0.6.0
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- 5236e41: Scope anomaly rows by (check, environment), completing the deferred follow-up
|
|
50
|
+
from the per-environment work in #375 (which env-scoped only baselines).
|
|
51
|
+
|
|
52
|
+
Previously the `anomalies` table was cross-environment: the inline spike
|
|
53
|
+
detector and the drift evaluator located and created the open row by
|
|
54
|
+
`(systemId, configurationId, fieldPath, kind)` with no environment predicate.
|
|
55
|
+
When a `(system, configuration)` assignment fanned out to multiple environments,
|
|
56
|
+
a healthy value in environment A shared one row with an anomaly in environment B,
|
|
57
|
+
so one env could mask (or merge with) another.
|
|
58
|
+
|
|
59
|
+
- **Schema.** New nullable `anomalies.environment_id` column (migration
|
|
60
|
+
`0007_uneven_trauma.sql`, a single `ADD COLUMN`). No unique constraint is
|
|
61
|
+
added: the table intentionally allows multiple rows per identity tuple (a
|
|
62
|
+
`recovered` historical row plus a fresh active row), so uniqueness would break
|
|
63
|
+
the state machine.
|
|
64
|
+
- **Detection.** The spike detector (from the `checkCompleted` hook) and the
|
|
65
|
+
drift evaluator (from the analyzer's per-environment loop) now locate/create
|
|
66
|
+
the open row by `(systemId, configurationId, environmentId, fieldPath, kind)`,
|
|
67
|
+
matching `environment_id = <id>` when present or `IS NULL` for the env-less
|
|
68
|
+
slice - mirroring the per-environment baseline lookup.
|
|
69
|
+
- **Reads.** `getAnomalies` gains an optional `environmentId` tristate filter
|
|
70
|
+
(`undefined` = all envs, `null` = env-less slice, string = that env), and both
|
|
71
|
+
`AnomalyDto` and `getActiveSignalAnomalies` surface `environmentId`. The
|
|
72
|
+
system-detail widget renders an environment pill on env-scoped anomaly rows.
|
|
73
|
+
- **Notifications.** An env-scoped anomaly appends its environment id to the
|
|
74
|
+
collapse key, so two failing environments render as two independent cards
|
|
75
|
+
instead of collapsing into one. The env-less slice keeps the pre-feature
|
|
76
|
+
two-segment key. Mutes stay env-agnostic (per system / per field).
|
|
77
|
+
|
|
78
|
+
BREAKING (semantics, not types; BETA so minor only):
|
|
79
|
+
|
|
80
|
+
- **Anomaly row identity now includes `environmentId`.** For a fanned-out check,
|
|
81
|
+
an anomaly in one environment is a distinct row from another environment. Any
|
|
82
|
+
code that assumed a single anomaly row per `(system, config, field, kind)`
|
|
83
|
+
must account for the environment dimension.
|
|
84
|
+
- **`AnomalyDto` and `getActiveSignalAnomalies` rows carry a new
|
|
85
|
+
`environmentId: string | null` field**, and `getAnomalies` accepts a new
|
|
86
|
+
optional `environmentId` filter. Additive on the wire; consumers that reject
|
|
87
|
+
unknown fields should be updated.
|
|
88
|
+
- **Upgrade behaviour.** Existing rows backfill to `null` (the env-less slice)
|
|
89
|
+
and stay until they recover; the next detection tick opens fresh
|
|
90
|
+
per-environment rows for fanned-out checks. This mirrors how #375 handled
|
|
91
|
+
baselines.
|
|
92
|
+
|
|
93
|
+
State and scale: the anomaly state lives entirely in the shared `anomalies`
|
|
94
|
+
Postgres table. `environmentId` is just another column on the row, so every pod
|
|
95
|
+
reads the same per-`(system, config, env, field, kind)` state - no pod-local
|
|
96
|
+
state, and reads return the same answer on every pod. The baseline cache key
|
|
97
|
+
already carries the env segment (#375), so there is no cross-env cache shadowing.
|
|
98
|
+
|
|
99
|
+
### Patch Changes
|
|
100
|
+
|
|
101
|
+
- Updated dependencies [52c55bf]
|
|
102
|
+
- Updated dependencies [5236e41]
|
|
103
|
+
- Updated dependencies [d1b71b6]
|
|
104
|
+
- Updated dependencies [0d912a3]
|
|
105
|
+
- Updated dependencies [a07b375]
|
|
106
|
+
- Updated dependencies [d9f4654]
|
|
107
|
+
- Updated dependencies [b45be8e]
|
|
108
|
+
- Updated dependencies [d9f4654]
|
|
109
|
+
- Updated dependencies [3420d24]
|
|
110
|
+
- Updated dependencies [dea02f0]
|
|
111
|
+
- Updated dependencies [21e0d88]
|
|
112
|
+
- Updated dependencies [52c55bf]
|
|
113
|
+
- Updated dependencies [935d34e]
|
|
114
|
+
- Updated dependencies [e430fbe]
|
|
115
|
+
- Updated dependencies [eab80e3]
|
|
116
|
+
- Updated dependencies [259b93c]
|
|
117
|
+
- Updated dependencies [53666a7]
|
|
118
|
+
- Updated dependencies [b3b547c]
|
|
119
|
+
- Updated dependencies [d2d49cf]
|
|
120
|
+
- Updated dependencies [0d912a3]
|
|
121
|
+
- Updated dependencies [0d912a3]
|
|
122
|
+
- Updated dependencies [692fa18]
|
|
123
|
+
- @checkstack/anomaly-common@1.6.0
|
|
124
|
+
- @checkstack/healthcheck-common@1.10.0
|
|
125
|
+
- @checkstack/healthcheck-frontend@0.27.0
|
|
126
|
+
- @checkstack/notification-common@1.5.0
|
|
127
|
+
- @checkstack/notification-frontend@0.7.0
|
|
128
|
+
- @checkstack/ui@1.22.0
|
|
129
|
+
- @checkstack/frontend-api@0.13.0
|
|
130
|
+
- @checkstack/common@0.19.0
|
|
131
|
+
- @checkstack/catalog-common@2.6.0
|
|
132
|
+
- @checkstack/signal-frontend@0.3.2
|
|
133
|
+
|
|
3
134
|
## 0.5.18
|
|
4
135
|
|
|
5
136
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/anomaly-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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.
|
|
20
|
-
"@checkstack/catalog-common": "2.
|
|
21
|
-
"@checkstack/common": "0.
|
|
22
|
-
"@checkstack/frontend-api": "0.
|
|
23
|
-
"@checkstack/healthcheck-common": "1.
|
|
24
|
-
"@checkstack/healthcheck-frontend": "0.
|
|
25
|
-
"@checkstack/notification-common": "1.
|
|
26
|
-
"@checkstack/notification-frontend": "0.
|
|
27
|
-
"@checkstack/signal-frontend": "0.3.
|
|
28
|
-
"@checkstack/ui": "1.
|
|
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.11.0",
|
|
24
|
+
"@checkstack/healthcheck-frontend": "0.28.0",
|
|
25
|
+
"@checkstack/notification-common": "1.5.0",
|
|
26
|
+
"@checkstack/notification-frontend": "0.7.1",
|
|
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.
|
|
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,
|
|
@@ -59,8 +60,10 @@ export const AnomalySignalsFiller: React.FC<Props> = ({
|
|
|
59
60
|
systemId: row.systemId,
|
|
60
61
|
configurationId: row.configurationId,
|
|
61
62
|
}),
|
|
62
|
-
// The history detail page is
|
|
63
|
-
accessRule
|
|
63
|
+
// The history detail page is a manager surface; render as text for
|
|
64
|
+
// users without global healthcheck manage (a signal's accessRule is a
|
|
65
|
+
// global check - team-scoped managers reach history via their pages).
|
|
66
|
+
accessRule: healthCheckAccess.configuration.manage,
|
|
64
67
|
});
|
|
65
68
|
}, [confirmed, suspicious, systemIds]);
|
|
66
69
|
|
|
@@ -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}
|