- Add copilot usage check script #124960

- Add VSCode Plugin Scan poc script #124961
This commit is contained in:
Jurjen Ladenius
2025-10-17 15:04:40 +02:00
parent e78fc74d8c
commit d14e068914
2 changed files with 882 additions and 0 deletions

View File

@@ -0,0 +1,529 @@
<#
.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