@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.7.0 → 2.7.2
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.
- package/CHANGELOG.md +16 -0
- package/dist/database/DatabaseMetricValues.cjs.js +39 -6
- package/dist/database/DatabaseMetricValues.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +62 -5
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js +0 -1
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js.map +1 -1
- package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js +2 -2
- package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js.map +1 -1
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js +2 -1
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js.map +1 -1
- package/dist/service/mappers.cjs.js +3 -1
- package/dist/service/mappers.cjs.js.map +1 -1
- package/dist/utils/metricCalculationError.cjs.js +8 -0
- package/dist/utils/metricCalculationError.cjs.js.map +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 2.7.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.2
|
|
8
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.2
|
|
9
|
+
|
|
10
|
+
## 2.7.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 91e724f: Expose scorecard entity calculation health on drill-down and aggregation APIs, and align the drill-down warning plus homepage subheader with those counts.
|
|
15
|
+
- Updated dependencies [91e724f]
|
|
16
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.1
|
|
17
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.1
|
|
18
|
+
|
|
3
19
|
## 2.7.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -4,7 +4,6 @@ class DatabaseMetricValues {
|
|
|
4
4
|
constructor(dbClient) {
|
|
5
5
|
this.dbClient = dbClient;
|
|
6
6
|
}
|
|
7
|
-
dbClient;
|
|
8
7
|
tableName = "metric_values";
|
|
9
8
|
/**
|
|
10
9
|
* Insert multiple metric values
|
|
@@ -34,19 +33,50 @@ class DatabaseMetricValues {
|
|
|
34
33
|
* Get aggregated metrics by status for multiple entities and metrics.
|
|
35
34
|
*/
|
|
36
35
|
async readAggregatedMetricByEntityRefs(catalog_entity_refs, metric_id) {
|
|
36
|
+
if (catalog_entity_refs.length === 0) {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
37
39
|
const latestIdsSubquery = this.dbClient(this.tableName).max("id").where("metric_id", metric_id).whereIn("catalog_entity_ref", catalog_entity_refs).groupBy("catalog_entity_ref");
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
+
const metricValueIsMissingExpr = "(value IS NULL OR CAST(value AS TEXT) = 'null')";
|
|
41
|
+
const statsRow = await this.dbClient(this.tableName).whereIn("id", latestIdsSubquery).select(
|
|
42
|
+
this.dbClient.raw("COUNT(*) as latest_row_count"),
|
|
43
|
+
this.dbClient.raw(
|
|
44
|
+
`SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`
|
|
45
|
+
),
|
|
46
|
+
this.dbClient.raw("MAX(timestamp) as max_timestamp")
|
|
47
|
+
).first();
|
|
48
|
+
const latestRowCount = Number(
|
|
49
|
+
statsRow?.latest_row_count ?? 0
|
|
50
|
+
);
|
|
51
|
+
if (latestRowCount === 0) {
|
|
40
52
|
return void 0;
|
|
41
53
|
}
|
|
54
|
+
const calculation_error_count = Number(
|
|
55
|
+
statsRow?.calculation_error_count ?? 0
|
|
56
|
+
);
|
|
42
57
|
const normalizeTimestamp = (timestamp) => {
|
|
43
58
|
if (timestamp instanceof Date) {
|
|
44
59
|
return timestamp;
|
|
45
|
-
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof timestamp === "number" || typeof timestamp === "string") {
|
|
46
62
|
return new Date(timestamp);
|
|
47
63
|
}
|
|
48
64
|
return /* @__PURE__ */ new Date();
|
|
49
65
|
};
|
|
66
|
+
const maxTimestampAllLatest = normalizeTimestamp(
|
|
67
|
+
statsRow?.max_timestamp
|
|
68
|
+
);
|
|
69
|
+
const statusRows = await this.dbClient(this.tableName).select("status").count("* as count").max("timestamp as max_timestamp").whereIn("id", latestIdsSubquery).whereNotNull("status").whereRaw(`NOT ${metricValueIsMissingExpr}`).groupBy("status");
|
|
70
|
+
if (!statusRows || statusRows.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
metric_id,
|
|
73
|
+
total: 0,
|
|
74
|
+
max_timestamp: maxTimestampAllLatest,
|
|
75
|
+
statusCounts: {},
|
|
76
|
+
calculation_error_count,
|
|
77
|
+
latest_entity_count: latestRowCount
|
|
78
|
+
};
|
|
79
|
+
}
|
|
50
80
|
let maxTimestamp = /* @__PURE__ */ new Date(0);
|
|
51
81
|
let total = 0;
|
|
52
82
|
const statusCounts = {};
|
|
@@ -60,11 +90,14 @@ class DatabaseMetricValues {
|
|
|
60
90
|
statusCounts[name] = count;
|
|
61
91
|
total += count;
|
|
62
92
|
}
|
|
93
|
+
const mergedMax = maxTimestampAllLatest.getTime() >= maxTimestamp.getTime() ? maxTimestampAllLatest : maxTimestamp;
|
|
63
94
|
return {
|
|
64
95
|
metric_id,
|
|
65
96
|
total,
|
|
66
|
-
max_timestamp:
|
|
67
|
-
statusCounts
|
|
97
|
+
max_timestamp: mergedMax,
|
|
98
|
+
statusCounts,
|
|
99
|
+
calculation_error_count,
|
|
100
|
+
latest_entity_count: latestRowCount
|
|
68
101
|
};
|
|
69
102
|
}
|
|
70
103
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereNotNull('value')\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return undefined;\n }\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: any): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n } else if (\n typeof timestamp === 'number' ||\n typeof timestamp === 'string'\n ) {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n return {\n metric_id,\n total,\n max_timestamp: maxTimestamp,\n statusCounts,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DatabaseMetricValues.cjs.js","sources":["../../src/database/DatabaseMetricValues.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Knex } from 'knex';\nimport {\n DbMetricValueCreate,\n DbMetricValue,\n DbAggregatedMetric,\n} from './types';\n\ntype ReadEntityMetricsWithFiltersOptions = {\n status?: string;\n entityName?: string;\n entityKind?: string;\n entityNamespace?: string;\n entityOwner?: string[];\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n pagination?: { limit: number; offset: number };\n};\n\nexport class DatabaseMetricValues {\n private readonly tableName = 'metric_values';\n\n constructor(private readonly dbClient: Knex<any, any[]>) {}\n\n /**\n * Insert multiple metric values\n */\n async createMetricValues(metricValues: DbMetricValueCreate[]): Promise<void> {\n if (metricValues.length === 0) {\n return;\n }\n await this.dbClient(this.tableName).insert(metricValues);\n }\n\n /**\n * Get the latest metric values for a specific entity and metrics\n */\n async readLatestEntityMetricValues(\n catalog_entity_ref: string,\n metric_ids: string[],\n ): Promise<DbMetricValue[]> {\n return await this.dbClient(this.tableName)\n .select('*')\n .whereIn(\n 'id',\n this.dbClient(this.tableName)\n .max('id')\n .whereIn('metric_id', metric_ids)\n .where('catalog_entity_ref', catalog_entity_ref)\n .groupBy('metric_id'),\n );\n }\n\n /**\n * Delete metric values that are older than the given date\n */\n async cleanupExpiredMetrics(olderThan: Date): Promise<number> {\n return await this.dbClient(this.tableName)\n .where('timestamp', '<', olderThan)\n .del();\n }\n\n /**\n * Get aggregated metrics by status for multiple entities and metrics.\n */\n async readAggregatedMetricByEntityRefs(\n catalog_entity_refs: string[],\n metric_id: string,\n ): Promise<DbAggregatedMetric | undefined> {\n if (catalog_entity_refs.length === 0) {\n return undefined;\n }\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .whereIn('catalog_entity_ref', catalog_entity_refs)\n .groupBy('catalog_entity_ref');\n\n // `value` is a JSON column. Depending on database/driver, a \"missing\" metric value can\n // arrive either as SQL NULL or as JSON literal null (`CAST(value AS TEXT) = 'null'`).\n const metricValueIsMissingExpr =\n \"(value IS NULL OR CAST(value AS TEXT) = 'null')\";\n\n // One round-trip for latest-row count, calculation-error count, and max timestamp\n // (same latest-id set as the status breakdown query below).\n const statsRow = await this.dbClient(this.tableName)\n .whereIn('id', latestIdsSubquery)\n .select(\n this.dbClient.raw('COUNT(*) as latest_row_count'),\n this.dbClient.raw(\n `SUM(CASE WHEN error_message IS NOT NULL AND ${metricValueIsMissingExpr} THEN 1 ELSE 0 END) as calculation_error_count`,\n ),\n this.dbClient.raw('MAX(timestamp) as max_timestamp'),\n )\n .first();\n\n const latestRowCount = Number(\n (statsRow as { latest_row_count?: string | number } | undefined)\n ?.latest_row_count ?? 0,\n );\n if (latestRowCount === 0) {\n return undefined;\n }\n\n const calculation_error_count = Number(\n (statsRow as { calculation_error_count?: string | number } | undefined)\n ?.calculation_error_count ?? 0,\n );\n\n // Normalize types for cross-database compatibility\n // PostgreSQL returns COUNT/SUM as strings, SQLite returns numbers\n // PostgreSQL returns MAX(timestamp) as Date, SQLite returns number (milliseconds)\n const normalizeTimestamp = (timestamp: unknown): Date => {\n if (timestamp instanceof Date) {\n return timestamp;\n }\n if (typeof timestamp === 'number' || typeof timestamp === 'string') {\n return new Date(timestamp);\n }\n return new Date();\n };\n\n const maxTimestampAllLatest = normalizeTimestamp(\n (statsRow as { max_timestamp?: unknown })?.max_timestamp,\n );\n\n const statusRows = await this.dbClient(this.tableName)\n .select('status')\n .count('* as count')\n .max('timestamp as max_timestamp')\n .whereIn('id', latestIdsSubquery)\n .whereNotNull('status')\n .whereRaw(`NOT ${metricValueIsMissingExpr}`)\n .groupBy('status');\n\n if (!statusRows || statusRows.length === 0) {\n return {\n metric_id,\n total: 0,\n max_timestamp: maxTimestampAllLatest,\n statusCounts: {},\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n let maxTimestamp = new Date(0);\n let total = 0;\n const statusCounts: Record<string, number> = {};\n for (const row of statusRows) {\n const rowTimestamp = normalizeTimestamp(row.max_timestamp);\n if (rowTimestamp > maxTimestamp) {\n maxTimestamp = rowTimestamp;\n }\n const name = row.status as string;\n const count = Number(row.count);\n statusCounts[name] = count;\n total += count;\n }\n\n const mergedMax =\n maxTimestampAllLatest.getTime() >= maxTimestamp.getTime()\n ? maxTimestampAllLatest\n : maxTimestamp;\n\n return {\n metric_id,\n total,\n max_timestamp: mergedMax,\n statusCounts,\n calculation_error_count,\n latest_entity_count: latestRowCount,\n };\n }\n\n /**\n * Fetch the latest entity metric values for a given metric, with optional filtering\n * by status, name, kind, namespace, or owner, plus sorting and pagination.\n */\n async readEntityMetricsWithFilters(\n metric_id: string,\n options: ReadEntityMetricsWithFiltersOptions,\n ): Promise<DbMetricValue[]> {\n const clientName: string =\n (this.dbClient as any).client?.config?.client ?? '';\n const isPostgres = clientName === 'pg' || clientName.includes('postgres');\n\n const latestIdsSubquery = this.dbClient(this.tableName)\n .max('id')\n .where('metric_id', metric_id)\n .groupBy('catalog_entity_ref');\n\n const query = this.dbClient(this.tableName)\n .select('*')\n .whereIn('id', latestIdsSubquery);\n\n const sortColumnMap: Record<string, string> = {\n entityName: 'catalog_entity_ref',\n owner: 'entity_owner',\n entityKind: 'entity_kind',\n timestamp: 'timestamp',\n metricValue: 'value',\n namespace: 'entity_namespace',\n status: 'status',\n };\n\n const column =\n (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';\n const direction = options.sortOrder === 'asc' ? 'asc' : 'desc';\n\n this.applySort(query, options.sortBy, column, direction, isPostgres);\n\n if (options.status) {\n query.where('status', options.status);\n }\n\n if (options.entityName) {\n const escaped = options.entityName.replace(/[%_\\\\]/g, '\\\\$&');\n query.whereRaw(\"catalog_entity_ref LIKE ? ESCAPE '\\\\'\", [`%${escaped}%`]);\n }\n\n if (options.entityKind) {\n query.where('entity_kind', options.entityKind);\n }\n\n if (options.entityNamespace) {\n query.where('entity_namespace', options.entityNamespace);\n }\n\n if (options.entityOwner && options.entityOwner.length > 0) {\n query.whereIn('entity_owner', options.entityOwner);\n }\n\n if (options.pagination) {\n query.limit(options.pagination.limit).offset(options.pagination.offset);\n }\n\n return await query;\n }\n\n private applySort(\n query: any,\n sortBy: string | undefined,\n column: string,\n direction: string,\n isPostgres: boolean,\n ): void {\n if (sortBy === 'metricValue') {\n // value is JSON and nullable; cast for numeric sort with NULLs last\n if (isPostgres) {\n query.orderByRaw(\n `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,\n );\n } else {\n // SQLite: \"value IS NULL\" puts nulls last; double-cast handles JSON-stored values\n query.orderByRaw(\n `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,\n );\n }\n } else if (sortBy === 'status') {\n // status is nullable; NULLs always sort last regardless of direction\n if (isPostgres) {\n query.orderByRaw(`status ${direction} NULLS LAST, id ASC`);\n } else {\n // SQLite: \"status IS NULL\" evaluates to 1 for NULLs, pushing them to the end\n query.orderByRaw(`status IS NULL, status ${direction}, id ASC`);\n }\n } else {\n query.orderBy(column, direction);\n // Ensure a stable sort when two metrics share the same primary sort value\n query.orderBy('id', 'asc');\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
|
|
3
4
|
var normalizeOwnerRef = require('../utils/normalizeOwnerRef.cjs.js');
|
|
4
5
|
var errors = require('@backstage/errors');
|
|
5
6
|
var permissionUtils = require('../permissions/permissionUtils.cjs.js');
|
|
6
7
|
var mergeEntityAndProviderThresholds = require('../utils/mergeEntityAndProviderThresholds.cjs.js');
|
|
8
|
+
var metricCalculationError = require('../utils/metricCalculationError.cjs.js');
|
|
9
|
+
var mappers = require('./mappers.cjs.js');
|
|
7
10
|
|
|
8
11
|
class CatalogMetricService {
|
|
12
|
+
static entityHealthSummary(accessibleRows, countsArePartial) {
|
|
13
|
+
const calculationErrorCount = accessibleRows.filter(
|
|
14
|
+
(row) => metricCalculationError.isMetricCalculationError(row)
|
|
15
|
+
).length;
|
|
16
|
+
return {
|
|
17
|
+
totalEntities: accessibleRows.length,
|
|
18
|
+
calculationErrorCount,
|
|
19
|
+
countsArePartial
|
|
20
|
+
};
|
|
21
|
+
}
|
|
9
22
|
logger;
|
|
10
23
|
catalog;
|
|
11
24
|
auth;
|
|
@@ -61,7 +74,10 @@ class CatalogMetricService {
|
|
|
61
74
|
} catch (error) {
|
|
62
75
|
thresholdError = errors.stringifyError(error);
|
|
63
76
|
}
|
|
64
|
-
const isMetricCalcError =
|
|
77
|
+
const isMetricCalcError = metricCalculationError.isMetricCalculationError({
|
|
78
|
+
value,
|
|
79
|
+
error_message
|
|
80
|
+
});
|
|
65
81
|
return {
|
|
66
82
|
id: metric.id,
|
|
67
83
|
status: isMetricCalcError ? "error" : "success",
|
|
@@ -88,6 +104,37 @@ class CatalogMetricService {
|
|
|
88
104
|
}
|
|
89
105
|
);
|
|
90
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Get an aggregated metric by status grouped for multiple entities and a single metric ID.
|
|
109
|
+
*
|
|
110
|
+
* @param entityRefs - Array of entity references in format "kind:namespace/name"
|
|
111
|
+
* @param metricId - Metric ID to aggregate.
|
|
112
|
+
* @returns Aggregated metric by status grouped results
|
|
113
|
+
*/
|
|
114
|
+
async getStatusGroupedAggregatedMetrics(entityRefs, metricId) {
|
|
115
|
+
const aggregatedMetric = await this.database.readAggregatedMetricByEntityRefs(
|
|
116
|
+
entityRefs,
|
|
117
|
+
metricId
|
|
118
|
+
);
|
|
119
|
+
return mappers.AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get an aggregated metric by aggregation type.
|
|
123
|
+
*
|
|
124
|
+
* @param entityRefs - Array of entity references in format "kind:namespace/name"
|
|
125
|
+
* @param metricId - Metric ID to aggregate.
|
|
126
|
+
* @param aggregationType - Aggregation type to use.
|
|
127
|
+
* @returns Aggregated metric by aggregation type results
|
|
128
|
+
*/
|
|
129
|
+
async getAggregatedMetricByEntityRefs(entityRefs, metricId, aggregationType) {
|
|
130
|
+
if (entityRefs.length !== 0) {
|
|
131
|
+
if (aggregationType === backstagePluginScorecardCommon.aggregationTypes.statusGrouped) {
|
|
132
|
+
return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unsupported aggregation type: ${aggregationType}`);
|
|
135
|
+
}
|
|
136
|
+
return mappers.AggregatedMetricMapper.toAggregatedMetric();
|
|
137
|
+
}
|
|
91
138
|
/**
|
|
92
139
|
* Get detailed entity metrics for drill-down with filtering, sorting, and pagination.
|
|
93
140
|
*
|
|
@@ -126,7 +173,8 @@ class CatalogMetricService {
|
|
|
126
173
|
total: 0,
|
|
127
174
|
totalPages: 0,
|
|
128
175
|
isCapped: false
|
|
129
|
-
}
|
|
176
|
+
},
|
|
177
|
+
entityHealth: CatalogMetricService.entityHealthSummary([], false)
|
|
130
178
|
};
|
|
131
179
|
}
|
|
132
180
|
const rows = await this.database.readEntityMetricsWithFilters(metricId, {
|
|
@@ -182,7 +230,8 @@ class CatalogMetricService {
|
|
|
182
230
|
total: 0,
|
|
183
231
|
totalPages: 0,
|
|
184
232
|
isCapped: false
|
|
185
|
-
}
|
|
233
|
+
},
|
|
234
|
+
entityHealth: CatalogMetricService.entityHealthSummary([], false)
|
|
186
235
|
};
|
|
187
236
|
}
|
|
188
237
|
const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;
|
|
@@ -206,7 +255,11 @@ class CatalogMetricService {
|
|
|
206
255
|
total: totalFiltered,
|
|
207
256
|
totalPages: Math.ceil(totalFiltered / options.limit),
|
|
208
257
|
isCapped
|
|
209
|
-
}
|
|
258
|
+
},
|
|
259
|
+
entityHealth: CatalogMetricService.entityHealthSummary(
|
|
260
|
+
accessibleRows,
|
|
261
|
+
isCapped
|
|
262
|
+
)
|
|
210
263
|
};
|
|
211
264
|
}
|
|
212
265
|
const enrichedEntities = [];
|
|
@@ -238,7 +291,11 @@ class CatalogMetricService {
|
|
|
238
291
|
total: totalFiltered,
|
|
239
292
|
totalPages: Math.ceil(totalFiltered / options.limit),
|
|
240
293
|
isCapped
|
|
241
|
-
}
|
|
294
|
+
},
|
|
295
|
+
entityHealth: CatalogMetricService.entityHealthSummary(
|
|
296
|
+
accessibleRows,
|
|
297
|
+
isCapped
|
|
298
|
+
)
|
|
242
299
|
};
|
|
243
300
|
}
|
|
244
301
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { DbMetricValue } from '../database/types';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n};\n\nexport class CatalogMetricService {\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n metricIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(\n ({ metric_id, value, error_message, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const provider = this.registry.getProvider(metric_id);\n const metric = this.registry.getMetric(metric_id);\n\n try {\n thresholds = mergeEntityAndProviderThresholds(entity, provider);\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (error_message) {\n thresholdError = error_message;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = error_message !== null && value === null;\n\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.open_prs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n };\n }\n\n // Query database with all DB filters first\n // At the moment, this is going to be an O(MAX_FETCHABLE_ROWS) cost and is intentional to avoid leaking\n // pre-auth counts. MAX_FETCHABLE_ROWS and BATCH_SIZE are to be used as a method to of adjustment to\n // find the right amount of performance\n const rows = await this.database.readEntityMetricsWithFilters(metricId, {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: 0,\n },\n });\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with user\n // credentials. The catalog enforces auth natively: null = unauthorized or deleted.\n // We also cache the returned Entity objects so we can enrich the page rows without\n // a second catalog round-trip. Sequential processing preserves DB sort order.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n try {\n for (let i = 0; i < rows.length; i += CatalogMetricService.BATCH_SIZE) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalog_entity_ref),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n // Filter out the unauthorized entities\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n entityMap.set(batch[j].catalog_entity_ref, entity);\n accessibleRows.push(batch[j]);\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n };\n }\n\n // True when DB results were capped; pagination.total may undercount the full dataset.\n const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n };\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","normalizeOwnerRef"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogMetricService.cjs.js","sources":["../../src/service/CatalogMetricService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n MetricResult,\n ThresholdConfig,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n ScorecardEntityHealthSummary,\n aggregationTypes,\n AggregatedMetric,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Entity } from '@backstage/catalog-model';\nimport { normalizeOwnerRef } from '../utils/normalizeOwnerRef';\nimport { MetricProvidersRegistry } from '../providers/MetricProvidersRegistry';\nimport { NotFoundError, stringifyError } from '@backstage/errors';\nimport {\n AuthService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { filterAuthorizedMetrics } from '../permissions/permissionUtils';\nimport {\n PermissionCondition,\n PermissionCriteria,\n PermissionRuleParams,\n} from '@backstage/plugin-permission-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { DatabaseMetricValues } from '../database/DatabaseMetricValues';\nimport { mergeEntityAndProviderThresholds } from '../utils/mergeEntityAndProviderThresholds';\nimport { isMetricCalculationError } from '../utils/metricCalculationError';\nimport { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n};\n\nexport class CatalogMetricService {\n private static entityHealthSummary(\n accessibleRows: DbMetricValue[],\n countsArePartial: boolean,\n ): ScorecardEntityHealthSummary {\n const calculationErrorCount = accessibleRows.filter(row =>\n isMetricCalculationError(row),\n ).length;\n return {\n totalEntities: accessibleRows.length,\n calculationErrorCount,\n countsArePartial,\n };\n }\n\n private readonly logger: LoggerService;\n\n private readonly catalog: CatalogService;\n private readonly auth: AuthService;\n private readonly registry: MetricProvidersRegistry;\n private readonly database: DatabaseMetricValues;\n\n private static readonly MAX_FETCHABLE_ROWS = 10_000;\n private static readonly BATCH_SIZE = 100;\n\n constructor(options: CatalogMetricServiceOptions) {\n this.catalog = options.catalog;\n this.auth = options.auth;\n this.registry = options.registry;\n this.database = options.database;\n this.logger = options.logger;\n }\n\n /**\n * Get latest metric results for a specific catalog entity.\n *\n * @param entityRef - Entity reference in format \"kind:namespace/name\"\n * @param metricIds - Optional array of metric IDs to get latest metrics of.\n * If not provided, gets all available latest metrics.\n * @param filter - Permission filter\n * @returns Metric results with entity-specific thresholds applied\n */\n async getLatestEntityMetrics(\n entityRef: string,\n metricIds?: string[],\n filter?: PermissionCriteria<\n PermissionCondition<string, PermissionRuleParams>\n >,\n ): Promise<MetricResult[]> {\n const entity = await this.catalog.getEntityByRef(entityRef, {\n credentials: await this.auth.getOwnServiceCredentials(),\n });\n if (!entity) {\n throw new NotFoundError(`Entity not found: ${entityRef}`);\n }\n\n const metricsToFetch = this.registry.listMetrics(metricIds);\n\n const authorizedMetricsToFetch = filterAuthorizedMetrics(\n metricsToFetch,\n filter,\n );\n const rawResults = await this.database.readLatestEntityMetricValues(\n entityRef,\n authorizedMetricsToFetch.map(m => m.id),\n );\n\n return rawResults.map(\n ({ metric_id, value, error_message, timestamp, status }) => {\n let thresholds: ThresholdConfig | undefined;\n let thresholdError: string | undefined;\n\n const provider = this.registry.getProvider(metric_id);\n const metric = this.registry.getMetric(metric_id);\n\n try {\n thresholds = mergeEntityAndProviderThresholds(entity, provider);\n\n if (value === null) {\n thresholdError =\n 'Unable to evaluate thresholds, metric value is missing';\n } else if (error_message) {\n thresholdError = error_message;\n }\n } catch (error) {\n thresholdError = stringifyError(error);\n }\n\n const isMetricCalcError = isMetricCalculationError({\n value,\n error_message,\n });\n\n return {\n id: metric.id,\n status: isMetricCalcError ? 'error' : 'success',\n metadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n history: metric.history,\n },\n ...(isMetricCalcError && {\n error:\n error_message ??\n stringifyError(new Error(`Metric value is 'undefined'`)),\n }),\n result: {\n value,\n timestamp: new Date(timestamp).toISOString(),\n thresholdResult: {\n definition: thresholds,\n status: thresholdError ? 'error' : 'success',\n evaluation: status,\n ...(thresholdError && { error: thresholdError }),\n },\n },\n };\n },\n );\n }\n\n /**\n * Get an aggregated metric by status grouped for multiple entities and a single metric ID.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @returns Aggregated metric by status grouped results\n */\n async getStatusGroupedAggregatedMetrics(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n\n /**\n * Get an aggregated metric by aggregation type.\n *\n * @param entityRefs - Array of entity references in format \"kind:namespace/name\"\n * @param metricId - Metric ID to aggregate.\n * @param aggregationType - Aggregation type to use.\n * @returns Aggregated metric by aggregation type results\n */\n async getAggregatedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n aggregationType: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length !== 0) {\n if (aggregationType === aggregationTypes.statusGrouped) {\n return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);\n }\n throw new Error(`Unsupported aggregation type: ${aggregationType}`);\n }\n\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n /**\n * Get detailed entity metrics for drill-down with filtering, sorting, and pagination.\n *\n * Fetches individual entity metric values and enriches them with catalog metadata.\n * Supports database-level filtering (status, owner, kind, entityName),\n * database-level sorting, and in-memory pagination over the permission-filtered result set.\n * Returns empty entities if the catalog is unavailable (fail-secure).\n *\n * @param metricId - Metric ID to fetch (e.g., \"github.open_prs\")\n * @param options - Query options for filtering, sorting, and pagination\n * @param options.status - Filter by threshold status (database-level)\n * @param options.owner - Filter by owner entity reference (database-level)\n * @param options.kind - Filter by entity kind (database-level)\n * @param options.entityName - Substring search against the entity ref `kind:namespace/name` (database-level)\n * @param options.namespace - Exact match against the entity namespace (database-level)\n * @param options.sortBy - Field to sort by (default: \"timestamp\")\n * @param options.sortOrder - Sort direction: \"asc\" or \"desc\" (default: \"desc\")\n * @param options.page - Page number (1-indexed)\n * @param options.limit - Entities per page (max: 100)\n * @returns Paginated entity metric details with metadata\n */\n async getEntityMetricDetails(\n metricId: string,\n credentials: BackstageCredentials,\n options: {\n status?: string;\n owner?: string[];\n kind?: string;\n entityName?: string;\n namespace?: string;\n sortBy?:\n | 'entityName'\n | 'owner'\n | 'entityKind'\n | 'timestamp'\n | 'metricValue'\n | 'namespace'\n | 'status';\n sortOrder?: 'asc' | 'desc';\n page: number;\n limit: number;\n },\n ): Promise<EntityMetricDetailResponse> {\n // Get metric metadata\n const metric = this.registry.getMetric(metricId);\n\n // High-page early-exit guard\n if (\n (options.page - 1) * options.limit >=\n CatalogMetricService.MAX_FETCHABLE_ROWS\n ) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // Query database with all DB filters first\n // At the moment, this is going to be an O(MAX_FETCHABLE_ROWS) cost and is intentional to avoid leaking\n // pre-auth counts. MAX_FETCHABLE_ROWS and BATCH_SIZE are to be used as a method to of adjustment to\n // find the right amount of performance\n const rows = await this.database.readEntityMetricsWithFilters(metricId, {\n status: options.status,\n entityName: options.entityName,\n entityKind: options.kind,\n entityNamespace: options.namespace,\n entityOwner: options.owner,\n sortBy: options.sortBy,\n sortOrder: options.sortOrder,\n pagination: {\n limit: CatalogMetricService.MAX_FETCHABLE_ROWS,\n offset: 0,\n },\n });\n\n // Filter to authorized rows by batching through catalog.getEntitiesByRefs with user\n // credentials. The catalog enforces auth natively: null = unauthorized or deleted.\n // We also cache the returned Entity objects so we can enrich the page rows without\n // a second catalog round-trip. Sequential processing preserves DB sort order.\n const entityMap = new Map<string, Entity>();\n const accessibleRows: DbMetricValue[] = [];\n try {\n for (let i = 0; i < rows.length; i += CatalogMetricService.BATCH_SIZE) {\n const batch = rows.slice(i, i + CatalogMetricService.BATCH_SIZE);\n const response = await this.catalog.getEntitiesByRefs(\n {\n entityRefs: batch.map(row => row.catalog_entity_ref),\n fields: [\n 'kind',\n 'metadata.name',\n 'metadata.namespace',\n 'spec.owner',\n ],\n },\n { credentials },\n );\n\n // Filter out the unauthorized entities\n for (let j = 0; j < batch.length; j++) {\n const entity = response.items[j];\n if (!entity) continue; // null = unauthorized or not found, skip\n entityMap.set(batch[j].catalog_entity_ref, entity);\n accessibleRows.push(batch[j]);\n }\n }\n } catch (error) {\n // Fail secure: if the catalog is unavailable we cannot confirm authorization,\n // so return empty rather than potentially unauthorized data.\n this.logger.error('Failed to fetch entities from catalog', { error });\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: 0,\n totalPages: 0,\n isCapped: false,\n },\n entityHealth: CatalogMetricService.entityHealthSummary([], false),\n };\n }\n\n // True when DB results were capped; pagination.total may undercount the full dataset.\n const isCapped = rows.length === CatalogMetricService.MAX_FETCHABLE_ROWS;\n\n // Apply pagination to filtered entities\n const totalFiltered = accessibleRows.length;\n const pageRows = accessibleRows.slice(\n (options.page - 1) * options.limit,\n options.page * options.limit,\n );\n\n // No rows on this page — either no matching results or the requested page is beyond\n // the last page.\n if (pageRows.length === 0) {\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: [],\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n\n // Enrich page rows from the cached entity map\n const enrichedEntities: EntityMetricDetail[] = [];\n for (const row of pageRows) {\n const entity = entityMap.get(row.catalog_entity_ref);\n if (!entity) continue;\n enrichedEntities.push({\n entityRef: row.catalog_entity_ref,\n entityNamespace: entity.metadata.namespace,\n entityName: entity.metadata.name,\n entityKind: entity.kind,\n owner: normalizeOwnerRef(entity.spec?.owner) ?? '',\n metricValue: row.value,\n timestamp: new Date(row.timestamp).toISOString(),\n status: row.status,\n });\n }\n\n // Format and return response\n return {\n metricId: metric.id,\n metricMetadata: {\n title: metric.title,\n description: metric.description,\n type: metric.type,\n },\n entities: enrichedEntities,\n pagination: {\n page: options.page,\n pageSize: options.limit,\n total: totalFiltered,\n totalPages: Math.ceil(totalFiltered / options.limit),\n isCapped,\n },\n entityHealth: CatalogMetricService.entityHealthSummary(\n accessibleRows,\n isCapped,\n ),\n };\n }\n}\n"],"names":["isMetricCalculationError","NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AggregatedMetricLoader.cjs.js","sources":["../../../src/service/aggregations/AggregatedMetricLoader.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AggregatedMetric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport { AggregatedMetricMapper } from '../mappers';\n\nexport class AggregatedMetricLoader {\n constructor(private readonly database: DatabaseMetricValues) {}\n\n async loadStatusGroupedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length === 0) {\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAoBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,YAA6B,QAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAAiC,
|
|
1
|
+
{"version":3,"file":"AggregatedMetricLoader.cjs.js","sources":["../../../src/service/aggregations/AggregatedMetricLoader.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AggregatedMetric } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport { AggregatedMetricMapper } from '../mappers';\n\nexport class AggregatedMetricLoader {\n constructor(private readonly database: DatabaseMetricValues) {}\n\n async loadStatusGroupedMetricByEntityRefs(\n entityRefs: string[],\n metricId: string,\n ): Promise<AggregatedMetric> {\n if (entityRefs.length === 0) {\n return AggregatedMetricMapper.toAggregatedMetric();\n }\n\n const aggregatedMetric =\n await this.database.readAggregatedMetricByEntityRefs(\n entityRefs,\n metricId,\n );\n\n return AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAoBO,MAAM,sBAAuB,CAAA;AAAA,EAClC,YAA6B,QAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA;AAAiC,EAE9D,MAAM,mCACJ,CAAA,UAAA,EACA,QAC2B,EAAA;AAC3B,IAAI,IAAA,UAAA,CAAW,WAAW,CAAG,EAAA;AAC3B,MAAA,OAAOA,+BAAuB,kBAAmB,EAAA;AAAA;AAGnD,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,QAAS,CAAA,gCAAA;AAAA,MAClB,UAAA;AAAA,MACA;AAAA,KACF;AAEF,IAAO,OAAAA,8BAAA,CAAuB,mBAAmB,gBAAgB,CAAA;AAAA;AAErE;;;;"}
|
|
@@ -9,8 +9,6 @@ class AverageAggregationStrategy {
|
|
|
9
9
|
this.loader = loader;
|
|
10
10
|
this.logger = logger;
|
|
11
11
|
}
|
|
12
|
-
loader;
|
|
13
|
-
logger;
|
|
14
12
|
async aggregate({
|
|
15
13
|
entityRefs,
|
|
16
14
|
metric,
|
|
@@ -57,6 +55,8 @@ class AverageAggregationStrategy {
|
|
|
57
55
|
const result = {
|
|
58
56
|
total: aggregatedMetric.total,
|
|
59
57
|
timestamp: aggregatedMetric.timestamp,
|
|
58
|
+
entitiesConsidered: aggregatedMetric.entitiesConsidered,
|
|
59
|
+
calculationErrorCount: aggregatedMetric.calculationErrorCount,
|
|
60
60
|
values: thresholds.rules.map((rule) => ({
|
|
61
61
|
name: rule.key,
|
|
62
62
|
count: aggregatedMetric.values[rule.key] ?? 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AverageAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/AverageAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AggregatedMetric,\n type AggregatedMetricAverageResult,\n type AggregatedMetricResult,\n type ThresholdConfig,\n ThresholdRule,\n type AggregationConfigOptions,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\n\nexport class AverageAggregationStrategy implements AggregationStrategy {\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n const { options } = aggregationConfig;\n\n if (!options?.statusScores) {\n throw new Error(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores\" is required for average aggregation`,\n );\n }\n\n if (!options.thresholds) {\n this.logger.info(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds\" is not configured for average aggregation; ` +\n 'using the default 0–100% health scale (higher is better).',\n );\n }\n\n const headlineThresholds =\n options.thresholds ?? DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n options.statusScores,\n metric.id,\n );\n\n const { averageScore, maxPossibleScore } = this.prepareScoreValues(\n aggregatedMetric.total,\n options.statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const scorePercent = averageScore * 100;\n\n const aggregationChartDisplayColor = this.getAggregationChartDisplayColor(\n scorePercent,\n headlineThresholds,\n );\n\n if (!aggregationChartDisplayColor) {\n throw new Error(\n `The color for percentage '${scorePercent}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n );\n }\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: options.statusScores[rule.key] ?? 0,\n })),\n thresholds,\n averageScore,\n averageWeightedSum: weightedSum,\n averageMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as AggregatedMetricAverageResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: AggregationConfigOptions['statusScores'],\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for average aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private getAggregationChartDisplayColor(\n scorePercent: number,\n thresholds: ThresholdConfig,\n ): string | undefined {\n const thresholdEvaluator = new ThresholdEvaluator();\n\n const matchedThresholdKey = thresholdEvaluator.getFirstMatchingThreshold(\n scorePercent,\n 'number',\n thresholds,\n );\n\n return thresholds.rules.find(r => r.key === matchedThresholdKey)?.color;\n }\n\n private prepareScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: AggregationConfigOptions['statusScores'],\n rules: ThresholdRule[],\n weightedSum: number,\n ): { averageScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const precision = 1000;\n\n const averageScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * precision) / precision\n : 0;\n\n return { averageScore, maxPossibleScore };\n }\n}\n"],"names":["DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS","AggregatedMetricMapper","ThresholdEvaluator"],"mappings":";;;;;;AAgCO,MAAM,0BAA0D,CAAA;AAAA,EACrE,WAAA,CACmB,QACA,MACjB,EAAA;AAFiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAChB,
|
|
1
|
+
{"version":3,"file":"AverageAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/AverageAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AggregatedMetric,\n type AggregatedMetricAverageResult,\n type AggregatedMetricResult,\n type ThresholdConfig,\n ThresholdRule,\n type AggregationConfigOptions,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator';\n\nexport class AverageAggregationStrategy implements AggregationStrategy {\n constructor(\n private readonly loader: AggregatedMetricLoader,\n private readonly logger: LoggerService,\n ) {}\n\n async aggregate({\n entityRefs,\n metric,\n thresholds,\n aggregationConfig,\n }: AggregationOptions): Promise<AggregatedMetricResult> {\n const { options } = aggregationConfig;\n\n if (!options?.statusScores) {\n throw new Error(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores\" is required for average aggregation`,\n );\n }\n\n if (!options.thresholds) {\n this.logger.info(\n `The \"scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds\" is not configured for average aggregation; ` +\n 'using the default 0–100% health scale (higher is better).',\n );\n }\n\n const headlineThresholds =\n options.thresholds ?? DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const weightedSum = this.calculateWeightedSum(\n aggregatedMetric.values,\n options.statusScores,\n metric.id,\n );\n\n const { averageScore, maxPossibleScore } = this.prepareScoreValues(\n aggregatedMetric.total,\n options.statusScores,\n thresholds.rules,\n weightedSum,\n );\n\n const scorePercent = averageScore * 100;\n\n const aggregationChartDisplayColor = this.getAggregationChartDisplayColor(\n scorePercent,\n headlineThresholds,\n );\n\n if (!aggregationChartDisplayColor) {\n throw new Error(\n `The color for percentage '${scorePercent}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`,\n );\n }\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n score: options.statusScores[rule.key] ?? 0,\n })),\n thresholds,\n averageScore,\n averageWeightedSum: weightedSum,\n averageMaxPossible: maxPossibleScore,\n aggregationChartDisplayColor,\n } as AggregatedMetricAverageResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n\n private calculateWeightedSum(\n values: Pick<AggregatedMetric, 'values'>['values'],\n statusScores: AggregationConfigOptions['statusScores'],\n metricId: string,\n ): number {\n let weightedSum = 0;\n for (const [status, count] of Object.entries(values)) {\n const score = statusScores[status];\n\n if (score === undefined) {\n this.logger.warn(\n `The status \"${status}\" is not in the statusScores for average aggregation of metric \"${metricId}\"`,\n );\n }\n weightedSum += count * (score ?? 0);\n }\n return weightedSum;\n }\n\n private getAggregationChartDisplayColor(\n scorePercent: number,\n thresholds: ThresholdConfig,\n ): string | undefined {\n const thresholdEvaluator = new ThresholdEvaluator();\n\n const matchedThresholdKey = thresholdEvaluator.getFirstMatchingThreshold(\n scorePercent,\n 'number',\n thresholds,\n );\n\n return thresholds.rules.find(r => r.key === matchedThresholdKey)?.color;\n }\n\n private prepareScoreValues(\n numberOfEntities: Pick<AggregatedMetric, 'total'>['total'],\n statusScores: AggregationConfigOptions['statusScores'],\n rules: ThresholdRule[],\n weightedSum: number,\n ): { averageScore: number; maxPossibleScore: number } {\n const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0);\n\n const maxScore = Math.max(0, ...statusScoresValues);\n\n const maxPossibleScore = maxScore * numberOfEntities;\n\n const precision = 1000;\n\n const averageScore =\n numberOfEntities > 0 && maxPossibleScore > 0\n ? Math.round((weightedSum / maxPossibleScore) * precision) / precision\n : 0;\n\n return { averageScore, maxPossibleScore };\n }\n}\n"],"names":["DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS","AggregatedMetricMapper","ThresholdEvaluator"],"mappings":"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"}
|
|
@@ -6,7 +6,6 @@ class StatusGroupedAggregationStrategy {
|
|
|
6
6
|
constructor(loader) {
|
|
7
7
|
this.loader = loader;
|
|
8
8
|
}
|
|
9
|
-
loader;
|
|
10
9
|
async aggregate(options) {
|
|
11
10
|
const { entityRefs, metric, thresholds, aggregationConfig } = options;
|
|
12
11
|
const aggregatedMetric = await this.loader.loadStatusGroupedMetricByEntityRefs(
|
|
@@ -16,6 +15,8 @@ class StatusGroupedAggregationStrategy {
|
|
|
16
15
|
const result = {
|
|
17
16
|
total: aggregatedMetric.total,
|
|
18
17
|
timestamp: aggregatedMetric.timestamp,
|
|
18
|
+
entitiesConsidered: aggregatedMetric.entitiesConsidered,
|
|
19
|
+
calculationErrorCount: aggregatedMetric.calculationErrorCount,
|
|
19
20
|
values: thresholds.rules.map((rule) => ({
|
|
20
21
|
name: rule.key,
|
|
21
22
|
count: aggregatedMetric.values[rule.key] ?? 0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusGroupedAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/StatusGroupedAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n AggregatedMetricResult,\n StatusGroupedAggregationResult,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\n\nexport class StatusGroupedAggregationStrategy implements AggregationStrategy {\n constructor(private readonly loader: AggregatedMetricLoader) {}\n\n async aggregate(\n options: AggregationOptions,\n ): Promise<AggregatedMetricResult> {\n const { entityRefs, metric, thresholds, aggregationConfig } = options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n })),\n thresholds,\n } as StatusGroupedAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAyBO,MAAM,gCAAgE,CAAA;AAAA,EAC3E,YAA6B,MAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAiC,
|
|
1
|
+
{"version":3,"file":"StatusGroupedAggregationStrategy.cjs.js","sources":["../../../../src/service/aggregations/strategies/StatusGroupedAggregationStrategy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n AggregatedMetricResult,\n StatusGroupedAggregationResult,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { AggregatedMetricMapper } from '../../mappers';\nimport type { AggregatedMetricLoader } from '../AggregatedMetricLoader';\nimport type { AggregationOptions } from '../types';\nimport type { AggregationStrategy } from './types';\n\nexport class StatusGroupedAggregationStrategy implements AggregationStrategy {\n constructor(private readonly loader: AggregatedMetricLoader) {}\n\n async aggregate(\n options: AggregationOptions,\n ): Promise<AggregatedMetricResult> {\n const { entityRefs, metric, thresholds, aggregationConfig } = options;\n\n const aggregatedMetric =\n await this.loader.loadStatusGroupedMetricByEntityRefs(\n entityRefs,\n metric.id,\n );\n\n const result = {\n total: aggregatedMetric.total,\n timestamp: aggregatedMetric.timestamp,\n entitiesConsidered: aggregatedMetric.entitiesConsidered,\n calculationErrorCount: aggregatedMetric.calculationErrorCount,\n values: thresholds.rules.map(rule => ({\n name: rule.key,\n count: aggregatedMetric.values[rule.key] ?? 0,\n })),\n thresholds,\n } as StatusGroupedAggregationResult;\n\n return AggregatedMetricMapper.toAggregatedMetricResult(\n metric,\n result,\n aggregationConfig,\n );\n }\n}\n"],"names":["AggregatedMetricMapper"],"mappings":";;;;AAyBO,MAAM,gCAAgE,CAAA;AAAA,EAC3E,YAA6B,MAAgC,EAAA;AAAhC,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA;AAAiC,EAE9D,MAAM,UACJ,OACiC,EAAA;AACjC,IAAA,MAAM,EAAE,UAAA,EAAY,MAAQ,EAAA,UAAA,EAAY,mBAAsB,GAAA,OAAA;AAE9D,IAAM,MAAA,gBAAA,GACJ,MAAM,IAAA,CAAK,MAAO,CAAA,mCAAA;AAAA,MAChB,UAAA;AAAA,MACA,MAAO,CAAA;AAAA,KACT;AAEF,IAAA,MAAM,MAAS,GAAA;AAAA,MACb,OAAO,gBAAiB,CAAA,KAAA;AAAA,MACxB,WAAW,gBAAiB,CAAA,SAAA;AAAA,MAC5B,oBAAoB,gBAAiB,CAAA,kBAAA;AAAA,MACrC,uBAAuB,gBAAiB,CAAA,qBAAA;AAAA,MACxC,MAAQ,EAAA,UAAA,CAAW,KAAM,CAAA,GAAA,CAAI,CAAS,IAAA,MAAA;AAAA,QACpC,MAAM,IAAK,CAAA,GAAA;AAAA,QACX,KAAO,EAAA,gBAAA,CAAiB,MAAO,CAAA,IAAA,CAAK,GAAG,CAAK,IAAA;AAAA,OAC5C,CAAA,CAAA;AAAA,MACF;AAAA,KACF;AAEA,IAAA,OAAOA,8BAAuB,CAAA,wBAAA;AAAA,MAC5B,MAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF;AAAA;AAEJ;;;;"}
|
|
@@ -9,7 +9,9 @@ class AggregatedMetricMapper {
|
|
|
9
9
|
return {
|
|
10
10
|
values: aggregatedMetric?.statusCounts ?? {},
|
|
11
11
|
total,
|
|
12
|
-
timestamp
|
|
12
|
+
timestamp,
|
|
13
|
+
entitiesConsidered: aggregatedMetric?.latest_entity_count ?? 0,
|
|
14
|
+
calculationErrorCount: aggregatedMetric?.calculation_error_count ?? 0
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
static toAggregationMetadata(metric, aggregationConfig) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AggregatedMetric,\n AggregatedMetricResult,\n AggregationMetadata,\n Metric,\n aggregationTypes,\n AggregationResultByType,\n type AggregationConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const total = aggregatedMetric?.total ?? 0;\n const timestamp = aggregatedMetric?.max_timestamp\n ? new Date(aggregatedMetric.max_timestamp).toISOString()\n : new Date().toISOString();\n\n return {\n values: aggregatedMetric?.statusCounts ?? {},\n total,\n timestamp,\n };\n }\n\n static toAggregationMetadata(\n metric: Metric,\n aggregationConfig?: AggregationConfig,\n ): AggregationMetadata {\n return {\n title: aggregationConfig?.title ?? metric.title,\n description: aggregationConfig?.description ?? metric.description,\n type: metric.type,\n history: metric.history,\n aggregationType:\n aggregationConfig?.type ?? aggregationTypes.statusGrouped, // By default, return the status grouped aggregation type\n };\n }\n\n static toAggregatedMetricResult(\n metric: Metric,\n result: AggregationResultByType,\n aggregationConfig?: AggregationConfig,\n ): AggregatedMetricResult {\n return {\n id: metric.id,\n status: 'success',\n metadata: this.toAggregationMetadata(metric, aggregationConfig),\n result,\n };\n }\n}\n"],"names":["aggregationTypes"],"mappings":";;;;AA2BO,MAAM,sBAAuB,CAAA;AAAA,EAClC,OAAO,mBACL,gBACkB,EAAA;AAClB,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAA,MAAM,SAAY,GAAA,gBAAA,EAAkB,aAChC,GAAA,IAAI,IAAK,CAAA,gBAAA,CAAiB,aAAa,CAAA,CAAE,WAAY,EAAA,GAAA,iBACjD,IAAA,IAAA,IAAO,WAAY,EAAA;AAE3B,IAAO,OAAA;AAAA,MACL,MAAA,EAAQ,gBAAkB,EAAA,YAAA,IAAgB,EAAC;AAAA,MAC3C,KAAA;AAAA,MACA;AAAA,
|
|
1
|
+
{"version":3,"file":"mappers.cjs.js","sources":["../../src/service/mappers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AggregatedMetric,\n AggregatedMetricResult,\n AggregationMetadata,\n Metric,\n aggregationTypes,\n AggregationResultByType,\n type AggregationConfig,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { DbAggregatedMetric } from '../database/types';\n\nexport class AggregatedMetricMapper {\n static toAggregatedMetric(\n aggregatedMetric?: DbAggregatedMetric,\n ): AggregatedMetric {\n const total = aggregatedMetric?.total ?? 0;\n const timestamp = aggregatedMetric?.max_timestamp\n ? new Date(aggregatedMetric.max_timestamp).toISOString()\n : new Date().toISOString();\n\n return {\n values: aggregatedMetric?.statusCounts ?? {},\n total,\n timestamp,\n entitiesConsidered: aggregatedMetric?.latest_entity_count ?? 0,\n calculationErrorCount: aggregatedMetric?.calculation_error_count ?? 0,\n };\n }\n\n static toAggregationMetadata(\n metric: Metric,\n aggregationConfig?: AggregationConfig,\n ): AggregationMetadata {\n return {\n title: aggregationConfig?.title ?? metric.title,\n description: aggregationConfig?.description ?? metric.description,\n type: metric.type,\n history: metric.history,\n aggregationType:\n aggregationConfig?.type ?? aggregationTypes.statusGrouped, // By default, return the status grouped aggregation type\n };\n }\n\n static toAggregatedMetricResult(\n metric: Metric,\n result: AggregationResultByType,\n aggregationConfig?: AggregationConfig,\n ): AggregatedMetricResult {\n return {\n id: metric.id,\n status: 'success',\n metadata: this.toAggregationMetadata(metric, aggregationConfig),\n result,\n };\n }\n}\n"],"names":["aggregationTypes"],"mappings":";;;;AA2BO,MAAM,sBAAuB,CAAA;AAAA,EAClC,OAAO,mBACL,gBACkB,EAAA;AAClB,IAAM,MAAA,KAAA,GAAQ,kBAAkB,KAAS,IAAA,CAAA;AACzC,IAAA,MAAM,SAAY,GAAA,gBAAA,EAAkB,aAChC,GAAA,IAAI,IAAK,CAAA,gBAAA,CAAiB,aAAa,CAAA,CAAE,WAAY,EAAA,GAAA,iBACjD,IAAA,IAAA,IAAO,WAAY,EAAA;AAE3B,IAAO,OAAA;AAAA,MACL,MAAA,EAAQ,gBAAkB,EAAA,YAAA,IAAgB,EAAC;AAAA,MAC3C,KAAA;AAAA,MACA,SAAA;AAAA,MACA,kBAAA,EAAoB,kBAAkB,mBAAuB,IAAA,CAAA;AAAA,MAC7D,qBAAA,EAAuB,kBAAkB,uBAA2B,IAAA;AAAA,KACtE;AAAA;AACF,EAEA,OAAO,qBACL,CAAA,MAAA,EACA,iBACqB,EAAA;AACrB,IAAO,OAAA;AAAA,MACL,KAAA,EAAO,iBAAmB,EAAA,KAAA,IAAS,MAAO,CAAA,KAAA;AAAA,MAC1C,WAAA,EAAa,iBAAmB,EAAA,WAAA,IAAe,MAAO,CAAA,WAAA;AAAA,MACtD,MAAM,MAAO,CAAA,IAAA;AAAA,MACb,SAAS,MAAO,CAAA,OAAA;AAAA,MAChB,eAAA,EACE,iBAAmB,EAAA,IAAA,IAAQA,+CAAiB,CAAA;AAAA;AAAA,KAChD;AAAA;AACF,EAEA,OAAO,wBAAA,CACL,MACA,EAAA,MAAA,EACA,iBACwB,EAAA;AACxB,IAAO,OAAA;AAAA,MACL,IAAI,MAAO,CAAA,EAAA;AAAA,MACX,MAAQ,EAAA,SAAA;AAAA,MACR,QAAU,EAAA,IAAA,CAAK,qBAAsB,CAAA,MAAA,EAAQ,iBAAiB,CAAA;AAAA,MAC9D;AAAA,KACF;AAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricCalculationError.cjs.js","sources":["../../src/utils/metricCalculationError.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { DbMetricValue } from '../database/types';\n\n/**\n * True when the persisted latest row represents a provider-side calculation failure,\n * aligned with `CatalogMetricService.getLatestEntityMetrics`.\n */\nexport function isMetricCalculationError(row: {\n value: DbMetricValue['value'];\n error_message: DbMetricValue['error_message'];\n}): boolean {\n return row.error_message !== null && row.value === null;\n}\n"],"names":[],"mappings":";;AAsBO,SAAS,yBAAyB,GAG7B,EAAA;AACV,EAAA,OAAO,GAAI,CAAA,aAAA,KAAkB,IAAQ,IAAA,GAAA,CAAI,KAAU,KAAA,IAAA;AACrD;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@backstage/plugin-catalog-node": "^2.1.0",
|
|
48
48
|
"@backstage/plugin-permission-common": "^0.9.7",
|
|
49
49
|
"@backstage/plugin-permission-node": "^0.10.11",
|
|
50
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.7.
|
|
51
|
-
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.7.
|
|
50
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "^2.7.2",
|
|
51
|
+
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "^2.7.2",
|
|
52
52
|
"express": "^4.17.1",
|
|
53
53
|
"express-promise-router": "^4.1.0",
|
|
54
54
|
"knex": "^3.1.0",
|