@c2l2c/backstage-plugin-dora-metrics 0.1.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/README.md +91 -0
- package/config.d.ts +45 -0
- package/dist/alpha.d.ts +60 -0
- package/dist/alpha.esm.js +2 -0
- package/dist/alpha.esm.js.map +1 -0
- package/dist/api/DoraMetricsClient.esm.js +200 -0
- package/dist/api/DoraMetricsClient.esm.js.map +1 -0
- package/dist/api/types.esm.js +8 -0
- package/dist/api/types.esm.js.map +1 -0
- package/dist/components/DoraMetricsContent.esm.js +1311 -0
- package/dist/components/DoraMetricsContent.esm.js.map +1 -0
- package/dist/components/ui/badge.esm.js +33 -0
- package/dist/components/ui/badge.esm.js.map +1 -0
- package/dist/components/ui/card.esm.js +77 -0
- package/dist/components/ui/card.esm.js.map +1 -0
- package/dist/components/ui/select.esm.js +110 -0
- package/dist/components/ui/select.esm.js.map +1 -0
- package/dist/extensions/api.esm.js +21 -0
- package/dist/extensions/api.esm.js.map +1 -0
- package/dist/extensions/entityContent.esm.js +14 -0
- package/dist/extensions/entityContent.esm.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +11 -0
- package/dist/plugin.esm.js.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @c2l2c/backstage-plugin-dora-metrics
|
|
2
|
+
|
|
3
|
+
A Backstage frontend plugin that displays **DORA metrics** for catalog entities using the GitHub API.
|
|
4
|
+
|
|
5
|
+
Tracks the four key DORA metrics:
|
|
6
|
+
- **Deployment Frequency** — how often you deploy to a given branch
|
|
7
|
+
- **Lead Time for Changes** — time from first commit to merge
|
|
8
|
+
- **Change Failure Rate** — percentage of deployments causing failures (production only)
|
|
9
|
+
- **MTTR** — mean time to recovery from failures (production only)
|
|
10
|
+
|
|
11
|
+
Ratings are classified as **Elite / High / Medium / Low** per DORA research benchmarks.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Backstage v1.35+ (new frontend system)
|
|
18
|
+
- GitHub OAuth configured in your Backstage instance (the plugin uses `githubAuthApi` to call the GitHub REST API)
|
|
19
|
+
- Catalog entities annotated with `github.com/project-slug: org/repo`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# In your Backstage app workspace
|
|
27
|
+
yarn workspace app add @c2l2c/backstage-plugin-dora-metrics
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Register the plugin
|
|
31
|
+
|
|
32
|
+
In `packages/app/src/App.tsx`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import doraMetricsPlugin from '@c2l2c/backstage-plugin-dora-metrics/alpha';
|
|
36
|
+
|
|
37
|
+
// Add to your app's plugins array
|
|
38
|
+
const app = createApp({
|
|
39
|
+
plugins: [doraMetricsPlugin],
|
|
40
|
+
// ...
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
Add to your `app-config.yaml`:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
app:
|
|
52
|
+
doraMetrics:
|
|
53
|
+
# Lookback window in days (default: 30)
|
|
54
|
+
collection:
|
|
55
|
+
initialDays: 30
|
|
56
|
+
|
|
57
|
+
# Define the environments / branches to track
|
|
58
|
+
environments:
|
|
59
|
+
- name: Production
|
|
60
|
+
branch: main
|
|
61
|
+
isProduction: true
|
|
62
|
+
label: hotfix # PR label for production failures (used for CFR/MTTR)
|
|
63
|
+
- name: Staging
|
|
64
|
+
branch: staging
|
|
65
|
+
isProduction: false
|
|
66
|
+
|
|
67
|
+
# Optional: override DORA benchmark targets
|
|
68
|
+
targets:
|
|
69
|
+
deploymentFrequency: 1 # deploys per day (Elite ≥ 1)
|
|
70
|
+
leadTime: 24 # hours (Elite ≤ 24h)
|
|
71
|
+
changeFailureRate: 5 # % (Elite ≤ 5%)
|
|
72
|
+
mttr: 1 # hours (Elite ≤ 1h)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
The plugin reads the `github.com/project-slug` annotation from the catalog entity and calls the GitHub REST API (authenticated via the user's GitHub OAuth session) to fetch merged pull requests within the configured time window.
|
|
80
|
+
|
|
81
|
+
It calculates:
|
|
82
|
+
- **Deployment Frequency**: merged PRs per day to the target branch
|
|
83
|
+
- **Lead Time**: time from PR creation to merge
|
|
84
|
+
- **CFR**: percentage of PRs labelled as hotfixes (production environments only)
|
|
85
|
+
- **MTTR**: average time hotfix PRs were open (production environments only)
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
Apache-2.0
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
app?: {
|
|
3
|
+
/**
|
|
4
|
+
* DORA metrics configuration.
|
|
5
|
+
* @visibility frontend
|
|
6
|
+
*/
|
|
7
|
+
doraMetrics?: {
|
|
8
|
+
/**
|
|
9
|
+
* @visibility frontend
|
|
10
|
+
*/
|
|
11
|
+
environments?: Array<{
|
|
12
|
+
/** @visibility frontend */
|
|
13
|
+
name: string;
|
|
14
|
+
/** @visibility frontend */
|
|
15
|
+
branch: string;
|
|
16
|
+
/** @visibility frontend */
|
|
17
|
+
isProduction?: boolean;
|
|
18
|
+
/** Label on PRs that represent production failures (hotfixes). @visibility frontend */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Label on PRs that represent planned feature deployments. @visibility frontend */
|
|
21
|
+
featureLabel?: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* @visibility frontend
|
|
25
|
+
*/
|
|
26
|
+
collection?: {
|
|
27
|
+
/** @visibility frontend */
|
|
28
|
+
initialDays?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @visibility frontend
|
|
32
|
+
*/
|
|
33
|
+
targets?: {
|
|
34
|
+
/** @visibility frontend */
|
|
35
|
+
deploymentFrequency?: number;
|
|
36
|
+
/** @visibility frontend */
|
|
37
|
+
leadTime?: number;
|
|
38
|
+
/** @visibility frontend */
|
|
39
|
+
changeFailureRate?: number;
|
|
40
|
+
/** @visibility frontend */
|
|
41
|
+
mttr?: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/alpha.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as _backstage_catalog_model from '@backstage/catalog-model';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import * as _backstage_filter_predicates from '@backstage/filter-predicates';
|
|
4
|
+
import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
|
|
5
|
+
|
|
6
|
+
declare const doraMetricsPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
|
|
7
|
+
"api:dora-metrics/dora-metrics": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
8
|
+
kind: "api";
|
|
9
|
+
name: "dora-metrics";
|
|
10
|
+
config: {};
|
|
11
|
+
configInput: {};
|
|
12
|
+
output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
|
|
13
|
+
inputs: {};
|
|
14
|
+
params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
|
|
15
|
+
}>;
|
|
16
|
+
"entity-content:dora-metrics": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
17
|
+
kind: "entity-content";
|
|
18
|
+
name: undefined;
|
|
19
|
+
config: {
|
|
20
|
+
path: string | undefined;
|
|
21
|
+
title: string | undefined;
|
|
22
|
+
filter: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
23
|
+
group: string | false | undefined;
|
|
24
|
+
icon: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
configInput: {
|
|
27
|
+
filter?: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
28
|
+
title?: string | undefined;
|
|
29
|
+
path?: string | undefined;
|
|
30
|
+
group?: string | false | undefined;
|
|
31
|
+
icon?: string | undefined;
|
|
32
|
+
};
|
|
33
|
+
output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
|
|
34
|
+
optional: true;
|
|
35
|
+
}> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<(entity: _backstage_catalog_model.Entity) => boolean, "catalog.entity-filter-function", {
|
|
36
|
+
optional: true;
|
|
37
|
+
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-filter-expression", {
|
|
38
|
+
optional: true;
|
|
39
|
+
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-title", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-group", {
|
|
40
|
+
optional: true;
|
|
41
|
+
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string | react.ReactElement<any, string | react.JSXElementConstructor<any>>, "catalog.entity-content-icon", {
|
|
42
|
+
optional: true;
|
|
43
|
+
}>;
|
|
44
|
+
inputs: {};
|
|
45
|
+
params: {
|
|
46
|
+
defaultPath?: [Error: `Use the 'path' param instead`];
|
|
47
|
+
path: string;
|
|
48
|
+
defaultTitle?: [Error: `Use the 'title' param instead`];
|
|
49
|
+
title: string;
|
|
50
|
+
defaultGroup?: [Error: `Use the 'group' param instead`];
|
|
51
|
+
group?: ("overview" | "documentation" | "development" | "deployment" | "operation" | "observability") | (string & {});
|
|
52
|
+
icon?: string | react.ReactElement;
|
|
53
|
+
loader: () => Promise<JSX.Element>;
|
|
54
|
+
routeRef?: _backstage_frontend_plugin_api.RouteRef;
|
|
55
|
+
filter?: string | _backstage_filter_predicates.FilterPredicate | ((entity: _backstage_catalog_model.Entity) => boolean);
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
export { doraMetricsPlugin as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alpha.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
function computeRating(value, target, lowerIsBetter) {
|
|
2
|
+
if (lowerIsBetter) {
|
|
3
|
+
if (target === 0) {
|
|
4
|
+
if (value === 0) return "elite";
|
|
5
|
+
if (value <= 2) return "high";
|
|
6
|
+
if (value <= 5) return "medium";
|
|
7
|
+
return "low";
|
|
8
|
+
}
|
|
9
|
+
if (value <= target) return "elite";
|
|
10
|
+
if (value <= target * 2) return "high";
|
|
11
|
+
if (value <= target * 4) return "medium";
|
|
12
|
+
return "low";
|
|
13
|
+
}
|
|
14
|
+
if (value >= target) return "elite";
|
|
15
|
+
if (value >= target * 0.7) return "high";
|
|
16
|
+
if (value >= target * 0.4) return "medium";
|
|
17
|
+
return "low";
|
|
18
|
+
}
|
|
19
|
+
function toPrDetail(pr, durationHours) {
|
|
20
|
+
return {
|
|
21
|
+
number: pr.number,
|
|
22
|
+
title: pr.title,
|
|
23
|
+
url: pr.html_url,
|
|
24
|
+
durationHours: Math.round(durationHours * 10) / 10,
|
|
25
|
+
mergedAt: pr.merged_at,
|
|
26
|
+
author: pr.user?.login,
|
|
27
|
+
authorAvatar: pr.user?.avatar_url
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
class DoraMetricsClient {
|
|
31
|
+
githubAuthApi;
|
|
32
|
+
configApi;
|
|
33
|
+
constructor(githubAuthApi, configApi) {
|
|
34
|
+
this.githubAuthApi = githubAuthApi;
|
|
35
|
+
this.configApi = configApi;
|
|
36
|
+
}
|
|
37
|
+
getConfig() {
|
|
38
|
+
const appCfg = this.configApi.getConfig("app");
|
|
39
|
+
const cfg = appCfg.getOptionalConfig("doraMetrics");
|
|
40
|
+
const rawEnvs = cfg?.getOptionalConfigArray("environments") ?? [];
|
|
41
|
+
const environments = rawEnvs.map((e) => ({
|
|
42
|
+
name: e.getString("name"),
|
|
43
|
+
branch: e.getString("branch"),
|
|
44
|
+
isProduction: e.getOptionalBoolean("isProduction") ?? false,
|
|
45
|
+
label: e.getOptionalString("label") ?? "hotfix"
|
|
46
|
+
}));
|
|
47
|
+
const initialDays = cfg?.getOptionalConfig("collection")?.getOptionalNumber("initialDays") ?? 30;
|
|
48
|
+
const targetsCfg = cfg?.getOptionalConfig("targets");
|
|
49
|
+
const targets = {
|
|
50
|
+
deploymentFrequency: targetsCfg?.getOptionalNumber("deploymentFrequency") ?? 7,
|
|
51
|
+
leadTime: targetsCfg?.getOptionalNumber("leadTime") ?? 24,
|
|
52
|
+
changeFailureRate: targetsCfg?.getOptionalNumber("changeFailureRate") ?? 15,
|
|
53
|
+
mttr: targetsCfg?.getOptionalNumber("mttr") ?? 1
|
|
54
|
+
};
|
|
55
|
+
return { environments, initialDays, targets };
|
|
56
|
+
}
|
|
57
|
+
getEnvironments() {
|
|
58
|
+
return this.getConfig().environments;
|
|
59
|
+
}
|
|
60
|
+
getDefaultDays() {
|
|
61
|
+
return this.getConfig().initialDays;
|
|
62
|
+
}
|
|
63
|
+
async fetchWithAuth(url, token) {
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `token ${token}`,
|
|
67
|
+
Accept: "application/vnd.github.v3+json"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`GitHub API request failed: ${response.status} ${response.statusText} \u2014 ${url}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return response.json();
|
|
76
|
+
}
|
|
77
|
+
async getMetrics(projectSlug, branch, isProduction, label, days) {
|
|
78
|
+
const token = await this.githubAuthApi.getAccessToken("repo");
|
|
79
|
+
const { targets } = this.getConfig();
|
|
80
|
+
const [owner, repo] = projectSlug.split("/");
|
|
81
|
+
if (!owner || !repo) {
|
|
82
|
+
throw new Error(`Invalid project slug "${projectSlug}". Expected "owner/repo".`);
|
|
83
|
+
}
|
|
84
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
85
|
+
const prsUrl = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&base=${branch}&per_page=100`;
|
|
86
|
+
const allPRs = await this.fetchWithAuth(prsUrl, token);
|
|
87
|
+
const mergedPRs = allPRs.filter((pr) => {
|
|
88
|
+
if (!pr.merged_at) return false;
|
|
89
|
+
return new Date(pr.merged_at) >= cutoff;
|
|
90
|
+
});
|
|
91
|
+
const deploymentsPerWeek = days > 0 ? mergedPRs.length / days * 7 : 0;
|
|
92
|
+
const deploymentFrequency = {
|
|
93
|
+
value: Math.round(deploymentsPerWeek * 100) / 100,
|
|
94
|
+
unit: "per week",
|
|
95
|
+
rating: computeRating(deploymentsPerWeek, targets.deploymentFrequency, false),
|
|
96
|
+
target: targets.deploymentFrequency,
|
|
97
|
+
// All merged PRs sorted newest-first — used in expanded view date table
|
|
98
|
+
slowestPRs: [...mergedPRs].sort((a, b) => new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime()).map((pr) => toPrDetail(
|
|
99
|
+
pr,
|
|
100
|
+
(new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
101
|
+
))
|
|
102
|
+
};
|
|
103
|
+
const prsWithLeadTime = mergedPRs.map((pr) => ({
|
|
104
|
+
pr,
|
|
105
|
+
durationHours: (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
106
|
+
})).sort((a, b) => b.durationHours - a.durationHours);
|
|
107
|
+
const avgLeadTimeHours = prsWithLeadTime.length > 0 ? prsWithLeadTime.reduce((sum, p) => sum + p.durationHours, 0) / prsWithLeadTime.length : 0;
|
|
108
|
+
const leadTime = {
|
|
109
|
+
value: Math.round(avgLeadTimeHours * 10) / 10,
|
|
110
|
+
unit: "hours",
|
|
111
|
+
rating: computeRating(avgLeadTimeHours, targets.leadTime, true),
|
|
112
|
+
target: targets.leadTime,
|
|
113
|
+
slowestPRs: prsWithLeadTime.slice(0, 10).map((p) => toPrDetail(p.pr, p.durationHours))
|
|
114
|
+
};
|
|
115
|
+
if (!isProduction) {
|
|
116
|
+
return {
|
|
117
|
+
deploymentFrequency,
|
|
118
|
+
leadTime,
|
|
119
|
+
changeFailureRate: null,
|
|
120
|
+
mttr: null,
|
|
121
|
+
numberOfHotfixes: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const hotfixPRs = mergedPRs.filter(
|
|
125
|
+
(pr) => (pr.labels ?? []).some((l) => l.name === label)
|
|
126
|
+
);
|
|
127
|
+
const numberOfHotfixes = {
|
|
128
|
+
value: hotfixPRs.length,
|
|
129
|
+
unit: "PRs",
|
|
130
|
+
rating: computeRating(hotfixPRs.length, 0, true),
|
|
131
|
+
target: 0
|
|
132
|
+
};
|
|
133
|
+
const changeFailureRatePct = mergedPRs.length > 0 ? hotfixPRs.length / mergedPRs.length * 100 : 0;
|
|
134
|
+
const changeFailureRate = {
|
|
135
|
+
value: Math.round(changeFailureRatePct * 10) / 10,
|
|
136
|
+
unit: "%",
|
|
137
|
+
rating: computeRating(changeFailureRatePct, targets.changeFailureRate, true),
|
|
138
|
+
target: targets.changeFailureRate
|
|
139
|
+
};
|
|
140
|
+
const hotfixWithDuration = hotfixPRs.map((pr) => ({
|
|
141
|
+
pr,
|
|
142
|
+
durationHours: (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
143
|
+
})).sort((a, b) => b.durationHours - a.durationHours);
|
|
144
|
+
const avgMttrHours = hotfixWithDuration.length > 0 ? hotfixWithDuration.reduce((sum, p) => sum + p.durationHours, 0) / hotfixWithDuration.length : 0;
|
|
145
|
+
const mttr = {
|
|
146
|
+
value: Math.round(avgMttrHours * 10) / 10,
|
|
147
|
+
unit: "hours",
|
|
148
|
+
rating: computeRating(avgMttrHours, targets.mttr, true),
|
|
149
|
+
target: targets.mttr,
|
|
150
|
+
slowestPRs: hotfixWithDuration.slice(0, 10).map((p) => toPrDetail(p.pr, p.durationHours))
|
|
151
|
+
};
|
|
152
|
+
return { deploymentFrequency, leadTime, changeFailureRate, mttr, numberOfHotfixes };
|
|
153
|
+
}
|
|
154
|
+
async getHistory(projectSlug, branch, isProduction, label, days) {
|
|
155
|
+
const token = await this.githubAuthApi.getAccessToken("repo");
|
|
156
|
+
const [owner, repo] = projectSlug.split("/");
|
|
157
|
+
if (!owner || !repo) throw new Error(`Invalid project slug "${projectSlug}".`);
|
|
158
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
159
|
+
const prsUrl = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&base=${branch}&per_page=100`;
|
|
160
|
+
const allPRs = await this.fetchWithAuth(prsUrl, token);
|
|
161
|
+
const mergedPRs = allPRs.filter((pr) => pr.merged_at && new Date(pr.merged_at) >= cutoff);
|
|
162
|
+
const bucketDays = Math.max(1, Math.round(days / 7));
|
|
163
|
+
const numBuckets = Math.max(2, Math.ceil(days / bucketDays));
|
|
164
|
+
const buckets = [];
|
|
165
|
+
for (let w = numBuckets - 1; w >= 0; w--) {
|
|
166
|
+
const weekEnd = new Date(Date.now() - w * bucketDays * 24 * 60 * 60 * 1e3);
|
|
167
|
+
const weekStart = new Date(weekEnd.getTime() - bucketDays * 24 * 60 * 60 * 1e3);
|
|
168
|
+
const weekPRs = mergedPRs.filter((pr) => {
|
|
169
|
+
const t = new Date(pr.merged_at).getTime();
|
|
170
|
+
return t >= weekStart.getTime() && t < weekEnd.getTime();
|
|
171
|
+
});
|
|
172
|
+
const leadTimes = weekPRs.map(
|
|
173
|
+
(pr) => (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
174
|
+
);
|
|
175
|
+
const avgLeadTime = leadTimes.length > 0 ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0;
|
|
176
|
+
let changeFailureRate;
|
|
177
|
+
let mttrHours;
|
|
178
|
+
if (isProduction) {
|
|
179
|
+
const hotfixPRs = weekPRs.filter((pr) => pr.labels.some((l) => l.name === label));
|
|
180
|
+
changeFailureRate = weekPRs.length > 0 ? Math.round(hotfixPRs.length / weekPRs.length * 1e3) / 10 : 0;
|
|
181
|
+
const hotfixDurations = hotfixPRs.map(
|
|
182
|
+
(pr) => (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
183
|
+
);
|
|
184
|
+
mttrHours = hotfixDurations.length > 0 ? Math.round(hotfixDurations.reduce((a, b) => a + b, 0) / hotfixDurations.length * 10) / 10 : 0;
|
|
185
|
+
}
|
|
186
|
+
buckets.push({
|
|
187
|
+
weekLabel: weekStart.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
|
188
|
+
bucketMidMs: Math.round((weekStart.getTime() + weekEnd.getTime()) / 2),
|
|
189
|
+
deploymentCount: weekPRs.length,
|
|
190
|
+
leadTimeHours: Math.round(avgLeadTime * 10) / 10,
|
|
191
|
+
changeFailureRate,
|
|
192
|
+
mttrHours
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return buckets;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { DoraMetricsClient };
|
|
200
|
+
//# sourceMappingURL=DoraMetricsClient.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DoraMetricsClient.esm.js","sources":["../../src/api/DoraMetricsClient.ts"],"sourcesContent":["import { ConfigApi, OAuthApi } from '@backstage/core-plugin-api';\nimport {\n DoraEnvironment,\n DoraHistoryPoint,\n DoraMetrics,\n DoraMetricValue,\n DoraMetricsApi,\n DoraRating,\n PrDetail,\n} from './types';\n\ninterface GitHubPR {\n number: number;\n title: string;\n html_url: string;\n created_at: string;\n merged_at: string | null;\n closed_at: string | null;\n labels: Array<{ name: string }>;\n user: { login: string; avatar_url: string } | null;\n}\n\n/**\n * Compute a DORA rating based on the configured target.\n *\n * lowerIsBetter (lead time, CFR, MTTR, hotfix count):\n * elite = value <= target\n * high = value <= target * 2\n * medium = value <= target * 4\n * low = value > target * 4\n *\n * higherIsBetter (deployment frequency):\n * elite = value >= target\n * high = value >= target * 0.7\n * medium = value >= target * 0.4\n * low = value < target * 0.4\n *\n * Special case — target = 0 (hotfix count ideal = 0):\n * elite=0, high≤2, medium≤5, low>5\n */\nfunction computeRating(value: number, target: number, lowerIsBetter: boolean): DoraRating {\n if (lowerIsBetter) {\n if (target === 0) {\n if (value === 0) return 'elite';\n if (value <= 2) return 'high';\n if (value <= 5) return 'medium';\n return 'low';\n }\n if (value <= target) return 'elite';\n if (value <= target * 2) return 'high';\n if (value <= target * 4) return 'medium';\n return 'low';\n }\n if (value >= target) return 'elite';\n if (value >= target * 0.7) return 'high';\n if (value >= target * 0.4) return 'medium';\n return 'low';\n}\n\nfunction toPrDetail(pr: GitHubPR, durationHours: number): PrDetail {\n return {\n number: pr.number,\n title: pr.title,\n url: pr.html_url,\n durationHours: Math.round(durationHours * 10) / 10,\n mergedAt: pr.merged_at!,\n author: pr.user?.login,\n authorAvatar: pr.user?.avatar_url,\n };\n}\n\nexport class DoraMetricsClient implements DoraMetricsApi {\n private readonly githubAuthApi: OAuthApi;\n private readonly configApi: ConfigApi;\n\n constructor(githubAuthApi: OAuthApi, configApi: ConfigApi) {\n this.githubAuthApi = githubAuthApi;\n this.configApi = configApi;\n }\n\n private getConfig() {\n const appCfg = this.configApi.getConfig('app');\n const cfg = appCfg.getOptionalConfig('doraMetrics');\n\n const rawEnvs = cfg?.getOptionalConfigArray('environments') ?? [];\n const environments: DoraEnvironment[] = rawEnvs.map(e => ({\n name: e.getString('name'),\n branch: e.getString('branch'),\n isProduction: e.getOptionalBoolean('isProduction') ?? false,\n label: e.getOptionalString('label') ?? 'hotfix',\n }));\n\n const initialDays =\n cfg?.getOptionalConfig('collection')?.getOptionalNumber('initialDays') ?? 30;\n\n const targetsCfg = cfg?.getOptionalConfig('targets');\n const targets = {\n deploymentFrequency: targetsCfg?.getOptionalNumber('deploymentFrequency') ?? 7,\n leadTime: targetsCfg?.getOptionalNumber('leadTime') ?? 24,\n changeFailureRate: targetsCfg?.getOptionalNumber('changeFailureRate') ?? 15,\n mttr: targetsCfg?.getOptionalNumber('mttr') ?? 1,\n };\n\n return { environments, initialDays, targets };\n }\n\n getEnvironments(): DoraEnvironment[] {\n return this.getConfig().environments;\n }\n\n getDefaultDays(): number {\n return this.getConfig().initialDays;\n }\n\n private async fetchWithAuth<T>(url: string, token: string): Promise<T> {\n const response = await fetch(url, {\n headers: {\n Authorization: `token ${token}`,\n Accept: 'application/vnd.github.v3+json',\n },\n });\n\n if (!response.ok) {\n throw new Error(\n `GitHub API request failed: ${response.status} ${response.statusText} — ${url}`,\n );\n }\n\n return response.json() as Promise<T>;\n }\n\n async getMetrics(\n projectSlug: string,\n branch: string,\n isProduction: boolean,\n label: string,\n days: number,\n ): Promise<DoraMetrics> {\n const token = await this.githubAuthApi.getAccessToken('repo');\n const { targets } = this.getConfig();\n\n const [owner, repo] = projectSlug.split('/');\n if (!owner || !repo) {\n throw new Error(`Invalid project slug \"${projectSlug}\". Expected \"owner/repo\".`);\n }\n\n const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n\n const prsUrl =\n `https://api.github.com/repos/${owner}/${repo}/pulls` +\n `?state=closed&base=${branch}&per_page=100`;\n\n const allPRs = await this.fetchWithAuth<GitHubPR[]>(prsUrl, token);\n\n const mergedPRs = allPRs.filter(pr => {\n if (!pr.merged_at) return false;\n return new Date(pr.merged_at) >= cutoff;\n });\n\n // --- Deployment Frequency ---\n const deploymentsPerWeek = days > 0 ? (mergedPRs.length / days) * 7 : 0;\n\n const deploymentFrequency: DoraMetricValue = {\n value: Math.round(deploymentsPerWeek * 100) / 100,\n unit: 'per week',\n rating: computeRating(deploymentsPerWeek, targets.deploymentFrequency, false),\n target: targets.deploymentFrequency,\n // All merged PRs sorted newest-first — used in expanded view date table\n slowestPRs: [...mergedPRs]\n .sort((a, b) => new Date(b.merged_at!).getTime() - new Date(a.merged_at!).getTime())\n .map(pr => toPrDetail(pr,\n (new Date(pr.merged_at!).getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60)\n )),\n };\n\n // --- Lead Time ---\n const prsWithLeadTime = mergedPRs\n .map(pr => ({\n pr,\n durationHours:\n (new Date(pr.merged_at!).getTime() - new Date(pr.created_at).getTime()) /\n (1000 * 60 * 60),\n }))\n .sort((a, b) => b.durationHours - a.durationHours);\n\n const avgLeadTimeHours =\n prsWithLeadTime.length > 0\n ? prsWithLeadTime.reduce((sum, p) => sum + p.durationHours, 0) / prsWithLeadTime.length\n : 0;\n\n const leadTime: DoraMetricValue = {\n value: Math.round(avgLeadTimeHours * 10) / 10,\n unit: 'hours',\n rating: computeRating(avgLeadTimeHours, targets.leadTime, true),\n target: targets.leadTime,\n slowestPRs: prsWithLeadTime\n .slice(0, 10)\n .map(p => toPrDetail(p.pr, p.durationHours)),\n };\n\n // --- Production-only metrics ---\n if (!isProduction) {\n return {\n deploymentFrequency,\n leadTime,\n changeFailureRate: null,\n mttr: null,\n numberOfHotfixes: null,\n };\n }\n\n const hotfixPRs = mergedPRs.filter(pr =>\n (pr.labels ?? []).some(l => l.name === label),\n );\n\n // Number of Hotfixes\n const numberOfHotfixes: DoraMetricValue = {\n value: hotfixPRs.length,\n unit: 'PRs',\n rating: computeRating(hotfixPRs.length, 0, true),\n target: 0,\n };\n\n // Change Failure Rate = fix PRs / all merged PRs (DORA standard: always 0–100%)\n const changeFailureRatePct =\n mergedPRs.length > 0 ? (hotfixPRs.length / mergedPRs.length) * 100 : 0;\n\n const changeFailureRate: DoraMetricValue = {\n value: Math.round(changeFailureRatePct * 10) / 10,\n unit: '%',\n rating: computeRating(changeFailureRatePct, targets.changeFailureRate, true),\n target: targets.changeFailureRate,\n };\n\n // MTTR\n const hotfixWithDuration = hotfixPRs\n .map(pr => ({\n pr,\n durationHours:\n (new Date(pr.merged_at!).getTime() - new Date(pr.created_at).getTime()) /\n (1000 * 60 * 60),\n }))\n .sort((a, b) => b.durationHours - a.durationHours);\n\n const avgMttrHours =\n hotfixWithDuration.length > 0\n ? hotfixWithDuration.reduce((sum, p) => sum + p.durationHours, 0) /\n hotfixWithDuration.length\n : 0;\n\n const mttr: DoraMetricValue = {\n value: Math.round(avgMttrHours * 10) / 10,\n unit: 'hours',\n rating: computeRating(avgMttrHours, targets.mttr, true),\n target: targets.mttr,\n slowestPRs: hotfixWithDuration\n .slice(0, 10)\n .map(p => toPrDetail(p.pr, p.durationHours)),\n };\n\n return { deploymentFrequency, leadTime, changeFailureRate, mttr, numberOfHotfixes };\n }\n\n async getHistory(\n projectSlug: string,\n branch: string,\n isProduction: boolean,\n label: string,\n days: number,\n ): Promise<DoraHistoryPoint[]> {\n const token = await this.githubAuthApi.getAccessToken('repo');\n const [owner, repo] = projectSlug.split('/');\n if (!owner || !repo) throw new Error(`Invalid project slug \"${projectSlug}\".`);\n\n const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n const prsUrl =\n `https://api.github.com/repos/${owner}/${repo}/pulls` +\n `?state=closed&base=${branch}&per_page=100`;\n const allPRs = await this.fetchWithAuth<GitHubPR[]>(prsUrl, token);\n const mergedPRs = allPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) >= cutoff);\n\n // Scale bucket size so we always get ~7 data points regardless of date range\n const bucketDays = Math.max(1, Math.round(days / 7));\n const numBuckets = Math.max(2, Math.ceil(days / bucketDays));\n const buckets: DoraHistoryPoint[] = [];\n\n for (let w = numBuckets - 1; w >= 0; w--) {\n const weekEnd = new Date(Date.now() - w * bucketDays * 24 * 60 * 60 * 1000);\n const weekStart = new Date(weekEnd.getTime() - bucketDays * 24 * 60 * 60 * 1000);\n\n const weekPRs = mergedPRs.filter(pr => {\n const t = new Date(pr.merged_at!).getTime();\n return t >= weekStart.getTime() && t < weekEnd.getTime();\n });\n\n const leadTimes = weekPRs.map(pr =>\n (new Date(pr.merged_at!).getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60),\n );\n const avgLeadTime =\n leadTimes.length > 0 ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0;\n\n let changeFailureRate: number | undefined;\n let mttrHours: number | undefined;\n\n if (isProduction) {\n const hotfixPRs = weekPRs.filter(pr => pr.labels.some(l => l.name === label));\n changeFailureRate =\n weekPRs.length > 0 ? Math.round((hotfixPRs.length / weekPRs.length) * 1000) / 10 : 0;\n const hotfixDurations = hotfixPRs.map(pr =>\n (new Date(pr.merged_at!).getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60),\n );\n mttrHours =\n hotfixDurations.length > 0\n ? Math.round((hotfixDurations.reduce((a, b) => a + b, 0) / hotfixDurations.length) * 10) / 10\n : 0;\n }\n\n buckets.push({\n weekLabel: weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),\n bucketMidMs: Math.round((weekStart.getTime() + weekEnd.getTime()) / 2),\n deploymentCount: weekPRs.length,\n leadTimeHours: Math.round(avgLeadTime * 10) / 10,\n changeFailureRate,\n mttrHours,\n });\n }\n\n return buckets;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.esm.js","sources":["../../src/api/types.ts"],"sourcesContent":["import { createApiRef } from '@backstage/core-plugin-api';\n\nexport type DoraRating = 'elite' | 'high' | 'medium' | 'low';\n\nexport interface PrDetail {\n number: number;\n title: string;\n url: string;\n durationHours: number;\n mergedAt: string;\n author?: string;\n authorAvatar?: string;\n}\n\nexport interface DoraMetricValue {\n value: number;\n unit: string;\n rating: DoraRating;\n target: number;\n slowestPRs?: PrDetail[];\n}\n\nexport interface DoraEnvironment {\n name: string;\n branch: string;\n isProduction: boolean;\n label: string;\n}\n\nexport interface DoraMetrics {\n deploymentFrequency: DoraMetricValue;\n leadTime: DoraMetricValue;\n /** null for non-production environments */\n changeFailureRate: DoraMetricValue | null;\n /** null for non-production environments */\n mttr: DoraMetricValue | null;\n /** null for non-production environments */\n numberOfHotfixes: DoraMetricValue | null;\n}\n\nexport interface DoraHistoryPoint {\n weekLabel: string; // e.g. \"Mar 10\"\n bucketMidMs: number; // timestamp (ms) of bucket midpoint — for chart x-axis alignment\n deploymentCount: number; // total PRs merged that week\n leadTimeHours: number; // avg lead time that week (0 if no PRs)\n changeFailureRate?: number; // % (prod only)\n mttrHours?: number; // avg (prod only)\n}\n\nexport interface DoraMetricsApi {\n getEnvironments(): DoraEnvironment[];\n getDefaultDays(): number;\n getMetrics(\n projectSlug: string,\n branch: string,\n isProduction: boolean,\n label: string,\n days: number,\n ): Promise<DoraMetrics>;\n getHistory(\n projectSlug: string,\n branch: string,\n isProduction: boolean,\n label: string,\n days: number,\n ): Promise<DoraHistoryPoint[]>;\n}\n\nexport const doraMetricsApiRef = createApiRef<DoraMetricsApi>({\n id: 'plugin.dora-metrics.service',\n});\n"],"names":[],"mappings":";;AAoEO,MAAM,oBAAoB,YAAA,CAA6B;AAAA,EAC5D,EAAA,EAAI;AACN,CAAC;;;;"}
|