@oneuptime/common 11.3.8 → 11.3.10

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 (80) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +27 -0
  2. package/Models/AnalyticsModels/Log.ts +1 -0
  3. package/Models/AnalyticsModels/Metric.ts +1 -0
  4. package/Models/AnalyticsModels/MutableMetric.ts +333 -0
  5. package/Models/AnalyticsModels/Profile.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +1 -0
  7. package/Server/Infrastructure/Queue.ts +19 -2
  8. package/Server/Infrastructure/QueueWorker.ts +67 -1
  9. package/Server/Services/AlertService.ts +167 -206
  10. package/Server/Services/AlertStateTimelineService.ts +121 -1
  11. package/Server/Services/AnalyticsDatabaseService.ts +19 -23
  12. package/Server/Services/IncidentService.ts +179 -252
  13. package/Server/Services/IncidentStateTimelineService.ts +120 -1
  14. package/Server/Services/Index.ts +2 -0
  15. package/Server/Services/MetricAggregationService.ts +73 -3
  16. package/Server/Services/MetricService.ts +228 -0
  17. package/Server/Services/MutableMetricService.ts +275 -0
  18. package/Server/Services/TelemetryAttributeService.ts +186 -5
  19. package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +2 -0
  20. package/Server/Utils/AnalyticsDatabase/Statement.ts +2 -0
  21. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +81 -2
  22. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +143 -0
  23. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +208 -0
  24. package/Tests/Types/Metrics/MetricPipelineRuleFilterCondition.test.ts +208 -0
  25. package/Tests/Types/Monitor/MonitorStepEntityScope.test.ts +0 -56
  26. package/Types/AnalyticsDatabase/AnalyticsTableEngine.ts +1 -0
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  28. package/Types/AnalyticsDatabase/TableColumn.ts +16 -0
  29. package/Types/Monitor/MonitorStepMetricMonitor.ts +0 -27
  30. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +24 -0
  31. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  32. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  33. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  34. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  35. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  36. package/build/dist/Models/AnalyticsModels/MutableMetric.js +275 -0
  37. package/build/dist/Models/AnalyticsModels/MutableMetric.js.map +1 -0
  38. package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
  39. package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
  40. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  41. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  42. package/build/dist/Server/Infrastructure/Queue.js +14 -2
  43. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  44. package/build/dist/Server/Infrastructure/QueueWorker.js +45 -1
  45. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  46. package/build/dist/Server/Services/AlertService.js +133 -170
  47. package/build/dist/Server/Services/AlertService.js.map +1 -1
  48. package/build/dist/Server/Services/AlertStateTimelineService.js +97 -0
  49. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  50. package/build/dist/Server/Services/AnalyticsDatabaseService.js +16 -15
  51. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  52. package/build/dist/Server/Services/IncidentService.js +142 -207
  53. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  54. package/build/dist/Server/Services/IncidentStateTimelineService.js +97 -0
  55. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  56. package/build/dist/Server/Services/Index.js +2 -0
  57. package/build/dist/Server/Services/Index.js.map +1 -1
  58. package/build/dist/Server/Services/MetricAggregationService.js +49 -3
  59. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  60. package/build/dist/Server/Services/MetricService.js +155 -0
  61. package/build/dist/Server/Services/MetricService.js.map +1 -1
  62. package/build/dist/Server/Services/MutableMetricService.js +182 -0
  63. package/build/dist/Server/Services/MutableMetricService.js.map +1 -0
  64. package/build/dist/Server/Services/TelemetryAttributeService.js +132 -7
  65. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  66. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +2 -0
  67. package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -1
  68. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -0
  69. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +66 -2
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js +1 -0
  73. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js.map +1 -1
  74. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  75. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  76. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  77. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  78. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +0 -13
  79. package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
  80. package/package.json +1 -1
@@ -43,11 +43,12 @@ import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
43
43
  import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
44
44
  import User from "../../Models/DatabaseModels/User";
45
45
  import { IsBillingEnabled } from "../EnvironmentConfig";
46
- import MetricService from "./MetricService";
46
+ import MutableMetricService from "./MutableMetricService";
47
47
  import GlobalConfigService from "./GlobalConfigService";
48
48
  import GlobalConfig from "../../Models/DatabaseModels/GlobalConfig";
49
49
  import IncidentMetricType from "../../Types/Incident/IncidentMetricType";
50
- import Metric, { MetricPointType } from "../../Models/AnalyticsModels/Metric";
50
+ import MutableMetric from "../../Models/AnalyticsModels/MutableMetric";
51
+ import { MetricPointType } from "../../Models/AnalyticsModels/Metric";
51
52
  import ServiceType from "../../Types/Telemetry/ServiceType";
52
53
  import OneUptimeDate from "../../Types/Date";
53
54
  import TelemetryUtil from "../Utils/Telemetry/Telemetry";
@@ -1765,12 +1766,14 @@ ${incident.remediationNotes || "No remediation notes provided."}
1765
1766
 
1766
1767
  // only emit if the incident has been resolved
1767
1768
  if (resolvedTimeline && resolvedTimeline.startsAt) {
1768
- const postmortemMetric: Metric = new Metric();
1769
+ const postmortemMetric: MutableMetric = new MutableMetric();
1769
1770
  postmortemMetric.projectId = projectId;
1770
1771
  postmortemMetric.primaryEntityId = incidentId;
1771
1772
  postmortemMetric.primaryEntityType = ServiceType.Incident;
1772
1773
  postmortemMetric.name =
1773
1774
  IncidentMetricType.PostmortemCompletionTime;
1775
+ postmortemMetric.metricPointId =
1776
+ IncidentMetricType.PostmortemCompletionTime;
1774
1777
  postmortemMetric.value = OneUptimeDate.getDifferenceInSeconds(
1775
1778
  postmortemPostedAt,
1776
1779
  resolvedTimeline.startsAt,
@@ -1794,11 +1797,8 @@ ${incident.remediationNotes || "No remediation notes provided."}
1794
1797
  postmortemRetentionDays,
1795
1798
  );
1796
1799
 
1797
- await MetricService.create({
1798
- data: postmortemMetric,
1799
- props: {
1800
- isRoot: true,
1801
- },
1800
+ await MutableMetricService.createMutableMetrics({
1801
+ metrics: [postmortemMetric],
1802
1802
  });
1803
1803
 
1804
1804
  const postmortemMetricType: MetricType = new MetricType();
@@ -2019,11 +2019,12 @@ ${incidentSeverity.name}
2019
2019
 
2020
2020
  // emit severity change metric
2021
2021
  try {
2022
- const severityChangeMetric: Metric = new Metric();
2022
+ const severityChangeMetric: MutableMetric = new MutableMetric();
2023
2023
  severityChangeMetric.projectId = projectId;
2024
2024
  severityChangeMetric.primaryEntityId = incidentId;
2025
2025
  severityChangeMetric.primaryEntityType = ServiceType.Incident;
2026
2026
  severityChangeMetric.name = IncidentMetricType.SeverityChange;
2027
+ severityChangeMetric.metricPointId = `severity-change:${ObjectID.generate().toString()}`;
2027
2028
  severityChangeMetric.value = 1;
2028
2029
  severityChangeMetric.attributes = {
2029
2030
  incidentId: incidentId.toString(),
@@ -2046,11 +2047,8 @@ ${incidentSeverity.name}
2046
2047
  severityRetentionDays,
2047
2048
  );
2048
2049
 
2049
- await MetricService.create({
2050
- data: severityChangeMetric,
2051
- props: {
2052
- isRoot: true,
2053
- },
2050
+ await MutableMetricService.createMutableMetrics({
2051
+ metrics: [severityChangeMetric],
2054
2052
  });
2055
2053
 
2056
2054
  const severityChangeMetricType: MetricType = new MetricType();
@@ -2492,14 +2490,18 @@ ${incidentSeverity.name}
2492
2490
  }
2493
2491
 
2494
2492
  if (incident.projectId && incident.id) {
2495
- await MetricService.deleteBy({
2496
- query: {
2497
- projectId: incident.projectId,
2498
- primaryEntityId: incident.id,
2499
- },
2500
- props: {
2501
- isRoot: true,
2502
- },
2493
+ const metricRetentionDays: number =
2494
+ await this.getMetricRetentionDays();
2495
+
2496
+ await MutableMetricService.tombstoneEntityMetrics({
2497
+ projectId: incident.projectId,
2498
+ primaryEntityId: incident.id,
2499
+ primaryEntityType: ServiceType.Incident,
2500
+ metricNames: Object.values(IncidentMetricType),
2501
+ retentionDate: OneUptimeDate.addRemoveDays(
2502
+ OneUptimeDate.getCurrentDate(),
2503
+ metricRetentionDays,
2504
+ ),
2503
2505
  });
2504
2506
  }
2505
2507
  }
@@ -2639,6 +2641,7 @@ ${incidentSeverity.name}
2639
2641
  select: {
2640
2642
  projectId: true,
2641
2643
  declaredAt: true,
2644
+ postmortemPostedAt: true,
2642
2645
  monitors: {
2643
2646
  _id: true,
2644
2647
  name: true,
@@ -2740,6 +2743,7 @@ ${incidentSeverity.name}
2740
2743
  incidentId: data.incidentId,
2741
2744
  },
2742
2745
  select: {
2746
+ _id: true,
2743
2747
  projectId: true,
2744
2748
  incidentStateId: true,
2745
2749
  incidentState: {
@@ -2765,18 +2769,10 @@ ${incidentSeverity.name}
2765
2769
  incidentStateTimelines[0];
2766
2770
 
2767
2771
  /*
2768
- * Serialize concurrent refreshes for this incident across pods. The
2769
- * write-once guard below is read-then-insert (findBy existing, then a
2770
- * conditional createMany). Both call sites fire refresh fire-and-forget
2771
- * after releasing the per-incident state-timeline mutex, so two
2772
- * close-together transitions (e.g. auto-acknowledge then auto-resolve)
2773
- * could interleave: both read a snapshot missing a metric and both
2774
- * insert it. The Metric table is a plain MergeTree with no row collapse,
2775
- * so those duplicates would be permanent and re-inflate the very Sum/Avg
2776
- * widgets this change fixes. A best-effort distributed lock keyed on the
2777
- * incident makes the read+insert atomic; if the lock can't be taken we
2778
- * still proceed (the existence check alone covers the common sequential
2779
- * case).
2772
+ * Serialize concurrent refreshes for this incident across pods. Mutable
2773
+ * metrics are versioned inserts, but the replace operation also tombstones
2774
+ * stale metric-point identities. The lock keeps that read+insert cycle
2775
+ * ordered for each incident.
2780
2776
  */
2781
2777
  let metricRefreshMutex: SemaphoreMutex | null = null;
2782
2778
  try {
@@ -2796,84 +2792,7 @@ ${incidentSeverity.name}
2796
2792
  }
2797
2793
 
2798
2794
  try {
2799
- /*
2800
- * Write-once metrics — no deletes.
2801
- *
2802
- * The raw Metric table (`MetricItemV3`) feeds an AggregatingMergeTree
2803
- * materialized view (`MetricItemAggMV1m_mv`) that accumulates
2804
- * `sumState`/`countState` per (projectId, name, primaryEntityId,
2805
- * minute). The MV trigger fires only on INSERT — an `ALTER ... DELETE`
2806
- * mutation on the source rolls back nothing in the MV. So the old
2807
- * "delete every metric for this incident, then re-insert" refresh both
2808
- * (a) inflated those rollups (the Sum-of-IncidentCount widget read high)
2809
- * and (b) issued one heavy `ALTER ... DELETE` mutation per state
2810
- * transition per incident — a mutation storm.
2811
- *
2812
- * Every incident metric has a single eventually-final value:
2813
- * IncidentCount is constant `value = 1`; MTTA/MTTR/duration/time-in-state
2814
- * are each fixed once the relevant state transition has happened. So we
2815
- * load what we've already emitted for this incident and insert each
2816
- * metric exactly once, when it becomes final. Repeat refreshes that find
2817
- * nothing new are pure no-ops (no insert, no mutation).
2818
- */
2819
- const existingMetrics: Array<Metric> = await MetricService.findBy({
2820
- query: {
2821
- projectId: incident.projectId,
2822
- primaryEntityId: data.incidentId,
2823
- },
2824
- select: {
2825
- name: true,
2826
- time: true,
2827
- attributes: true,
2828
- },
2829
- skip: 0,
2830
- limit: LIMIT_PER_PROJECT,
2831
- props: {
2832
- isRoot: true,
2833
- },
2834
- });
2835
-
2836
- const existingMetricNames: Set<string> = new Set<string>(
2837
- existingMetrics
2838
- .map((metric: Metric) => {
2839
- return metric.name;
2840
- })
2841
- .filter(Boolean) as Array<string>,
2842
- );
2843
-
2844
- /*
2845
- * Identity of a TimeInState row: the state plus the instant that state
2846
- * began (a state can be entered more than once over a re-opened
2847
- * incident). Second granularity is robust against DateTime64 round-trip
2848
- * jitter and two distinct entries into the same state within one second
2849
- * never happen.
2850
- */
2851
- const buildTimeInStateKey: (
2852
- stateId: string | undefined,
2853
- startsAt: Date | undefined,
2854
- ) => string = (
2855
- stateId: string | undefined,
2856
- startsAt: Date | undefined,
2857
- ): string => {
2858
- return `${stateId || ""}|${
2859
- startsAt ? OneUptimeDate.toUnixTimestamp(startsAt) : ""
2860
- }`;
2861
- };
2862
-
2863
- const existingTimeInStateKeys: Set<string> = new Set<string>(
2864
- existingMetrics
2865
- .filter((metric: Metric) => {
2866
- return metric.name === IncidentMetricType.TimeInState;
2867
- })
2868
- .map((metric: Metric) => {
2869
- return buildTimeInStateKey(
2870
- metric.attributes?.["incidentStateId"] as string | undefined,
2871
- metric.time,
2872
- );
2873
- }),
2874
- );
2875
-
2876
- const itemsToSave: Array<Metric> = [];
2795
+ const itemsToSave: Array<MutableMetric> = [];
2877
2796
 
2878
2797
  const metricRetentionDays: number = await this.getMetricRetentionDays();
2879
2798
  const incidentMetricRetentionDate: Date = OneUptimeDate.addRemoveDays(
@@ -2921,36 +2840,27 @@ ${incidentSeverity.name}
2921
2840
  ownerTeamNames: ownerTeamNames.join(", "),
2922
2841
  };
2923
2842
 
2924
- /*
2925
- * Only emit IncidentCount on the very first refresh (i.e. when no
2926
- * existing IncidentCount row is present for this primaryEntityId). See
2927
- * the write-once note above — emitting it on every refresh would
2928
- * accumulate phantom `sumState` entries in the MV. By keeping the
2929
- * original row alive and never re-emitting, the dashboard Sum stays
2930
- * equal to the true count of distinct incidents.
2931
- */
2932
- if (!existingMetricNames.has(IncidentMetricType.IncidentCount)) {
2933
- const incidentCountMetric: Metric = new Metric();
2934
-
2935
- incidentCountMetric.projectId = incident.projectId;
2936
- incidentCountMetric.primaryEntityId = incident.id!;
2937
- incidentCountMetric.primaryEntityType = ServiceType.Incident;
2938
- incidentCountMetric.name = IncidentMetricType.IncidentCount;
2939
- incidentCountMetric.value = 1;
2940
- incidentCountMetric.attributes = { ...baseMetricAttributes };
2941
- incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
2942
- incidentCountMetric.attributes,
2943
- );
2843
+ const incidentCountMetric: MutableMetric = new MutableMetric();
2844
+
2845
+ incidentCountMetric.projectId = incident.projectId;
2846
+ incidentCountMetric.primaryEntityId = incident.id!;
2847
+ incidentCountMetric.primaryEntityType = ServiceType.Incident;
2848
+ incidentCountMetric.name = IncidentMetricType.IncidentCount;
2849
+ incidentCountMetric.metricPointId = IncidentMetricType.IncidentCount;
2850
+ incidentCountMetric.value = 1;
2851
+ incidentCountMetric.attributes = { ...baseMetricAttributes };
2852
+ incidentCountMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
2853
+ incidentCountMetric.attributes,
2854
+ );
2944
2855
 
2945
- incidentCountMetric.time = incidentStartsAt;
2946
- incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
2947
- incidentCountMetric.time,
2948
- );
2949
- incidentCountMetric.metricPointType = MetricPointType.Sum;
2950
- incidentCountMetric.retentionDate = incidentMetricRetentionDate;
2856
+ incidentCountMetric.time = incidentStartsAt;
2857
+ incidentCountMetric.timeUnixNano = OneUptimeDate.toUnixNano(
2858
+ incidentCountMetric.time,
2859
+ );
2860
+ incidentCountMetric.metricPointType = MetricPointType.Sum;
2861
+ incidentCountMetric.retentionDate = incidentMetricRetentionDate;
2951
2862
 
2952
- itemsToSave.push(incidentCountMetric);
2953
- }
2863
+ itemsToSave.push(incidentCountMetric);
2954
2864
 
2955
2865
  // Always register the metric type so it shows up in the type catalog.
2956
2866
  const metricType: MetricType = new MetricType();
@@ -2982,39 +2892,35 @@ ${incidentSeverity.name}
2982
2892
  metricType.unit = "seconds";
2983
2893
  metricTypesMap[IncidentMetricType.TimeToAcknowledge] = metricType;
2984
2894
 
2985
- // write-once: MTTA is fixed once the incident is first acknowledged.
2986
- if (!existingMetricNames.has(IncidentMetricType.TimeToAcknowledge)) {
2987
- const timeToAcknowledgeMetric: Metric = new Metric();
2988
-
2989
- timeToAcknowledgeMetric.projectId = incident.projectId;
2990
- timeToAcknowledgeMetric.primaryEntityId = incident.id!;
2991
- timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
2992
- timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
2993
- timeToAcknowledgeMetric.value =
2994
- OneUptimeDate.getDifferenceInSeconds(
2995
- ackIncidentStateTimeline?.startsAt ||
2996
- OneUptimeDate.getCurrentDate(),
2997
- incidentStartsAt,
2998
- );
2999
- timeToAcknowledgeMetric.attributes = { ...baseMetricAttributes };
3000
- timeToAcknowledgeMetric.attributeKeys =
3001
- TelemetryUtil.getAttributeKeys(
3002
- timeToAcknowledgeMetric.attributes,
3003
- );
2895
+ const timeToAcknowledgeMetric: MutableMetric = new MutableMetric();
3004
2896
 
3005
- timeToAcknowledgeMetric.time =
3006
- ackIncidentStateTimeline?.startsAt ||
3007
- incident.declaredAt ||
3008
- incident.createdAt ||
3009
- OneUptimeDate.getCurrentDate();
3010
- timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
3011
- timeToAcknowledgeMetric.time,
3012
- );
3013
- timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
3014
- timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
2897
+ timeToAcknowledgeMetric.projectId = incident.projectId;
2898
+ timeToAcknowledgeMetric.primaryEntityId = incident.id!;
2899
+ timeToAcknowledgeMetric.primaryEntityType = ServiceType.Incident;
2900
+ timeToAcknowledgeMetric.name = IncidentMetricType.TimeToAcknowledge;
2901
+ timeToAcknowledgeMetric.metricPointId =
2902
+ IncidentMetricType.TimeToAcknowledge;
2903
+ timeToAcknowledgeMetric.value = OneUptimeDate.getDifferenceInSeconds(
2904
+ ackIncidentStateTimeline?.startsAt ||
2905
+ OneUptimeDate.getCurrentDate(),
2906
+ incidentStartsAt,
2907
+ );
2908
+ timeToAcknowledgeMetric.attributes = { ...baseMetricAttributes };
2909
+ timeToAcknowledgeMetric.attributeKeys =
2910
+ TelemetryUtil.getAttributeKeys(timeToAcknowledgeMetric.attributes);
3015
2911
 
3016
- itemsToSave.push(timeToAcknowledgeMetric);
3017
- }
2912
+ timeToAcknowledgeMetric.time =
2913
+ ackIncidentStateTimeline?.startsAt ||
2914
+ incident.declaredAt ||
2915
+ incident.createdAt ||
2916
+ OneUptimeDate.getCurrentDate();
2917
+ timeToAcknowledgeMetric.timeUnixNano = OneUptimeDate.toUnixNano(
2918
+ timeToAcknowledgeMetric.time,
2919
+ );
2920
+ timeToAcknowledgeMetric.metricPointType = MetricPointType.Sum;
2921
+ timeToAcknowledgeMetric.retentionDate = incidentMetricRetentionDate;
2922
+
2923
+ itemsToSave.push(timeToAcknowledgeMetric);
3018
2924
  }
3019
2925
  }
3020
2926
 
@@ -3038,50 +2944,37 @@ ${incidentSeverity.name}
3038
2944
  metricType.unit = "seconds";
3039
2945
  metricTypesMap[IncidentMetricType.TimeToResolve] = metricType;
3040
2946
 
3041
- // write-once: MTTR is fixed once the incident is first resolved.
3042
- if (!existingMetricNames.has(IncidentMetricType.TimeToResolve)) {
3043
- const timeToResolveMetric: Metric = new Metric();
3044
-
3045
- timeToResolveMetric.projectId = incident.projectId;
3046
- timeToResolveMetric.primaryEntityId = incident.id!;
3047
- timeToResolveMetric.primaryEntityType = ServiceType.Incident;
3048
- timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
3049
- timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
3050
- resolvedIncidentStateTimeline?.startsAt ||
3051
- OneUptimeDate.getCurrentDate(),
3052
- incidentStartsAt,
3053
- );
3054
- timeToResolveMetric.attributes = { ...baseMetricAttributes };
3055
- timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
3056
- timeToResolveMetric.attributes,
3057
- );
2947
+ const timeToResolveMetric: MutableMetric = new MutableMetric();
2948
+
2949
+ timeToResolveMetric.projectId = incident.projectId;
2950
+ timeToResolveMetric.primaryEntityId = incident.id!;
2951
+ timeToResolveMetric.primaryEntityType = ServiceType.Incident;
2952
+ timeToResolveMetric.name = IncidentMetricType.TimeToResolve;
2953
+ timeToResolveMetric.metricPointId = IncidentMetricType.TimeToResolve;
2954
+ timeToResolveMetric.value = OneUptimeDate.getDifferenceInSeconds(
2955
+ resolvedIncidentStateTimeline?.startsAt ||
2956
+ OneUptimeDate.getCurrentDate(),
2957
+ incidentStartsAt,
2958
+ );
2959
+ timeToResolveMetric.attributes = { ...baseMetricAttributes };
2960
+ timeToResolveMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
2961
+ timeToResolveMetric.attributes,
2962
+ );
3058
2963
 
3059
- timeToResolveMetric.time =
3060
- resolvedIncidentStateTimeline?.startsAt ||
3061
- incident.declaredAt ||
3062
- incident.createdAt ||
3063
- OneUptimeDate.getCurrentDate();
3064
- timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
3065
- timeToResolveMetric.time,
3066
- );
3067
- timeToResolveMetric.metricPointType = MetricPointType.Sum;
3068
- timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
2964
+ timeToResolveMetric.time =
2965
+ resolvedIncidentStateTimeline?.startsAt ||
2966
+ incident.declaredAt ||
2967
+ incident.createdAt ||
2968
+ OneUptimeDate.getCurrentDate();
2969
+ timeToResolveMetric.timeUnixNano = OneUptimeDate.toUnixNano(
2970
+ timeToResolveMetric.time,
2971
+ );
2972
+ timeToResolveMetric.metricPointType = MetricPointType.Sum;
2973
+ timeToResolveMetric.retentionDate = incidentMetricRetentionDate;
3069
2974
 
3070
- itemsToSave.push(timeToResolveMetric);
3071
- }
2975
+ itemsToSave.push(timeToResolveMetric);
3072
2976
  }
3073
2977
 
3074
- /*
3075
- * Incident duration — write-once, finalized at resolution.
3076
- *
3077
- * The previous implementation recomputed duration as
3078
- * (latest state transition − start) on every refresh, so an open
3079
- * incident's duration grew and had to be deleted + re-inserted each
3080
- * time. With no deletes we emit a single final duration once the
3081
- * incident reaches a resolved state: (first resolution − start). An
3082
- * incident that is never resolved has no duration row (its lifetime is
3083
- * not yet final), which keeps the "Avg Duration" widget meaningful.
3084
- */
3085
2978
  if (isIncidentResolved && resolvedIncidentStateTimeline) {
3086
2979
  // register the metric type so the catalog stays complete across refreshes.
3087
2980
  const metricType: MetricType = new MetricType();
@@ -3090,34 +2983,70 @@ ${incidentSeverity.name}
3090
2983
  metricType.unit = "seconds";
3091
2984
  metricTypesMap[IncidentMetricType.IncidentDuration] = metricType;
3092
2985
 
3093
- if (!existingMetricNames.has(IncidentMetricType.IncidentDuration)) {
3094
- const incidentEndsAt: Date =
3095
- resolvedIncidentStateTimeline.startsAt ||
3096
- OneUptimeDate.getCurrentDate();
3097
-
3098
- const incidentDurationMetric: Metric = new Metric();
2986
+ const incidentEndsAt: Date =
2987
+ resolvedIncidentStateTimeline.startsAt ||
2988
+ OneUptimeDate.getCurrentDate();
2989
+
2990
+ const incidentDurationMetric: MutableMetric = new MutableMetric();
2991
+
2992
+ incidentDurationMetric.projectId = incident.projectId;
2993
+ incidentDurationMetric.primaryEntityId = incident.id!;
2994
+ incidentDurationMetric.primaryEntityType = ServiceType.Incident;
2995
+ incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
2996
+ incidentDurationMetric.metricPointId =
2997
+ IncidentMetricType.IncidentDuration;
2998
+ incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(
2999
+ incidentEndsAt,
3000
+ incidentStartsAt,
3001
+ );
3002
+ incidentDurationMetric.attributes = { ...baseMetricAttributes };
3003
+ incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
3004
+ incidentDurationMetric.attributes,
3005
+ );
3099
3006
 
3100
- incidentDurationMetric.projectId = incident.projectId;
3101
- incidentDurationMetric.primaryEntityId = incident.id!;
3102
- incidentDurationMetric.primaryEntityType = ServiceType.Incident;
3103
- incidentDurationMetric.name = IncidentMetricType.IncidentDuration;
3104
- incidentDurationMetric.value = OneUptimeDate.getDifferenceInSeconds(
3007
+ incidentDurationMetric.time = incidentEndsAt;
3008
+ incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
3009
+ incidentDurationMetric.time,
3010
+ );
3011
+ incidentDurationMetric.metricPointType = MetricPointType.Sum;
3012
+ incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
3013
+
3014
+ itemsToSave.push(incidentDurationMetric);
3015
+
3016
+ if (incident.postmortemPostedAt) {
3017
+ const postmortemMetricType: MetricType = new MetricType();
3018
+ postmortemMetricType.name =
3019
+ IncidentMetricType.PostmortemCompletionTime;
3020
+ postmortemMetricType.description =
3021
+ "Time from incident resolution to postmortem publication";
3022
+ postmortemMetricType.unit = "seconds";
3023
+ metricTypesMap[IncidentMetricType.PostmortemCompletionTime] =
3024
+ postmortemMetricType;
3025
+
3026
+ const postmortemMetric: MutableMetric = new MutableMetric();
3027
+
3028
+ postmortemMetric.projectId = incident.projectId;
3029
+ postmortemMetric.primaryEntityId = incident.id!;
3030
+ postmortemMetric.primaryEntityType = ServiceType.Incident;
3031
+ postmortemMetric.name = IncidentMetricType.PostmortemCompletionTime;
3032
+ postmortemMetric.metricPointId =
3033
+ IncidentMetricType.PostmortemCompletionTime;
3034
+ postmortemMetric.value = OneUptimeDate.getDifferenceInSeconds(
3035
+ incident.postmortemPostedAt,
3105
3036
  incidentEndsAt,
3106
- incidentStartsAt,
3107
3037
  );
3108
- incidentDurationMetric.attributes = { ...baseMetricAttributes };
3109
- incidentDurationMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
3110
- incidentDurationMetric.attributes,
3038
+ postmortemMetric.attributes = { ...baseMetricAttributes };
3039
+ postmortemMetric.attributeKeys = TelemetryUtil.getAttributeKeys(
3040
+ postmortemMetric.attributes,
3111
3041
  );
3112
-
3113
- incidentDurationMetric.time = incidentEndsAt;
3114
- incidentDurationMetric.timeUnixNano = OneUptimeDate.toUnixNano(
3115
- incidentDurationMetric.time,
3042
+ postmortemMetric.time = incident.postmortemPostedAt;
3043
+ postmortemMetric.timeUnixNano = OneUptimeDate.toUnixNano(
3044
+ postmortemMetric.time,
3116
3045
  );
3117
- incidentDurationMetric.metricPointType = MetricPointType.Sum;
3118
- incidentDurationMetric.retentionDate = incidentMetricRetentionDate;
3046
+ postmortemMetric.metricPointType = MetricPointType.Sum;
3047
+ postmortemMetric.retentionDate = incidentMetricRetentionDate;
3119
3048
 
3120
- itemsToSave.push(incidentDurationMetric);
3049
+ itemsToSave.push(postmortemMetric);
3121
3050
  }
3122
3051
  }
3123
3052
 
@@ -3127,24 +3056,16 @@ ${incidentSeverity.name}
3127
3056
  continue;
3128
3057
  }
3129
3058
 
3130
- // write-once: each completed state transition is emitted exactly once.
3131
- const timeInStateKey: string = buildTimeInStateKey(
3132
- timeline.incidentStateId?.toString(),
3133
- timeline.startsAt,
3134
- );
3135
- if (existingTimeInStateKeys.has(timeInStateKey)) {
3136
- continue;
3137
- }
3138
-
3139
3059
  const stateName: string =
3140
3060
  timeline.incidentState?.name?.toString() || "Unknown";
3141
3061
 
3142
- const timeInStateMetric: Metric = new Metric();
3062
+ const timeInStateMetric: MutableMetric = new MutableMetric();
3143
3063
 
3144
3064
  timeInStateMetric.projectId = incident.projectId;
3145
3065
  timeInStateMetric.primaryEntityId = incident.id!;
3146
3066
  timeInStateMetric.primaryEntityType = ServiceType.Incident;
3147
3067
  timeInStateMetric.name = IncidentMetricType.TimeInState;
3068
+ timeInStateMetric.metricPointId = `time-in-state:${timeline.id!.toString()}`;
3148
3069
  timeInStateMetric.value = OneUptimeDate.getDifferenceInSeconds(
3149
3070
  timeline.endsAt,
3150
3071
  timeline.startsAt,
@@ -3188,15 +3109,21 @@ ${incidentSeverity.name}
3188
3109
  metricTypesMap[timeInStateMetricType.name] = timeInStateMetricType;
3189
3110
  }
3190
3111
 
3191
- // write-once: a refresh that finds nothing new inserts nothing.
3192
- if (itemsToSave.length > 0) {
3193
- await MetricService.createMany({
3194
- items: itemsToSave,
3195
- props: {
3196
- isRoot: true,
3197
- },
3198
- });
3199
- }
3112
+ await MutableMetricService.replaceEntityMetrics({
3113
+ projectId: incident.projectId,
3114
+ primaryEntityId: incident.id!,
3115
+ primaryEntityType: ServiceType.Incident,
3116
+ metricNames: [
3117
+ IncidentMetricType.IncidentCount,
3118
+ IncidentMetricType.TimeToAcknowledge,
3119
+ IncidentMetricType.TimeToResolve,
3120
+ IncidentMetricType.IncidentDuration,
3121
+ IncidentMetricType.TimeInState,
3122
+ IncidentMetricType.PostmortemCompletionTime,
3123
+ ],
3124
+ metrics: itemsToSave,
3125
+ retentionDate: incidentMetricRetentionDate,
3126
+ });
3200
3127
 
3201
3128
  TelemetryUtil.indexMetricNameServiceNameMap({
3202
3129
  metricNameServiceNameMap: metricTypesMap,