@checkstack/incident-backend 1.12.0 → 1.13.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,93 @@
1
1
  # @checkstack/incident-backend
2
2
 
3
+ ## 1.13.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [1f20b5a]
8
+ - Updated dependencies [5e704cd]
9
+ - @checkstack/ai-backend@0.10.12
10
+ - @checkstack/automation-backend@0.11.3
11
+ - @checkstack/catalog-backend@1.8.1
12
+ - @checkstack/catalog-common@2.7.2
13
+ - @checkstack/incident-common@1.10.2
14
+ - @checkstack/status-page-common@0.6.2
15
+ - @checkstack/backend-api@0.32.1
16
+ - @checkstack/status-page-backend@0.6.1
17
+ - @checkstack/command-backend@0.2.24
18
+ - @checkstack/integration-backend@0.7.6
19
+
20
+ ## 1.13.0
21
+
22
+ ### Minor Changes
23
+
24
+ - bd41130: perf(incident): add indexes for reverse system lookup and update status derivation
25
+
26
+ Add two Postgres indexes to speed up hot read paths:
27
+
28
+ - `incident_systems_system_idx` on `incident_systems (system_id)` - the junction
29
+ primary key leads with `incident_id`, leaving the `system_id` direction
30
+ unindexed. This index serves the reverse lookup used by
31
+ `getIncidentsForSystem` / `getActiveHealthOverrides`, which fans out per
32
+ system on every status-page render.
33
+ - `incident_updates_incident_created_idx` on
34
+ `incident_updates (incident_id, created_at)` - serves the status-derivation
35
+ query (`WHERE incident_id, status_change IS NOT NULL ORDER BY created_at DESC
36
+ LIMIT 1`) and the bulk timeline fetch.
37
+
38
+ - bd41130: fix(status-page): scope email subscriptions to published environments and author-selected systems
39
+
40
+ Two correctness fixes to status-page email subscriptions:
41
+
42
+ - **Health notifications now respect the page's published environments.** A
43
+ per-environment health transition carries the environment it happened in
44
+ (`originEnvironmentId`, threaded through `notifyForSubscription` ->
45
+ `NotificationAudienceEvent` -> the status-page fan-out). A page that publishes
46
+ a specific environment set is now skipped for a change in an environment it
47
+ does not publish - so a `development` failure never emails a prod-only page's
48
+ subscribers, even for a system that is also shown in prod. Pages publishing all
49
+ environments, and env-less sources (incident, maintenance, whole-system health
50
+ rollup), are unaffected.
51
+ - **Notifications are scoped per category to the widgets the author placed.** The
52
+ send-time fan-out now surfaces a notification only through widgets of its own
53
+ category: a health status change reaches a page only through a HEALTH widget
54
+ (`banner` / `systemHealth` / `groupStatus` / `uptime`, which now implement
55
+ `resolveScopedSystems` and declare `subscriptionCategory: "health"`), an
56
+ incident only through an incident widget, and so on. A page that lists a
57
+ system's incidents but never its health no longer emails health subscribers
58
+ about it, and a health-only page now correctly surfaces its systems for
59
+ subscription. Health widgets also participate in the public subscribe picker.
60
+
61
+ BREAKING CHANGE: on a page publishing a specific environment set, health
62
+ subscribers now only receive changes that occurred in a published environment
63
+ (previously any environment of a surfaced system triggered a notification), and a
64
+ notification is surfaced only by a widget of its own category (previously any
65
+ scoping widget on the page could surface any category). Legacy subscribers (NULL
66
+ categories) and all-environment pages are unchanged; no data migration is needed.
67
+
68
+ ### Patch Changes
69
+
70
+ - Updated dependencies [bd41130]
71
+ - Updated dependencies [bd41130]
72
+ - Updated dependencies [bd41130]
73
+ - Updated dependencies [bd41130]
74
+ - Updated dependencies [bd41130]
75
+ - Updated dependencies [bd41130]
76
+ - Updated dependencies [bd41130]
77
+ - @checkstack/backend-api@0.32.0
78
+ - @checkstack/auth-common@0.14.0
79
+ - @checkstack/cache-utils@0.3.0
80
+ - @checkstack/catalog-backend@1.8.0
81
+ - @checkstack/ai-backend@0.10.11
82
+ - @checkstack/notification-common@1.7.0
83
+ - @checkstack/status-page-backend@0.6.0
84
+ - @checkstack/automation-backend@0.11.2
85
+ - @checkstack/command-backend@0.2.23
86
+ - @checkstack/integration-backend@0.7.5
87
+ - @checkstack/catalog-common@2.7.1
88
+ - @checkstack/incident-common@1.10.1
89
+ - @checkstack/status-page-common@0.6.1
90
+
3
91
  ## 1.12.0
4
92
 
5
93
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ CREATE INDEX "incident_systems_system_idx" ON "incident_systems" USING btree ("system_id");--> statement-breakpoint
2
+ CREATE INDEX "incident_updates_incident_created_idx" ON "incident_updates" USING btree ("incident_id","created_at");
@@ -0,0 +1,391 @@
1
+ {
2
+ "id": "a430b9d2-36ed-4c87-a41a-6f9e2a7517c7",
3
+ "prevId": "ff7f73e0-3d6b-47a4-b759-000739af4ee6",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.incident_links": {
8
+ "name": "incident_links",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "incident_id": {
18
+ "name": "incident_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "label": {
24
+ "name": "label",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": false
28
+ },
29
+ "url": {
30
+ "name": "url",
31
+ "type": "text",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "visibility": {
36
+ "name": "visibility",
37
+ "type": "incident_content_visibility",
38
+ "typeSchema": "public",
39
+ "primaryKey": false,
40
+ "notNull": true,
41
+ "default": "'public'"
42
+ },
43
+ "created_at": {
44
+ "name": "created_at",
45
+ "type": "timestamp",
46
+ "primaryKey": false,
47
+ "notNull": true,
48
+ "default": "now()"
49
+ }
50
+ },
51
+ "indexes": {
52
+ "incident_links_incident_url_unique": {
53
+ "name": "incident_links_incident_url_unique",
54
+ "columns": [
55
+ {
56
+ "expression": "incident_id",
57
+ "isExpression": false,
58
+ "asc": true,
59
+ "nulls": "last"
60
+ },
61
+ {
62
+ "expression": "url",
63
+ "isExpression": false,
64
+ "asc": true,
65
+ "nulls": "last"
66
+ }
67
+ ],
68
+ "isUnique": true,
69
+ "concurrently": false,
70
+ "method": "btree",
71
+ "with": {}
72
+ }
73
+ },
74
+ "foreignKeys": {
75
+ "incident_links_incident_id_incidents_id_fk": {
76
+ "name": "incident_links_incident_id_incidents_id_fk",
77
+ "tableFrom": "incident_links",
78
+ "tableTo": "incidents",
79
+ "columnsFrom": [
80
+ "incident_id"
81
+ ],
82
+ "columnsTo": [
83
+ "id"
84
+ ],
85
+ "onDelete": "cascade",
86
+ "onUpdate": "no action"
87
+ }
88
+ },
89
+ "compositePrimaryKeys": {},
90
+ "uniqueConstraints": {},
91
+ "policies": {},
92
+ "checkConstraints": {},
93
+ "isRLSEnabled": false
94
+ },
95
+ "public.incident_systems": {
96
+ "name": "incident_systems",
97
+ "schema": "",
98
+ "columns": {
99
+ "incident_id": {
100
+ "name": "incident_id",
101
+ "type": "text",
102
+ "primaryKey": false,
103
+ "notNull": true
104
+ },
105
+ "system_id": {
106
+ "name": "system_id",
107
+ "type": "text",
108
+ "primaryKey": false,
109
+ "notNull": true
110
+ }
111
+ },
112
+ "indexes": {
113
+ "incident_systems_system_idx": {
114
+ "name": "incident_systems_system_idx",
115
+ "columns": [
116
+ {
117
+ "expression": "system_id",
118
+ "isExpression": false,
119
+ "asc": true,
120
+ "nulls": "last"
121
+ }
122
+ ],
123
+ "isUnique": false,
124
+ "concurrently": false,
125
+ "method": "btree",
126
+ "with": {}
127
+ }
128
+ },
129
+ "foreignKeys": {
130
+ "incident_systems_incident_id_incidents_id_fk": {
131
+ "name": "incident_systems_incident_id_incidents_id_fk",
132
+ "tableFrom": "incident_systems",
133
+ "tableTo": "incidents",
134
+ "columnsFrom": [
135
+ "incident_id"
136
+ ],
137
+ "columnsTo": [
138
+ "id"
139
+ ],
140
+ "onDelete": "cascade",
141
+ "onUpdate": "no action"
142
+ }
143
+ },
144
+ "compositePrimaryKeys": {
145
+ "incident_systems_incident_id_system_id_pk": {
146
+ "name": "incident_systems_incident_id_system_id_pk",
147
+ "columns": [
148
+ "incident_id",
149
+ "system_id"
150
+ ]
151
+ }
152
+ },
153
+ "uniqueConstraints": {},
154
+ "policies": {},
155
+ "checkConstraints": {},
156
+ "isRLSEnabled": false
157
+ },
158
+ "public.incident_updates": {
159
+ "name": "incident_updates",
160
+ "schema": "",
161
+ "columns": {
162
+ "id": {
163
+ "name": "id",
164
+ "type": "text",
165
+ "primaryKey": true,
166
+ "notNull": true
167
+ },
168
+ "incident_id": {
169
+ "name": "incident_id",
170
+ "type": "text",
171
+ "primaryKey": false,
172
+ "notNull": true
173
+ },
174
+ "message": {
175
+ "name": "message",
176
+ "type": "text",
177
+ "primaryKey": false,
178
+ "notNull": true
179
+ },
180
+ "status_change": {
181
+ "name": "status_change",
182
+ "type": "incident_status",
183
+ "typeSchema": "public",
184
+ "primaryKey": false,
185
+ "notNull": false
186
+ },
187
+ "visibility": {
188
+ "name": "visibility",
189
+ "type": "incident_content_visibility",
190
+ "typeSchema": "public",
191
+ "primaryKey": false,
192
+ "notNull": true,
193
+ "default": "'public'"
194
+ },
195
+ "created_at": {
196
+ "name": "created_at",
197
+ "type": "timestamp",
198
+ "primaryKey": false,
199
+ "notNull": true,
200
+ "default": "now()"
201
+ },
202
+ "edited_at": {
203
+ "name": "edited_at",
204
+ "type": "timestamp",
205
+ "primaryKey": false,
206
+ "notNull": false
207
+ },
208
+ "edit_history": {
209
+ "name": "edit_history",
210
+ "type": "jsonb",
211
+ "primaryKey": false,
212
+ "notNull": true,
213
+ "default": "'[]'::jsonb"
214
+ },
215
+ "created_by": {
216
+ "name": "created_by",
217
+ "type": "text",
218
+ "primaryKey": false,
219
+ "notNull": false
220
+ }
221
+ },
222
+ "indexes": {
223
+ "incident_updates_incident_created_idx": {
224
+ "name": "incident_updates_incident_created_idx",
225
+ "columns": [
226
+ {
227
+ "expression": "incident_id",
228
+ "isExpression": false,
229
+ "asc": true,
230
+ "nulls": "last"
231
+ },
232
+ {
233
+ "expression": "created_at",
234
+ "isExpression": false,
235
+ "asc": true,
236
+ "nulls": "last"
237
+ }
238
+ ],
239
+ "isUnique": false,
240
+ "concurrently": false,
241
+ "method": "btree",
242
+ "with": {}
243
+ }
244
+ },
245
+ "foreignKeys": {
246
+ "incident_updates_incident_id_incidents_id_fk": {
247
+ "name": "incident_updates_incident_id_incidents_id_fk",
248
+ "tableFrom": "incident_updates",
249
+ "tableTo": "incidents",
250
+ "columnsFrom": [
251
+ "incident_id"
252
+ ],
253
+ "columnsTo": [
254
+ "id"
255
+ ],
256
+ "onDelete": "cascade",
257
+ "onUpdate": "no action"
258
+ }
259
+ },
260
+ "compositePrimaryKeys": {},
261
+ "uniqueConstraints": {},
262
+ "policies": {},
263
+ "checkConstraints": {},
264
+ "isRLSEnabled": false
265
+ },
266
+ "public.incidents": {
267
+ "name": "incidents",
268
+ "schema": "",
269
+ "columns": {
270
+ "id": {
271
+ "name": "id",
272
+ "type": "text",
273
+ "primaryKey": true,
274
+ "notNull": true
275
+ },
276
+ "title": {
277
+ "name": "title",
278
+ "type": "text",
279
+ "primaryKey": false,
280
+ "notNull": true
281
+ },
282
+ "description": {
283
+ "name": "description",
284
+ "type": "text",
285
+ "primaryKey": false,
286
+ "notNull": false
287
+ },
288
+ "status": {
289
+ "name": "status",
290
+ "type": "incident_status",
291
+ "typeSchema": "public",
292
+ "primaryKey": false,
293
+ "notNull": true,
294
+ "default": "'investigating'"
295
+ },
296
+ "severity": {
297
+ "name": "severity",
298
+ "type": "incident_severity",
299
+ "typeSchema": "public",
300
+ "primaryKey": false,
301
+ "notNull": true,
302
+ "default": "'major'"
303
+ },
304
+ "suppress_notifications": {
305
+ "name": "suppress_notifications",
306
+ "type": "boolean",
307
+ "primaryKey": false,
308
+ "notNull": true,
309
+ "default": false
310
+ },
311
+ "health_override": {
312
+ "name": "health_override",
313
+ "type": "incident_health_override",
314
+ "typeSchema": "public",
315
+ "primaryKey": false,
316
+ "notNull": false
317
+ },
318
+ "created_at": {
319
+ "name": "created_at",
320
+ "type": "timestamp",
321
+ "primaryKey": false,
322
+ "notNull": true,
323
+ "default": "now()"
324
+ },
325
+ "updated_at": {
326
+ "name": "updated_at",
327
+ "type": "timestamp",
328
+ "primaryKey": false,
329
+ "notNull": true,
330
+ "default": "now()"
331
+ }
332
+ },
333
+ "indexes": {},
334
+ "foreignKeys": {},
335
+ "compositePrimaryKeys": {},
336
+ "uniqueConstraints": {},
337
+ "policies": {},
338
+ "checkConstraints": {},
339
+ "isRLSEnabled": false
340
+ }
341
+ },
342
+ "enums": {
343
+ "public.incident_health_override": {
344
+ "name": "incident_health_override",
345
+ "schema": "public",
346
+ "values": [
347
+ "degraded",
348
+ "unhealthy"
349
+ ]
350
+ },
351
+ "public.incident_severity": {
352
+ "name": "incident_severity",
353
+ "schema": "public",
354
+ "values": [
355
+ "minor",
356
+ "major",
357
+ "critical"
358
+ ]
359
+ },
360
+ "public.incident_status": {
361
+ "name": "incident_status",
362
+ "schema": "public",
363
+ "values": [
364
+ "investigating",
365
+ "identified",
366
+ "fixing",
367
+ "monitoring",
368
+ "resolved"
369
+ ]
370
+ },
371
+ "public.incident_content_visibility": {
372
+ "name": "incident_content_visibility",
373
+ "schema": "public",
374
+ "values": [
375
+ "public",
376
+ "logged_in",
377
+ "internal"
378
+ ]
379
+ }
380
+ },
381
+ "schemas": {},
382
+ "sequences": {},
383
+ "roles": {},
384
+ "policies": {},
385
+ "views": {},
386
+ "_meta": {
387
+ "columns": {},
388
+ "schemas": {},
389
+ "tables": {}
390
+ }
391
+ }
@@ -50,6 +50,13 @@
50
50
  "when": 1783535507970,
51
51
  "tag": "0006_last_princess_powerful",
52
52
  "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1783607735789,
58
+ "tag": "0007_colorful_true_believers",
59
+ "breakpoints": true
53
60
  }
54
61
  ]
55
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/incident-backend",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,23 +14,23 @@
14
14
  "lint:code": "eslint . --max-warnings 0"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/ai-backend": "0.10.10",
17
+ "@checkstack/ai-backend": "0.10.12",
18
18
  "@checkstack/ai-common": "0.6.6",
19
- "@checkstack/backend-api": "0.31.1",
19
+ "@checkstack/backend-api": "0.32.1",
20
20
  "@checkstack/cache-api": "0.3.19",
21
- "@checkstack/cache-utils": "0.2.24",
22
- "@checkstack/incident-common": "1.10.0",
23
- "@checkstack/catalog-common": "2.7.0",
24
- "@checkstack/catalog-backend": "1.7.0",
25
- "@checkstack/notification-common": "1.6.0",
26
- "@checkstack/auth-common": "0.13.0",
27
- "@checkstack/command-backend": "0.2.22",
21
+ "@checkstack/cache-utils": "0.3.0",
22
+ "@checkstack/incident-common": "1.10.2",
23
+ "@checkstack/catalog-common": "2.7.2",
24
+ "@checkstack/catalog-backend": "1.8.1",
25
+ "@checkstack/notification-common": "1.7.0",
26
+ "@checkstack/auth-common": "0.14.0",
27
+ "@checkstack/command-backend": "0.2.24",
28
28
  "@checkstack/signal-common": "0.2.17",
29
- "@checkstack/status-page-backend": "0.5.0",
30
- "@checkstack/status-page-common": "0.6.0",
31
- "@checkstack/integration-backend": "0.7.4",
29
+ "@checkstack/status-page-backend": "0.6.1",
30
+ "@checkstack/status-page-common": "0.6.2",
31
+ "@checkstack/integration-backend": "0.7.6",
32
32
  "@checkstack/integration-common": "0.9.8",
33
- "@checkstack/automation-backend": "0.11.1",
33
+ "@checkstack/automation-backend": "0.11.3",
34
34
  "@checkstack/automation-common": "0.10.0",
35
35
  "@checkstack/common": "0.22.0",
36
36
  "drizzle-orm": "^0.45.0",
@@ -41,7 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@checkstack/drizzle-helper": "0.0.6",
43
43
  "@checkstack/scripts": "0.7.3",
44
- "@checkstack/test-utils-backend": "0.1.56",
44
+ "@checkstack/test-utils-backend": "0.1.58",
45
45
  "@checkstack/tsconfig": "0.0.7",
46
46
  "@types/bun": "^1.0.0",
47
47
  "@types/pg": "^8.20.0",
package/src/schema.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  primaryKey,
7
7
  boolean,
8
8
  uniqueIndex,
9
+ index,
9
10
  jsonb,
10
11
  } from "drizzle-orm/pg-core";
11
12
  import type { IncidentUpdateEditSnapshot } from "@checkstack/incident-common";
@@ -82,33 +83,50 @@ export const incidentSystems = pgTable(
82
83
  },
83
84
  (t) => ({
84
85
  pk: primaryKey(t.incidentId, t.systemId),
86
+ // Reverse lookup (getIncidentsForSystem / getActiveHealthOverrides): the PK
87
+ // leads with incident_id, so the system_id direction is otherwise unindexed
88
+ // and fans out per system per status-page render.
89
+ systemIdx: index("incident_systems_system_idx").on(t.systemId),
85
90
  }),
86
91
  );
87
92
 
88
93
  /**
89
94
  * Status updates for incidents
90
95
  */
91
- export const incidentUpdates = pgTable("incident_updates", {
92
- id: text("id").primaryKey(),
93
- incidentId: text("incident_id")
94
- .notNull()
95
- .references(() => incidents.id, { onDelete: "cascade" }),
96
- message: text("message").notNull(),
97
- statusChange: incidentStatusEnum("status_change"),
98
- visibility: incidentVisibilityEnum("visibility").notNull().default("public"),
99
- createdAt: timestamp("created_at").defaultNow().notNull(),
100
- // Set when the update is edited in place (null = never edited).
101
- editedAt: timestamp("edited_at"),
102
- // Prior versions archived on each in-place edit (oldest first). Durable,
103
- // globally-readable history of edits (jsonb, defaults to an empty array so
104
- // existing rows backfill cleanly). Manager-facing; the read path strips it
105
- // for non-manager audiences (see read-visibility).
106
- editHistory: jsonb("edit_history")
107
- .$type<IncidentUpdateEditSnapshot[]>()
108
- .notNull()
109
- .default([]),
110
- createdBy: text("created_by"),
111
- });
96
+ export const incidentUpdates = pgTable(
97
+ "incident_updates",
98
+ {
99
+ id: text("id").primaryKey(),
100
+ incidentId: text("incident_id")
101
+ .notNull()
102
+ .references(() => incidents.id, { onDelete: "cascade" }),
103
+ message: text("message").notNull(),
104
+ statusChange: incidentStatusEnum("status_change"),
105
+ visibility: incidentVisibilityEnum("visibility")
106
+ .notNull()
107
+ .default("public"),
108
+ createdAt: timestamp("created_at").defaultNow().notNull(),
109
+ // Set when the update is edited in place (null = never edited).
110
+ editedAt: timestamp("edited_at"),
111
+ // Prior versions archived on each in-place edit (oldest first). Durable,
112
+ // globally-readable history of edits (jsonb, defaults to an empty array so
113
+ // existing rows backfill cleanly). Manager-facing; the read path strips it
114
+ // for non-manager audiences (see read-visibility).
115
+ editHistory: jsonb("edit_history")
116
+ .$type<IncidentUpdateEditSnapshot[]>()
117
+ .notNull()
118
+ .default([]),
119
+ createdBy: text("created_by"),
120
+ },
121
+ (t) => ({
122
+ // Serves the status-derivation query (WHERE incident_id, status_change IS
123
+ // NOT NULL ORDER BY created_at DESC LIMIT 1) and the bulk timeline fetch.
124
+ incidentCreatedIdx: index("incident_updates_incident_created_idx").on(
125
+ t.incidentId,
126
+ t.createdAt,
127
+ ),
128
+ }),
129
+ );
112
130
 
113
131
  /**
114
132
  * Hotlinks attached to an incident — e.g. a Jira ticket, runbook, or chat
@@ -138,6 +138,9 @@ const incidents: WidgetTypeDefinition = {
138
138
  groupIds: c.groupIds,
139
139
  });
140
140
  },
141
+ // This widget surfaces the INCIDENT category: a page emails incident
142
+ // subscribers about a system only when this widget shows it.
143
+ subscriptionCategory: "incident",
141
144
  // Send-time scoping for the subscriber fan-out uses the SAME expansion as the
142
145
  // DTO resolve, so a page never emails about a system its widget does not show.
143
146
  resolveScopedSystems: ({ config, ctx }) => effectiveScope(config, ctx),