diff --git a/github/copilot.go b/github/copilot.go index 349b16ebde..7ea13bd7c8 100644 --- a/github/copilot.go +++ b/github/copilot.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "time" ) // CopilotService provides access to the Copilot-related functions @@ -52,6 +53,7 @@ type CopilotSeatDetails struct { LastActivityEditor *string `json:"last_activity_editor,omitempty"` CreatedAt *Timestamp `json:"created_at"` UpdatedAt *Timestamp `json:"updated_at,omitempty"` + PlanType *string `json:"plan_type,omitempty"` } // SeatAssignments represents the number of seats assigned. @@ -64,6 +66,39 @@ type SeatCancellations struct { SeatsCancelled int `json:"seats_cancelled"` } +// CopilotUsageSummaryListOptions represents the optional parameters to the CopilotService.GetOrganizationUsage method. +type CopilotUsageSummaryListOptions struct { + Since *time.Time `url:"since,omitempty"` + Until *time.Time `url:"until,omitempty"` + + ListOptions +} + +// CopilotUsageBreakdown represents the breakdown of Copilot usage for a specific language and editor. +type CopilotUsageBreakdown struct { + Language string `json:"language"` + Editor string `json:"editor"` + SuggestionsCount int64 `json:"suggestions_count"` + AcceptancesCount int64 `json:"acceptances_count"` + LinesSuggested int64 `json:"lines_suggested"` + LinesAccepted int64 `json:"lines_accepted"` + ActiveUsers int `json:"active_users"` +} + +// CopilotUsageSummary represents the daily breakdown of aggregated usage metrics for Copilot completions and Copilot Chat in the IDE across an organization. +type CopilotUsageSummary struct { + Day string `json:"day"` + TotalSuggestionsCount int64 `json:"total_suggestions_count"` + TotalAcceptancesCount int64 `json:"total_acceptances_count"` + TotalLinesSuggested int64 `json:"total_lines_suggested"` + TotalLinesAccepted int64 `json:"total_lines_accepted"` + TotalActiveUsers int64 `json:"total_active_users"` + TotalChatAcceptances int64 `json:"total_chat_acceptances"` + TotalChatTurns int64 `json:"total_chat_turns"` + TotalActiveChatUsers int `json:"total_active_chat_users"` + Breakdown []*CopilotUsageBreakdown `json:"breakdown"` +} + func (cp *CopilotSeatDetails) UnmarshalJSON(data []byte) error { // Using an alias to avoid infinite recursion when calling json.Unmarshal type alias CopilotSeatDetails @@ -79,6 +114,7 @@ func (cp *CopilotSeatDetails) UnmarshalJSON(data []byte) error { cp.LastActivityEditor = seatDetail.LastActivityEditor cp.CreatedAt = seatDetail.CreatedAt cp.UpdatedAt = seatDetail.UpdatedAt + cp.PlanType = seatDetail.PlanType switch v := seatDetail.Assignee.(type) { case map[string]interface{}: @@ -181,6 +217,34 @@ func (s *CopilotService) ListCopilotSeats(ctx context.Context, org string, opts return copilotSeats, resp, nil } +// ListCopilotEnterpriseSeats lists Copilot for Business seat assignments for an enterprise. +// +// To paginate through all seats, populate 'Page' with the number of the last page. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-user-management#list-all-copilot-seat-assignments-for-an-enterprise +// +//meta:operation GET /enterprises/{enterprise}/copilot/billing/seats +func (s *CopilotService) ListCopilotEnterpriseSeats(ctx context.Context, enterprise string, opts *ListOptions) (*ListCopilotSeatsResponse, *Response, error) { + u := fmt.Sprintf("enterprises/%v/copilot/billing/seats", enterprise) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var copilotSeats *ListCopilotSeatsResponse + resp, err := s.client.Do(ctx, req, &copilotSeats) + if err != nil { + return nil, resp, err + } + + return copilotSeats, resp, nil +} + // AddCopilotTeams adds teams to the Copilot for Business subscription for an organization. // // GitHub API docs: https://docs.github.com/rest/copilot/copilot-user-management#add-teams-to-the-copilot-subscription-for-an-organization @@ -314,3 +378,107 @@ func (s *CopilotService) GetSeatDetails(ctx context.Context, org, user string) ( return seatDetails, resp, nil } + +// GetOrganizationUsage gets daily breakdown of aggregated usage metrics for Copilot completions and Copilot Chat in the IDE across an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage#get-a-summary-of-copilot-usage-for-organization-members +// +//meta:operation GET /orgs/{org}/copilot/usage +func (s *CopilotService) GetOrganizationUsage(ctx context.Context, org string, opts *CopilotUsageSummaryListOptions) ([]*CopilotUsageSummary, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/usage", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var usage []*CopilotUsageSummary + resp, err := s.client.Do(ctx, req, &usage) + if err != nil { + return nil, resp, err + } + + return usage, resp, nil +} + +// GetEnterpriseUsage gets daily breakdown of aggregated usage metrics for Copilot completions and Copilot Chat in the IDE across an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage#get-a-summary-of-copilot-usage-for-enterprise-members +// +//meta:operation GET /enterprises/{enterprise}/copilot/usage +func (s *CopilotService) GetEnterpriseUsage(ctx context.Context, enterprise string, opts *CopilotUsageSummaryListOptions) ([]*CopilotUsageSummary, *Response, error) { + u := fmt.Sprintf("enterprises/%v/copilot/usage", enterprise) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var usage []*CopilotUsageSummary + resp, err := s.client.Do(ctx, req, &usage) + if err != nil { + return nil, resp, err + } + + return usage, resp, nil +} + +// GetEnterpriseTeamUsage gets daily breakdown of aggregated usage metrics for Copilot completions and Copilot Chat in the IDE for a team in the enterprise. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage#get-a-summary-of-copilot-usage-for-an-enterprise-team +// +//meta:operation GET /enterprises/{enterprise}/team/{team_slug}/copilot/usage +func (s *CopilotService) GetEnterpriseTeamUsage(ctx context.Context, enterprise, team string, opts *CopilotUsageSummaryListOptions) ([]*CopilotUsageSummary, *Response, error) { + u := fmt.Sprintf("enterprises/%v/team/%v/copilot/usage", enterprise, team) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var usage []*CopilotUsageSummary + resp, err := s.client.Do(ctx, req, &usage) + if err != nil { + return nil, resp, err + } + + return usage, resp, nil +} + +// GetOrganizationTeamUsage gets daily breakdown of aggregated usage metrics for Copilot completions and Copilot Chat in the IDE for a team in the organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-usage#get-a-summary-of-copilot-usage-for-a-team +// +//meta:operation GET /orgs/{org}/team/{team_slug}/copilot/usage +func (s *CopilotService) GetOrganizationTeamUsage(ctx context.Context, org, team string, opts *CopilotUsageSummaryListOptions) ([]*CopilotUsageSummary, *Response, error) { + u := fmt.Sprintf("orgs/%v/team/%v/copilot/usage", org, team) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var usage []*CopilotUsageSummary + resp, err := s.client.Do(ctx, req, &usage) + if err != nil { + return nil, resp, err + } + + return usage, resp, nil +} diff --git a/github/copilot_test.go b/github/copilot_test.go index f27350be79..6ac689a9ef 100644 --- a/github/copilot_test.go +++ b/github/copilot_test.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "testing" "time" @@ -609,6 +610,235 @@ func TestCopilotService_ListCopilotSeats(t *testing.T) { }) } +func TestCopilotService_ListCopilotEnterpriseSeats(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/copilot/billing/seats", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "per_page": "100", + "page": "1", + }) + fmt.Fprint(w, `{ + "total_seats": 2, + "seats": [ + { + "created_at": "2021-08-03T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": null, + "last_activity_at": "2021-10-14T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "plan_type": "business", + "assignee": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "assigning_team": { + "id": 1, + "node_id": "MDQ6VGVhbTE=", + "url": "https://api.github.com/teams/1", + "html_url": "https://github.com/orgs/github/teams/justice-league", + "name": "Justice League", + "slug": "justice-league", + "description": "A great team.", + "privacy": "closed", + "notification_setting": "notifications_enabled", + "permission": "admin", + "members_url": "https://api.github.com/teams/1/members{/member}", + "repositories_url": "https://api.github.com/teams/1/repos", + "parent": null + } + }, + { + "created_at": "2021-09-23T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": "2021-11-01", + "last_activity_at": "2021-10-13T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "login": "octokitten", + "id": 1, + "node_id": "MDQ76VNlcjE=", + "avatar_url": "https://github.com/images/error/octokitten_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octokitten", + "html_url": "https://github.com/octokitten", + "followers_url": "https://api.github.com/users/octokitten/followers", + "following_url": "https://api.github.com/users/octokitten/following{/other_user}", + "gists_url": "https://api.github.com/users/octokitten/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octokitten/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octokitten/subscriptions", + "organizations_url": "https://api.github.com/users/octokitten/orgs", + "repos_url": "https://api.github.com/users/octokitten/repos", + "events_url": "https://api.github.com/users/octokitten/events{/privacy}", + "received_events_url": "https://api.github.com/users/octokitten/received_events", + "type": "User", + "site_admin": false + } + } + ] + }`) + }) + + tmp, err := time.Parse(time.RFC3339, "2021-08-03T18:00:00-06:00") + if err != nil { + panic(err) + } + createdAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T15:00:00-06:00") + if err != nil { + panic(err) + } + updatedAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-10-14T00:53:32-06:00") + if err != nil { + panic(err) + } + lastActivityAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T18:00:00-06:00") + if err != nil { + panic(err) + } + createdAt2 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T15:00:00-06:00") + if err != nil { + panic(err) + } + updatedAt2 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-10-13T00:53:32-06:00") + if err != nil { + panic(err) + } + lastActivityAt2 := Timestamp{tmp} + + ctx := context.Background() + opts := &ListOptions{Page: 1, PerPage: 100} + got, _, err := client.Copilot.ListCopilotEnterpriseSeats(ctx, "e", opts) + if err != nil { + t.Errorf("Copilot.ListCopilotEnterpriseSeats returned error: %v", err) + } + + want := &ListCopilotSeatsResponse{ + TotalSeats: 2, + Seats: []*CopilotSeatDetails{ + { + Assignee: &User{ + Login: String("octocat"), + ID: Int64(1), + NodeID: String("MDQ6VXNlcjE="), + AvatarURL: String("https://github.com/images/error/octocat_happy.gif"), + GravatarID: String(""), + URL: String("https://api.github.com/users/octocat"), + HTMLURL: String("https://github.com/octocat"), + FollowersURL: String("https://api.github.com/users/octocat/followers"), + FollowingURL: String("https://api.github.com/users/octocat/following{/other_user}"), + GistsURL: String("https://api.github.com/users/octocat/gists{/gist_id}"), + StarredURL: String("https://api.github.com/users/octocat/starred{/owner}{/repo}"), + SubscriptionsURL: String("https://api.github.com/users/octocat/subscriptions"), + OrganizationsURL: String("https://api.github.com/users/octocat/orgs"), + ReposURL: String("https://api.github.com/users/octocat/repos"), + EventsURL: String("https://api.github.com/users/octocat/events{/privacy}"), + ReceivedEventsURL: String("https://api.github.com/users/octocat/received_events"), + Type: String("User"), + SiteAdmin: Bool(false), + }, + AssigningTeam: &Team{ + ID: Int64(1), + NodeID: String("MDQ6VGVhbTE="), + URL: String("https://api.github.com/teams/1"), + HTMLURL: String("https://github.com/orgs/github/teams/justice-league"), + Name: String("Justice League"), + Slug: String("justice-league"), + Description: String("A great team."), + Privacy: String("closed"), + Permission: String("admin"), + MembersURL: String("https://api.github.com/teams/1/members{/member}"), + RepositoriesURL: String("https://api.github.com/teams/1/repos"), + Parent: nil, + }, + CreatedAt: &createdAt1, + UpdatedAt: &updatedAt1, + PendingCancellationDate: nil, + LastActivityAt: &lastActivityAt1, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + PlanType: String("business"), + }, + { + Assignee: &User{ + Login: String("octokitten"), + ID: Int64(1), + NodeID: String("MDQ76VNlcjE="), + AvatarURL: String("https://github.com/images/error/octokitten_happy.gif"), + GravatarID: String(""), + URL: String("https://api.github.com/users/octokitten"), + HTMLURL: String("https://github.com/octokitten"), + FollowersURL: String("https://api.github.com/users/octokitten/followers"), + FollowingURL: String("https://api.github.com/users/octokitten/following{/other_user}"), + GistsURL: String("https://api.github.com/users/octokitten/gists{/gist_id}"), + StarredURL: String("https://api.github.com/users/octokitten/starred{/owner}{/repo}"), + SubscriptionsURL: String("https://api.github.com/users/octokitten/subscriptions"), + OrganizationsURL: String("https://api.github.com/users/octokitten/orgs"), + ReposURL: String("https://api.github.com/users/octokitten/repos"), + EventsURL: String("https://api.github.com/users/octokitten/events{/privacy}"), + ReceivedEventsURL: String("https://api.github.com/users/octokitten/received_events"), + Type: String("User"), + SiteAdmin: Bool(false), + }, + AssigningTeam: nil, + CreatedAt: &createdAt2, + UpdatedAt: &updatedAt2, + PendingCancellationDate: String("2021-11-01"), + LastActivityAt: &lastActivityAt2, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + PlanType: nil, + }, + }, + } + + if !cmp.Equal(got, want) { + log.Printf("got: %+v", got.Seats[1]) + log.Printf("want: %+v", want.Seats[1]) + t.Errorf("Copilot.ListCopilotEnterpriseSeats returned %+v, want %+v", got, want) + } + + const methodName = "ListCopilotEnterpriseSeats" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.ListCopilotEnterpriseSeats(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.ListCopilotEnterpriseSeats(ctx, "e", opts) + if got != nil { + t.Errorf("Copilot.ListCopilotEnterpriseSeats returned %+v, want nil", got) + } + return resp, err + }) +} + func TestCopilotService_AddCopilotTeams(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -896,3 +1126,807 @@ func TestCopilotService_GetSeatDetails(t *testing.T) { return resp, err }) } + +func TestCopilotService_GetOrganisationUsage(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/copilot/usage", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "day": "2023-10-15", + "total_suggestions_count": 1000, + "total_acceptances_count": 800, + "total_lines_suggested": 1800, + "total_lines_accepted": 1200, + "total_active_users": 10, + "total_chat_acceptances": 32, + "total_chat_turns": 200, + "total_active_chat_users": 4, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 250, + "lines_suggested": 900, + "lines_accepted": 700, + "active_users": 5 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 400, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 400, + "acceptances_count": 350, + "lines_suggested": 500, + "lines_accepted": 200, + "active_users": 3 + } + ] + }, + { + "day": "2023-10-16", + "total_suggestions_count": 800, + "total_acceptances_count": 600, + "total_lines_suggested": 1100, + "total_lines_accepted": 700, + "total_active_users": 12, + "total_chat_acceptances": 57, + "total_chat_turns": 426, + "total_active_chat_users": 8, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 600, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 150, + "lines_suggested": 300, + "lines_accepted": 250, + "active_users": 6 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 200, + "acceptances_count": 150, + "lines_suggested": 200, + "lines_accepted": 150, + "active_users": 3 + } + ] + } + ]`) + }) + + summaryOne := time.Date(2023, time.October, 15, 0, 0, 0, 0, time.UTC) + summaryTwoDate := time.Date(2023, time.October, 16, 0, 0, 0, 0, time.UTC) + ctx := context.Background() + got, _, err := client.Copilot.GetOrganizationUsage(ctx, "o", &CopilotUsageSummaryListOptions{}) + if err != nil { + t.Errorf("Copilot.GetOrganizationUsage returned error: %v", err) + } + + want := []*CopilotUsageSummary{ + { + Day: summaryOne.Format("2006-01-02"), + TotalSuggestionsCount: 1000, + TotalAcceptancesCount: 800, + TotalLinesSuggested: 1800, + TotalLinesAccepted: 1200, + TotalActiveUsers: 10, + TotalChatAcceptances: 32, + TotalChatTurns: 200, + TotalActiveChatUsers: 4, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 250, + LinesSuggested: 900, + LinesAccepted: 700, + ActiveUsers: 5, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 400, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 400, + AcceptancesCount: 350, + LinesSuggested: 500, + LinesAccepted: 200, + ActiveUsers: 3, + }, + }, + }, + { + Day: summaryTwoDate.Format("2006-01-02"), + TotalSuggestionsCount: 800, + TotalAcceptancesCount: 600, + TotalLinesSuggested: 1100, + TotalLinesAccepted: 700, + TotalActiveUsers: 12, + TotalChatAcceptances: 57, + TotalChatTurns: 426, + TotalActiveChatUsers: 8, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 600, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 150, + LinesSuggested: 300, + LinesAccepted: 250, + ActiveUsers: 6, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 200, + AcceptancesCount: 150, + LinesSuggested: 200, + LinesAccepted: 150, + ActiveUsers: 3, + }, + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetOrganizationUsage returned %+v, want %+v", got, want) + } + + const methodName = "GetOrganizationUsage" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetOrganizationUsage(ctx, "\n", &CopilotUsageSummaryListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetOrganizationUsage(ctx, "o", &CopilotUsageSummaryListOptions{}) + if got != nil { + t.Errorf("Copilot.GetOrganizationUsage returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_GetEnterpriseUsage(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/copilot/usage", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "day": "2023-10-15", + "total_suggestions_count": 5000, + "total_acceptances_count": 3000, + "total_lines_suggested": 7000, + "total_lines_accepted": 3500, + "total_active_users": 15, + "total_chat_acceptances": 45, + "total_chat_turns": 350, + "total_active_chat_users": 8, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 3000, + "acceptances_count": 2000, + "lines_suggested": 3000, + "lines_accepted": 1500, + "active_users": 5 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 1000, + "acceptances_count": 500, + "lines_suggested": 2000, + "lines_accepted": 1000, + "active_users": 5 + }, + { + "language": "javascript", + "editor": "vscode", + "suggestions_count": 1000, + "acceptances_count": 500, + "lines_suggested": 2000, + "lines_accepted": 1000, + "active_users": 5 + } + ] + }, + { + "day": "2023-10-16", + "total_suggestions_count": 5200, + "total_acceptances_count": 5100, + "total_lines_suggested": 5300, + "total_lines_accepted": 5000, + "total_active_users": 15, + "total_chat_acceptances": 57, + "total_chat_turns": 455, + "total_active_chat_users": 12, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 3100, + "acceptances_count": 3000, + "lines_suggested": 3200, + "lines_accepted": 3100, + "active_users": 5 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 1100, + "acceptances_count": 1000, + "lines_suggested": 1200, + "lines_accepted": 1100, + "active_users": 5 + }, + { + "language": "javascript", + "editor": "vscode", + "suggestions_count": 1000, + "acceptances_count": 900, + "lines_suggested": 1100, + "lines_accepted": 1000, + "active_users": 5 + } + ] + } + ]`) + }) + + summaryOne := time.Date(2023, time.October, 15, 0, 0, 0, 0, time.UTC) + summaryTwoDate := time.Date(2023, time.October, 16, 0, 0, 0, 0, time.UTC) + ctx := context.Background() + got, _, err := client.Copilot.GetEnterpriseUsage(ctx, "e", &CopilotUsageSummaryListOptions{}) + if err != nil { + t.Errorf("Copilot.GetEnterpriseUsage returned error: %v", err) + } + + want := []*CopilotUsageSummary{ + { + Day: summaryOne.Format("2006-01-02"), + TotalSuggestionsCount: 5000, + TotalAcceptancesCount: 3000, + TotalLinesSuggested: 7000, + TotalLinesAccepted: 3500, + TotalActiveUsers: 15, + TotalChatAcceptances: 45, + TotalChatTurns: 350, + TotalActiveChatUsers: 8, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 3000, + AcceptancesCount: 2000, + LinesSuggested: 3000, + LinesAccepted: 1500, + ActiveUsers: 5, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 1000, + AcceptancesCount: 500, + LinesSuggested: 2000, + LinesAccepted: 1000, + ActiveUsers: 5, + }, + { + Language: "javascript", + Editor: "vscode", + SuggestionsCount: 1000, + AcceptancesCount: 500, + LinesSuggested: 2000, + LinesAccepted: 1000, + ActiveUsers: 5, + }, + }, + }, + { + Day: summaryTwoDate.Format("2006-01-02"), + TotalSuggestionsCount: 5200, + TotalAcceptancesCount: 5100, + TotalLinesSuggested: 5300, + TotalLinesAccepted: 5000, + TotalActiveUsers: 15, + TotalChatAcceptances: 57, + TotalChatTurns: 455, + TotalActiveChatUsers: 12, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 3100, + AcceptancesCount: 3000, + LinesSuggested: 3200, + LinesAccepted: 3100, + ActiveUsers: 5, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 1100, + AcceptancesCount: 1000, + LinesSuggested: 1200, + LinesAccepted: 1100, + ActiveUsers: 5, + }, + { + Language: "javascript", + Editor: "vscode", + SuggestionsCount: 1000, + AcceptancesCount: 900, + LinesSuggested: 1100, + LinesAccepted: 1000, + ActiveUsers: 5, + }, + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetEnterpriseUsage returned %+v, want %+v", got, want) + } + + const methodName = "GetEnterpriseUsage" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetEnterpriseUsage(ctx, "\n", &CopilotUsageSummaryListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetEnterpriseUsage(ctx, "e", &CopilotUsageSummaryListOptions{}) + if got != nil { + t.Errorf("Copilot.GetEnterpriseUsage returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_GetEnterpriseTeamUsage(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/team/t/copilot/usage", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "day": "2023-10-15", + "total_suggestions_count": 1000, + "total_acceptances_count": 800, + "total_lines_suggested": 1800, + "total_lines_accepted": 1200, + "total_active_users": 10, + "total_chat_acceptances": 32, + "total_chat_turns": 200, + "total_active_chat_users": 4, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 250, + "lines_suggested": 900, + "lines_accepted": 700, + "active_users": 5 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 400, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 400, + "acceptances_count": 350, + "lines_suggested": 500, + "lines_accepted": 200, + "active_users": 3 + } + ] + }, + { + "day": "2023-10-16", + "total_suggestions_count": 800, + "total_acceptances_count": 600, + "total_lines_suggested": 1100, + "total_lines_accepted": 700, + "total_active_users": 12, + "total_chat_acceptances": 57, + "total_chat_turns": 426, + "total_active_chat_users": 8, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 600, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 150, + "lines_suggested": 300, + "lines_accepted": 250, + "active_users": 6 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 200, + "acceptances_count": 150, + "lines_suggested": 200, + "lines_accepted": 150, + "active_users": 3 + } + ] + } + ]`) + }) + + summaryOne := time.Date(2023, time.October, 15, 0, 0, 0, 0, time.UTC) + summaryTwoDate := time.Date(2023, time.October, 16, 0, 0, 0, 0, time.UTC) + ctx := context.Background() + got, _, err := client.Copilot.GetEnterpriseTeamUsage(ctx, "e", "t", &CopilotUsageSummaryListOptions{}) + if err != nil { + t.Errorf("Copilot.GetEnterpriseTeamUsage returned error: %v", err) + } + + want := []*CopilotUsageSummary{ + { + Day: summaryOne.Format("2006-01-02"), + TotalSuggestionsCount: 1000, + TotalAcceptancesCount: 800, + TotalLinesSuggested: 1800, + TotalLinesAccepted: 1200, + TotalActiveUsers: 10, + TotalChatAcceptances: 32, + TotalChatTurns: 200, + TotalActiveChatUsers: 4, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 250, + LinesSuggested: 900, + LinesAccepted: 700, + ActiveUsers: 5, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 400, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 400, + AcceptancesCount: 350, + LinesSuggested: 500, + LinesAccepted: 200, + ActiveUsers: 3, + }, + }, + }, + { + Day: summaryTwoDate.Format("2006-01-02"), + TotalSuggestionsCount: 800, + TotalAcceptancesCount: 600, + TotalLinesSuggested: 1100, + TotalLinesAccepted: 700, + TotalActiveUsers: 12, + TotalChatAcceptances: 57, + TotalChatTurns: 426, + TotalActiveChatUsers: 8, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 600, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 150, + LinesSuggested: 300, + LinesAccepted: 250, + ActiveUsers: 6, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 200, + AcceptancesCount: 150, + LinesSuggested: 200, + LinesAccepted: 150, + ActiveUsers: 3, + }, + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetEnterpriseTeamUsage returned %+v, want %+v", got, want) + } + + const methodName = "GetEnterpriseTeamUsage" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetEnterpriseTeamUsage(ctx, "\n", "\n", &CopilotUsageSummaryListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetEnterpriseTeamUsage(ctx, "e", "t", &CopilotUsageSummaryListOptions{}) + if got != nil { + t.Errorf("Copilot.GetEnterpriseTeamUsage returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_GetOrganizationTeamUsage(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/team/t/copilot/usage", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "day": "2023-10-15", + "total_suggestions_count": 1000, + "total_acceptances_count": 800, + "total_lines_suggested": 1800, + "total_lines_accepted": 1200, + "total_active_users": 10, + "total_chat_acceptances": 32, + "total_chat_turns": 200, + "total_active_chat_users": 4, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 250, + "lines_suggested": 900, + "lines_accepted": 700, + "active_users": 5 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 400, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 400, + "acceptances_count": 350, + "lines_suggested": 500, + "lines_accepted": 200, + "active_users": 3 + } + ] + }, + { + "day": "2023-10-16", + "total_suggestions_count": 800, + "total_acceptances_count": 600, + "total_lines_suggested": 1100, + "total_lines_accepted": 700, + "total_active_users": 12, + "total_chat_acceptances": 57, + "total_chat_turns": 426, + "total_active_chat_users": 8, + "breakdown": [ + { + "language": "python", + "editor": "vscode", + "suggestions_count": 300, + "acceptances_count": 200, + "lines_suggested": 600, + "lines_accepted": 300, + "active_users": 2 + }, + { + "language": "python", + "editor": "jetbrains", + "suggestions_count": 300, + "acceptances_count": 150, + "lines_suggested": 300, + "lines_accepted": 250, + "active_users": 6 + }, + { + "language": "ruby", + "editor": "vscode", + "suggestions_count": 200, + "acceptances_count": 150, + "lines_suggested": 200, + "lines_accepted": 150, + "active_users": 3 + } + ] + } + ]`) + }) + + summaryOne := time.Date(2023, time.October, 15, 0, 0, 0, 0, time.UTC) + summaryTwoDate := time.Date(2023, time.October, 16, 0, 0, 0, 0, time.UTC) + ctx := context.Background() + got, _, err := client.Copilot.GetOrganizationTeamUsage(ctx, "o", "t", &CopilotUsageSummaryListOptions{}) + if err != nil { + t.Errorf("Copilot.GetOrganizationTeamUsage returned error: %v", err) + } + + want := []*CopilotUsageSummary{ + { + Day: summaryOne.Format("2006-01-02"), + TotalSuggestionsCount: 1000, + TotalAcceptancesCount: 800, + TotalLinesSuggested: 1800, + TotalLinesAccepted: 1200, + TotalActiveUsers: 10, + TotalChatAcceptances: 32, + TotalChatTurns: 200, + TotalActiveChatUsers: 4, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 250, + LinesSuggested: 900, + LinesAccepted: 700, + ActiveUsers: 5, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 400, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 400, + AcceptancesCount: 350, + LinesSuggested: 500, + LinesAccepted: 200, + ActiveUsers: 3, + }, + }, + }, + { + Day: summaryTwoDate.Format("2006-01-02"), + TotalSuggestionsCount: 800, + TotalAcceptancesCount: 600, + TotalLinesSuggested: 1100, + TotalLinesAccepted: 700, + TotalActiveUsers: 12, + TotalChatAcceptances: 57, + TotalChatTurns: 426, + TotalActiveChatUsers: 8, + Breakdown: []*CopilotUsageBreakdown{ + { + Language: "python", + Editor: "vscode", + SuggestionsCount: 300, + AcceptancesCount: 200, + LinesSuggested: 600, + LinesAccepted: 300, + ActiveUsers: 2, + }, + { + Language: "python", + Editor: "jetbrains", + SuggestionsCount: 300, + AcceptancesCount: 150, + LinesSuggested: 300, + LinesAccepted: 250, + ActiveUsers: 6, + }, + { + Language: "ruby", + Editor: "vscode", + SuggestionsCount: 200, + AcceptancesCount: 150, + LinesSuggested: 200, + LinesAccepted: 150, + ActiveUsers: 3, + }, + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetOrganizationTeamUsage returned %+v, want %+v", got, want) + } + + const methodName = "GetOrganizationTeamUsage" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetOrganizationTeamUsage(ctx, "\n", "\n", &CopilotUsageSummaryListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetOrganizationTeamUsage(ctx, "o", "t", &CopilotUsageSummaryListOptions{}) + if got != nil { + t.Errorf("Copilot.GetOrganizationTeamUsage returned %+v, want nil", got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 7995145faa..a43ce8b9bd 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4294,6 +4294,14 @@ func (c *CopilotSeatDetails) GetPendingCancellationDate() string { return *c.PendingCancellationDate } +// GetPlanType returns the PlanType field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetPlanType() string { + if c == nil || c.PlanType == nil { + return "" + } + return *c.PlanType +} + // GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. func (c *CopilotSeatDetails) GetUpdatedAt() Timestamp { if c == nil || c.UpdatedAt == nil { @@ -4302,6 +4310,22 @@ func (c *CopilotSeatDetails) GetUpdatedAt() Timestamp { return *c.UpdatedAt } +// GetSince returns the Since field if it's non-nil, zero value otherwise. +func (c *CopilotUsageSummaryListOptions) GetSince() time.Time { + if c == nil || c.Since == nil { + return time.Time{} + } + return *c.Since +} + +// GetUntil returns the Until field if it's non-nil, zero value otherwise. +func (c *CopilotUsageSummaryListOptions) GetUntil() time.Time { + if c == nil || c.Until == nil { + return time.Time{} + } + return *c.Until +} + // GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. func (c *CreateCheckRunOptions) GetCompletedAt() Timestamp { if c == nil || c.CompletedAt == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index bc6e2be8d0..a4d5315ba4 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -5573,6 +5573,17 @@ func TestCopilotSeatDetails_GetPendingCancellationDate(tt *testing.T) { c.GetPendingCancellationDate() } +func TestCopilotSeatDetails_GetPlanType(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CopilotSeatDetails{PlanType: &zeroValue} + c.GetPlanType() + c = &CopilotSeatDetails{} + c.GetPlanType() + c = nil + c.GetPlanType() +} + func TestCopilotSeatDetails_GetUpdatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -5584,6 +5595,28 @@ func TestCopilotSeatDetails_GetUpdatedAt(tt *testing.T) { c.GetUpdatedAt() } +func TestCopilotUsageSummaryListOptions_GetSince(tt *testing.T) { + tt.Parallel() + var zeroValue time.Time + c := &CopilotUsageSummaryListOptions{Since: &zeroValue} + c.GetSince() + c = &CopilotUsageSummaryListOptions{} + c.GetSince() + c = nil + c.GetSince() +} + +func TestCopilotUsageSummaryListOptions_GetUntil(tt *testing.T) { + tt.Parallel() + var zeroValue time.Time + c := &CopilotUsageSummaryListOptions{Until: &zeroValue} + c.GetUntil() + c = &CopilotUsageSummaryListOptions{} + c.GetUntil() + c = nil + c.GetUntil() +} + func TestCreateCheckRunOptions_GetCompletedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp