Skip to content

Commit 6f368d9

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Improve alignment
1 parent bf67c56 commit 6f368d9

File tree

3 files changed

+94
-34
lines changed

3 files changed

+94
-34
lines changed

internal/server/server.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,7 +1765,7 @@ func (s *Server) processRepoSampleWithProgress(ctx context.Context, req *RepoSam
17651765
PR: 0,
17661766
Owner: req.Owner,
17671767
Repo: req.Repo,
1768-
Progress: "Querying GitHub for PRs...",
1768+
Progress: fmt.Sprintf("Querying GitHub GraphQL API for %s/%s PRs (last %d days)...", req.Owner, req.Repo, req.Days),
17691769
}))
17701770

17711771
// Start keep-alive to prevent client timeout during GraphQL query
@@ -1890,7 +1890,7 @@ func (s *Server) processOrgSampleWithProgress(ctx context.Context, req *OrgSampl
18901890
logSSEError(ctx, s.logger, sendSSE(writer, ProgressUpdate{
18911891
Type: "fetching",
18921892
PR: 0,
1893-
Progress: "Querying GitHub for PRs...",
1893+
Progress: fmt.Sprintf("Querying GitHub Search API for %s org PRs (last %d days)...", req.Org, req.Days),
18941894
}))
18951895

18961896
// Start keep-alive to prevent client timeout during GraphQL query
@@ -1965,29 +1965,14 @@ func (s *Server) processOrgSampleWithProgress(ctx context.Context, req *OrgSampl
19651965
// Count unique authors across all PRs (not just samples)
19661966
totalAuthors := github.CountUniqueAuthors(prs)
19671967

1968-
// Count open PRs across all unique repos in the organization
1969-
uniqueRepos := make(map[string]bool)
1970-
for _, pr := range prs {
1971-
repoKey := pr.Owner + "/" + pr.Repo
1972-
uniqueRepos[repoKey] = true
1973-
}
1974-
1975-
totalOpenPRs := 0
1976-
for repoKey := range uniqueRepos {
1977-
parts := strings.SplitN(repoKey, "/", 2)
1978-
if len(parts) != 2 {
1979-
continue
1980-
}
1981-
owner, repo := parts[0], parts[1]
1982-
//nolint:contextcheck // Using background context intentionally to prevent client timeout from canceling work
1983-
openCount, err := github.CountOpenPRsInRepo(workCtx, owner, repo, token)
1984-
if err != nil {
1985-
s.logger.WarnContext(ctx, "Failed to count open PRs for repo", "repo", repoKey, errorKey, err)
1986-
continue
1987-
}
1988-
totalOpenPRs += openCount
1968+
// Count open PRs across the entire organization with a single GraphQL query
1969+
//nolint:contextcheck // Using background context intentionally to prevent client timeout from canceling work
1970+
totalOpenPRs, err := github.CountOpenPRsInOrg(workCtx, req.Org, token)
1971+
if err != nil {
1972+
s.logger.WarnContext(ctx, "Failed to count open PRs for organization", "org", req.Org, errorKey, err)
1973+
totalOpenPRs = 0 // Continue with 0 if we can't get the count
19891974
}
1990-
s.logger.InfoContext(ctx, "Counted total open PRs across organization", "open_prs", totalOpenPRs, "repos", len(uniqueRepos))
1975+
s.logger.InfoContext(ctx, "Counted total open PRs across organization", "open_prs", totalOpenPRs, "org", req.Org)
19911976

19921977
// Extrapolate costs from samples
19931978
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)

internal/server/static/index.html

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,7 @@ <h3>Why calculate PR costs?</h3>
16241624
output += ` Automated Updates ${formatCurrency(avgAutomatedUpdatesCost).padStart(10)} ${formatTimeUnit(avgAutomatedUpdatesHours)} (${e.bot_prs} PRs)\n`;
16251625
}
16261626
if (avgPRTrackingCost > 0.01) {
1627-
output += ` PR Tracking ${formatCurrency(avgPRTrackingCost).padStart(10)} ${formatTimeUnit(avgPRTrackingHours)} (${e.open_prs} open PRs)\n`;
1627+
output += ` PR Tracking ${formatCurrency(avgPRTrackingCost).padStart(10)} ${formatTimeUnit(avgPRTrackingHours)} (${e.open_prs} open PRs)\n`;
16281628
}
16291629
const avgMergeDelayCost = avgDeliveryDelayCost + avgCodeChurnCost + avgAutomatedUpdatesCost + avgPRTrackingCost;
16301630
const avgMergeDelayHours = avgDeliveryDelayHours + avgCodeChurnHours + avgAutomatedUpdatesHours + avgPRTrackingHours;
@@ -1656,11 +1656,6 @@ <h3>Why calculate PR costs?</h3>
16561656
output += ` Subtotal ${formatCurrency(avgFutureCost).padStart(10)} ${formatTimeUnit(avgFutureHours)} (${pct.toFixed(1)}%)\n\n`;
16571657
}
16581658

1659-
// Weekly waste per PR author
1660-
if (e.waste_hours_per_author_per_week > 0 && e.total_authors > 0) {
1661-
output += ` Weekly waste per PR author: ${formatCurrency(e.waste_cost_per_author_per_week).padStart(10)} ${formatTimeUnit(e.waste_hours_per_author_per_week)} (${e.total_authors} authors)\n`;
1662-
}
1663-
16641659
// Average Preventable Loss Total (before grand total)
16651660
const avgPreventableCost = avgCodeChurnCost + avgDeliveryDelayCost + avgCodeChurnCost + (e.automated_updates_cost / totalPRs) + (e.pr_tracking_cost / totalPRs);
16661661
const avgPreventableHours = avgCodeChurnHours + avgDeliveryDelayHours + avgCodeChurnHours + (e.automated_updates_hours / totalPRs) + (e.pr_tracking_hours / totalPRs);
@@ -1693,7 +1688,7 @@ <h3>Why calculate PR costs?</h3>
16931688

16941689
// Show bot PR LOC even though cost is $0
16951690
if ((e.bot_prs || 0) > 0) {
1696-
output += ` Automated Updates ${formatTimeUnit(0)} (${e.bot_prs} PRs, ${botLOC.toFixed(1)}k LOC)\n`;
1691+
output += ` Automated Updates ${formatTimeUnit(0)} (${e.bot_prs} PRs, ${botLOC.toFixed(1)}k LOC)\n`;
16971692
}
16981693

16991694
output += ' ──────────\n';
@@ -1732,7 +1727,7 @@ <h3>Why calculate PR costs?</h3>
17321727
output += ` Automated Updates ${formatCurrency(e.automated_updates_cost).padStart(10)} ${formatTimeUnit(e.automated_updates_hours)} (${e.bot_prs || 0} PRs)\n`;
17331728
}
17341729
if ((e.pr_tracking_cost || 0) > 0) {
1735-
output += ` PR Tracking ${formatCurrency(e.pr_tracking_cost).padStart(10)} ${formatTimeUnit(e.pr_tracking_hours)} (${e.open_prs || 0} open PRs)\n`;
1730+
output += ` PR Tracking ${formatCurrency(e.pr_tracking_cost).padStart(10)} ${formatTimeUnit(e.pr_tracking_hours)} (${e.open_prs || 0} open PRs)\n`;
17361731
}
17371732

17381733
const mergeDelayCost = (e.delivery_delay_cost || 0) + (e.code_churn_cost || 0) + (e.automated_updates_cost || 0) + (e.pr_tracking_cost || 0);
@@ -1769,14 +1764,14 @@ <h3>Why calculate PR costs?</h3>
17691764

17701765
// Weekly waste per PR author
17711766
if ((e.waste_hours_per_author_per_week || 0) > 0 && (e.total_authors || 0) > 0) {
1772-
output += ` Weekly waste per PR author: ${formatCurrency(e.waste_cost_per_author_per_week).padStart(10)} ${formatTimeUnit(e.waste_hours_per_author_per_week)} (${e.total_authors} authors)\n`;
1767+
output += ` Weekly waste per PR author: ${formatCurrency(e.waste_cost_per_author_per_week).padStart(12)} ${formatTimeUnit(e.waste_hours_per_author_per_week)} (${e.total_authors} authors)\n`;
17731768
}
17741769

17751770
// Preventable Loss Total (before grand total)
17761771
const preventableCost = (e.code_churn_cost || 0) + (e.delivery_delay_cost || 0) + (e.automated_updates_cost || 0) + (e.pr_tracking_cost || 0);
17771772
const preventableHours = (e.code_churn_hours || 0) + (e.delivery_delay_hours || 0) + (e.automated_updates_hours || 0) + (e.pr_tracking_hours || 0);
17781773
const preventablePct = (preventableCost / e.total_cost) * 100;
1779-
output += ` Total Preventable Waste (${days} days): ${formatCurrency(preventableCost).padStart(10)} ${formatTimeUnit(preventableHours)} (${preventablePct.toFixed(1)}%)\n`;
1774+
output += ` Preventable Loss Total ${formatCurrency(preventableCost).padStart(10)} ${formatTimeUnit(preventableHours)} (${preventablePct.toFixed(1)}%)\n`;
17801775

17811776
// Total
17821777
output += ' ════════════════════════════════════════════════════\n';

pkg/github/query.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,83 @@ func CountOpenPRsInRepo(ctx context.Context, owner, repo, token string) (int, er
825825

826826
return count, nil
827827
}
828+
829+
// CountOpenPRsInOrg counts all open PRs across an entire GitHub organization with a single GraphQL query.
830+
// This is much more efficient than counting PRs repo-by-repo for organizations with many repositories.
831+
// Only counts PRs created more than 24 hours ago to exclude brand-new PRs.
832+
func CountOpenPRsInOrg(ctx context.Context, org, token string) (int, error) {
833+
// Only count PRs created more than 24 hours ago
834+
twentyFourHoursAgo := time.Now().Add(-24 * time.Hour).Format("2006-01-02T15:04:05Z")
835+
836+
query := `query($searchQuery: String!) {
837+
search(query: $searchQuery, type: ISSUE, first: 0) {
838+
issueCount
839+
}
840+
}`
841+
842+
// Search query: is:pr is:open org:orgname created:<date
843+
searchQuery := fmt.Sprintf("is:pr is:open org:%s created:<%s", org, twentyFourHoursAgo)
844+
845+
variables := map[string]any{
846+
"searchQuery": searchQuery,
847+
}
848+
849+
queryJSON, err := json.Marshal(map[string]any{
850+
"query": query,
851+
"variables": variables,
852+
})
853+
if err != nil {
854+
return 0, fmt.Errorf("failed to marshal query: %w", err)
855+
}
856+
857+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.github.com/graphql", bytes.NewBuffer(queryJSON))
858+
if err != nil {
859+
return 0, fmt.Errorf("failed to create request: %w", err)
860+
}
861+
862+
req.Header.Set("Authorization", "Bearer "+token)
863+
req.Header.Set("Content-Type", "application/json")
864+
865+
slog.Info("HTTP request starting",
866+
"method", "POST",
867+
"url", "https://api.github.com/graphql",
868+
"host", "api.github.com")
869+
870+
resp, err := http.DefaultClient.Do(req)
871+
if err != nil {
872+
return 0, fmt.Errorf("request failed: %w", err)
873+
}
874+
defer resp.Body.Close()
875+
876+
if resp.StatusCode != http.StatusOK {
877+
return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
878+
}
879+
880+
var result struct {
881+
Data struct {
882+
Search struct {
883+
IssueCount int `json:"issueCount"`
884+
} `json:"search"`
885+
} `json:"data"`
886+
Errors []struct {
887+
Message string
888+
}
889+
}
890+
891+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
892+
return 0, fmt.Errorf("failed to decode response: %w", err)
893+
}
894+
895+
if len(result.Errors) > 0 {
896+
return 0, fmt.Errorf("GraphQL error: %s", result.Errors[0].Message)
897+
}
898+
899+
count := result.Data.Search.IssueCount
900+
901+
slog.Info("Counted PRs open >24 hours in organization",
902+
"org", org,
903+
"open_prs", count,
904+
"filter", "created >24h ago")
905+
906+
return count, nil
907+
}

0 commit comments

Comments
 (0)