@checkstack/slo-common 0.2.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 ADDED
@@ -0,0 +1,30 @@
1
+ # @checkstack/slo-common
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3c34b07: Complete SLO Reliability Engine frontend and backend
8
+
9
+ **Frontend** — 7 new visualization components:
10
+
11
+ - `StreakCounter`: Fire-themed compliance streak counter with color-coded flame and best-streak trophy
12
+ - `AchievementBadge`: Emoji-labeled badges for 9 achievement types with hover tooltip
13
+ - `AttributionChart`: Horizontal stacked bar showing error budget split (self/upstream/remaining)
14
+ - `DowntimeTimeline`: Dot-and-line timeline with attribution badges and timestamps
15
+ - `SloTrendChart`: Pure SVG availability trend line chart from daily snapshots
16
+ - `MilestoneFeed`: Organization-wide milestone feed on the SLO overview sidebar
17
+ - `DependencyExclusionConfig`: Interactive upstream dependency picker for SLO editor
18
+
19
+ **Backend** — Weekly digest scheduled integration event:
20
+
21
+ - `weekly-digest.ts`: Cron job (Monday 09:00 UTC) emitting SLO performance summary
22
+ - Top/worst performers, breach counts, and streak data delivered via configured notification channels
23
+ - New `sloWeeklyDigest` hook registered as integration event
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [d1a2796]
28
+ - @checkstack/common@0.6.5
29
+ - @checkstack/frontend-api@0.3.9
30
+ - @checkstack/signal-common@0.1.9
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@checkstack/slo-common",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./src/index.ts"
8
+ }
9
+ },
10
+ "dependencies": {
11
+ "@checkstack/common": "0.6.4",
12
+ "@checkstack/frontend-api": "0.3.8",
13
+ "@checkstack/signal-common": "0.1.8",
14
+ "@orpc/contract": "^1.13.14",
15
+ "zod": "^4.2.1"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.2",
19
+ "@checkstack/tsconfig": "0.0.5",
20
+ "@checkstack/scripts": "0.1.2"
21
+ },
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "bun run lint:code",
25
+ "lint:code": "eslint . --max-warnings 0"
26
+ },
27
+ "checkstack": {
28
+ "type": "common"
29
+ }
30
+ }
package/src/access.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { accessPair } from "@checkstack/common";
2
+
3
+ /**
4
+ * Access rules for the SLO plugin.
5
+ */
6
+ export const sloAccess = {
7
+ /**
8
+ * SLO access with both read and manage levels.
9
+ * Read is public by default (anyone can view SLO status).
10
+ * Manage requires authentication (create, edit, delete SLOs).
11
+ */
12
+ slo: accessPair("slo", {
13
+ read: {
14
+ description: "View SLOs and error budgets",
15
+ isDefault: true,
16
+ isPublic: true,
17
+ },
18
+ manage: {
19
+ description: "Create, edit, and delete SLOs",
20
+ },
21
+ }),
22
+ };
23
+
24
+ /**
25
+ * All access rules for registration with the plugin system.
26
+ */
27
+ export const sloAccessRules = [sloAccess.slo.read, sloAccess.slo.manage];
package/src/index.ts ADDED
@@ -0,0 +1,79 @@
1
+ export { sloAccess, sloAccessRules } from "./access";
2
+ export { sloContract, SloApi, type SloContract } from "./rpc-contract";
3
+ export {
4
+ DependencyExclusionModeSchema,
5
+ AttributionTypeSchema,
6
+ AchievementTypeSchema,
7
+ BurnRateThresholdsSchema,
8
+ SloObjectiveSchema,
9
+ SloDowntimeEventSchema,
10
+ SloDailySnapshotSchema,
11
+ SloStreakSchema,
12
+ SloAchievementSchema,
13
+ SloAttributionEntrySchema,
14
+ SloStatusSchema,
15
+ CreateSloObjectiveInputSchema,
16
+ UpdateSloObjectiveInputSchema,
17
+ type DependencyExclusionMode,
18
+ type AttributionType,
19
+ type AchievementType,
20
+ type BurnRateThresholds,
21
+ type SloObjective,
22
+ type SloDowntimeEvent,
23
+ type SloDailySnapshot,
24
+ type SloStreak,
25
+ type SloAchievement,
26
+ type SloAttributionEntry,
27
+ type SloStatus,
28
+ type CreateSloObjectiveInput,
29
+ type UpdateSloObjectiveInput,
30
+ } from "./schemas";
31
+ export * from "./plugin-metadata";
32
+ export { sloRoutes } from "./routes";
33
+
34
+ // =============================================================================
35
+ // REALTIME SIGNALS
36
+ // =============================================================================
37
+
38
+ import { createSignal } from "@checkstack/signal-common";
39
+ import { z } from "zod";
40
+
41
+ /**
42
+ * Broadcast when an SLO's status changes (budget consumed, breach started/ended).
43
+ * Frontend components can refetch SLO data for the affected system.
44
+ */
45
+ export const SLO_STATUS_CHANGED = createSignal(
46
+ "slo.status.changed",
47
+ z.object({
48
+ systemId: z.string(),
49
+ objectiveId: z.string(),
50
+ budgetRemainingPercent: z.number(),
51
+ isBreaching: z.boolean(),
52
+ }),
53
+ );
54
+
55
+ /**
56
+ * Broadcast when a streak is updated (incremented or broken).
57
+ * Used to update streak counter UI in real-time.
58
+ */
59
+ export const SLO_STREAK_UPDATED = createSignal(
60
+ "slo.streak.updated",
61
+ z.object({
62
+ systemId: z.string(),
63
+ objectiveId: z.string(),
64
+ currentStreak: z.number(),
65
+ broken: z.boolean(),
66
+ }),
67
+ );
68
+
69
+ /**
70
+ * Broadcast when a system unlocks a new achievement.
71
+ * Used to trigger celebration UI and milestone feed updates.
72
+ */
73
+ export const SLO_ACHIEVEMENT_UNLOCKED = createSignal(
74
+ "slo.achievement.unlocked",
75
+ z.object({
76
+ systemId: z.string(),
77
+ achievement: z.string(),
78
+ }),
79
+ );
@@ -0,0 +1,9 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the SLO plugin.
5
+ * Exported from the common package so both backend and frontend can reference it.
6
+ */
7
+ export const pluginMetadata = definePluginMetadata({
8
+ pluginId: "slo",
9
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { createRoutes } from "@checkstack/common";
2
+
3
+ /**
4
+ * Route definitions for the SLO plugin.
5
+ *
6
+ * @example Frontend plugin usage
7
+ * ```tsx
8
+ * import { sloRoutes } from "@checkstack/slo-common";
9
+ *
10
+ * createFrontendPlugin({
11
+ * routes: [
12
+ * { route: sloRoutes.routes.overview, element: <SloOverviewPage /> },
13
+ * ],
14
+ * });
15
+ * ```
16
+ *
17
+ * @example Link generation
18
+ * ```tsx
19
+ * import { sloRoutes } from "@checkstack/slo-common";
20
+ * import { resolveRoute } from "@checkstack/common";
21
+ *
22
+ * const detailPath = resolveRoute(sloRoutes.routes.detail, { sloId });
23
+ * ```
24
+ */
25
+ export const sloRoutes = createRoutes("slo", {
26
+ overview: "/",
27
+ config: "/config",
28
+ configDetail: "/config/:sloId",
29
+ detail: "/:sloId",
30
+ });
@@ -0,0 +1,194 @@
1
+ import { z } from "zod";
2
+ import { createClientDefinition, proc } from "@checkstack/common";
3
+ import { sloAccess } from "./access";
4
+ import { pluginMetadata } from "./plugin-metadata";
5
+ import {
6
+ SloObjectiveSchema,
7
+ SloStatusSchema,
8
+ SloDowntimeEventSchema,
9
+ SloDailySnapshotSchema,
10
+ SloStreakSchema,
11
+ SloAchievementSchema,
12
+ CreateSloObjectiveInputSchema,
13
+ UpdateSloObjectiveInputSchema,
14
+ } from "./schemas";
15
+
16
+ export const sloContract = {
17
+ // ==========================================================================
18
+ // SLO OBJECTIVE MANAGEMENT
19
+ // ==========================================================================
20
+
21
+ /** List all SLO objectives with their computed status */
22
+ listObjectives: proc({
23
+ operationType: "query",
24
+ userType: "public",
25
+ access: [sloAccess.slo.read],
26
+ }).output(
27
+ z.object({
28
+ objectives: z.array(
29
+ z.object({
30
+ objective: SloObjectiveSchema,
31
+ status: SloStatusSchema,
32
+ }),
33
+ ),
34
+ }),
35
+ ),
36
+
37
+ /** Get a single SLO objective with full status and attribution */
38
+ getObjective: proc({
39
+ operationType: "query",
40
+ userType: "public",
41
+ access: [sloAccess.slo.read],
42
+ })
43
+ .input(z.object({ id: z.string() }))
44
+ .output(
45
+ z
46
+ .object({
47
+ objective: SloObjectiveSchema,
48
+ status: SloStatusSchema,
49
+ })
50
+ .nullable(),
51
+ ),
52
+
53
+ /** Get SLOs for a specific system (for badge display) */
54
+ getObjectivesForSystem: proc({
55
+ operationType: "query",
56
+ userType: "public",
57
+ access: [sloAccess.slo.read],
58
+ })
59
+ .input(z.object({ systemId: z.string() }))
60
+ .output(
61
+ z.array(
62
+ z.object({
63
+ objective: SloObjectiveSchema,
64
+ status: SloStatusSchema,
65
+ }),
66
+ ),
67
+ ),
68
+
69
+ /** Bulk fetch SLO statuses for multiple systems (dashboard, avoid N+1) */
70
+ getBulkObjectivesForSystems: proc({
71
+ operationType: "query",
72
+ userType: "public",
73
+ access: [sloAccess.slo.read],
74
+ instanceAccess: { recordKey: "systems" },
75
+ })
76
+ .input(z.object({ systemIds: z.array(z.string()) }))
77
+ .output(
78
+ z.object({
79
+ systems: z.record(
80
+ z.string(),
81
+ z.array(
82
+ z.object({
83
+ objective: SloObjectiveSchema,
84
+ status: SloStatusSchema,
85
+ }),
86
+ ),
87
+ ),
88
+ }),
89
+ ),
90
+
91
+ /** Create a new SLO objective */
92
+ createObjective: proc({
93
+ operationType: "mutation",
94
+ userType: "authenticated",
95
+ access: [sloAccess.slo.manage],
96
+ })
97
+ .input(CreateSloObjectiveInputSchema)
98
+ .output(SloObjectiveSchema),
99
+
100
+ /** Update an existing SLO objective */
101
+ updateObjective: proc({
102
+ operationType: "mutation",
103
+ userType: "authenticated",
104
+ access: [sloAccess.slo.manage],
105
+ })
106
+ .input(UpdateSloObjectiveInputSchema)
107
+ .output(SloObjectiveSchema),
108
+
109
+ /** Delete an SLO objective and all associated data */
110
+ deleteObjective: proc({
111
+ operationType: "mutation",
112
+ userType: "authenticated",
113
+ access: [sloAccess.slo.manage],
114
+ })
115
+ .input(z.object({ id: z.string() }))
116
+ .output(z.object({ success: z.boolean() })),
117
+
118
+ // ==========================================================================
119
+ // DOWNTIME EVENTS & DAILY SNAPSHOTS
120
+ // ==========================================================================
121
+
122
+ /** Get recent downtime events for an SLO objective */
123
+ getDowntimeEvents: proc({
124
+ operationType: "query",
125
+ userType: "public",
126
+ access: [sloAccess.slo.read],
127
+ })
128
+ .input(
129
+ z.object({
130
+ objectiveId: z.string(),
131
+ limit: z.number().optional().default(50),
132
+ }),
133
+ )
134
+ .output(z.object({ events: z.array(SloDowntimeEventSchema) })),
135
+
136
+ /** Get daily SLO trend data for a time range */
137
+ getDailySnapshots: proc({
138
+ operationType: "query",
139
+ userType: "public",
140
+ access: [sloAccess.slo.read],
141
+ })
142
+ .input(
143
+ z.object({
144
+ objectiveId: z.string(),
145
+ startDate: z.date(),
146
+ endDate: z.date(),
147
+ }),
148
+ )
149
+ .output(z.object({ snapshots: z.array(SloDailySnapshotSchema) })),
150
+
151
+ // ==========================================================================
152
+ // STREAKS & ACHIEVEMENTS
153
+ // ==========================================================================
154
+
155
+ /** Get streaks for all SLO objectives */
156
+ getStreaks: proc({
157
+ operationType: "query",
158
+ userType: "public",
159
+ access: [sloAccess.slo.read],
160
+ }).output(z.object({ streaks: z.array(SloStreakSchema) })),
161
+
162
+ /** Get achievements for a specific system */
163
+ getAchievements: proc({
164
+ operationType: "query",
165
+ userType: "public",
166
+ access: [sloAccess.slo.read],
167
+ })
168
+ .input(z.object({ systemId: z.string() }))
169
+ .output(z.object({ achievements: z.array(SloAchievementSchema) })),
170
+
171
+ /** Get recent milestones across all systems (for milestone feed) */
172
+ getRecentMilestones: proc({
173
+ operationType: "query",
174
+ userType: "public",
175
+ access: [sloAccess.slo.read],
176
+ })
177
+ .input(z.object({ limit: z.number().optional().default(20) }))
178
+ .output(
179
+ z.object({
180
+ milestones: z.array(
181
+ z.object({
182
+ systemId: z.string(),
183
+ systemName: z.string().optional(),
184
+ achievement: z.string(),
185
+ unlockedAt: z.date(),
186
+ }),
187
+ ),
188
+ }),
189
+ ),
190
+ };
191
+
192
+ export type SloContract = typeof sloContract;
193
+
194
+ export const SloApi = createClientDefinition(sloContract, pluginMetadata);
package/src/schemas.ts ADDED
@@ -0,0 +1,225 @@
1
+ import { z } from "zod";
2
+
3
+ // =============================================================================
4
+ // ENUMS
5
+ // =============================================================================
6
+
7
+ /**
8
+ * Dependency exclusion mode for SLO error budget calculation.
9
+ * - strict: Count ALL downtime regardless of cause
10
+ * - self-only: Only count self-caused downtime (upstream-attributed downtime is excluded)
11
+ */
12
+ export const DependencyExclusionModeSchema = z.enum([
13
+ "strict",
14
+ "self-only",
15
+ ]);
16
+ export type DependencyExclusionMode = z.infer<
17
+ typeof DependencyExclusionModeSchema
18
+ >;
19
+
20
+ /**
21
+ * Attribution type for a downtime event.
22
+ * - self: Downtime is caused by the system itself
23
+ * - upstream: Downtime is attributed to an upstream dependency failure
24
+ */
25
+ export const AttributionTypeSchema = z.enum(["self", "upstream"]);
26
+ export type AttributionType = z.infer<typeof AttributionTypeSchema>;
27
+
28
+ /**
29
+ * Achievement types that can be unlocked by systems.
30
+ */
31
+ export const AchievementTypeSchema = z.enum([
32
+ "first_steps",
33
+ "iron_uptime",
34
+ "diamond_uptime",
35
+ "budget_miser",
36
+ "clean_sheet",
37
+ "nines_club",
38
+ "cascade_breaker",
39
+ "full_coverage",
40
+ "rapid_recovery",
41
+ ]);
42
+ export type AchievementType = z.infer<typeof AchievementTypeSchema>;
43
+
44
+ // =============================================================================
45
+ // CONFIGURABLE THRESHOLDS
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Burn rate alert thresholds, configurable per SLO objective.
50
+ */
51
+ export const BurnRateThresholdsSchema = z.object({
52
+ warningPercent: z.number().min(0).max(100).default(50),
53
+ criticalPercent: z.number().min(0).max(100).default(80),
54
+ fastBurnMultiplier: z.number().min(1).default(5),
55
+ });
56
+ export type BurnRateThresholds = z.infer<typeof BurnRateThresholdsSchema>;
57
+
58
+ // =============================================================================
59
+ // CORE ENTITIES
60
+ // =============================================================================
61
+
62
+ /**
63
+ * SLO objective definition.
64
+ * Represents a reliability target for a system (or a specific health check on a system).
65
+ * Multiple SLOs can be defined per system. When healthCheckConfigurationId is null,
66
+ * the SLO applies to the system's aggregate availability across all health checks.
67
+ */
68
+ export const SloObjectiveSchema = z.object({
69
+ id: z.string(),
70
+ systemId: z.string(),
71
+ healthCheckConfigurationId: z.string().nullable(),
72
+ target: z.number().min(0).max(100),
73
+ windowDays: z.number().int().positive(),
74
+ dependencyExclusion: DependencyExclusionModeSchema,
75
+ excludedDependencyIds: z.array(z.string()).optional(),
76
+ burnRateThresholds: BurnRateThresholdsSchema,
77
+ createdAt: z.date(),
78
+ updatedAt: z.date(),
79
+ });
80
+ export type SloObjective = z.infer<typeof SloObjectiveSchema>;
81
+
82
+ /**
83
+ * Event-sourced downtime record, written in real-time on SYSTEM_STATUS_CHANGED.
84
+ * Events are immutable after creation — when attribution changes mid-outage,
85
+ * the open event is closed and a new one starts (event splitting).
86
+ */
87
+ export const SloDowntimeEventSchema = z.object({
88
+ id: z.string(),
89
+ objectiveId: z.string(),
90
+ systemId: z.string(),
91
+ startTime: z.date(),
92
+ endTime: z.date().nullable(),
93
+ durationSeconds: z.number().nullable(),
94
+ attributionType: AttributionTypeSchema,
95
+ upstreamSystemId: z.string().nullable(),
96
+ upstreamSystemName: z.string().nullable(),
97
+ });
98
+ export type SloDowntimeEvent = z.infer<typeof SloDowntimeEventSchema>;
99
+
100
+ /**
101
+ * Daily SLO snapshot for trend charts.
102
+ * Persisted by a daily cron job at UTC midnight.
103
+ */
104
+ export const SloDailySnapshotSchema = z.object({
105
+ id: z.string(),
106
+ objectiveId: z.string(),
107
+ date: z.date(),
108
+ availabilityPercent: z.number(),
109
+ budgetConsumedMinutes: z.number(),
110
+ budgetRemainingPercent: z.number(),
111
+ burnRate: z.number().nullable(),
112
+ streakDays: z.number(),
113
+ });
114
+ export type SloDailySnapshot = z.infer<typeof SloDailySnapshotSchema>;
115
+
116
+ /**
117
+ * Streak tracking for an SLO objective.
118
+ * One streak record per objective (1:1).
119
+ */
120
+ export const SloStreakSchema = z.object({
121
+ objectiveId: z.string(),
122
+ systemId: z.string(),
123
+ currentStreak: z.number(),
124
+ bestStreak: z.number(),
125
+ streakStart: z.date().nullable(),
126
+ bestStreakEnd: z.date().nullable(),
127
+ });
128
+ export type SloStreak = z.infer<typeof SloStreakSchema>;
129
+
130
+ /**
131
+ * Achievement unlocked by a system.
132
+ * System-centric only — no user attribution.
133
+ */
134
+ export const SloAchievementSchema = z.object({
135
+ id: z.string(),
136
+ systemId: z.string(),
137
+ achievement: AchievementTypeSchema,
138
+ unlockedAt: z.date(),
139
+ });
140
+ export type SloAchievement = z.infer<typeof SloAchievementSchema>;
141
+
142
+ // =============================================================================
143
+ // COMPUTED STATUS (returned by API, not persisted)
144
+ // =============================================================================
145
+
146
+ /**
147
+ * Attribution breakdown entry for a single source (self or an upstream dependency).
148
+ */
149
+ export const SloAttributionEntrySchema = z.object({
150
+ sourceType: AttributionTypeSchema,
151
+ systemId: z.string().optional(),
152
+ systemName: z.string().optional(),
153
+ minutes: z.number(),
154
+ });
155
+ export type SloAttributionEntry = z.infer<typeof SloAttributionEntrySchema>;
156
+
157
+ /**
158
+ * Computed SLO status for display, aggregated from downtime events.
159
+ * Returned by the API, not persisted directly.
160
+ */
161
+ export const SloStatusSchema = z.object({
162
+ objectiveId: z.string(),
163
+ systemId: z.string(),
164
+ target: z.number(),
165
+ windowDays: z.number(),
166
+ healthCheckConfigurationId: z.string().nullable(),
167
+ healthCheckConfigurationName: z.string().nullable(),
168
+ currentAvailability: z.number().nullable(),
169
+ strictAvailability: z.number().nullable(),
170
+ errorBudgetTotalMinutes: z.number(),
171
+ errorBudgetConsumedMinutes: z.number(),
172
+ errorBudgetConsumedStrictMinutes: z.number(),
173
+ errorBudgetRemainingMinutes: z.number(),
174
+ errorBudgetRemainingPercent: z.number(),
175
+ burnRate: z.number().nullable(),
176
+ dependencyExclusion: DependencyExclusionModeSchema,
177
+ isBreaching: z.boolean(),
178
+ hasOpenDowntime: z.boolean(),
179
+ attribution: z.array(SloAttributionEntrySchema),
180
+ });
181
+ export type SloStatus = z.infer<typeof SloStatusSchema>;
182
+
183
+ // =============================================================================
184
+ // INPUT SCHEMAS
185
+ // =============================================================================
186
+
187
+ /**
188
+ * Input for creating a new SLO objective.
189
+ */
190
+ export const CreateSloObjectiveInputSchema = z.object({
191
+ systemId: z.string().min(1, "System is required"),
192
+ healthCheckConfigurationId: z.string().optional(),
193
+ target: z
194
+ .number()
195
+ .min(0, "Target must be >= 0")
196
+ .max(100, "Target must be <= 100"),
197
+ windowDays: z.number().int().positive("Window must be at least 1 day"),
198
+ dependencyExclusion: DependencyExclusionModeSchema.optional().default(
199
+ "strict",
200
+ ),
201
+ excludedDependencyIds: z.array(z.string()).optional().default([]),
202
+ burnRateThresholds: BurnRateThresholdsSchema.optional().default({
203
+ warningPercent: 50,
204
+ criticalPercent: 80,
205
+ fastBurnMultiplier: 5,
206
+ }),
207
+ });
208
+ export type CreateSloObjectiveInput = z.infer<
209
+ typeof CreateSloObjectiveInputSchema
210
+ >;
211
+
212
+ /**
213
+ * Input for updating an existing SLO objective.
214
+ */
215
+ export const UpdateSloObjectiveInputSchema = z.object({
216
+ id: z.string(),
217
+ target: z.number().min(0).max(100).optional(),
218
+ windowDays: z.number().int().positive().optional(),
219
+ dependencyExclusion: DependencyExclusionModeSchema.optional(),
220
+ excludedDependencyIds: z.array(z.string()).optional(),
221
+ burnRateThresholds: BurnRateThresholdsSchema.optional(),
222
+ });
223
+ export type UpdateSloObjectiveInput = z.infer<
224
+ typeof UpdateSloObjectiveInputSchema
225
+ >;
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/common.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }