Skip to content

Commit 9027488

Browse files
authored
Add GitHub Copilot Connection and associated dashboards (#8728)
* feat(gh-copilot): add backend plugin * feat(config-ui): add gh-copilot connection UI * feat(grafana): add GitHub Copilot dashboards * fix: gh-copilot CI fixes
1 parent df683a6 commit 9027488

File tree

99 files changed

+6805
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+6805
-103
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ bin
150150
libgit2
151151
.air.toml
152152

153+
# Playwright CLI snapshots/logs (local dev)
154+
.playwright-cli/
155+
153156
# auto generated code
154157
backend/mocks/
155158
backend/server/api/docs/swagger.json
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
# GitHub Copilot Plugin (Adoption Metrics)
18+
19+
This plugin ingests GitHub Copilot **organization-level adoption metrics** (daily usage and seat assignments) and provides a Grafana dashboard for adoption trends.
20+
21+
It follows the same structure/patterns as other DevLake data-source plugins (notably `backend/plugins/q_dev`).
22+
23+
## What it collects
24+
25+
**Phase 1 endpoints** (GitHub Copilot REST API):
26+
27+
- `GET /orgs/{org}/copilot/billing`
28+
- `GET /orgs/{org}/copilot/billing/seats`
29+
- `GET /orgs/{org}/copilot/metrics`
30+
31+
**Stored data (tool layer)**:
32+
33+
- `_tool_copilot_org_metrics` (daily aggregates)
34+
- `_tool_copilot_language_metrics` (editor/language breakdown)
35+
- `_tool_copilot_seats` (seat assignments)
36+
37+
## Data flow (high level)
38+
39+
```mermaid
40+
flowchart LR
41+
GH[GitHub Copilot REST API]
42+
RAW[(Raw tables\n_raw_copilot_*)]
43+
TOOL[(Tool tables\n_tool_copilot_*)]
44+
GRAF[Grafana Dashboard\nGitHub Copilot Adoption]
45+
46+
GH --> RAW --> TOOL --> GRAF
47+
```
48+
49+
## Repository layout
50+
51+
- `api/` – REST layer for connections/scopes
52+
- `impl/` – plugin meta, options, connection helpers
53+
- `models/` – tool-layer models + migrations
54+
- `tasks/` – collectors/extractors and pipeline registration
55+
- `e2e/` – E2E fixtures and golden CSV assertions
56+
- `docs/` – documentation assets
57+
58+
## Setup
59+
60+
### Prerequisites
61+
62+
- GitHub Copilot Business or Enterprise enabled for the target organization
63+
- A token that can access GitHub Copilot billing/metrics (classic PAT with `manage_billing:copilot` works)
64+
65+
### 1) Create a connection
66+
67+
1. DevLake UI → **Data Integrations → Add Connection → GitHub Copilot**
68+
2. Fill in:
69+
- **Name**: e.g. `GitHub Copilot Octodemo`
70+
- **Endpoint**: defaults to `https://api.github.com`
71+
- **Organization**: GitHub org slug
72+
- **Token**: PAT with required scope
73+
3. Click **Test Connection** (calls `GET /orgs/{org}/copilot/billing`).
74+
4. Save the connection.
75+
76+
### 2) Create a scope
77+
78+
Add an organization scope for that connection. For Phase 1, `implementationDate` is optional.
79+
80+
### 3) Create a blueprint (recipe)
81+
82+
Use a blueprint plan like:
83+
84+
```json
85+
[
86+
[
87+
{
88+
"plugin": "gh-copilot",
89+
"options": {
90+
"connectionId": 1,
91+
"scopeId": "octodemo"
92+
}
93+
}
94+
]
95+
]
96+
```
97+
98+
Run the blueprint daily to keep metrics up to date.
99+
100+
## Dashboard
101+
102+
The Grafana dashboard JSON is in `grafana/dashboards/copilot/adoption.json`.
103+
104+
Link: `grafana/dashboards/copilot/adoption.json`
105+
106+
## Error handling guidance
107+
108+
- **403 Forbidden** → token missing required billing/metrics scope, or org lacks GitHub Copilot access
109+
- **404 Not Found** → incorrect org slug, or GitHub Copilot endpoints unavailable for the org
110+
- **422 Unprocessable Entity** → GitHub Copilot metrics disabled in GitHub org settings
111+
112+
- **429 Too Many Requests** → respect `Retry-After`; collectors implement backoff/retry
113+
114+
Tokens are sanitized before persisting. When patching an existing connection, omit the token to retain the encrypted value already stored in DevLake.
115+
116+
## Limitations (Phase 1)
117+
118+
- Metrics endpoint is limited to a rolling **100-day** window (GitHub API constraint)
119+
- GitHub enforces a privacy threshold (often **≥ 5 engaged users**) and may omit daily data
120+
- Enterprise download endpoints and per-user metrics (JSONL exports) are intentionally deferred to Phase 2+
121+
122+
## More docs
123+
124+
- Spec quickstart: `specs/001-copilot-metrics-plugin/quickstart.md`
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
coreModels "github.com/apache/incubator-devlake/core/models"
23+
"github.com/apache/incubator-devlake/core/plugin"
24+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
25+
"github.com/apache/incubator-devlake/helpers/srvhelper"
26+
"github.com/apache/incubator-devlake/plugins/gh-copilot/models"
27+
"github.com/apache/incubator-devlake/plugins/gh-copilot/tasks"
28+
)
29+
30+
// MakeDataSourcePipelinePlanV200 generates the pipeline plan for blueprint v2.0.0.
31+
func MakeDataSourcePipelinePlanV200(
32+
subtaskMetas []plugin.SubTaskMeta,
33+
connectionId uint64,
34+
bpScopes []*coreModels.BlueprintScope,
35+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
36+
// load connection and scopes from the db
37+
_, err := dsHelper.ConnSrv.FindByPk(connectionId)
38+
if err != nil {
39+
return nil, nil, err
40+
}
41+
// map blueprint scopes (scopeId/scopeConfigId) to concrete stored scopes
42+
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
47+
plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails)
48+
if err != nil {
49+
return nil, nil, err
50+
}
51+
52+
// Copilot metrics are org-level and currently don't map to a standard domain-layer top-level entity.
53+
// Return an empty scope list to avoid adding meaningless project mappings.
54+
return plan, nil, nil
55+
}
56+
57+
func makeDataSourcePipelinePlanV200(
58+
subtaskMetas []plugin.SubTaskMeta,
59+
scopeDetails []*srvhelper.ScopeDetail[models.GhCopilotScope, models.GhCopilotScopeConfig],
60+
) (coreModels.PipelinePlan, errors.Error) {
61+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
62+
for i, scopeDetail := range scopeDetails {
63+
stage := plan[i]
64+
if stage == nil {
65+
stage = coreModels.PipelineStage{}
66+
}
67+
68+
scope := scopeDetail.Scope
69+
task, err := helper.MakePipelinePlanTask(
70+
"gh-copilot",
71+
subtaskMetas,
72+
nil,
73+
tasks.GhCopilotOptions{
74+
ConnectionId: scope.ConnectionId,
75+
ScopeId: scope.Id,
76+
},
77+
)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
stage = append(stage, task)
83+
plan[i] = stage
84+
}
85+
86+
return plan, nil
87+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
"github.com/apache/incubator-devlake/core/plugin"
23+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
24+
"github.com/apache/incubator-devlake/plugins/gh-copilot/models"
25+
)
26+
27+
// PostConnections creates a new Copilot connection.
28+
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
29+
connection := &models.GhCopilotConnection{}
30+
if err := helper.Decode(input.Body, connection, vld); err != nil {
31+
return nil, err
32+
}
33+
34+
connection.Normalize()
35+
if err := validateConnection(connection); err != nil {
36+
return nil, err
37+
}
38+
39+
if err := connectionHelper.Create(connection, input); err != nil {
40+
return nil, err
41+
}
42+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
43+
}
44+
45+
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
46+
connection := &models.GhCopilotConnection{}
47+
if err := connectionHelper.First(connection, input.Params); err != nil {
48+
return nil, err
49+
}
50+
if err := (&models.GhCopilotConnection{}).MergeFromRequest(connection, input.Body); err != nil {
51+
return nil, errors.Convert(err)
52+
}
53+
connection.Normalize()
54+
if err := validateConnection(connection); err != nil {
55+
return nil, err
56+
}
57+
if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil {
58+
return nil, err
59+
}
60+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
61+
}
62+
63+
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
64+
conn := &models.GhCopilotConnection{}
65+
output, err := connectionHelper.Delete(conn, input)
66+
if err != nil {
67+
return output, err
68+
}
69+
output.Body = conn.Sanitize()
70+
return output, nil
71+
}
72+
73+
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
74+
var connections []models.GhCopilotConnection
75+
if err := connectionHelper.List(&connections); err != nil {
76+
return nil, err
77+
}
78+
for i := range connections {
79+
connections[i] = connections[i].Sanitize()
80+
}
81+
return &plugin.ApiResourceOutput{Body: connections}, nil
82+
}
83+
84+
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
85+
connection := &models.GhCopilotConnection{}
86+
if err := connectionHelper.First(connection, input.Params); err != nil {
87+
return nil, err
88+
}
89+
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
90+
}
91+
92+
func validateConnection(connection *models.GhCopilotConnection) errors.Error {
93+
if connection == nil {
94+
return errors.BadInput.New("connection is required")
95+
}
96+
if connection.Organization == "" && !connection.HasEnterprise() {
97+
return errors.BadInput.New("either enterprise or organization is required")
98+
}
99+
if connection.Token == "" {
100+
return errors.BadInput.New("token is required")
101+
}
102+
if connection.RateLimitPerHour < 0 {
103+
return errors.BadInput.New("rateLimitPerHour must be non-negative")
104+
}
105+
return nil
106+
}

0 commit comments

Comments
 (0)