diff --git a/github/copilot.go b/github/copilot.go index e375f829a66..8d77e2f4435 100644 --- a/github/copilot.go +++ b/github/copilot.go @@ -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). @@ -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 { @@ -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 +} diff --git a/github/copilot_test.go b/github/copilot_test.go index 92573b5e7f2..71847860524 100644 --- a/github/copilot_test.go +++ b/github/copilot_test.go @@ -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) @@ -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") + } +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 53189d4f12b..77aeed647aa 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -11246,6 +11246,62 @@ func (c *CopilotUserPeriodicMetrics) GetUserLogin() string { return c.UserLogin } +// GetDay returns the Day field. +func (c *CopilotUserTeamsDailyMetrics) GetDay() string { + if c == nil { + return "" + } + return c.Day +} + +// GetEnterpriseID returns the EnterpriseID field if it's non-nil, zero value otherwise. +func (c *CopilotUserTeamsDailyMetrics) GetEnterpriseID() string { + if c == nil || c.EnterpriseID == nil { + return "" + } + return *c.EnterpriseID +} + +// GetOrganizationID returns the OrganizationID field if it's non-nil, zero value otherwise. +func (c *CopilotUserTeamsDailyMetrics) GetOrganizationID() string { + if c == nil || c.OrganizationID == nil { + return "" + } + return *c.OrganizationID +} + +// GetSlug returns the Slug field. +func (c *CopilotUserTeamsDailyMetrics) GetSlug() string { + if c == nil { + return "" + } + return c.Slug +} + +// GetTeamID returns the TeamID field. +func (c *CopilotUserTeamsDailyMetrics) GetTeamID() int64 { + if c == nil { + return 0 + } + return c.TeamID +} + +// GetUserID returns the UserID field. +func (c *CopilotUserTeamsDailyMetrics) GetUserID() int64 { + if c == nil { + return 0 + } + return c.UserID +} + +// GetUserLogin returns the UserLogin field. +func (c *CopilotUserTeamsDailyMetrics) GetUserLogin() string { + if c == nil { + return "" + } + return c.UserLogin +} + // GetAzureSubscription returns the AzureSubscription field if it's non-nil, zero value otherwise. func (c *CostCenter) GetAzureSubscription() string { if c == nil || c.AzureSubscription == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 692e1f44ebd..4ab0b62cab9 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -14217,6 +14217,68 @@ func TestCopilotUserPeriodicMetrics_GetUserLogin(tt *testing.T) { c.GetUserLogin() } +func TestCopilotUserTeamsDailyMetrics_GetDay(tt *testing.T) { + tt.Parallel() + c := &CopilotUserTeamsDailyMetrics{} + c.GetDay() + c = nil + c.GetDay() +} + +func TestCopilotUserTeamsDailyMetrics_GetEnterpriseID(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CopilotUserTeamsDailyMetrics{EnterpriseID: &zeroValue} + c.GetEnterpriseID() + c = &CopilotUserTeamsDailyMetrics{} + c.GetEnterpriseID() + c = nil + c.GetEnterpriseID() +} + +func TestCopilotUserTeamsDailyMetrics_GetOrganizationID(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CopilotUserTeamsDailyMetrics{OrganizationID: &zeroValue} + c.GetOrganizationID() + c = &CopilotUserTeamsDailyMetrics{} + c.GetOrganizationID() + c = nil + c.GetOrganizationID() +} + +func TestCopilotUserTeamsDailyMetrics_GetSlug(tt *testing.T) { + tt.Parallel() + c := &CopilotUserTeamsDailyMetrics{} + c.GetSlug() + c = nil + c.GetSlug() +} + +func TestCopilotUserTeamsDailyMetrics_GetTeamID(tt *testing.T) { + tt.Parallel() + c := &CopilotUserTeamsDailyMetrics{} + c.GetTeamID() + c = nil + c.GetTeamID() +} + +func TestCopilotUserTeamsDailyMetrics_GetUserID(tt *testing.T) { + tt.Parallel() + c := &CopilotUserTeamsDailyMetrics{} + c.GetUserID() + c = nil + c.GetUserID() +} + +func TestCopilotUserTeamsDailyMetrics_GetUserLogin(tt *testing.T) { + tt.Parallel() + c := &CopilotUserTeamsDailyMetrics{} + c.GetUserLogin() + c = nil + c.GetUserLogin() +} + func TestCostCenter_GetAzureSubscription(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/tools/metadata/metadata.go b/tools/metadata/metadata.go index d6306c2e8ad..181fe611681 100644 --- a/tools/metadata/metadata.go +++ b/tools/metadata/metadata.go @@ -566,9 +566,10 @@ func nodeServiceMethod(fn *ast.FuncDecl) string { // skipServiceMethod lists helper methods that download from URLs returned by // other endpoints and therefore have no REST API operation of their own. var skipServiceMethod = map[string]bool{ - "CopilotService.DownloadCopilotMetrics": true, - "CopilotService.DownloadDailyMetrics": true, - "CopilotService.DownloadPeriodicMetrics": true, - "CopilotService.DownloadUserDailyMetrics": true, - "CopilotService.DownloadUserPeriodicMetrics": true, + "CopilotService.DownloadCopilotMetrics": true, + "CopilotService.DownloadDailyMetrics": true, + "CopilotService.DownloadPeriodicMetrics": true, + "CopilotService.DownloadUserDailyMetrics": true, + "CopilotService.DownloadUserPeriodicMetrics": true, + "CopilotService.DownloadUserTeamsDailyMetrics": true, }