@oneuptime/common 11.3.5 → 11.3.7

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 (65) hide show
  1. package/Server/Infrastructure/Queue.ts +103 -16
  2. package/Server/Services/AlertService.ts +302 -202
  3. package/Server/Services/AnalyticsDatabaseService.ts +9 -5
  4. package/Server/Services/IncidentService.ts +410 -306
  5. package/Server/Services/TeamMemberService.ts +38 -1
  6. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +31 -0
  7. package/Server/Utils/Monitor/IncomingRequestIncidentGrouping.ts +428 -0
  8. package/Server/Utils/Monitor/MonitorAlert.ts +158 -8
  9. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +26 -0
  10. package/Server/Utils/Monitor/MonitorIncident.ts +147 -2
  11. package/Server/Utils/Monitor/MonitorResource.ts +74 -0
  12. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +2 -1
  13. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +32 -0
  14. package/Tests/Server/Utils/Monitor/IncomingRequestIncidentGrouping.test.ts +389 -0
  15. package/Tests/Server/Utils/Monitor/SeriesAbsenceResolutionGuard.test.ts +177 -0
  16. package/Tests/Types/Events/Recurring.test.ts +475 -0
  17. package/Tests/Types/Monitor/CriteriaFilter.test.ts +348 -0
  18. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +331 -0
  19. package/Tests/UI/Components/Alert.test.tsx +23 -15
  20. package/Tests/UI/Components/Breadcrumbs.test.tsx +18 -6
  21. package/Tests/UI/Components/Button.test.tsx +3 -3
  22. package/Tests/UI/Components/DictionaryFilterOperator.test.ts +174 -0
  23. package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +4 -3
  24. package/Tests/UI/Components/Input.test.tsx +3 -2
  25. package/Tests/UI/Components/Pill.test.tsx +4 -2
  26. package/Tests/UI/Components/SideMenuItem.test.tsx +18 -8
  27. package/Tests/UI/Components/TextArea.test.tsx +6 -4
  28. package/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.ts +74 -0
  29. package/Types/Monitor/MonitorCriteriaInstance.ts +22 -0
  30. package/UI/Components/Dictionary/Dictionary.tsx +123 -50
  31. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +96 -1
  32. package/UI/Components/MasterPage/MasterPage.tsx +5 -3
  33. package/build/dist/Server/Infrastructure/Queue.js +83 -15
  34. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  35. package/build/dist/Server/Services/AlertService.js +255 -161
  36. package/build/dist/Server/Services/AlertService.js.map +1 -1
  37. package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -5
  38. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  39. package/build/dist/Server/Services/IncidentService.js +322 -236
  40. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  41. package/build/dist/Server/Services/TeamMemberService.js +37 -1
  42. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  43. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -0
  44. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  45. package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js +288 -0
  46. package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js.map +1 -0
  47. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +119 -8
  48. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  49. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +24 -0
  50. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  51. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -2
  52. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/MonitorResource.js +62 -0
  54. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  55. package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js +8 -0
  56. package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js.map +1 -0
  57. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
  58. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  59. package/build/dist/UI/Components/Dictionary/Dictionary.js +60 -9
  60. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  61. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +69 -0
  62. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
  63. package/build/dist/UI/Components/MasterPage/MasterPage.js +5 -3
  64. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  65. package/package.json +1 -1
@@ -49,7 +49,7 @@ import ServiceType from "../../Types/Telemetry/ServiceType";
49
49
  import OneUptimeDate from "../../Types/Date";
50
50
  import TelemetryUtil from "../Utils/Telemetry/Telemetry";
51
51
  import logger from "../Utils/Logger";
52
- import NotEqual from "../../Types/BaseDatabase/NotEqual";
52
+ import Semaphore from "../Infrastructure/Semaphore";
53
53
  import IncidentFeedService from "./IncidentFeedService";
54
54
  import IncidentSlaService from "./IncidentSlaService";
55
55
  import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
@@ -1941,7 +1941,7 @@ ${incidentSeverity.name}
1941
1941
  return Service.DEFAULT_METRIC_RETENTION_DAYS;
1942
1942
  }
1943
1943
  async refreshIncidentMetrics(data) {
1944
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1944
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
1945
1945
  const incident = await this.findOneById({
1946
1946
  id: data.incidentId,
1947
1947
  select: {
@@ -2061,254 +2061,340 @@ ${incidentSeverity.name}
2061
2061
  });
2062
2062
  const firstIncidentStateTimeline = incidentStateTimelines[0];
2063
2063
  /*
2064
- * Delete the existing metrics for this incident so the time-varying
2065
- * ones (TimeToAcknowledge / TimeToResolve / IncidentDuration /
2066
- * TimeInState) get rewritten with the latest state-timeline values
2067
- * on this refresh. IncidentCount is excluded from the delete: it is
2068
- * a constant `value = 1` keyed by `primaryEntityId + bucketTime` that
2069
- * never changes. Re-emitting it across refreshes inflated the
2070
- * 1-minute aggregating materialized view (`MetricItemAggMV1m_mv`),
2071
- * because the MV trigger only fires on inserts ALTER DELETE
2072
- * mutations don't roll back the previously-accumulated
2073
- * `sumState` / `countState`. That's why the Incident Dashboard
2074
- * sum-of-IncidentCount widget read ~33% higher than the actual
2075
- * unique-incident count.
2064
+ * Serialize concurrent refreshes for this incident across pods. The
2065
+ * write-once guard below is read-then-insert (findBy existing, then a
2066
+ * conditional createMany). Both call sites fire refresh fire-and-forget
2067
+ * after releasing the per-incident state-timeline mutex, so two
2068
+ * close-together transitions (e.g. auto-acknowledge then auto-resolve)
2069
+ * could interleave: both read a snapshot missing a metric and both
2070
+ * insert it. The Metric table is a plain MergeTree with no row collapse,
2071
+ * so those duplicates would be permanent and re-inflate the very Sum/Avg
2072
+ * widgets this change fixes. A best-effort distributed lock keyed on the
2073
+ * incident makes the read+insert atomic; if the lock can't be taken we
2074
+ * still proceed (the existence check alone covers the common sequential
2075
+ * case).
2076
2076
  */
2077
- await MetricService.deleteBy({
2078
- query: {
2079
- projectId: incident.projectId,
2080
- primaryEntityId: data.incidentId,
2081
- name: new NotEqual(IncidentMetricType.IncidentCount),
2082
- },
2083
- props: {
2084
- isRoot: true,
2085
- },
2086
- });
2087
- const itemsToSave = [];
2088
- const metricRetentionDays = await this.getMetricRetentionDays();
2089
- const incidentMetricRetentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), metricRetentionDays);
2090
- // now we need to create new metrics for this incident - TimeToAcknowledge, TimeToResolve, IncidentCount, IncidentDuration
2091
- const incidentStartsAt = (firstIncidentStateTimeline === null || firstIncidentStateTimeline === void 0 ? void 0 : firstIncidentStateTimeline.startsAt) ||
2092
- incident.declaredAt ||
2093
- incident.createdAt ||
2094
- OneUptimeDate.getCurrentDate();
2095
- const metricTypesMap = {};
2096
- /*
2097
- * common attributes shared by all incident metrics
2098
- * All values must be strings for ClickHouse Map(String, String) storage.
2099
- * Arrays are joined as comma-separated strings.
2100
- */
2101
- const baseMetricAttributes = {
2102
- incidentId: data.incidentId.toString(),
2103
- projectId: incident.projectId.toString(),
2104
- monitorIds: (((_a = incident.monitors) === null || _a === void 0 ? void 0 : _a.map((monitor) => {
2105
- var _a;
2106
- return (_a = monitor._id) === null || _a === void 0 ? void 0 : _a.toString();
2107
- }).filter(Boolean)) || []).join(", "),
2108
- monitorNames: (((_b = incident.monitors) === null || _b === void 0 ? void 0 : _b.map((monitor) => {
2077
+ let metricRefreshMutex = null;
2078
+ try {
2079
+ metricRefreshMutex = await Semaphore.lock({
2080
+ key: data.incidentId.toString(),
2081
+ namespace: "IncidentService.refreshIncidentMetrics",
2082
+ lockTimeout: 30000,
2083
+ });
2084
+ }
2085
+ catch (err) {
2086
+ logger.error(err, {
2087
+ projectId: (_a = incident.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
2088
+ incidentId: (_b = incident.id) === null || _b === void 0 ? void 0 : _b.toString(),
2089
+ });
2090
+ }
2091
+ try {
2092
+ /*
2093
+ * Write-once metrics — no deletes.
2094
+ *
2095
+ * The raw Metric table (`MetricItemV3`) feeds an AggregatingMergeTree
2096
+ * materialized view (`MetricItemAggMV1m_mv`) that accumulates
2097
+ * `sumState`/`countState` per (projectId, name, primaryEntityId,
2098
+ * minute). The MV trigger fires only on INSERT an `ALTER ... DELETE`
2099
+ * mutation on the source rolls back nothing in the MV. So the old
2100
+ * "delete every metric for this incident, then re-insert" refresh both
2101
+ * (a) inflated those rollups (the Sum-of-IncidentCount widget read high)
2102
+ * and (b) issued one heavy `ALTER ... DELETE` mutation per state
2103
+ * transition per incident — a mutation storm.
2104
+ *
2105
+ * Every incident metric has a single eventually-final value:
2106
+ * IncidentCount is constant `value = 1`; MTTA/MTTR/duration/time-in-state
2107
+ * are each fixed once the relevant state transition has happened. So we
2108
+ * load what we've already emitted for this incident and insert each
2109
+ * metric exactly once, when it becomes final. Repeat refreshes that find
2110
+ * nothing new are pure no-ops (no insert, no mutation).
2111
+ */
2112
+ const existingMetrics = await MetricService.findBy({
2113
+ query: {
2114
+ projectId: incident.projectId,
2115
+ primaryEntityId: data.incidentId,
2116
+ },
2117
+ select: {
2118
+ name: true,
2119
+ time: true,
2120
+ attributes: true,
2121
+ },
2122
+ skip: 0,
2123
+ limit: LIMIT_PER_PROJECT,
2124
+ props: {
2125
+ isRoot: true,
2126
+ },
2127
+ });
2128
+ const existingMetricNames = new Set(existingMetrics
2129
+ .map((metric) => {
2130
+ return metric.name;
2131
+ })
2132
+ .filter(Boolean));
2133
+ /*
2134
+ * Identity of a TimeInState row: the state plus the instant that state
2135
+ * began (a state can be entered more than once over a re-opened
2136
+ * incident). Second granularity is robust against DateTime64 round-trip
2137
+ * jitter and two distinct entries into the same state within one second
2138
+ * never happen.
2139
+ */
2140
+ const buildTimeInStateKey = (stateId, startsAt) => {
2141
+ return `${stateId || ""}|${startsAt ? OneUptimeDate.toUnixTimestamp(startsAt) : ""}`;
2142
+ };
2143
+ const existingTimeInStateKeys = new Set(existingMetrics
2144
+ .filter((metric) => {
2145
+ return metric.name === IncidentMetricType.TimeInState;
2146
+ })
2147
+ .map((metric) => {
2109
2148
  var _a;
2110
- return (_a = monitor.name) === null || _a === void 0 ? void 0 : _a.toString();
2111
- }).filter(Boolean)) || []).join(", "),
2112
- incidentSeverityId: (_d = (_c = incident.incidentSeverity) === null || _c === void 0 ? void 0 : _c._id) === null || _d === void 0 ? void 0 : _d.toString(),
2113
- incidentSeverityName: (_f = (_e = incident.incidentSeverity) === null || _e === void 0 ? void 0 : _e.name) === null || _f === void 0 ? void 0 : _f.toString(),
2114
- ownerUserIds: ownerUserIds.join(", "),
2115
- ownerUserNames: ownerUserNames.join(", "),
2116
- ownerTeamIds: ownerTeamIds.join(", "),
2117
- ownerTeamNames: ownerTeamNames.join(", "),
2118
- };
2119
- /*
2120
- * Only emit IncidentCount on the very first refresh (i.e. when no
2121
- * existing IncidentCount row is present for this primaryEntityId). See
2122
- * the delete comment above emitting it on every refresh would
2123
- * accumulate phantom `sumState` entries in the MV that ALTER
2124
- * DELETE can't undo. By keeping the original row alive and never
2125
- * re-emitting, the dashboard Sum stays equal to the true count of
2126
- * distinct incidents.
2127
- */
2128
- const incidentCountMetricExists = await MetricService.existsBy({
2129
- query: {
2130
- projectId: incident.projectId,
2131
- primaryEntityId: data.incidentId,
2132
- name: IncidentMetricType.IncidentCount,
2133
- },
2134
- props: {
2135
- isRoot: true,
2136
- },
2137
- });
2138
- if (!incidentCountMetricExists) {
2139
- const incidentCountMetric = new Metric();
2140
- incidentCountMetric.projectId = incident.projectId;
2141
- incidentCountMetric.primaryEntityId = incident.id;
2142
- incidentCountMetric.primaryEntityType = ServiceType.Incident;
2143
- incidentCountMetric.name = IncidentMetricType.IncidentCount;
2144
- incidentCountMetric.value = 1;
2145
- incidentCountMetric.attributes = Object.assign({}, baseMetricAttributes);
2146
- incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentCountMetric.attributes);
2147
- incidentCountMetric.time = incidentStartsAt;
2148
- incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentCountMetric.time);
2149
- incidentCountMetric.metricPointType = MetricPointType.Sum;
2150
- incidentCountMetric.retentionDate = incidentMetricRetentionDate;
2151
- itemsToSave.push(incidentCountMetric);
2152
- }
2153
- // Always register the metric type so it shows up in the type catalog.
2154
- const metricType = new MetricType();
2155
- metricType.name = IncidentMetricType.IncidentCount;
2156
- metricType.description = "Number of incidents created";
2157
- metricType.unit = "";
2158
- metricType.services = [];
2159
- metricTypesMap[IncidentMetricType.IncidentCount] = metricType;
2160
- // is the incident acknowledged?
2161
- const isIncidentAcknowledged = incidentStateTimelines.some((timeline) => {
2162
- var _a;
2163
- return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isAcknowledgedState;
2164
- });
2165
- if (isIncidentAcknowledged) {
2166
- const ackIncidentStateTimeline = incidentStateTimelines.find((timeline) => {
2149
+ return buildTimeInStateKey((_a = metric.attributes) === null || _a === void 0 ? void 0 : _a["incidentStateId"], metric.time);
2150
+ }));
2151
+ const itemsToSave = [];
2152
+ const metricRetentionDays = await this.getMetricRetentionDays();
2153
+ const incidentMetricRetentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), metricRetentionDays);
2154
+ // now we need to create new metrics for this incident - TimeToAcknowledge, TimeToResolve, IncidentCount, IncidentDuration
2155
+ const incidentStartsAt = (firstIncidentStateTimeline === null || firstIncidentStateTimeline === void 0 ? void 0 : firstIncidentStateTimeline.startsAt) ||
2156
+ incident.declaredAt ||
2157
+ incident.createdAt ||
2158
+ OneUptimeDate.getCurrentDate();
2159
+ const metricTypesMap = {};
2160
+ /*
2161
+ * common attributes shared by all incident metrics
2162
+ * All values must be strings for ClickHouse Map(String, String) storage.
2163
+ * Arrays are joined as comma-separated strings.
2164
+ */
2165
+ const baseMetricAttributes = {
2166
+ incidentId: data.incidentId.toString(),
2167
+ projectId: incident.projectId.toString(),
2168
+ monitorIds: (((_c = incident.monitors) === null || _c === void 0 ? void 0 : _c.map((monitor) => {
2169
+ var _a;
2170
+ return (_a = monitor._id) === null || _a === void 0 ? void 0 : _a.toString();
2171
+ }).filter(Boolean)) || []).join(", "),
2172
+ monitorNames: (((_d = incident.monitors) === null || _d === void 0 ? void 0 : _d.map((monitor) => {
2173
+ var _a;
2174
+ return (_a = monitor.name) === null || _a === void 0 ? void 0 : _a.toString();
2175
+ }).filter(Boolean)) || []).join(", "),
2176
+ incidentSeverityId: (_f = (_e = incident.incidentSeverity) === null || _e === void 0 ? void 0 : _e._id) === null || _f === void 0 ? void 0 : _f.toString(),
2177
+ incidentSeverityName: (_h = (_g = incident.incidentSeverity) === null || _g === void 0 ? void 0 : _g.name) === null || _h === void 0 ? void 0 : _h.toString(),
2178
+ ownerUserIds: ownerUserIds.join(", "),
2179
+ ownerUserNames: ownerUserNames.join(", "),
2180
+ ownerTeamIds: ownerTeamIds.join(", "),
2181
+ ownerTeamNames: ownerTeamNames.join(", "),
2182
+ };
2183
+ /*
2184
+ * Only emit IncidentCount on the very first refresh (i.e. when no
2185
+ * existing IncidentCount row is present for this primaryEntityId). See
2186
+ * the write-once note above — emitting it on every refresh would
2187
+ * accumulate phantom `sumState` entries in the MV. By keeping the
2188
+ * original row alive and never re-emitting, the dashboard Sum stays
2189
+ * equal to the true count of distinct incidents.
2190
+ */
2191
+ if (!existingMetricNames.has(IncidentMetricType.IncidentCount)) {
2192
+ const incidentCountMetric = new Metric();
2193
+ incidentCountMetric.projectId = incident.projectId;
2194
+ incidentCountMetric.primaryEntityId = incident.id;
2195
+ incidentCountMetric.primaryEntityType = ServiceType.Incident;
2196
+ incidentCountMetric.name = IncidentMetricType.IncidentCount;
2197
+ incidentCountMetric.value = 1;
2198
+ incidentCountMetric.attributes = Object.assign({}, baseMetricAttributes);
2199
+ incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentCountMetric.attributes);
2200
+ incidentCountMetric.time = incidentStartsAt;
2201
+ incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentCountMetric.time);
2202
+ incidentCountMetric.metricPointType = MetricPointType.Sum;
2203
+ incidentCountMetric.retentionDate = incidentMetricRetentionDate;
2204
+ itemsToSave.push(incidentCountMetric);
2205
+ }
2206
+ // Always register the metric type so it shows up in the type catalog.
2207
+ const metricType = new MetricType();
2208
+ metricType.name = IncidentMetricType.IncidentCount;
2209
+ metricType.description = "Number of incidents created";
2210
+ metricType.unit = "";
2211
+ metricType.services = [];
2212
+ metricTypesMap[IncidentMetricType.IncidentCount] = metricType;
2213
+ // is the incident acknowledged?
2214
+ const isIncidentAcknowledged = incidentStateTimelines.some((timeline) => {
2167
2215
  var _a;
2168
2216
  return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isAcknowledgedState;
2169
2217
  });
2170
- if (ackIncidentStateTimeline) {
2171
- const timeToAcknowledgeMetric = new Metric();
2172
- timeToAcknowledgeMetric.projectId = incident.projectId;
2173
- timeToAcknowledgeMetric.primaryEntityId = incident.id;
2174
- timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
2175
- timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
2176
- timeToAcknowledgeMetric.value = OneUptimeDate.getDifferenceInSeconds((ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) || OneUptimeDate.getCurrentDate(), incidentStartsAt);
2177
- timeToAcknowledgeMetric.attributes = Object.assign({}, baseMetricAttributes);
2178
- timeToAcknowledgeMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToAcknowledgeMetric.attributes);
2179
- timeToAcknowledgeMetric.time =
2180
- (ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
2181
- incident.declaredAt ||
2182
- incident.createdAt ||
2183
- OneUptimeDate.getCurrentDate();
2184
- timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToAcknowledgeMetric.time);
2185
- timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
2186
- timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
2187
- itemsToSave.push(timeToAcknowledgeMetric);
2188
- // add metric type for this to map.
2189
- const metricType = new MetricType();
2190
- metricType.name = timeToAcknowledgeMetric.name;
2191
- metricType.description = "Time taken to acknowledge the incident";
2192
- metricType.unit = "seconds";
2193
- // add to map.
2194
- metricTypesMap[timeToAcknowledgeMetric.name] = metricType;
2218
+ if (isIncidentAcknowledged) {
2219
+ const ackIncidentStateTimeline = incidentStateTimelines.find((timeline) => {
2220
+ var _a;
2221
+ return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isAcknowledgedState;
2222
+ });
2223
+ if (ackIncidentStateTimeline) {
2224
+ // register the metric type so the catalog stays complete across refreshes.
2225
+ const metricType = new MetricType();
2226
+ metricType.name = IncidentMetricType.TimeToAcknowledge;
2227
+ metricType.description = "Time taken to acknowledge the incident";
2228
+ metricType.unit = "seconds";
2229
+ metricTypesMap[IncidentMetricType.TimeToAcknowledge] = metricType;
2230
+ // write-once: MTTA is fixed once the incident is first acknowledged.
2231
+ if (!existingMetricNames.has(IncidentMetricType.TimeToAcknowledge)) {
2232
+ const timeToAcknowledgeMetric = new Metric();
2233
+ timeToAcknowledgeMetric.projectId = incident.projectId;
2234
+ timeToAcknowledgeMetric.primaryEntityId = incident.id;
2235
+ timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
2236
+ timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
2237
+ timeToAcknowledgeMetric.value =
2238
+ OneUptimeDate.getDifferenceInSeconds((ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
2239
+ OneUptimeDate.getCurrentDate(), incidentStartsAt);
2240
+ timeToAcknowledgeMetric.attributes = Object.assign({}, baseMetricAttributes);
2241
+ timeToAcknowledgeMetric.attributeKeys =
2242
+ TelemetryUtil.getAttributeKeys(timeToAcknowledgeMetric.attributes);
2243
+ timeToAcknowledgeMetric.time =
2244
+ (ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
2245
+ incident.declaredAt ||
2246
+ incident.createdAt ||
2247
+ OneUptimeDate.getCurrentDate();
2248
+ timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToAcknowledgeMetric.time);
2249
+ timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
2250
+ timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
2251
+ itemsToSave.push(timeToAcknowledgeMetric);
2252
+ }
2253
+ }
2195
2254
  }
2196
- }
2197
- // time to resolve
2198
- const isIncidentResolved = incidentStateTimelines.some((timeline) => {
2199
- var _a;
2200
- return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isResolvedState;
2201
- });
2202
- if (isIncidentResolved) {
2255
+ // time to resolve
2256
+ const isIncidentResolved = incidentStateTimelines.some((timeline) => {
2257
+ var _a;
2258
+ return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isResolvedState;
2259
+ });
2203
2260
  const resolvedIncidentStateTimeline = incidentStateTimelines.find((timeline) => {
2204
2261
  var _a;
2205
2262
  return (_a = timeline.incidentState) === null || _a === void 0 ? void 0 : _a.isResolvedState;
2206
2263
  });
2207
- if (resolvedIncidentStateTimeline) {
2208
- const timeToResolveMetric = new Metric();
2209
- timeToResolveMetric.projectId = incident.projectId;
2210
- timeToResolveMetric.primaryEntityId = incident.id;
2211
- timeToResolveMetric.primaryEntityType = ServiceType.Incident;
2212
- timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
2213
- timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds((resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2214
- OneUptimeDate.getCurrentDate(), incidentStartsAt);
2215
- timeToResolveMetric.attributes = Object.assign({}, baseMetricAttributes);
2216
- timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToResolveMetric.attributes);
2217
- timeToResolveMetric.time =
2218
- (resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2219
- incident.declaredAt ||
2220
- incident.createdAt ||
2221
- OneUptimeDate.getCurrentDate();
2222
- timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToResolveMetric.time);
2223
- timeToResolveMetric.metricPointType = MetricPointType.Sum;
2224
- timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
2225
- itemsToSave.push(timeToResolveMetric);
2226
- // add metric type for this to map.
2264
+ if (isIncidentResolved && resolvedIncidentStateTimeline) {
2265
+ // register the metric type so the catalog stays complete across refreshes.
2227
2266
  const metricType = new MetricType();
2228
- metricType.name = timeToResolveMetric.name;
2267
+ metricType.name = IncidentMetricType.TimeToResolve;
2229
2268
  metricType.description = "Time taken to resolve the incident";
2230
2269
  metricType.unit = "seconds";
2231
- // add to map.
2232
- metricTypesMap[timeToResolveMetric.name] = metricType;
2233
- }
2234
- }
2235
- // incident duration
2236
- const incidentDurationMetric = new Metric();
2237
- const lastIncidentStateTimeline = incidentStateTimelines[incidentStateTimelines.length - 1];
2238
- if (lastIncidentStateTimeline) {
2239
- const incidentEndsAt = lastIncidentStateTimeline.startsAt || OneUptimeDate.getCurrentDate();
2240
- // save metric.
2241
- incidentDurationMetric.projectId = incident.projectId;
2242
- incidentDurationMetric.primaryEntityId = incident.id;
2243
- incidentDurationMetric.primaryEntityType = ServiceType.Incident;
2244
- incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
2245
- incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(incidentEndsAt, incidentStartsAt);
2246
- incidentDurationMetric.attributes = Object.assign({}, baseMetricAttributes);
2247
- incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentDurationMetric.attributes);
2248
- incidentDurationMetric.time =
2249
- (lastIncidentStateTimeline === null || lastIncidentStateTimeline === void 0 ? void 0 : lastIncidentStateTimeline.startsAt) ||
2250
- incident.declaredAt ||
2251
- incident.createdAt ||
2252
- OneUptimeDate.getCurrentDate();
2253
- incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentDurationMetric.time);
2254
- incidentDurationMetric.metricPointType = MetricPointType.Sum;
2255
- incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
2256
- itemsToSave.push(incidentDurationMetric);
2257
- // add metric type for this to map.
2258
- const metricType = new MetricType();
2259
- metricType.name = incidentDurationMetric.name;
2260
- metricType.description = "Duration of the incident";
2261
- metricType.unit = "seconds";
2262
- // add to map.
2263
- metricTypesMap[incidentDurationMetric.name] = metricType;
2264
- }
2265
- // time-in-state metrics emit one metric per state transition that has a completed duration
2266
- for (const timeline of incidentStateTimelines) {
2267
- if (!timeline.startsAt || !timeline.endsAt) {
2268
- continue;
2269
- }
2270
- const stateName = ((_h = (_g = timeline.incidentState) === null || _g === void 0 ? void 0 : _g.name) === null || _h === void 0 ? void 0 : _h.toString()) || "Unknown";
2271
- const timeInStateMetric = new Metric();
2272
- timeInStateMetric.projectId = incident.projectId;
2273
- timeInStateMetric.primaryEntityId = incident.id;
2274
- timeInStateMetric.primaryEntityType = ServiceType.Incident;
2275
- timeInStateMetric.name = IncidentMetricType.TimeInState;
2276
- timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(timeline.endsAt, timeline.startsAt);
2277
- timeInStateMetric.attributes = Object.assign(Object.assign({}, baseMetricAttributes), { incidentStateName: stateName, incidentStateId: (_j = timeline.incidentStateId) === null || _j === void 0 ? void 0 : _j.toString(), isCreatedState: ((_l = (_k = timeline.incidentState) === null || _k === void 0 ? void 0 : _k.isCreatedState) === null || _l === void 0 ? void 0 : _l.toString()) || "false", isAcknowledgedState: ((_o = (_m = timeline.incidentState) === null || _m === void 0 ? void 0 : _m.isAcknowledgedState) === null || _o === void 0 ? void 0 : _o.toString()) || "false", isResolvedState: ((_q = (_p = timeline.incidentState) === null || _p === void 0 ? void 0 : _p.isResolvedState) === null || _q === void 0 ? void 0 : _q.toString()) || "false" });
2278
- timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeInStateMetric.attributes);
2279
- timeInStateMetric.time = timeline.startsAt;
2280
- timeInStateMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeInStateMetric.time);
2281
- timeInStateMetric.metricPointType = MetricPointType.Sum;
2282
- timeInStateMetric.retentionDate = incidentMetricRetentionDate;
2283
- itemsToSave.push(timeInStateMetric);
2284
- }
2285
- // add metric type for time-in-state to map (only once)
2286
- if (incidentStateTimelines.some((t) => {
2287
- return t.startsAt && t.endsAt;
2288
- })) {
2289
- const timeInStateMetricType = new MetricType();
2290
- timeInStateMetricType.name = IncidentMetricType.TimeInState;
2291
- timeInStateMetricType.description =
2292
- "Time spent in each incident state (e.g. Created, Investigating, Acknowledged)";
2293
- timeInStateMetricType.unit = "seconds";
2294
- metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
2295
- }
2296
- await MetricService.createMany({
2297
- items: itemsToSave,
2298
- props: {
2299
- isRoot: true,
2300
- },
2301
- });
2302
- TelemetryUtil.indexMetricNameServiceNameMap({
2303
- metricNameServiceNameMap: metricTypesMap,
2304
- projectId: incident.projectId,
2305
- }).catch((err) => {
2306
- var _a, _b;
2307
- logger.error(err, {
2308
- projectId: (_a = incident.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
2309
- incidentId: (_b = incident.id) === null || _b === void 0 ? void 0 : _b.toString(),
2270
+ metricTypesMap[IncidentMetricType.TimeToResolve] = metricType;
2271
+ // write-once: MTTR is fixed once the incident is first resolved.
2272
+ if (!existingMetricNames.has(IncidentMetricType.TimeToResolve)) {
2273
+ const timeToResolveMetric = new Metric();
2274
+ timeToResolveMetric.projectId = incident.projectId;
2275
+ timeToResolveMetric.primaryEntityId = incident.id;
2276
+ timeToResolveMetric.primaryEntityType = ServiceType.Incident;
2277
+ timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
2278
+ timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds((resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2279
+ OneUptimeDate.getCurrentDate(), incidentStartsAt);
2280
+ timeToResolveMetric.attributes = Object.assign({}, baseMetricAttributes);
2281
+ timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToResolveMetric.attributes);
2282
+ timeToResolveMetric.time =
2283
+ (resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2284
+ incident.declaredAt ||
2285
+ incident.createdAt ||
2286
+ OneUptimeDate.getCurrentDate();
2287
+ timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToResolveMetric.time);
2288
+ timeToResolveMetric.metricPointType = MetricPointType.Sum;
2289
+ timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
2290
+ itemsToSave.push(timeToResolveMetric);
2291
+ }
2292
+ }
2293
+ /*
2294
+ * Incident duration — write-once, finalized at resolution.
2295
+ *
2296
+ * The previous implementation recomputed duration as
2297
+ * (latest state transition − start) on every refresh, so an open
2298
+ * incident's duration grew and had to be deleted + re-inserted each
2299
+ * time. With no deletes we emit a single final duration once the
2300
+ * incident reaches a resolved state: (first resolution − start). An
2301
+ * incident that is never resolved has no duration row (its lifetime is
2302
+ * not yet final), which keeps the "Avg Duration" widget meaningful.
2303
+ */
2304
+ if (isIncidentResolved && resolvedIncidentStateTimeline) {
2305
+ // register the metric type so the catalog stays complete across refreshes.
2306
+ const metricType = new MetricType();
2307
+ metricType.name = IncidentMetricType.IncidentDuration;
2308
+ metricType.description = "Duration of the incident";
2309
+ metricType.unit = "seconds";
2310
+ metricTypesMap[IncidentMetricType.IncidentDuration] = metricType;
2311
+ if (!existingMetricNames.has(IncidentMetricType.IncidentDuration)) {
2312
+ const incidentEndsAt = resolvedIncidentStateTimeline.startsAt ||
2313
+ OneUptimeDate.getCurrentDate();
2314
+ const incidentDurationMetric = new Metric();
2315
+ incidentDurationMetric.projectId = incident.projectId;
2316
+ incidentDurationMetric.primaryEntityId = incident.id;
2317
+ incidentDurationMetric.primaryEntityType = ServiceType.Incident;
2318
+ incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
2319
+ incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(incidentEndsAt, incidentStartsAt);
2320
+ incidentDurationMetric.attributes = Object.assign({}, baseMetricAttributes);
2321
+ incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentDurationMetric.attributes);
2322
+ incidentDurationMetric.time = incidentEndsAt;
2323
+ incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentDurationMetric.time);
2324
+ incidentDurationMetric.metricPointType = MetricPointType.Sum;
2325
+ incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
2326
+ itemsToSave.push(incidentDurationMetric);
2327
+ }
2328
+ }
2329
+ // time-in-state metrics — emit one metric per state transition that has a completed duration
2330
+ for (const timeline of incidentStateTimelines) {
2331
+ if (!timeline.startsAt || !timeline.endsAt) {
2332
+ continue;
2333
+ }
2334
+ // write-once: each completed state transition is emitted exactly once.
2335
+ const timeInStateKey = buildTimeInStateKey((_j = timeline.incidentStateId) === null || _j === void 0 ? void 0 : _j.toString(), timeline.startsAt);
2336
+ if (existingTimeInStateKeys.has(timeInStateKey)) {
2337
+ continue;
2338
+ }
2339
+ const stateName = ((_l = (_k = timeline.incidentState) === null || _k === void 0 ? void 0 : _k.name) === null || _l === void 0 ? void 0 : _l.toString()) || "Unknown";
2340
+ const timeInStateMetric = new Metric();
2341
+ timeInStateMetric.projectId = incident.projectId;
2342
+ timeInStateMetric.primaryEntityId = incident.id;
2343
+ timeInStateMetric.primaryEntityType = ServiceType.Incident;
2344
+ timeInStateMetric.name = IncidentMetricType.TimeInState;
2345
+ timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(timeline.endsAt, timeline.startsAt);
2346
+ timeInStateMetric.attributes = Object.assign(Object.assign({}, baseMetricAttributes), { incidentStateName: stateName, incidentStateId: (_m = timeline.incidentStateId) === null || _m === void 0 ? void 0 : _m.toString(), isCreatedState: ((_p = (_o = timeline.incidentState) === null || _o === void 0 ? void 0 : _o.isCreatedState) === null || _p === void 0 ? void 0 : _p.toString()) || "false", isAcknowledgedState: ((_r = (_q = timeline.incidentState) === null || _q === void 0 ? void 0 : _q.isAcknowledgedState) === null || _r === void 0 ? void 0 : _r.toString()) || "false", isResolvedState: ((_t = (_s = timeline.incidentState) === null || _s === void 0 ? void 0 : _s.isResolvedState) === null || _t === void 0 ? void 0 : _t.toString()) || "false" });
2347
+ timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeInStateMetric.attributes);
2348
+ timeInStateMetric.time = timeline.startsAt;
2349
+ timeInStateMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeInStateMetric.time);
2350
+ timeInStateMetric.metricPointType = MetricPointType.Sum;
2351
+ timeInStateMetric.retentionDate = incidentMetricRetentionDate;
2352
+ itemsToSave.push(timeInStateMetric);
2353
+ }
2354
+ // add metric type for time-in-state to map (only once)
2355
+ if (incidentStateTimelines.some((t) => {
2356
+ return t.startsAt && t.endsAt;
2357
+ })) {
2358
+ const timeInStateMetricType = new MetricType();
2359
+ timeInStateMetricType.name = IncidentMetricType.TimeInState;
2360
+ timeInStateMetricType.description =
2361
+ "Time spent in each incident state (e.g. Created, Investigating, Acknowledged)";
2362
+ timeInStateMetricType.unit = "seconds";
2363
+ metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
2364
+ }
2365
+ // write-once: a refresh that finds nothing new inserts nothing.
2366
+ if (itemsToSave.length > 0) {
2367
+ await MetricService.createMany({
2368
+ items: itemsToSave,
2369
+ props: {
2370
+ isRoot: true,
2371
+ },
2372
+ });
2373
+ }
2374
+ TelemetryUtil.indexMetricNameServiceNameMap({
2375
+ metricNameServiceNameMap: metricTypesMap,
2376
+ projectId: incident.projectId,
2377
+ }).catch((err) => {
2378
+ var _a, _b;
2379
+ logger.error(err, {
2380
+ projectId: (_a = incident.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
2381
+ incidentId: (_b = incident.id) === null || _b === void 0 ? void 0 : _b.toString(),
2382
+ });
2310
2383
  });
2311
- });
2384
+ }
2385
+ finally {
2386
+ if (metricRefreshMutex) {
2387
+ try {
2388
+ await Semaphore.release(metricRefreshMutex);
2389
+ }
2390
+ catch (err) {
2391
+ logger.error(err, {
2392
+ projectId: (_u = incident.projectId) === null || _u === void 0 ? void 0 : _u.toString(),
2393
+ incidentId: (_v = incident.id) === null || _v === void 0 ? void 0 : _v.toString(),
2394
+ });
2395
+ }
2396
+ }
2397
+ }
2312
2398
  }
2313
2399
  async getWorkspaceChannelForIncident(data) {
2314
2400
  const incident = await this.findOneById({