@checkstack/incident-backend 0.2.8 → 0.3.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,41 @@
1
1
  # @checkstack/incident-backend
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0b9fc58: Fix workspace:\* protocol resolution in published packages
8
+
9
+ Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
10
+
11
+ - Updated dependencies [0b9fc58]
12
+ - @checkstack/backend-api@0.5.2
13
+ - @checkstack/catalog-backend@0.2.9
14
+ - @checkstack/catalog-common@1.2.5
15
+ - @checkstack/command-backend@0.1.8
16
+ - @checkstack/common@0.6.1
17
+ - @checkstack/incident-common@0.4.1
18
+ - @checkstack/integration-backend@0.1.8
19
+ - @checkstack/integration-common@0.2.4
20
+ - @checkstack/signal-common@0.1.5
21
+
22
+ ## 0.3.0
23
+
24
+ ### Minor Changes
25
+
26
+ - cce5453: Add notification suppression for incidents
27
+
28
+ - Added `suppressNotifications` field to incidents, allowing active incidents to optionally suppress health check notifications
29
+ - When enabled, health status change notifications will not be sent for affected systems while the incident is active (not resolved)
30
+ - Mirrors the existing maintenance notification suppression pattern
31
+ - Added toggle UI in the IncidentEditor dialog
32
+ - Added `hasActiveIncidentWithSuppression` RPC endpoint for service-to-service queries
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies [cce5453]
37
+ - @checkstack/incident-common@0.4.0
38
+
3
39
  ## 0.2.8
4
40
 
5
41
  ### Patch Changes
@@ -0,0 +1 @@
1
+ ALTER TABLE "incidents" ADD COLUMN "suppress_notifications" boolean DEFAULT false NOT NULL;
@@ -0,0 +1,220 @@
1
+ {
2
+ "id": "e53ec0d1-e4c4-45cb-b45d-7ac3c1317744",
3
+ "prevId": "b27ece12-64f5-454b-aef9-e0f95aeda850",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.incident_systems": {
8
+ "name": "incident_systems",
9
+ "schema": "",
10
+ "columns": {
11
+ "incident_id": {
12
+ "name": "incident_id",
13
+ "type": "text",
14
+ "primaryKey": false,
15
+ "notNull": true
16
+ },
17
+ "system_id": {
18
+ "name": "system_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ }
23
+ },
24
+ "indexes": {},
25
+ "foreignKeys": {
26
+ "incident_systems_incident_id_incidents_id_fk": {
27
+ "name": "incident_systems_incident_id_incidents_id_fk",
28
+ "tableFrom": "incident_systems",
29
+ "tableTo": "incidents",
30
+ "columnsFrom": [
31
+ "incident_id"
32
+ ],
33
+ "columnsTo": [
34
+ "id"
35
+ ],
36
+ "onDelete": "cascade",
37
+ "onUpdate": "no action"
38
+ }
39
+ },
40
+ "compositePrimaryKeys": {
41
+ "incident_systems_incident_id_system_id_pk": {
42
+ "name": "incident_systems_incident_id_system_id_pk",
43
+ "columns": [
44
+ "incident_id",
45
+ "system_id"
46
+ ]
47
+ }
48
+ },
49
+ "uniqueConstraints": {},
50
+ "policies": {},
51
+ "checkConstraints": {},
52
+ "isRLSEnabled": false
53
+ },
54
+ "public.incident_updates": {
55
+ "name": "incident_updates",
56
+ "schema": "",
57
+ "columns": {
58
+ "id": {
59
+ "name": "id",
60
+ "type": "text",
61
+ "primaryKey": true,
62
+ "notNull": true
63
+ },
64
+ "incident_id": {
65
+ "name": "incident_id",
66
+ "type": "text",
67
+ "primaryKey": false,
68
+ "notNull": true
69
+ },
70
+ "message": {
71
+ "name": "message",
72
+ "type": "text",
73
+ "primaryKey": false,
74
+ "notNull": true
75
+ },
76
+ "status_change": {
77
+ "name": "status_change",
78
+ "type": "incident_status",
79
+ "typeSchema": "public",
80
+ "primaryKey": false,
81
+ "notNull": false
82
+ },
83
+ "created_at": {
84
+ "name": "created_at",
85
+ "type": "timestamp",
86
+ "primaryKey": false,
87
+ "notNull": true,
88
+ "default": "now()"
89
+ },
90
+ "created_by": {
91
+ "name": "created_by",
92
+ "type": "text",
93
+ "primaryKey": false,
94
+ "notNull": false
95
+ }
96
+ },
97
+ "indexes": {},
98
+ "foreignKeys": {
99
+ "incident_updates_incident_id_incidents_id_fk": {
100
+ "name": "incident_updates_incident_id_incidents_id_fk",
101
+ "tableFrom": "incident_updates",
102
+ "tableTo": "incidents",
103
+ "columnsFrom": [
104
+ "incident_id"
105
+ ],
106
+ "columnsTo": [
107
+ "id"
108
+ ],
109
+ "onDelete": "cascade",
110
+ "onUpdate": "no action"
111
+ }
112
+ },
113
+ "compositePrimaryKeys": {},
114
+ "uniqueConstraints": {},
115
+ "policies": {},
116
+ "checkConstraints": {},
117
+ "isRLSEnabled": false
118
+ },
119
+ "public.incidents": {
120
+ "name": "incidents",
121
+ "schema": "",
122
+ "columns": {
123
+ "id": {
124
+ "name": "id",
125
+ "type": "text",
126
+ "primaryKey": true,
127
+ "notNull": true
128
+ },
129
+ "title": {
130
+ "name": "title",
131
+ "type": "text",
132
+ "primaryKey": false,
133
+ "notNull": true
134
+ },
135
+ "description": {
136
+ "name": "description",
137
+ "type": "text",
138
+ "primaryKey": false,
139
+ "notNull": false
140
+ },
141
+ "status": {
142
+ "name": "status",
143
+ "type": "incident_status",
144
+ "typeSchema": "public",
145
+ "primaryKey": false,
146
+ "notNull": true,
147
+ "default": "'investigating'"
148
+ },
149
+ "severity": {
150
+ "name": "severity",
151
+ "type": "incident_severity",
152
+ "typeSchema": "public",
153
+ "primaryKey": false,
154
+ "notNull": true,
155
+ "default": "'major'"
156
+ },
157
+ "suppress_notifications": {
158
+ "name": "suppress_notifications",
159
+ "type": "boolean",
160
+ "primaryKey": false,
161
+ "notNull": true,
162
+ "default": false
163
+ },
164
+ "created_at": {
165
+ "name": "created_at",
166
+ "type": "timestamp",
167
+ "primaryKey": false,
168
+ "notNull": true,
169
+ "default": "now()"
170
+ },
171
+ "updated_at": {
172
+ "name": "updated_at",
173
+ "type": "timestamp",
174
+ "primaryKey": false,
175
+ "notNull": true,
176
+ "default": "now()"
177
+ }
178
+ },
179
+ "indexes": {},
180
+ "foreignKeys": {},
181
+ "compositePrimaryKeys": {},
182
+ "uniqueConstraints": {},
183
+ "policies": {},
184
+ "checkConstraints": {},
185
+ "isRLSEnabled": false
186
+ }
187
+ },
188
+ "enums": {
189
+ "public.incident_severity": {
190
+ "name": "incident_severity",
191
+ "schema": "public",
192
+ "values": [
193
+ "minor",
194
+ "major",
195
+ "critical"
196
+ ]
197
+ },
198
+ "public.incident_status": {
199
+ "name": "incident_status",
200
+ "schema": "public",
201
+ "values": [
202
+ "investigating",
203
+ "identified",
204
+ "fixing",
205
+ "monitoring",
206
+ "resolved"
207
+ ]
208
+ }
209
+ },
210
+ "schemas": {},
211
+ "sequences": {},
212
+ "roles": {},
213
+ "policies": {},
214
+ "views": {},
215
+ "_meta": {
216
+ "columns": {},
217
+ "schemas": {},
218
+ "tables": {}
219
+ }
220
+ }
@@ -8,6 +8,13 @@
8
8
  "when": 1767395825368,
9
9
  "tag": "0000_sticky_unus",
10
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1768933110925,
16
+ "tag": "0001_soft_gamma_corps",
17
+ "breakpoints": true
11
18
  }
12
19
  ]
13
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/incident-backend",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -10,23 +10,23 @@
10
10
  "lint:code": "eslint . --max-warnings 0"
11
11
  },
12
12
  "dependencies": {
13
- "@checkstack/backend-api": "workspace:*",
14
- "@checkstack/incident-common": "workspace:*",
15
- "@checkstack/catalog-common": "workspace:*",
16
- "@checkstack/catalog-backend": "workspace:*",
17
- "@checkstack/command-backend": "workspace:*",
18
- "@checkstack/signal-common": "workspace:*",
19
- "@checkstack/integration-backend": "workspace:*",
20
- "@checkstack/integration-common": "workspace:*",
21
- "@checkstack/common": "workspace:*",
13
+ "@checkstack/backend-api": "0.5.1",
14
+ "@checkstack/incident-common": "0.4.0",
15
+ "@checkstack/catalog-common": "1.2.4",
16
+ "@checkstack/catalog-backend": "0.2.8",
17
+ "@checkstack/command-backend": "0.1.7",
18
+ "@checkstack/signal-common": "0.1.4",
19
+ "@checkstack/integration-backend": "0.1.7",
20
+ "@checkstack/integration-common": "0.2.3",
21
+ "@checkstack/common": "0.6.0",
22
22
  "drizzle-orm": "^0.45.1",
23
23
  "zod": "^4.2.1"
24
24
  },
25
25
  "devDependencies": {
26
- "@checkstack/drizzle-helper": "workspace:*",
27
- "@checkstack/scripts": "workspace:*",
28
- "@checkstack/test-utils-backend": "workspace:*",
29
- "@checkstack/tsconfig": "workspace:*",
26
+ "@checkstack/drizzle-helper": "0.0.2",
27
+ "@checkstack/scripts": "0.1.0",
28
+ "@checkstack/test-utils-backend": "0.1.7",
29
+ "@checkstack/tsconfig": "0.0.2",
30
30
  "@orpc/server": "^1.13.2",
31
31
  "@types/bun": "^1.0.0",
32
32
  "drizzle-kit": "^0.31.8",
package/src/router.ts CHANGED
@@ -20,7 +20,7 @@ export function createRouter(
20
20
  service: IncidentService,
21
21
  signalService: SignalService,
22
22
  catalogClient: InferClient<typeof CatalogApi>,
23
- logger: Logger
23
+ logger: Logger,
24
24
  ) {
25
25
  const os = implement(incidentContract)
26
26
  .$context<RpcContext>()
@@ -44,15 +44,15 @@ export function createRouter(
44
44
  action === "created"
45
45
  ? "reported"
46
46
  : action === "resolved"
47
- ? "resolved"
48
- : "updated";
47
+ ? "resolved"
48
+ : "updated";
49
49
 
50
50
  const importance =
51
51
  severity === "critical"
52
52
  ? "critical"
53
53
  : severity === "major"
54
- ? "warning"
55
- : "info";
54
+ ? "warning"
55
+ : "info";
56
56
 
57
57
  const incidentDetailPath = resolveRoute(incidentRoutes.routes.detail, {
58
58
  incidentId,
@@ -75,7 +75,7 @@ export function createRouter(
75
75
  // Log but don't fail the operation - notifications are best-effort
76
76
  logger.warn(
77
77
  `Failed to notify subscribers for system ${systemId}:`,
78
- error
78
+ error,
79
79
  );
80
80
  }
81
81
  }
@@ -95,7 +95,7 @@ export function createRouter(
95
95
  getIncidentsForSystem: os.getIncidentsForSystem.handler(
96
96
  async ({ input }) => {
97
97
  return service.getIncidentsForSystem(input.systemId);
98
- }
98
+ },
99
99
  ),
100
100
 
101
101
  getBulkIncidentsForSystems: os.getBulkIncidentsForSystems.handler(
@@ -109,11 +109,11 @@ export function createRouter(
109
109
  await Promise.all(
110
110
  input.systemIds.map(async (systemId) => {
111
111
  incidents[systemId] = await service.getIncidentsForSystem(systemId);
112
- })
112
+ }),
113
113
  );
114
114
 
115
115
  return { incidents };
116
- }
116
+ },
117
117
  ),
118
118
 
119
119
  createIncident: os.createIncident.handler(async ({ input, context }) => {
@@ -232,7 +232,7 @@ export function createRouter(
232
232
  const result = await service.resolveIncident(
233
233
  input.id,
234
234
  input.message,
235
- userId
235
+ userId,
236
236
  );
237
237
  if (!result) {
238
238
  throw new ORPCError("NOT_FOUND", { message: "Incident not found" });
@@ -279,5 +279,13 @@ export function createRouter(
279
279
  }
280
280
  return { success };
281
281
  }),
282
+
283
+ hasActiveIncidentWithSuppression:
284
+ os.hasActiveIncidentWithSuppression.handler(async ({ input }) => {
285
+ const suppressed = await service.hasActiveIncidentWithSuppression(
286
+ input.systemId,
287
+ );
288
+ return { suppressed };
289
+ }),
282
290
  });
283
291
  }
package/src/schema.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  text,
5
5
  timestamp,
6
6
  primaryKey,
7
+ boolean,
7
8
  } from "drizzle-orm/pg-core";
8
9
 
9
10
  /**
@@ -35,6 +36,9 @@ export const incidents = pgTable("incidents", {
35
36
  description: text("description"),
36
37
  status: incidentStatusEnum("status").notNull().default("investigating"),
37
38
  severity: incidentSeverityEnum("severity").notNull().default("major"),
39
+ suppressNotifications: boolean("suppress_notifications")
40
+ .default(false)
41
+ .notNull(),
38
42
  createdAt: timestamp("created_at").defaultNow().notNull(),
39
43
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
40
44
  });
@@ -52,7 +56,7 @@ export const incidentSystems = pgTable(
52
56
  },
53
57
  (t) => ({
54
58
  pk: primaryKey(t.incidentId, t.systemId),
55
- })
59
+ }),
56
60
  );
57
61
 
58
62
  /**
package/src/service.ts CHANGED
@@ -166,6 +166,7 @@ export class IncidentService {
166
166
  description: input.description,
167
167
  status: "investigating",
168
168
  severity: input.severity,
169
+ suppressNotifications: input.suppressNotifications ?? false,
169
170
  });
170
171
 
171
172
  // Insert system associations
@@ -211,6 +212,8 @@ export class IncidentService {
211
212
  if (input.description !== undefined)
212
213
  updateData.description = input.description;
213
214
  if (input.severity !== undefined) updateData.severity = input.severity;
215
+ if (input.suppressNotifications !== undefined)
216
+ updateData.suppressNotifications = input.suppressNotifications;
214
217
 
215
218
  await this.db
216
219
  .update(incidents)
@@ -328,4 +331,34 @@ export class IncidentService {
328
331
  .delete(incidentSystems)
329
332
  .where(eq(incidentSystems.systemId, systemId));
330
333
  }
334
+
335
+ /**
336
+ * Check if a system has an active incident with notification suppression enabled.
337
+ * An incident is considered "active" if its status is NOT "resolved".
338
+ */
339
+ async hasActiveIncidentWithSuppression(systemId: string): Promise<boolean> {
340
+ // Get incident IDs for this system
341
+ const systemIncidents = await this.db
342
+ .select({ incidentId: incidentSystems.incidentId })
343
+ .from(incidentSystems)
344
+ .where(eq(incidentSystems.systemId, systemId));
345
+
346
+ const ids = systemIncidents.map((r) => r.incidentId);
347
+ if (ids.length === 0) return false;
348
+
349
+ // Check if any of these incidents are active (not resolved) with suppressNotifications enabled
350
+ const [match] = await this.db
351
+ .select({ id: incidents.id })
352
+ .from(incidents)
353
+ .where(
354
+ and(
355
+ inArray(incidents.id, ids),
356
+ ne(incidents.status, "resolved"),
357
+ eq(incidents.suppressNotifications, true),
358
+ ),
359
+ )
360
+ .limit(1);
361
+
362
+ return !!match;
363
+ }
331
364
  }