@oneuptime/common 11.3.9 → 11.3.11

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 (54) hide show
  1. package/Models/AnalyticsModels/MutableMetric.ts +333 -0
  2. package/Server/Services/AlertService.ts +167 -206
  3. package/Server/Services/AlertStateTimelineService.ts +121 -1
  4. package/Server/Services/AnalyticsDatabaseService.ts +19 -23
  5. package/Server/Services/IncidentService.ts +179 -252
  6. package/Server/Services/IncidentStateTimelineService.ts +120 -1
  7. package/Server/Services/Index.ts +2 -0
  8. package/Server/Services/MetricAggregationService.ts +73 -3
  9. package/Server/Services/MetricService.ts +228 -0
  10. package/Server/Services/MutableMetricService.ts +275 -0
  11. package/Server/Services/TelemetryAttributeService.ts +183 -1
  12. package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +2 -0
  13. package/Server/Utils/AnalyticsDatabase/Statement.ts +2 -0
  14. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +145 -0
  15. package/Tests/Types/Monitor/MonitorStepEntityScope.test.ts +0 -56
  16. package/Types/AnalyticsDatabase/AnalyticsTableEngine.ts +1 -0
  17. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  18. package/Types/Dashboard/DashboardTemplates.ts +267 -7
  19. package/Types/Monitor/MonitorStepMetricMonitor.ts +0 -27
  20. package/build/dist/Models/AnalyticsModels/MutableMetric.js +275 -0
  21. package/build/dist/Models/AnalyticsModels/MutableMetric.js.map +1 -0
  22. package/build/dist/Server/Services/AlertService.js +133 -170
  23. package/build/dist/Server/Services/AlertService.js.map +1 -1
  24. package/build/dist/Server/Services/AlertStateTimelineService.js +97 -0
  25. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  26. package/build/dist/Server/Services/AnalyticsDatabaseService.js +16 -15
  27. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  28. package/build/dist/Server/Services/IncidentService.js +142 -207
  29. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  30. package/build/dist/Server/Services/IncidentStateTimelineService.js +97 -0
  31. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  32. package/build/dist/Server/Services/Index.js +2 -0
  33. package/build/dist/Server/Services/Index.js.map +1 -1
  34. package/build/dist/Server/Services/MetricAggregationService.js +49 -3
  35. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  36. package/build/dist/Server/Services/MetricService.js +155 -0
  37. package/build/dist/Server/Services/MetricService.js.map +1 -1
  38. package/build/dist/Server/Services/MutableMetricService.js +182 -0
  39. package/build/dist/Server/Services/MutableMetricService.js.map +1 -0
  40. package/build/dist/Server/Services/TelemetryAttributeService.js +131 -6
  41. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  42. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +2 -0
  43. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -1
  44. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -0
  45. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  46. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js +1 -0
  47. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js.map +1 -1
  48. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  49. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  50. package/build/dist/Types/Dashboard/DashboardTemplates.js +252 -6
  51. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  52. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +0 -13
  53. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
  54. package/package.json +1 -1
@@ -41,10 +41,11 @@ import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTime
41
41
  import Monitor from "../../Models/DatabaseModels/Monitor";
42
42
  import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
43
43
  import { IsBillingEnabled } from "../EnvironmentConfig";
44
- import MetricService from "./MetricService";
44
+ import MutableMetricService from "./MutableMetricService";
45
45
  import GlobalConfigService from "./GlobalConfigService";
46
46
  import IncidentMetricType from "../../Types/Incident/IncidentMetricType";
47
- import Metric, { MetricPointType } from "../../Models/AnalyticsModels/Metric";
47
+ import MutableMetric from "../../Models/AnalyticsModels/MutableMetric";
48
+ import { MetricPointType } from "../../Models/AnalyticsModels/Metric";
48
49
  import ServiceType from "../../Types/Telemetry/ServiceType";
49
50
  import OneUptimeDate from "../../Types/Date";
50
51
  import TelemetryUtil from "../Utils/Telemetry/Telemetry";
@@ -1319,12 +1320,14 @@ ${incident.remediationNotes || "No remediation notes provided."}
1319
1320
  });
1320
1321
  // only emit if the incident has been resolved
1321
1322
  if (resolvedTimeline && resolvedTimeline.startsAt) {
1322
- const postmortemMetric = new Metric();
1323
+ const postmortemMetric = new MutableMetric();
1323
1324
  postmortemMetric.projectId = projectId;
1324
1325
  postmortemMetric.primaryEntityId = incidentId;
1325
1326
  postmortemMetric.primaryEntityType = ServiceType.Incident;
1326
1327
  postmortemMetric.name =
1327
1328
  IncidentMetricType.PostmortemCompletionTime;
1329
+ postmortemMetric.metricPointId =
1330
+ IncidentMetricType.PostmortemCompletionTime;
1328
1331
  postmortemMetric.value = OneUptimeDate.getDifferenceInSeconds(postmortemPostedAt, resolvedTimeline.startsAt);
1329
1332
  postmortemMetric.attributes = {
1330
1333
  incidentId: incidentId.toString(),
@@ -1336,11 +1339,8 @@ ${incident.remediationNotes || "No remediation notes provided."}
1336
1339
  postmortemMetric.metricPointType = MetricPointType.Sum;
1337
1340
  const postmortemRetentionDays = await this.getMetricRetentionDays();
1338
1341
  postmortemMetric.retentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), postmortemRetentionDays);
1339
- await MetricService.create({
1340
- data: postmortemMetric,
1341
- props: {
1342
- isRoot: true,
1343
- },
1342
+ await MutableMetricService.createMutableMetrics({
1343
+ metrics: [postmortemMetric],
1344
1344
  });
1345
1345
  const postmortemMetricType = new MetricType();
1346
1346
  postmortemMetricType.name =
@@ -1500,11 +1500,12 @@ ${incidentSeverity.name}
1500
1500
  }
1501
1501
  // emit severity change metric
1502
1502
  try {
1503
- const severityChangeMetric = new Metric();
1503
+ const severityChangeMetric = new MutableMetric();
1504
1504
  severityChangeMetric.projectId = projectId;
1505
1505
  severityChangeMetric.primaryEntityId = incidentId;
1506
1506
  severityChangeMetric.primaryEntityType = ServiceType.Incident;
1507
1507
  severityChangeMetric.name = IncidentMetricType.SeverityChange;
1508
+ severityChangeMetric.metricPointId = `severity-change:${ObjectID.generate().toString()}`;
1508
1509
  severityChangeMetric.value = 1;
1509
1510
  severityChangeMetric.attributes = {
1510
1511
  incidentId: incidentId.toString(),
@@ -1519,11 +1520,8 @@ ${incidentSeverity.name}
1519
1520
  severityChangeMetric.metricPointType = MetricPointType.Sum;
1520
1521
  const severityRetentionDays = await this.getMetricRetentionDays();
1521
1522
  severityChangeMetric.retentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), severityRetentionDays);
1522
- await MetricService.create({
1523
- data: severityChangeMetric,
1524
- props: {
1525
- isRoot: true,
1526
- },
1523
+ await MutableMetricService.createMutableMetrics({
1524
+ metrics: [severityChangeMetric],
1527
1525
  });
1528
1526
  const severityChangeMetricType = new MetricType();
1529
1527
  severityChangeMetricType.name = IncidentMetricType.SeverityChange;
@@ -1847,14 +1845,13 @@ ${incidentSeverity.name}
1847
1845
  await this.markMonitorsActiveForMonitoring(incident.projectId, incident.monitors);
1848
1846
  }
1849
1847
  if (incident.projectId && incident.id) {
1850
- await MetricService.deleteBy({
1851
- query: {
1852
- projectId: incident.projectId,
1853
- primaryEntityId: incident.id,
1854
- },
1855
- props: {
1856
- isRoot: true,
1857
- },
1848
+ const metricRetentionDays = await this.getMetricRetentionDays();
1849
+ await MutableMetricService.tombstoneEntityMetrics({
1850
+ projectId: incident.projectId,
1851
+ primaryEntityId: incident.id,
1852
+ primaryEntityType: ServiceType.Incident,
1853
+ metricNames: Object.values(IncidentMetricType),
1854
+ retentionDate: OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), metricRetentionDays),
1858
1855
  });
1859
1856
  }
1860
1857
  }
@@ -1941,12 +1938,13 @@ ${incidentSeverity.name}
1941
1938
  return Service.DEFAULT_METRIC_RETENTION_DAYS;
1942
1939
  }
1943
1940
  async refreshIncidentMetrics(data) {
1944
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
1941
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
1945
1942
  const incident = await this.findOneById({
1946
1943
  id: data.incidentId,
1947
1944
  select: {
1948
1945
  projectId: true,
1949
1946
  declaredAt: true,
1947
+ postmortemPostedAt: true,
1950
1948
  monitors: {
1951
1949
  _id: true,
1952
1950
  name: true,
@@ -2039,6 +2037,7 @@ ${incidentSeverity.name}
2039
2037
  incidentId: data.incidentId,
2040
2038
  },
2041
2039
  select: {
2040
+ _id: true,
2042
2041
  projectId: true,
2043
2042
  incidentStateId: true,
2044
2043
  incidentState: {
@@ -2061,18 +2060,10 @@ ${incidentSeverity.name}
2061
2060
  });
2062
2061
  const firstIncidentStateTimeline = incidentStateTimelines[0];
2063
2062
  /*
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).
2063
+ * Serialize concurrent refreshes for this incident across pods. Mutable
2064
+ * metrics are versioned inserts, but the replace operation also tombstones
2065
+ * stale metric-point identities. The lock keeps that read+insert cycle
2066
+ * ordered for each incident.
2076
2067
  */
2077
2068
  let metricRefreshMutex = null;
2078
2069
  try {
@@ -2089,65 +2080,6 @@ ${incidentSeverity.name}
2089
2080
  });
2090
2081
  }
2091
2082
  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) => {
2148
- var _a;
2149
- return buildTimeInStateKey((_a = metric.attributes) === null || _a === void 0 ? void 0 : _a["incidentStateId"], metric.time);
2150
- }));
2151
2083
  const itemsToSave = [];
2152
2084
  const metricRetentionDays = await this.getMetricRetentionDays();
2153
2085
  const incidentMetricRetentionDate = OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), metricRetentionDays);
@@ -2180,29 +2112,20 @@ ${incidentSeverity.name}
2180
2112
  ownerTeamIds: ownerTeamIds.join(", "),
2181
2113
  ownerTeamNames: ownerTeamNames.join(", "),
2182
2114
  };
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
- }
2115
+ const incidentCountMetric = new MutableMetric();
2116
+ incidentCountMetric.projectId = incident.projectId;
2117
+ incidentCountMetric.primaryEntityId = incident.id;
2118
+ incidentCountMetric.primaryEntityType = ServiceType.Incident;
2119
+ incidentCountMetric.name = IncidentMetricType.IncidentCount;
2120
+ incidentCountMetric.metricPointId = IncidentMetricType.IncidentCount;
2121
+ incidentCountMetric.value = 1;
2122
+ incidentCountMetric.attributes = Object.assign({}, baseMetricAttributes);
2123
+ incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentCountMetric.attributes);
2124
+ incidentCountMetric.time = incidentStartsAt;
2125
+ incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentCountMetric.time);
2126
+ incidentCountMetric.metricPointType = MetricPointType.Sum;
2127
+ incidentCountMetric.retentionDate = incidentMetricRetentionDate;
2128
+ itemsToSave.push(incidentCountMetric);
2206
2129
  // Always register the metric type so it shows up in the type catalog.
2207
2130
  const metricType = new MetricType();
2208
2131
  metricType.name = IncidentMetricType.IncidentCount;
@@ -2227,29 +2150,27 @@ ${incidentSeverity.name}
2227
2150
  metricType.description = "Time taken to acknowledge the incident";
2228
2151
  metricType.unit = "seconds";
2229
2152
  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
- }
2153
+ const timeToAcknowledgeMetric = new MutableMetric();
2154
+ timeToAcknowledgeMetric.projectId = incident.projectId;
2155
+ timeToAcknowledgeMetric.primaryEntityId = incident.id;
2156
+ timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
2157
+ timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
2158
+ timeToAcknowledgeMetric.metricPointId =
2159
+ IncidentMetricType.TimeToAcknowledge;
2160
+ timeToAcknowledgeMetric.value = OneUptimeDate.getDifferenceInSeconds((ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
2161
+ OneUptimeDate.getCurrentDate(), incidentStartsAt);
2162
+ timeToAcknowledgeMetric.attributes = Object.assign({}, baseMetricAttributes);
2163
+ timeToAcknowledgeMetric.attributeKeys =
2164
+ TelemetryUtil.getAttributeKeys(timeToAcknowledgeMetric.attributes);
2165
+ timeToAcknowledgeMetric.time =
2166
+ (ackIncidentStateTimeline === null || ackIncidentStateTimeline === void 0 ? void 0 : ackIncidentStateTimeline.startsAt) ||
2167
+ incident.declaredAt ||
2168
+ incident.createdAt ||
2169
+ OneUptimeDate.getCurrentDate();
2170
+ timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToAcknowledgeMetric.time);
2171
+ timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
2172
+ timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
2173
+ itemsToSave.push(timeToAcknowledgeMetric);
2253
2174
  }
2254
2175
  }
2255
2176
  // time to resolve
@@ -2268,39 +2189,26 @@ ${incidentSeverity.name}
2268
2189
  metricType.description = "Time taken to resolve the incident";
2269
2190
  metricType.unit = "seconds";
2270
2191
  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
- }
2192
+ const timeToResolveMetric = new MutableMetric();
2193
+ timeToResolveMetric.projectId = incident.projectId;
2194
+ timeToResolveMetric.primaryEntityId = incident.id;
2195
+ timeToResolveMetric.primaryEntityType = ServiceType.Incident;
2196
+ timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
2197
+ timeToResolveMetric.metricPointId = IncidentMetricType.TimeToResolve;
2198
+ timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds((resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2199
+ OneUptimeDate.getCurrentDate(), incidentStartsAt);
2200
+ timeToResolveMetric.attributes = Object.assign({}, baseMetricAttributes);
2201
+ timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeToResolveMetric.attributes);
2202
+ timeToResolveMetric.time =
2203
+ (resolvedIncidentStateTimeline === null || resolvedIncidentStateTimeline === void 0 ? void 0 : resolvedIncidentStateTimeline.startsAt) ||
2204
+ incident.declaredAt ||
2205
+ incident.createdAt ||
2206
+ OneUptimeDate.getCurrentDate();
2207
+ timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeToResolveMetric.time);
2208
+ timeToResolveMetric.metricPointType = MetricPointType.Sum;
2209
+ timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
2210
+ itemsToSave.push(timeToResolveMetric);
2292
2211
  }
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
2212
  if (isIncidentResolved && resolvedIncidentStateTimeline) {
2305
2213
  // register the metric type so the catalog stays complete across refreshes.
2306
2214
  const metricType = new MetricType();
@@ -2308,22 +2216,47 @@ ${incidentSeverity.name}
2308
2216
  metricType.description = "Duration of the incident";
2309
2217
  metricType.unit = "seconds";
2310
2218
  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);
2219
+ const incidentEndsAt = resolvedIncidentStateTimeline.startsAt ||
2220
+ OneUptimeDate.getCurrentDate();
2221
+ const incidentDurationMetric = new MutableMetric();
2222
+ incidentDurationMetric.projectId = incident.projectId;
2223
+ incidentDurationMetric.primaryEntityId = incident.id;
2224
+ incidentDurationMetric.primaryEntityType = ServiceType.Incident;
2225
+ incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
2226
+ incidentDurationMetric.metricPointId =
2227
+ IncidentMetricType.IncidentDuration;
2228
+ incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(incidentEndsAt, incidentStartsAt);
2229
+ incidentDurationMetric.attributes = Object.assign({}, baseMetricAttributes);
2230
+ incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(incidentDurationMetric.attributes);
2231
+ incidentDurationMetric.time = incidentEndsAt;
2232
+ incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(incidentDurationMetric.time);
2233
+ incidentDurationMetric.metricPointType = MetricPointType.Sum;
2234
+ incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
2235
+ itemsToSave.push(incidentDurationMetric);
2236
+ if (incident.postmortemPostedAt) {
2237
+ const postmortemMetricType = new MetricType();
2238
+ postmortemMetricType.name =
2239
+ IncidentMetricType.PostmortemCompletionTime;
2240
+ postmortemMetricType.description =
2241
+ "Time from incident resolution to postmortem publication";
2242
+ postmortemMetricType.unit = "seconds";
2243
+ metricTypesMap[IncidentMetricType.PostmortemCompletionTime] =
2244
+ postmortemMetricType;
2245
+ const postmortemMetric = new MutableMetric();
2246
+ postmortemMetric.projectId = incident.projectId;
2247
+ postmortemMetric.primaryEntityId = incident.id;
2248
+ postmortemMetric.primaryEntityType = ServiceType.Incident;
2249
+ postmortemMetric.name = IncidentMetricType.PostmortemCompletionTime;
2250
+ postmortemMetric.metricPointId =
2251
+ IncidentMetricType.PostmortemCompletionTime;
2252
+ postmortemMetric.value = OneUptimeDate.getDifferenceInSeconds(incident.postmortemPostedAt, incidentEndsAt);
2253
+ postmortemMetric.attributes = Object.assign({}, baseMetricAttributes);
2254
+ postmortemMetric.attributeKeys = TelemetryUtil.getAttributeKeys(postmortemMetric.attributes);
2255
+ postmortemMetric.time = incident.postmortemPostedAt;
2256
+ postmortemMetric.timeUnixNano = OneUptimeDate.toUnixNano(postmortemMetric.time);
2257
+ postmortemMetric.metricPointType = MetricPointType.Sum;
2258
+ postmortemMetric.retentionDate = incidentMetricRetentionDate;
2259
+ itemsToSave.push(postmortemMetric);
2327
2260
  }
2328
2261
  }
2329
2262
  // time-in-state metrics — emit one metric per state transition that has a completed duration
@@ -2331,19 +2264,15 @@ ${incidentSeverity.name}
2331
2264
  if (!timeline.startsAt || !timeline.endsAt) {
2332
2265
  continue;
2333
2266
  }
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();
2267
+ const stateName = ((_k = (_j = timeline.incidentState) === null || _j === void 0 ? void 0 : _j.name) === null || _k === void 0 ? void 0 : _k.toString()) || "Unknown";
2268
+ const timeInStateMetric = new MutableMetric();
2341
2269
  timeInStateMetric.projectId = incident.projectId;
2342
2270
  timeInStateMetric.primaryEntityId = incident.id;
2343
2271
  timeInStateMetric.primaryEntityType = ServiceType.Incident;
2344
2272
  timeInStateMetric.name = IncidentMetricType.TimeInState;
2273
+ timeInStateMetric.metricPointId = `time-in-state:${timeline.id.toString()}`;
2345
2274
  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" });
2275
+ timeInStateMetric.attributes = Object.assign(Object.assign({}, baseMetricAttributes), { incidentStateName: stateName, incidentStateId: (_l = timeline.incidentStateId) === null || _l === void 0 ? void 0 : _l.toString(), isCreatedState: ((_o = (_m = timeline.incidentState) === null || _m === void 0 ? void 0 : _m.isCreatedState) === null || _o === void 0 ? void 0 : _o.toString()) || "false", isAcknowledgedState: ((_q = (_p = timeline.incidentState) === null || _p === void 0 ? void 0 : _p.isAcknowledgedState) === null || _q === void 0 ? void 0 : _q.toString()) || "false", isResolvedState: ((_s = (_r = timeline.incidentState) === null || _r === void 0 ? void 0 : _r.isResolvedState) === null || _s === void 0 ? void 0 : _s.toString()) || "false" });
2347
2276
  timeInStateMetric.attributeKeys = TelemetryUtil.getAttributeKeys(timeInStateMetric.attributes);
2348
2277
  timeInStateMetric.time = timeline.startsAt;
2349
2278
  timeInStateMetric.timeUnixNano = OneUptimeDate.toUnixNano(timeInStateMetric.time);
@@ -2362,15 +2291,21 @@ ${incidentSeverity.name}
2362
2291
  timeInStateMetricType.unit = "seconds";
2363
2292
  metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
2364
2293
  }
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
- }
2294
+ await MutableMetricService.replaceEntityMetrics({
2295
+ projectId: incident.projectId,
2296
+ primaryEntityId: incident.id,
2297
+ primaryEntityType: ServiceType.Incident,
2298
+ metricNames: [
2299
+ IncidentMetricType.IncidentCount,
2300
+ IncidentMetricType.TimeToAcknowledge,
2301
+ IncidentMetricType.TimeToResolve,
2302
+ IncidentMetricType.IncidentDuration,
2303
+ IncidentMetricType.TimeInState,
2304
+ IncidentMetricType.PostmortemCompletionTime,
2305
+ ],
2306
+ metrics: itemsToSave,
2307
+ retentionDate: incidentMetricRetentionDate,
2308
+ });
2374
2309
  TelemetryUtil.indexMetricNameServiceNameMap({
2375
2310
  metricNameServiceNameMap: metricTypesMap,
2376
2311
  projectId: incident.projectId,
@@ -2389,8 +2324,8 @@ ${incidentSeverity.name}
2389
2324
  }
2390
2325
  catch (err) {
2391
2326
  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(),
2327
+ projectId: (_t = incident.projectId) === null || _t === void 0 ? void 0 : _t.toString(),
2328
+ incidentId: (_u = incident.id) === null || _u === void 0 ? void 0 : _u.toString(),
2394
2329
  });
2395
2330
  }
2396
2331
  }