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
100 changes: 99 additions & 1 deletion github/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,11 +879,74 @@ func (s *CopilotService) GetOrganizationUsersMetricsReport(ctx context.Context,
return report, resp, nil
}

// GetEnterpriseUserTeamsDailyMetricsReport gets a report containing Copilot
// user-team membership data for a single day for an enterprise.
//
// Use DownloadUserTeamsDailyMetrics to decode the payloads served at the returned download links.
// Join these records with per-user usage metrics on user_id, day, and enterprise_id to
// construct team-level metrics.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage-metrics?apiVersion=2022-11-28#get-copilot-enterprise-user-teams-report-for-a-specific-day
//
//meta:operation GET /enterprises/{enterprise}/copilot/metrics/reports/user-teams-1-day
func (s *CopilotService) GetEnterpriseUserTeamsDailyMetricsReport(ctx context.Context, enterprise string, opts *CopilotMetricsReportOptions) (*CopilotDailyMetricsReport, *Response, error) {
u := fmt.Sprintf("enterprises/%v/copilot/metrics/reports/user-teams-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
}

// GetOrganizationUserTeamsDailyMetricsReport gets a report containing Copilot
// user-team membership data for a single day for an organization.
//
// Use DownloadUserTeamsDailyMetrics to decode the payloads served at the returned download links.
// Join these records with per-user usage metrics on user_id, day, and organization_id to
// construct team-level metrics.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage-metrics?apiVersion=2022-11-28#get-copilot-organization-user-teams-report-for-a-specific-day
//
//meta:operation GET /orgs/{org}/copilot/metrics/reports/user-teams-1-day
func (s *CopilotService) GetOrganizationUserTeamsDailyMetricsReport(ctx context.Context, org string, opts *CopilotMetricsReportOptions) (*CopilotDailyMetricsReport, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/metrics/reports/user-teams-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,
// DownloadUserTeamsDailyMetrics 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 Down Expand Up @@ -1157,6 +1220,22 @@ type CopilotUserMetricsIDE struct {
LastKnownIDEVersion *CopilotUserMetricsIDEVersion `json:"last_known_ide_version,omitempty"`
}

// CopilotUserTeamsDailyMetrics represents a user-team membership record from a user-teams-1-day
// report. These records are joined with per-user usage metrics to construct team-level metrics.
// Teams with fewer than 5 seated Copilot users are omitted from the report.
// User-teams reports are served as newline-delimited JSON.
//
// GitHub API docs: https://docs.github.com/en/copilot/reference/copilot-usage-metrics/copilot-usage-metrics#user-teams-fields
type CopilotUserTeamsDailyMetrics struct {
UserID int64 `json:"user_id"`
UserLogin string `json:"user_login"`
Day string `json:"day"`
OrganizationID *string `json:"organization_id,omitempty"`
EnterpriseID *string `json:"enterprise_id,omitempty"`
TeamID int64 `json:"team_id"`
Slug string `json:"slug"`
}

// CopilotUserDailyMetrics represents a single user's per-day Copilot usage metrics record from a
// 1-day user metrics report. User metrics reports are served as newline-delimited JSON.
type CopilotUserDailyMetrics struct {
Expand Down Expand Up @@ -1348,3 +1427,22 @@ func (s *CopilotService) DownloadUserPeriodicMetrics(ctx context.Context, url st
}
return records, r, nil
}

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

records, err := decodeNDJSONMetrics[CopilotUserTeamsDailyMetrics](resp.Body)
if err != nil {
return nil, r, err
}
return records, r, nil
}
177 changes: 177 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_GetEnterpriseUserTeamsDailyMetricsReport(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

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

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

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

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

const methodName = "GetEnterpriseUserTeamsDailyMetricsReport"

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

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

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

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

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

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

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

const methodName = "GetOrganizationUserTeamsDailyMetricsReport"

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

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
got, resp, err := client.Copilot.GetOrganizationUserTeamsDailyMetricsReport(ctx, "o", opts)
if got != nil {
t.Errorf("Copilot.GetOrganizationUserTeamsDailyMetricsReport 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,90 @@ func TestCopilotService_DownloadUserPeriodicMetrics(t *testing.T) {
t.Error("Copilot.DownloadUserPeriodicMetrics expected error for bad JSON, got none")
}
}

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

mux.HandleFunc("/path/to/user-teams-daily", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"user_id":1001,"user_login":"octocat","day":"2026-05-14","organization_id":"999","team_id":42,"slug":"frontend"}
{"user_id":1001,"user_login":"octocat","day":"2026-05-14","organization_id":"999","team_id":43,"slug":"backend"}
{"user_id":1002,"user_login":"hubot","day":"2026-05-14","enterprise_id":"1","team_id":9001,"slug":"eng-platform"}
`)
})

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

want := []*CopilotUserTeamsDailyMetrics{
{
UserID: 1001,
UserLogin: "octocat",
Day: "2026-05-14",
OrganizationID: new("999"),
TeamID: 42,
Slug: "frontend",
},
{
UserID: 1001,
UserLogin: "octocat",
Day: "2026-05-14",
OrganizationID: new("999"),
TeamID: 43,
Slug: "backend",
},
{
UserID: 1002,
UserLogin: "hubot",
Day: "2026-05-14",
EnterpriseID: new("1"),
TeamID: 9001,
Slug: "eng-platform",
},
}

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

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

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

mux.HandleFunc("/path/to/user-teams-daily/badjson", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, "{\"user_id\":1,\"day\":\"2026-05-14\"}\n{bad\n")
})
if _, _, err := client.Copilot.DownloadUserTeamsDailyMetrics(ctx, client.baseURL.String()+"path/to/user-teams-daily/badjson"); err == nil {
t.Error("Copilot.DownloadUserTeamsDailyMetrics expected error for bad JSON, got none")
}
}
56 changes: 56 additions & 0 deletions github/github-accessors.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading