@checkstack/incident-backend 1.6.7 → 1.7.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 +37 -0
- package/package.json +9 -9
- package/src/index.ts +16 -0
- package/src/service.test.ts +93 -0
- package/src/service.ts +53 -0
- package/src/system-signals.test.ts +100 -0
- package/src/system-signals.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @checkstack/incident-backend
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0b6f01b: feat(incident): contribute incident signals to the backend system.issues aggregator
|
|
8
|
+
|
|
9
|
+
The incident plugin now registers a `system.issues` contributor (sourceId
|
|
10
|
+
`incident`) from its backend `init`, so the AI assistant surfaces open incidents
|
|
11
|
+
alongside SLOs, health checks, anomalies, and dependency problems.
|
|
12
|
+
|
|
13
|
+
The contributor enforces its own `incident.read` access gate (returning an empty
|
|
14
|
+
map - never throwing - when the principal lacks access; service users carry no
|
|
15
|
+
access rules and so get no signals), then reads every OPEN (not-resolved)
|
|
16
|
+
incident for all systems from the shared, durable `incidents` +
|
|
17
|
+
`incident_systems` tables via a new global `listOpenIncidentsBySystem` service
|
|
18
|
+
method. The answer is therefore identical on every pod, and only systems with an
|
|
19
|
+
open incident appear in the result.
|
|
20
|
+
|
|
21
|
+
The row->signal mapping (source/tone/label/detail/href/accessRule/since/iconName)
|
|
22
|
+
is extracted into a new pure `deriveIncidentSignals` deriver in
|
|
23
|
+
`@checkstack/incident-common`, shared by both the backend contributor and the
|
|
24
|
+
frontend `IncidentSignalsFiller` so the two surfaces stay in lockstep. The
|
|
25
|
+
frontend filler now delegates to that deriver with unchanged behavior.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [dbb76a2]
|
|
30
|
+
- Updated dependencies [0b6f01b]
|
|
31
|
+
- Updated dependencies [0b6f01b]
|
|
32
|
+
- @checkstack/ai-backend@0.3.0
|
|
33
|
+
- @checkstack/incident-common@1.5.0
|
|
34
|
+
- @checkstack/automation-backend@0.5.8
|
|
35
|
+
- @checkstack/catalog-backend@1.4.8
|
|
36
|
+
- @checkstack/backend-api@0.21.6
|
|
37
|
+
- @checkstack/command-backend@0.2.6
|
|
38
|
+
- @checkstack/integration-backend@0.4.6
|
|
39
|
+
|
|
3
40
|
## 1.6.7
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/incident-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,21 +14,21 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/ai-backend": "0.
|
|
17
|
+
"@checkstack/ai-backend": "0.3.0",
|
|
18
18
|
"@checkstack/ai-common": "0.1.3",
|
|
19
|
-
"@checkstack/backend-api": "0.21.
|
|
19
|
+
"@checkstack/backend-api": "0.21.6",
|
|
20
20
|
"@checkstack/cache-api": "0.3.12",
|
|
21
21
|
"@checkstack/cache-utils": "0.2.17",
|
|
22
|
-
"@checkstack/incident-common": "1.
|
|
22
|
+
"@checkstack/incident-common": "1.5.0",
|
|
23
23
|
"@checkstack/catalog-common": "2.3.4",
|
|
24
|
-
"@checkstack/catalog-backend": "1.4.
|
|
24
|
+
"@checkstack/catalog-backend": "1.4.8",
|
|
25
25
|
"@checkstack/notification-common": "1.3.3",
|
|
26
26
|
"@checkstack/auth-common": "0.8.3",
|
|
27
|
-
"@checkstack/command-backend": "0.2.
|
|
27
|
+
"@checkstack/command-backend": "0.2.6",
|
|
28
28
|
"@checkstack/signal-common": "0.2.9",
|
|
29
|
-
"@checkstack/integration-backend": "0.4.
|
|
29
|
+
"@checkstack/integration-backend": "0.4.6",
|
|
30
30
|
"@checkstack/integration-common": "0.7.3",
|
|
31
|
-
"@checkstack/automation-backend": "0.5.
|
|
31
|
+
"@checkstack/automation-backend": "0.5.8",
|
|
32
32
|
"@checkstack/automation-common": "0.4.3",
|
|
33
33
|
"@checkstack/common": "0.15.0",
|
|
34
34
|
"drizzle-orm": "^0.45.0",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
41
41
|
"@checkstack/scripts": "0.6.1",
|
|
42
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
42
|
+
"@checkstack/test-utils-backend": "0.1.40",
|
|
43
43
|
"@checkstack/tsconfig": "0.0.7",
|
|
44
44
|
"@types/bun": "^1.0.0",
|
|
45
45
|
"drizzle-kit": "^0.31.10",
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { SafeDatabase } from "@checkstack/backend-api";
|
|
|
3
3
|
import {
|
|
4
4
|
aiToolExtensionPoint,
|
|
5
5
|
aiToolProjectionExtensionPoint,
|
|
6
|
+
systemSignalsExtensionPoint,
|
|
7
|
+
createSystemAccessResolver,
|
|
6
8
|
deferredProjectionExecute,
|
|
7
9
|
} from "@checkstack/ai-backend";
|
|
8
10
|
import {
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
incidentTriggers,
|
|
49
51
|
} from "./automations";
|
|
50
52
|
import { buildIncidentAiTools } from "./ai/register-ai-tools";
|
|
53
|
+
import { createIncidentSignalsContributor } from "./system-signals";
|
|
51
54
|
|
|
52
55
|
// =============================================================================
|
|
53
56
|
// Plugin Definition
|
|
@@ -224,6 +227,19 @@ export default createBackendPlugin({
|
|
|
224
227
|
execute: deferredProjectionExecute,
|
|
225
228
|
});
|
|
226
229
|
|
|
230
|
+
// Contribute incident problem state to the `system.issues` AI tool.
|
|
231
|
+
// Mirrors the frontend `SystemSignalsSlot` filler: returns one signal
|
|
232
|
+
// per OPEN incident for EVERY system globally (keyed by systemId),
|
|
233
|
+
// using the SAME shared deriver so frontend and backend agree. The
|
|
234
|
+
// per-source access gate + global read live in the contributor factory.
|
|
235
|
+
const signalsExt = env.getExtensionPoint(systemSignalsExtensionPoint);
|
|
236
|
+
signalsExt.contribute(
|
|
237
|
+
createIncidentSignalsContributor({
|
|
238
|
+
service,
|
|
239
|
+
resolver: createSystemAccessResolver(rpcClient),
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
227
243
|
// Register "Create Incident" command in the command palette
|
|
228
244
|
registerSearchProvider({
|
|
229
245
|
pluginMetadata,
|
package/src/service.test.ts
CHANGED
|
@@ -227,6 +227,99 @@ describe("IncidentService.getManyEntityStates (plugin-backed entity read)", () =
|
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
describe("IncidentService.listOpenIncidentsBySystem (global signals read)", () => {
|
|
231
|
+
it("returns {} without a junction query when no open incidents exist", async () => {
|
|
232
|
+
const dbHelper = createProgrammableSelectDb([
|
|
233
|
+
// 1st query: open incidents -> none, so the junction query is skipped.
|
|
234
|
+
[],
|
|
235
|
+
]);
|
|
236
|
+
const service = new IncidentService(
|
|
237
|
+
dbHelper.db as never,
|
|
238
|
+
makeFakeAdvisoryLock(),
|
|
239
|
+
);
|
|
240
|
+
expect(await service.listOpenIncidentsBySystem()).toEqual({});
|
|
241
|
+
expect(dbHelper.getCallCount()).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("groups open incidents under each affected system, full systemIds per entry", async () => {
|
|
245
|
+
const createdAt = new Date("2026-06-01T10:00:00.000Z");
|
|
246
|
+
const updatedAt = new Date("2026-06-01T10:05:00.000Z");
|
|
247
|
+
const dbHelper = createProgrammableSelectDb([
|
|
248
|
+
// 1st query: open incident rows (resolved excluded by the WHERE clause).
|
|
249
|
+
[
|
|
250
|
+
{
|
|
251
|
+
id: "inc-1",
|
|
252
|
+
title: "DB down",
|
|
253
|
+
description: null,
|
|
254
|
+
status: "investigating",
|
|
255
|
+
severity: "critical",
|
|
256
|
+
suppressNotifications: false,
|
|
257
|
+
createdAt,
|
|
258
|
+
updatedAt,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "inc-2",
|
|
262
|
+
title: "Slow",
|
|
263
|
+
description: "elevated latency",
|
|
264
|
+
status: "monitoring",
|
|
265
|
+
severity: "major",
|
|
266
|
+
suppressNotifications: false,
|
|
267
|
+
createdAt,
|
|
268
|
+
updatedAt,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
// 2nd query: junction rows. inc-1 spans two systems; inc-2 one.
|
|
272
|
+
[
|
|
273
|
+
{ incidentId: "inc-1", systemId: "sys-a" },
|
|
274
|
+
{ incidentId: "inc-1", systemId: "sys-b" },
|
|
275
|
+
{ incidentId: "inc-2", systemId: "sys-a" },
|
|
276
|
+
],
|
|
277
|
+
]);
|
|
278
|
+
const service = new IncidentService(
|
|
279
|
+
dbHelper.db as never,
|
|
280
|
+
makeFakeAdvisoryLock(),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const out = await service.listOpenIncidentsBySystem();
|
|
284
|
+
|
|
285
|
+
expect(Object.keys(out).sort()).toEqual(["sys-a", "sys-b"]);
|
|
286
|
+
// sys-a sees both incidents; sys-b only inc-1.
|
|
287
|
+
expect(out["sys-a"].map((i) => i.id)).toEqual(["inc-1", "inc-2"]);
|
|
288
|
+
expect(out["sys-b"].map((i) => i.id)).toEqual(["inc-1"]);
|
|
289
|
+
// Multi-system incident carries its FULL systemIds under each key.
|
|
290
|
+
expect(out["sys-a"][0].systemIds).toEqual(["sys-a", "sys-b"]);
|
|
291
|
+
expect(out["sys-b"][0].systemIds).toEqual(["sys-a", "sys-b"]);
|
|
292
|
+
// null description is normalized to undefined (IncidentWithSystems shape).
|
|
293
|
+
expect(out["sys-a"][0].description).toBeUndefined();
|
|
294
|
+
expect(out["sys-a"][1].description).toBe("elevated latency");
|
|
295
|
+
expect(dbHelper.getCallCount()).toBe(2);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("yields no entry for an open incident with no system associations", async () => {
|
|
299
|
+
const dbHelper = createProgrammableSelectDb([
|
|
300
|
+
[
|
|
301
|
+
{
|
|
302
|
+
id: "inc-orphan",
|
|
303
|
+
title: "Orphan",
|
|
304
|
+
description: null,
|
|
305
|
+
status: "identified",
|
|
306
|
+
severity: "minor",
|
|
307
|
+
suppressNotifications: false,
|
|
308
|
+
createdAt: new Date("2026-06-01T10:00:00.000Z"),
|
|
309
|
+
updatedAt: new Date("2026-06-01T10:00:00.000Z"),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
[], // no junction rows
|
|
313
|
+
]);
|
|
314
|
+
const service = new IncidentService(
|
|
315
|
+
dbHelper.db as never,
|
|
316
|
+
makeFakeAdvisoryLock(),
|
|
317
|
+
);
|
|
318
|
+
expect(await service.listOpenIncidentsBySystem()).toEqual({});
|
|
319
|
+
expect(dbHelper.getCallCount()).toBe(2);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
230
323
|
/**
|
|
231
324
|
* Table-backed fake `db` for the dedup-create path. Models just enough of
|
|
232
325
|
* the Drizzle surface the service touches (select/insert by TABLE IDENTITY,
|
package/src/service.ts
CHANGED
|
@@ -224,6 +224,59 @@ export class IncidentService {
|
|
|
224
224
|
return result;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Global read of every OPEN (not-resolved) incident across ALL systems,
|
|
229
|
+
* grouped by systemId. Powers the backend `system.issues` contributor, which
|
|
230
|
+
* needs problem state for every system on every pod (not a caller-supplied
|
|
231
|
+
* systemId list). Reads the authoritative `incidents` + `incident_systems`
|
|
232
|
+
* tables, so the answer is identical on every pod (state-and-scale rule).
|
|
233
|
+
*
|
|
234
|
+
* An incident affecting multiple systems appears under each of its systemIds,
|
|
235
|
+
* each entry carrying the incident's full `systemIds` list (mirrors the bulk
|
|
236
|
+
* RPC shape `getBulkIncidentsForSystems` returns). Systems with no open
|
|
237
|
+
* incident are simply absent from the returned record.
|
|
238
|
+
*/
|
|
239
|
+
async listOpenIncidentsBySystem(): Promise<
|
|
240
|
+
Record<string, IncidentWithSystems[]>
|
|
241
|
+
> {
|
|
242
|
+
const openRows = await this.db
|
|
243
|
+
.select()
|
|
244
|
+
.from(incidents)
|
|
245
|
+
.where(ne(incidents.status, "resolved"));
|
|
246
|
+
if (openRows.length === 0) return {};
|
|
247
|
+
|
|
248
|
+
const openIds = openRows.map((i) => i.id);
|
|
249
|
+
const systemRows = await this.db
|
|
250
|
+
.select({
|
|
251
|
+
incidentId: incidentSystems.incidentId,
|
|
252
|
+
systemId: incidentSystems.systemId,
|
|
253
|
+
})
|
|
254
|
+
.from(incidentSystems)
|
|
255
|
+
.where(inArray(incidentSystems.incidentId, openIds));
|
|
256
|
+
|
|
257
|
+
const systemsByIncident = new Map<string, string[]>();
|
|
258
|
+
for (const r of systemRows) {
|
|
259
|
+
const list = systemsByIncident.get(r.incidentId);
|
|
260
|
+
if (list) list.push(r.systemId);
|
|
261
|
+
else systemsByIncident.set(r.incidentId, [r.systemId]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result: Record<string, IncidentWithSystems[]> = {};
|
|
265
|
+
for (const i of openRows) {
|
|
266
|
+
const systemIds = systemsByIncident.get(i.id) ?? [];
|
|
267
|
+
const incident: IncidentWithSystems = {
|
|
268
|
+
...i,
|
|
269
|
+
description: i.description ?? undefined,
|
|
270
|
+
systemIds,
|
|
271
|
+
};
|
|
272
|
+
for (const systemId of systemIds) {
|
|
273
|
+
(result[systemId] ??= []).push(incident);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
227
280
|
/**
|
|
228
281
|
* Create a new incident.
|
|
229
282
|
*
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import type { AuthUser } from "@checkstack/backend-api";
|
|
3
|
+
import type { SystemAccessResolver } from "@checkstack/ai-backend";
|
|
4
|
+
import type { IncidentWithSystems } from "@checkstack/incident-common";
|
|
5
|
+
import { createIncidentSignalsContributor } from "./system-signals";
|
|
6
|
+
import type { IncidentService } from "./service";
|
|
7
|
+
|
|
8
|
+
const openIncident: IncidentWithSystems = {
|
|
9
|
+
id: "inc-1",
|
|
10
|
+
title: "Database down",
|
|
11
|
+
description: undefined,
|
|
12
|
+
status: "investigating",
|
|
13
|
+
severity: "critical",
|
|
14
|
+
suppressNotifications: false,
|
|
15
|
+
createdAt: new Date("2026-06-01T10:00:00.000Z"),
|
|
16
|
+
updatedAt: new Date("2026-06-01T10:00:00.000Z"),
|
|
17
|
+
systemIds: ["sys-1"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeService(
|
|
21
|
+
bySystem: Record<string, IncidentWithSystems[]>,
|
|
22
|
+
): Pick<IncidentService, "listOpenIncidentsBySystem"> {
|
|
23
|
+
return {
|
|
24
|
+
listOpenIncidentsBySystem: mock(async () => bySystem),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The per-source gate (global rule, team grants, service trust) is owned and
|
|
29
|
+
// tested by `createGatedSystemSignalsContributor`; here we use simple resolver
|
|
30
|
+
// stubs and focus on this plugin's wiring (source id, service, shared deriver).
|
|
31
|
+
const allowAll: SystemAccessResolver = {
|
|
32
|
+
accessibleSystemIds: async ({ systemIds }) => systemIds,
|
|
33
|
+
};
|
|
34
|
+
const denyAll: SystemAccessResolver = { accessibleSystemIds: async () => [] };
|
|
35
|
+
|
|
36
|
+
const userWithRead: AuthUser = {
|
|
37
|
+
type: "user",
|
|
38
|
+
id: "u1",
|
|
39
|
+
accessRules: ["incident.incident.read"],
|
|
40
|
+
};
|
|
41
|
+
const userWithoutRead: AuthUser = { type: "user", id: "u2", accessRules: [] };
|
|
42
|
+
|
|
43
|
+
describe("incident system-signals contributor", () => {
|
|
44
|
+
test("uses the shared incident source id", () => {
|
|
45
|
+
const contributor = createIncidentSignalsContributor({
|
|
46
|
+
service: makeService({}),
|
|
47
|
+
resolver: allowAll,
|
|
48
|
+
});
|
|
49
|
+
expect(contributor.sourceId).toBe("incident");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("wires the service + shared deriver for an authorized principal", async () => {
|
|
53
|
+
const service = makeService({ "sys-1": [openIncident] });
|
|
54
|
+
const contributor = createIncidentSignalsContributor({
|
|
55
|
+
service,
|
|
56
|
+
resolver: allowAll,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await contributor.read({ principal: userWithRead });
|
|
60
|
+
|
|
61
|
+
expect(result.accessible).toBe(true);
|
|
62
|
+
expect(Object.keys(result.signals)).toEqual(["sys-1"]);
|
|
63
|
+
expect(result.signals["sys-1"][0]).toMatchObject({
|
|
64
|
+
source: "incident",
|
|
65
|
+
tone: "error",
|
|
66
|
+
label: "Critical incident",
|
|
67
|
+
detail: "Database down",
|
|
68
|
+
since: "2026-06-01T10:00:00.000Z",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("filters to team-granted systems for a non-global user", async () => {
|
|
73
|
+
const service = makeService({
|
|
74
|
+
"sys-1": [openIncident],
|
|
75
|
+
"sys-2": [{ ...openIncident, id: "inc-2", systemIds: ["sys-2"] }],
|
|
76
|
+
});
|
|
77
|
+
const recorded: string[] = [];
|
|
78
|
+
const resolver: SystemAccessResolver = {
|
|
79
|
+
accessibleSystemIds: async ({ resourceType, systemIds }) => {
|
|
80
|
+
recorded.push(resourceType);
|
|
81
|
+
return systemIds.filter((id) => id === "sys-2");
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const contributor = createIncidentSignalsContributor({ service, resolver });
|
|
85
|
+
|
|
86
|
+
const result = await contributor.read({ principal: userWithoutRead });
|
|
87
|
+
|
|
88
|
+
expect(Object.keys(result.signals)).toEqual(["sys-2"]);
|
|
89
|
+
expect(recorded).toEqual(["incident.incident"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a non-global user with no team grants gets nothing", async () => {
|
|
93
|
+
const contributor = createIncidentSignalsContributor({
|
|
94
|
+
service: makeService({ "sys-1": [openIncident] }),
|
|
95
|
+
resolver: denyAll,
|
|
96
|
+
});
|
|
97
|
+
const result = await contributor.read({ principal: userWithoutRead });
|
|
98
|
+
expect(result).toEqual({ accessible: false, signals: {} });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGatedSystemSignalsContributor,
|
|
3
|
+
type SystemAccessResolver,
|
|
4
|
+
type SystemSignalsContributor,
|
|
5
|
+
} from "@checkstack/ai-backend";
|
|
6
|
+
import {
|
|
7
|
+
incidentAccess,
|
|
8
|
+
INCIDENT_SIGNAL_SOURCE_ID,
|
|
9
|
+
deriveIncidentSignals,
|
|
10
|
+
} from "@checkstack/incident-common";
|
|
11
|
+
import type { IncidentService } from "./service";
|
|
12
|
+
|
|
13
|
+
/** The slice of `IncidentService` the contributor needs - eases testing. */
|
|
14
|
+
type SignalsService = Pick<IncidentService, "listOpenIncidentsBySystem">;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the incident `SystemSignalsContributor` for the backend `system.issues`
|
|
18
|
+
* AI tool. Reads OPEN incidents for every system globally and runs the SAME
|
|
19
|
+
* shared `deriveIncidentSignals` deriver the dashboard filler uses, so frontend
|
|
20
|
+
* and backend signals match. The per-source access gate (global rule plus
|
|
21
|
+
* per-system team grants) is applied by
|
|
22
|
+
* {@link createGatedSystemSignalsContributor}; the global read resolves from the
|
|
23
|
+
* authoritative incident tables, so the answer is identical on every pod.
|
|
24
|
+
*/
|
|
25
|
+
export function createIncidentSignalsContributor({
|
|
26
|
+
service,
|
|
27
|
+
resolver,
|
|
28
|
+
}: {
|
|
29
|
+
service: SignalsService;
|
|
30
|
+
resolver: SystemAccessResolver;
|
|
31
|
+
}): SystemSignalsContributor {
|
|
32
|
+
return createGatedSystemSignalsContributor({
|
|
33
|
+
sourceId: INCIDENT_SIGNAL_SOURCE_ID,
|
|
34
|
+
accessRule: incidentAccess.incident.read,
|
|
35
|
+
resolver,
|
|
36
|
+
readSignals: async () => {
|
|
37
|
+
const incidentsBySystem = await service.listOpenIncidentsBySystem();
|
|
38
|
+
return deriveIncidentSignals({
|
|
39
|
+
incidentsBySystem,
|
|
40
|
+
systemIds: Object.keys(incidentsBySystem),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|