Skip to content

Commit ce453d8

Browse files
committed
Adding copilot metrics
1 parent 6e259a7 commit ce453d8

19 files changed

+25250
-2180
lines changed

package-lock.json

Lines changed: 21714 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/github/client/client.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,77 @@ func (client *Client) getWorkflowRuns(ctx context.Context, owner, repo, workflow
358358

359359
return workflowRuns, response.NextPage, nil
360360
}
361+
362+
// GetCopilotMetrics sends a request to the GitHub REST API to get Copilot metrics for an organization
363+
func (client *Client) GetCopilotMetrics(ctx context.Context, organization string, opts models.ListCopilotMetricsOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
364+
u := fmt.Sprintf("orgs/%s/copilot/metrics", organization)
365+
366+
// Build query parameters
367+
params := url.Values{}
368+
if opts.Since != nil {
369+
params.Add("since", opts.Since.Format("2006-01-02"))
370+
}
371+
if opts.Until != nil {
372+
params.Add("until", opts.Until.Format("2006-01-02"))
373+
}
374+
if opts.Page > 0 {
375+
params.Add("page", strconv.Itoa(opts.Page))
376+
}
377+
if opts.PerPage > 0 {
378+
params.Add("per_page", strconv.Itoa(opts.PerPage))
379+
}
380+
381+
if len(params) > 0 {
382+
u += "?" + params.Encode()
383+
}
384+
385+
req, err := client.restClient.NewRequest("GET", u, nil)
386+
if err != nil {
387+
return nil, nil, err
388+
}
389+
390+
var metrics []models.CopilotMetrics
391+
resp, err := client.restClient.Do(ctx, req, &metrics)
392+
if err != nil {
393+
return nil, resp, addErrorSourceToError(err, resp)
394+
}
395+
396+
return metrics, resp, nil
397+
}
398+
399+
// GetCopilotMetricsTeam sends a request to the GitHub REST API to get Copilot metrics for a team
400+
func (client *Client) GetCopilotMetricsTeam(ctx context.Context, organization, teamSlug string, opts models.ListCopilotMetricsTeamOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
401+
u := fmt.Sprintf("orgs/%s/team/%s/copilot/metrics", organization, teamSlug)
402+
403+
// Build query parameters
404+
params := url.Values{}
405+
if opts.Since != nil {
406+
params.Add("since", opts.Since.Format("2006-01-02"))
407+
}
408+
if opts.Until != nil {
409+
params.Add("until", opts.Until.Format("2006-01-02"))
410+
}
411+
if opts.Page > 0 {
412+
params.Add("page", strconv.Itoa(opts.Page))
413+
}
414+
if opts.PerPage > 0 {
415+
params.Add("per_page", strconv.Itoa(opts.PerPage))
416+
}
417+
418+
if len(params) > 0 {
419+
u += "?" + params.Encode()
420+
}
421+
422+
req, err := client.restClient.NewRequest("GET", u, nil)
423+
if err != nil {
424+
return nil, nil, err
425+
}
426+
427+
var metrics []models.CopilotMetrics
428+
resp, err := client.restClient.Do(ctx, req, &metrics)
429+
if err != nil {
430+
return nil, resp, addErrorSourceToError(err, resp)
431+
}
432+
433+
return metrics, resp, nil
434+
}

pkg/github/codescanning_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ func (m *mockClient) ListAlertsForOrg(ctx context.Context, owner string, opts *g
5252
return m.mockAlerts, m.mockResponse, nil
5353
}
5454

55+
func (m *mockClient) GetCopilotMetrics(ctx context.Context, organization string, opts models.ListCopilotMetricsOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
56+
return nil, nil, nil
57+
}
58+
59+
func (m *mockClient) GetCopilotMetricsTeam(ctx context.Context, organization, teamSlug string, opts models.ListCopilotMetricsTeamOptions) ([]models.CopilotMetrics, *googlegithub.Response, error) {
60+
return nil, nil, nil
61+
}
62+
5563
func TestGetCodeScanningAlerts(t *testing.T) {
5664
var (
5765
ctx = context.Background()

pkg/github/copilot_metrics.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
"github.com/grafana/github-datasource/pkg/dfutil"
10+
"github.com/grafana/github-datasource/pkg/models"
11+
"github.com/grafana/grafana-plugin-sdk-go/data"
12+
)
13+
14+
// CopilotMetricsResponse represents the response from GitHub's Copilot metrics API
15+
type CopilotMetricsResponse []models.CopilotMetrics
16+
17+
// GetCopilotMetrics retrieves Copilot metrics for an organization
18+
func GetCopilotMetrics(ctx context.Context, client models.Client, opts models.ListCopilotMetricsOptions) (dfutil.Framer, error) {
19+
metrics, _, err := client.GetCopilotMetrics(ctx, opts.Organization, opts)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
return copilotMetricsToDataFrame(CopilotMetricsResponse(metrics), "copilot_metrics")
25+
}
26+
27+
// GetCopilotMetricsTeam retrieves Copilot metrics for a team
28+
func GetCopilotMetricsTeam(ctx context.Context, client models.Client, opts models.ListCopilotMetricsTeamOptions) (dfutil.Framer, error) {
29+
metrics, _, err := client.GetCopilotMetricsTeam(ctx, opts.Organization, opts.TeamSlug, opts)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
return copilotMetricsToDataFrame(CopilotMetricsResponse(metrics), "copilot_metrics_team")
35+
}
36+
37+
// copilotMetricsToDataFrame converts Copilot metrics to a Grafana data frame
38+
func copilotMetricsToDataFrame(metrics CopilotMetricsResponse, name string) (dfutil.Framer, error) {
39+
return metrics, nil
40+
}
41+
42+
// Frames converts the list of copilot metrics to a Grafana DataFrame
43+
func (c CopilotMetricsResponse) Frames() data.Frames {
44+
frame := data.NewFrame("copilot_metrics")
45+
46+
if len(c) == 0 {
47+
return data.Frames{frame}
48+
}
49+
50+
// Create time series for the main metrics
51+
dates := make([]time.Time, len(c))
52+
totalActiveUsers := make([]int, len(c))
53+
totalEngagedUsers := make([]int, len(c))
54+
ideCompletionUsers := make([]int, len(c))
55+
ideChatUsers := make([]int, len(c))
56+
dotcomChatUsers := make([]int, len(c))
57+
dotcomPRUsers := make([]int, len(c))
58+
59+
for i, metric := range c {
60+
date, err := time.Parse("2006-01-02", metric.Date)
61+
if err != nil {
62+
// If date parsing fails, use a default date
63+
date = time.Now().AddDate(0, 0, -i)
64+
}
65+
66+
dates[i] = date
67+
totalActiveUsers[i] = metric.TotalActiveUsers
68+
totalEngagedUsers[i] = metric.TotalEngagedUsers
69+
ideCompletionUsers[i] = metric.CopilotIDECodeCompletions.TotalEngagedUsers
70+
ideChatUsers[i] = metric.CopilotIDEChat.TotalEngagedUsers
71+
dotcomChatUsers[i] = metric.CopilotDotcomChat.TotalEngagedUsers
72+
dotcomPRUsers[i] = metric.CopilotDotcomPullRequests.TotalEngagedUsers
73+
}
74+
75+
// Add fields to the frame
76+
frame.Fields = append(frame.Fields, data.NewField("time", nil, dates))
77+
frame.Fields = append(frame.Fields, data.NewField("total_active_users", nil, totalActiveUsers))
78+
frame.Fields = append(frame.Fields, data.NewField("total_engaged_users", nil, totalEngagedUsers))
79+
frame.Fields = append(frame.Fields, data.NewField("ide_completion_users", nil, ideCompletionUsers))
80+
frame.Fields = append(frame.Fields, data.NewField("ide_chat_users", nil, ideChatUsers))
81+
frame.Fields = append(frame.Fields, data.NewField("dotcom_chat_users", nil, dotcomChatUsers))
82+
frame.Fields = append(frame.Fields, data.NewField("dotcom_pr_users", nil, dotcomPRUsers))
83+
84+
// Add language breakdown data if available
85+
if len(c) > 0 && len(c[0].CopilotIDECodeCompletions.Languages) > 0 {
86+
langData := make(map[string][]int)
87+
for _, metric := range c {
88+
for _, lang := range metric.CopilotIDECodeCompletions.Languages {
89+
if langData[lang.Name] == nil {
90+
langData[lang.Name] = make([]int, len(c))
91+
}
92+
}
93+
}
94+
95+
for i, metric := range c {
96+
for langName := range langData {
97+
found := false
98+
for _, lang := range metric.CopilotIDECodeCompletions.Languages {
99+
if lang.Name == langName {
100+
langData[langName][i] = lang.TotalEngagedUsers
101+
found = true
102+
break
103+
}
104+
}
105+
if !found {
106+
langData[langName][i] = 0
107+
}
108+
}
109+
}
110+
111+
for langName, users := range langData {
112+
fieldName := fmt.Sprintf("language_%s_users", langName)
113+
frame.Fields = append(frame.Fields, data.NewField(fieldName, nil, users))
114+
}
115+
}
116+
117+
// Add editor breakdown data if available
118+
if len(c) > 0 && len(c[0].CopilotIDECodeCompletions.Editors) > 0 {
119+
editorData := make(map[string][]int)
120+
for _, metric := range c {
121+
for _, editor := range metric.CopilotIDECodeCompletions.Editors {
122+
if editorData[editor.Name] == nil {
123+
editorData[editor.Name] = make([]int, len(c))
124+
}
125+
}
126+
}
127+
128+
for i, metric := range c {
129+
for editorName := range editorData {
130+
found := false
131+
for _, editor := range metric.CopilotIDECodeCompletions.Editors {
132+
if editor.Name == editorName {
133+
editorData[editorName][i] = editor.TotalEngagedUsers
134+
found = true
135+
break
136+
}
137+
}
138+
if !found {
139+
editorData[editorName][i] = 0
140+
}
141+
}
142+
}
143+
144+
for editorName, users := range editorData {
145+
fieldName := fmt.Sprintf("editor_%s_users", editorName)
146+
frame.Fields = append(frame.Fields, data.NewField(fieldName, nil, users))
147+
}
148+
}
149+
150+
// Add detailed JSON for complex nested data
151+
detailedData := make([]string, len(c))
152+
for i, metric := range c {
153+
jsonData, err := json.Marshal(metric)
154+
if err != nil {
155+
detailedData[i] = ""
156+
} else {
157+
detailedData[i] = string(jsonData)
158+
}
159+
}
160+
frame.Fields = append(frame.Fields, data.NewField("detailed_metrics", nil, detailedData))
161+
162+
return data.Frames{frame}
163+
}

pkg/github/copilot_metrics_handler.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/github-datasource/pkg/dfutil"
7+
"github.com/grafana/github-datasource/pkg/models"
8+
"github.com/grafana/grafana-plugin-sdk-go/backend"
9+
)
10+
11+
func (s *QueryHandler) handleCopilotMetricsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
12+
query := &models.CopilotMetricsQuery{}
13+
if err := UnmarshalQuery(q.JSON, query); err != nil {
14+
return *err
15+
}
16+
return dfutil.FrameResponseWithError(s.Datasource.HandleCopilotMetricsQuery(ctx, query, q))
17+
}
18+
19+
// HandleCopilotMetrics handles the plugin query for GitHub Copilot metrics
20+
func (s *QueryHandler) HandleCopilotMetrics(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
21+
return &backend.QueryDataResponse{
22+
Responses: processQueries(ctx, req, s.handleCopilotMetricsQuery),
23+
}, nil
24+
}
25+
26+
func (s *QueryHandler) handleCopilotMetricsTeamQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
27+
query := &models.CopilotMetricsTeamQuery{}
28+
if err := UnmarshalQuery(q.JSON, query); err != nil {
29+
return *err
30+
}
31+
return dfutil.FrameResponseWithError(s.Datasource.HandleCopilotMetricsTeamQuery(ctx, query, q))
32+
}
33+
34+
// HandleCopilotMetricsTeam handles the plugin query for GitHub Copilot metrics for a team
35+
func (s *QueryHandler) HandleCopilotMetricsTeam(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
36+
return &backend.QueryDataResponse{
37+
Responses: processQueries(ctx, req, s.handleCopilotMetricsTeamQuery),
38+
}, nil
39+
}

pkg/github/datasource.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ func (d *Datasource) HandleWorkflowRunsQuery(ctx context.Context, query *models.
212212
return GetWorkflowRuns(ctx, d.client, opt, req.TimeRange)
213213
}
214214

215+
// HandleCopilotMetricsQuery is the query handler for listing GitHub Copilot metrics for an organization
216+
func (d *Datasource) HandleCopilotMetricsQuery(ctx context.Context, query *models.CopilotMetricsQuery, req backend.DataQuery) (dfutil.Framer, error) {
217+
opt := models.CopilotMetricsOptionsWithOrg(query.Options, query.Owner)
218+
return GetCopilotMetrics(ctx, d.client, opt)
219+
}
220+
221+
// HandleCopilotMetricsTeamQuery is the query handler for listing GitHub Copilot metrics for a team
222+
func (d *Datasource) HandleCopilotMetricsTeamQuery(ctx context.Context, query *models.CopilotMetricsTeamQuery, req backend.DataQuery) (dfutil.Framer, error) {
223+
opt := models.CopilotMetricsTeamOptionsWithOrg(query.Options, query.Owner)
224+
return GetCopilotMetricsTeam(ctx, d.client, opt)
225+
}
226+
215227
// CheckHealth is the health check for GitHub
216228
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
217229
_, err := GetAllRepositories(ctx, d.client, models.ListRepositoriesOptions{

pkg/github/query_handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
6262
mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage)
6363
mux.HandleFunc(models.QueryTypeWorkflowRuns, s.HandleWorkflowRuns)
6464
mux.HandleFunc(models.QueryTypeCodeScanning, s.HandleCodeScanning)
65+
mux.HandleFunc(models.QueryTypeCopilotMetrics, s.HandleCopilotMetrics)
66+
mux.HandleFunc(models.QueryTypeCopilotMetricsTeam, s.HandleCopilotMetricsTeam)
6567

6668
return mux
6769
}

pkg/models/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ type Client interface {
1616
GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error)
1717
ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
1818
ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
19+
GetCopilotMetrics(ctx context.Context, organization string, opts ListCopilotMetricsOptions) ([]CopilotMetrics, *googlegithub.Response, error)
20+
GetCopilotMetricsTeam(ctx context.Context, organization, teamSlug string, opts ListCopilotMetricsTeamOptions) ([]CopilotMetrics, *googlegithub.Response, error)
1921
}

0 commit comments

Comments
 (0)