@c2l2c/backstage-plugin-dora-metrics 0.1.0 → 0.3.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 +34 -6
- package/config.d.ts +32 -9
- package/dist/alpha.d.ts +1 -56
- package/dist/api/DoraMetricsClient.esm.js +60 -17
- package/dist/api/DoraMetricsClient.esm.js.map +1 -1
- package/dist/api/MockDoraMetricsClient.esm.js +72 -0
- package/dist/api/MockDoraMetricsClient.esm.js.map +1 -0
- package/dist/api/types.esm.js.map +1 -1
- package/dist/components/DoraMetricsContent.esm.js +746 -945
- package/dist/components/DoraMetricsContent.esm.js.map +1 -1
- package/dist/components/ui/badge.esm.js +4 -6
- package/dist/components/ui/badge.esm.js.map +1 -1
- package/dist/components/ui/card.esm.js +14 -12
- package/dist/components/ui/card.esm.js.map +1 -1
- package/dist/components/ui/select.esm.js +23 -27
- package/dist/components/ui/select.esm.js.map +1 -1
- package/dist/extensions/api.esm.js +5 -1
- package/dist/extensions/api.esm.js.map +1 -1
- package/dist/extensions/entityContent.esm.js +1 -2
- package/dist/extensions/entityContent.esm.js.map +1 -1
- package/dist/index.d.ts +37 -13
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/package.json +6 -13
package/README.md
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
# @c2l2c/backstage-plugin-dora-metrics
|
|
2
2
|
|
|
3
|
-
A Backstage frontend plugin that
|
|
3
|
+
A Backstage frontend plugin that surfaces **DORA metrics per catalog entity** — scoped to the specific service you're viewing, not your whole org.
|
|
4
|
+
|
|
5
|
+
For each catalog service annotated with a GitHub repo, this plugin adds a **DORA Metrics tab** that computes all four key metrics directly from GitHub PRs via OAuth — no extra backend or data pipeline required:
|
|
6
|
+
|
|
7
|
+
- **Deployment Frequency** — how often PRs are merged to the target branch
|
|
8
|
+
- **Lead Time for Changes** — average time from PR open to merge
|
|
9
|
+
- **Change Failure Rate** — percentage of deployments that were hotfixes (production only)
|
|
10
|
+
- **MTTR** — mean time to recover, measured as average hotfix PR duration (production only)
|
|
11
|
+
- **Hotfixes to Production** - number of hotfixes deployed to production in the given time period (production only)
|
|
4
12
|
|
|
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
13
|
|
|
11
14
|
Ratings are classified as **Elite / High / Medium / Low** per DORA research benchmarks.
|
|
12
15
|
|
|
16
|
+
### How this differs from org-level DORA scorecards
|
|
17
|
+
|
|
18
|
+
Org-level [DORA plugins](https://github.com/zc149/backstage-plugin-dora-scorecard) aggregate metrics across all repositories in your GitHub org. This plugin instead scopes metrics to **a single catalog entity** — useful when you want to track DORA health at the service level, compare environments (e.g. production vs staging), or drill into which specific PRs are driving your lead time or failure rate. It also requires no backend: all data is fetched client-side using the user's existing GitHub OAuth session.
|
|
19
|
+
|
|
13
20
|
---
|
|
14
21
|
|
|
22
|
+
## Screenshots/Videos
|
|
23
|
+
|
|
24
|
+
### 1. DORA metrics dashboard
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### 2. Deployment frquency exapnded view
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
### 3. LTFC expanded view
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
### 4. CFR expanded view
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
### 5. MTTR expanded view
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
|
|
15
41
|
## Requirements
|
|
16
42
|
|
|
17
43
|
- Backstage v1.35+ (new frontend system)
|
|
@@ -83,6 +109,8 @@ It calculates:
|
|
|
83
109
|
- **Lead Time**: time from PR creation to merge
|
|
84
110
|
- **CFR**: percentage of PRs labelled as hotfixes (production environments only)
|
|
85
111
|
- **MTTR**: average time hotfix PRs were open (production environments only)
|
|
112
|
+
- **Hotfixes to Production** - number of hotfixes deployed to production in the given time period (production environments only)
|
|
113
|
+
|
|
86
114
|
|
|
87
115
|
---
|
|
88
116
|
|
package/config.d.ts
CHANGED
|
@@ -6,16 +6,37 @@ export interface Config {
|
|
|
6
6
|
*/
|
|
7
7
|
doraMetrics?: {
|
|
8
8
|
/**
|
|
9
|
+
* Enable debug mode — uses mock data instead of the GitHub API.
|
|
10
|
+
* Useful for local development and screenshots.
|
|
11
|
+
* @visibility frontend
|
|
12
|
+
*/
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Environments to track. At most one environment may have isProduction: true.
|
|
9
16
|
* @visibility frontend
|
|
10
17
|
*/
|
|
11
18
|
environments?: Array<{
|
|
12
|
-
/** @visibility frontend */
|
|
19
|
+
/** Display name shown in the UI (e.g. "Production"). @visibility frontend */
|
|
13
20
|
name: string;
|
|
14
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Branch(es) to track for this environment. Accepts:
|
|
23
|
+
* - A single branch name: "main"
|
|
24
|
+
* - Comma-separated names: "main,master"
|
|
25
|
+
* - A JavaScript regex string: "^(main|master)$"
|
|
26
|
+
*
|
|
27
|
+
* When multiple branches match, PRs are fetched from all of them and
|
|
28
|
+
* deduplicated by PR number before metrics are computed.
|
|
29
|
+
* @visibility frontend
|
|
30
|
+
*/
|
|
15
31
|
branch: string;
|
|
16
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Mark this environment as the production environment.
|
|
34
|
+
* Only one environment across the entire list may set this to true.
|
|
35
|
+
* Production environments unlock Change Failure Rate, MTTR, and Hotfix count metrics.
|
|
36
|
+
* @visibility frontend
|
|
37
|
+
*/
|
|
17
38
|
isProduction?: boolean;
|
|
18
|
-
/** Label on PRs that represent production failures (hotfixes). @visibility frontend */
|
|
39
|
+
/** Label on PRs that represent production failures (hotfixes). Defaults to "hotfix". @visibility frontend */
|
|
19
40
|
label?: string;
|
|
20
41
|
/** Label on PRs that represent planned feature deployments. @visibility frontend */
|
|
21
42
|
featureLabel?: string;
|
|
@@ -24,20 +45,22 @@ export interface Config {
|
|
|
24
45
|
* @visibility frontend
|
|
25
46
|
*/
|
|
26
47
|
collection?: {
|
|
27
|
-
/** @visibility frontend */
|
|
48
|
+
/** Number of days of history to fetch on initial load. Defaults to 30. @visibility frontend */
|
|
28
49
|
initialDays?: number;
|
|
29
50
|
};
|
|
30
51
|
/**
|
|
52
|
+
* Default performance targets used for rating calculations.
|
|
53
|
+
* These can be overridden per entity via the dora-metrics/targets annotation.
|
|
31
54
|
* @visibility frontend
|
|
32
55
|
*/
|
|
33
56
|
targets?: {
|
|
34
|
-
/** @visibility frontend */
|
|
57
|
+
/** Target deployments per week (higher is better). Defaults to 7. @visibility frontend */
|
|
35
58
|
deploymentFrequency?: number;
|
|
36
|
-
/** @visibility frontend */
|
|
59
|
+
/** Target lead time in hours (lower is better). Defaults to 24. @visibility frontend */
|
|
37
60
|
leadTime?: number;
|
|
38
|
-
/** @visibility frontend */
|
|
61
|
+
/** Target change failure rate as a percentage (lower is better). Defaults to 15. @visibility frontend */
|
|
39
62
|
changeFailureRate?: number;
|
|
40
|
-
/** @visibility frontend */
|
|
63
|
+
/** Target mean time to restore in hours (lower is better). Defaults to 1. @visibility frontend */
|
|
41
64
|
mttr?: number;
|
|
42
65
|
};
|
|
43
66
|
};
|
package/dist/alpha.d.ts
CHANGED
|
@@ -1,60 +1,5 @@
|
|
|
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
1
|
import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
|
|
5
2
|
|
|
6
|
-
declare const doraMetricsPlugin: _backstage_frontend_plugin_api.
|
|
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
|
-
}>;
|
|
3
|
+
declare const doraMetricsPlugin: _backstage_frontend_plugin_api.FrontendPlugin<{}, {}, {}>;
|
|
59
4
|
|
|
60
5
|
export { doraMetricsPlugin as default };
|
|
@@ -27,6 +27,19 @@ function toPrDetail(pr, durationHours) {
|
|
|
27
27
|
authorAvatar: pr.user?.avatar_url
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
function isRegexPattern(pattern) {
|
|
31
|
+
return /[|^$[\](){}?+*\\]/.test(pattern);
|
|
32
|
+
}
|
|
33
|
+
async function resolveBranches(owner, repo, branchPattern, fetchFn) {
|
|
34
|
+
if (isRegexPattern(branchPattern)) {
|
|
35
|
+
const repoBranches = await fetchFn(
|
|
36
|
+
`https://api.github.com/repos/${owner}/${repo}/branches?per_page=100`
|
|
37
|
+
);
|
|
38
|
+
const regex = new RegExp(branchPattern);
|
|
39
|
+
return repoBranches.map((b) => b.name).filter((name) => regex.test(name));
|
|
40
|
+
}
|
|
41
|
+
return branchPattern.split(",").map((b) => b.trim()).filter(Boolean);
|
|
42
|
+
}
|
|
30
43
|
class DoraMetricsClient {
|
|
31
44
|
githubAuthApi;
|
|
32
45
|
configApi;
|
|
@@ -44,6 +57,12 @@ class DoraMetricsClient {
|
|
|
44
57
|
isProduction: e.getOptionalBoolean("isProduction") ?? false,
|
|
45
58
|
label: e.getOptionalString("label") ?? "hotfix"
|
|
46
59
|
}));
|
|
60
|
+
const prodEnvs = environments.filter((e) => e.isProduction);
|
|
61
|
+
if (prodEnvs.length > 1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`DORA metrics config error: only one environment may have isProduction: true, but found ${prodEnvs.length}: ${prodEnvs.map((e) => e.name).join(", ")}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
47
66
|
const initialDays = cfg?.getOptionalConfig("collection")?.getOptionalNumber("initialDays") ?? 30;
|
|
48
67
|
const targetsCfg = cfg?.getOptionalConfig("targets");
|
|
49
68
|
const targets = {
|
|
@@ -60,6 +79,9 @@ class DoraMetricsClient {
|
|
|
60
79
|
getDefaultDays() {
|
|
61
80
|
return this.getConfig().initialDays;
|
|
62
81
|
}
|
|
82
|
+
getTargets() {
|
|
83
|
+
return this.getConfig().targets;
|
|
84
|
+
}
|
|
63
85
|
async fetchWithAuth(url, token) {
|
|
64
86
|
const response = await fetch(url, {
|
|
65
87
|
headers: {
|
|
@@ -74,20 +96,39 @@ class DoraMetricsClient {
|
|
|
74
96
|
}
|
|
75
97
|
return response.json();
|
|
76
98
|
}
|
|
77
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Fetch all merged PRs for an environment, resolving branch patterns and
|
|
101
|
+
* deduplicating across multiple branches by PR number.
|
|
102
|
+
*/
|
|
103
|
+
async fetchMergedPRs(owner, repo, env, cutoff, token) {
|
|
104
|
+
const boundFetch = (url) => this.fetchWithAuth(url, token);
|
|
105
|
+
const branches = await resolveBranches(owner, repo, env.branch, boundFetch);
|
|
106
|
+
const allByNumber = /* @__PURE__ */ new Map();
|
|
107
|
+
await Promise.all(
|
|
108
|
+
branches.map(async (branch) => {
|
|
109
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&base=${branch}&per_page=100`;
|
|
110
|
+
const prs = await boundFetch(url);
|
|
111
|
+
for (const pr of prs) {
|
|
112
|
+
if (pr.merged_at && new Date(pr.merged_at) >= cutoff) {
|
|
113
|
+
if (!allByNumber.has(pr.number)) {
|
|
114
|
+
allByNumber.set(pr.number, pr);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
return Array.from(allByNumber.values());
|
|
121
|
+
}
|
|
122
|
+
async getMetrics(projectSlug, env, days, targetsOverride) {
|
|
78
123
|
const token = await this.githubAuthApi.getAccessToken("repo");
|
|
79
|
-
const { targets } = this.getConfig();
|
|
124
|
+
const { targets: configTargets } = this.getConfig();
|
|
125
|
+
const targets = { ...configTargets, ...targetsOverride };
|
|
80
126
|
const [owner, repo] = projectSlug.split("/");
|
|
81
127
|
if (!owner || !repo) {
|
|
82
128
|
throw new Error(`Invalid project slug "${projectSlug}". Expected "owner/repo".`);
|
|
83
129
|
}
|
|
84
130
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
85
|
-
const
|
|
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
|
-
});
|
|
131
|
+
const mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);
|
|
91
132
|
const deploymentsPerWeek = days > 0 ? mergedPRs.length / days * 7 : 0;
|
|
92
133
|
const deploymentFrequency = {
|
|
93
134
|
value: Math.round(deploymentsPerWeek * 100) / 100,
|
|
@@ -112,7 +153,7 @@ class DoraMetricsClient {
|
|
|
112
153
|
target: targets.leadTime,
|
|
113
154
|
slowestPRs: prsWithLeadTime.slice(0, 10).map((p) => toPrDetail(p.pr, p.durationHours))
|
|
114
155
|
};
|
|
115
|
-
if (!isProduction) {
|
|
156
|
+
if (!env.isProduction) {
|
|
116
157
|
return {
|
|
117
158
|
deploymentFrequency,
|
|
118
159
|
leadTime,
|
|
@@ -122,13 +163,17 @@ class DoraMetricsClient {
|
|
|
122
163
|
};
|
|
123
164
|
}
|
|
124
165
|
const hotfixPRs = mergedPRs.filter(
|
|
125
|
-
(pr) => (pr.labels ?? []).some((l) => l.name === label)
|
|
166
|
+
(pr) => (pr.labels ?? []).some((l) => l.name === env.label)
|
|
126
167
|
);
|
|
127
168
|
const numberOfHotfixes = {
|
|
128
169
|
value: hotfixPRs.length,
|
|
129
170
|
unit: "PRs",
|
|
130
171
|
rating: computeRating(hotfixPRs.length, 0, true),
|
|
131
|
-
target: 0
|
|
172
|
+
target: 0,
|
|
173
|
+
slowestPRs: [...hotfixPRs].sort((a, b) => new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime()).map((pr) => toPrDetail(
|
|
174
|
+
pr,
|
|
175
|
+
(new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
176
|
+
))
|
|
132
177
|
};
|
|
133
178
|
const changeFailureRatePct = mergedPRs.length > 0 ? hotfixPRs.length / mergedPRs.length * 100 : 0;
|
|
134
179
|
const changeFailureRate = {
|
|
@@ -151,14 +196,12 @@ class DoraMetricsClient {
|
|
|
151
196
|
};
|
|
152
197
|
return { deploymentFrequency, leadTime, changeFailureRate, mttr, numberOfHotfixes };
|
|
153
198
|
}
|
|
154
|
-
async getHistory(projectSlug,
|
|
199
|
+
async getHistory(projectSlug, env, days) {
|
|
155
200
|
const token = await this.githubAuthApi.getAccessToken("repo");
|
|
156
201
|
const [owner, repo] = projectSlug.split("/");
|
|
157
202
|
if (!owner || !repo) throw new Error(`Invalid project slug "${projectSlug}".`);
|
|
158
203
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
159
|
-
const
|
|
160
|
-
const allPRs = await this.fetchWithAuth(prsUrl, token);
|
|
161
|
-
const mergedPRs = allPRs.filter((pr) => pr.merged_at && new Date(pr.merged_at) >= cutoff);
|
|
204
|
+
const mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);
|
|
162
205
|
const bucketDays = Math.max(1, Math.round(days / 7));
|
|
163
206
|
const numBuckets = Math.max(2, Math.ceil(days / bucketDays));
|
|
164
207
|
const buckets = [];
|
|
@@ -175,8 +218,8 @@ class DoraMetricsClient {
|
|
|
175
218
|
const avgLeadTime = leadTimes.length > 0 ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0;
|
|
176
219
|
let changeFailureRate;
|
|
177
220
|
let mttrHours;
|
|
178
|
-
if (isProduction) {
|
|
179
|
-
const hotfixPRs = weekPRs.filter((pr) => pr.labels.some((l) => l.name === label));
|
|
221
|
+
if (env.isProduction) {
|
|
222
|
+
const hotfixPRs = weekPRs.filter((pr) => pr.labels.some((l) => l.name === env.label));
|
|
180
223
|
changeFailureRate = weekPRs.length > 0 ? Math.round(hotfixPRs.length / weekPRs.length * 1e3) / 10 : 0;
|
|
181
224
|
const hotfixDurations = hotfixPRs.map(
|
|
182
225
|
(pr) => (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
|
|
@@ -1 +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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"}
|
|
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 DoraTargets,\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\n/**\n * Determine whether a branch pattern string is a regex (vs a plain name or\n * comma-separated list of names). A string is treated as a regex if it\n * contains any regex meta-character: | ^ $ [ ] ( ) { } ? + * \\\n */\nfunction isRegexPattern(pattern: string): boolean {\n return /[|^$[\\](){}?+*\\\\]/.test(pattern);\n}\n\n/**\n * Resolve a branch pattern to a concrete list of branch names.\n *\n * - Single name \"main\" → [\"main\"]\n * - Comma-sep \"main,master\" → [\"main\", \"master\"]\n * - Regex \"^(main|master)$\"→ fetched from the repo's branch list and filtered\n */\nasync function resolveBranches(\n owner: string,\n repo: string,\n branchPattern: string,\n fetchFn: <T>(url: string) => Promise<T>,\n): Promise<string[]> {\n if (isRegexPattern(branchPattern)) {\n const repoBranches = await fetchFn<Array<{ name: string }>>(\n `https://api.github.com/repos/${owner}/${repo}/branches?per_page=100`,\n );\n const regex = new RegExp(branchPattern);\n return repoBranches.map(b => b.name).filter(name => regex.test(name));\n }\n // Comma-separated or single branch name\n return branchPattern.split(',').map(b => b.trim()).filter(Boolean);\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 // Validate: at most one isProduction environment\n const prodEnvs = environments.filter(e => e.isProduction);\n if (prodEnvs.length > 1) {\n throw new Error(\n `DORA metrics config error: only one environment may have isProduction: true, ` +\n `but found ${prodEnvs.length}: ${prodEnvs.map(e => e.name).join(', ')}`,\n );\n }\n\n const initialDays =\n cfg?.getOptionalConfig('collection')?.getOptionalNumber('initialDays') ?? 30;\n\n const targetsCfg = cfg?.getOptionalConfig('targets');\n const targets: DoraTargets = {\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 getTargets(): DoraTargets {\n return this.getConfig().targets;\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 /**\n * Fetch all merged PRs for an environment, resolving branch patterns and\n * deduplicating across multiple branches by PR number.\n */\n private async fetchMergedPRs(\n owner: string,\n repo: string,\n env: DoraEnvironment,\n cutoff: Date,\n token: string,\n ): Promise<GitHubPR[]> {\n const boundFetch = <T>(url: string) => this.fetchWithAuth<T>(url, token);\n\n const branches = await resolveBranches(owner, repo, env.branch, boundFetch);\n\n const allByNumber = new Map<number, GitHubPR>();\n\n await Promise.all(\n branches.map(async branch => {\n const url =\n `https://api.github.com/repos/${owner}/${repo}/pulls` +\n `?state=closed&base=${branch}&per_page=100`;\n const prs = await boundFetch<GitHubPR[]>(url);\n for (const pr of prs) {\n if (pr.merged_at && new Date(pr.merged_at) >= cutoff) {\n // Keep first occurrence (branches may share PRs — shouldn't happen\n // for base-branch-filtered queries, but guard anyway)\n if (!allByNumber.has(pr.number)) {\n allByNumber.set(pr.number, pr);\n }\n }\n }\n }),\n );\n\n return Array.from(allByNumber.values());\n }\n\n async getMetrics(\n projectSlug: string,\n env: DoraEnvironment,\n days: number,\n targetsOverride?: Partial<DoraTargets>,\n ): Promise<DoraMetrics> {\n const token = await this.githubAuthApi.getAccessToken('repo');\n const { targets: configTargets } = this.getConfig();\n const targets: DoraTargets = { ...configTargets, ...targetsOverride };\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 const mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);\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 (!env.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 === env.label),\n );\n\n // Number of Hotfixes — sorted newest-first for the expanded list\n const numberOfHotfixes: DoraMetricValue = {\n value: hotfixPRs.length,\n unit: 'PRs',\n rating: computeRating(hotfixPRs.length, 0, true),\n target: 0,\n slowestPRs: [...hotfixPRs]\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 // 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 env: DoraEnvironment,\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 mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);\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 (env.isProduction) {\n const hotfixPRs = weekPRs.filter(pr => pr.labels.some(l => l.name === env.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,72 @@
|
|
|
1
|
+
const MOCK_ENVIRONMENTS = [
|
|
2
|
+
{ name: "Production", branch: "main", isProduction: true, label: "hotfix" },
|
|
3
|
+
{ name: "Staging", branch: "staging", isProduction: false, label: "" }
|
|
4
|
+
];
|
|
5
|
+
const MOCK_TARGETS = {
|
|
6
|
+
deploymentFrequency: 7,
|
|
7
|
+
leadTime: 24,
|
|
8
|
+
changeFailureRate: 15,
|
|
9
|
+
mttr: 1
|
|
10
|
+
};
|
|
11
|
+
const MOCK_PRS = [
|
|
12
|
+
{ number: 142, title: "feat: redesign dashboard layout", url: "https://github.com/example/repo/pull/142", durationHours: 31.2, mergedAt: "2026-03-15T14:22:00Z", author: "alice", authorAvatar: "https://avatars.githubusercontent.com/u/1?v=4" },
|
|
13
|
+
{ number: 138, title: "fix: resolve memory leak in worker thread", url: "https://github.com/example/repo/pull/138", durationHours: 22.5, mergedAt: "2026-03-12T09:10:00Z", author: "bob", authorAvatar: "https://avatars.githubusercontent.com/u/2?v=4" },
|
|
14
|
+
{ number: 135, title: "feat: add export to CSV functionality", url: "https://github.com/example/repo/pull/135", durationHours: 18.8, mergedAt: "2026-03-10T16:45:00Z", author: "carol", authorAvatar: "https://avatars.githubusercontent.com/u/3?v=4" },
|
|
15
|
+
{ number: 131, title: "chore: upgrade dependencies to latest", url: "https://github.com/example/repo/pull/131", durationHours: 14.1, mergedAt: "2026-03-08T11:30:00Z", author: "dave", authorAvatar: "https://avatars.githubusercontent.com/u/4?v=4" },
|
|
16
|
+
{ number: 128, title: "feat: implement rate limiting middleware", url: "https://github.com/example/repo/pull/128", durationHours: 9.6, mergedAt: "2026-03-05T13:20:00Z", author: "alice", authorAvatar: "https://avatars.githubusercontent.com/u/1?v=4" }
|
|
17
|
+
];
|
|
18
|
+
const MOCK_HOTFIX_PRS = [
|
|
19
|
+
{ number: 140, title: "hotfix: fix broken auth token refresh", url: "https://github.com/example/repo/pull/140", durationHours: 1.2, mergedAt: "2026-03-14T03:45:00Z", author: "bob", authorAvatar: "https://avatars.githubusercontent.com/u/2?v=4" },
|
|
20
|
+
{ number: 133, title: "hotfix: patch SQL injection in search query", url: "https://github.com/example/repo/pull/133", durationHours: 0.8, mergedAt: "2026-03-09T22:10:00Z", author: "carol", authorAvatar: "https://avatars.githubusercontent.com/u/3?v=4" }
|
|
21
|
+
];
|
|
22
|
+
const MOCK_HISTORY = [
|
|
23
|
+
{ weekLabel: "Feb 17", bucketMidMs: (/* @__PURE__ */ new Date("2026-02-20")).getTime(), deploymentCount: 3, leadTimeHours: 24.1, changeFailureRate: 0, mttrHours: 0 },
|
|
24
|
+
{ weekLabel: "Feb 24", bucketMidMs: (/* @__PURE__ */ new Date("2026-02-27")).getTime(), deploymentCount: 5, leadTimeHours: 19.8, changeFailureRate: 20, mttrHours: 1.2 },
|
|
25
|
+
{ weekLabel: "Mar 3", bucketMidMs: (/* @__PURE__ */ new Date("2026-03-06")).getTime(), deploymentCount: 4, leadTimeHours: 22.3, changeFailureRate: 0, mttrHours: 0 },
|
|
26
|
+
{ weekLabel: "Mar 10", bucketMidMs: (/* @__PURE__ */ new Date("2026-03-13")).getTime(), deploymentCount: 6, leadTimeHours: 16.5, changeFailureRate: 16.7, mttrHours: 0.9 },
|
|
27
|
+
{ weekLabel: "Mar 17", bucketMidMs: (/* @__PURE__ */ new Date("2026-03-20")).getTime(), deploymentCount: 5, leadTimeHours: 18.4, changeFailureRate: 0, mttrHours: 0 }
|
|
28
|
+
];
|
|
29
|
+
class MockDoraMetricsClient {
|
|
30
|
+
getEnvironments() {
|
|
31
|
+
return MOCK_ENVIRONMENTS;
|
|
32
|
+
}
|
|
33
|
+
getDefaultDays() {
|
|
34
|
+
return 30;
|
|
35
|
+
}
|
|
36
|
+
getTargets() {
|
|
37
|
+
return MOCK_TARGETS;
|
|
38
|
+
}
|
|
39
|
+
async getMetrics(_projectSlug, env, _days, _targetsOverride) {
|
|
40
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
41
|
+
return {
|
|
42
|
+
deploymentFrequency: {
|
|
43
|
+
value: 4.2,
|
|
44
|
+
unit: "per week",
|
|
45
|
+
rating: "high",
|
|
46
|
+
target: 7,
|
|
47
|
+
slowestPRs: MOCK_PRS
|
|
48
|
+
},
|
|
49
|
+
leadTime: {
|
|
50
|
+
value: 18.4,
|
|
51
|
+
unit: "hours",
|
|
52
|
+
rating: "elite",
|
|
53
|
+
target: 24,
|
|
54
|
+
slowestPRs: MOCK_PRS
|
|
55
|
+
},
|
|
56
|
+
changeFailureRate: env.isProduction ? { value: 8.3, unit: "%", rating: "high", target: 15 } : null,
|
|
57
|
+
mttr: env.isProduction ? { value: 0.8, unit: "hours", rating: "elite", target: 1, slowestPRs: MOCK_HOTFIX_PRS } : null,
|
|
58
|
+
numberOfHotfixes: env.isProduction ? { value: 2, unit: "PRs", rating: "high", target: 0, slowestPRs: MOCK_HOTFIX_PRS } : null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async getHistory(_projectSlug, env, _days) {
|
|
62
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
63
|
+
return MOCK_HISTORY.map((point) => ({
|
|
64
|
+
...point,
|
|
65
|
+
changeFailureRate: env.isProduction ? point.changeFailureRate : void 0,
|
|
66
|
+
mttrHours: env.isProduction ? point.mttrHours : void 0
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { MockDoraMetricsClient };
|
|
72
|
+
//# sourceMappingURL=MockDoraMetricsClient.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MockDoraMetricsClient.esm.js","sources":["../../src/api/MockDoraMetricsClient.ts"],"sourcesContent":["import { DoraEnvironment, DoraHistoryPoint, DoraMetrics, DoraMetricsApi, DoraTargets } from './types';\n\nconst MOCK_ENVIRONMENTS: DoraEnvironment[] = [\n { name: 'Production', branch: 'main', isProduction: true, label: 'hotfix' },\n { name: 'Staging', branch: 'staging', isProduction: false, label: '' },\n];\n\nconst MOCK_TARGETS: DoraTargets = {\n deploymentFrequency: 7,\n leadTime: 24,\n changeFailureRate: 15,\n mttr: 1,\n};\n\nconst MOCK_PRS = [\n { number: 142, title: 'feat: redesign dashboard layout', url: 'https://github.com/example/repo/pull/142', durationHours: 31.2, mergedAt: '2026-03-15T14:22:00Z', author: 'alice', authorAvatar: 'https://avatars.githubusercontent.com/u/1?v=4' },\n { number: 138, title: 'fix: resolve memory leak in worker thread', url: 'https://github.com/example/repo/pull/138', durationHours: 22.5, mergedAt: '2026-03-12T09:10:00Z', author: 'bob', authorAvatar: 'https://avatars.githubusercontent.com/u/2?v=4' },\n { number: 135, title: 'feat: add export to CSV functionality', url: 'https://github.com/example/repo/pull/135', durationHours: 18.8, mergedAt: '2026-03-10T16:45:00Z', author: 'carol', authorAvatar: 'https://avatars.githubusercontent.com/u/3?v=4' },\n { number: 131, title: 'chore: upgrade dependencies to latest', url: 'https://github.com/example/repo/pull/131', durationHours: 14.1, mergedAt: '2026-03-08T11:30:00Z', author: 'dave', authorAvatar: 'https://avatars.githubusercontent.com/u/4?v=4' },\n { number: 128, title: 'feat: implement rate limiting middleware', url: 'https://github.com/example/repo/pull/128', durationHours: 9.6, mergedAt: '2026-03-05T13:20:00Z', author: 'alice', authorAvatar: 'https://avatars.githubusercontent.com/u/1?v=4' },\n];\n\nconst MOCK_HOTFIX_PRS = [\n { number: 140, title: 'hotfix: fix broken auth token refresh', url: 'https://github.com/example/repo/pull/140', durationHours: 1.2, mergedAt: '2026-03-14T03:45:00Z', author: 'bob', authorAvatar: 'https://avatars.githubusercontent.com/u/2?v=4' },\n { number: 133, title: 'hotfix: patch SQL injection in search query', url: 'https://github.com/example/repo/pull/133', durationHours: 0.8, mergedAt: '2026-03-09T22:10:00Z', author: 'carol', authorAvatar: 'https://avatars.githubusercontent.com/u/3?v=4' },\n];\n\nconst MOCK_HISTORY: DoraHistoryPoint[] = [\n { weekLabel: 'Feb 17', bucketMidMs: new Date('2026-02-20').getTime(), deploymentCount: 3, leadTimeHours: 24.1, changeFailureRate: 0, mttrHours: 0 },\n { weekLabel: 'Feb 24', bucketMidMs: new Date('2026-02-27').getTime(), deploymentCount: 5, leadTimeHours: 19.8, changeFailureRate: 20, mttrHours: 1.2 },\n { weekLabel: 'Mar 3', bucketMidMs: new Date('2026-03-06').getTime(), deploymentCount: 4, leadTimeHours: 22.3, changeFailureRate: 0, mttrHours: 0 },\n { weekLabel: 'Mar 10', bucketMidMs: new Date('2026-03-13').getTime(), deploymentCount: 6, leadTimeHours: 16.5, changeFailureRate: 16.7, mttrHours: 0.9 },\n { weekLabel: 'Mar 17', bucketMidMs: new Date('2026-03-20').getTime(), deploymentCount: 5, leadTimeHours: 18.4, changeFailureRate: 0, mttrHours: 0 },\n];\n\nexport class MockDoraMetricsClient implements DoraMetricsApi {\n getEnvironments(): DoraEnvironment[] {\n return MOCK_ENVIRONMENTS;\n }\n\n getDefaultDays(): number {\n return 30;\n }\n\n getTargets(): DoraTargets {\n return MOCK_TARGETS;\n }\n\n async getMetrics(\n _projectSlug: string,\n env: DoraEnvironment,\n _days: number,\n _targetsOverride?: Partial<DoraTargets>,\n ): Promise<DoraMetrics> {\n await new Promise(r => setTimeout(r, 600)); // simulate network delay\n\n return {\n deploymentFrequency: {\n value: 4.2,\n unit: 'per week',\n rating: 'high',\n target: 7,\n slowestPRs: MOCK_PRS,\n },\n leadTime: {\n value: 18.4,\n unit: 'hours',\n rating: 'elite',\n target: 24,\n slowestPRs: MOCK_PRS,\n },\n changeFailureRate: env.isProduction\n ? { value: 8.3, unit: '%', rating: 'high', target: 15 }\n : null,\n mttr: env.isProduction\n ? { value: 0.8, unit: 'hours', rating: 'elite', target: 1, slowestPRs: MOCK_HOTFIX_PRS }\n : null,\n numberOfHotfixes: env.isProduction\n ? { value: 2, unit: 'PRs', rating: 'high', target: 0, slowestPRs: MOCK_HOTFIX_PRS }\n : null,\n };\n }\n\n async getHistory(\n _projectSlug: string,\n env: DoraEnvironment,\n _days: number,\n ): Promise<DoraHistoryPoint[]> {\n await new Promise(r => setTimeout(r, 400));\n\n return MOCK_HISTORY.map(point => ({\n ...point,\n changeFailureRate: env.isProduction ? point.changeFailureRate : undefined,\n mttrHours: env.isProduction ? point.mttrHours : undefined,\n }));\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +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
|
|
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 /**\n * Raw branch pattern as configured — a single branch name, a comma-separated\n * list (\"main,master\"), or a JavaScript regex string (\"^(main|master)$\").\n * The client resolves this to a concrete list of branches at query time.\n */\n branch: string;\n isProduction: boolean;\n label: string;\n}\n\nexport interface DoraTargets {\n deploymentFrequency: number;\n leadTime: number;\n changeFailureRate: number;\n mttr: number;\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 getTargets(): DoraTargets;\n getMetrics(\n projectSlug: string,\n env: DoraEnvironment,\n days: number,\n targetsOverride?: Partial<DoraTargets>,\n ): Promise<DoraMetrics>;\n getHistory(\n projectSlug: string,\n env: DoraEnvironment,\n days: number,\n ): Promise<DoraHistoryPoint[]>;\n}\n\nexport const doraMetricsApiRef = createApiRef<DoraMetricsApi>({\n id: 'plugin.dora-metrics.service',\n});\n"],"names":[],"mappings":";;AA8EO,MAAM,oBAAoB,YAAA,CAA6B;AAAA,EAC5D,EAAA,EAAI;AACN,CAAC;;;;"}
|