@checkstack/healthcheck-backend 0.12.0 → 0.12.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,48 @@
1
1
  # @checkstack/healthcheck-backend
2
2
 
3
+ ## 0.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
8
+
9
+ **New utility**
10
+
11
+ - `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
12
+
13
+ **ESLint rules**
14
+
15
+ - `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
16
+ - `no-console` in frontend packages — forces `toast` over silent `console.error`
17
+ - `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
18
+ - Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
19
+
20
+ **Refactoring**
21
+
22
+ - Replace 141 `instanceof Error` boilerplate patterns across the codebase
23
+ - Replace swallowed `console.error` with user-visible `toast.error()` feedback
24
+ - Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
25
+ - Consolidate 3 identical callback handlers into `handleDialogClose`
26
+ - Fix conditional React hook call in `FormField.tsx`
27
+ - Fix unstable useMemo deps in `Dashboard.tsx`
28
+ - Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
29
+ - Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
30
+ - Delete obvious comments in `encryption.ts` and Teams `provider.ts`
31
+
32
+ - Updated dependencies [d1a2796]
33
+ - Updated dependencies [3c34b07]
34
+ - @checkstack/common@0.6.5
35
+ - @checkstack/backend-api@0.11.1
36
+ - @checkstack/catalog-backend@0.2.23
37
+ - @checkstack/integration-backend@0.1.18
38
+ - @checkstack/catalog-common@1.3.1
39
+ - @checkstack/healthcheck-common@0.10.1
40
+ - @checkstack/command-backend@0.1.18
41
+ - @checkstack/incident-common@0.4.7
42
+ - @checkstack/maintenance-common@0.4.9
43
+ - @checkstack/signal-common@0.1.9
44
+ - @checkstack/queue-api@0.2.12
45
+
3
46
  ## 0.12.0
4
47
 
5
48
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-backend",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -13,16 +13,16 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/backend-api": "0.10.0",
17
- "@checkstack/catalog-backend": "0.2.20",
18
- "@checkstack/catalog-common": "1.2.11",
19
- "@checkstack/command-backend": "0.1.15",
16
+ "@checkstack/backend-api": "0.11.0",
17
+ "@checkstack/catalog-backend": "0.2.22",
18
+ "@checkstack/catalog-common": "1.3.0",
19
+ "@checkstack/command-backend": "0.1.17",
20
20
  "@checkstack/common": "0.6.4",
21
- "@checkstack/healthcheck-common": "0.8.4",
21
+ "@checkstack/healthcheck-common": "0.10.0",
22
22
  "@checkstack/incident-common": "0.4.6",
23
- "@checkstack/integration-backend": "0.1.15",
23
+ "@checkstack/integration-backend": "0.1.17",
24
24
  "@checkstack/maintenance-common": "0.4.8",
25
- "@checkstack/queue-api": "0.2.9",
25
+ "@checkstack/queue-api": "0.2.11",
26
26
  "@checkstack/signal-common": "0.1.8",
27
27
  "@hono/zod-validator": "^0.7.6",
28
28
  "drizzle-orm": "^0.45.0",
@@ -34,8 +34,8 @@
34
34
  "devDependencies": {
35
35
  "@checkstack/drizzle-helper": "0.0.4",
36
36
  "@checkstack/scripts": "0.1.2",
37
- "@checkstack/test-utils-backend": "0.1.15",
38
- "@checkstack/tsconfig": "0.0.4",
37
+ "@checkstack/test-utils-backend": "0.1.17",
38
+ "@checkstack/tsconfig": "0.0.5",
39
39
  "@types/bun": "^1.0.0",
40
40
  "@types/tdigest": "^0.1.5",
41
41
  "date-fns": "^4.1.0",
@@ -27,7 +27,7 @@ import {
27
27
  import { CatalogApi, catalogRoutes } from "@checkstack/catalog-common";
28
28
  import { MaintenanceApi } from "@checkstack/maintenance-common";
29
29
  import { IncidentApi } from "@checkstack/incident-common";
30
- import { resolveRoute, type InferClient } from "@checkstack/common";
30
+ import { resolveRoute, type InferClient, extractErrorMessage} from "@checkstack/common";
31
31
  import { HealthCheckService } from "./service";
32
32
  import { healthCheckHooks } from "./hooks";
33
33
  import { incrementHourlyAggregate } from "./realtime-aggregation";
@@ -400,7 +400,7 @@ async function executeHealthCheckJob(props: {
400
400
  };
401
401
  } catch (error) {
402
402
  const errorStr =
403
- error instanceof Error ? error.message : String(error);
403
+ extractErrorMessage(error);
404
404
  logger.debug(`Collector ${storageKey} failed: ${errorStr}`);
405
405
  return {
406
406
  storageKey,
@@ -465,7 +465,7 @@ async function executeHealthCheckJob(props: {
465
465
  } catch (error) {
466
466
  const latencyMs = Math.round(performance.now() - start);
467
467
  const caughtError =
468
- error instanceof Error ? error.message : String(error);
468
+ extractErrorMessage(error);
469
469
 
470
470
  // Use a specific error message if available, otherwise use the caught error
471
471
  const finalError = errorMessage || caughtError;
package/src/router.ts CHANGED
@@ -230,10 +230,6 @@ export const createHealthCheckRouter = (
230
230
  return service.getSystemHealthOverview(input.systemId);
231
231
  },
232
232
  ),
233
-
234
- getAvailabilityStats: os.getAvailabilityStats.handler(async ({ input }) => {
235
- return service.getAvailabilityStats(input);
236
- }),
237
233
  });
238
234
  };
239
235
 
package/src/service.ts CHANGED
@@ -12,10 +12,9 @@ import {
12
12
  healthCheckRuns,
13
13
  healthCheckAggregates,
14
14
  VersionedStateThresholds,
15
- DEFAULT_RETENTION_CONFIG,
16
15
  } from "./schema";
17
16
  import * as schema from "./schema";
18
- import { eq, and, InferSelectModel, desc, gte, lte, lt } from "drizzle-orm";
17
+ import { eq, and, InferSelectModel, desc, gte, lte } from "drizzle-orm";
19
18
  import { ORPCError } from "@orpc/server";
20
19
  import { evaluateHealthStatus } from "./state-evaluator";
21
20
  import { stateThresholds } from "./state-thresholds-migrations";
@@ -928,122 +927,6 @@ export class HealthCheckService {
928
927
  * Calculate bucket start time for dynamic interval sizing.
929
928
  * Aligns buckets to the query start time.
930
929
  */
931
- /**
932
- * Get availability statistics for a health check over 31-day and 365-day periods.
933
- * Availability is calculated as (healthyCount / totalRunCount) * 100.
934
- *
935
- * With incremental real-time aggregation, hourly aggregates are always up-to-date
936
- * (updated immediately on every run), so we don't need to query raw runs.
937
- */
938
- async getAvailabilityStats(props: {
939
- systemId: string;
940
- configurationId: string;
941
- }): Promise<{
942
- availability31Days: number | null;
943
- availability365Days: number | null;
944
- totalRuns31Days: number;
945
- totalRuns365Days: number;
946
- }> {
947
- const { systemId, configurationId } = props;
948
- const now = new Date();
949
-
950
- // Get retention config to determine what data tiers are available
951
- const { retentionConfig } = await this.getRetentionConfig(
952
- systemId,
953
- configurationId,
954
- );
955
- const config = retentionConfig ?? DEFAULT_RETENTION_CONFIG;
956
-
957
- // Calculate cutoff dates
958
- const cutoff31Days = new Date(now.getTime() - 31 * 24 * 60 * 60 * 1000);
959
- const cutoff365Days = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
960
-
961
- // Cutoff for hourly aggregates based on retention config
962
- const hourlyCutoff = new Date(
963
- now.getTime() - config.hourlyRetentionDays * 24 * 60 * 60 * 1000,
964
- );
965
-
966
- // Query hourly aggregates for the period they cover (up to hourlyRetentionDays)
967
- // These are always up-to-date due to incremental real-time aggregation
968
- const hourlyAggregates = await this.db
969
- .select({
970
- bucketStart: healthCheckAggregates.bucketStart,
971
- runCount: healthCheckAggregates.runCount,
972
- healthyCount: healthCheckAggregates.healthyCount,
973
- })
974
- .from(healthCheckAggregates)
975
- .where(
976
- and(
977
- eq(healthCheckAggregates.systemId, systemId),
978
- eq(healthCheckAggregates.configurationId, configurationId),
979
- eq(healthCheckAggregates.bucketSize, "hourly"),
980
- gte(healthCheckAggregates.bucketStart, hourlyCutoff),
981
- ),
982
- );
983
-
984
- // Query daily aggregates for data beyond hourly retention
985
- const dailyAggregates = await this.db
986
- .select({
987
- bucketStart: healthCheckAggregates.bucketStart,
988
- runCount: healthCheckAggregates.runCount,
989
- healthyCount: healthCheckAggregates.healthyCount,
990
- })
991
- .from(healthCheckAggregates)
992
- .where(
993
- and(
994
- eq(healthCheckAggregates.systemId, systemId),
995
- eq(healthCheckAggregates.configurationId, configurationId),
996
- eq(healthCheckAggregates.bucketSize, "daily"),
997
- gte(healthCheckAggregates.bucketStart, cutoff365Days),
998
- lt(healthCheckAggregates.bucketStart, hourlyCutoff),
999
- ),
1000
- );
1001
-
1002
- // Aggregate counts
1003
- let totalRuns31Days = 0;
1004
- let healthyRuns31Days = 0;
1005
- let totalRuns365Days = 0;
1006
- let healthyRuns365Days = 0;
1007
-
1008
- // Process hourly aggregates (fresh data within hourlyRetentionDays)
1009
- for (const agg of hourlyAggregates) {
1010
- totalRuns365Days += agg.runCount;
1011
- healthyRuns365Days += agg.healthyCount;
1012
-
1013
- if (agg.bucketStart >= cutoff31Days) {
1014
- totalRuns31Days += agg.runCount;
1015
- healthyRuns31Days += agg.healthyCount;
1016
- }
1017
- }
1018
-
1019
- // Process daily aggregates (older data beyond hourly retention)
1020
- for (const agg of dailyAggregates) {
1021
- totalRuns365Days += agg.runCount;
1022
- healthyRuns365Days += agg.healthyCount;
1023
-
1024
- if (agg.bucketStart >= cutoff31Days) {
1025
- totalRuns31Days += agg.runCount;
1026
- healthyRuns31Days += agg.healthyCount;
1027
- }
1028
- }
1029
-
1030
- // Calculate availability percentages
1031
- const availability31Days =
1032
- // eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
1033
- totalRuns31Days > 0 ? (healthyRuns31Days / totalRuns31Days) * 100 : null;
1034
- const availability365Days =
1035
- totalRuns365Days > 0
1036
- ? (healthyRuns365Days / totalRuns365Days) * 100
1037
- : // eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
1038
- null;
1039
-
1040
- return {
1041
- availability31Days,
1042
- availability365Days,
1043
- totalRuns31Days,
1044
- totalRuns365Days,
1045
- };
1046
- }
1047
930
 
1048
931
  private getBucketStartDynamic(
1049
932
  timestamp: Date,
@@ -1,236 +0,0 @@
1
- import { describe, it, expect, mock, beforeEach } from "bun:test";
2
- import { HealthCheckService } from "./service";
3
- import { subDays, subHours } from "date-fns";
4
-
5
- describe("HealthCheckService.getAvailabilityStats", () => {
6
- // Mock database
7
- let mockDb: ReturnType<typeof createMockDb>;
8
- let service: HealthCheckService;
9
-
10
- // Store mock data for different queries
11
- let mockHourlyAggregates: Array<{
12
- bucketStart: Date;
13
- runCount: number;
14
- healthyCount: number;
15
- }> = [];
16
- let mockDailyAggregates: Array<{
17
- bucketStart: Date;
18
- runCount: number;
19
- healthyCount: number;
20
- }> = [];
21
- let mockRetentionConfig: { retentionConfig: unknown } | undefined = undefined;
22
-
23
- function createMockDb() {
24
- // Select call order for getAvailabilityStats:
25
- // 1. getRetentionConfig (from systemHealthChecks) - uses .then() pattern
26
- // 2. hourlyAggregates
27
- // 3. dailyAggregates
28
- let selectCallCount = 0;
29
-
30
- const createSelectChain = () => {
31
- const currentCall = selectCallCount++;
32
-
33
- return {
34
- from: mock(() => ({
35
- where: mock(() => {
36
- // Call 0: retentionConfig - uses .then() pattern
37
- if (currentCall === 0) {
38
- const result = mockRetentionConfig ? [mockRetentionConfig] : [];
39
- // Return a promise-like object with .then()
40
- return Promise.resolve(result);
41
- }
42
- // Call 1: hourly aggregates
43
- if (currentCall === 1) return Promise.resolve(mockHourlyAggregates);
44
- // Call 2: daily aggregates
45
- return Promise.resolve(mockDailyAggregates);
46
- }),
47
- })),
48
- };
49
- };
50
-
51
- return {
52
- select: mock(createSelectChain),
53
- };
54
- }
55
-
56
- beforeEach(() => {
57
- // Reset mock data
58
- mockHourlyAggregates = [];
59
- mockDailyAggregates = [];
60
- mockRetentionConfig = undefined;
61
- mockDb = createMockDb();
62
- service = new HealthCheckService(mockDb as never, {} as never, {} as never);
63
- });
64
-
65
- describe("with no data", () => {
66
- it("returns null availability when no aggregates exist", async () => {
67
- const result = await service.getAvailabilityStats({
68
- systemId: "sys-1",
69
- configurationId: "config-1",
70
- });
71
-
72
- expect(result.availability31Days).toBeNull();
73
- expect(result.availability365Days).toBeNull();
74
- expect(result.totalRuns31Days).toBe(0);
75
- expect(result.totalRuns365Days).toBe(0);
76
- });
77
- });
78
-
79
- describe("with hourly aggregates (real-time incremental)", () => {
80
- it("calculates 100% availability when all runs are healthy", async () => {
81
- mockHourlyAggregates = [
82
- {
83
- bucketStart: subHours(new Date(), 2),
84
- runCount: 100,
85
- healthyCount: 100,
86
- },
87
- {
88
- bucketStart: subHours(new Date(), 5),
89
- runCount: 100,
90
- healthyCount: 100,
91
- },
92
- ];
93
-
94
- const result = await service.getAvailabilityStats({
95
- systemId: "sys-1",
96
- configurationId: "config-1",
97
- });
98
-
99
- expect(result.availability31Days).toBe(100);
100
- expect(result.availability365Days).toBe(100);
101
- expect(result.totalRuns31Days).toBe(200);
102
- expect(result.totalRuns365Days).toBe(200);
103
- });
104
-
105
- it("calculates correct availability with mixed results", async () => {
106
- mockHourlyAggregates = [
107
- {
108
- bucketStart: subHours(new Date(), 2),
109
- runCount: 100,
110
- healthyCount: 90,
111
- },
112
- {
113
- bucketStart: subHours(new Date(), 5),
114
- runCount: 100,
115
- healthyCount: 80,
116
- },
117
- ];
118
-
119
- const result = await service.getAvailabilityStats({
120
- systemId: "sys-1",
121
- configurationId: "config-1",
122
- });
123
-
124
- // 170 healthy / 200 total = 85%
125
- expect(result.availability31Days).toBe(85);
126
- expect(result.availability365Days).toBe(85);
127
- });
128
-
129
- it("includes current hour data since aggregates are updated incrementally", async () => {
130
- const currentHourStart = new Date();
131
- currentHourStart.setMinutes(0, 0, 0);
132
-
133
- mockHourlyAggregates = [
134
- { bucketStart: currentHourStart, runCount: 10, healthyCount: 9 },
135
- ];
136
-
137
- const result = await service.getAvailabilityStats({
138
- systemId: "sys-1",
139
- configurationId: "config-1",
140
- });
141
-
142
- expect(result.totalRuns31Days).toBe(10);
143
- expect(result.availability31Days).toBe(90);
144
- });
145
- });
146
-
147
- describe("with combined hourly and daily aggregates", () => {
148
- it("combines hourly and daily data correctly", async () => {
149
- mockHourlyAggregates = [
150
- {
151
- bucketStart: subHours(new Date(), 2),
152
- runCount: 100,
153
- healthyCount: 99,
154
- },
155
- ];
156
-
157
- mockDailyAggregates = [
158
- {
159
- bucketStart: subDays(new Date(), 60),
160
- runCount: 100,
161
- healthyCount: 50,
162
- },
163
- ];
164
-
165
- const result = await service.getAvailabilityStats({
166
- systemId: "sys-1",
167
- configurationId: "config-1",
168
- });
169
-
170
- // 31 days: only hourly (99/100) = 99%
171
- expect(result.availability31Days).toBe(99);
172
- expect(result.totalRuns31Days).toBe(100);
173
-
174
- // 365 days: (99+50)/200 = 74.5%
175
- expect(result.availability365Days).toBe(74.5);
176
- expect(result.totalRuns365Days).toBe(200);
177
- });
178
- });
179
-
180
- describe("99.9% availability calculation", () => {
181
- it("calculates precise availability for SLA tracking", async () => {
182
- mockHourlyAggregates = [
183
- {
184
- bucketStart: subHours(new Date(), 5),
185
- runCount: 1000,
186
- healthyCount: 999,
187
- },
188
- ];
189
-
190
- const result = await service.getAvailabilityStats({
191
- systemId: "sys-1",
192
- configurationId: "config-1",
193
- });
194
-
195
- expect(result.availability31Days).toBe(99.9);
196
- expect(result.availability365Days).toBe(99.9);
197
- });
198
-
199
- it("calculates very high availability correctly", async () => {
200
- mockHourlyAggregates = [
201
- {
202
- bucketStart: subHours(new Date(), 5),
203
- runCount: 10_000,
204
- healthyCount: 9999,
205
- },
206
- ];
207
-
208
- const result = await service.getAvailabilityStats({
209
- systemId: "sys-1",
210
- configurationId: "config-1",
211
- });
212
-
213
- expect(result.availability31Days).toBe(99.99);
214
- });
215
- });
216
-
217
- describe("real-time incremental aggregation behavior", () => {
218
- it("uses hourly aggregates directly without raw run queries", async () => {
219
- mockHourlyAggregates = [
220
- {
221
- bucketStart: subHours(new Date(), 1),
222
- runCount: 50,
223
- healthyCount: 48,
224
- },
225
- ];
226
-
227
- const result = await service.getAvailabilityStats({
228
- systemId: "sys-1",
229
- configurationId: "config-1",
230
- });
231
-
232
- expect(result.availability31Days).toBe(96);
233
- expect(result.totalRuns31Days).toBe(50);
234
- });
235
- });
236
- });