Skip to content

Commit efaafb8

Browse files
committed
webserver: gh, better callout
1 parent c08a41d commit efaafb8

4 files changed

Lines changed: 47 additions & 33 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,17 @@ Analyze all PRs across an entire organization:
9595
prcost --org chainguard-dev
9696

9797
# Custom sampling parameters
98-
prcost --org myorg --samples 100 --days 60
98+
prcost --org myorg --samples 50 --days 60
9999
```
100100

101101
### Sampling Strategy
102102

103103
Repository and organization modes use time-bucket sampling to ensure even distribution across the time period. This provides more representative estimates than random sampling by avoiding temporal clustering.
104104

105+
**Sample Size Options:**
106+
- **25 samples** (default): Fast analysis with ±20% confidence interval
107+
- **50 samples**: Slower but more accurate with ±14% confidence interval (1.4× better accuracy)
108+
105109
## Library Usage
106110

107111
All functionality is available as reusable library packages:

cmd/prcost/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func main() {
3030
// Org/Repo sampling flags
3131
org := flag.String("org", "", "GitHub organization to analyze (optionally with --repo for single repo)")
3232
repo := flag.String("repo", "", "GitHub repository to analyze (requires --org)")
33-
samples := flag.Int("samples", 20, "Number of PRs to sample for extrapolation")
33+
samples := flag.Int("samples", 25, "Number of PRs to sample for extrapolation (25=fast/±20%, 50=slower/±14%)")
3434
days := flag.Int("days", 90, "Number of days to look back for PR modifications")
3535

3636
flag.Usage = func() {
@@ -52,7 +52,7 @@ func main() {
5252
fmt.Fprintf(os.Stderr, " %s --org myorg --repo myrepo --samples 50 --days 30\n\n", os.Args[0])
5353
fmt.Fprint(os.Stderr, " Organization-wide analysis:\n")
5454
fmt.Fprintf(os.Stderr, " %s --org chainguard-dev\n", os.Args[0])
55-
fmt.Fprintf(os.Stderr, " %s --org myorg --samples 100 --days 60\n", os.Args[0])
55+
fmt.Fprintf(os.Stderr, " %s --org myorg --samples 50 --days 60\n", os.Args[0])
5656
}
5757

5858
flag.Parse()

internal/server/server.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http"
1414
"net/url"
1515
"os"
16+
"os/exec"
1617
"regexp"
1718
"strings"
1819
"sync"
@@ -678,6 +679,23 @@ func (s *Server) token(ctx context.Context) string {
678679
return token
679680
}
680681

682+
// Try gh auth token if gh is in PATH
683+
if ghPath, err := exec.LookPath("gh"); err == nil {
684+
s.logger.InfoContext(ctx, "Found gh CLI in PATH", "path", ghPath)
685+
cmd := exec.CommandContext(ctx, "gh", "auth", "token")
686+
output, err := cmd.Output()
687+
if err == nil {
688+
token := strings.TrimSpace(string(output))
689+
if token != "" {
690+
s.logger.InfoContext(ctx, "Using GITHUB_TOKEN from gh auth token")
691+
s.fallbackToken = token
692+
return token
693+
}
694+
} else {
695+
s.logger.WarnContext(ctx, "Failed to get token from gh auth token", errorKey, err)
696+
}
697+
}
698+
681699
// Try Google Secret Manager for GITHUB_TOKEN
682700
token, err := gsm.Fetch(ctx, "GITHUB_TOKEN")
683701
if err != nil {
@@ -691,7 +709,7 @@ func (s *Server) token(ctx context.Context) string {
691709
return token
692710
}
693711

694-
s.logger.WarnContext(ctx, "No fallback GitHub token found (tried GITHUB_TOKEN env and GITHUB_TOKEN GSM)")
712+
s.logger.WarnContext(ctx, "No fallback GitHub token found (tried GITHUB_TOKEN env, gh auth token, and GITHUB_TOKEN GSM)")
695713
return ""
696714
}
697715

internal/server/static/index.html

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -988,10 +988,9 @@ <h1>PR Cost Calculator</h1>
988988
id="repoSampleSize"
989989
value="25"
990990
min="1"
991-
max="25"
992-
disabled
991+
max="50"
993992
>
994-
<div class="help-text">Fixed at 25 PRs</div>
993+
<div class="help-text">25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</div>
995994
</div>
996995
<div class="form-group">
997996
<label for="repoDays">Days Back</label>
@@ -1029,10 +1028,9 @@ <h1>PR Cost Calculator</h1>
10291028
id="orgSampleSize"
10301029
value="25"
10311030
min="1"
1032-
max="25"
1033-
disabled
1031+
max="50"
10341032
>
1035-
<div class="help-text">Fixed at 25 PRs</div>
1033+
<div class="help-text">25 (fast, ±20% accuracy) or 50 (slower, ±14% accuracy)</div>
10361034
</div>
10371035
<div class="form-group">
10381036
<label for="orgDays">Days Back</label>
@@ -1301,33 +1299,27 @@ <h3>Why calculate PR costs?</h3>
13011299
savings -= pricingCost;
13021300

13031301
if (savings > 0) {
1304-
// Format savings without decimals
1305-
const savingsRounded = Math.round(savings);
1306-
1307-
// Format savings in millions or thousands
1308-
let savingsFormatted;
1309-
if (savingsRounded >= 1000000) {
1310-
const millions = (savingsRounded / 1000000).toFixed(1);
1311-
savingsFormatted = '$' + millions + 'M';
1312-
} else if (savingsRounded >= 1000) {
1313-
const thousands = Math.round(savingsRounded / 1000);
1314-
savingsFormatted = '$' + thousands + 'K';
1315-
} else {
1316-
savingsFormatted = '$' + savingsRounded.toLocaleString('en-US');
1317-
}
1302+
// Calculate average waste per PR
1303+
const avgWasteHoursPerPR = extPreventableHours / e.total_prs;
1304+
const avgWasteCostPerPR = extPreventableCost / e.total_prs;
1305+
1306+
// Estimate PRs per author: total PRs in period / unique users in sample
1307+
const daysInPeriod = parseInt(days);
1308+
const prsPerPersonInPeriod = e.total_prs / userCount;
1309+
const prsPerPersonPerYear = prsPerPersonInPeriod * (365 / daysInPeriod);
13181310

1319-
// Calculate headcount savings (fully-loaded salary)
1320-
const fullyLoadedSalary = salary * benefitsMultiplier;
1321-
const headcountSavings = Math.round(savings / fullyLoadedSalary);
1311+
// Calculate annual waste per author
1312+
const annualWasteHoursPerAuthor = avgWasteHoursPerPR * prsPerPersonPerYear;
1313+
const annualWasteCostPerAuthor = avgWasteCostPerPR * prsPerPersonPerYear;
13221314

1323-
// Use sourceName if available, otherwise default to "you"
1324-
const targetName = sourceName || 'you';
1315+
// Weekly waste per author
1316+
const weeklyWastePerAuthor = annualWasteHoursPerAuthor / 52;
1317+
const hoursPerWeek = weeklyWastePerAuthor.toFixed(1);
1318+
const costPerPersonFormatted = '$' + Math.round(annualWasteCostPerAuthor).toLocaleString('en-US');
13251319

13261320
html += '<div class="efficiency-callout">';
1327-
html += "<p style=\"font-size: 20px; font-weight: 600; margin-bottom: 20px; line-height: 1.4;\">Your eng team isn't slow. Your process is.</p>";
1328-
html += `<p style="font-size: 17px; margin-bottom: 16px;">codeGROOVE can save <strong>${targetName}</strong> <strong>${savingsFormatted}+ annually</strong>—that's <strong>${headcountSavings} senior engineers</strong> worth of productivity back on your team.</p>`;
1329-
html += '<p style="font-size: 17px; margin-bottom: 20px;">We collapse review cycles so your team ships faster. <strong>ROI shows week one.</strong> Free forever for open-source projects.</p>';
1330-
html += '<p style="margin: 0;"><a href="mailto:go-faster@codeGROOVE.dev" style="font-size: 17px;">go-faster@codeGROOVE.dev</a></p>';
1321+
html += `<p style="font-size: 16px; margin-bottom: 16px; line-height: 1.5;">Your devs lose <strong>${hoursPerWeek} hours/week</strong> waiting on code reviews. That's <strong>${costPerPersonFormatted}/year per person</strong>. Ready-to-Review fixes this.</p>`;
1322+
html += '<p style="font-size: 16px; margin-bottom: 0; line-height: 1.5;">Free for OSS, $5/dev/month for private. Beta begins November 3rd: <a href="mailto:beta@codeGROOVE.dev">beta@codeGROOVE.dev</a></p>';
13311323
html += '</div>';
13321324
}
13331325
}

0 commit comments

Comments
 (0)