@red-hat-developer-hub/backstage-plugin-scorecard-backend 2.6.0 → 2.7.0
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 +12 -0
- package/README.md +31 -8
- package/config.d.ts +20 -42
- package/dist/constants/aggregationKPIs.cjs.js +22 -0
- package/dist/constants/aggregationKPIs.cjs.js.map +1 -1
- package/dist/plugin.cjs.js +12 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/CatalogMetricService.cjs.js +0 -68
- package/dist/service/CatalogMetricService.cjs.js.map +1 -1
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js +23 -0
- package/dist/service/aggregations/AggregatedMetricLoader.cjs.js.map +1 -0
- package/dist/service/aggregations/AggregationService.cjs.js +54 -0
- package/dist/service/aggregations/AggregationService.cjs.js.map +1 -0
- package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js +110 -0
- package/dist/service/aggregations/strategies/AverageAggregationStrategy.cjs.js.map +1 -0
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js +34 -0
- package/dist/service/aggregations/strategies/StatusGroupedAggregationStrategy.cjs.js.map +1 -0
- package/dist/service/aggregations/strategies/registerStrategies.cjs.js +18 -0
- package/dist/service/aggregations/strategies/registerStrategies.cjs.js.map +1 -0
- package/dist/service/mappers.cjs.js +2 -10
- package/dist/service/mappers.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +14 -26
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/utils/buildAggregationConfig.cjs.js +31 -1
- package/dist/utils/buildAggregationConfig.cjs.js.map +1 -1
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js +1 -1
- package/dist/utils/mergeEntityAndProviderThresholds.cjs.js.map +1 -1
- package/dist/validation/schemas/aggregationConfigSchemas.cjs.js +40 -0
- package/dist/validation/schemas/aggregationConfigSchemas.cjs.js.map +1 -0
- package/dist/validation/validateAggregationConfig.cjs.js +7 -10
- package/dist/validation/validateAggregationConfig.cjs.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-scorecard-backend
|
|
2
2
|
|
|
3
|
+
## 2.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bf72ffc: Adds `**average**` as an aggregation KPI type alongside `**statusGrouped**`, with configurable `**options.statusScores**` and optional `**options.thresholds**` (same shape as metric thresholds) for homepage donut coloring against `**averageScore × 100**`, with built-in defaults when omitted.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [bf72ffc]
|
|
12
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-common@2.7.0
|
|
13
|
+
- @red-hat-developer-hub/backstage-plugin-scorecard-node@2.7.0
|
|
14
|
+
|
|
3
15
|
## 2.6.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ Thresholds define conditions to assign metric values to specific visual categori
|
|
|
118
118
|
|
|
119
119
|
Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`).
|
|
120
120
|
|
|
121
|
-
For comprehensive threshold configuration guide, examples,
|
|
121
|
+
For comprehensive threshold configuration guide, examples, best practices, and **aggregation KPI result thresholds** for **`type: average`**, see [thresholds.md](./docs/thresholds.md).
|
|
122
122
|
|
|
123
123
|
## Aggregation KPIs (homepage and `GET /aggregations`)
|
|
124
124
|
|
|
@@ -137,18 +137,41 @@ scorecard:
|
|
|
137
137
|
description: 'Open pull requests grouped by status.'
|
|
138
138
|
type: statusGrouped
|
|
139
139
|
metricId: github.open_prs
|
|
140
|
+
openPrsWeightedKpi:
|
|
141
|
+
title: 'GitHub open PRs (weighted health)'
|
|
142
|
+
description: 'Weighted health from status counts using configurable scores.'
|
|
143
|
+
type: average
|
|
144
|
+
metricId: github.open_prs
|
|
145
|
+
options:
|
|
146
|
+
statusScores:
|
|
147
|
+
success: 100
|
|
148
|
+
warning: 50
|
|
149
|
+
error: 0
|
|
150
|
+
# Optional: colors for the average-score donut (expressions apply to percentage 0–100)
|
|
151
|
+
thresholds:
|
|
152
|
+
rules:
|
|
153
|
+
- key: success
|
|
154
|
+
expression: '>=75'
|
|
155
|
+
color: success.main
|
|
156
|
+
- key: warning
|
|
157
|
+
expression: '10-74'
|
|
158
|
+
color: warning.main
|
|
159
|
+
- key: error
|
|
160
|
+
expression: '<10'
|
|
161
|
+
color: error.main
|
|
140
162
|
```
|
|
141
163
|
|
|
142
|
-
| Field | Description
|
|
143
|
-
| ------------- |
|
|
144
|
-
| `title` | Display title for this aggregation (returned in API metadata).
|
|
145
|
-
| `description` | Display description for this aggregation.
|
|
146
|
-
| `type` | Aggregation algorithm
|
|
147
|
-
| `metricId` | Metric provider id used to load thresholds and compute counts.
|
|
164
|
+
| Field | Description |
|
|
165
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
166
|
+
| `title` | Display title for this aggregation (returned in API metadata). |
|
|
167
|
+
| `description` | Display description for this aggregation. |
|
|
168
|
+
| `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `average` (normalized weighted score). |
|
|
169
|
+
| `metricId` | Metric provider id used to load thresholds and compute counts. |
|
|
170
|
+
| `options` | Optional for `statusGrouped`. **Required** for `average`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-average-type)); evaluated on **`averageScore × 100`**; first match sets **`aggregationChartDisplayColor`**. The API includes **`averageScore`** (ratio **0–1**, **one decimal**), **`averageWeightedSum`**, **`averageMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). |
|
|
148
171
|
|
|
149
172
|
- **Path**: `scorecard.aggregationKPIs.<aggregationId>`.
|
|
150
173
|
- If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description.
|
|
151
|
-
- Invalid
|
|
174
|
+
- **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`average`** KPIs without **`options.statusScores`**, bad expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation).
|
|
152
175
|
|
|
153
176
|
**Homepage cards** are configured in the app (for example Dynamic Home Page mount points). They should pass **`aggregationId`** matching a key in `aggregationKPIs` or the metric id for the default case. See the [Scorecard frontend plugin README](../scorecard/README.md#homepage-scorecard-cards).
|
|
154
177
|
|
package/config.d.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
AggregationType,
|
|
20
|
+
ThresholdConfig,
|
|
21
|
+
AggregationThresholdRule,
|
|
22
|
+
} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
|
|
19
23
|
|
|
20
24
|
export interface Config {
|
|
21
25
|
/** Configuration for scorecard plugin */
|
|
@@ -32,6 +36,20 @@ export interface Config {
|
|
|
32
36
|
type: AggregationType;
|
|
33
37
|
/** Metric ID for which the aggregation is calculated */
|
|
34
38
|
metricId: string;
|
|
39
|
+
/** Type-specific settings */
|
|
40
|
+
options?: {
|
|
41
|
+
/** Required under `options` when `type` is `average` */
|
|
42
|
+
statusScores?: {
|
|
43
|
+
[thresholdRuleKey: string]: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Optional: threshold rules for coloring the KPI headline value from the aggregation result
|
|
47
|
+
* (e.g. average percentage 0–100 for `average` KPIs).
|
|
48
|
+
*/
|
|
49
|
+
thresholds?: {
|
|
50
|
+
rules: AggregationThresholdRule[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
35
53
|
};
|
|
36
54
|
};
|
|
37
55
|
/** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
|
|
@@ -57,47 +75,7 @@ export interface Config {
|
|
|
57
75
|
*/
|
|
58
76
|
[metricName: string]: {
|
|
59
77
|
/** Threshold configuration for the metric */
|
|
60
|
-
thresholds?:
|
|
61
|
-
/**
|
|
62
|
-
* Rules describe how metric values are categorized and how that category is presented in the UI.
|
|
63
|
-
* They are evaluated in order and the first matching rule is applied.
|
|
64
|
-
*/
|
|
65
|
-
rules?: Array<{
|
|
66
|
-
/**
|
|
67
|
-
* Threshold category key that a metric value is assigned to when this rule
|
|
68
|
-
* matches (for example `success`, `warning`, `error`, or a custom key).
|
|
69
|
-
*/
|
|
70
|
-
key: string;
|
|
71
|
-
/**
|
|
72
|
-
* Threshold expression that determines whether a metric value matches this
|
|
73
|
-
* rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
|
|
74
|
-
*
|
|
75
|
-
* @example `<= 10` - Metric value must be less than or equal to 10.
|
|
76
|
-
* @example `10-60` - Metric value must be between 10 and 60 (inclusive).
|
|
77
|
-
*/
|
|
78
|
-
expression: string;
|
|
79
|
-
/**
|
|
80
|
-
* Color configuration - supports multiple formats:
|
|
81
|
-
* - theme palette reference (`success.main` / `warning.main` / `error.main`)
|
|
82
|
-
* - HEX code (e.g. '#FFA500')
|
|
83
|
-
* - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
|
|
84
|
-
*
|
|
85
|
-
* Threshold rules 'success', 'warning' and 'error' have default colors.
|
|
86
|
-
*/
|
|
87
|
-
color?: string;
|
|
88
|
-
/**
|
|
89
|
-
* Icon configuration - supports multiple formats:
|
|
90
|
-
* - Backstage system icons: 'kind:component', 'kind:api', etc.
|
|
91
|
-
* - Material Design icons: 'settings', 'home', 'build', etc.
|
|
92
|
-
* - SVG strings: '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
|
|
93
|
-
* - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
|
|
94
|
-
* - Data URIs: 'data:image/svg+xml;base64,...'
|
|
95
|
-
*
|
|
96
|
-
* Threshold rules 'success', 'warning' and 'error' have default icons.
|
|
97
|
-
*/
|
|
98
|
-
icon?: string;
|
|
99
|
-
}>;
|
|
100
|
-
};
|
|
78
|
+
thresholds?: ThresholdConfig;
|
|
101
79
|
/**
|
|
102
80
|
* Schedule for collecting this metric. If not set, the default hourly schedule is used.
|
|
103
81
|
*
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
|
|
4
|
+
|
|
3
5
|
const AGGREGATION_KPIS_CONFIG_PATH = "scorecard.aggregationKPIs";
|
|
6
|
+
const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS = {
|
|
7
|
+
rules: [
|
|
8
|
+
{
|
|
9
|
+
key: "success",
|
|
10
|
+
expression: ">=80",
|
|
11
|
+
color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.SUCCESS
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "warning",
|
|
15
|
+
expression: "30-79",
|
|
16
|
+
color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.WARNING
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "error",
|
|
20
|
+
expression: "<30",
|
|
21
|
+
color: backstagePluginScorecardCommon.ScorecardThresholdRuleColors.ERROR
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
};
|
|
4
25
|
|
|
5
26
|
exports.AGGREGATION_KPIS_CONFIG_PATH = AGGREGATION_KPIS_CONFIG_PATH;
|
|
27
|
+
exports.DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS = DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS;
|
|
6
28
|
//# sourceMappingURL=aggregationKPIs.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.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\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aggregationKPIs.cjs.js","sources":["../../src/constants/aggregationKPIs.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 ThresholdConfig,\n ScorecardThresholdRuleColors,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\n\nexport const AGGREGATION_KPIS_CONFIG_PATH =\n 'scorecard.aggregationKPIs' as const;\n\n/**\n * Default applied by `AverageAggregationStrategy` when `options.thresholds` is omitted\n * from app-config. Higher headline percentage (0–100) = better. Evaluated in order; first match wins.\n */\nexport const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS: ThresholdConfig = {\n rules: [\n {\n key: 'success',\n expression: '>=80',\n color: ScorecardThresholdRuleColors.SUCCESS,\n },\n {\n key: 'warning',\n expression: '30-79',\n color: ScorecardThresholdRuleColors.WARNING,\n },\n {\n key: 'error',\n expression: '<30',\n color: ScorecardThresholdRuleColors.ERROR,\n },\n ],\n};\n"],"names":["ScorecardThresholdRuleColors"],"mappings":";;;;AAqBO,MAAM,4BACX,GAAA;AAMK,MAAM,qCAAyD,GAAA;AAAA,EACpE,KAAO,EAAA;AAAA,IACL;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,MAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,OAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA,KACtC;AAAA,IACA;AAAA,MACE,GAAK,EAAA,OAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ,OAAOA,2DAA6B,CAAA;AAAA;AACtC;AAEJ;;;;;"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -13,6 +13,7 @@ var migration = require('./database/migration.cjs.js');
|
|
|
13
13
|
var DatabaseMetricValues = require('./database/DatabaseMetricValues.cjs.js');
|
|
14
14
|
var index = require('./scheduler/index.cjs.js');
|
|
15
15
|
var validateAggregationConfig = require('./validation/validateAggregationConfig.cjs.js');
|
|
16
|
+
var AggregationService = require('./service/aggregations/AggregationService.cjs.js');
|
|
16
17
|
|
|
17
18
|
const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
18
19
|
pluginId: "scorecard",
|
|
@@ -66,8 +67,12 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
|
66
67
|
auth,
|
|
67
68
|
registry: metricProvidersRegistry,
|
|
68
69
|
database: dbMetricValues,
|
|
69
|
-
logger
|
|
70
|
-
|
|
70
|
+
logger
|
|
71
|
+
});
|
|
72
|
+
const aggregationsService = new AggregationService.AggregationsService({
|
|
73
|
+
config,
|
|
74
|
+
database: dbMetricValues,
|
|
75
|
+
logger
|
|
71
76
|
});
|
|
72
77
|
validateAggregationConfig.validateAggregationConfig({
|
|
73
78
|
rootConfig: config,
|
|
@@ -83,10 +88,14 @@ const scorecardPlugin = backendPluginApi.createBackendPlugin({
|
|
|
83
88
|
metricProvidersRegistry,
|
|
84
89
|
thresholdEvaluator: new ThresholdEvaluator.ThresholdEvaluator()
|
|
85
90
|
}).start();
|
|
91
|
+
const service = {
|
|
92
|
+
aggregationsService,
|
|
93
|
+
catalogMetricService
|
|
94
|
+
};
|
|
86
95
|
httpRouter.use(
|
|
87
96
|
await router.createRouter({
|
|
88
97
|
metricProvidersRegistry,
|
|
89
|
-
|
|
98
|
+
service,
|
|
90
99
|
catalog,
|
|
91
100
|
httpAuth,
|
|
92
101
|
permissions,
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\nimport { migrate } from './database/migration';\nimport { DatabaseMetricValues } from './database/DatabaseMetricValues';\nimport { Scheduler } from './scheduler';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n config: coreServices.rootConfig,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n scheduler: coreServices.scheduler,\n },\n async init({\n auth,\n catalog,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n // Run database migrations\n await migrate(database);\n\n const client = await database.getClient();\n const dbMetricValues = new DatabaseMetricValues(client);\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n config,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n }).start();\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport {\n MetricProvider,\n scorecardMetricsExtensionPoint,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-node';\nimport { MetricProvidersRegistry } from './providers/MetricProvidersRegistry';\nimport { CatalogMetricService } from './service/CatalogMetricService';\nimport { ThresholdEvaluator } from './threshold/ThresholdEvaluator';\nimport { scorecardPermissions } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport {\n scorecardMetricPermissionResourceRef,\n rules as scorecardRules,\n} from './permissions/rules';\nimport { migrate } from './database/migration';\nimport { DatabaseMetricValues } from './database/DatabaseMetricValues';\nimport { Scheduler } from './scheduler';\nimport { validateAggregationConfig } from './validation/validateAggregationConfig';\nimport { AggregationsService } from './service/aggregations/AggregationService';\n\n/**\n * scorecardPlugin backend plugin\n *\n * @public\n */\nexport const scorecardPlugin = createBackendPlugin({\n pluginId: 'scorecard',\n register(env) {\n const metricProvidersRegistry = new MetricProvidersRegistry();\n\n env.registerExtensionPoint(scorecardMetricsExtensionPoint, {\n addMetricProvider(...newMetricProviders: MetricProvider[]) {\n newMetricProviders.forEach(metricProvider => {\n metricProvidersRegistry.register(metricProvider);\n });\n },\n });\n\n env.registerInit({\n deps: {\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n config: coreServices.rootConfig,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n scheduler: coreServices.scheduler,\n },\n async init({\n auth,\n catalog,\n config,\n database,\n httpRouter,\n httpAuth,\n logger,\n permissions,\n permissionsRegistry,\n scheduler,\n }) {\n permissionsRegistry.addResourceType({\n resourceRef: scorecardMetricPermissionResourceRef,\n getResources: async (resourceRefs: string[]) => {\n return metricProvidersRegistry.listMetrics(resourceRefs);\n },\n permissions: scorecardPermissions,\n rules: Object.values(scorecardRules),\n });\n\n // Run database migrations\n await migrate(database);\n\n const client = await database.getClient();\n const dbMetricValues = new DatabaseMetricValues(client);\n\n const catalogMetricService = new CatalogMetricService({\n catalog,\n auth,\n registry: metricProvidersRegistry,\n database: dbMetricValues,\n logger: logger,\n });\n\n const aggregationsService = new AggregationsService({\n config,\n database: dbMetricValues,\n logger: logger,\n });\n\n validateAggregationConfig({\n rootConfig: config,\n registry: metricProvidersRegistry,\n });\n\n Scheduler.create({\n auth,\n catalog,\n config,\n logger,\n scheduler,\n database: dbMetricValues,\n metricProvidersRegistry,\n thresholdEvaluator: new ThresholdEvaluator(),\n }).start();\n\n const service = {\n aggregationsService: aggregationsService,\n catalogMetricService: catalogMetricService,\n };\n\n httpRouter.use(\n await createRouter({\n metricProvidersRegistry,\n service,\n catalog,\n httpAuth,\n permissions,\n logger,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","MetricProvidersRegistry","scorecardMetricsExtensionPoint","coreServices","catalogServiceRef","scorecardMetricPermissionResourceRef","scorecardPermissions","scorecardRules","migrate","DatabaseMetricValues","CatalogMetricService","AggregationsService","validateAggregationConfig","Scheduler","ThresholdEvaluator","createRouter"],"mappings":"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"}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
|
|
4
3
|
var normalizeOwnerRef = require('../utils/normalizeOwnerRef.cjs.js');
|
|
5
4
|
var errors = require('@backstage/errors');
|
|
6
5
|
var permissionUtils = require('../permissions/permissionUtils.cjs.js');
|
|
7
6
|
var mergeEntityAndProviderThresholds = require('../utils/mergeEntityAndProviderThresholds.cjs.js');
|
|
8
|
-
var mappers = require('./mappers.cjs.js');
|
|
9
|
-
var buildAggregationConfig = require('../utils/buildAggregationConfig.cjs.js');
|
|
10
|
-
var aggregationKPIs = require('../constants/aggregationKPIs.cjs.js');
|
|
11
7
|
|
|
12
8
|
class CatalogMetricService {
|
|
13
9
|
logger;
|
|
@@ -15,7 +11,6 @@ class CatalogMetricService {
|
|
|
15
11
|
auth;
|
|
16
12
|
registry;
|
|
17
13
|
database;
|
|
18
|
-
config;
|
|
19
14
|
static MAX_FETCHABLE_ROWS = 1e4;
|
|
20
15
|
static BATCH_SIZE = 100;
|
|
21
16
|
constructor(options) {
|
|
@@ -24,7 +19,6 @@ class CatalogMetricService {
|
|
|
24
19
|
this.registry = options.registry;
|
|
25
20
|
this.database = options.database;
|
|
26
21
|
this.logger = options.logger;
|
|
27
|
-
this.config = options.config;
|
|
28
22
|
}
|
|
29
23
|
/**
|
|
30
24
|
* Get latest metric results for a specific catalog entity.
|
|
@@ -94,37 +88,6 @@ class CatalogMetricService {
|
|
|
94
88
|
}
|
|
95
89
|
);
|
|
96
90
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Get an aggregated metric by status grouped for multiple entities and a single metric ID.
|
|
99
|
-
*
|
|
100
|
-
* @param entityRefs - Array of entity references in format "kind:namespace/name"
|
|
101
|
-
* @param metricId - Metric ID to aggregate.
|
|
102
|
-
* @returns Aggregated metric by status grouped results
|
|
103
|
-
*/
|
|
104
|
-
async getStatusGroupedAggregatedMetrics(entityRefs, metricId) {
|
|
105
|
-
const aggregatedMetric = await this.database.readAggregatedMetricByEntityRefs(
|
|
106
|
-
entityRefs,
|
|
107
|
-
metricId
|
|
108
|
-
);
|
|
109
|
-
return mappers.AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Get an aggregated metric by aggregation type.
|
|
113
|
-
*
|
|
114
|
-
* @param entityRefs - Array of entity references in format "kind:namespace/name"
|
|
115
|
-
* @param metricId - Metric ID to aggregate.
|
|
116
|
-
* @param aggregationType - Aggregation type to use.
|
|
117
|
-
* @returns Aggregated metric by aggregation type results
|
|
118
|
-
*/
|
|
119
|
-
async getAggregatedMetricByEntityRefs(entityRefs, metricId, aggregationType) {
|
|
120
|
-
if (entityRefs.length !== 0) {
|
|
121
|
-
if (aggregationType === backstagePluginScorecardCommon.aggregationTypes.statusGrouped) {
|
|
122
|
-
return this.getStatusGroupedAggregatedMetrics(entityRefs, metricId);
|
|
123
|
-
}
|
|
124
|
-
throw new Error(`Unsupported aggregation type: ${aggregationType}`);
|
|
125
|
-
}
|
|
126
|
-
return mappers.AggregatedMetricMapper.toAggregatedMetric();
|
|
127
|
-
}
|
|
128
91
|
/**
|
|
129
92
|
* Get detailed entity metrics for drill-down with filtering, sorting, and pagination.
|
|
130
93
|
*
|
|
@@ -278,37 +241,6 @@ class CatalogMetricService {
|
|
|
278
241
|
}
|
|
279
242
|
};
|
|
280
243
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Get the aggregation configs for a given aggregation IDs filtered by metric IDs.
|
|
283
|
-
* If no aggregation IDs are provided, all aggregation configs will be returned.
|
|
284
|
-
* If no metric IDs are provided, all metrics will be included.
|
|
285
|
-
*
|
|
286
|
-
* @param aggregationIds - Optional array of aggregation IDs to fetch the configs for.
|
|
287
|
-
* @param metricIds - Optional array of metric IDs, when provided, only aggregations whose config metricId is in this array will be returned.
|
|
288
|
-
* @returns Aggregation configs
|
|
289
|
-
*/
|
|
290
|
-
getAggregationConfigs(aggregationIds = [], metricIds = []) {
|
|
291
|
-
const aggregationKPIsConfig = this.config.getOptionalConfig(
|
|
292
|
-
aggregationKPIs.AGGREGATION_KPIS_CONFIG_PATH
|
|
293
|
-
);
|
|
294
|
-
if (!aggregationKPIsConfig) {
|
|
295
|
-
return [];
|
|
296
|
-
}
|
|
297
|
-
const aggregations = [];
|
|
298
|
-
const aggregationConfigIds = aggregationIds.length > 0 ? aggregationIds : aggregationKPIsConfig.keys();
|
|
299
|
-
for (const aggregationId of aggregationConfigIds) {
|
|
300
|
-
const config = aggregationKPIsConfig.getOptionalConfig(aggregationId);
|
|
301
|
-
if (config) {
|
|
302
|
-
const aggregationConfig = buildAggregationConfig.buildAggregationConfig(aggregationId, {
|
|
303
|
-
config
|
|
304
|
-
});
|
|
305
|
-
if (metricIds.length === 0 || metricIds.includes(aggregationConfig.metricId)) {
|
|
306
|
-
aggregations.push(aggregationConfig);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return aggregations;
|
|
311
|
-
}
|
|
312
244
|
}
|
|
313
245
|
|
|
314
246
|
exports.CatalogMetricService = CatalogMetricService;
|
|
@@ -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 AggregatedMetric,\n EntityMetricDetailResponse,\n EntityMetricDetail,\n aggregationTypes,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport { 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 { AggregatedMetricMapper } from './mappers';\nimport { DbMetricValue } from '../database/types';\nimport type { Config } from '@backstage/config';\nimport {\n buildAggregationConfig,\n type AggregationConfig,\n} from '../utils/buildAggregationConfig';\nimport { AGGREGATION_KPIS_CONFIG_PATH } from '../constants';\n\ntype CatalogMetricServiceOptions = {\n catalog: CatalogService;\n auth: AuthService;\n registry: MetricProvidersRegistry;\n database: DatabaseMetricValues;\n logger: LoggerService;\n config: Config;\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 private readonly config: Config;\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 this.config = options.config;\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 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 };\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 /**\n * Get the aggregation configs for a given aggregation IDs filtered by metric IDs.\n * If no aggregation IDs are provided, all aggregation configs will be returned.\n * If no metric IDs are provided, all metrics will be included.\n *\n * @param aggregationIds - Optional array of aggregation IDs to fetch the configs for.\n * @param metricIds - Optional array of metric IDs, when provided, only aggregations whose config metricId is in this array will be returned.\n * @returns Aggregation configs\n */\n getAggregationConfigs(\n aggregationIds: string[] = [],\n metricIds: string[] = [],\n ): AggregationConfig[] {\n const aggregationKPIsConfig = this.config.getOptionalConfig(\n AGGREGATION_KPIS_CONFIG_PATH,\n );\n\n if (!aggregationKPIsConfig) {\n return [];\n }\n\n const aggregations: AggregationConfig[] = [];\n\n const aggregationConfigIds =\n aggregationIds.length > 0 ? aggregationIds : aggregationKPIsConfig.keys();\n\n for (const aggregationId of aggregationConfigIds) {\n const config = aggregationKPIsConfig.getOptionalConfig(aggregationId);\n\n if (config) {\n const aggregationConfig = buildAggregationConfig(aggregationId, {\n config,\n });\n\n if (\n metricIds.length === 0 ||\n metricIds.includes(aggregationConfig.metricId)\n ) {\n aggregations.push(aggregationConfig);\n }\n }\n }\n\n return aggregations;\n }\n}\n"],"names":["NotFoundError","filterAuthorizedMetrics","mergeEntityAndProviderThresholds","stringifyError","AggregatedMetricMapper","aggregationTypes","normalizeOwnerRef","AGGREGATION_KPIS_CONFIG_PATH","buildAggregationConfig"],"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} 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":";;;;;;;AAkDO,MAAM,oBAAqB,CAAA;AAAA,EACf,MAAA;AAAA,EAEA,OAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EAEjB,OAAwB,kBAAqB,GAAA,GAAA;AAAA,EAC7C,OAAwB,UAAa,GAAA,GAAA;AAAA,EAErC,YAAY,OAAsC,EAAA;AAChD,IAAA,IAAA,CAAK,UAAU,OAAQ,CAAA,OAAA;AACvB,IAAA,IAAA,CAAK,OAAO,OAAQ,CAAA,IAAA;AACpB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AACxB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AACxB,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AAAA;AACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,sBAAA,CACJ,SACA,EAAA,SAAA,EACA,MAGyB,EAAA;AACzB,IAAA,MAAM,MAAS,GAAA,MAAM,IAAK,CAAA,OAAA,CAAQ,eAAe,SAAW,EAAA;AAAA,MAC1D,WAAa,EAAA,MAAM,IAAK,CAAA,IAAA,CAAK,wBAAyB;AAAA,KACvD,CAAA;AACD,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAA,MAAM,IAAIA,oBAAA,CAAc,CAAqB,kBAAA,EAAA,SAAS,CAAE,CAAA,CAAA;AAAA;AAG1D,IAAA,MAAM,cAAiB,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,SAAS,CAAA;AAE1D,IAAA,MAAM,wBAA2B,GAAAC,uCAAA;AAAA,MAC/B,cAAA;AAAA,MACA;AAAA,KACF;AACA,IAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,QAAS,CAAA,4BAAA;AAAA,MACrC,SAAA;AAAA,MACA,wBAAyB,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,EAAE;AAAA,KACxC;AAEA,IAAA,OAAO,UAAW,CAAA,GAAA;AAAA,MAChB,CAAC,EAAE,SAAA,EAAW,OAAO,aAAe,EAAA,SAAA,EAAW,QAAa,KAAA;AAC1D,QAAI,IAAA,UAAA;AACJ,QAAI,IAAA,cAAA;AAEJ,QAAA,MAAM,QAAW,GAAA,IAAA,CAAK,QAAS,CAAA,WAAA,CAAY,SAAS,CAAA;AACpD,QAAA,MAAM,MAAS,GAAA,IAAA,CAAK,QAAS,CAAA,SAAA,CAAU,SAAS,CAAA;AAEhD,QAAI,IAAA;AACF,UAAa,UAAA,GAAAC,iEAAA,CAAiC,QAAQ,QAAQ,CAAA;AAE9D,UAAA,IAAI,UAAU,IAAM,EAAA;AAClB,YACE,cAAA,GAAA,wDAAA;AAAA,qBACO,aAAe,EAAA;AACxB,YAAiB,cAAA,GAAA,aAAA;AAAA;AACnB,iBACO,KAAO,EAAA;AACd,UAAA,cAAA,GAAiBC,sBAAe,KAAK,CAAA;AAAA;AAGvC,QAAM,MAAA,iBAAA,GAAoB,aAAkB,KAAA,IAAA,IAAQ,KAAU,KAAA,IAAA;AAE9D,QAAO,OAAA;AAAA,UACL,IAAI,MAAO,CAAA,EAAA;AAAA,UACX,MAAA,EAAQ,oBAAoB,OAAU,GAAA,SAAA;AAAA,UACtC,QAAU,EAAA;AAAA,YACR,OAAO,MAAO,CAAA,KAAA;AAAA,YACd,aAAa,MAAO,CAAA,WAAA;AAAA,YACpB,MAAM,MAAO,CAAA,IAAA;AAAA,YACb,SAAS,MAAO,CAAA;AAAA,WAClB;AAAA,UACA,GAAI,iBAAqB,IAAA;AAAA,YACvB,OACE,aACA,IAAAA,qBAAA,CAAe,IAAI,KAAA,CAAM,6BAA6B,CAAC;AAAA,WAC3D;AAAA,UACA,MAAQ,EAAA;AAAA,YACN,KAAA;AAAA,YACA,SAAW,EAAA,IAAI,IAAK,CAAA,SAAS,EAAE,WAAY,EAAA;AAAA,YAC3C,eAAiB,EAAA;AAAA,cACf,UAAY,EAAA,UAAA;AAAA,cACZ,MAAA,EAAQ,iBAAiB,OAAU,GAAA,SAAA;AAAA,cACnC,UAAY,EAAA,MAAA;AAAA,cACZ,GAAI,cAAA,IAAkB,EAAE,KAAA,EAAO,cAAe;AAAA;AAChD;AACF,SACF;AAAA;AACF,KACF;AAAA;AACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,MAAM,sBAAA,CACJ,QACA,EAAA,WAAA,EACA,OAkBqC,EAAA;AAErC,IAAA,MAAM,MAAS,GAAA,IAAA,CAAK,QAAS,CAAA,SAAA,CAAU,QAAQ,CAAA;AAG/C,IAAA,IAAA,CACG,QAAQ,IAAO,GAAA,CAAA,IAAK,OAAQ,CAAA,KAAA,IAC7B,qBAAqB,kBACrB,EAAA;AACA,MAAO,OAAA;AAAA,QACL,UAAU,MAAO,CAAA,EAAA;AAAA,QACjB,cAAgB,EAAA;AAAA,UACd,OAAO,MAAO,CAAA,KAAA;AAAA,UACd,aAAa,MAAO,CAAA,WAAA;AAAA,UACpB,MAAM,MAAO,CAAA;AAAA,SACf;AAAA,QACA,UAAU,EAAC;AAAA,QACX,UAAY,EAAA;AAAA,UACV,MAAM,OAAQ,CAAA,IAAA;AAAA,UACd,UAAU,OAAQ,CAAA,KAAA;AAAA,UAClB,KAAO,EAAA,CAAA;AAAA,UACP,UAAY,EAAA,CAAA;AAAA,UACZ,QAAU,EAAA;AAAA;AACZ,OACF;AAAA;AAOF,IAAA,MAAM,IAAO,GAAA,MAAM,IAAK,CAAA,QAAA,CAAS,6BAA6B,QAAU,EAAA;AAAA,MACtE,QAAQ,OAAQ,CAAA,MAAA;AAAA,MAChB,YAAY,OAAQ,CAAA,UAAA;AAAA,MACpB,YAAY,OAAQ,CAAA,IAAA;AAAA,MACpB,iBAAiB,OAAQ,CAAA,SAAA;AAAA,MACzB,aAAa,OAAQ,CAAA,KAAA;AAAA,MACrB,QAAQ,OAAQ,CAAA,MAAA;AAAA,MAChB,WAAW,OAAQ,CAAA,SAAA;AAAA,MACnB,UAAY,EAAA;AAAA,QACV,OAAO,oBAAqB,CAAA,kBAAA;AAAA,QAC5B,MAAQ,EAAA;AAAA;AACV,KACD,CAAA;AAMD,IAAM,MAAA,SAAA,uBAAgB,GAAoB,EAAA;AAC1C,IAAA,MAAM,iBAAkC,EAAC;AACzC,IAAI,IAAA;AACF,MAAA,KAAA,IAAS,IAAI,CAAG,EAAA,CAAA,GAAI,KAAK,MAAQ,EAAA,CAAA,IAAK,qBAAqB,UAAY,EAAA;AACrE,QAAA,MAAM,QAAQ,IAAK,CAAA,KAAA,CAAM,CAAG,EAAA,CAAA,GAAI,qBAAqB,UAAU,CAAA;AAC/D,QAAM,MAAA,QAAA,GAAW,MAAM,IAAA,CAAK,OAAQ,CAAA,iBAAA;AAAA,UAClC;AAAA,YACE,UAAY,EAAA,KAAA,CAAM,GAAI,CAAA,CAAA,GAAA,KAAO,IAAI,kBAAkB,CAAA;AAAA,YACnD,MAAQ,EAAA;AAAA,cACN,MAAA;AAAA,cACA,eAAA;AAAA,cACA,oBAAA;AAAA,cACA;AAAA;AACF,WACF;AAAA,UACA,EAAE,WAAY;AAAA,SAChB;AAGA,QAAA,KAAA,IAAS,CAAI,GAAA,CAAA,EAAG,CAAI,GAAA,KAAA,CAAM,QAAQ,CAAK,EAAA,EAAA;AACrC,UAAM,MAAA,MAAA,GAAS,QAAS,CAAA,KAAA,CAAM,CAAC,CAAA;AAC/B,UAAA,IAAI,CAAC,MAAQ,EAAA;AACb,UAAA,SAAA,CAAU,GAAI,CAAA,KAAA,CAAM,CAAC,CAAA,CAAE,oBAAoB,MAAM,CAAA;AACjD,UAAe,cAAA,CAAA,IAAA,CAAK,KAAM,CAAA,CAAC,CAAC,CAAA;AAAA;AAC9B;AACF,aACO,KAAO,EAAA;AAGd,MAAA,IAAA,CAAK,MAAO,CAAA,KAAA,CAAM,uCAAyC,EAAA,EAAE,OAAO,CAAA;AACpE,MAAO,OAAA;AAAA,QACL,UAAU,MAAO,CAAA,EAAA;AAAA,QACjB,cAAgB,EAAA;AAAA,UACd,OAAO,MAAO,CAAA,KAAA;AAAA,UACd,aAAa,MAAO,CAAA,WAAA;AAAA,UACpB,MAAM,MAAO,CAAA;AAAA,SACf;AAAA,QACA,UAAU,EAAC;AAAA,QACX,UAAY,EAAA;AAAA,UACV,MAAM,OAAQ,CAAA,IAAA;AAAA,UACd,UAAU,OAAQ,CAAA,KAAA;AAAA,UAClB,KAAO,EAAA,CAAA;AAAA,UACP,UAAY,EAAA,CAAA;AAAA,UACZ,QAAU,EAAA;AAAA;AACZ,OACF;AAAA;AAIF,IAAM,MAAA,QAAA,GAAW,IAAK,CAAA,MAAA,KAAW,oBAAqB,CAAA,kBAAA;AAGtD,IAAA,MAAM,gBAAgB,cAAe,CAAA,MAAA;AACrC,IAAA,MAAM,WAAW,cAAe,CAAA,KAAA;AAAA,MAC7B,CAAA,OAAA,CAAQ,IAAO,GAAA,CAAA,IAAK,OAAQ,CAAA,KAAA;AAAA,MAC7B,OAAA,CAAQ,OAAO,OAAQ,CAAA;AAAA,KACzB;AAIA,IAAI,IAAA,QAAA,CAAS,WAAW,CAAG,EAAA;AACzB,MAAO,OAAA;AAAA,QACL,UAAU,MAAO,CAAA,EAAA;AAAA,QACjB,cAAgB,EAAA;AAAA,UACd,OAAO,MAAO,CAAA,KAAA;AAAA,UACd,aAAa,MAAO,CAAA,WAAA;AAAA,UACpB,MAAM,MAAO,CAAA;AAAA,SACf;AAAA,QACA,UAAU,EAAC;AAAA,QACX,UAAY,EAAA;AAAA,UACV,MAAM,OAAQ,CAAA,IAAA;AAAA,UACd,UAAU,OAAQ,CAAA,KAAA;AAAA,UAClB,KAAO,EAAA,aAAA;AAAA,UACP,UAAY,EAAA,IAAA,CAAK,IAAK,CAAA,aAAA,GAAgB,QAAQ,KAAK,CAAA;AAAA,UACnD;AAAA;AACF,OACF;AAAA;AAIF,IAAA,MAAM,mBAAyC,EAAC;AAChD,IAAA,KAAA,MAAW,OAAO,QAAU,EAAA;AAC1B,MAAA,MAAM,MAAS,GAAA,SAAA,CAAU,GAAI,CAAA,GAAA,CAAI,kBAAkB,CAAA;AACnD,MAAA,IAAI,CAAC,MAAQ,EAAA;AACb,MAAA,gBAAA,CAAiB,IAAK,CAAA;AAAA,QACpB,WAAW,GAAI,CAAA,kBAAA;AAAA,QACf,eAAA,EAAiB,OAAO,QAAS,CAAA,SAAA;AAAA,QACjC,UAAA,EAAY,OAAO,QAAS,CAAA,IAAA;AAAA,QAC5B,YAAY,MAAO,CAAA,IAAA;AAAA,QACnB,KAAO,EAAAC,mCAAA,CAAkB,MAAO,CAAA,IAAA,EAAM,KAAK,CAAK,IAAA,EAAA;AAAA,QAChD,aAAa,GAAI,CAAA,KAAA;AAAA,QACjB,WAAW,IAAI,IAAA,CAAK,GAAI,CAAA,SAAS,EAAE,WAAY,EAAA;AAAA,QAC/C,QAAQ,GAAI,CAAA;AAAA,OACb,CAAA;AAAA;AAIH,IAAO,OAAA;AAAA,MACL,UAAU,MAAO,CAAA,EAAA;AAAA,MACjB,cAAgB,EAAA;AAAA,QACd,OAAO,MAAO,CAAA,KAAA;AAAA,QACd,aAAa,MAAO,CAAA,WAAA;AAAA,QACpB,MAAM,MAAO,CAAA;AAAA,OACf;AAAA,MACA,QAAU,EAAA,gBAAA;AAAA,MACV,UAAY,EAAA;AAAA,QACV,MAAM,OAAQ,CAAA,IAAA;AAAA,QACd,UAAU,OAAQ,CAAA,KAAA;AAAA,QAClB,KAAO,EAAA,aAAA;AAAA,QACP,UAAY,EAAA,IAAA,CAAK,IAAK,CAAA,aAAA,GAAgB,QAAQ,KAAK,CAAA;AAAA,QACnD;AAAA;AACF,KACF;AAAA;AAEJ;;;;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var mappers = require('../mappers.cjs.js');
|
|
4
|
+
|
|
5
|
+
class AggregatedMetricLoader {
|
|
6
|
+
constructor(database) {
|
|
7
|
+
this.database = database;
|
|
8
|
+
}
|
|
9
|
+
database;
|
|
10
|
+
async loadStatusGroupedMetricByEntityRefs(entityRefs, metricId) {
|
|
11
|
+
if (entityRefs.length === 0) {
|
|
12
|
+
return mappers.AggregatedMetricMapper.toAggregatedMetric();
|
|
13
|
+
}
|
|
14
|
+
const aggregatedMetric = await this.database.readAggregatedMetricByEntityRefs(
|
|
15
|
+
entityRefs,
|
|
16
|
+
metricId
|
|
17
|
+
);
|
|
18
|
+
return mappers.AggregatedMetricMapper.toAggregatedMetric(aggregatedMetric);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
exports.AggregatedMetricLoader = AggregatedMetricLoader;
|
|
23
|
+
//# sourceMappingURL=AggregatedMetricLoader.cjs.js.map
|
|
@@ -0,0 +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,EAAjC,QAAA;AAAA,EAE7B,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;;;;"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backstagePluginScorecardCommon = require('@red-hat-developer-hub/backstage-plugin-scorecard-common');
|
|
4
|
+
var aggregationKPIs = require('../../constants/aggregationKPIs.cjs.js');
|
|
5
|
+
var buildAggregationConfig = require('../../utils/buildAggregationConfig.cjs.js');
|
|
6
|
+
var AggregatedMetricLoader = require('./AggregatedMetricLoader.cjs.js');
|
|
7
|
+
var registerStrategies = require('./strategies/registerStrategies.cjs.js');
|
|
8
|
+
|
|
9
|
+
class AggregationsService {
|
|
10
|
+
config;
|
|
11
|
+
database;
|
|
12
|
+
strategyRegistry;
|
|
13
|
+
logger;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.config = options.config;
|
|
16
|
+
this.logger = options.logger;
|
|
17
|
+
this.database = options.database;
|
|
18
|
+
this.strategyRegistry = registerStrategies.createAggregationStrategyRegistry(
|
|
19
|
+
new AggregatedMetricLoader.AggregatedMetricLoader(this.database),
|
|
20
|
+
this.logger
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
getAggregationConfig(aggregationId) {
|
|
24
|
+
const config = this.config.getOptionalConfig(
|
|
25
|
+
`${aggregationKPIs.AGGREGATION_KPIS_CONFIG_PATH}.${aggregationId}`
|
|
26
|
+
);
|
|
27
|
+
if (!config) {
|
|
28
|
+
this.logger.warn(
|
|
29
|
+
`No "${aggregationKPIs.AGGREGATION_KPIS_CONFIG_PATH}.${aggregationId}" block in app-config; using default type "${backstagePluginScorecardCommon.aggregationTypes.statusGrouped}" with metricId="${aggregationId}" (same as aggregation id). Add a KPI entry if you meant a custom title, description, or type.`
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
id: aggregationId,
|
|
33
|
+
type: backstagePluginScorecardCommon.aggregationTypes.statusGrouped,
|
|
34
|
+
metricId: aggregationId
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return buildAggregationConfig.buildAggregationConfig(aggregationId, {
|
|
38
|
+
config
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async getAggregatedMetricByEntityRefs(options) {
|
|
42
|
+
const { aggregationConfig } = options;
|
|
43
|
+
const strategy = this.strategyRegistry.get(aggregationConfig.type);
|
|
44
|
+
if (!strategy) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Unsupported aggregation type: ${aggregationConfig.type}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return strategy.aggregate(options);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exports.AggregationsService = AggregationsService;
|
|
54
|
+
//# sourceMappingURL=AggregationService.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AggregationService.cjs.js","sources":["../../../src/service/aggregations/AggregationService.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 AggregatedMetricResult,\n type AggregationConfig,\n type AggregationType,\n aggregationTypes,\n} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';\nimport type { Config } from '@backstage/config';\nimport { AGGREGATION_KPIS_CONFIG_PATH } from '../../constants';\nimport { buildAggregationConfig } from '../../utils/buildAggregationConfig';\nimport type { AggregationOptions } from './types';\nimport type { AggregationStrategy } from './strategies/types';\nimport { DatabaseMetricValues } from '../../database/DatabaseMetricValues';\nimport { AggregatedMetricLoader } from './AggregatedMetricLoader';\nimport { createAggregationStrategyRegistry } from './strategies/registerStrategies';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport type AggregationsServiceOptions = {\n config: Config;\n logger: LoggerService;\n database: DatabaseMetricValues;\n};\n\nexport class AggregationsService {\n private readonly config: Config;\n private readonly database: DatabaseMetricValues;\n private readonly strategyRegistry: Map<AggregationType, AggregationStrategy>;\n private readonly logger: LoggerService;\n\n constructor(options: AggregationsServiceOptions) {\n this.config = options.config;\n this.logger = options.logger;\n this.database = options.database;\n this.strategyRegistry = createAggregationStrategyRegistry(\n new AggregatedMetricLoader(this.database),\n this.logger,\n );\n }\n\n getAggregationConfig(aggregationId: string): AggregationConfig {\n const config = this.config.getOptionalConfig(\n `${AGGREGATION_KPIS_CONFIG_PATH}.${aggregationId}`,\n );\n\n if (!config) {\n this.logger.warn(\n `No \"${AGGREGATION_KPIS_CONFIG_PATH}.${aggregationId}\" block in app-config; ` +\n `using default type \"${aggregationTypes.statusGrouped}\" with metricId=\"${aggregationId}\" ` +\n '(same as aggregation id). Add a KPI entry if you meant a custom title, description, or type.',\n );\n return {\n id: aggregationId,\n type: aggregationTypes.statusGrouped,\n metricId: aggregationId,\n } as AggregationConfig;\n }\n\n return buildAggregationConfig(aggregationId, {\n config,\n });\n }\n\n async getAggregatedMetricByEntityRefs(\n options: AggregationOptions,\n ): Promise<AggregatedMetricResult> {\n const { aggregationConfig } = options;\n\n const strategy = this.strategyRegistry.get(aggregationConfig.type);\n\n if (!strategy) {\n throw new Error(\n `Unsupported aggregation type: ${aggregationConfig.type}`,\n );\n }\n\n return strategy.aggregate(options);\n }\n}\n"],"names":["createAggregationStrategyRegistry","AggregatedMetricLoader","AGGREGATION_KPIS_CONFIG_PATH","aggregationTypes","buildAggregationConfig"],"mappings":";;;;;;;;AAsCO,MAAM,mBAAoB,CAAA;AAAA,EACd,MAAA;AAAA,EACA,QAAA;AAAA,EACA,gBAAA;AAAA,EACA,MAAA;AAAA,EAEjB,YAAY,OAAqC,EAAA;AAC/C,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,SAAS,OAAQ,CAAA,MAAA;AACtB,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA;AACxB,IAAA,IAAA,CAAK,gBAAmB,GAAAA,oDAAA;AAAA,MACtB,IAAIC,6CAAuB,CAAA,IAAA,CAAK,QAAQ,CAAA;AAAA,MACxC,IAAK,CAAA;AAAA,KACP;AAAA;AACF,EAEA,qBAAqB,aAA0C,EAAA;AAC7D,IAAM,MAAA,MAAA,GAAS,KAAK,MAAO,CAAA,iBAAA;AAAA,MACzB,CAAA,EAAGC,4CAA4B,CAAA,CAAA,EAAI,aAAa,CAAA;AAAA,KAClD;AAEA,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAA,IAAA,CAAK,MAAO,CAAA,IAAA;AAAA,QACV,CAAA,IAAA,EAAOA,4CAA4B,CAAI,CAAA,EAAA,aAAa,8CAC3BC,+CAAiB,CAAA,aAAa,oBAAoB,aAAa,CAAA,8FAAA;AAAA,OAE1F;AACA,MAAO,OAAA;AAAA,QACL,EAAI,EAAA,aAAA;AAAA,QACJ,MAAMA,+CAAiB,CAAA,aAAA;AAAA,QACvB,QAAU,EAAA;AAAA,OACZ;AAAA;AAGF,IAAA,OAAOC,8CAAuB,aAAe,EAAA;AAAA,MAC3C;AAAA,KACD,CAAA;AAAA;AACH,EAEA,MAAM,gCACJ,OACiC,EAAA;AACjC,IAAM,MAAA,EAAE,mBAAsB,GAAA,OAAA;AAE9B,IAAA,MAAM,QAAW,GAAA,IAAA,CAAK,gBAAiB,CAAA,GAAA,CAAI,kBAAkB,IAAI,CAAA;AAEjE,IAAA,IAAI,CAAC,QAAU,EAAA;AACb,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,8BAAA,EAAiC,kBAAkB,IAAI,CAAA;AAAA,OACzD;AAAA;AAGF,IAAO,OAAA,QAAA,CAAS,UAAU,OAAO,CAAA;AAAA;AAErC;;;;"}
|