@oneuptime/common 10.0.6 → 10.0.8

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.
Files changed (57) hide show
  1. package/Server/Middleware/UserAuthorization.ts +36 -0
  2. package/Server/Services/MonitorService.ts +9 -0
  3. package/Server/Services/UserNotificationRuleService.ts +83 -0
  4. package/Server/Types/Markdown.ts +17 -4
  5. package/Server/Utils/Browser.ts +3 -3
  6. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +158 -0
  7. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +15 -0
  8. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +32 -0
  9. package/Server/Utils/VM/VMRunner.ts +273 -52
  10. package/Server/Views/Partials/Head.ejs +3 -1
  11. package/Tests/Server/Middleware/UserAuthorization.test.ts +1 -0
  12. package/Types/Monitor/CriteriaFilter.ts +12 -2
  13. package/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.ts +16 -0
  14. package/Types/Monitor/ExternalStatusPageProviderType.ts +8 -0
  15. package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
  16. package/Types/Monitor/MonitorStep.ts +34 -0
  17. package/Types/Monitor/MonitorStepExternalStatusPageMonitor.ts +48 -0
  18. package/Types/Monitor/MonitorType.ts +16 -2
  19. package/Types/Probe/ProbeMonitorResponse.ts +2 -0
  20. package/Utils/Monitor/MonitorMetricType.ts +3 -1
  21. package/build/dist/Server/Middleware/UserAuthorization.js +30 -1
  22. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  23. package/build/dist/Server/Services/MonitorService.js +7 -1
  24. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  25. package/build/dist/Server/Services/UserNotificationRuleService.js +58 -5
  26. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  27. package/build/dist/Server/Types/Markdown.js +16 -4
  28. package/build/dist/Server/Types/Markdown.js.map +1 -1
  29. package/build/dist/Server/Utils/Browser.js +3 -3
  30. package/build/dist/Server/Utils/Browser.js.map +1 -1
  31. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +119 -0
  32. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -0
  33. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
  34. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  35. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -0
  36. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  37. package/build/dist/Server/Utils/VM/VMRunner.js +243 -49
  38. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  39. package/build/dist/Tests/Server/Middleware/UserAuthorization.test.js +1 -0
  40. package/build/dist/Tests/Server/Middleware/UserAuthorization.test.js.map +1 -1
  41. package/build/dist/Types/Monitor/CriteriaFilter.js +11 -2
  42. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  43. package/build/dist/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.js +2 -0
  44. package/build/dist/Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse.js.map +1 -0
  45. package/build/dist/Types/Monitor/ExternalStatusPageProviderType.js +9 -0
  46. package/build/dist/Types/Monitor/ExternalStatusPageProviderType.js.map +1 -0
  47. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
  48. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  49. package/build/dist/Types/Monitor/MonitorStep.js +22 -0
  50. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  51. package/build/dist/Types/Monitor/MonitorStepExternalStatusPageMonitor.js +32 -0
  52. package/build/dist/Types/Monitor/MonitorStepExternalStatusPageMonitor.js.map +1 -0
  53. package/build/dist/Types/Monitor/MonitorType.js +14 -2
  54. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  55. package/build/dist/Utils/Monitor/MonitorMetricType.js +3 -1
  56. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  57. package/package.json +1 -1
@@ -116,6 +116,42 @@ export default class UserMiddleware {
116
116
  }
117
117
  }
118
118
 
119
+ /*
120
+ * Also check x-sso-tokens header (mobile app flow).
121
+ * The header value is a JSON-encoded map of { projectId: ssoToken }.
122
+ */
123
+ const ssoTokensHeader: string | undefined = req.headers["x-sso-tokens"] as
124
+ | string
125
+ | undefined;
126
+
127
+ if (ssoTokensHeader) {
128
+ try {
129
+ const headerTokens: Record<string, string> =
130
+ JSON.parse(ssoTokensHeader);
131
+
132
+ for (const projectId of Object.keys(headerTokens)) {
133
+ const token: string | undefined = headerTokens[projectId];
134
+
135
+ if (!token || typeof token !== "string") {
136
+ continue;
137
+ }
138
+
139
+ try {
140
+ const decoded: JSONWebTokenData = JSONWebToken.decode(token);
141
+
142
+ if (decoded.projectId?.toString() === projectId) {
143
+ ssoTokens[projectId] = token;
144
+ }
145
+ } catch (err) {
146
+ logger.error(err);
147
+ continue;
148
+ }
149
+ }
150
+ } catch (err) {
151
+ logger.error(err);
152
+ }
153
+ }
154
+
119
155
  return ssoTokens;
120
156
  }
121
157
 
@@ -135,6 +135,15 @@ export class Service extends DatabaseService<Model> {
135
135
  monitorDestination = `${monitorDestination} @${firstStep.data.dnsMonitor.hostname}`;
136
136
  }
137
137
  }
138
+
139
+ // For External Status Page monitors, use the statusPageUrl
140
+ if (
141
+ monitorType === MonitorType.ExternalStatusPage &&
142
+ firstStep?.data?.externalStatusPageMonitor
143
+ ) {
144
+ monitorDestination =
145
+ firstStep.data.externalStatusPageMonitor.statusPageUrl || "";
146
+ }
138
147
  }
139
148
  }
140
149
 
@@ -1110,6 +1110,9 @@ export class Service extends DatabaseService<Model> {
1110
1110
  ...(alert.alertNumber !== undefined && {
1111
1111
  alertNumber: alert.alertNumber,
1112
1112
  }),
1113
+ ...(alert.alertNumberWithPrefix && {
1114
+ alertNumberWithPrefix: alert.alertNumberWithPrefix,
1115
+ }),
1113
1116
  alertId: alert.id!.toString(),
1114
1117
  projectId: alert.projectId!.toString(),
1115
1118
  });
@@ -1187,6 +1190,9 @@ export class Service extends DatabaseService<Model> {
1187
1190
  ...(incident.incidentNumber !== undefined && {
1188
1191
  incidentNumber: incident.incidentNumber,
1189
1192
  }),
1193
+ ...(incident.incidentNumberWithPrefix && {
1194
+ incidentNumberWithPrefix: incident.incidentNumberWithPrefix,
1195
+ }),
1190
1196
  incidentId: incident.id!.toString(),
1191
1197
  projectId: incident.projectId!.toString(),
1192
1198
  });
@@ -1309,6 +1315,83 @@ export class Service extends DatabaseService<Model> {
1309
1315
  });
1310
1316
  });
1311
1317
  }
1318
+
1319
+ // send push notification for incident episode
1320
+ if (
1321
+ options.userNotificationEventType ===
1322
+ UserNotificationEventType.IncidentEpisodeCreated &&
1323
+ incidentEpisode
1324
+ ) {
1325
+ logTimelineItem.status = UserNotificationStatus.Sending;
1326
+ logTimelineItem.statusMessage = `Sending push notification to device.`;
1327
+ logTimelineItem.userPushId = notificationRuleItem.userPush.id!;
1328
+
1329
+ const updatedLog: UserOnCallLogTimeline =
1330
+ await UserOnCallLogTimelineService.create({
1331
+ data: logTimelineItem,
1332
+ props: {
1333
+ isRoot: true,
1334
+ },
1335
+ });
1336
+
1337
+ const pushMessage: PushNotificationMessage =
1338
+ PushNotificationUtil.createIncidentEpisodeCreatedNotification({
1339
+ incidentEpisodeTitle: incidentEpisode.title!,
1340
+ projectName: incidentEpisode.project?.name || "OneUptime",
1341
+ incidentEpisodeViewLink: (
1342
+ await IncidentEpisodeService.getEpisodeLinkInDashboard(
1343
+ incidentEpisode.projectId!,
1344
+ incidentEpisode.id!,
1345
+ )
1346
+ ).toString(),
1347
+ ...(incidentEpisode.episodeNumber !== undefined && {
1348
+ episodeNumber: incidentEpisode.episodeNumber,
1349
+ }),
1350
+ ...(incidentEpisode.episodeNumberWithPrefix && {
1351
+ episodeNumberWithPrefix: incidentEpisode.episodeNumberWithPrefix,
1352
+ }),
1353
+ incidentEpisodeId: incidentEpisode.id!.toString(),
1354
+ projectId: incidentEpisode.projectId!.toString(),
1355
+ });
1356
+
1357
+ PushNotificationService.sendPushNotification(
1358
+ {
1359
+ devices: [
1360
+ {
1361
+ token: notificationRuleItem.userPush.deviceToken!,
1362
+ ...(notificationRuleItem.userPush.deviceName && {
1363
+ name: notificationRuleItem.userPush.deviceName,
1364
+ }),
1365
+ },
1366
+ ],
1367
+ message: pushMessage,
1368
+ deviceType: notificationRuleItem.userPush
1369
+ .deviceType! as PushDeviceType,
1370
+ },
1371
+ {
1372
+ projectId: options.projectId,
1373
+ userOnCallLogTimelineId: updatedLog.id!,
1374
+ userId: notificationRuleItem.userId!,
1375
+ onCallPolicyId: options.onCallPolicyId,
1376
+ onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
1377
+ teamId: options.userBelongsToTeamId,
1378
+ onCallDutyPolicyExecutionLogTimelineId:
1379
+ options.onCallDutyPolicyExecutionLogTimelineId,
1380
+ onCallScheduleId: options.onCallScheduleId,
1381
+ },
1382
+ ).catch(async (err: Error) => {
1383
+ await UserOnCallLogTimelineService.updateOneById({
1384
+ id: updatedLog.id!,
1385
+ data: {
1386
+ status: UserNotificationStatus.Error,
1387
+ statusMessage: err.message || "Error sending push notification.",
1388
+ },
1389
+ props: {
1390
+ isRoot: true,
1391
+ },
1392
+ });
1393
+ });
1394
+ }
1312
1395
  }
1313
1396
 
1314
1397
  if (
@@ -103,6 +103,15 @@ export default class Markdown {
103
103
  return htmlBody;
104
104
  }
105
105
 
106
+ private static escapeHtml(text: string): string {
107
+ return text
108
+ .replace(/&/g, "&amp;")
109
+ .replace(/</g, "&lt;")
110
+ .replace(/>/g, "&gt;")
111
+ .replace(/"/g, "&quot;")
112
+ .replace(/'/g, "&#39;");
113
+ }
114
+
106
115
  private static getEmailRenderer(): Renderer {
107
116
  if (this.emailRenderer !== null) {
108
117
  return this.emailRenderer;
@@ -140,7 +149,8 @@ export default class Markdown {
140
149
  if (language === "mermaid") {
141
150
  return `<div class="mermaid">${code}</div>`;
142
151
  }
143
- return `<pre><code class="language-${language}">${code}</code></pre>`;
152
+ const escaped: string = Markdown.escapeHtml(code);
153
+ return `<pre><code class="language-${language}">${escaped}</code></pre>`;
144
154
  };
145
155
 
146
156
  renderer.heading = function (text, level) {
@@ -160,7 +170,8 @@ export default class Markdown {
160
170
 
161
171
  // Inline code
162
172
  renderer.codespan = function (code) {
163
- return `<code class="rounded-md bg-slate-100 px-1.5 py-0.5 text-sm text-slate-700 font-mono">${code}</code>`;
173
+ const escaped: string = Markdown.escapeHtml(code);
174
+ return `<code class="rounded-md bg-slate-100 px-1.5 py-0.5 text-sm text-slate-700 font-mono">${escaped}</code>`;
164
175
  };
165
176
 
166
177
  this.docsRenderer = renderer;
@@ -186,7 +197,8 @@ export default class Markdown {
186
197
  };
187
198
 
188
199
  renderer.code = function (code, language) {
189
- return `<pre class="language-${language} rounded-md"><code class="language-${language} rounded-md">${code}</code></pre>`;
200
+ const escaped: string = Markdown.escapeHtml(code);
201
+ return `<pre class="language-${language} rounded-md"><code class="language-${language} rounded-md">${escaped}</code></pre>`;
190
202
  };
191
203
 
192
204
  renderer.heading = function (text, level) {
@@ -237,7 +249,8 @@ export default class Markdown {
237
249
 
238
250
  // Inline code
239
251
  renderer.codespan = function (code) {
240
- return `<code class="rounded-md bg-gray-100 px-1.5 py-0.5 text-sm text-pink-600">${code}</code>`;
252
+ const escaped: string = Markdown.escapeHtml(code);
253
+ return `<code class="rounded-md bg-gray-100 px-1.5 py-0.5 text-sm text-pink-600">${escaped}</code>`;
241
254
  };
242
255
 
243
256
  // Horizontal rule
@@ -23,7 +23,9 @@ export default class BrowserUtil {
23
23
  "--disable-dev-shm-usage",
24
24
  "--disable-gpu",
25
25
  "--disable-software-rasterizer",
26
- "--single-process",
26
+ "--disable-dbus", // no D-Bus daemon in containers
27
+ "--disable-features=dbus", // additional D-Bus feature gate
28
+ "--no-zygote", // skip zygote process that fails OOM score adjustments in containers
27
29
  ];
28
30
 
29
31
  // Firefox preferences for stability in containerized environments
@@ -31,8 +33,6 @@ export default class BrowserUtil {
31
33
  string,
32
34
  string | number | boolean
33
35
  > = {
34
- "browser.tabs.remote.autostart": false, // disable multi-process (electrolysis)
35
- "dom.ipc.processCount": 1, // single content process
36
36
  "gfx.webrender.all": false, // disable GPU-based WebRender
37
37
  "media.hardware-video-decoding.enabled": false, // disable hardware video decoding
38
38
  "layers.acceleration.disabled": true, // disable GPU-accelerated layers
@@ -0,0 +1,158 @@
1
+ import DataToProcess from "../DataToProcess";
2
+ import CompareCriteria from "./CompareCriteria";
3
+ import {
4
+ CheckOn,
5
+ CriteriaFilter,
6
+ } from "../../../../Types/Monitor/CriteriaFilter";
7
+ import ExternalStatusPageMonitorResponse from "../../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
8
+ import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
9
+ import EvaluateOverTime from "./EvaluateOverTime";
10
+ import CaptureSpan from "../../Telemetry/CaptureSpan";
11
+ import logger from "../../Logger";
12
+
13
+ export default class ExternalStatusPageMonitorCriteria {
14
+ @CaptureSpan()
15
+ public static async isMonitorInstanceCriteriaFilterMet(input: {
16
+ dataToProcess: DataToProcess;
17
+ criteriaFilter: CriteriaFilter;
18
+ }): Promise<string | null> {
19
+ let threshold: number | string | undefined | null =
20
+ input.criteriaFilter.value;
21
+
22
+ const dataToProcess: ProbeMonitorResponse =
23
+ input.dataToProcess as ProbeMonitorResponse;
24
+
25
+ const externalStatusPageResponse:
26
+ | ExternalStatusPageMonitorResponse
27
+ | undefined = dataToProcess.externalStatusPageResponse;
28
+
29
+ let overTimeValue: Array<number | boolean> | number | boolean | undefined =
30
+ undefined;
31
+
32
+ if (
33
+ input.criteriaFilter.evaluateOverTime &&
34
+ input.criteriaFilter.evaluateOverTimeOptions
35
+ ) {
36
+ try {
37
+ overTimeValue = await EvaluateOverTime.getValueOverTime({
38
+ projectId: (input.dataToProcess as ProbeMonitorResponse).projectId,
39
+ monitorId: input.dataToProcess.monitorId!,
40
+ evaluateOverTimeOptions: input.criteriaFilter.evaluateOverTimeOptions,
41
+ metricType: input.criteriaFilter.checkOn,
42
+ });
43
+
44
+ if (Array.isArray(overTimeValue) && overTimeValue.length === 0) {
45
+ overTimeValue = undefined;
46
+ }
47
+ } catch (err) {
48
+ logger.error(
49
+ `Error in getting over time value for ${input.criteriaFilter.checkOn}`,
50
+ );
51
+ logger.error(err);
52
+ overTimeValue = undefined;
53
+ }
54
+ }
55
+
56
+ // Check if external status page is online
57
+ if (input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageIsOnline) {
58
+ const currentIsOnline: boolean | Array<boolean> =
59
+ (overTimeValue as Array<boolean>) ||
60
+ (input.dataToProcess as ProbeMonitorResponse).isOnline;
61
+
62
+ return CompareCriteria.compareCriteriaBoolean({
63
+ value: currentIsOnline,
64
+ criteriaFilter: input.criteriaFilter,
65
+ });
66
+ }
67
+
68
+ // Check external status page response time
69
+ if (
70
+ input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageResponseTime
71
+ ) {
72
+ threshold = CompareCriteria.convertToNumber(threshold);
73
+
74
+ if (threshold === null || threshold === undefined) {
75
+ return null;
76
+ }
77
+
78
+ const currentResponseTime: number | Array<number> =
79
+ (overTimeValue as Array<number>) ||
80
+ externalStatusPageResponse?.responseTimeInMs ||
81
+ (input.dataToProcess as ProbeMonitorResponse).responseTimeInMs;
82
+
83
+ if (currentResponseTime === null || currentResponseTime === undefined) {
84
+ return null;
85
+ }
86
+
87
+ return CompareCriteria.compareCriteriaNumbers({
88
+ value: currentResponseTime,
89
+ threshold: threshold as number,
90
+ criteriaFilter: input.criteriaFilter,
91
+ });
92
+ }
93
+
94
+ // Check overall status
95
+ if (
96
+ input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageOverallStatus
97
+ ) {
98
+ if (!externalStatusPageResponse?.overallStatus) {
99
+ return null;
100
+ }
101
+
102
+ return CompareCriteria.compareCriteriaStrings({
103
+ value: externalStatusPageResponse.overallStatus,
104
+ threshold: String(threshold),
105
+ criteriaFilter: input.criteriaFilter,
106
+ });
107
+ }
108
+
109
+ // Check component status
110
+ if (
111
+ input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageComponentStatus
112
+ ) {
113
+ if (
114
+ !externalStatusPageResponse?.componentStatuses ||
115
+ externalStatusPageResponse.componentStatuses.length === 0
116
+ ) {
117
+ return null;
118
+ }
119
+
120
+ // Check if any component status matches the criteria
121
+ for (const component of externalStatusPageResponse.componentStatuses) {
122
+ const result: string | null = CompareCriteria.compareCriteriaStrings({
123
+ value: component.status,
124
+ threshold: String(threshold),
125
+ criteriaFilter: input.criteriaFilter,
126
+ });
127
+
128
+ if (result) {
129
+ return `Component "${component.name}": ${result}`;
130
+ }
131
+ }
132
+
133
+ return null;
134
+ }
135
+
136
+ // Check active incidents count
137
+ if (
138
+ input.criteriaFilter.checkOn === CheckOn.ExternalStatusPageActiveIncidents
139
+ ) {
140
+ threshold = CompareCriteria.convertToNumber(threshold);
141
+
142
+ if (threshold === null || threshold === undefined) {
143
+ return null;
144
+ }
145
+
146
+ const activeIncidents: number =
147
+ externalStatusPageResponse?.activeIncidentCount || 0;
148
+
149
+ return CompareCriteria.compareCriteriaNumbers({
150
+ value: activeIncidents,
151
+ threshold: threshold as number,
152
+ criteriaFilter: input.criteriaFilter,
153
+ });
154
+ }
155
+
156
+ return null;
157
+ }
158
+ }
@@ -14,6 +14,7 @@ import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
14
14
  import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
15
15
  import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
16
16
  import DomainMonitorCriteria from "./Criteria/DomainMonitorCriteria";
17
+ import ExternalStatusPageMonitorCriteria from "./Criteria/ExternalStatusPageMonitorCriteria";
17
18
  import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
18
19
  import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
19
20
  import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
@@ -519,6 +520,20 @@ ${contextBlock}
519
520
  }
520
521
  }
521
522
 
523
+ if (input.monitor.monitorType === MonitorType.ExternalStatusPage) {
524
+ const externalStatusPageResult: string | null =
525
+ await ExternalStatusPageMonitorCriteria.isMonitorInstanceCriteriaFilterMet(
526
+ {
527
+ dataToProcess: input.dataToProcess,
528
+ criteriaFilter: input.criteriaFilter,
529
+ },
530
+ );
531
+
532
+ if (externalStatusPageResult) {
533
+ return externalStatusPageResult;
534
+ }
535
+ }
536
+
522
537
  return null;
523
538
  }
524
539
 
@@ -18,6 +18,9 @@ import DnsMonitorResponse, {
18
18
  DnsRecordResponse,
19
19
  } from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
20
20
  import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
21
+ import ExternalStatusPageMonitorResponse, {
22
+ ExternalStatusPageComponentStatus,
23
+ } from "../../../Types/Monitor/ExternalStatusPageMonitor/ExternalStatusPageMonitorResponse";
21
24
  import Typeof from "../../../Types/Typeof";
22
25
  import VMUtil from "../VM/VMAPI";
23
26
  import DataToProcess from "./DataToProcess";
@@ -298,6 +301,35 @@ export default class MonitorTemplateUtil {
298
301
  dnssec: domainResponse?.dnssec,
299
302
  } as JSONObject;
300
303
  }
304
+
305
+ if (data.monitorType === MonitorType.ExternalStatusPage) {
306
+ const externalStatusPageResponse:
307
+ | ExternalStatusPageMonitorResponse
308
+ | undefined = (data.dataToProcess as ProbeMonitorResponse)
309
+ .externalStatusPageResponse;
310
+
311
+ storageMap = {
312
+ isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline,
313
+ responseTimeInMs: externalStatusPageResponse?.responseTimeInMs,
314
+ failureCause: externalStatusPageResponse?.failureCause,
315
+ overallStatus: externalStatusPageResponse?.overallStatus,
316
+ activeIncidentCount: externalStatusPageResponse?.activeIncidentCount,
317
+ } as JSONObject;
318
+
319
+ // Add component statuses
320
+ if (externalStatusPageResponse?.componentStatuses) {
321
+ storageMap["componentStatuses"] =
322
+ externalStatusPageResponse.componentStatuses.map(
323
+ (component: ExternalStatusPageComponentStatus) => {
324
+ return {
325
+ name: component.name,
326
+ status: component.status,
327
+ description: component.description,
328
+ };
329
+ },
330
+ );
331
+ }
332
+ }
301
333
  } catch (err) {
302
334
  logger.error(err);
303
335
  }