<# .SYNOPSIS Retrieves GitHub Copilot usage metrics per user for an organization. .DESCRIPTION This script calls the GitHub REST API to get Copilot metrics for an organization, showing usage per user to help identify who is actively using their premium seats. .PARAMETER Organization The GitHub organization name. .PARAMETER Token GitHub Personal Access Token with 'copilot', 'manage_billing:copilot', or 'read:org' scope. If not provided, will attempt to use GITHUB_TOKEN environment variable. .PARAMETER Days Number of days to look back (default: 28, max: 28). .PARAMETER ExportToExcel Optional path to export results to Excel file (e.g., "copilot-usage.xlsx"). .PARAMETER IncludeUserSeats Include individual user seat information (who has access and when they last used it). .EXAMPLE .\Get-CopilotUsage.ps1 -Organization "myorg" -Token "ghp_xxxxx" .EXAMPLE .\Get-CopilotUsage.ps1 -Organization "myorg" -Days 7 -ExportToExcel "copilot-usage.xlsx" .EXAMPLE .\Get-CopilotUsage.ps1 -Organization "myorg" -IncludeUserSeats -ExportToExcel "copilot-usage.xlsx" .NOTES API Documentation: https://docs.github.com/en/rest/copilot/copilot-metrics Required Token Scopes: 'copilot', 'manage_billing:copilot', or 'read:org' This script requires the ImportExcel module. Install it with: Install-Module -Name ImportExcel #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Organization, [Parameter(Mandatory = $false)] [string]$Token, [Parameter(Mandatory = $false)] [ValidateRange(1, 28)] [int]$Days = 28, [Parameter(Mandatory = $false)] [string]$ExportToExcel, [Parameter(Mandatory = $false)] [switch]$IncludeUserSeats ) # Check if ImportExcel module is available when export is requested if (-not [string]::IsNullOrEmpty($ExportToExcel)) { if (-not (Get-Module -ListAvailable -Name ImportExcel)) { Write-Warning "ImportExcel module is not installed. Installing it now..." try { Install-Module -Name ImportExcel -Scope CurrentUser -Force -AllowClobber Write-Host "ImportExcel module installed successfully." -ForegroundColor Green } catch { Write-Error "Failed to install ImportExcel module. Please install it manually: Install-Module -Name ImportExcel" Write-Host "Continuing without Excel export..." -ForegroundColor Yellow $ExportToExcel = "" } } if (-not [string]::IsNullOrEmpty($ExportToExcel)) { Import-Module ImportExcel } } # Use environment variable if token not provided if ([string]::IsNullOrEmpty($Token)) { $Token = $env:GITHUB_TOKEN if ([string]::IsNullOrEmpty($Token)) { Write-Error "GitHub token not provided. Use -Token parameter or set GITHUB_TOKEN environment variable." exit 1 } } # API endpoint $apiUrl = "https://api.github.com/orgs/$Organization/copilot/metrics" # Headers for authentication $headers = @{ "Accept" = "application/vnd.github+json" "Authorization" = "Bearer $Token" "X-GitHub-Api-Version" = "2022-11-28" } Write-Host "Fetching Copilot usage metrics for organization: $Organization" -ForegroundColor Cyan Write-Host "Time period: Last $Days days" -ForegroundColor Cyan Write-Host "" # Variables to store data $dailySummary = @() $languageSummary = @() $editorSummary = @() $overallSummary = $null $userSeats = @() try { # Calculate date range $since = (Get-Date).AddDays(-$Days).ToString("yyyy-MM-dd") $until = (Get-Date).ToString("yyyy-MM-dd") # Add query parameters $uri = "$apiUrl`?since=$since&until=$until" # Make API request $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get if ($null -eq $response -or $response.Count -eq 0) { Write-Warning "No usage data found for the specified period." exit 0 } # Process and display results $userMetrics = @() foreach ($dayMetric in $response) { $date = $dayMetric.date if ($null -ne $dayMetric.copilot_ide_code_completions) { foreach ($editor in $dayMetric.copilot_ide_code_completions.editors) { foreach ($model in $editor.models) { foreach ($language in $model.languages) { $userMetrics += [PSCustomObject]@{ Date = $date Editor = $editor.name Model = $model.name Language = $language.name TotalEngagedUsers = $language.total_engaged_users SuggestionsCount = $language.total_code_suggestions AcceptancesCount = $language.total_code_acceptances LinesAccepted = $language.total_code_lines_accepted LinesSuggested = $language.total_code_lines_suggested ActiveUsers = $language.total_engaged_users } } } } } } # Aggregate by user activity Write-Host "=== COPILOT USAGE SUMMARY ===" -ForegroundColor Green Write-Host "" if ($userMetrics.Count -eq 0) { Write-Warning "No detailed metrics available for this period." } else { # Group by date and calculate totals $dailySummary = $userMetrics | Group-Object Date | ForEach-Object { $dayData = $_.Group [PSCustomObject]@{ Date = $_.Name TotalEngagedUsers = ($dayData | Measure-Object -Property TotalEngagedUsers -Maximum).Maximum TotalSuggestions = ($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum TotalAcceptances = ($dayData | Measure-Object -Property AcceptancesCount -Sum).Sum TotalLinesAccepted = ($dayData | Measure-Object -Property LinesAccepted -Sum).Sum AcceptanceRate = if (($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum -gt 0) { [math]::Round((($dayData | Measure-Object -Property AcceptancesCount -Sum).Sum / ($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum) * 100, 2) } else { 0 } } } | Sort-Object Date -Descending # Display daily summary Write-Host "Daily Summary:" -ForegroundColor Yellow $dailySummary | Format-Table -AutoSize # Calculate overall statistics $totalUsers = ($dailySummary | Measure-Object -Property TotalEngagedUsers -Maximum).Maximum $totalSuggestions = ($dailySummary | Measure-Object -Property TotalSuggestions -Sum).Sum $totalAcceptances = ($dailySummary | Measure-Object -Property TotalAcceptances -Sum).Sum $overallAcceptanceRate = if ($totalSuggestions -gt 0) { [math]::Round(($totalAcceptances / $totalSuggestions) * 100, 2) } else { 0 } Write-Host "" Write-Host "=== OVERALL STATISTICS ===" -ForegroundColor Green Write-Host "Peak Engaged Users: $totalUsers" -ForegroundColor White Write-Host "Total Suggestions: $totalSuggestions" -ForegroundColor White Write-Host "Total Acceptances: $totalAcceptances" -ForegroundColor White Write-Host "Overall Acceptance Rate: $overallAcceptanceRate%" -ForegroundColor White Write-Host "" # Language breakdown $languageSummary = $userMetrics | Group-Object Language | ForEach-Object { $langData = $_.Group [PSCustomObject]@{ Language = $_.Name TotalSuggestions = ($langData | Measure-Object -Property SuggestionsCount -Sum).Sum TotalAcceptances = ($langData | Measure-Object -Property AcceptancesCount -Sum).Sum AcceptanceRate = if (($langData | Measure-Object -Property SuggestionsCount -Sum).Sum -gt 0) { [math]::Round((($langData | Measure-Object -Property AcceptancesCount -Sum).Sum / ($langData | Measure-Object -Property SuggestionsCount -Sum).Sum) * 100, 2) } else { 0 } } } | Sort-Object TotalAcceptances -Descending Write-Host "Language Breakdown:" -ForegroundColor Yellow $languageSummary | Format-Table -AutoSize # Editor breakdown $editorSummary = $userMetrics | Group-Object Editor | ForEach-Object { $editorData = $_.Group [PSCustomObject]@{ Editor = $_.Name TotalSuggestions = ($editorData | Measure-Object -Property SuggestionsCount -Sum).Sum TotalAcceptances = ($editorData | Measure-Object -Property AcceptancesCount -Sum).Sum } } | Sort-Object TotalAcceptances -Descending Write-Host "Editor Breakdown:" -ForegroundColor Yellow $editorSummary | Format-Table -AutoSize # Create overall summary object for Excel $overallSummary = [PSCustomObject]@{ Organization = $Organization DateRange = "$since to $until" PeakEngagedUsers = $totalUsers TotalSuggestions = $totalSuggestions TotalAcceptances = $totalAcceptances OverallAcceptanceRate = "$overallAcceptanceRate%" ReportGeneratedDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } } # Fetch individual user seat information if requested if ($IncludeUserSeats) { Write-Host "" Write-Host "=== FETCHING USER SEAT INFORMATION ===" -ForegroundColor Green Write-Host "" try { # First, get the seat assignments $seatsUrl = "https://api.github.com/orgs/$Organization/copilot/billing/seats" $page = 1 $perPage = 100 $allSeats = @() do { $seatsUri = "$seatsUrl`?per_page=$perPage&page=$page" $seatsResponse = Invoke-RestMethod -Uri $seatsUri -Headers $headers -Method Get if ($null -ne $seatsResponse.seats) { $allSeats += $seatsResponse.seats } $page++ } while ($seatsResponse.seats.Count -eq $perPage) Write-Host "Found $($allSeats.Count) user seats" -ForegroundColor Cyan Write-Host "" Write-Host "NOTE: GitHub does not provide per-user 'credit usage' via API." -ForegroundColor Yellow Write-Host "Credit calculation is based on billing model: 1 seat = 300 credits/month" -ForegroundColor Yellow Write-Host "'Wasting credits' means: seat assigned but user inactive >30 days" -ForegroundColor Yellow Write-Host "" # Calculate credits consumed per user # GitHub Copilot Business/Enterprise charges per seat regardless of usage # If a user has a seat assigned, they consume the full month's credits # The "last_activity_at" tells us if they're actually using it $creditsPerMonth = 300 $daysInMonth = [DateTime]::DaysInMonth((Get-Date).Year, (Get-Date).Month) $currentDay = (Get-Date).Day # Calculate how many days have passed in the current month $daysElapsedInMonth = $currentDay if ($allSeats.Count -eq 0) { Write-Warning "No user seats found." } else { $userSeats = $allSeats | ForEach-Object { $username = $_.assignee.login $lastActivityDate = if ($_.last_activity_at) { try { # Try parsing as DateTime - handles multiple formats if ($_.last_activity_at -is [DateTime]) { $_.last_activity_at } else { [DateTime]::Parse($_.last_activity_at, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) } } catch { # Fallback: try different parsing methods try { [DateTime]::ParseExact($_.last_activity_at, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) } catch { try { # ISO 8601 format [DateTime]::Parse($_.last_activity_at) } catch { Write-Verbose "Could not parse date: $($_.last_activity_at) for user $username" $null } } } } else { $null } $daysSinceLastActivity = if ($lastActivityDate) { [math]::Round(((Get-Date) - $lastActivityDate).TotalDays) } else { $null } $isActive = if ($daysSinceLastActivity -ne $null) { $daysSinceLastActivity -le 30 } else { $false } $assignedDate = if ($_.created_at) { try { if ($_.created_at -is [DateTime]) { $_.created_at } else { $parsedDate = [DateTime]::Parse($_.created_at, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) $parsedDate } } catch { try { [DateTime]::ParseExact($_.created_at, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) } catch { $null } } } else { $null } # Calculate credits consumed # GitHub Copilot billing model: Charges per seat per month # Once a seat is assigned, the FULL monthly cost applies # Credits don't depend on usage - they're consumed just by having the seat # Check if the seat is currently assigned (not pending cancellation) $isPendingCancellation = ![string]::IsNullOrEmpty($_.pending_cancellation_date) if ($isPendingCancellation) { # Seat is being cancelled - no credits for full month $creditsConsumed = 0 } elseif ($assignedDate -and $assignedDate.Year -eq (Get-Date).Year -and $assignedDate.Month -eq (Get-Date).Month) { # Assigned THIS month - pro-rate based on days from assignment to end of month $daysAssignedThisMonth = $currentDay - $assignedDate.Day + 1 $creditsConsumed = [math]::Round(($daysAssignedThisMonth / $daysInMonth) * $creditsPerMonth, 2) } else { # Assigned before this month - FULL month charge $creditsConsumed = $creditsPerMonth } $creditsRemaining = [math]::Round($creditsPerMonth - $creditsConsumed, 2) $creditsUsagePercent = if ($creditsPerMonth -gt 0) { [math]::Round(($creditsConsumed / $creditsPerMonth) * 100, 2) } else { 0 } # Determine if credits are being wasted (assigned but not actively using) $isWastingCredits = ($creditsConsumed -gt 0) -and ($daysSinceLastActivity -eq $null -or $daysSinceLastActivity -gt 30) [PSCustomObject]@{ Username = $username DisplayName = $_.assignee.name Email = $_.assignee.email AssignedAt = $assignedDateFormatted LastActivityAt = if ($lastActivityDate) { $lastActivityDate.ToString("yyyy-MM-dd HH:mm:ss") } else { "Never" } DaysSinceLastActivity = if ($daysSinceLastActivity -ne $null) { $daysSinceLastActivity } else { "N/A" } IsActive30Days = $isActive IsWastingCredits = $isWastingCredits DaysAssignedThisMonth = if ($assignedDate) { $daysAssignedThisMonth } else { $currentDay } CreditsAllocated = $creditsPerMonth CreditsConsumed = $creditsConsumed CreditsRemaining = $creditsRemaining CreditsUsagePercent = $creditsUsagePercent PendingCancellationDate = if ($_.pending_cancellation_date) { $_.pending_cancellation_date } else { "" } Editor = $_.last_activity_editor AssigningTeam = if ($_.assigning_team) { $_.assigning_team.name } else { "" } } } | Sort-Object CreditsConsumed -Descending Write-Host "Total Seats Assigned: $($userSeats.Count)" -ForegroundColor Cyan $activeUsers = ($userSeats | Where-Object { $_.IsActive30Days -eq $true }).Count $inactiveUsers = ($userSeats | Where-Object { $_.IsActive30Days -eq $false }).Count $wastingCredits = ($userSeats | Where-Object { $_.IsWastingCredits -eq $true }).Count Write-Host "Active Users (last 30 days): $activeUsers" -ForegroundColor Green Write-Host "Inactive Users: $inactiveUsers" -ForegroundColor Yellow Write-Host "Users Wasting Credits: $wastingCredits" -ForegroundColor Red Write-Host "" # Show top users by credit consumption Write-Host "All Users by Credit Consumption (Month of $(Get-Date -Format 'MMMM yyyy')):" -ForegroundColor Yellow $userSeats | Select-Object -First 50 | Format-Table Username, DaysAssignedThisMonth, CreditsConsumed, CreditsUsagePercent, LastActivityAt, IsWastingCredits, Editor -AutoSize # Show users wasting credits Write-Host "" Write-Host "Users WASTING Credits (assigned but inactive >30 days):" -ForegroundColor Red $userSeats | Where-Object { $_.IsWastingCredits -eq $true } | Select-Object -First 50 | Format-Table Username, CreditsConsumed, DaysSinceLastActivity, LastActivityAt, AssignedAt -AutoSize # Credit usage summary $totalCreditsAllocated = $userSeats.Count * $creditsPerMonth $totalCreditsConsumed = ($userSeats | Measure-Object -Property CreditsConsumed -Sum).Sum $totalCreditsWasted = ($userSeats | Where-Object { $_.IsWastingCredits -eq $true } | Measure-Object -Property CreditsConsumed -Sum).Sum $creditEfficiency = if ($totalCreditsAllocated -gt 0) { [math]::Round(($totalCreditsConsumed / $totalCreditsAllocated) * 100, 2) } else { 0 } Write-Host "" Write-Host "=== CREDIT USAGE ANALYSIS (Month of $(Get-Date -Format 'MMMM yyyy')) ===" -ForegroundColor Green Write-Host "Credits per User per Month: $creditsPerMonth" -ForegroundColor White Write-Host "Total Seats: $($userSeats.Count)" -ForegroundColor White Write-Host "Total Credits Allocated (Full Month): $totalCreditsAllocated ($($userSeats.Count) users x $creditsPerMonth)" -ForegroundColor White Write-Host "Total Credits Consumed (Month-to-Date): $([math]::Round($totalCreditsConsumed, 2))" -ForegroundColor Cyan Write-Host "Total Credits on WASTED Seats: $([math]::Round($totalCreditsWasted, 2))" -ForegroundColor Red Write-Host "Potential Monthly Savings (remove inactive): $([math]::Round(($wastingCredits * $creditsPerMonth), 2)) credits" -ForegroundColor Yellow Write-Host "" Write-Host "Days Elapsed in Month: $currentDay of $daysInMonth" -ForegroundColor White Write-Host "Credit Utilization: $creditEfficiency% (of full-month allocation)" -ForegroundColor $(if ($creditEfficiency -lt 50) { "Red" } elseif ($creditEfficiency -lt 75) { "Yellow" } else { "Green" }) Write-Host "" # Recommendations if ($wastingCredits -gt 0) { Write-Host "💡 RECOMMENDATIONS:" -ForegroundColor Cyan Write-Host " → Remove $wastingCredits inactive user(s) to save ~$([math]::Round(($wastingCredits * $creditsPerMonth), 2)) credits/month" -ForegroundColor Yellow Write-Host " → That's approximately `$$([math]::Round(($wastingCredits * 19), 2))/month in wasted costs (at ~`$19/seat)" -ForegroundColor Yellow } } } catch { Write-Warning "Failed to retrieve user seat information: $_" Write-Host "You may need 'manage_billing:copilot' scope to access seat details." -ForegroundColor Yellow } } } catch { Write-Error "An error occurred: $_" exit 1 } # Export to Excel if requested if (-not [string]::IsNullOrEmpty($ExportToExcel)) { Write-Host "" Write-Host "Exporting to Excel: $ExportToExcel" -ForegroundColor Cyan try { # Remove existing file if it exists if (Test-Path $ExportToExcel) { Remove-Item $ExportToExcel -Force } # Export to multiple worksheets $excelParams = @{ Path = $ExportToExcel AutoSize = $true FreezeTopRow = $true BoldTopRow = $true } $sheetCount = 1 # Sheet 1: Overall Summary if ($null -ne $overallSummary) { $overallSummary | Export-Excel @excelParams -WorksheetName "Overall Summary" -TableName "OverallSummary" $sheetCount++ } # Sheet 2: Daily Summary if ($dailySummary.Count -gt 0) { $dailySummary | Export-Excel @excelParams -WorksheetName "Daily Summary" -TableName "DailySummary" $sheetCount++ } # Sheet 3: Language Breakdown if ($languageSummary.Count -gt 0) { $languageSummary | Export-Excel @excelParams -WorksheetName "Language Breakdown" -TableName "LanguageBreakdown" $sheetCount++ } # Sheet 4: Editor Breakdown if ($editorSummary.Count -gt 0) { $editorSummary | Export-Excel @excelParams -WorksheetName "Editor Breakdown" -TableName "EditorBreakdown" $sheetCount++ } # Sheet 5: Detailed Metrics if ($userMetrics.Count -gt 0) { $userMetrics | Sort-Object Date -Descending | Export-Excel @excelParams -WorksheetName "Detailed Metrics" -TableName "DetailedMetrics" $sheetCount++ } # Sheet 6: User Seats (if included) if ($userSeats.Count -gt 0) { $userSeats | Export-Excel @excelParams -WorksheetName "User Seats" -TableName "UserSeats" $sheetCount++ } Write-Host "Results exported to: $ExportToExcel" -ForegroundColor Green Write-Host "Excel file contains multiple worksheets:" -ForegroundColor Green Write-Host " - Overall Summary" -ForegroundColor White Write-Host " - Daily Summary" -ForegroundColor White Write-Host " - Language Breakdown" -ForegroundColor White Write-Host " - Editor Breakdown" -ForegroundColor White Write-Host " - Detailed Metrics" -ForegroundColor White if ($userSeats.Count -gt 0) { Write-Host " - User Seats (per-user credit information)" -ForegroundColor White } } catch { Write-Error "Failed to export to Excel: $_" } } Write-Host "" Write-Host "Note: For individual user seat management in browser, visit:" -ForegroundColor Cyan Write-Host "https://github.com/organizations/$Organization/settings/copilot/seat_management" -ForegroundColor Cyan Write-Host "" Write-Host "Script completed." -ForegroundColor Green