@checkstack/maintenance-backend 0.5.17 → 0.6.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @checkstack/maintenance-backend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 298bf42: ### Notification System Optimizations
8
+
9
+ **System context in notifications**: All notification senders (healthcheck, incident, maintenance, dependency) now include the affected system name in the notification title and body. Users can immediately identify which system is affected without clicking through to the detail page.
10
+
11
+ **Upstream notification deduplication**: When an upstream dependency goes down affecting multiple downstream systems, the dependency notification sidecar now sends **one personalized notification per user** instead of one notification per affected system. Each user's notification lists only the systems they are subscribed to, with a link to the upstream root cause system. This prevents notification floods for users subscribed to groups containing many dependent systems.
12
+
13
+ **New catalog endpoint**: Added `getSystemGroupIds` S2S RPC endpoint on the catalog to resolve which catalog groups contain a given system, used by the dependency plugin for efficient subscriber resolution during batched notification dispatch.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [298bf42]
18
+ - @checkstack/catalog-common@1.5.0
19
+
3
20
  ## 0.5.17
4
21
 
5
22
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/maintenance-backend",
3
- "version": "0.5.17",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -16,8 +16,8 @@
16
16
  "@checkstack/backend-api": "0.12.0",
17
17
  "@checkstack/maintenance-common": "0.4.9",
18
18
  "@checkstack/notification-common": "0.2.8",
19
- "@checkstack/catalog-common": "1.4.0",
20
- "@checkstack/auth-common": "0.6.1",
19
+ "@checkstack/catalog-common": "1.4.1",
20
+ "@checkstack/auth-common": "0.6.2",
21
21
  "@checkstack/command-backend": "0.1.19",
22
22
  "@checkstack/signal-common": "0.1.9",
23
23
  "@checkstack/integration-backend": "0.1.19",
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, mock, beforeEach } from "bun:test";
2
+ import { notifyAffectedSystems } from "./notifications";
3
+
4
+ // Mock catalog client
5
+ function createMockCatalogClient() {
6
+ return {
7
+ notifySystemSubscribers: mock(() => Promise.resolve()),
8
+ };
9
+ }
10
+
11
+ // Mock logger
12
+ function createMockLogger() {
13
+ return {
14
+ warn: mock(() => {}),
15
+ error: mock(() => {}),
16
+ info: mock(() => {}),
17
+ debug: mock(() => {}),
18
+ };
19
+ }
20
+
21
+ describe("notifyAffectedSystems (maintenance)", () => {
22
+ let mockCatalogClient: ReturnType<typeof createMockCatalogClient>;
23
+ let mockLogger: ReturnType<typeof createMockLogger>;
24
+
25
+ beforeEach(() => {
26
+ mockCatalogClient = createMockCatalogClient();
27
+ mockLogger = createMockLogger();
28
+ });
29
+
30
+ describe("system name inclusion", () => {
31
+ it("should include system name in title when systemNames map is provided", async () => {
32
+ const systemNames = new Map([
33
+ ["sys-1", "Production Database"],
34
+ ]);
35
+
36
+ await notifyAffectedSystems({
37
+ catalogClient: mockCatalogClient as never,
38
+ logger: mockLogger as never,
39
+ maintenanceId: "maint-1",
40
+ maintenanceTitle: "Scheduled DB Upgrade",
41
+ systemIds: ["sys-1"],
42
+ systemNames,
43
+ action: "created",
44
+ });
45
+
46
+ expect(mockCatalogClient.notifySystemSubscribers).toHaveBeenCalledWith(
47
+ expect.objectContaining({
48
+ title: "Maintenance scheduled: Production Database",
49
+ body: expect.stringContaining("**Production Database**"),
50
+ }),
51
+ );
52
+ });
53
+
54
+ it("should fall back to systemId when systemNames map is not provided", async () => {
55
+ await notifyAffectedSystems({
56
+ catalogClient: mockCatalogClient as never,
57
+ logger: mockLogger as never,
58
+ maintenanceId: "maint-1",
59
+ maintenanceTitle: "Scheduled DB Upgrade",
60
+ systemIds: ["sys-1"],
61
+ action: "started",
62
+ });
63
+
64
+ expect(mockCatalogClient.notifySystemSubscribers).toHaveBeenCalledWith(
65
+ expect.objectContaining({
66
+ title: "Maintenance started: sys-1",
67
+ body: expect.stringContaining("**sys-1**"),
68
+ }),
69
+ );
70
+ });
71
+ });
72
+
73
+ describe("action text mapping", () => {
74
+ it("should use 'scheduled' for created action", async () => {
75
+ await notifyAffectedSystems({
76
+ catalogClient: mockCatalogClient as never,
77
+ logger: mockLogger as never,
78
+ maintenanceId: "maint-1",
79
+ maintenanceTitle: "Test",
80
+ systemIds: ["sys-1"],
81
+ action: "created",
82
+ });
83
+
84
+ expect(mockCatalogClient.notifySystemSubscribers).toHaveBeenCalledWith(
85
+ expect.objectContaining({
86
+ title: expect.stringContaining("scheduled"),
87
+ }),
88
+ );
89
+ });
90
+
91
+ it("should use 'completed' for completed action", async () => {
92
+ await notifyAffectedSystems({
93
+ catalogClient: mockCatalogClient as never,
94
+ logger: mockLogger as never,
95
+ maintenanceId: "maint-1",
96
+ maintenanceTitle: "Test",
97
+ systemIds: ["sys-1"],
98
+ action: "completed",
99
+ });
100
+
101
+ expect(mockCatalogClient.notifySystemSubscribers).toHaveBeenCalledWith(
102
+ expect.objectContaining({
103
+ title: expect.stringContaining("completed"),
104
+ }),
105
+ );
106
+ });
107
+ });
108
+
109
+ describe("importance", () => {
110
+ it("should always use 'info' importance", async () => {
111
+ await notifyAffectedSystems({
112
+ catalogClient: mockCatalogClient as never,
113
+ logger: mockLogger as never,
114
+ maintenanceId: "maint-1",
115
+ maintenanceTitle: "Test",
116
+ systemIds: ["sys-1"],
117
+ action: "started",
118
+ });
119
+
120
+ expect(mockCatalogClient.notifySystemSubscribers).toHaveBeenCalledWith(
121
+ expect.objectContaining({
122
+ importance: "info",
123
+ }),
124
+ );
125
+ });
126
+ });
127
+
128
+ describe("error handling", () => {
129
+ it("should log warning but not throw when notification fails", async () => {
130
+ mockCatalogClient.notifySystemSubscribers.mockRejectedValue(
131
+ new Error("Network error"),
132
+ );
133
+
134
+ await notifyAffectedSystems({
135
+ catalogClient: mockCatalogClient as never,
136
+ logger: mockLogger as never,
137
+ maintenanceId: "maint-1",
138
+ maintenanceTitle: "Test",
139
+ systemIds: ["sys-1"],
140
+ action: "created",
141
+ });
142
+
143
+ expect(mockLogger.warn).toHaveBeenCalled();
144
+ });
145
+ });
146
+ });
@@ -15,6 +15,7 @@ export async function notifyAffectedSystems(props: {
15
15
  maintenanceId: string;
16
16
  maintenanceTitle: string;
17
17
  systemIds: string[];
18
+ systemNames?: Map<string, string>;
18
19
  action: "created" | "updated" | "started" | "completed";
19
20
  }): Promise<void> {
20
21
  const {
@@ -23,6 +24,7 @@ export async function notifyAffectedSystems(props: {
23
24
  maintenanceId,
24
25
  maintenanceTitle,
25
26
  systemIds,
27
+ systemNames,
26
28
  action,
27
29
  } = props;
28
30
 
@@ -38,11 +40,14 @@ export async function notifyAffectedSystems(props: {
38
40
  });
39
41
 
40
42
  for (const systemId of systemIds) {
43
+ // Resolve system name from provided map, or fall back to systemId
44
+ const systemName = systemNames?.get(systemId) ?? systemId;
45
+
41
46
  try {
42
47
  await catalogClient.notifySystemSubscribers({
43
48
  systemId,
44
- title: `Maintenance ${actionText}`,
45
- body: `A maintenance **"${maintenanceTitle}"** has been ${actionText} for a system you're subscribed to.`,
49
+ title: `Maintenance ${actionText}: ${systemName}`,
50
+ body: `Maintenance **"${maintenanceTitle}"** has been ${actionText} for **${systemName}**.`,
46
51
  importance: "info",
47
52
  action: { label: "View Maintenance", url: maintenanceDetailPath },
48
53
  includeGroupSubscribers: true,
@@ -56,3 +61,4 @@ export async function notifyAffectedSystems(props: {
56
61
  }
57
62
  }
58
63
  }
64
+