@checkstack/anomaly-frontend 0.3.0 → 0.4.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.
@@ -1,4 +1,12 @@
1
- import { Label, Toggle, Slider } from "@checkstack/ui";
1
+ import {
2
+ Label,
3
+ Toggle,
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@checkstack/ui";
2
10
  import type {
3
11
  AnomalyDirection,
4
12
  AnomalyFieldConfig,
@@ -8,10 +16,8 @@ import type { AnomalyFieldMeta } from "./useAnomalyFields";
8
16
 
9
17
  export interface AnomalySettingsFormValues {
10
18
  enabled: boolean;
11
- sensitivity: number;
12
- confirmationWindow: number;
13
- driftEnabled: boolean;
14
- driftThreshold: number;
19
+ baselineWindow: string;
20
+ notify: boolean;
15
21
  fieldOverrides: Record<string, AnomalyFieldConfig>;
16
22
  }
17
23
 
@@ -29,26 +35,37 @@ export interface AnomalySettingsFormProps {
29
35
 
30
36
  const COPY = {
31
37
  template: {
32
- enabledLabel: "Enable Anomaly Detection by Default",
38
+ enabledLabel: "Enable anomaly detection by default",
33
39
  enabledDescription:
34
40
  "Run background analysis to detect deviations from expected behavior across all systems using this template.",
35
- sensitivityLabel: "Global Sensitivity Multiplier",
36
- confirmationLabel: "Confirmation Window",
37
- fieldOverridesTitle: "Global Field-Level Defaults",
41
+ fieldOverridesTitle: "Field-level defaults",
38
42
  fieldOverridesDescription:
39
- "Set default anomaly behavior for specific metrics collected by this health check.",
43
+ "Tune anomaly detection for specific metrics. Each field uses the plugin's tuned defaults until you override it here.",
44
+ notifyLabel: "Send notifications on confirmed anomalies",
45
+ notifyDescription:
46
+ "Page the assigned subscribers when an anomaly transitions to confirmed.",
40
47
  },
41
48
  assignment: {
42
- enabledLabel: "Enable Assignment Exceptions",
43
- enabledDescription: "Run background analysis for this specific system",
44
- sensitivityLabel: "Sensitivity Multiplier Override",
45
- confirmationLabel: "Confirmation Window Override",
46
- fieldOverridesTitle: "Field-Level Overrides",
49
+ enabledLabel: "Enable assignment exceptions",
50
+ enabledDescription:
51
+ "Run background analysis for this specific system assignment.",
52
+ fieldOverridesTitle: "Field-level overrides",
47
53
  fieldOverridesDescription:
48
- "Override anomaly settings for specific metrics collected by this health check.",
54
+ "Override anomaly settings for specific metrics on this system. Other systems using the same template are unaffected.",
55
+ notifyLabel: "Send notifications on confirmed anomalies",
56
+ notifyDescription:
57
+ "Page the assigned subscribers for this system when an anomaly transitions to confirmed.",
49
58
  },
50
59
  } as const;
51
60
 
61
+ const BASELINE_WINDOW_OPTIONS: { value: string; label: string }[] = [
62
+ { value: "1d", label: "1 day" },
63
+ { value: "3d", label: "3 days" },
64
+ { value: "7d", label: "7 days (recommended)" },
65
+ { value: "14d", label: "14 days" },
66
+ { value: "30d", label: "30 days" },
67
+ ];
68
+
52
69
  export function AnomalySettingsForm({
53
70
  values,
54
71
  onChange,
@@ -57,14 +74,7 @@ export function AnomalySettingsForm({
57
74
  variant,
58
75
  }: AnomalySettingsFormProps) {
59
76
  const copy = COPY[variant];
60
- const {
61
- enabled,
62
- sensitivity,
63
- confirmationWindow,
64
- driftEnabled,
65
- driftThreshold,
66
- fieldOverrides,
67
- } = values;
77
+ const { enabled, baselineWindow, notify, fieldOverrides } = values;
68
78
 
69
79
  const handleFieldOverrideChange = (
70
80
  field: string,
@@ -76,15 +86,34 @@ export function AnomalySettingsForm({
76
86
  onChange("fieldOverrides", next);
77
87
  };
78
88
 
89
+ const handleFieldPatch = (
90
+ field: string,
91
+ patch: Partial<AnomalyFieldConfig>,
92
+ ) => {
93
+ const next = { ...fieldOverrides };
94
+ next[field] = { ...next[field], ...patch };
95
+ onChange("fieldOverrides", next);
96
+ };
97
+
98
+ const handleFieldReset = (field: string) => {
99
+ const next = { ...fieldOverrides };
100
+ delete next[field];
101
+ onChange("fieldOverrides", next);
102
+ };
103
+
79
104
  return (
80
105
  <div className="space-y-4">
81
106
  <div className="flex items-center justify-between p-4 border rounded-md">
82
107
  <div className="space-y-0.5">
83
108
  <Label className="text-base font-medium">{copy.enabledLabel}</Label>
84
- <div className="text-sm text-muted-foreground">{copy.enabledDescription}</div>
109
+ <div className="text-sm text-muted-foreground">
110
+ {copy.enabledDescription}
111
+ </div>
85
112
  </div>
86
113
  <div className="flex items-center gap-3">
87
- <span className="text-sm font-medium">{enabled ? "Enabled" : "Disabled"}</span>
114
+ <span className="text-sm font-medium">
115
+ {enabled ? "Enabled" : "Disabled"}
116
+ </span>
88
117
  <Toggle
89
118
  checked={enabled}
90
119
  onCheckedChange={(val) => onChange("enabled", val)}
@@ -95,101 +124,44 @@ export function AnomalySettingsForm({
95
124
 
96
125
  <div className="grid gap-6 md:grid-cols-2 p-4 border rounded-md">
97
126
  <div className="space-y-2">
98
- <Label htmlFor="sensitivity">{copy.sensitivityLabel}</Label>
99
- <div className="pt-4 pb-2 px-1">
100
- <Slider
101
- id="sensitivity"
102
- value={[sensitivity]}
103
- min={0.5}
104
- max={3}
105
- step={0.1}
106
- onValueChange={(val) => onChange("sensitivity", val[0])}
107
- disabled={!enabled || isLocked}
108
- />
109
- </div>
110
- <div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
111
- <span>0.5 (More)</span>
112
- <span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
113
- {sensitivity.toFixed(1)}x
114
- </span>
115
- <span>3.0 (Fewer)</span>
116
- </div>
117
- {variant === "template" && (
118
- <p className="text-xs text-muted-foreground pt-2">
119
- Higher multiplier = wider expected range (fewer alerts).
120
- </p>
121
- )}
122
- </div>
123
-
124
- <div className="space-y-2">
125
- <Label htmlFor="confirmationWindow">{copy.confirmationLabel}</Label>
126
- <div className="pt-4 pb-2 px-1">
127
- <Slider
128
- id="confirmationWindow"
129
- value={[confirmationWindow]}
130
- min={1}
131
- max={10}
132
- step={1}
133
- onValueChange={(val) => onChange("confirmationWindow", val[0])}
134
- disabled={!enabled || isLocked}
135
- />
136
- </div>
137
- <div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
138
- <span>1 Run</span>
139
- <span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
140
- {confirmationWindow} Runs
141
- </span>
142
- <span>10 Runs</span>
143
- </div>
144
- {variant === "template" && (
145
- <p className="text-xs text-muted-foreground pt-2">
146
- Number of consecutive anomalous runs required before an alert is triggered.
147
- </p>
148
- )}
127
+ <Label htmlFor="baselineWindow" className="text-sm font-medium">
128
+ Baseline window
129
+ </Label>
130
+ <Select
131
+ value={baselineWindow}
132
+ onValueChange={(val) => onChange("baselineWindow", val)}
133
+ disabled={!enabled || isLocked}
134
+ >
135
+ <SelectTrigger id="baselineWindow" className="h-10">
136
+ <SelectValue />
137
+ </SelectTrigger>
138
+ <SelectContent>
139
+ {BASELINE_WINDOW_OPTIONS.map((opt) => (
140
+ <SelectItem key={opt.value} value={opt.value}>
141
+ {opt.label}
142
+ </SelectItem>
143
+ ))}
144
+ </SelectContent>
145
+ </Select>
146
+ <p className="text-xs text-muted-foreground">
147
+ How much history to use when computing each metric's baseline.
148
+ Longer windows are smoother but slower to react to legitimate
149
+ changes.
150
+ </p>
149
151
  </div>
150
- </div>
151
152
 
152
- <div className="grid gap-6 md:grid-cols-2 p-4 border rounded-md">
153
- <div className="flex items-center justify-between md:col-span-2">
153
+ <div className="flex items-center justify-between md:col-span-1">
154
154
  <div className="space-y-0.5">
155
- <Label className="text-base font-medium">Trend Drift Detection</Label>
156
- <div className="text-sm text-muted-foreground">
157
- Catch slow, gradual degradation that never triggers a spike alert.
155
+ <Label className="text-sm font-medium">{copy.notifyLabel}</Label>
156
+ <div className="text-xs text-muted-foreground">
157
+ {copy.notifyDescription}
158
158
  </div>
159
159
  </div>
160
- <div className="flex items-center gap-3">
161
- <span className="text-sm font-medium">{driftEnabled ? "Enabled" : "Disabled"}</span>
162
- <Toggle
163
- checked={driftEnabled}
164
- onCheckedChange={(val) => onChange("driftEnabled", val)}
165
- disabled={!enabled || isLocked}
166
- />
167
- </div>
168
- </div>
169
-
170
- <div className="space-y-2 md:col-span-2">
171
- <Label htmlFor="driftThreshold">Drift Threshold (σ)</Label>
172
- <div className="pt-4 pb-2 px-1">
173
- <Slider
174
- id="driftThreshold"
175
- value={[driftThreshold]}
176
- min={1}
177
- max={4}
178
- step={0.1}
179
- onValueChange={(val) => onChange("driftThreshold", val[0])}
180
- disabled={!enabled || !driftEnabled || isLocked}
181
- />
182
- </div>
183
- <div className="flex justify-between items-center text-[10px] font-mono text-muted-foreground pt-1">
184
- <span>1.0σ (More)</span>
185
- <span className="font-semibold text-foreground bg-muted px-1.5 py-0.5 rounded">
186
- {driftThreshold.toFixed(1)}σ
187
- </span>
188
- <span>4.0σ (Fewer)</span>
189
- </div>
190
- <p className="text-xs text-muted-foreground">
191
- Drift fires when the projected change over the baseline window exceeds this many standard deviations.
192
- </p>
160
+ <Toggle
161
+ checked={notify}
162
+ onCheckedChange={(val) => onChange("notify", val)}
163
+ disabled={!enabled || isLocked}
164
+ />
193
165
  </div>
194
166
  </div>
195
167
 
@@ -199,12 +171,10 @@ export function AnomalySettingsForm({
199
171
  availableFields={availableFields}
200
172
  fieldOverrides={fieldOverrides}
201
173
  onChange={handleFieldOverrideChange}
174
+ onPatchField={handleFieldPatch}
175
+ onResetField={handleFieldReset}
202
176
  parentEnabled={enabled}
203
177
  isLocked={isLocked}
204
- defaultSensitivity={sensitivity}
205
- defaultConfirmationWindow={confirmationWindow}
206
- defaultDriftEnabled={driftEnabled}
207
- defaultDriftThreshold={driftThreshold}
208
178
  />
209
179
  </div>
210
180
  );
@@ -24,10 +24,8 @@ import {
24
24
 
25
25
  const DEFAULT_VALUES: AnomalySettingsFormValues = {
26
26
  enabled: true,
27
- sensitivity: 1,
28
- confirmationWindow: 3,
29
- driftEnabled: true,
30
- driftThreshold: 2,
27
+ baselineWindow: "7d",
28
+ notify: true,
31
29
  fieldOverrides: {},
32
30
  };
33
31
 
@@ -36,9 +34,6 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
36
34
  const anomalyClient = usePluginClient(AnomalyApi);
37
35
 
38
36
  const [values, setValues] = useState<AnomalySettingsFormValues>(DEFAULT_VALUES);
39
- // Template-only settings — preserved on save but not surfaced as form controls.
40
- const [baselineWindow, setBaselineWindow] = useState("7d");
41
- const [notify, setNotify] = useState(true);
42
37
 
43
38
  const { data: configRecord, isLoading } = anomalyClient.getAnomalyConfig.useQuery(
44
39
  { configurationId: context.configurationId },
@@ -56,16 +51,12 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
56
51
  if (configRecord?.data) {
57
52
  setValues({
58
53
  enabled: configRecord.data.enabled ?? true,
59
- sensitivity: configRecord.data.sensitivity ?? 1,
60
- confirmationWindow: configRecord.data.confirmationWindow ?? 3,
61
- driftEnabled: configRecord.data.driftEnabled ?? true,
62
- driftThreshold: configRecord.data.driftThreshold ?? 2,
54
+ baselineWindow: configRecord.data.baselineWindow ?? "7d",
55
+ notify: configRecord.data.notify ?? true,
63
56
  fieldOverrides:
64
57
  (configRecord.data.fieldOverrides as Record<string, AnomalyFieldConfig>) ??
65
58
  {},
66
59
  });
67
- setBaselineWindow(configRecord.data.baselineWindow ?? "7d");
68
- setNotify(configRecord.data.notify ?? true);
69
60
  }
70
61
  }, [configRecord]);
71
62
 
@@ -79,11 +70,7 @@ export function AnomalyTemplatePanel({ context }: { context: HealthCheckConfigID
79
70
  const handleSave = () => {
80
71
  updateMutation.mutate({
81
72
  configurationId: context.configurationId,
82
- config: {
83
- ...values,
84
- baselineWindow,
85
- notify,
86
- },
73
+ config: values,
87
74
  });
88
75
  };
89
76
 
@@ -8,12 +8,15 @@ import type { AnomalyDirection } from "@checkstack/anomaly-common";
8
8
  export type AnomalyFieldMeta = {
9
9
  path: string;
10
10
  type: string;
11
+ unit?: string;
11
12
  defaultEnabled: boolean;
12
13
  defaultDirection?: AnomalyDirection;
13
14
  defaultSensitivity?: number;
14
15
  defaultConfirmationWindow?: number;
15
16
  defaultDriftEnabled?: boolean;
16
17
  defaultDriftThreshold?: number;
18
+ defaultMinAbsoluteDelta?: number;
19
+ defaultMinRelativeDelta?: number;
17
20
  };
18
21
 
19
22
  export function useAnomalyFields(configurationId: string | undefined) {
@@ -50,12 +53,15 @@ export function useAnomalyFields(configurationId: string | undefined) {
50
53
  keys.push({
51
54
  path,
52
55
  type: value.type || "string",
56
+ unit: typeof value["x-chart-unit"] === "string" ? value["x-chart-unit"] : undefined,
53
57
  defaultEnabled,
54
58
  defaultDirection: value["x-anomaly-direction"] as AnomalyDirection | undefined,
55
59
  defaultSensitivity: typeof value["x-anomaly-sensitivity"] === "number" ? value["x-anomaly-sensitivity"] : undefined,
56
60
  defaultConfirmationWindow: typeof value["x-anomaly-confirmation-window"] === "number" ? value["x-anomaly-confirmation-window"] : undefined,
57
61
  defaultDriftEnabled: typeof value["x-anomaly-drift-enabled"] === "boolean" ? value["x-anomaly-drift-enabled"] : undefined,
58
62
  defaultDriftThreshold: typeof value["x-anomaly-drift-threshold"] === "number" ? value["x-anomaly-drift-threshold"] : undefined,
63
+ defaultMinAbsoluteDelta: typeof value["x-anomaly-min-absolute-delta"] === "number" ? value["x-anomaly-min-absolute-delta"] : undefined,
64
+ defaultMinRelativeDelta: typeof value["x-anomaly-min-relative-delta"] === "number" ? value["x-anomaly-min-relative-delta"] : undefined,
59
65
  });
60
66
  }
61
67
  }
package/tsconfig.json CHANGED
@@ -5,5 +5,37 @@
5
5
  },
6
6
  "include": [
7
7
  "src"
8
+ ],
9
+ "references": [
10
+ {
11
+ "path": "../anomaly-common"
12
+ },
13
+ {
14
+ "path": "../catalog-common"
15
+ },
16
+ {
17
+ "path": "../common"
18
+ },
19
+ {
20
+ "path": "../frontend-api"
21
+ },
22
+ {
23
+ "path": "../healthcheck-common"
24
+ },
25
+ {
26
+ "path": "../healthcheck-frontend"
27
+ },
28
+ {
29
+ "path": "../notification-common"
30
+ },
31
+ {
32
+ "path": "../notification-frontend"
33
+ },
34
+ {
35
+ "path": "../signal-frontend"
36
+ },
37
+ {
38
+ "path": "../ui"
39
+ }
8
40
  ]
9
41
  }