Skip to content

Commit 094088f

Browse files
authored
feat: extend enricher to cover extra fields [CM-1220] (#4186)
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 9867df3 commit 094088f

8 files changed

Lines changed: 783 additions & 21 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_policy_enabled boolean;
2+
ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_file_enabled boolean;
3+
ALTER TABLE repos ADD COLUMN IF NOT EXISTS snapshot_at timestamptz;
4+
5+
CREATE TABLE IF NOT EXISTS repo_activity_snapshot (
6+
repo_id bigint PRIMARY KEY REFERENCES repos(id) ON DELETE CASCADE,
7+
snapshot_at timestamptz NOT NULL,
8+
window_months int NOT NULL DEFAULT 12,
9+
-- commit activity
10+
commits_last_12m int,
11+
commits_last_6m int,
12+
commits_prior_6m int,
13+
-- PR health
14+
prs_opened_last_12m int,
15+
prs_merged_last_12m int,
16+
prs_closed_unmerged_12m int,
17+
pr_median_time_to_merge_hours int,
18+
pr_median_time_to_first_response_hours int,
19+
-- issue health
20+
issues_opened_last_12m int,
21+
issues_closed_last_12m int,
22+
issues_opened_last_6m int,
23+
issues_opened_prior_6m int,
24+
issues_open_now int,
25+
issue_median_time_to_close_hours int,
26+
issue_median_time_to_first_response_hours int
27+
);
28+
29+
CREATE INDEX IF NOT EXISTS repo_activity_snapshot_snapshot_at_idx
30+
ON repo_activity_snapshot (snapshot_at);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const MS_PER_HOUR = 1000 * 60 * 60
2+
3+
function median(values: number[]): number | null {
4+
if (values.length === 0) return null
5+
const sorted = [...values].sort((a, b) => a - b)
6+
const middleIndex = Math.floor(sorted.length / 2)
7+
return sorted.length % 2 === 0
8+
? (sorted[middleIndex - 1] + sorted[middleIndex]) / 2
9+
: sorted[middleIndex]
10+
}
11+
12+
function hoursBetween(startDate: string, endDate: string): number {
13+
return (new Date(endDate).getTime() - new Date(startDate).getTime()) / MS_PER_HOUR
14+
}
15+
16+
function toIntHours(hours: number | null): number | null {
17+
return hours != null ? Math.round(hours) : null
18+
}
19+
20+
// Shapes mirror exactly what GitHub GraphQL returns for comments/reviews nodes
21+
export interface ResponseNode {
22+
createdAt: string
23+
author: { login: string } | null
24+
}
25+
26+
export interface PrNode {
27+
createdAt: string
28+
mergedAt: string | null
29+
author: { login: string } | null
30+
comments: { nodes: ResponseNode[] }
31+
reviews: { nodes: ResponseNode[] }
32+
}
33+
34+
export interface IssueNode {
35+
createdAt: string
36+
closedAt: string | null
37+
author: { login: string } | null
38+
comments: { nodes: ResponseNode[] }
39+
}
40+
41+
function firstNonAuthorResponseHours(
42+
itemCreatedAt: string,
43+
authorLogin: string | null,
44+
responses: ResponseNode[],
45+
): number | null {
46+
const firstResponse = responses.find(
47+
(response) => response.author?.login && response.author.login !== authorLogin,
48+
)
49+
return firstResponse ? hoursBetween(itemCreatedAt, firstResponse.createdAt) : null
50+
}
51+
52+
export function computePrMedians(prs: PrNode[]): {
53+
medianTimeToMergeHours: number | null
54+
medianTimeToFirstResponseHours: number | null
55+
} {
56+
const mergeHours: number[] = []
57+
const firstResponseHours: number[] = []
58+
59+
for (const pr of prs) {
60+
if (pr.mergedAt != null) {
61+
mergeHours.push(hoursBetween(pr.createdAt, pr.mergedAt))
62+
}
63+
64+
const allResponses = [...pr.comments.nodes, ...pr.reviews.nodes].sort(
65+
(left, right) => new Date(left.createdAt).getTime() - new Date(right.createdAt).getTime(),
66+
)
67+
const responseHours = firstNonAuthorResponseHours(
68+
pr.createdAt,
69+
pr.author?.login ?? null,
70+
allResponses,
71+
)
72+
if (responseHours != null) firstResponseHours.push(responseHours)
73+
}
74+
75+
return {
76+
medianTimeToMergeHours: toIntHours(median(mergeHours)),
77+
medianTimeToFirstResponseHours: toIntHours(median(firstResponseHours)),
78+
}
79+
}
80+
81+
export function computeIssueMedians(issues: IssueNode[]): {
82+
medianTimeToCloseHours: number | null
83+
medianTimeToFirstResponseHours: number | null
84+
} {
85+
const closeHours: number[] = []
86+
const firstResponseHours: number[] = []
87+
88+
for (const issue of issues) {
89+
if (issue.closedAt != null) {
90+
closeHours.push(hoursBetween(issue.createdAt, issue.closedAt))
91+
}
92+
93+
const responseHours = firstNonAuthorResponseHours(
94+
issue.createdAt,
95+
issue.author?.login ?? null,
96+
issue.comments.nodes,
97+
)
98+
if (responseHours != null) firstResponseHours.push(responseHours)
99+
}
100+
101+
return {
102+
medianTimeToCloseHours: toIntHours(median(closeHours)),
103+
medianTimeToFirstResponseHours: toIntHours(median(firstResponseHours)),
104+
}
105+
}

0 commit comments

Comments
 (0)