Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 119 additions & 15 deletions github/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,11 +879,70 @@ func (s *CopilotService) GetOrganizationUsersMetricsReport(ctx context.Context,
return report, resp, nil
}

// GetEnterpriseRepositoriesDailyMetricsReport gets a report containing Copilot
// repository-level pull request metrics for a single day for an enterprise.
//
// Use DownloadRepositoryDailyMetrics to decode the payloads served at the returned download links.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage-metrics?apiVersion=2022-11-28#get-copilot-enterprise-repository-report-for-a-specific-day
//
//meta:operation GET /enterprises/{enterprise}/copilot/metrics/reports/repos-1-day
func (s *CopilotService) GetEnterpriseRepositoriesDailyMetricsReport(ctx context.Context, enterprise string, opts *CopilotMetricsReportOptions) (*CopilotDailyMetricsReport, *Response, error) {
u := fmt.Sprintf("enterprises/%v/copilot/metrics/reports/repos-1-day", enterprise)
u, err := addOptions(u, opts)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequest(ctx, "GET", u, nil)
if err != nil {
return nil, nil, err
}

var report *CopilotDailyMetricsReport
resp, err := s.client.Do(req, &report)
if err != nil {
return nil, resp, err
}

return report, resp, nil
}

// GetOrganizationRepositoriesDailyMetricsReport gets a report containing Copilot
// repository-level pull request metrics for a single day for an organization.
//
// Use DownloadRepositoryDailyMetrics to decode the payloads served at the returned download links.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage-metrics?apiVersion=2022-11-28#get-copilot-organization-repository-report-for-a-specific-day
//
//meta:operation GET /orgs/{org}/copilot/metrics/reports/repos-1-day
func (s *CopilotService) GetOrganizationRepositoriesDailyMetricsReport(ctx context.Context, org string, opts *CopilotMetricsReportOptions) (*CopilotDailyMetricsReport, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/metrics/reports/repos-1-day", org)
u, err := addOptions(u, opts)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequest(ctx, "GET", u, nil)
if err != nil {
return nil, nil, err
}

var report *CopilotDailyMetricsReport
resp, err := s.client.Do(req, &report)
if err != nil {
return nil, resp, err
}

return report, resp, nil
}

// DownloadCopilotMetrics downloads a Copilot metrics report from the provided download link
// and decodes it as a []*CopilotMetrics.
//
// Deprecated: Use DownloadDailyMetrics,
// DownloadPeriodicMetrics, DownloadUserDailyMetrics, DownloadUserPeriodicMetrics instead.
// DownloadPeriodicMetrics, DownloadUserDailyMetrics, DownloadUserPeriodicMetrics,
// DownloadRepositoryDailyMetrics instead.
// The payloads served at the download links returned by the new
// Get*MetricsReport endpoints on GitHub.com do not match the CopilotMetrics shape
// (see https://github.com/google/go-github/issues/4136).
Expand All @@ -909,22 +968,48 @@ func (s *CopilotService) DownloadCopilotMetrics(ctx context.Context, url string)
return metrics, resp, nil
}

// CopilotMetricsCopilotSuggestionByCommentType represents Copilot code review suggestion
// counts broken down by comment type in a pull_requests object.
//
// GitHub API docs: https://docs.github.com/en/copilot/reference/copilot-usage-metrics/copilot-usage-metrics#pull-request-activity-fields
type CopilotMetricsCopilotSuggestionByCommentType struct {
CommentType string `json:"comment_type"`
TotalCopilotSuggestions *int `json:"total_copilot_suggestions,omitempty"`
TotalCopilotAppliedSuggestions *int `json:"total_copilot_applied_suggestions,omitempty"`
}

// CopilotMetricsPullRequests represents pull request totals in a Copilot metrics report.
type CopilotMetricsPullRequests struct {
TotalReviewed *int `json:"total_reviewed,omitempty"`
TotalCreated *int `json:"total_created,omitempty"`
TotalCreatedByCopilot *int `json:"total_created_by_copilot,omitempty"`
TotalReviewedByCopilot *int `json:"total_reviewed_by_copilot,omitempty"`
TotalMerged *int `json:"total_merged,omitempty"`
MedianMinutesToMerge *float64 `json:"median_minutes_to_merge,omitempty"`
TotalSuggestions *int `json:"total_suggestions,omitempty"`
TotalAppliedSuggestions *int `json:"total_applied_suggestions,omitempty"`
TotalMergedCreatedByCopilot *int `json:"total_merged_created_by_copilot,omitempty"`
MedianMinutesToMergeCopilotAuthored *float64 `json:"median_minutes_to_merge_copilot_authored,omitempty"`
TotalCopilotSuggestions *int `json:"total_copilot_suggestions,omitempty"`
TotalCopilotAppliedSuggestions *int `json:"total_copilot_applied_suggestions,omitempty"`
MedianMinutesToMergeCopilotReviewed *float64 `json:"median_minutes_to_merge_copilot_reviewed,omitempty"`
TotalMergedReviewedByCopilot *int `json:"total_merged_reviewed_by_copilot,omitempty"`
TotalReviewed *int `json:"total_reviewed,omitempty"`
TotalCreated *int `json:"total_created,omitempty"`
TotalCreatedByCopilot *int `json:"total_created_by_copilot,omitempty"`
TotalReviewedByCopilot *int `json:"total_reviewed_by_copilot,omitempty"`
TotalMerged *int `json:"total_merged,omitempty"`
MedianMinutesToMerge *float64 `json:"median_minutes_to_merge,omitempty"`
TotalSuggestions *int `json:"total_suggestions,omitempty"`
TotalAppliedSuggestions *int `json:"total_applied_suggestions,omitempty"`
TotalMergedCreatedByCopilot *int `json:"total_merged_created_by_copilot,omitempty"`
MedianMinutesToMergeCopilotAuthored *float64 `json:"median_minutes_to_merge_copilot_authored,omitempty"`
TotalCopilotSuggestions *int `json:"total_copilot_suggestions,omitempty"`
TotalCopilotAppliedSuggestions *int `json:"total_copilot_applied_suggestions,omitempty"`
MedianMinutesToMergeCopilotReviewed *float64 `json:"median_minutes_to_merge_copilot_reviewed,omitempty"`
TotalMergedReviewedByCopilot *int `json:"total_merged_reviewed_by_copilot,omitempty"`
CopilotSuggestionsByCommentType []*CopilotMetricsCopilotSuggestionByCommentType `json:"copilot_suggestions_by_comment_type,omitempty"`
}

// CopilotRepositoryDailyMetrics represents a single repository's per-day Copilot pull request
// metrics record from a repos-1-day report. Repository reports are served as newline-delimited JSON.
//
// GitHub API docs: https://docs.github.com/en/copilot/reference/copilot-usage-metrics/copilot-usage-metrics#repository-level-fields-api-only
type CopilotRepositoryDailyMetrics struct {
Day string `json:"day"`
EnterpriseID *string `json:"enterprise_id,omitempty"`
OrganizationID *string `json:"organization_id,omitempty"`
RepoID int64 `json:"repo_id"`
RepoOwnerName string `json:"repo_owner_name"`
RepoName string `json:"repo_name"`
RepoVisibility string `json:"repo_visibility"`
PullRequests *CopilotMetricsPullRequests `json:"pull_requests,omitempty"`
}

// CopilotMetricsCodeActivity captures the code-generation activity counts and lines-of-code (LOC)
Expand Down Expand Up @@ -1348,3 +1433,22 @@ func (s *CopilotService) DownloadUserPeriodicMetrics(ctx context.Context, url st
}
return records, r, nil
}

// DownloadRepositoryDailyMetrics downloads the payload of a 1-day Copilot repository metrics
// report from a download link returned by GetEnterpriseRepositoriesDailyMetricsReport or
// GetOrganizationRepositoriesDailyMetricsReport.
//
// The response is newline-delimited JSON, with one CopilotRepositoryDailyMetrics record per line.
func (s *CopilotService) DownloadRepositoryDailyMetrics(ctx context.Context, url string) ([]*CopilotRepositoryDailyMetrics, *Response, error) {
resp, r, err := s.fetchMetricsReport(ctx, url)
if err != nil {
return nil, r, err
}
defer resp.Body.Close()

records, err := decodeNDJSONMetrics[CopilotRepositoryDailyMetrics](resp.Body)
if err != nil {
return nil, r, err
}
return records, r, nil
}
209 changes: 209 additions & 0 deletions github/copilot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2888,6 +2888,96 @@ func TestCopilotService_GetOrganizationUsersMetricsReport(t *testing.T) {
})
}

func TestCopilotService_GetEnterpriseRepositoriesDailyMetricsReport(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/enterprises/e/copilot/metrics/reports/repos-1-day", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"day": "2026-07-14"})
fmt.Fprint(w, `{
"download_links": ["https://example.com/repos-1.json", "https://example.com/repos-2.json"],
"report_day": "2026-07-14"
}`)
})

ctx := t.Context()
opts := &CopilotMetricsReportOptions{Day: "2026-07-14"}
got, _, err := client.Copilot.GetEnterpriseRepositoriesDailyMetricsReport(ctx, "e", opts)
if err != nil {
t.Errorf("Copilot.GetEnterpriseRepositoriesDailyMetricsReport returned error: %v", err)
}

want := &CopilotDailyMetricsReport{
DownloadLinks: []string{"https://example.com/repos-1.json", "https://example.com/repos-2.json"},
ReportDay: "2026-07-14",
}

if !cmp.Equal(got, want) {
t.Errorf("Copilot.GetEnterpriseRepositoriesDailyMetricsReport returned %+v, want %+v", got, want)
}

const methodName = "GetEnterpriseRepositoriesDailyMetricsReport"

testBadOptions(t, methodName, func() (err error) {
_, _, err = client.Copilot.GetEnterpriseRepositoriesDailyMetricsReport(ctx, "\n", opts)
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
got, resp, err := client.Copilot.GetEnterpriseRepositoriesDailyMetricsReport(ctx, "e", opts)
if got != nil {
t.Errorf("Copilot.GetEnterpriseRepositoriesDailyMetricsReport returned %+v, want nil", got)
}
return resp, err
})
}

func TestCopilotService_GetOrganizationRepositoriesDailyMetricsReport(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/orgs/o/copilot/metrics/reports/repos-1-day", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"day": "2026-07-14"})
fmt.Fprint(w, `{
"download_links": ["https://example.com/repos-1.json"],
"report_day": "2026-07-14"
}`)
})

ctx := t.Context()
opts := &CopilotMetricsReportOptions{Day: "2026-07-14"}
got, _, err := client.Copilot.GetOrganizationRepositoriesDailyMetricsReport(ctx, "o", opts)
if err != nil {
t.Errorf("Copilot.GetOrganizationRepositoriesDailyMetricsReport returned error: %v", err)
}

want := &CopilotDailyMetricsReport{
DownloadLinks: []string{"https://example.com/repos-1.json"},
ReportDay: "2026-07-14",
}

if !cmp.Equal(got, want) {
t.Errorf("Copilot.GetOrganizationRepositoriesDailyMetricsReport returned %+v, want %+v", got, want)
}

const methodName = "GetOrganizationRepositoriesDailyMetricsReport"

testBadOptions(t, methodName, func() (err error) {
_, _, err = client.Copilot.GetOrganizationRepositoriesDailyMetricsReport(ctx, "\n", opts)
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
got, resp, err := client.Copilot.GetOrganizationRepositoriesDailyMetricsReport(ctx, "o", opts)
if got != nil {
t.Errorf("Copilot.GetOrganizationRepositoriesDailyMetricsReport returned %+v, want nil", got)
}
return resp, err
})
}

func TestCopilotService_DownloadCopilotMetrics(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)
Expand Down Expand Up @@ -3654,3 +3744,122 @@ func TestCopilotService_DownloadUserPeriodicMetrics(t *testing.T) {
t.Error("Copilot.DownloadUserPeriodicMetrics expected error for bad JSON, got none")
}
}

func TestCopilotService_DownloadRepositoryDailyMetrics(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/path/to/repos-daily", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"day":"2026-07-14","enterprise_id":"1001","organization_id":"2002","repo_id":900000001,"repo_owner_name":"octodemo-metrics","repo_name":"example-service-alpha","repo_visibility":"INTERNAL","pull_requests":{"total_reviewed":1,"total_created":1,"total_created_by_copilot":1,"total_reviewed_by_copilot":1,"total_merged":1,"median_minutes_to_merge":372.62,"total_suggestions":0,"total_applied_suggestions":0,"total_merged_created_by_copilot":1,"median_minutes_to_merge_copilot_authored":372.62,"total_copilot_suggestions":0,"total_copilot_applied_suggestions":0,"total_merged_reviewed_by_copilot":1,"median_minutes_to_merge_copilot_reviewed":372.62,"copilot_suggestions_by_comment_type":[]}}
{"day":"2026-07-14","enterprise_id":"1001","organization_id":"2002","repo_id":900000003,"repo_owner_name":"octodemo-metrics","repo_name":"example-service-gamma","repo_visibility":"INTERNAL","pull_requests":{"total_reviewed":1,"total_created":0,"total_created_by_copilot":0,"total_reviewed_by_copilot":1,"total_merged":1,"median_minutes_to_merge":1020.53,"total_suggestions":0,"total_applied_suggestions":1,"total_merged_created_by_copilot":0,"total_copilot_suggestions":0,"total_copilot_applied_suggestions":1,"total_merged_reviewed_by_copilot":1,"median_minutes_to_merge_copilot_reviewed":1020.53,"copilot_suggestions_by_comment_type":[{"comment_type":"spelling","total_copilot_suggestions":0,"total_copilot_applied_suggestions":1}]}}
`)
})

ctx := t.Context()
url := client.baseURL.String() + "path/to/repos-daily"
got, resp, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, url)
if err != nil {
t.Errorf("Copilot.DownloadRepositoryDailyMetrics returned error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Copilot.DownloadRepositoryDailyMetrics returned status code: %v", resp.StatusCode)
}

want := []*CopilotRepositoryDailyMetrics{
{
Day: "2026-07-14",
EnterpriseID: new("1001"),
OrganizationID: new("2002"),
RepoID: 900000001,
RepoOwnerName: "octodemo-metrics",
RepoName: "example-service-alpha",
RepoVisibility: "INTERNAL",
PullRequests: &CopilotMetricsPullRequests{
TotalReviewed: new(1),
TotalCreated: new(1),
TotalCreatedByCopilot: new(1),
TotalReviewedByCopilot: new(1),
TotalMerged: new(1),
MedianMinutesToMerge: new(372.62),
TotalSuggestions: new(0),
TotalAppliedSuggestions: new(0),
TotalMergedCreatedByCopilot: new(1),
MedianMinutesToMergeCopilotAuthored: new(372.62),
TotalCopilotSuggestions: new(0),
TotalCopilotAppliedSuggestions: new(0),
TotalMergedReviewedByCopilot: new(1),
MedianMinutesToMergeCopilotReviewed: new(372.62),
CopilotSuggestionsByCommentType: []*CopilotMetricsCopilotSuggestionByCommentType{},
},
},
{
Day: "2026-07-14",
EnterpriseID: new("1001"),
OrganizationID: new("2002"),
RepoID: 900000003,
RepoOwnerName: "octodemo-metrics",
RepoName: "example-service-gamma",
RepoVisibility: "INTERNAL",
PullRequests: &CopilotMetricsPullRequests{
TotalReviewed: new(1),
TotalCreated: new(0),
TotalCreatedByCopilot: new(0),
TotalReviewedByCopilot: new(1),
TotalMerged: new(1),
MedianMinutesToMerge: new(1020.53),
TotalSuggestions: new(0),
TotalAppliedSuggestions: new(1),
TotalMergedCreatedByCopilot: new(0),
TotalCopilotSuggestions: new(0),
TotalCopilotAppliedSuggestions: new(1),
TotalMergedReviewedByCopilot: new(1),
MedianMinutesToMergeCopilotReviewed: new(1020.53),
CopilotSuggestionsByCommentType: []*CopilotMetricsCopilotSuggestionByCommentType{
{
CommentType: "spelling",
TotalCopilotSuggestions: new(0),
TotalCopilotAppliedSuggestions: new(1),
},
},
},
},
}

if !cmp.Equal(got, want) {
t.Errorf("Copilot.DownloadRepositoryDailyMetrics returned %+v, want %+v", got, want)
}

mux.HandleFunc("/path/to/repos-daily/empty", func(_ http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
})
gotEmpty, _, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, client.baseURL.String()+"path/to/repos-daily/empty")
if err != nil {
t.Errorf("Copilot.DownloadRepositoryDailyMetrics empty body returned error: %v", err)
}
if gotEmpty != nil {
t.Errorf("Copilot.DownloadRepositoryDailyMetrics empty body returned %+v, want nil", gotEmpty)
}

mux.HandleFunc("/path/to/repos-daily/error", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusNotFound)
})
if _, _, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, client.baseURL.String()+"path/to/repos-daily/error"); err == nil {
t.Error("Copilot.DownloadRepositoryDailyMetrics expected error but got none")
}
if _, _, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, "\n"); err == nil {
t.Error("Copilot.DownloadRepositoryDailyMetrics expected error for invalid URL, got none")
}
if _, _, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, "invalid-scheme://test"); err == nil {
t.Error("Copilot.DownloadRepositoryDailyMetrics expected error for invalid scheme, got none")
}

mux.HandleFunc("/path/to/repos-daily/badjson", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, "{\"repo_id\":1,\"day\":\"2026-07-14\"}\n{bad\n")
})
if _, _, err := client.Copilot.DownloadRepositoryDailyMetrics(ctx, client.baseURL.String()+"path/to/repos-daily/badjson"); err == nil {
t.Error("Copilot.DownloadRepositoryDailyMetrics expected error for bad JSON, got none")
}
}
Loading
Loading