diff --git a/backend/plugins/gh-copilot/models/migrationscripts/20260303_add_organization_id_to_user_metrics.go b/backend/plugins/gh-copilot/models/migrationscripts/20260303_add_organization_id_to_user_metrics.go new file mode 100644 index 00000000000..d868be69084 --- /dev/null +++ b/backend/plugins/gh-copilot/models/migrationscripts/20260303_add_organization_id_to_user_metrics.go @@ -0,0 +1,49 @@ +/* +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 ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" +) + +var _ plugin.MigrationScript = (*addOrganizationIdToUserMetrics)(nil) + +type addOrganizationIdToUserMetrics struct{} + +func (script *addOrganizationIdToUserMetrics) Up(basicRes context.BasicRes) errors.Error { + db := basicRes.GetDal() + return db.AutoMigrate(&userDailyMetrics20260303{}) +} + +func (*addOrganizationIdToUserMetrics) Version() uint64 { + return 20260303000000 +} + +func (*addOrganizationIdToUserMetrics) Name() string { + return "add organization_id to user daily metrics" +} + +type userDailyMetrics20260303 struct { + OrganizationId string `gorm:"type:varchar(100)"` +} + +func (userDailyMetrics20260303) TableName() string { + return "_tool_copilot_user_daily_metrics" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go b/backend/plugins/gh-copilot/models/migrationscripts/register.go index 5e275f943bf..a9c1a770bfa 100644 --- a/backend/plugins/gh-copilot/models/migrationscripts/register.go +++ b/backend/plugins/gh-copilot/models/migrationscripts/register.go @@ -29,5 +29,6 @@ func All() []plugin.MigrationScript { new(addScopeConfig20260121), new(migrateToUsageMetricsV2), new(addPRFieldsToEnterpriseMetrics), + new(addOrganizationIdToUserMetrics), } } diff --git a/backend/plugins/gh-copilot/models/user_metrics.go b/backend/plugins/gh-copilot/models/user_metrics.go index c53f767ffbc..1f17acad80a 100644 --- a/backend/plugins/gh-copilot/models/user_metrics.go +++ b/backend/plugins/gh-copilot/models/user_metrics.go @@ -30,10 +30,11 @@ type GhCopilotUserDailyMetrics struct { Day time.Time `gorm:"primaryKey;type:date" json:"day"` UserId int64 `gorm:"primaryKey" json:"userId"` - 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"` CopilotActivityMetrics `mapstructure:",squash"` common.NoPKModel diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go index 526c13bf34b..f665a85e74f 100644 --- a/backend/plugins/gh-copilot/tasks/user_metrics_collector.go +++ b/backend/plugins/gh-copilot/tasks/user_metrics_collector.go @@ -32,9 +32,9 @@ import ( const rawUserMetricsTable = "copilot_user_metrics" -// CollectUserMetrics collects enterprise user-level daily Copilot usage reports. +// CollectUserMetrics collects user-level daily Copilot usage reports. // These reports are in JSONL format (one JSON object per line per user). -// Only available for enterprise-scoped connections. +// Utilizes the enterprise or organization endpoints depending on connection configuration func CollectUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { data, ok := taskCtx.TaskContext().GetData().(*GhCopilotTaskData) if !ok { @@ -43,16 +43,21 @@ func CollectUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { connection := data.Connection connection.Normalize() - if !connection.HasEnterprise() { - taskCtx.GetLogger().Info("No enterprise configured, skipping user metrics collection") - return nil - } - apiClient, err := CreateApiClient(taskCtx.TaskContext(), connection) if err != nil { return err } + var urlTemplate string + + if connection.HasEnterprise() { + urlTemplate = fmt.Sprintf("enterprises/%s/copilot/metrics/reports/users-1-day", connection.Enterprise) + } else if connection.Organization != "" { + urlTemplate = fmt.Sprintf("orgs/%s/copilot/metrics/reports/users-1-day", connection.Organization) + } else { + return nil + } + rawArgs := helper.RawDataSubTaskArgs{ Ctx: taskCtx, Table: rawUserMetricsTable, @@ -76,10 +81,9 @@ func CollectUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { dayIter := newDayIterator(start, until) err = collector.InitCollector(helper.ApiCollectorArgs{ - ApiClient: apiClient, - Input: dayIter, - UrlTemplate: fmt.Sprintf("enterprises/%s/copilot/metrics/reports/users-1-day", - connection.Enterprise), + ApiClient: apiClient, + Input: dayIter, + UrlTemplate: urlTemplate, Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { input := reqData.Input.(*dayInput) q := url.Values{} diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go index 1eb73554bb3..96f5570f758 100644 --- a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go +++ b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go @@ -33,6 +33,7 @@ type userDailyReport struct { ReportStartDay string `json:"report_start_day"` ReportEndDay string `json:"report_end_day"` Day string `json:"day"` + OrganizationId string `json:"organization_id"` EnterpriseId string `json:"enterprise_id"` UserId int64 `json:"user_id"` UserLogin string `json:"user_login"` @@ -78,11 +79,6 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { connection := data.Connection connection.Normalize() - if !connection.HasEnterprise() { - taskCtx.GetLogger().Info("No enterprise configured, skipping user metrics extraction") - return nil - } - params := copilotRawParams{ ConnectionId: data.Options.ConnectionId, ScopeId: data.Options.ScopeId, @@ -111,14 +107,15 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { // Main user daily metrics results = append(results, &models.GhCopilotUserDailyMetrics{ - ConnectionId: data.Options.ConnectionId, - ScopeId: data.Options.ScopeId, - Day: day, - UserId: u.UserId, - EnterpriseId: u.EnterpriseId, - UserLogin: u.UserLogin, - UsedAgent: u.UsedAgent, - UsedChat: u.UsedChat, + 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, CopilotActivityMetrics: models.CopilotActivityMetrics{ UserInitiatedInteractionCount: u.UserInitiatedInteractionCount, CodeGenerationActivityCount: u.CodeGenerationActivityCount,