This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new a9650e4ca feat(gh-copilot): close Copilot metrics parity gaps (#8889)
a9650e4ca is described below
commit a9650e4ca045155604dea563152123042be68da2
Author: Jarek <[email protected]>
AuthorDate: Tue Jun 9 03:30:19 2026 +0200
feat(gh-copilot): close Copilot metrics parity gaps (#8889)
* feat(gh-copilot): close API gaps for per-user metrics, teams, CLI, code
review, and PR fields
Add missing GitHub Copilot Metrics API fields to achieve full API parity:
Enterprise/Org metrics:
- CLI active user counts and CLI breakdown (sessions, requests, tokens)
- Code review user counts (daily/weekly/monthly × active/passive)
- Chat panel mode breakdown (agent/ask/custom/edit/plan/unknown)
- Expanded PR metrics (merged, merge time, suggestions, Copilot impact)
Per-user metrics:
- used_cli, used_copilot_code_review_active/passive boolean flags
- CLI breakdown per user (sessions, requests, tokens)
User-team mapping (new):
- New collector/extractor for user-teams-1-day endpoint
- Enables team-level metrics via JOIN with per-user tables
Seat assignments:
- Team assignment fields (assigning_team id/name/slug)
- User detail fields (name, email)
Includes migration script 20260527 and comprehensive docs in
COPILOT_METRICS_GAPS.md.
Co-authored-by: Copilot <[email protected]>
* feat(copilot-metrics): remove outdated metrics gaps documentation and
implement new user-team mapping and metrics enhancements
Signed-off-by: Jarek <[email protected]>
---------
Signed-off-by: Jarek <[email protected]>
Co-authored-by: Copilot <[email protected]>
---
.../_tool_copilot_enterprise_daily_metrics.csv | 6 +-
.../snapshot_tables/_tool_copilot_seats.csv | 6 +-
.../gh-copilot/models/enterprise_metrics.go | 48 ++++++-
.../20260527_add_copilot_metrics_gaps.go | 153 +++++++++++++++++++++
.../gh-copilot/models/migrationscripts/register.go | 1 +
backend/plugins/gh-copilot/models/models.go | 2 +
backend/plugins/gh-copilot/models/models_test.go | 1 +
backend/plugins/gh-copilot/models/seat.go | 5 +
backend/plugins/gh-copilot/models/user_metrics.go | 14 +-
backend/plugins/gh-copilot/models/user_team.go | 45 ++++++
.../tasks/enterprise_metrics_extractor.go | 100 ++++++++++++--
.../plugins/gh-copilot/tasks/metrics_extractor.go | 46 +++++++
.../gh-copilot/tasks/org_metrics_collector.go | 1 +
backend/plugins/gh-copilot/tasks/register.go | 2 +
backend/plugins/gh-copilot/tasks/seat_extractor.go | 7 +
backend/plugins/gh-copilot/tasks/subtasks.go | 17 +++
.../gh-copilot/tasks/user_metrics_extractor.go | 41 ++++--
...etrics_collector.go => user_teams_collector.go} | 47 ++++---
.../gh-copilot/tasks/user_teams_extractor.go | 93 +++++++++++++
19 files changed, 574 insertions(+), 61 deletions(-)
diff --git
a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_daily_metrics.csv
b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_daily_metrics.csv
index 57e3f3627..7a74a5cc8 100644
---
a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_daily_metrics.csv
+++
b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_enterprise_daily_metrics.csv
@@ -1,3 +1,3 @@
-connection_id,scope_id,day,enterprise_id,daily_active_users,weekly_active_users,monthly_active_users,monthly_active_chat_users,monthly_active_agent_users,pr_total_reviewed,pr_total_created,pr_total_created_by_copilot,pr_total_reviewed_by_copilot,user_initiated_interaction_count,code_generation_activity_count,code_acceptance_activity_count,loc_suggested_to_add_sum,loc_suggested_to_delete_sum,loc_added_sum,loc_deleted_sum
-1,octodemo,2025-09-01T00:00:00.000+00:00,,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
-1,octodemo,2025-09-02T00:00:00.000+00:00,,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+connection_id,scope_id,day,enterprise_id,daily_active_users,weekly_active_users,monthly_active_users,monthly_active_chat_users,monthly_active_agent_users,daily_active_cli_users,daily_active_copilot_code_review_users,daily_passive_copilot_code_review_users,weekly_active_copilot_code_review_users,weekly_passive_copilot_code_review_users,monthly_active_copilot_code_review_users,monthly_passive_copilot_code_review_users,chat_panel_agent_mode,chat_panel_ask_mode,chat_panel_custom_mode,chat_pa
[...]
+1,octodemo,2025-09-01T00:00:00.000+00:00,,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+1,octodemo,2025-09-02T00:00:00.000+00:00,,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
diff --git
a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_seats.csv
b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_seats.csv
index 87b28f6b8..291a98ca6 100644
---
a/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_seats.csv
+++
b/backend/plugins/gh-copilot/e2e/metrics/snapshot_tables/_tool_copilot_seats.csv
@@ -1,3 +1,3 @@
-connection_id,organization,user_login,user_id,plan_type,created_at,last_activity_at,last_activity_editor,last_authenticated_at,pending_cancellation_date,updated_at
-1,octodemo,nathos,4215,enterprise,2023-08-28T23:50:42.000+00:00,2025-11-06T16:12:15.000+00:00,copilot_pr_review,2025-12-04T15:53:22.000+00:00,,2024-02-01T00:00:00.000+00:00
-1,octodemo,octocat,1,enterprise,2024-01-10T10:11:12.000+00:00,,vscode/1.0.0/copilot-chat/0.1.0,,,2024-02-02T00:00:00.000+00:00
+connection_id,organization,user_login,user_id,user_name,user_email,plan_type,assigning_team_id,assigning_team_name,assigning_team_slug,created_at,last_activity_at,last_activity_editor,last_authenticated_at,pending_cancellation_date,updated_at
+1,octodemo,nathos,4215,,,enterprise,0,,,2023-08-28T23:50:42.000+00:00,2025-11-06T16:12:15.000+00:00,copilot_pr_review,2025-12-04T15:53:22.000+00:00,,2024-02-01T00:00:00.000+00:00
+1,octodemo,octocat,1,,,enterprise,0,,,2024-01-10T10:11:12.000+00:00,,vscode/1.0.0/copilot-chat/0.1.0,,,2024-02-02T00:00:00.000+00:00
diff --git a/backend/plugins/gh-copilot/models/enterprise_metrics.go
b/backend/plugins/gh-copilot/models/enterprise_metrics.go
index 07663aa6d..967e3ecd3 100644
--- a/backend/plugins/gh-copilot/models/enterprise_metrics.go
+++ b/backend/plugins/gh-copilot/models/enterprise_metrics.go
@@ -44,6 +44,15 @@ type CopilotCodeMetrics struct {
LocDeletedSum int `json:"locDeletedSum"`
}
+// CopilotCliMetrics contains CLI usage breakdown metrics.
+type CopilotCliMetrics struct {
+ CliSessionCount int `json:"cliSessionCount" gorm:"comment:Number of
CLI sessions"`
+ CliRequestCount int `json:"cliRequestCount" gorm:"comment:Number of
CLI requests"`
+ CliPromptCount int `json:"cliPromptCount" gorm:"comment:Number of
CLI prompts"`
+ CliOutputTokenSum int `json:"cliOutputTokenSum" gorm:"comment:Total
output tokens from CLI"`
+ CliPromptTokenSum int `json:"cliPromptTokenSum" gorm:"comment:Total
prompt tokens from CLI"`
+}
+
// GhCopilotEnterpriseDailyMetrics captures daily enterprise-level aggregate
Copilot metrics.
type GhCopilotEnterpriseDailyMetrics struct {
ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
@@ -57,12 +66,43 @@ type GhCopilotEnterpriseDailyMetrics struct {
MonthlyActiveChatUsers int `json:"monthlyActiveChatUsers"`
MonthlyActiveAgentUsers int `json:"monthlyActiveAgentUsers"`
- PRTotalReviewed int `json:"prTotalReviewed"
gorm:"comment:Total PRs reviewed"`
- PRTotalCreated int `json:"prTotalCreated" gorm:"comment:Total
PRs created"`
- PRTotalCreatedByCopilot int `json:"prTotalCreatedByCopilot"
gorm:"comment:PRs created by Copilot"`
- PRTotalReviewedByCopilot int `json:"prTotalReviewedByCopilot"
gorm:"comment:PRs reviewed by Copilot"`
+ // CLI active users
+ DailyActiveCliUsers int `json:"dailyActiveCliUsers" gorm:"comment:Daily
active CLI users"`
+
+ // Code review user counts
+ DailyActiveCopilotCodeReviewUsers int
`json:"dailyActiveCopilotCodeReviewUsers"`
+ DailyPassiveCopilotCodeReviewUsers int
`json:"dailyPassiveCopilotCodeReviewUsers"`
+ WeeklyActiveCopilotCodeReviewUsers int
`json:"weeklyActiveCopilotCodeReviewUsers"`
+ WeeklyPassiveCopilotCodeReviewUsers int
`json:"weeklyPassiveCopilotCodeReviewUsers"`
+ MonthlyActiveCopilotCodeReviewUsers int
`json:"monthlyActiveCopilotCodeReviewUsers"`
+ MonthlyPassiveCopilotCodeReviewUsers int
`json:"monthlyPassiveCopilotCodeReviewUsers"`
+
+ // Chat panel mode breakdown
+ ChatPanelAgentMode int `json:"chatPanelAgentMode" gorm:"comment:Chat
panel agent mode interactions"`
+ ChatPanelAskMode int `json:"chatPanelAskMode" gorm:"comment:Chat
panel ask mode interactions"`
+ ChatPanelCustomMode int `json:"chatPanelCustomMode" gorm:"comment:Chat
panel custom mode interactions"`
+ ChatPanelEditMode int `json:"chatPanelEditMode" gorm:"comment:Chat
panel edit mode interactions"`
+ ChatPanelPlanMode int `json:"chatPanelPlanMode" gorm:"comment:Chat
panel plan mode interactions"`
+ ChatPanelUnknownMode int `json:"chatPanelUnknownMode"
gorm:"comment:Chat panel unknown mode interactions"`
+
+ // Pull request metrics (expanded)
+ PRTotalReviewed int `json:"prTotalReviewed"
gorm:"comment:Total PRs reviewed"`
+ PRTotalCreated int `json:"prTotalCreated"
gorm:"comment:Total PRs created"`
+ PRTotalMerged int `json:"prTotalMerged"
gorm:"comment:Total PRs merged"`
+ PRMedianMinutesToMerge float64
`json:"prMedianMinutesToMerge" gorm:"comment:Median minutes to merge PRs"`
+ PRTotalSuggestions int `json:"prTotalSuggestions"
gorm:"comment:Total PR review suggestions"`
+ PRTotalAppliedSuggestions int
`json:"prTotalAppliedSuggestions" gorm:"comment:Total applied PR suggestions"`
+ PRTotalCreatedByCopilot int
`json:"prTotalCreatedByCopilot" gorm:"comment:PRs created by Copilot"`
+ PRTotalReviewedByCopilot int
`json:"prTotalReviewedByCopilot" gorm:"comment:PRs reviewed by Copilot"`
+ PRTotalMergedCreatedByCopilot int
`json:"prTotalMergedCreatedByCopilot" gorm:"comment:Merged PRs created by
Copilot"`
+ PRTotalMergedReviewedByCopilot int
`json:"prTotalMergedReviewedByCopilot" gorm:"comment:Merged PRs reviewed by
Copilot"`
+ PRMedianMinToMergeCopilotAuthored float64
`json:"prMedianMinToMergeCopilotAuthored" gorm:"comment:Median min to merge
Copilot-authored PRs"`
+ PRMedianMinToMergeCopilotReviewed float64
`json:"prMedianMinToMergeCopilotReviewed" gorm:"comment:Median min to merge
Copilot-reviewed PRs"`
+ PRTotalCopilotSuggestions int
`json:"prTotalCopilotSuggestions" gorm:"comment:Total Copilot review
suggestions"`
+ PRTotalCopilotAppliedSuggestions int
`json:"prTotalCopilotAppliedSuggestions" gorm:"comment:Total Copilot applied
suggestions"`
CopilotActivityMetrics `mapstructure:",squash"`
+ CopilotCliMetrics `mapstructure:",squash"`
common.NoPKModel
}
diff --git
a/backend/plugins/gh-copilot/models/migrationscripts/20260527_add_copilot_metrics_gaps.go
b/backend/plugins/gh-copilot/models/migrationscripts/20260527_add_copilot_metrics_gaps.go
new file mode 100644
index 000000000..f676b0e4b
--- /dev/null
+++
b/backend/plugins/gh-copilot/models/migrationscripts/20260527_add_copilot_metrics_gaps.go
@@ -0,0 +1,153 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+type addCopilotMetricsGaps struct{}
+
+// --- Enterprise daily metrics: new columns ---
+
+type enterpriseDailyMetrics20260527 struct {
+ // CLI
+ DailyActiveCliUsers int
+
+ // Code review user counts
+ DailyActiveCopilotCodeReviewUsers int
+ DailyPassiveCopilotCodeReviewUsers int
+ WeeklyActiveCopilotCodeReviewUsers int
+ WeeklyPassiveCopilotCodeReviewUsers int
+ MonthlyActiveCopilotCodeReviewUsers int
+ MonthlyPassiveCopilotCodeReviewUsers int
+
+ // Chat panel mode breakdown
+ ChatPanelAgentMode int
+ ChatPanelAskMode int
+ ChatPanelCustomMode int
+ ChatPanelEditMode int
+ ChatPanelPlanMode int
+ ChatPanelUnknownMode int
+
+ // Expanded PR metrics
+ PRTotalMerged int
+ PRMedianMinutesToMerge float64
+ PRTotalSuggestions int
+ PRTotalAppliedSuggestions int
+ PRTotalMergedCreatedByCopilot int
+ PRTotalMergedReviewedByCopilot int
+ PRMedianMinToMergeCopilotAuthored float64
+ PRMedianMinToMergeCopilotReviewed float64
+ PRTotalCopilotSuggestions int
+ PRTotalCopilotAppliedSuggestions int
+
+ // CLI breakdown
+ CliSessionCount int
+ CliRequestCount int
+ CliPromptCount int
+ CliOutputTokenSum int
+ CliPromptTokenSum int
+}
+
+func (enterpriseDailyMetrics20260527) TableName() string {
+ return "_tool_copilot_enterprise_daily_metrics"
+}
+
+// --- User daily metrics: new columns ---
+
+type userDailyMetrics20260527 struct {
+ UsedCli bool
+ UsedCopilotCodeReviewActive bool
+ UsedCopilotCodeReviewPassive bool
+
+ // CLI breakdown
+ CliSessionCount int
+ CliRequestCount int
+ CliPromptCount int
+ CliOutputTokenSum int
+ CliPromptTokenSum int
+}
+
+func (userDailyMetrics20260527) TableName() string {
+ return "_tool_copilot_user_daily_metrics"
+}
+
+// --- Seat: new columns ---
+
+type seat20260527 struct {
+ UserName string `gorm:"type:varchar(255)"`
+ UserEmail string `gorm:"type:varchar(255)"`
+ AssigningTeamId int64
+ AssigningTeamName string `gorm:"type:varchar(255)"`
+ AssigningTeamSlug string `gorm:"type:varchar(255)"`
+}
+
+func (seat20260527) TableName() string {
+ return "_tool_copilot_seats"
+}
+
+// --- User-teams: new table ---
+
+type userTeam20260527 struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"`
+ Day time.Time `gorm:"primaryKey;type:date"`
+ UserId int64 `gorm:"primaryKey"`
+ TeamId int64 `gorm:"primaryKey"`
+
+ UserLogin string `gorm:"type:varchar(255);index"`
+ OrganizationId string `gorm:"type:varchar(100)"`
+ EnterpriseId string `gorm:"type:varchar(100)"`
+ TeamSlug string `gorm:"type:varchar(255)"`
+
+ archived.NoPKModel
+}
+
+func (userTeam20260527) TableName() string {
+ return "_tool_copilot_user_teams"
+}
+
+func (script *addCopilotMetricsGaps) Up(basicRes context.BasicRes)
errors.Error {
+ // Add new columns to existing tables
+ if err := migrationhelper.AutoMigrateTables(basicRes,
+ &enterpriseDailyMetrics20260527{},
+ &userDailyMetrics20260527{},
+ &seat20260527{},
+ ); err != nil {
+ return err
+ }
+
+ // Create new user-teams table
+ return migrationhelper.AutoMigrateTables(basicRes,
+ &userTeam20260527{},
+ )
+}
+
+func (*addCopilotMetricsGaps) Version() uint64 {
+ return 20260527000000
+}
+
+func (*addCopilotMetricsGaps) Name() string {
+ return "Add Copilot metrics gaps: CLI, code review, chat modes, PR
expansion, user-teams"
+}
diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go
b/backend/plugins/gh-copilot/models/migrationscripts/register.go
index a9c1a770b..399735695 100644
--- a/backend/plugins/gh-copilot/models/migrationscripts/register.go
+++ b/backend/plugins/gh-copilot/models/migrationscripts/register.go
@@ -30,5 +30,6 @@ func All() []plugin.MigrationScript {
new(migrateToUsageMetricsV2),
new(addPRFieldsToEnterpriseMetrics),
new(addOrganizationIdToUserMetrics),
+ new(addCopilotMetricsGaps),
}
}
diff --git a/backend/plugins/gh-copilot/models/models.go
b/backend/plugins/gh-copilot/models/models.go
index f223c8218..5143ce5f8 100644
--- a/backend/plugins/gh-copilot/models/models.go
+++ b/backend/plugins/gh-copilot/models/models.go
@@ -45,5 +45,7 @@ func GetTablesInfo() []dal.Tabler {
&GhCopilotUserMetricsByModelFeature{},
// Seat assignments
&GhCopilotSeat{},
+ // User-team mappings
+ &GhCopilotUserTeam{},
}
}
diff --git a/backend/plugins/gh-copilot/models/models_test.go
b/backend/plugins/gh-copilot/models/models_test.go
index 8c61d2220..ef5b3eff6 100644
--- a/backend/plugins/gh-copilot/models/models_test.go
+++ b/backend/plugins/gh-copilot/models/models_test.go
@@ -40,6 +40,7 @@ func TestGetTablesInfo(t *testing.T) {
(&GhCopilotUserMetricsByLanguageModel{}).TableName(): false,
(&GhCopilotUserMetricsByModelFeature{}).TableName(): false,
(&GhCopilotSeat{}).TableName(): false,
+ (&GhCopilotUserTeam{}).TableName(): false,
}
if len(tables) != len(expected) {
diff --git a/backend/plugins/gh-copilot/models/seat.go
b/backend/plugins/gh-copilot/models/seat.go
index 85ebf177a..d65c80f2e 100644
--- a/backend/plugins/gh-copilot/models/seat.go
+++ b/backend/plugins/gh-copilot/models/seat.go
@@ -29,7 +29,12 @@ type GhCopilotSeat struct {
Organization string `gorm:"primaryKey;type:varchar(255)"`
UserLogin string `gorm:"primaryKey;type:varchar(255)"`
UserId int64 `gorm:"index"`
+ UserName string `gorm:"type:varchar(255)"
json:"userName"`
+ UserEmail string `gorm:"type:varchar(255)"
json:"userEmail"`
PlanType string `gorm:"type:varchar(32)"`
+ AssigningTeamId int64 `json:"assigningTeamId"
gorm:"comment:Team that assigned the seat"`
+ AssigningTeamName string `json:"assigningTeamName"
gorm:"type:varchar(255)"`
+ AssigningTeamSlug string `json:"assigningTeamSlug"
gorm:"type:varchar(255)"`
CreatedAt time.Time
LastActivityAt *time.Time
LastActivityEditor string
diff --git a/backend/plugins/gh-copilot/models/user_metrics.go
b/backend/plugins/gh-copilot/models/user_metrics.go
index 1f17acad8..18e9134c2 100644
--- a/backend/plugins/gh-copilot/models/user_metrics.go
+++ b/backend/plugins/gh-copilot/models/user_metrics.go
@@ -30,13 +30,17 @@ type GhCopilotUserDailyMetrics struct {
Day time.Time `gorm:"primaryKey;type:date" json:"day"`
UserId int64 `gorm:"primaryKey" json:"userId"`
- OrganizationId string `json:"organizationId" gorm:"type:varchar(100)"`
- EnterpriseId string `json:"enterpriseId" gorm:"type:varchar(100)"`
- UserLogin string `json:"userLogin" gorm:"type:varchar(255);index"`
- UsedAgent bool `json:"usedAgent"`
- UsedChat bool `json:"usedChat"`
+ OrganizationId string `json:"organizationId"
gorm:"type:varchar(100)"`
+ EnterpriseId string `json:"enterpriseId"
gorm:"type:varchar(100)"`
+ UserLogin string `json:"userLogin"
gorm:"type:varchar(255);index"`
+ UsedAgent bool `json:"usedAgent"`
+ UsedChat bool `json:"usedChat"`
+ UsedCli bool `json:"usedCli"
gorm:"comment:Whether user used Copilot CLI"`
+ UsedCopilotCodeReviewActive bool `json:"usedCopilotCodeReviewActive"
gorm:"comment:Whether user actively used code review"`
+ UsedCopilotCodeReviewPassive bool
`json:"usedCopilotCodeReviewPassive" gorm:"comment:Whether user passively used
code review"`
CopilotActivityMetrics `mapstructure:",squash"`
+ CopilotCliMetrics `mapstructure:",squash"`
common.NoPKModel
}
diff --git a/backend/plugins/gh-copilot/models/user_team.go
b/backend/plugins/gh-copilot/models/user_team.go
new file mode 100644
index 000000000..d04d55ac5
--- /dev/null
+++ b/backend/plugins/gh-copilot/models/user_team.go
@@ -0,0 +1,45 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// GhCopilotUserTeam maps users to teams per day from the user-teams-1-day
report.
+// This enables team-level metrics aggregation by joining with per-user daily
metrics.
+type GhCopilotUserTeam struct {
+ ConnectionId uint64 `gorm:"primaryKey" json:"connectionId"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"
json:"scopeId"`
+ Day time.Time `gorm:"primaryKey;type:date" json:"day"`
+ UserId int64 `gorm:"primaryKey" json:"userId"`
+ TeamId int64 `gorm:"primaryKey" json:"teamId"`
+
+ UserLogin string `json:"userLogin" gorm:"type:varchar(255);index"`
+ OrganizationId string `json:"organizationId" gorm:"type:varchar(100)"`
+ EnterpriseId string `json:"enterpriseId" gorm:"type:varchar(100)"`
+ TeamSlug string `json:"teamSlug" gorm:"type:varchar(255)"`
+
+ common.NoPKModel
+}
+
+func (GhCopilotUserTeam) TableName() string {
+ return "_tool_copilot_user_teams"
+}
diff --git a/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go
b/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go
index 8686b8cc4..e98a3c4f0 100644
--- a/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go
+++ b/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go
@@ -30,13 +30,31 @@ import (
// --- Enterprise report JSON structures ---
type enterpriseDayTotal struct {
- Day string `json:"day"`
- EnterpriseId string
`json:"enterprise_id"`
- DailyActiveUsers int
`json:"daily_active_users"`
- WeeklyActiveUsers int
`json:"weekly_active_users"`
- MonthlyActiveUsers int
`json:"monthly_active_users"`
- MonthlyActiveChatUsers int
`json:"monthly_active_chat_users"`
- MonthlyActiveAgentUsers int
`json:"monthly_active_agent_users"`
+ Day string `json:"day"`
+ EnterpriseId string `json:"enterprise_id"`
+ DailyActiveUsers int `json:"daily_active_users"`
+ WeeklyActiveUsers int `json:"weekly_active_users"`
+ MonthlyActiveUsers int `json:"monthly_active_users"`
+ MonthlyActiveChatUsers int `json:"monthly_active_chat_users"`
+ MonthlyActiveAgentUsers int `json:"monthly_active_agent_users"`
+ DailyActiveCliUsers int `json:"daily_active_cli_users"`
+
+ // Code review user counts
+ DailyActiveCopilotCodeReviewUsers int
`json:"daily_active_copilot_code_review_users"`
+ DailyPassiveCopilotCodeReviewUsers int
`json:"daily_passive_copilot_code_review_users"`
+ WeeklyActiveCopilotCodeReviewUsers int
`json:"weekly_active_copilot_code_review_users"`
+ WeeklyPassiveCopilotCodeReviewUsers int
`json:"weekly_passive_copilot_code_review_users"`
+ MonthlyActiveCopilotCodeReviewUsers int
`json:"monthly_active_copilot_code_review_users"`
+ MonthlyPassiveCopilotCodeReviewUsers int
`json:"monthly_passive_copilot_code_review_users"`
+
+ // Chat panel mode breakdown
+ ChatPanelAgentMode int `json:"chat_panel_agent_mode"`
+ ChatPanelAskMode int `json:"chat_panel_ask_mode"`
+ ChatPanelCustomMode int `json:"chat_panel_custom_mode"`
+ ChatPanelEditMode int `json:"chat_panel_edit_mode"`
+ ChatPanelPlanMode int `json:"chat_panel_plan_mode"`
+ ChatPanelUnknownMode int `json:"chat_panel_unknown_mode"`
+
UserInitiatedInteractionCount int
`json:"user_initiated_interaction_count"`
CodeGenerationActivityCount int
`json:"code_generation_activity_count"`
CodeAcceptanceActivityCount int
`json:"code_acceptance_activity_count"`
@@ -49,6 +67,7 @@ type enterpriseDayTotal struct {
TotalsByLanguageFeature []totalsByLangFeature
`json:"totals_by_language_feature"`
TotalsByLanguageModel []totalsByLangModel
`json:"totals_by_language_model"`
TotalsByModelFeature []totalsByModelFeature
`json:"totals_by_model_feature"`
+ TotalsByCli *totalsByCli
`json:"totals_by_cli"`
PullRequests *pullRequestStats
`json:"pull_requests"`
}
@@ -97,10 +116,32 @@ type totalsByLangModel struct {
}
type pullRequestStats struct {
- TotalReviewed int `json:"total_reviewed"`
- TotalCreated int `json:"total_created"`
- TotalCreatedByCopilot int `json:"total_created_by_copilot"`
- TotalReviewedByCopilot int `json:"total_reviewed_by_copilot"`
+ TotalReviewed int `json:"total_reviewed"`
+ TotalCreated int `json:"total_created"`
+ TotalMerged int `json:"total_merged"`
+ MedianMinutesToMerge float64 `json:"median_minutes_to_merge"`
+ TotalSuggestions int `json:"total_suggestions"`
+ TotalAppliedSuggestions int
`json:"total_applied_suggestions"`
+ TotalCreatedByCopilot int
`json:"total_created_by_copilot"`
+ TotalReviewedByCopilot int
`json:"total_reviewed_by_copilot"`
+ TotalMergedCreatedByCopilot int
`json:"total_merged_created_by_copilot"`
+ TotalMergedReviewedByCopilot int
`json:"total_merged_reviewed_by_copilot"`
+ MedianMinToMergeCopilotAuthored float64
`json:"median_minutes_to_merge_copilot_authored"`
+ MedianMinToMergeCopilotReviewed float64
`json:"median_minutes_to_merge_copilot_reviewed"`
+ TotalCopilotSuggestions int
`json:"total_copilot_suggestions"`
+ TotalCopilotAppliedSuggestions int
`json:"total_copilot_applied_suggestions"`
+}
+
+type totalsByCli struct {
+ SessionCount int `json:"session_count"`
+ RequestCount int `json:"request_count"`
+ PromptCount int `json:"prompt_count"`
+ TokenUsage *cliTokens `json:"token_usage"`
+}
+
+type cliTokens struct {
+ OutputTokensSum int `json:"output_tokens_sum"`
+ PromptTokensSum int `json:"prompt_tokens_sum"`
}
type totalsByModelFeature struct {
@@ -167,6 +208,22 @@ func ExtractEnterpriseMetrics(taskCtx
plugin.SubTaskContext) errors.Error {
MonthlyActiveUsers: dt.MonthlyActiveUsers,
MonthlyActiveChatUsers:
dt.MonthlyActiveChatUsers,
MonthlyActiveAgentUsers:
dt.MonthlyActiveAgentUsers,
+ DailyActiveCliUsers: dt.DailyActiveCliUsers,
+
+ DailyActiveCopilotCodeReviewUsers:
dt.DailyActiveCopilotCodeReviewUsers,
+ DailyPassiveCopilotCodeReviewUsers:
dt.DailyPassiveCopilotCodeReviewUsers,
+ WeeklyActiveCopilotCodeReviewUsers:
dt.WeeklyActiveCopilotCodeReviewUsers,
+ WeeklyPassiveCopilotCodeReviewUsers:
dt.WeeklyPassiveCopilotCodeReviewUsers,
+ MonthlyActiveCopilotCodeReviewUsers:
dt.MonthlyActiveCopilotCodeReviewUsers,
+ MonthlyPassiveCopilotCodeReviewUsers:
dt.MonthlyPassiveCopilotCodeReviewUsers,
+
+ ChatPanelAgentMode: dt.ChatPanelAgentMode,
+ ChatPanelAskMode: dt.ChatPanelAskMode,
+ ChatPanelCustomMode: dt.ChatPanelCustomMode,
+ ChatPanelEditMode: dt.ChatPanelEditMode,
+ ChatPanelPlanMode: dt.ChatPanelPlanMode,
+ ChatPanelUnknownMode: dt.ChatPanelUnknownMode,
+
CopilotActivityMetrics:
models.CopilotActivityMetrics{
UserInitiatedInteractionCount:
dt.UserInitiatedInteractionCount,
CodeGenerationActivityCount:
dt.CodeGenerationActivityCount,
@@ -177,11 +234,32 @@ func ExtractEnterpriseMetrics(taskCtx
plugin.SubTaskContext) errors.Error {
LocDeletedSum:
dt.LocDeletedSum,
},
}
+ if dt.TotalsByCli != nil {
+ dailyMetrics.CopilotCliMetrics =
models.CopilotCliMetrics{
+ CliSessionCount:
dt.TotalsByCli.SessionCount,
+ CliRequestCount:
dt.TotalsByCli.RequestCount,
+ CliPromptCount:
dt.TotalsByCli.PromptCount,
+ }
+ if dt.TotalsByCli.TokenUsage != nil {
+
dailyMetrics.CopilotCliMetrics.CliOutputTokenSum =
dt.TotalsByCli.TokenUsage.OutputTokensSum
+
dailyMetrics.CopilotCliMetrics.CliPromptTokenSum =
dt.TotalsByCli.TokenUsage.PromptTokensSum
+ }
+ }
if dt.PullRequests != nil {
dailyMetrics.PRTotalReviewed =
dt.PullRequests.TotalReviewed
dailyMetrics.PRTotalCreated =
dt.PullRequests.TotalCreated
+ dailyMetrics.PRTotalMerged =
dt.PullRequests.TotalMerged
+ dailyMetrics.PRMedianMinutesToMerge =
dt.PullRequests.MedianMinutesToMerge
+ dailyMetrics.PRTotalSuggestions =
dt.PullRequests.TotalSuggestions
+ dailyMetrics.PRTotalAppliedSuggestions =
dt.PullRequests.TotalAppliedSuggestions
dailyMetrics.PRTotalCreatedByCopilot =
dt.PullRequests.TotalCreatedByCopilot
dailyMetrics.PRTotalReviewedByCopilot =
dt.PullRequests.TotalReviewedByCopilot
+ dailyMetrics.PRTotalMergedCreatedByCopilot =
dt.PullRequests.TotalMergedCreatedByCopilot
+ dailyMetrics.PRTotalMergedReviewedByCopilot =
dt.PullRequests.TotalMergedReviewedByCopilot
+ dailyMetrics.PRMedianMinToMergeCopilotAuthored
= dt.PullRequests.MedianMinToMergeCopilotAuthored
+ dailyMetrics.PRMedianMinToMergeCopilotReviewed
= dt.PullRequests.MedianMinToMergeCopilotReviewed
+ dailyMetrics.PRTotalCopilotSuggestions =
dt.PullRequests.TotalCopilotSuggestions
+ dailyMetrics.PRTotalCopilotAppliedSuggestions =
dt.PullRequests.TotalCopilotAppliedSuggestions
}
results = append(results, dailyMetrics)
diff --git a/backend/plugins/gh-copilot/tasks/metrics_extractor.go
b/backend/plugins/gh-copilot/tasks/metrics_extractor.go
index 4d635c172..d89eababd 100644
--- a/backend/plugins/gh-copilot/tasks/metrics_extractor.go
+++ b/backend/plugins/gh-copilot/tasks/metrics_extractor.go
@@ -38,12 +38,21 @@ type copilotSeatResponse struct {
LastActivityAt *string `json:"last_activity_at"`
LastActivityEditor string `json:"last_activity_editor"`
Assignee copilotAssignee `json:"assignee"`
+ AssigningTeam *copilotTeam `json:"assigning_team"`
}
type copilotAssignee struct {
Login string `json:"login"`
Id int64 `json:"id"`
Type string `json:"type"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+}
+
+type copilotTeam struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
}
// ExtractOrgMetrics parses org report data from the new report download API.
@@ -100,6 +109,22 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
MonthlyActiveUsers: dt.MonthlyActiveUsers,
MonthlyActiveChatUsers:
dt.MonthlyActiveChatUsers,
MonthlyActiveAgentUsers:
dt.MonthlyActiveAgentUsers,
+ DailyActiveCliUsers: dt.DailyActiveCliUsers,
+
+ DailyActiveCopilotCodeReviewUsers:
dt.DailyActiveCopilotCodeReviewUsers,
+ DailyPassiveCopilotCodeReviewUsers:
dt.DailyPassiveCopilotCodeReviewUsers,
+ WeeklyActiveCopilotCodeReviewUsers:
dt.WeeklyActiveCopilotCodeReviewUsers,
+ WeeklyPassiveCopilotCodeReviewUsers:
dt.WeeklyPassiveCopilotCodeReviewUsers,
+ MonthlyActiveCopilotCodeReviewUsers:
dt.MonthlyActiveCopilotCodeReviewUsers,
+ MonthlyPassiveCopilotCodeReviewUsers:
dt.MonthlyPassiveCopilotCodeReviewUsers,
+
+ ChatPanelAgentMode: dt.ChatPanelAgentMode,
+ ChatPanelAskMode: dt.ChatPanelAskMode,
+ ChatPanelCustomMode: dt.ChatPanelCustomMode,
+ ChatPanelEditMode: dt.ChatPanelEditMode,
+ ChatPanelPlanMode: dt.ChatPanelPlanMode,
+ ChatPanelUnknownMode: dt.ChatPanelUnknownMode,
+
CopilotActivityMetrics:
models.CopilotActivityMetrics{
UserInitiatedInteractionCount:
dt.UserInitiatedInteractionCount,
CodeGenerationActivityCount:
dt.CodeGenerationActivityCount,
@@ -110,11 +135,32 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
LocDeletedSum:
dt.LocDeletedSum,
},
}
+ if dt.TotalsByCli != nil {
+ dailyMetrics.CopilotCliMetrics =
models.CopilotCliMetrics{
+ CliSessionCount:
dt.TotalsByCli.SessionCount,
+ CliRequestCount:
dt.TotalsByCli.RequestCount,
+ CliPromptCount:
dt.TotalsByCli.PromptCount,
+ }
+ if dt.TotalsByCli.TokenUsage != nil {
+
dailyMetrics.CopilotCliMetrics.CliOutputTokenSum =
dt.TotalsByCli.TokenUsage.OutputTokensSum
+
dailyMetrics.CopilotCliMetrics.CliPromptTokenSum =
dt.TotalsByCli.TokenUsage.PromptTokensSum
+ }
+ }
if dt.PullRequests != nil {
dailyMetrics.PRTotalReviewed =
dt.PullRequests.TotalReviewed
dailyMetrics.PRTotalCreated =
dt.PullRequests.TotalCreated
+ dailyMetrics.PRTotalMerged =
dt.PullRequests.TotalMerged
+ dailyMetrics.PRMedianMinutesToMerge =
dt.PullRequests.MedianMinutesToMerge
+ dailyMetrics.PRTotalSuggestions =
dt.PullRequests.TotalSuggestions
+ dailyMetrics.PRTotalAppliedSuggestions =
dt.PullRequests.TotalAppliedSuggestions
dailyMetrics.PRTotalCreatedByCopilot =
dt.PullRequests.TotalCreatedByCopilot
dailyMetrics.PRTotalReviewedByCopilot =
dt.PullRequests.TotalReviewedByCopilot
+ dailyMetrics.PRTotalMergedCreatedByCopilot =
dt.PullRequests.TotalMergedCreatedByCopilot
+ dailyMetrics.PRTotalMergedReviewedByCopilot =
dt.PullRequests.TotalMergedReviewedByCopilot
+ dailyMetrics.PRMedianMinToMergeCopilotAuthored
= dt.PullRequests.MedianMinToMergeCopilotAuthored
+ dailyMetrics.PRMedianMinToMergeCopilotReviewed
= dt.PullRequests.MedianMinToMergeCopilotReviewed
+ dailyMetrics.PRTotalCopilotSuggestions =
dt.PullRequests.TotalCopilotSuggestions
+ dailyMetrics.PRTotalCopilotAppliedSuggestions =
dt.PullRequests.TotalCopilotAppliedSuggestions
}
results = append(results, dailyMetrics)
diff --git a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
index 8f651c482..c3a8b5e44 100644
--- a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
@@ -20,6 +20,7 @@ package tasks
import (
"encoding/json"
"fmt"
+ "io"
"net/http"
"net/url"
"time"
diff --git a/backend/plugins/gh-copilot/tasks/register.go
b/backend/plugins/gh-copilot/tasks/register.go
index ee1dcc797..3c7e5b1ee 100644
--- a/backend/plugins/gh-copilot/tasks/register.go
+++ b/backend/plugins/gh-copilot/tasks/register.go
@@ -27,10 +27,12 @@ func GetSubTaskMetas() []plugin.SubTaskMeta {
CollectCopilotSeatAssignmentsMeta,
CollectEnterpriseMetricsMeta,
CollectUserMetricsMeta,
+ CollectUserTeamsMeta,
// Extractors
ExtractSeatsMeta,
ExtractOrgMetricsMeta,
ExtractEnterpriseMetricsMeta,
ExtractUserMetricsMeta,
+ ExtractUserTeamsMeta,
}
}
diff --git a/backend/plugins/gh-copilot/tasks/seat_extractor.go
b/backend/plugins/gh-copilot/tasks/seat_extractor.go
index 48abc3c0c..1a1b6b135 100644
--- a/backend/plugins/gh-copilot/tasks/seat_extractor.go
+++ b/backend/plugins/gh-copilot/tasks/seat_extractor.go
@@ -96,6 +96,8 @@ func ExtractSeats(taskCtx plugin.SubTaskContext) errors.Error
{
Organization:
connection.Organization,
UserLogin: seat.Assignee.Login,
UserId: seat.Assignee.Id,
+ UserName: seat.Assignee.Name,
+ UserEmail: seat.Assignee.Email,
PlanType: seat.PlanType,
CreatedAt: createdAt,
LastActivityAt: lastAct,
@@ -104,6 +106,11 @@ func ExtractSeats(taskCtx plugin.SubTaskContext)
errors.Error {
PendingCancellationDate: pendingCancel,
UpdatedAt: updatedAt,
}
+ if seat.AssigningTeam != nil {
+ toolSeat.AssigningTeamId = seat.AssigningTeam.Id
+ toolSeat.AssigningTeamName =
seat.AssigningTeam.Name
+ toolSeat.AssigningTeamSlug =
seat.AssigningTeam.Slug
+ }
return []interface{}{toolSeat}, nil
},
diff --git a/backend/plugins/gh-copilot/tasks/subtasks.go
b/backend/plugins/gh-copilot/tasks/subtasks.go
index 24a2c95f1..61ed57995 100644
--- a/backend/plugins/gh-copilot/tasks/subtasks.go
+++ b/backend/plugins/gh-copilot/tasks/subtasks.go
@@ -53,6 +53,14 @@ var CollectUserMetricsMeta = plugin.SubTaskMeta{
Description: "Collect GitHub Copilot enterprise user-level usage
metrics reports",
}
+var CollectUserTeamsMeta = plugin.SubTaskMeta{
+ Name: "collectUserTeams",
+ EntryPoint: CollectUserTeams,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Collect GitHub Copilot user-team mappings from
user-teams-1-day report",
+}
+
var ExtractOrgMetricsMeta = plugin.SubTaskMeta{
Name: "extractOrgMetrics",
EntryPoint: ExtractOrgMetrics,
@@ -88,3 +96,12 @@ var ExtractUserMetricsMeta = plugin.SubTaskMeta{
Description: "Extract Copilot user metrics into tool-layer tables",
Dependencies: []*plugin.SubTaskMeta{&CollectUserMetricsMeta},
}
+
+var ExtractUserTeamsMeta = plugin.SubTaskMeta{
+ Name: "extractUserTeams",
+ EntryPoint: ExtractUserTeams,
+ EnabledByDefault: true,
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Description: "Extract Copilot user-team mappings into tool-layer
table",
+ Dependencies: []*plugin.SubTaskMeta{&CollectUserTeamsMeta},
+}
diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go
b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go
index 96f5570f7..729921940 100644
--- a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go
+++ b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go
@@ -46,11 +46,15 @@ type userDailyReport struct {
LocDeletedSum int
`json:"loc_deleted_sum"`
UsedAgent bool `json:"used_agent"`
UsedChat bool `json:"used_chat"`
+ UsedCli bool `json:"used_cli"`
+ UsedCopilotCodeReviewActive bool
`json:"used_copilot_code_review_active"`
+ UsedCopilotCodeReviewPassive bool
`json:"used_copilot_code_review_passive"`
TotalsByIde []userTotalsByIde
`json:"totals_by_ide"`
TotalsByFeature []totalsByFeature
`json:"totals_by_feature"`
TotalsByLanguageFeature []totalsByLangFeature
`json:"totals_by_language_feature"`
TotalsByLanguageModel []totalsByLangModel
`json:"totals_by_language_model"`
TotalsByModelFeature []totalsByModelFeature
`json:"totals_by_model_feature"`
+ TotalsByCli *totalsByCli
`json:"totals_by_cli"`
}
type userTotalsByIde struct {
@@ -106,16 +110,19 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
var results []interface{}
// Main user daily metrics
- results = append(results,
&models.GhCopilotUserDailyMetrics{
- ConnectionId: data.Options.ConnectionId,
- ScopeId: data.Options.ScopeId,
- Day: day,
- UserId: u.UserId,
- OrganizationId: u.OrganizationId,
- EnterpriseId: u.EnterpriseId,
- UserLogin: u.UserLogin,
- UsedAgent: u.UsedAgent,
- UsedChat: u.UsedChat,
+ userMetrics := &models.GhCopilotUserDailyMetrics{
+ ConnectionId:
data.Options.ConnectionId,
+ ScopeId:
data.Options.ScopeId,
+ Day: day,
+ UserId: u.UserId,
+ OrganizationId: u.OrganizationId,
+ EnterpriseId: u.EnterpriseId,
+ UserLogin: u.UserLogin,
+ UsedAgent: u.UsedAgent,
+ UsedChat: u.UsedChat,
+ UsedCli: u.UsedCli,
+ UsedCopilotCodeReviewActive:
u.UsedCopilotCodeReviewActive,
+ UsedCopilotCodeReviewPassive:
u.UsedCopilotCodeReviewPassive,
CopilotActivityMetrics:
models.CopilotActivityMetrics{
UserInitiatedInteractionCount:
u.UserInitiatedInteractionCount,
CodeGenerationActivityCount:
u.CodeGenerationActivityCount,
@@ -125,7 +132,19 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
LocAddedSum:
u.LocAddedSum,
LocDeletedSum:
u.LocDeletedSum,
},
- })
+ }
+ if u.TotalsByCli != nil {
+ userMetrics.CopilotCliMetrics =
models.CopilotCliMetrics{
+ CliSessionCount:
u.TotalsByCli.SessionCount,
+ CliRequestCount:
u.TotalsByCli.RequestCount,
+ CliPromptCount:
u.TotalsByCli.PromptCount,
+ }
+ if u.TotalsByCli.TokenUsage != nil {
+
userMetrics.CopilotCliMetrics.CliOutputTokenSum =
u.TotalsByCli.TokenUsage.OutputTokensSum
+
userMetrics.CopilotCliMetrics.CliPromptTokenSum =
u.TotalsByCli.TokenUsage.PromptTokensSum
+ }
+ }
+ results = append(results, userMetrics)
// User by IDE
for _, ide := range u.TotalsByIde {
diff --git a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
b/backend/plugins/gh-copilot/tasks/user_teams_collector.go
similarity index 73%
copy from backend/plugins/gh-copilot/tasks/org_metrics_collector.go
copy to backend/plugins/gh-copilot/tasks/user_teams_collector.go
index 8f651c482..2ae0200d2 100644
--- a/backend/plugins/gh-copilot/tasks/org_metrics_collector.go
+++ b/backend/plugins/gh-copilot/tasks/user_teams_collector.go
@@ -20,6 +20,7 @@ package tasks
import (
"encoding/json"
"fmt"
+ "io"
"net/http"
"net/url"
"time"
@@ -29,11 +30,11 @@ import (
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)
-const rawOrgMetricsTable = "copilot_org_metrics"
+const rawUserTeamsTable = "copilot_user_teams"
-// CollectOrgMetrics collects organization-level daily Copilot usage reports
-// using the new report download API. Replaces the deprecated
/orgs/{org}/copilot/metrics endpoint.
-func CollectOrgMetrics(taskCtx plugin.SubTaskContext) errors.Error {
+// CollectUserTeams collects user-team mapping data from the user-teams-1-day
report.
+// This enables team-level metrics aggregation by joining with per-user daily
metrics.
+func CollectUserTeams(taskCtx plugin.SubTaskContext) errors.Error {
data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
if !ok {
return errors.Default.New("task data is not GhCopilotTaskData")
@@ -41,8 +42,13 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
connection := data.Connection
connection.Normalize()
- if connection.Organization == "" {
- taskCtx.GetLogger().Info("No organization configured, skipping
org metrics collection")
+ var urlTemplate string
+
+ if connection.HasEnterprise() {
+ urlTemplate =
fmt.Sprintf("enterprises/%s/copilot/metrics/reports/user-teams-1-day",
connection.Enterprise)
+ } else if connection.Organization != "" {
+ urlTemplate =
fmt.Sprintf("orgs/%s/copilot/metrics/reports/user-teams-1-day",
connection.Organization)
+ } else {
return nil
}
@@ -53,7 +59,7 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
rawArgs := helper.RawDataSubTaskArgs{
Ctx: taskCtx,
- Table: rawOrgMetricsTable,
+ Table: rawUserTeamsTable,
Options: copilotRawParams{
ConnectionId: data.Options.ConnectionId,
ScopeId: data.Options.ScopeId,
@@ -69,16 +75,14 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
now := time.Now().UTC()
start, until := computeReportDateRange(now, collector.GetSince())
- start = clampDailyMetricsStartForBackfill(start, until)
logger := taskCtx.GetLogger()
dayIter := newDayIterator(start, until)
err = collector.InitCollector(helper.ApiCollectorArgs{
- ApiClient: apiClient,
- Input: dayIter,
- UrlTemplate:
fmt.Sprintf("orgs/%s/copilot/metrics/reports/organization-1-day",
- connection.Organization),
+ ApiClient: apiClient,
+ Input: dayIter,
+ UrlTemplate: urlTemplate,
Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
input := reqData.Input.(*dayInput)
q := url.Values{}
@@ -100,19 +104,9 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
var meta reportMetadataResponse
if jsonErr := json.Unmarshal(body, &meta); jsonErr !=
nil {
- snippet := string(body)
- if len(snippet) > 200 {
- snippet = snippet[:200]
- }
- logger.Error(jsonErr, "failed to parse report
metadata, body=%s", snippet)
return nil, errors.Default.Wrap(jsonErr,
"failed to parse report metadata")
}
- if len(meta.DownloadLinks) == 0 {
- logger.Info("No download links for report
day=%s, skipping", meta.ReportDay)
- return nil, nil
- }
-
var results []json.RawMessage
for _, link := range meta.DownloadLinks {
reportBody, dlErr := downloadReport(link,
logger)
@@ -120,9 +114,14 @@ func CollectOrgMetrics(taskCtx plugin.SubTaskContext)
errors.Error {
return nil, dlErr
}
if reportBody == nil {
- continue // blob not found, skip
+ continue
+ }
+ // User-teams reports are JSONL format
+ records, parseErr := parseJSONL(reportBody)
+ if parseErr != nil {
+ return nil, parseErr
}
- results = append(results,
json.RawMessage(reportBody))
+ results = append(results, records...)
}
return results, nil
},
diff --git a/backend/plugins/gh-copilot/tasks/user_teams_extractor.go
b/backend/plugins/gh-copilot/tasks/user_teams_extractor.go
new file mode 100644
index 000000000..72a3de8ab
--- /dev/null
+++ b/backend/plugins/gh-copilot/tasks/user_teams_extractor.go
@@ -0,0 +1,93 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/gh-copilot/models"
+)
+
+// userTeamRecord represents a single line from the user-teams-1-day JSONL
report.
+type userTeamRecord struct {
+ Day string `json:"day"`
+ UserId int64 `json:"user_id"`
+ UserLogin string `json:"user_login"`
+ OrganizationId string `json:"organization_id"`
+ EnterpriseId string `json:"enterprise_id"`
+ TeamId int64 `json:"team_id"`
+ Slug string `json:"slug"`
+}
+
+// ExtractUserTeams parses user-team JSONL records into the GhCopilotUserTeam
model.
+func ExtractUserTeams(taskCtx plugin.SubTaskContext) errors.Error {
+ data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData)
+ if !ok {
+ return errors.Default.New("task data is not GhCopilotTaskData")
+ }
+ connection := data.Connection
+ connection.Normalize()
+
+ params := copilotRawParams{
+ ConnectionId: data.Options.ConnectionId,
+ ScopeId: data.Options.ScopeId,
+ Organization: connection.Organization,
+ Endpoint: connection.Endpoint,
+ }
+
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Table: rawUserTeamsTable,
+ Options: params,
+ },
+ Extract: func(row *helper.RawData) ([]interface{},
errors.Error) {
+ var rec userTeamRecord
+ if err := errors.Convert(json.Unmarshal(row.Data,
&rec)); err != nil {
+ return nil, err
+ }
+
+ day, parseErr := time.Parse("2006-01-02", rec.Day)
+ if parseErr != nil {
+ return nil, errors.BadInput.Wrap(parseErr,
"invalid day in user-teams report")
+ }
+
+ return []interface{}{
+ &models.GhCopilotUserTeam{
+ ConnectionId:
data.Options.ConnectionId,
+ ScopeId: data.Options.ScopeId,
+ Day: day,
+ UserId: rec.UserId,
+ TeamId: rec.TeamId,
+ UserLogin: rec.UserLogin,
+ OrganizationId: rec.OrganizationId,
+ EnterpriseId: rec.EnterpriseId,
+ TeamSlug: rec.Slug,
+ },
+ }, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return extractor.Execute()
+}