From a8f8adcc71eafa6ee7f349e0671638cea0f30533 Mon Sep 17 00:00:00 2001 From: Stas Ostrovskyi <7407450+stasostrovskyi@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:34:43 +0200 Subject: [PATCH 1/3] Add function to get copilot org usage --- github/copilot.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/github/copilot.go b/github/copilot.go index 2697b71850c..4dd08fe65e9 100644 --- a/github/copilot.go +++ b/github/copilot.go @@ -63,6 +63,29 @@ type SeatCancellations struct { SeatsCancelled int `json:"seats_cancelled"` } +type CopilotUsageBreakdown struct { + Language string `json:"language"` + Editor string `json:"editor"` + SuggestionsCount int `json:"suggestions_count"` + AcceptancesCount int `json:"acceptances_count"` + LinesSuggested int `json:"lines_suggested"` + LinesAccepted int `json:"lines_accepted"` + ActiveUsers int `json:"active_users"` +} + +type CopilotUsageSummary struct { + Day string `json:"day"` + TotalSuggestionsCount int `json:"total_suggestions_count"` + TotalAcceptancesCount int `json:"total_acceptances_count"` + TotalLinesSuggested int `json:"total_lines_suggested"` + TotalLinesAccepted int `json:"total_lines_accepted"` + TotalActiveUsers int `json:"total_active_users"` + TotalChatAcceptances int `json:"total_chat_acceptances"` + TotalChatTurns int `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 @@ -313,3 +336,25 @@ 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/en/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) ([]*CopilotUsageSummary, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/usage", org) + + 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 +} From e579fb1a55f01b812fe78320e5da8a9769f889db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Lundstr=C3=B6m?= Date: Mon, 7 Oct 2024 16:44:43 +0200 Subject: [PATCH 2/3] Support list options for Copilot usage summary --- github/copilot.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/github/copilot.go b/github/copilot.go index 4dd08fe65e9..7be6650c0b0 100644 --- a/github/copilot.go +++ b/github/copilot.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "time" ) // CopilotService provides access to the Copilot-related functions @@ -63,6 +64,13 @@ type SeatCancellations struct { SeatsCancelled int `json:"seats_cancelled"` } +type CopilotUsageSummaryListOptions struct { + Since time.Time `url:"since,omitempty"` + Until time.Time `url:"until,omitempty"` + + ListOptions +} + type CopilotUsageBreakdown struct { Language string `json:"language"` Editor string `json:"editor"` @@ -74,16 +82,16 @@ type CopilotUsageBreakdown struct { } type CopilotUsageSummary struct { - Day string `json:"day"` - TotalSuggestionsCount int `json:"total_suggestions_count"` - TotalAcceptancesCount int `json:"total_acceptances_count"` - TotalLinesSuggested int `json:"total_lines_suggested"` - TotalLinesAccepted int `json:"total_lines_accepted"` - TotalActiveUsers int `json:"total_active_users"` - TotalChatAcceptances int `json:"total_chat_acceptances"` - TotalChatTurns int `json:"total_chat_turns"` - TotalActiveChatUsers int `json:"total_active_chat_users"` - Breakdown []CopilotUsageBreakdown `json:"breakdown"` + Day string `json:"day"` + TotalSuggestionsCount int `json:"total_suggestions_count"` + TotalAcceptancesCount int `json:"total_acceptances_count"` + TotalLinesSuggested int `json:"total_lines_suggested"` + TotalLinesAccepted int `json:"total_lines_accepted"` + TotalActiveUsers int `json:"total_active_users"` + TotalChatAcceptances int `json:"total_chat_acceptances"` + TotalChatTurns int `json:"total_chat_turns"` + TotalActiveChatUsers int `json:"total_active_chat_users"` + Breakdown []*CopilotUsageBreakdown `json:"breakdown"` } func (cp *CopilotSeatDetails) UnmarshalJSON(data []byte) error { @@ -342,8 +350,12 @@ func (s *CopilotService) GetSeatDetails(ctx context.Context, org, user string) ( // GitHub API docs: https://docs.github.com/en/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) ([]*CopilotUsageSummary, *Response, error) { +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 { From c082b15396c9837ab253e1e141ebffe15882e970 Mon Sep 17 00:00:00 2001 From: Clemens W Date: Wed, 9 Oct 2024 14:29:18 +0200 Subject: [PATCH 3/3] Added test case --- github/copilot_test.go | 200 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/github/copilot_test.go b/github/copilot_test.go index 355ff130c85..7f1913a3512 100644 --- a/github/copilot_test.go +++ b/github/copilot_test.go @@ -883,3 +883,203 @@ func TestCopilotService_GetSeatDetails(t *testing.T) { return resp, err }) } + +func TestCopilotService_GetOrganisationUsage(t *testing.T) { + 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 + }) +}