@oneuptime/common 10.0.71 → 10.0.72

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 (97) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/Incident.ts +55 -0
  3. package/Models/DatabaseModels/StatusPage.ts +80 -0
  4. package/Server/API/StatusPageAPI.ts +4 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Services/AnalyticsDatabaseService.ts +17 -7
  9. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
  10. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
  11. package/Server/Utils/Monitor/MonitorAlert.ts +91 -7
  12. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
  13. package/Server/Utils/Monitor/MonitorIncident.ts +133 -8
  14. package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
  15. package/Server/Utils/Monitor/MonitorResource.ts +2 -0
  16. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
  17. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
  18. package/Types/Infrastructure/BasicMetrics.ts +75 -0
  19. package/Types/Metrics/MetricQueryData.ts +11 -0
  20. package/Types/Monitor/CriteriaFilter.ts +10 -0
  21. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
  22. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
  23. package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
  24. package/Types/Monitor/MonitorMetricType.ts +34 -0
  25. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
  26. package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
  27. package/Types/StatusPage/StatusPageLanguage.ts +29 -0
  28. package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
  29. package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
  30. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
  31. package/UI/Components/Charts/Line/LineChart.tsx +16 -11
  32. package/UI/Components/Filters/FiltersForm.tsx +26 -2
  33. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
  34. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
  35. package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
  36. package/Utils/Monitor/MonitorMetricType.ts +309 -19
  37. package/build/dist/Models/DatabaseModels/Alert.js +57 -0
  38. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  39. package/build/dist/Models/DatabaseModels/Incident.js +57 -0
  40. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  41. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  42. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  43. package/build/dist/Server/API/StatusPageAPI.js +4 -0
  44. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
  52. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
  54. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  55. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
  56. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  57. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +66 -12
  58. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  59. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
  60. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  61. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +91 -15
  62. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  63. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
  64. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  65. package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
  66. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  67. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
  68. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  69. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
  70. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
  71. package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
  72. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  73. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
  74. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
  75. package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
  76. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  77. package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
  78. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
  79. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
  80. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  81. package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
  82. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  83. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
  84. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  85. package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
  86. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  87. package/build/dist/UI/Components/Filters/FiltersForm.js +6 -2
  88. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  89. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
  90. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
  91. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
  92. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
  93. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
  94. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
  95. package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
  96. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  97. package/package.json +1 -1
@@ -975,6 +975,61 @@ export default class Alert extends BaseModel {
975
975
  })
976
976
  public createdCriteriaId?: string = undefined;
977
977
 
978
+ @ColumnAccessControl({
979
+ create: [],
980
+ read: [
981
+ Permission.ProjectOwner,
982
+ Permission.ProjectAdmin,
983
+ Permission.ProjectMember,
984
+ Permission.Viewer,
985
+ Permission.AlertManager,
986
+ Permission.ReadAlert,
987
+ Permission.ReadAllProjectResources,
988
+ ],
989
+ update: [],
990
+ })
991
+ @Index()
992
+ @TableColumn({
993
+ type: TableColumnType.LongText,
994
+ required: false,
995
+ isDefaultValueColumn: false,
996
+ title: "Series Fingerprint",
997
+ description:
998
+ "For metric monitors with per-series alerting (e.g. grouped by host.name), this is a stable hash of the series label values so one alert is created per affected series.",
999
+ })
1000
+ @Column({
1001
+ type: ColumnType.LongText,
1002
+ nullable: true,
1003
+ })
1004
+ public seriesFingerprint?: string = undefined;
1005
+
1006
+ @ColumnAccessControl({
1007
+ create: [],
1008
+ read: [
1009
+ Permission.ProjectOwner,
1010
+ Permission.ProjectAdmin,
1011
+ Permission.ProjectMember,
1012
+ Permission.Viewer,
1013
+ Permission.AlertManager,
1014
+ Permission.ReadAlert,
1015
+ Permission.ReadAllProjectResources,
1016
+ ],
1017
+ update: [],
1018
+ })
1019
+ @TableColumn({
1020
+ type: TableColumnType.JSON,
1021
+ required: false,
1022
+ isDefaultValueColumn: false,
1023
+ title: "Series Labels",
1024
+ description:
1025
+ "Attribute key/value pairs that identify the affected series (e.g. {host.name: prod-db-01}) when this alert was created from a per-series metric breach.",
1026
+ })
1027
+ @Column({
1028
+ type: ColumnType.JSON,
1029
+ nullable: true,
1030
+ })
1031
+ public seriesLabels?: JSONObject = undefined;
1032
+
978
1033
  @ColumnAccessControl({
979
1034
  create: [],
980
1035
  read: [
@@ -1456,6 +1456,61 @@ export default class Incident extends BaseModel {
1456
1456
  })
1457
1457
  public createdIncidentTemplateId?: string = undefined;
1458
1458
 
1459
+ @ColumnAccessControl({
1460
+ create: [],
1461
+ read: [
1462
+ Permission.ProjectOwner,
1463
+ Permission.ProjectAdmin,
1464
+ Permission.ProjectMember,
1465
+ Permission.Viewer,
1466
+ Permission.IncidentManager,
1467
+ Permission.ReadProjectIncident,
1468
+ Permission.ReadAllProjectResources,
1469
+ ],
1470
+ update: [],
1471
+ })
1472
+ @Index()
1473
+ @TableColumn({
1474
+ type: TableColumnType.LongText,
1475
+ required: false,
1476
+ isDefaultValueColumn: false,
1477
+ title: "Series Fingerprint",
1478
+ description:
1479
+ "For metric monitors with per-series alerting (e.g. grouped by host.name), this is a stable hash of the series label values so one incident is created per affected series.",
1480
+ })
1481
+ @Column({
1482
+ type: ColumnType.LongText,
1483
+ nullable: true,
1484
+ })
1485
+ public seriesFingerprint?: string = undefined;
1486
+
1487
+ @ColumnAccessControl({
1488
+ create: [],
1489
+ read: [
1490
+ Permission.ProjectOwner,
1491
+ Permission.ProjectAdmin,
1492
+ Permission.ProjectMember,
1493
+ Permission.Viewer,
1494
+ Permission.IncidentManager,
1495
+ Permission.ReadProjectIncident,
1496
+ Permission.ReadAllProjectResources,
1497
+ ],
1498
+ update: [],
1499
+ })
1500
+ @TableColumn({
1501
+ type: TableColumnType.JSON,
1502
+ required: false,
1503
+ isDefaultValueColumn: false,
1504
+ title: "Series Labels",
1505
+ description:
1506
+ "Attribute key/value pairs that identify the affected series (e.g. {host.name: prod-db-01}) when this incident was created from a per-series metric breach.",
1507
+ })
1508
+ @Column({
1509
+ type: ColumnType.JSON,
1510
+ nullable: true,
1511
+ })
1512
+ public seriesLabels?: JSONObject = undefined;
1513
+
1459
1514
  @ColumnAccessControl({
1460
1515
  create: [],
1461
1516
  read: [
@@ -43,6 +43,7 @@ import {
43
43
  ManyToOne,
44
44
  } from "typeorm";
45
45
  import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
46
+ import { DEFAULT_STATUS_PAGE_LANGUAGE } from "../../Types/StatusPage/StatusPageLanguage";
46
47
 
47
48
  @EnableDocumentation()
48
49
  @EnableMCP()
@@ -2991,4 +2992,83 @@ export default class StatusPage extends BaseModel {
2991
2992
  create: PlanType.Free,
2992
2993
  })
2993
2994
  public embeddedOverallStatusToken?: string = undefined;
2995
+
2996
+ @ColumnAccessControl({
2997
+ create: [
2998
+ Permission.ProjectOwner,
2999
+ Permission.ProjectAdmin,
3000
+ Permission.ProjectMember,
3001
+ Permission.StatusPageManager,
3002
+ Permission.CreateProjectStatusPage,
3003
+ ],
3004
+ read: [
3005
+ Permission.ProjectOwner,
3006
+ Permission.ProjectAdmin,
3007
+ Permission.ProjectMember,
3008
+ Permission.Viewer,
3009
+ Permission.StatusPageManager,
3010
+ Permission.ReadProjectStatusPage,
3011
+ Permission.ReadAllProjectResources,
3012
+ ],
3013
+ update: [
3014
+ Permission.ProjectOwner,
3015
+ Permission.ProjectAdmin,
3016
+ Permission.ProjectMember,
3017
+ Permission.StatusPageManager,
3018
+ Permission.EditProjectStatusPage,
3019
+ ],
3020
+ })
3021
+ @TableColumn({
3022
+ type: TableColumnType.ShortText,
3023
+ title: "Default Language",
3024
+ required: false,
3025
+ defaultValue: DEFAULT_STATUS_PAGE_LANGUAGE,
3026
+ description:
3027
+ "Default language that the status page is shown in when a visitor arrives for the first time.",
3028
+ })
3029
+ @Column({
3030
+ type: ColumnType.ShortText,
3031
+ nullable: true,
3032
+ default: DEFAULT_STATUS_PAGE_LANGUAGE,
3033
+ })
3034
+ public defaultLanguage?: string = undefined;
3035
+
3036
+ @ColumnAccessControl({
3037
+ create: [
3038
+ Permission.ProjectOwner,
3039
+ Permission.ProjectAdmin,
3040
+ Permission.ProjectMember,
3041
+ Permission.StatusPageManager,
3042
+ Permission.CreateProjectStatusPage,
3043
+ ],
3044
+ read: [
3045
+ Permission.ProjectOwner,
3046
+ Permission.ProjectAdmin,
3047
+ Permission.ProjectMember,
3048
+ Permission.Viewer,
3049
+ Permission.StatusPageManager,
3050
+ Permission.ReadProjectStatusPage,
3051
+ Permission.ReadAllProjectResources,
3052
+ ],
3053
+ update: [
3054
+ Permission.ProjectOwner,
3055
+ Permission.ProjectAdmin,
3056
+ Permission.ProjectMember,
3057
+ Permission.StatusPageManager,
3058
+ Permission.EditProjectStatusPage,
3059
+ ],
3060
+ })
3061
+ @TableColumn({
3062
+ isDefaultValueColumn: false,
3063
+ required: false,
3064
+ type: TableColumnType.JSON,
3065
+ title: "Enabled Languages",
3066
+ description:
3067
+ "Languages offered in the footer language switcher. Leave empty to offer all supported languages.",
3068
+ })
3069
+ @Column({
3070
+ type: ColumnType.JSON,
3071
+ nullable: true,
3072
+ })
3073
+ public enabledLanguages?: Array<string> = undefined;
2994
3074
  }
@@ -225,6 +225,7 @@ export default class StatusPageAPI extends BaseAPI<
225
225
  pageTitle: true,
226
226
  pageDescription: true,
227
227
  name: true,
228
+ defaultLanguage: true,
228
229
  },
229
230
  props: {
230
231
  isRoot: true,
@@ -244,6 +245,7 @@ export default class StatusPageAPI extends BaseAPI<
244
245
  title: statusPage.pageTitle || statusPage.name,
245
246
  description: statusPage.pageDescription,
246
247
  _id: statusPage._id?.toString(),
248
+ defaultLanguage: statusPage.defaultLanguage || null,
247
249
  });
248
250
  },
249
251
  );
@@ -883,6 +885,8 @@ export default class StatusPageAPI extends BaseAPI<
883
885
  showAnnouncementsOnStatusPage: true,
884
886
  showScheduledMaintenanceEventsOnStatusPage: true,
885
887
  showSubscriberPageOnStatusPage: true,
888
+ defaultLanguage: true,
889
+ enabledLanguages: true,
886
890
  };
887
891
 
888
892
  const hasEnabledSSO: PositiveNumber =
@@ -0,0 +1,41 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1776940714709 implements MigrationInterface {
4
+ public name = "MigrationName1776940714709";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Incident" ADD "seriesFingerprint" character varying`,
9
+ );
10
+ await queryRunner.query(`ALTER TABLE "Incident" ADD "seriesLabels" jsonb`);
11
+ await queryRunner.query(
12
+ `ALTER TABLE "Alert" ADD "seriesFingerprint" character varying`,
13
+ );
14
+ await queryRunner.query(`ALTER TABLE "Alert" ADD "seriesLabels" jsonb`);
15
+ await queryRunner.query(
16
+ `CREATE INDEX "IDX_865fc7905f35947b294ca36b83" ON "Incident" ("seriesFingerprint") `,
17
+ );
18
+ await queryRunner.query(
19
+ `CREATE INDEX "IDX_5705362784705d225735b1a844" ON "Alert" ("seriesFingerprint") `,
20
+ );
21
+ }
22
+
23
+ public async down(queryRunner: QueryRunner): Promise<void> {
24
+ await queryRunner.query(
25
+ `DROP INDEX "public"."IDX_5705362784705d225735b1a844"`,
26
+ );
27
+ await queryRunner.query(
28
+ `DROP INDEX "public"."IDX_865fc7905f35947b294ca36b83"`,
29
+ );
30
+ await queryRunner.query(`ALTER TABLE "Alert" DROP COLUMN "seriesLabels"`);
31
+ await queryRunner.query(
32
+ `ALTER TABLE "Alert" DROP COLUMN "seriesFingerprint"`,
33
+ );
34
+ await queryRunner.query(
35
+ `ALTER TABLE "Incident" DROP COLUMN "seriesLabels"`,
36
+ );
37
+ await queryRunner.query(
38
+ `ALTER TABLE "Incident" DROP COLUMN "seriesFingerprint"`,
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,25 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddStatusPageLanguageSettings1776971364783
4
+ implements MigrationInterface
5
+ {
6
+ public name: string = "AddStatusPageLanguageSettings1776971364783";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "StatusPage" ADD "defaultLanguage" character varying DEFAULT 'en'`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "StatusPage" ADD "enabledLanguages" jsonb`,
14
+ );
15
+ }
16
+
17
+ public async down(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "StatusPage" DROP COLUMN "enabledLanguages"`,
20
+ );
21
+ await queryRunner.query(
22
+ `ALTER TABLE "StatusPage" DROP COLUMN "defaultLanguage"`,
23
+ );
24
+ }
25
+ }
@@ -289,6 +289,8 @@ import { MigrationName1776761171349 } from "./1776761171349-MigrationName";
289
289
  import { MigrationName1776801030808 } from "./1776801030808-MigrationName";
290
290
  import { MigrationName1776865086264 } from "./1776865086264-MigrationName";
291
291
  import { DedupeKubernetesClustersAndAddUniqueIndex1776881254913 } from "./1776881254913-DedupeKubernetesClustersAndAddUniqueIndex";
292
+ import { MigrationName1776940714709 } from "./1776940714709-MigrationName";
293
+ import { AddStatusPageLanguageSettings1776971364783 } from "./1776971364783-AddStatusPageLanguageSettings";
292
294
  export default [
293
295
  InitialMigration,
294
296
  MigrationName1717678334852,
@@ -581,4 +583,6 @@ export default [
581
583
  MigrationName1776801030808,
582
584
  MigrationName1776865086264,
583
585
  DedupeKubernetesClustersAndAddUniqueIndex1776881254913,
586
+ MigrationName1776940714709,
587
+ AddStatusPageLanguageSettings1776971364783,
584
588
  ];
@@ -454,10 +454,9 @@ export default class AnalyticsDatabaseService<
454
454
 
455
455
  // convert date column from string to date.
456
456
 
457
- const groupByColumnName: keyof TBaseModel | undefined =
458
- aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0
459
- ? (Object.keys(aggregateBy.groupBy)[0] as keyof TBaseModel)
460
- : undefined;
457
+ const groupByColumnNames: Array<string> = aggregateBy.groupBy
458
+ ? Object.keys(aggregateBy.groupBy)
459
+ : [];
461
460
 
462
461
  for (const item of items) {
463
462
  if (
@@ -483,6 +482,14 @@ export default class AnalyticsDatabaseService<
483
482
  );
484
483
  }
485
484
 
485
+ /*
486
+ * Preserve every group-by column on the aggregated row. The
487
+ * previous implementation only copied the first column, which
488
+ * silently dropped the rest when callers grouped by more than
489
+ * one dimension (e.g. attributes + name). `AggregatedModel`'s
490
+ * index signature already accepts arbitrary keys, so existing
491
+ * single-column consumers still work.
492
+ */
486
493
  const aggregatedModel: AggregatedModel = {
487
494
  timestamp: OneUptimeDate.fromString(
488
495
  (item as JSONObject)[
@@ -492,11 +499,14 @@ export default class AnalyticsDatabaseService<
492
499
  value: (item as JSONObject)[
493
500
  aggregateBy.aggregateColumnName as string
494
501
  ] as number,
495
- [groupByColumnName as string]: (item as JSONObject)[
496
- groupByColumnName as string
497
- ],
498
502
  };
499
503
 
504
+ for (const groupByColumnName of groupByColumnNames) {
505
+ aggregatedModel[groupByColumnName] = (item as JSONObject)[
506
+ groupByColumnName
507
+ ] as AggregatedModel[string];
508
+ }
509
+
500
510
  aggregatedItems.push(aggregatedModel);
501
511
  }
502
512
 
@@ -8,6 +8,7 @@ import MetricCriteriaContext, {
8
8
  MetricComponent,
9
9
  MetricComponentValue,
10
10
  } from "../../../../Types/Monitor/MetricMonitor/MetricCriteriaContext";
11
+ import MetricSeriesResult from "../../../../Types/Monitor/MetricMonitor/MetricSeriesResult";
11
12
  import MonitorStep from "../../../../Types/Monitor/MonitorStep";
12
13
  import { JSONObject } from "../../../../Types/JSON";
13
14
  import DataToProcess from "../DataToProcess";
@@ -23,6 +24,20 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
23
24
  import MetricUnitUtil from "../../../../Utils/MetricUnitUtil";
24
25
  import MetricFormulaEvaluator from "../../../../Utils/Metrics/MetricFormulaEvaluator";
25
26
 
27
+ /**
28
+ * Result of evaluating a single criteria filter against a single metric
29
+ * series. `rootCause` is null when the filter did not match; otherwise
30
+ * it's the human-readable comparison message. `context` always reflects
31
+ * the metric identity for this series (used to render the metric
32
+ * details + breaching samples section of the incident root cause).
33
+ */
34
+ export interface MetricSeriesEvaluationResult {
35
+ fingerprint: string | undefined;
36
+ labels: JSONObject;
37
+ rootCause: string | null;
38
+ context: MetricCriteriaContext;
39
+ }
40
+
26
41
  export default class MetricMonitorCriteria {
27
42
  @CaptureSpan()
28
43
  public static async isMonitorInstanceCriteriaFilterMet(input: {
@@ -30,8 +45,50 @@ export default class MetricMonitorCriteria {
30
45
  criteriaFilter: CriteriaFilter;
31
46
  monitorStep: MonitorStep;
32
47
  }): Promise<string | null> {
33
- // Metric Monitoring Check
48
+ const evaluations: Array<MetricSeriesEvaluationResult> =
49
+ MetricMonitorCriteria.evaluateAllSeries(input);
50
+
51
+ /*
52
+ * Backwards-compat: the scalar entrypoint collapses per-series
53
+ * evaluation down to the first matching series so existing callers
54
+ * (single-incident path) keep working. The per-series code path uses
55
+ * `evaluateAllSeries` directly.
56
+ */
57
+ const match: MetricSeriesEvaluationResult | undefined = evaluations.find(
58
+ (e: MetricSeriesEvaluationResult) => {
59
+ return e.rootCause !== null;
60
+ },
61
+ );
62
+
63
+ /*
64
+ * Always populate the legacy single-context field so the root-cause
65
+ * renderer can still read metric identity from the criteria filter
66
+ * even when nothing matched. Pick the first evaluation's context.
67
+ */
68
+ if (evaluations.length > 0) {
69
+ input.criteriaFilter.metricCriteriaContext = (
70
+ match || evaluations[0]!
71
+ ).context;
72
+ }
34
73
 
74
+ return match ? match.rootCause : null;
75
+ }
76
+
77
+ /**
78
+ * Evaluate a single criteria filter against every series produced by
79
+ * the monitor. For monitors without group-by, this returns a single
80
+ * evaluation covering all aggregated results (legacy behavior). For
81
+ * monitors with group-by attributes set, it returns one evaluation
82
+ * per unique series fingerprint — each with its own
83
+ * `MetricCriteriaContext` carrying that series' breaching samples
84
+ * and labels. The caller fans this out into one incident per
85
+ * breaching series.
86
+ */
87
+ public static evaluateAllSeries(input: {
88
+ dataToProcess: DataToProcess;
89
+ criteriaFilter: CriteriaFilter;
90
+ monitorStep: MonitorStep;
91
+ }): Array<MetricSeriesEvaluationResult> {
35
92
  if (
36
93
  input.criteriaFilter.metricMonitorOptions &&
37
94
  !input.criteriaFilter.metricMonitorOptions.metricAggregationType
@@ -41,20 +98,14 @@ export default class MetricMonitorCriteria {
41
98
  }
42
99
 
43
100
  if (input.criteriaFilter.checkOn !== CheckOn.MetricValue) {
44
- return null;
101
+ return [];
45
102
  }
46
103
 
47
- const rawThreshold: number | null = CompareCriteria.convertToNumber(
48
- input.criteriaFilter.value,
49
- );
50
-
51
- const metricAlias: string =
52
- input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
53
-
54
104
  const metricResponse: MetricMonitorResponse =
55
105
  input.dataToProcess as MetricMonitorResponse;
56
- const metricAggregatedResult: Array<AggregatedResult> =
57
- metricResponse.metricResult || [];
106
+
107
+ const seriesBreakdown: Array<MetricSeriesResult> | undefined =
108
+ metricResponse.seriesBreakdown;
58
109
 
59
110
  const queryConfigs: Array<MetricQueryConfigData> =
60
111
  input.monitorStep.data?.metricMonitor?.metricViewConfig?.queryConfigs ||
@@ -63,6 +114,60 @@ export default class MetricMonitorCriteria {
63
114
  input.monitorStep.data?.metricMonitor?.metricViewConfig?.formulaConfigs ||
64
115
  [];
65
116
 
117
+ /*
118
+ * Series-less path: one synthetic "all-series" evaluation over the
119
+ * flat metricResult. Preserves the pre-group-by behavior exactly.
120
+ */
121
+ if (!seriesBreakdown || seriesBreakdown.length === 0) {
122
+ const result: MetricSeriesEvaluationResult =
123
+ MetricMonitorCriteria.evaluateOneSeries({
124
+ criteriaFilter: input.criteriaFilter,
125
+ aggregatedResults: metricResponse.metricResult || [],
126
+ queryConfigs,
127
+ formulaConfigs,
128
+ seriesFingerprint: undefined,
129
+ seriesLabels: {},
130
+ });
131
+ return [result];
132
+ }
133
+
134
+ return seriesBreakdown.map((series: MetricSeriesResult) => {
135
+ return MetricMonitorCriteria.evaluateOneSeries({
136
+ criteriaFilter: input.criteriaFilter,
137
+ aggregatedResults: series.aggregatedResults,
138
+ queryConfigs,
139
+ formulaConfigs,
140
+ seriesFingerprint: series.fingerprint,
141
+ seriesLabels: series.labels,
142
+ });
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Core evaluation loop: compare the samples for one metric series
148
+ * against the criteria threshold. Builds the metric identity context,
149
+ * identifies breaching samples, and assembles the human-readable
150
+ * root-cause message. Factored out so `evaluateAllSeries` can invoke
151
+ * it once per series without duplicating logic.
152
+ */
153
+ private static evaluateOneSeries(input: {
154
+ criteriaFilter: CriteriaFilter;
155
+ aggregatedResults: Array<AggregatedResult>;
156
+ queryConfigs: Array<MetricQueryConfigData>;
157
+ formulaConfigs: Array<MetricFormulaConfigData>;
158
+ seriesFingerprint: string | undefined;
159
+ seriesLabels: JSONObject;
160
+ }): MetricSeriesEvaluationResult {
161
+ const rawThreshold: number | null = CompareCriteria.convertToNumber(
162
+ input.criteriaFilter.value,
163
+ );
164
+
165
+ const metricAlias: string =
166
+ input.criteriaFilter.metricMonitorOptions?.metricAlias || "";
167
+
168
+ const metricAggregatedResult: Array<AggregatedResult> =
169
+ input.aggregatedResults;
170
+
66
171
  /*
67
172
  * Resolve which query/formula the alias refers to. Use explicit index
68
173
  * checks (not `findIndex() || -1`, which incorrectly falls back to -1
@@ -73,25 +178,25 @@ export default class MetricMonitorCriteria {
73
178
  let aliasIndex: number = -1;
74
179
 
75
180
  if (metricAlias) {
76
- const qIdx: number = queryConfigs.findIndex(
181
+ const qIdx: number = input.queryConfigs.findIndex(
77
182
  (q: MetricQueryConfigData) => {
78
183
  return q.metricAliasData?.metricVariable === metricAlias;
79
184
  },
80
185
  );
81
186
 
82
187
  if (qIdx >= 0) {
83
- matchedQuery = queryConfigs[qIdx] || null;
188
+ matchedQuery = input.queryConfigs[qIdx] || null;
84
189
  aliasIndex = qIdx;
85
190
  } else {
86
- const fIdx: number = formulaConfigs.findIndex(
191
+ const fIdx: number = input.formulaConfigs.findIndex(
87
192
  (f: MetricFormulaConfigData) => {
88
193
  return f.metricAliasData?.metricVariable === metricAlias;
89
194
  },
90
195
  );
91
196
 
92
197
  if (fIdx >= 0) {
93
- matchedFormula = formulaConfigs[fIdx] || null;
94
- aliasIndex = queryConfigs.length + fIdx;
198
+ matchedFormula = input.formulaConfigs[fIdx] || null;
199
+ aliasIndex = input.queryConfigs.length + fIdx;
95
200
  }
96
201
  }
97
202
  }
@@ -105,8 +210,8 @@ export default class MetricMonitorCriteria {
105
210
  ? metricAggregatedResult[aliasIndex]
106
211
  : metricAggregatedResult[0];
107
212
 
108
- if (!matchedQuery && !matchedFormula && queryConfigs[0]) {
109
- matchedQuery = queryConfigs[0];
213
+ if (!matchedQuery && !matchedFormula && input.queryConfigs[0]) {
214
+ matchedQuery = input.queryConfigs[0];
110
215
  }
111
216
 
112
217
  /*
@@ -119,14 +224,24 @@ export default class MetricMonitorCriteria {
119
224
  matchedFormula,
120
225
  metricAlias,
121
226
  criteriaFilter: input.criteriaFilter,
122
- queryConfigs,
123
- formulaConfigs,
227
+ queryConfigs: input.queryConfigs,
228
+ formulaConfigs: input.formulaConfigs,
124
229
  });
125
230
 
126
- input.criteriaFilter.metricCriteriaContext = metricContext;
231
+ if (input.seriesFingerprint) {
232
+ metricContext.seriesFingerprint = input.seriesFingerprint;
233
+ }
234
+ if (input.seriesLabels && Object.keys(input.seriesLabels).length > 0) {
235
+ metricContext.seriesLabels = input.seriesLabels;
236
+ }
127
237
 
128
238
  if (rawThreshold === null) {
129
- return null;
239
+ return {
240
+ fingerprint: input.seriesFingerprint,
241
+ labels: input.seriesLabels,
242
+ rootCause: null,
243
+ context: metricContext,
244
+ };
130
245
  }
131
246
 
132
247
  /*
@@ -180,11 +295,21 @@ export default class MetricMonitorCriteria {
180
295
  NoDataPolicy.Ignore;
181
296
 
182
297
  if (policy === NoDataPolicy.Ignore) {
183
- return null;
298
+ return {
299
+ fingerprint: input.seriesFingerprint,
300
+ labels: input.seriesLabels,
301
+ rootCause: null,
302
+ context: metricContext,
303
+ };
184
304
  }
185
305
 
186
306
  if (policy === NoDataPolicy.Trigger) {
187
- return `No data received for ${metricContext.metricName} in the evaluation window — triggering per no-data policy.`;
307
+ return {
308
+ fingerprint: input.seriesFingerprint,
309
+ labels: input.seriesLabels,
310
+ rootCause: `No data received for ${metricContext.metricName} in the evaluation window — triggering per no-data policy.`,
311
+ context: metricContext,
312
+ };
188
313
  }
189
314
 
190
315
  // TreatAsZero: fall through to the comparator with value 0.
@@ -206,7 +331,12 @@ export default class MetricMonitorCriteria {
206
331
  });
207
332
 
208
333
  if (!comparisonMessage) {
209
- return null;
334
+ return {
335
+ fingerprint: input.seriesFingerprint,
336
+ labels: input.seriesLabels,
337
+ rootCause: null,
338
+ context: metricContext,
339
+ };
210
340
  }
211
341
 
212
342
  /*
@@ -232,8 +362,8 @@ export default class MetricMonitorCriteria {
232
362
  matchedFormula
233
363
  ? MetricMonitorCriteria.buildComponentValueLookup({
234
364
  components: metricContext.components || [],
235
- queryConfigs,
236
- formulaConfigs,
365
+ queryConfigs: input.queryConfigs,
366
+ formulaConfigs: input.formulaConfigs,
237
367
  metricAggregatedResult,
238
368
  })
239
369
  : null;
@@ -276,7 +406,12 @@ export default class MetricMonitorCriteria {
276
406
  metricContext.breachingSamples = breachingSamples;
277
407
  }
278
408
 
279
- return comparisonMessage;
409
+ return {
410
+ fingerprint: input.seriesFingerprint,
411
+ labels: input.seriesLabels,
412
+ rootCause: comparisonMessage,
413
+ context: metricContext,
414
+ };
280
415
  }
281
416
 
282
417
  private static buildComponentValueLookup(input: {
@@ -438,6 +573,17 @@ export default class MetricMonitorCriteria {
438
573
  ? Object.keys(q.metricQueryData.groupBy as Record<string, unknown>)
439
574
  : [];
440
575
 
576
+ /*
577
+ * Include user-selected attribute keys as part of the groupBy view
578
+ * so the root-cause block shows "Grouped By: host.name" not just the
579
+ * raw columns ClickHouse was asked to partition on.
580
+ */
581
+ const groupByAttributeKeys: Array<string> =
582
+ q?.metricQueryData?.groupByAttributeKeys || [];
583
+ const allGroupBy: Array<string> = Array.from(
584
+ new Set([...groupBy, ...groupByAttributeKeys]),
585
+ );
586
+
441
587
  const components: Array<MetricComponent> | undefined = f
442
588
  ? MetricMonitorCriteria.buildFormulaComponents({
443
589
  formulaConfig: f,
@@ -454,7 +600,7 @@ export default class MetricMonitorCriteria {
454
600
  isFormula: Boolean(f),
455
601
  formulaExpression: f?.metricFormulaData?.metricFormula,
456
602
  filterAttributes,
457
- groupBy,
603
+ groupBy: allGroupBy,
458
604
  timeWindowMinutes:
459
605
  input.criteriaFilter.evaluateOverTimeOptions?.timeValueInMinutes,
460
606
  ...(components && components.length > 0 ? { components } : {}),