@c2l2c/backstage-plugin-dora-metrics 0.4.0 → 0.4.1

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.
@@ -0,0 +1,5 @@
1
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
2
+
3
+ declare const doraMetricsPlugin: _backstage_frontend_plugin_api.FrontendPlugin<{}, {}, {}>;
4
+
5
+ export { doraMetricsPlugin as default };
@@ -0,0 +1,2 @@
1
+ export { doraMetricsPlugin as default } from './plugin.esm.js';
2
+ //# sourceMappingURL=alpha.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alpha.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,259 @@
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
+ function isRegexPattern(pattern) {
31
+ return /[|^$[\](){}?+*\\]/.test(pattern);
32
+ }
33
+ function matchesLabel(prLabels, labelPattern) {
34
+ if (!labelPattern) return false;
35
+ const names = prLabels.map((l) => l.name);
36
+ if (isRegexPattern(labelPattern)) {
37
+ const regex = new RegExp(labelPattern, "i");
38
+ return names.some((n) => regex.test(n));
39
+ }
40
+ const patterns = labelPattern.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean);
41
+ return names.some((n) => patterns.includes(n.toLowerCase()));
42
+ }
43
+ async function resolveBranches(owner, repo, branchPattern, fetchFn) {
44
+ if (isRegexPattern(branchPattern)) {
45
+ const repoBranches = await fetchFn(
46
+ `https://api.github.com/repos/${owner}/${repo}/branches?per_page=100`
47
+ );
48
+ const regex = new RegExp(branchPattern);
49
+ return repoBranches.map((b) => b.name).filter((name) => regex.test(name));
50
+ }
51
+ return branchPattern.split(",").map((b) => b.trim()).filter(Boolean);
52
+ }
53
+ class DoraMetricsClient {
54
+ githubAuthApi;
55
+ configApi;
56
+ constructor(githubAuthApi, configApi) {
57
+ this.githubAuthApi = githubAuthApi;
58
+ this.configApi = configApi;
59
+ }
60
+ getConfig() {
61
+ const appCfg = this.configApi.getConfig("app");
62
+ const cfg = appCfg.getOptionalConfig("doraMetrics");
63
+ const rawEnvs = cfg?.getOptionalConfigArray("environments") ?? [];
64
+ const environments = rawEnvs.map((e) => ({
65
+ name: e.getString("name"),
66
+ branch: e.getString("branch"),
67
+ isProduction: e.getOptionalBoolean("isProduction") ?? false,
68
+ label: e.getOptionalString("label") ?? "hotfix"
69
+ }));
70
+ const prodEnvs = environments.filter((e) => e.isProduction);
71
+ if (prodEnvs.length > 1) {
72
+ throw new Error(
73
+ `DORA metrics config error: only one environment may have isProduction: true, but found ${prodEnvs.length}: ${prodEnvs.map((e) => e.name).join(", ")}`
74
+ );
75
+ }
76
+ const initialDays = cfg?.getOptionalConfig("collection")?.getOptionalNumber("initialDays") ?? 30;
77
+ const targetsCfg = cfg?.getOptionalConfig("targets");
78
+ const targets = {
79
+ deploymentFrequency: targetsCfg?.getOptionalNumber("deploymentFrequency") ?? 7,
80
+ leadTime: targetsCfg?.getOptionalNumber("leadTime") ?? 24,
81
+ changeFailureRate: targetsCfg?.getOptionalNumber("changeFailureRate") ?? 15,
82
+ mttr: targetsCfg?.getOptionalNumber("mttr") ?? 1
83
+ };
84
+ return { environments, initialDays, targets };
85
+ }
86
+ getEnvironments() {
87
+ return this.getConfig().environments;
88
+ }
89
+ getDefaultDays() {
90
+ return this.getConfig().initialDays;
91
+ }
92
+ getTargets() {
93
+ return this.getConfig().targets;
94
+ }
95
+ async fetchWithAuth(url, token) {
96
+ const response = await fetch(url, {
97
+ headers: {
98
+ Authorization: `token ${token}`,
99
+ Accept: "application/vnd.github.v3+json"
100
+ }
101
+ });
102
+ if (!response.ok) {
103
+ throw new Error(
104
+ `GitHub API request failed: ${response.status} ${response.statusText} \u2014 ${url}`
105
+ );
106
+ }
107
+ return response.json();
108
+ }
109
+ /**
110
+ * Fetch all merged PRs for an environment, resolving branch patterns and
111
+ * deduplicating across multiple branches by PR number.
112
+ */
113
+ async fetchMergedPRs(owner, repo, env, cutoff, token) {
114
+ const boundFetch = (url) => this.fetchWithAuth(url, token);
115
+ const branches = await resolveBranches(owner, repo, env.branch, boundFetch);
116
+ const allByNumber = /* @__PURE__ */ new Map();
117
+ await Promise.all(
118
+ branches.map(async (branch) => {
119
+ let hasMore = true;
120
+ for (let page = 1; hasMore; page++) {
121
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed&base=${encodeURIComponent(branch)}&per_page=100&page=${page}&sort=updated&direction=desc`;
122
+ const prs = await boundFetch(url);
123
+ for (const pr of prs) {
124
+ if (pr.merged_at && new Date(pr.merged_at) >= cutoff) {
125
+ if (!allByNumber.has(pr.number)) {
126
+ allByNumber.set(pr.number, pr);
127
+ }
128
+ }
129
+ }
130
+ const oldestUpdatedMs = prs.length > 0 ? Math.min(...prs.map((pr) => new Date(pr.updated_at).getTime())) : 0;
131
+ hasMore = prs.length >= 100 && oldestUpdatedMs >= cutoff.getTime();
132
+ }
133
+ })
134
+ );
135
+ return Array.from(allByNumber.values());
136
+ }
137
+ async getMetrics(projectSlug, env, days, targetsOverride) {
138
+ const token = await this.githubAuthApi.getAccessToken("repo");
139
+ const { targets: configTargets } = this.getConfig();
140
+ const targets = { ...configTargets, ...targetsOverride };
141
+ const [owner, repo] = projectSlug.split("/");
142
+ if (!owner || !repo) {
143
+ throw new Error(`Invalid project slug "${projectSlug}". Expected "owner/repo".`);
144
+ }
145
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
146
+ const mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);
147
+ const deploymentsPerWeek = days > 0 ? mergedPRs.length / days * 7 : 0;
148
+ const deploymentFrequency = {
149
+ value: Math.round(deploymentsPerWeek * 100) / 100,
150
+ unit: "per week",
151
+ rating: computeRating(deploymentsPerWeek, targets.deploymentFrequency, false),
152
+ target: targets.deploymentFrequency,
153
+ // All merged PRs sorted newest-first — used in expanded view date table
154
+ slowestPRs: [...mergedPRs].sort((a, b) => new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime()).map((pr) => toPrDetail(
155
+ pr,
156
+ (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
157
+ ))
158
+ };
159
+ const prsWithLeadTime = mergedPRs.map((pr) => ({
160
+ pr,
161
+ durationHours: (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
162
+ })).sort((a, b) => b.durationHours - a.durationHours);
163
+ const avgLeadTimeHours = prsWithLeadTime.length > 0 ? prsWithLeadTime.reduce((sum, p) => sum + p.durationHours, 0) / prsWithLeadTime.length : 0;
164
+ const leadTime = {
165
+ value: Math.round(avgLeadTimeHours * 10) / 10,
166
+ unit: "hours",
167
+ rating: computeRating(avgLeadTimeHours, targets.leadTime, true),
168
+ target: targets.leadTime,
169
+ slowestPRs: prsWithLeadTime.slice(0, 10).map((p) => toPrDetail(p.pr, p.durationHours))
170
+ };
171
+ if (!env.isProduction) {
172
+ return {
173
+ deploymentFrequency,
174
+ leadTime,
175
+ changeFailureRate: null,
176
+ mttr: null,
177
+ numberOfHotfixes: null
178
+ };
179
+ }
180
+ const hotfixPRs = mergedPRs.filter(
181
+ (pr) => matchesLabel(pr.labels ?? [], env.label)
182
+ );
183
+ const numberOfHotfixes = {
184
+ value: hotfixPRs.length,
185
+ unit: "PRs",
186
+ rating: computeRating(hotfixPRs.length, 0, true),
187
+ target: 0,
188
+ slowestPRs: [...hotfixPRs].sort((a, b) => new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime()).map((pr) => toPrDetail(
189
+ pr,
190
+ (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
191
+ ))
192
+ };
193
+ const changeFailureRatePct = mergedPRs.length > 0 ? hotfixPRs.length / mergedPRs.length * 100 : 0;
194
+ const changeFailureRate = {
195
+ value: Math.round(changeFailureRatePct * 10) / 10,
196
+ unit: "%",
197
+ rating: computeRating(changeFailureRatePct, targets.changeFailureRate, true),
198
+ target: targets.changeFailureRate
199
+ };
200
+ const hotfixWithDuration = hotfixPRs.map((pr) => ({
201
+ pr,
202
+ durationHours: (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
203
+ })).sort((a, b) => b.durationHours - a.durationHours);
204
+ const avgMttrHours = hotfixWithDuration.length > 0 ? hotfixWithDuration.reduce((sum, p) => sum + p.durationHours, 0) / hotfixWithDuration.length : 0;
205
+ const mttr = {
206
+ value: Math.round(avgMttrHours * 10) / 10,
207
+ unit: "hours",
208
+ rating: computeRating(avgMttrHours, targets.mttr, true),
209
+ target: targets.mttr,
210
+ slowestPRs: hotfixWithDuration.slice(0, 10).map((p) => toPrDetail(p.pr, p.durationHours))
211
+ };
212
+ return { deploymentFrequency, leadTime, changeFailureRate, mttr, numberOfHotfixes };
213
+ }
214
+ async getHistory(projectSlug, env, days) {
215
+ const token = await this.githubAuthApi.getAccessToken("repo");
216
+ const [owner, repo] = projectSlug.split("/");
217
+ if (!owner || !repo) throw new Error(`Invalid project slug "${projectSlug}".`);
218
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
219
+ const mergedPRs = await this.fetchMergedPRs(owner, repo, env, cutoff, token);
220
+ const bucketDays = Math.max(1, Math.round(days / 7));
221
+ const numBuckets = Math.max(2, Math.ceil(days / bucketDays));
222
+ const buckets = [];
223
+ for (let w = numBuckets - 1; w >= 0; w--) {
224
+ const weekEnd = new Date(Date.now() - w * bucketDays * 24 * 60 * 60 * 1e3);
225
+ const weekStart = new Date(weekEnd.getTime() - bucketDays * 24 * 60 * 60 * 1e3);
226
+ const weekPRs = mergedPRs.filter((pr) => {
227
+ const t = new Date(pr.merged_at).getTime();
228
+ return t >= weekStart.getTime() && t < weekEnd.getTime();
229
+ });
230
+ const leadTimes = weekPRs.map(
231
+ (pr) => (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
232
+ );
233
+ const avgLeadTime = leadTimes.length > 0 ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0;
234
+ let changeFailureRate;
235
+ let mttrHours;
236
+ if (env.isProduction) {
237
+ const hotfixPRs = weekPRs.filter((pr) => matchesLabel(pr.labels, env.label));
238
+ changeFailureRate = weekPRs.length > 0 ? Math.round(hotfixPRs.length / weekPRs.length * 1e3) / 10 : 0;
239
+ const hotfixDurations = hotfixPRs.map(
240
+ (pr) => (new Date(pr.merged_at).getTime() - new Date(pr.created_at).getTime()) / (1e3 * 60 * 60)
241
+ );
242
+ mttrHours = hotfixDurations.length > 0 ? Math.round(hotfixDurations.reduce((a, b) => a + b, 0) / hotfixDurations.length * 10) / 10 : 0;
243
+ }
244
+ const labelDate = new Date(Math.min(weekEnd.getTime(), Date.now()));
245
+ buckets.push({
246
+ weekLabel: labelDate.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
247
+ bucketMidMs: Math.round((weekStart.getTime() + weekEnd.getTime()) / 2),
248
+ deploymentCount: weekPRs.length,
249
+ leadTimeHours: Math.round(avgLeadTime * 10) / 10,
250
+ changeFailureRate,
251
+ mttrHours
252
+ });
253
+ }
254
+ return buckets;
255
+ }
256
+ }
257
+
258
+ export { DoraMetricsClient };
259
+ //# 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 DoraTargets,\n PrDetail,\n} from './types';\n\ninterface GitHubPR {\n number: number;\n title: string;\n html_url: string;\n created_at: string;\n updated_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 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 * Return true if any of the PR's labels match the label pattern.\n *\n * Supports the same three formats as branch patterns:\n * - Single name: \"hotfix\"\n * - Comma-separated: \"hotfix,fix,bug\"\n * - JavaScript regex: \"^(hotfix|fix)$\"\n */\nfunction matchesLabel(prLabels: Array<{ name: string }>, labelPattern: string): boolean {\n if (!labelPattern) return false;\n const names = prLabels.map(l => l.name);\n if (isRegexPattern(labelPattern)) {\n const regex = new RegExp(labelPattern, 'i');\n return names.some(n => regex.test(n));\n }\n const patterns = labelPattern.split(',').map(l => l.trim().toLowerCase()).filter(Boolean);\n return names.some(n => patterns.includes(n.toLowerCase()));\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 let hasMore = true;\n for (let page = 1; hasMore; page++) {\n const url =\n `https://api.github.com/repos/${owner}/${repo}/pulls` +\n `?state=closed&base=${encodeURIComponent(branch)}&per_page=100&page=${page}&sort=updated&direction=desc`;\n const prs = await boundFetch<GitHubPR[]>(url);\n\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\n if (!allByNumber.has(pr.number)) {\n allByNumber.set(pr.number, pr);\n }\n }\n }\n\n // PRs are sorted by updated_at desc. Since merged_at <= updated_at,\n // once the oldest updated_at on a page is before cutoff, no subsequent\n // page can contain a PR merged within the window — stop early.\n const oldestUpdatedMs = prs.length > 0\n ? Math.min(...prs.map(pr => new Date(pr.updated_at).getTime()))\n : 0;\n hasMore = prs.length >= 100 && oldestUpdatedMs >= cutoff.getTime();\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 matchesLabel(pr.labels ?? [], 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 => matchesLabel(pr.labels, 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 // Label with bucket end (capped at now) so all views show \"today\" as the last label.\n const labelDate = new Date(Math.min(weekEnd.getTime(), Date.now()));\n buckets.push({\n weekLabel: labelDate.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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"}
@@ -0,0 +1,20 @@
1
+ function niceHourTicks(maxHours) {
2
+ if (maxHours <= 0) return [0, 4, 8, 12, 16];
3
+ const steps = [0.25, 0.5, 1, 2, 4, 6, 8, 12, 24, 48, 72, 96, 168];
4
+ const step = steps.find((s) => Math.ceil(maxHours / s) <= 6) ?? 168;
5
+ const top = Math.ceil(maxHours / step) * step;
6
+ const ticks = [];
7
+ for (let v = 0; v <= top + 1e-3; v += step) ticks.push(v);
8
+ return ticks;
9
+ }
10
+ function niceCountTicks(maxCount) {
11
+ if (maxCount <= 0) return [0, 1, 2, 3, 4, 5];
12
+ const step = Math.max(1, Math.ceil(maxCount / 5));
13
+ const top = Math.ceil(maxCount / step) * step;
14
+ const ticks = [];
15
+ for (let v = 0; v <= top + 1e-3; v += step) ticks.push(v);
16
+ return ticks;
17
+ }
18
+
19
+ export { niceCountTicks, niceHourTicks };
20
+ //# sourceMappingURL=chartUtils.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chartUtils.esm.js","sources":["../../src/api/chartUtils.ts"],"sourcesContent":["/**\n * Generate clean hour-interval Y-axis ticks for time-based DORA charts.\n * Picks the smallest interval from a fixed set that keeps tick count ≤ 6.\n */\nexport function niceHourTicks(maxHours: number): number[] {\n if (maxHours <= 0) return [0, 4, 8, 12, 16];\n const steps = [0.25, 0.5, 1, 2, 4, 6, 8, 12, 24, 48, 72, 96, 168];\n const step = steps.find(s => Math.ceil(maxHours / s) <= 6) ?? 168;\n const top = Math.ceil(maxHours / step) * step;\n const ticks: number[] = [];\n for (let v = 0; v <= top + 0.001; v += step) ticks.push(v);\n return ticks;\n}\n\n/**\n * Generate integer Y-axis ticks for count-based DORA charts.\n * Picks a step size that keeps tick count ≤ 6, minimum step of 1.\n */\nexport function niceCountTicks(maxCount: number): number[] {\n if (maxCount <= 0) return [0, 1, 2, 3, 4, 5];\n const step = Math.max(1, Math.ceil(maxCount / 5));\n const top = Math.ceil(maxCount / step) * step;\n const ticks: number[] = [];\n for (let v = 0; v <= top + 0.001; v += step) ticks.push(v);\n return ticks;\n}\n"],"names":[],"mappings":"AAIO,SAAS,cAAc,QAAA,EAA4B;AACxD,EAAA,IAAI,QAAA,IAAY,GAAG,OAAO,CAAC,GAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAC1C,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,EAAM,GAAA,EAAK,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,IAAI,GAAG,CAAA;AAChE,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,CAAA,CAAA,KAAK,IAAA,CAAK,KAAK,QAAA,GAAW,CAAC,CAAA,IAAK,CAAC,CAAA,IAAK,GAAA;AAC9D,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,IAAA,CAAK,QAAA,GAAW,IAAI,CAAA,GAAI,IAAA;AACzC,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,IAAK,GAAA,GAAM,MAAO,CAAA,IAAK,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA;AACzD,EAAA,OAAO,KAAA;AACT;AAMO,SAAS,eAAe,QAAA,EAA4B;AACzD,EAAA,IAAI,QAAA,IAAY,GAAG,OAAO,CAAC,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAC3C,EAAA,MAAM,IAAA,GAAO,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,IAAA,CAAK,QAAA,GAAW,CAAC,CAAC,CAAA;AAChD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,IAAA,CAAK,QAAA,GAAW,IAAI,CAAA,GAAI,IAAA;AACzC,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,IAAK,GAAA,GAAM,MAAO,CAAA,IAAK,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA;AACzD,EAAA,OAAO,KAAA;AACT;;;;"}
@@ -0,0 +1,8 @@
1
+ import { createApiRef } from '@backstage/core-plugin-api';
2
+
3
+ const doraMetricsApiRef = createApiRef({
4
+ id: "plugin.dora-metrics.service"
5
+ });
6
+
7
+ export { doraMetricsApiRef };
8
+ //# sourceMappingURL=types.esm.js.map
@@ -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 /**\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;;;;"}