@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 +36 -0
- package/drizzle/0001_soft_gamma_corps.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +220 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -14
- package/src/router.ts +18 -10
- package/src/schema.ts +5 -1
- package/src/service.ts +33 -0
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/incident-backend",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
14
|
-
"@checkstack/incident-common": "
|
|
15
|
-
"@checkstack/catalog-common": "
|
|
16
|
-
"@checkstack/catalog-backend": "
|
|
17
|
-
"@checkstack/command-backend": "
|
|
18
|
-
"@checkstack/signal-common": "
|
|
19
|
-
"@checkstack/integration-backend": "
|
|
20
|
-
"@checkstack/integration-common": "
|
|
21
|
-
"@checkstack/common": "
|
|
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": "
|
|
27
|
-
"@checkstack/scripts": "
|
|
28
|
-
"@checkstack/test-utils-backend": "
|
|
29
|
-
"@checkstack/tsconfig": "
|
|
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
|
-
|
|
48
|
-
|
|
47
|
+
? "resolved"
|
|
48
|
+
: "updated";
|
|
49
49
|
|
|
50
50
|
const importance =
|
|
51
51
|
severity === "critical"
|
|
52
52
|
? "critical"
|
|
53
53
|
: severity === "major"
|
|
54
|
-
|
|
55
|
-
|
|
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
|
}
|