diff --git a/backend/plugins/gh-copilot/models/enterprise_metrics.go b/backend/plugins/gh-copilot/models/enterprise_metrics.go index 967e3ecd326..bbd20857dae 100644 --- a/backend/plugins/gh-copilot/models/enterprise_metrics.go +++ b/backend/plugins/gh-copilot/models/enterprise_metrics.go @@ -46,11 +46,13 @@ type CopilotCodeMetrics struct { // 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"` + 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"` + CliLastKnownVersion string `json:"cliLastKnownVersion" gorm:"type:varchar(50);comment:Last known Copilot CLI version"` + CliAvgTokensPerRequest float64 `json:"cliAvgTokensPerRequest" gorm:"comment:Average tokens consumed per CLI request"` } // GhCopilotEnterpriseDailyMetrics captures daily enterprise-level aggregate Copilot metrics. @@ -59,7 +61,12 @@ type GhCopilotEnterpriseDailyMetrics struct { ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"` Day time.Time `gorm:"primaryKey;type:date" json:"day"` - EnterpriseId string `json:"enterpriseId" gorm:"type:varchar(100)"` + EnterpriseId string `json:"enterpriseId" gorm:"type:varchar(100)"` + // OrganizationId identifies the owning org for org-level connections/rows. + // Previously always written as "" by ExtractOrgMetrics, making org-level + // rows in this shared table unidentifiable without a join back to the + // connection/scope config. + OrganizationId string `json:"organizationId" gorm:"type:varchar(100)"` DailyActiveUsers int `json:"dailyActiveUsers"` WeeklyActiveUsers int `json:"weeklyActiveUsers"` MonthlyActiveUsers int `json:"monthlyActiveUsers"` @@ -69,6 +76,11 @@ type GhCopilotEnterpriseDailyMetrics struct { // CLI active users DailyActiveCliUsers int `json:"dailyActiveCliUsers" gorm:"comment:Daily active CLI users"` + // Copilot cloud agent (coding agent) active user counts + DailyActiveCopilotCloudAgentUsers int `json:"dailyActiveCopilotCloudAgentUsers" gorm:"comment:Daily active Copilot cloud/coding agent users"` + WeeklyActiveCopilotCloudAgentUsers int `json:"weeklyActiveCopilotCloudAgentUsers" gorm:"comment:Weekly active Copilot cloud/coding agent users"` + MonthlyActiveCopilotCloudAgentUsers int `json:"monthlyActiveCopilotCloudAgentUsers" gorm:"comment:Monthly active Copilot cloud/coding agent users"` + // Code review user counts DailyActiveCopilotCodeReviewUsers int `json:"dailyActiveCopilotCodeReviewUsers"` DailyPassiveCopilotCodeReviewUsers int `json:"dailyPassiveCopilotCodeReviewUsers"` @@ -100,6 +112,10 @@ type GhCopilotEnterpriseDailyMetrics struct { 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"` + // PRCopilotSuggestionsByCommentType is a JSON-encoded array of + // {comment_type, total_suggestions, total_applied_suggestions}, e.g. the + // split between "suggestion" and "explanation" style review comments. + PRCopilotSuggestionsByCommentType string `json:"prCopilotSuggestionsByCommentType" gorm:"type:text;comment:JSON breakdown of Copilot PR suggestions by comment type"` CopilotActivityMetrics `mapstructure:",squash"` CopilotCliMetrics `mapstructure:",squash"` @@ -187,3 +203,39 @@ type GhCopilotMetricsByModelFeature struct { func (GhCopilotMetricsByModelFeature) TableName() string { return "_tool_copilot_metrics_by_model_feature" } + +// GhCopilotMetricsByAiAdoptionPhase stores enterprise/org metrics broken down +// by AI adoption cohort (phase 0-3, see GitHub's totals_by_ai_adoption_phase). +// Unlike CopilotActivityMetrics elsewhere in this file, most of these fields +// are per-user averages within the phase rather than sums. +type GhCopilotMetricsByAiAdoptionPhase 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"` + Phase int `gorm:"primaryKey" json:"phase"` + + PhaseVersion int `json:"phaseVersion" gorm:"comment:Adoption-phase classification version, starts at 1"` + EngagedUsers int `json:"engagedUsers" gorm:"comment:Users engaged in this phase (2-day-in-28 window)"` + + AvgUserInitiatedInteractionCount float64 `json:"avgUserInitiatedInteractionCount"` + AvgCodeGenerationActivityCount float64 `json:"avgCodeGenerationActivityCount"` + AvgCodeAcceptanceActivityCount float64 `json:"avgCodeAcceptanceActivityCount"` + AvgLocAddedSum float64 `json:"avgLocAddedSum"` + AvgLocDeletedSum float64 `json:"avgLocDeletedSum"` + + AvgPullRequestsCreated float64 `json:"avgPullRequestsCreated"` + AvgPullRequestsMerged float64 `json:"avgPullRequestsMerged"` + AvgPullRequestsReviewed float64 `json:"avgPullRequestsReviewed"` + AvgMedianMinutesToMerge float64 `json:"avgMedianMinutesToMerge"` + + AvgPullRequestsMinutesToReview float64 `json:"avgPullRequestsMinutesToReview" gorm:"comment:Added 2026-07-07"` + AvgPullRequestsReviewCycles float64 `json:"avgPullRequestsReviewCycles" gorm:"comment:Added 2026-07-07"` + + TotalPullRequestsMerged int `json:"totalPullRequestsMerged" gorm:"comment:True sum (not average), added 2026-06-26"` + + common.NoPKModel +} + +func (GhCopilotMetricsByAiAdoptionPhase) TableName() string { + return "_tool_copilot_metrics_by_ai_adoption_phase" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/20260720_add_copilot_metrics_gaps_v2.go b/backend/plugins/gh-copilot/models/migrationscripts/20260720_add_copilot_metrics_gaps_v2.go new file mode 100644 index 00000000000..0630a388406 --- /dev/null +++ b/backend/plugins/gh-copilot/models/migrationscripts/20260720_add_copilot_metrics_gaps_v2.go @@ -0,0 +1,123 @@ +/* +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 addCopilotMetricsGapsV2 struct{} + +// --- Enterprise daily metrics: columns added in the latest Copilot API pass --- + +type enterpriseDailyMetrics20260720 struct { + // totals_by_cli additions + CliLastKnownVersion string `gorm:"type:varchar(50)"` + CliAvgTokensPerRequest float64 + + // cloud/coding agent active user counts + DailyActiveCopilotCloudAgentUsers int + WeeklyActiveCopilotCloudAgentUsers int + MonthlyActiveCopilotCloudAgentUsers int + + // pull_requests.copilot_suggestions_by_comment_type (JSON blob) + PRCopilotSuggestionsByCommentType string `gorm:"type:text"` +} + +func (enterpriseDailyMetrics20260720) TableName() string { + return "_tool_copilot_enterprise_daily_metrics" +} + +// --- User daily metrics: columns added in the latest Copilot API pass --- + +type userDailyMetrics20260720 struct { + UsedCopilotCodingAgent bool + UsedCopilotCloudAgent bool + AiAdoptionPhase int + AiAdoptionPhaseVersion int + + // totals_by_cli additions (CopilotCliMetrics is squashed into this table too) + CliLastKnownVersion string `gorm:"type:varchar(50)"` + CliAvgTokensPerRequest float64 +} + +func (userDailyMetrics20260720) TableName() string { + return "_tool_copilot_user_daily_metrics" +} + +// --- New table: enterprise/org metrics broken down by AI adoption phase --- + +type metricsByAiAdoptionPhase20260720 struct { + ConnectionId uint64 `gorm:"primaryKey"` + ScopeId string `gorm:"primaryKey;type:varchar(255)"` + Day time.Time `gorm:"primaryKey;type:date"` + Phase int `gorm:"primaryKey"` + + PhaseVersion int + EngagedUsers int + + AvgUserInitiatedInteractionCount float64 + AvgCodeGenerationActivityCount float64 + AvgCodeAcceptanceActivityCount float64 + AvgLocAddedSum float64 + AvgLocDeletedSum float64 + + AvgPullRequestsCreated float64 + AvgPullRequestsMerged float64 + AvgPullRequestsReviewed float64 + AvgMedianMinutesToMerge float64 + + AvgPullRequestsMinutesToReview float64 + AvgPullRequestsReviewCycles float64 + + TotalPullRequestsMerged int + + archived.NoPKModel +} + +func (metricsByAiAdoptionPhase20260720) TableName() string { + return "_tool_copilot_metrics_by_ai_adoption_phase" +} + +func (script *addCopilotMetricsGapsV2) Up(basicRes context.BasicRes) errors.Error { + // Add the new columns to the existing daily-metrics tables + if err := migrationhelper.AutoMigrateTables(basicRes, + &enterpriseDailyMetrics20260720{}, + &userDailyMetrics20260720{}, + ); err != nil { + return err + } + + // Create the new AI-adoption-phase breakdown table + return migrationhelper.AutoMigrateTables(basicRes, + &metricsByAiAdoptionPhase20260720{}, + ) +} + +func (*addCopilotMetricsGapsV2) Version() uint64 { + return 20260720000000 +} + +func (*addCopilotMetricsGapsV2) Name() string { + return "Add Copilot metrics gaps v2: CLI version/tokens, cloud-agent users, PR suggestion-type blob, AI adoption phase" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go b/backend/plugins/gh-copilot/models/migrationscripts/register.go index 8f190264618..01a6bb81732 100644 --- a/backend/plugins/gh-copilot/models/migrationscripts/register.go +++ b/backend/plugins/gh-copilot/models/migrationscripts/register.go @@ -31,6 +31,7 @@ func All() []plugin.MigrationScript { new(addPRFieldsToEnterpriseMetrics), new(addOrganizationIdToUserMetrics), new(addCopilotMetricsGaps), + new(addCopilotMetricsGapsV2), new(addAiCreditUsageMetrics), new(fixAiCreditUsageBreakdownColumns), } diff --git a/backend/plugins/gh-copilot/models/models.go b/backend/plugins/gh-copilot/models/models.go index 814e45f4a51..74bea3fcc51 100644 --- a/backend/plugins/gh-copilot/models/models.go +++ b/backend/plugins/gh-copilot/models/models.go @@ -36,6 +36,7 @@ func GetTablesInfo() []dal.Tabler { &GhCopilotMetricsByLanguageFeature{}, &GhCopilotMetricsByLanguageModel{}, &GhCopilotMetricsByModelFeature{}, + &GhCopilotMetricsByAiAdoptionPhase{}, // User-level metrics (from enterprise user reports) &GhCopilotUserDailyMetrics{}, &GhCopilotUserMetricsByIde{}, @@ -47,7 +48,6 @@ func GetTablesInfo() []dal.Tabler { &GhCopilotSeat{}, // User-team mappings &GhCopilotUserTeam{}, - // AI credit usage billing (enterprise, org, user levels) &GhCopilotEnterpriseAiCreditUsage{}, &GhCopilotOrgAiCreditUsage{}, &GhCopilotUserAiCreditUsage{}, diff --git a/backend/plugins/gh-copilot/models/models_test.go b/backend/plugins/gh-copilot/models/models_test.go index 7e9a2f2e979..b382f3c0fc8 100644 --- a/backend/plugins/gh-copilot/models/models_test.go +++ b/backend/plugins/gh-copilot/models/models_test.go @@ -33,6 +33,7 @@ func TestGetTablesInfo(t *testing.T) { (&GhCopilotMetricsByLanguageFeature{}).TableName(): false, (&GhCopilotMetricsByLanguageModel{}).TableName(): false, (&GhCopilotMetricsByModelFeature{}).TableName(): false, + (&GhCopilotMetricsByAiAdoptionPhase{}).TableName(): false, (&GhCopilotUserDailyMetrics{}).TableName(): false, (&GhCopilotUserMetricsByIde{}).TableName(): false, (&GhCopilotUserMetricsByFeature{}).TableName(): false, diff --git a/backend/plugins/gh-copilot/models/user_metrics.go b/backend/plugins/gh-copilot/models/user_metrics.go index 0a16b3bce82..42bbc9759f6 100644 --- a/backend/plugins/gh-copilot/models/user_metrics.go +++ b/backend/plugins/gh-copilot/models/user_metrics.go @@ -38,8 +38,14 @@ type GhCopilotUserDailyMetrics struct { 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"` + UsedCopilotCodingAgent bool `json:"usedCopilotCodingAgent" gorm:"comment:Whether user used the Copilot coding agent"` + UsedCopilotCloudAgent bool `json:"usedCopilotCloudAgent" gorm:"comment:Whether user used the Copilot cloud agent"` AiCreditsUsed float64 `json:"aiCreditsUsed" gorm:"comment:AI credits consumed on this day"` + // AI adoption cohort classification (phase 0-3), added 2026-05-29. + AiAdoptionPhase int `json:"aiAdoptionPhase" gorm:"comment:0=no cohort,1=code-first,2=agent-first,3=multi-agent"` + AiAdoptionPhaseVersion int `json:"aiAdoptionPhaseVersion" gorm:"comment:Classification logic version, starts at 1"` + CopilotActivityMetrics `mapstructure:",squash"` CopilotCliMetrics `mapstructure:",squash"` common.NoPKModel diff --git a/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go b/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go index e98a3c4f0e5..beabe744840 100644 --- a/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go +++ b/backend/plugins/gh-copilot/tasks/enterprise_metrics_extractor.go @@ -30,8 +30,12 @@ import ( // --- Enterprise report JSON structures --- type enterpriseDayTotal struct { - Day string `json:"day"` - EnterpriseId string `json:"enterprise_id"` + Day string `json:"day"` + EnterpriseId string `json:"enterprise_id"` + // OrganizationId is only present on organization-level reports (org report + // shares this same flat schema). Needed so ExtractOrgMetrics can stamp the + // owning org onto rows instead of leaving them unidentifiable. + OrganizationId string `json:"organization_id"` DailyActiveUsers int `json:"daily_active_users"` WeeklyActiveUsers int `json:"weekly_active_users"` MonthlyActiveUsers int `json:"monthly_active_users"` @@ -39,6 +43,11 @@ type enterpriseDayTotal struct { MonthlyActiveAgentUsers int `json:"monthly_active_agent_users"` DailyActiveCliUsers int `json:"daily_active_cli_users"` + // Copilot cloud agent (coding agent) active user counts — added in 2026-03-10. + DailyActiveCopilotCloudAgentUsers int `json:"daily_active_copilot_cloud_agent_users"` + WeeklyActiveCopilotCloudAgentUsers int `json:"weekly_active_copilot_cloud_agent_users"` + MonthlyActiveCopilotCloudAgentUsers int `json:"monthly_active_copilot_cloud_agent_users"` + // Code review user counts DailyActiveCopilotCodeReviewUsers int `json:"daily_active_copilot_code_review_users"` DailyPassiveCopilotCodeReviewUsers int `json:"daily_passive_copilot_code_review_users"` @@ -69,6 +78,11 @@ type enterpriseDayTotal struct { TotalsByModelFeature []totalsByModelFeature `json:"totals_by_model_feature"` TotalsByCli *totalsByCli `json:"totals_by_cli"` PullRequests *pullRequestStats `json:"pull_requests"` + + // TotalsByAiAdoptionPhase groups engagement/activity metrics by AI adoption + // cohort (phase 0-3). Added 2026-05-29, extended with review-cycle metrics + // 2026-07-07. Only present on enterprise/org reports, not user reports. + TotalsByAiAdoptionPhase []totalsByAiAdoptionPhase `json:"totals_by_ai_adoption_phase"` } type totalsByIde struct { @@ -130,18 +144,58 @@ type pullRequestStats struct { MedianMinToMergeCopilotReviewed float64 `json:"median_minutes_to_merge_copilot_reviewed"` TotalCopilotSuggestions int `json:"total_copilot_suggestions"` TotalCopilotAppliedSuggestions int `json:"total_copilot_applied_suggestions"` + + // CopilotSuggestionsByCommentType breaks total_copilot_suggestions down by + // the kind of PR review comment Copilot generated (e.g. "suggestion", "explanation"). + CopilotSuggestionsByCommentType []prSuggestionsByCommentType `json:"copilot_suggestions_by_comment_type"` +} + +type prSuggestionsByCommentType struct { + CommentType string `json:"comment_type"` + TotalSuggestions int `json:"total_suggestions"` + TotalApplied int `json:"total_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"` + SessionCount int `json:"session_count"` + RequestCount int `json:"request_count"` + PromptCount int `json:"prompt_count"` + TokenUsage *cliTokens `json:"token_usage"` + LastKnownCliVersion string `json:"last_known_cli_version"` } type cliTokens struct { - OutputTokensSum int `json:"output_tokens_sum"` - PromptTokensSum int `json:"prompt_tokens_sum"` + OutputTokensSum int `json:"output_tokens_sum"` + PromptTokensSum int `json:"prompt_tokens_sum"` + AvgTokensPerRequest float64 `json:"avg_tokens_per_request"` +} + +// totalsByAiAdoptionPhase is one entry of the totals_by_ai_adoption_phase +// breakdown on enterprise/org reports. Per-user fields are averages within the +// phase, not sums (per GitHub's docs) — the *_total fields are true sums. +type totalsByAiAdoptionPhase struct { + Phase int `json:"phase"` + Version int `json:"version"` + + EngagedUsers int `json:"engaged_users"` + + AvgUserInitiatedInteractionCount float64 `json:"avg_user_initiated_interaction_count"` + AvgCodeGenerationActivityCount float64 `json:"avg_code_generation_activity_count"` + AvgCodeAcceptanceActivityCount float64 `json:"avg_code_acceptance_activity_count"` + AvgLocAddedSum float64 `json:"avg_loc_added_sum"` + AvgLocDeletedSum float64 `json:"avg_loc_deleted_sum"` + + AvgPullRequestsCreated float64 `json:"avg_pull_requests_created"` + AvgPullRequestsMerged float64 `json:"avg_pull_requests_merged"` + AvgPullRequestsReviewed float64 `json:"avg_pull_requests_reviewed"` + AvgMedianMinutesToMerge float64 `json:"avg_median_minutes_to_merge"` + + // Added 2026-07-07. + AvgPullRequestsMinutesToReview float64 `json:"avg_pull_requests_minutes_to_review"` + AvgPullRequestsReviewCycles float64 `json:"avg_pull_requests_review_cycles"` + + // Added 2026-06-26 — true total rather than a per-user average. + TotalPullRequestsMerged int `json:"total_pull_requests_merged"` } type totalsByModelFeature struct { @@ -203,6 +257,7 @@ func ExtractEnterpriseMetrics(taskCtx plugin.SubTaskContext) errors.Error { ScopeId: data.Options.ScopeId, Day: day, EnterpriseId: dt.EnterpriseId, + OrganizationId: dt.OrganizationId, DailyActiveUsers: dt.DailyActiveUsers, WeeklyActiveUsers: dt.WeeklyActiveUsers, MonthlyActiveUsers: dt.MonthlyActiveUsers, @@ -210,6 +265,10 @@ func ExtractEnterpriseMetrics(taskCtx plugin.SubTaskContext) errors.Error { MonthlyActiveAgentUsers: dt.MonthlyActiveAgentUsers, DailyActiveCliUsers: dt.DailyActiveCliUsers, + DailyActiveCopilotCloudAgentUsers: dt.DailyActiveCopilotCloudAgentUsers, + WeeklyActiveCopilotCloudAgentUsers: dt.WeeklyActiveCopilotCloudAgentUsers, + MonthlyActiveCopilotCloudAgentUsers: dt.MonthlyActiveCopilotCloudAgentUsers, + DailyActiveCopilotCodeReviewUsers: dt.DailyActiveCopilotCodeReviewUsers, DailyPassiveCopilotCodeReviewUsers: dt.DailyPassiveCopilotCodeReviewUsers, WeeklyActiveCopilotCodeReviewUsers: dt.WeeklyActiveCopilotCodeReviewUsers, @@ -236,13 +295,15 @@ func ExtractEnterpriseMetrics(taskCtx plugin.SubTaskContext) errors.Error { } if dt.TotalsByCli != nil { dailyMetrics.CopilotCliMetrics = models.CopilotCliMetrics{ - CliSessionCount: dt.TotalsByCli.SessionCount, - CliRequestCount: dt.TotalsByCli.RequestCount, - CliPromptCount: dt.TotalsByCli.PromptCount, + CliSessionCount: dt.TotalsByCli.SessionCount, + CliRequestCount: dt.TotalsByCli.RequestCount, + CliPromptCount: dt.TotalsByCli.PromptCount, + CliLastKnownVersion: dt.TotalsByCli.LastKnownCliVersion, } if dt.TotalsByCli.TokenUsage != nil { dailyMetrics.CopilotCliMetrics.CliOutputTokenSum = dt.TotalsByCli.TokenUsage.OutputTokensSum dailyMetrics.CopilotCliMetrics.CliPromptTokenSum = dt.TotalsByCli.TokenUsage.PromptTokensSum + dailyMetrics.CopilotCliMetrics.CliAvgTokensPerRequest = dt.TotalsByCli.TokenUsage.AvgTokensPerRequest } } if dt.PullRequests != nil { @@ -260,8 +321,41 @@ func ExtractEnterpriseMetrics(taskCtx plugin.SubTaskContext) errors.Error { dailyMetrics.PRMedianMinToMergeCopilotReviewed = dt.PullRequests.MedianMinToMergeCopilotReviewed dailyMetrics.PRTotalCopilotSuggestions = dt.PullRequests.TotalCopilotSuggestions dailyMetrics.PRTotalCopilotAppliedSuggestions = dt.PullRequests.TotalCopilotAppliedSuggestions + + // Stored as a JSON blob rather than a normalized breakdown table for + // now — comment-type cardinality is small (suggestion/explanation) + // and doesn't warrant its own table + primary key the way IDE/feature + // breakdowns do. Revisit if GitHub adds more comment types. + if len(dt.PullRequests.CopilotSuggestionsByCommentType) > 0 { + if b, jsonErr := json.Marshal(dt.PullRequests.CopilotSuggestionsByCommentType); jsonErr == nil { + dailyMetrics.PRCopilotSuggestionsByCommentType = string(b) + } + } } results = append(results, dailyMetrics) + // By AI adoption phase + for _, phase := range dt.TotalsByAiAdoptionPhase { + results = append(results, &models.GhCopilotMetricsByAiAdoptionPhase{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Day: day, + Phase: phase.Phase, + PhaseVersion: phase.Version, + EngagedUsers: phase.EngagedUsers, + AvgUserInitiatedInteractionCount: phase.AvgUserInitiatedInteractionCount, + AvgCodeGenerationActivityCount: phase.AvgCodeGenerationActivityCount, + AvgCodeAcceptanceActivityCount: phase.AvgCodeAcceptanceActivityCount, + AvgLocAddedSum: phase.AvgLocAddedSum, + AvgLocDeletedSum: phase.AvgLocDeletedSum, + AvgPullRequestsCreated: phase.AvgPullRequestsCreated, + AvgPullRequestsMerged: phase.AvgPullRequestsMerged, + AvgPullRequestsReviewed: phase.AvgPullRequestsReviewed, + AvgMedianMinutesToMerge: phase.AvgMedianMinutesToMerge, + AvgPullRequestsMinutesToReview: phase.AvgPullRequestsMinutesToReview, + AvgPullRequestsReviewCycles: phase.AvgPullRequestsReviewCycles, + TotalPullRequestsMerged: phase.TotalPullRequestsMerged, + }) + } // By IDE for _, ide := range dt.TotalsByIde { diff --git a/backend/plugins/gh-copilot/tasks/metrics_extractor.go b/backend/plugins/gh-copilot/tasks/metrics_extractor.go index d89eababde6..561434c922b 100644 --- a/backend/plugins/gh-copilot/tasks/metrics_extractor.go +++ b/backend/plugins/gh-copilot/tasks/metrics_extractor.go @@ -99,11 +99,16 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext) errors.Error { var results []interface{} // Main daily metrics — same model as enterprise extractor + organizationId := dt.OrganizationId + if organizationId == "" { + organizationId = connection.Organization + } dailyMetrics := &models.GhCopilotEnterpriseDailyMetrics{ ConnectionId: data.Options.ConnectionId, ScopeId: data.Options.ScopeId, Day: day, - EnterpriseId: "", // org-level, no enterprise + EnterpriseId: dt.EnterpriseId, + OrganizationId: organizationId, DailyActiveUsers: dt.DailyActiveUsers, WeeklyActiveUsers: dt.WeeklyActiveUsers, MonthlyActiveUsers: dt.MonthlyActiveUsers, @@ -111,6 +116,10 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext) errors.Error { MonthlyActiveAgentUsers: dt.MonthlyActiveAgentUsers, DailyActiveCliUsers: dt.DailyActiveCliUsers, + DailyActiveCopilotCloudAgentUsers: dt.DailyActiveCopilotCloudAgentUsers, + WeeklyActiveCopilotCloudAgentUsers: dt.WeeklyActiveCopilotCloudAgentUsers, + MonthlyActiveCopilotCloudAgentUsers: dt.MonthlyActiveCopilotCloudAgentUsers, + DailyActiveCopilotCodeReviewUsers: dt.DailyActiveCopilotCodeReviewUsers, DailyPassiveCopilotCodeReviewUsers: dt.DailyPassiveCopilotCodeReviewUsers, WeeklyActiveCopilotCodeReviewUsers: dt.WeeklyActiveCopilotCodeReviewUsers, @@ -137,13 +146,15 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext) errors.Error { } if dt.TotalsByCli != nil { dailyMetrics.CopilotCliMetrics = models.CopilotCliMetrics{ - CliSessionCount: dt.TotalsByCli.SessionCount, - CliRequestCount: dt.TotalsByCli.RequestCount, - CliPromptCount: dt.TotalsByCli.PromptCount, + CliSessionCount: dt.TotalsByCli.SessionCount, + CliRequestCount: dt.TotalsByCli.RequestCount, + CliPromptCount: dt.TotalsByCli.PromptCount, + CliLastKnownVersion: dt.TotalsByCli.LastKnownCliVersion, } if dt.TotalsByCli.TokenUsage != nil { dailyMetrics.CopilotCliMetrics.CliOutputTokenSum = dt.TotalsByCli.TokenUsage.OutputTokensSum dailyMetrics.CopilotCliMetrics.CliPromptTokenSum = dt.TotalsByCli.TokenUsage.PromptTokensSum + dailyMetrics.CopilotCliMetrics.CliAvgTokensPerRequest = dt.TotalsByCli.TokenUsage.AvgTokensPerRequest } } if dt.PullRequests != nil { @@ -161,9 +172,38 @@ func ExtractOrgMetrics(taskCtx plugin.SubTaskContext) errors.Error { dailyMetrics.PRMedianMinToMergeCopilotReviewed = dt.PullRequests.MedianMinToMergeCopilotReviewed dailyMetrics.PRTotalCopilotSuggestions = dt.PullRequests.TotalCopilotSuggestions dailyMetrics.PRTotalCopilotAppliedSuggestions = dt.PullRequests.TotalCopilotAppliedSuggestions + if len(dt.PullRequests.CopilotSuggestionsByCommentType) > 0 { + if b, jsonErr := json.Marshal(dt.PullRequests.CopilotSuggestionsByCommentType); jsonErr == nil { + dailyMetrics.PRCopilotSuggestionsByCommentType = string(b) + } + } } results = append(results, dailyMetrics) + // By AI adoption phase + for _, phase := range dt.TotalsByAiAdoptionPhase { + results = append(results, &models.GhCopilotMetricsByAiAdoptionPhase{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Day: day, + Phase: phase.Phase, + PhaseVersion: phase.Version, + EngagedUsers: phase.EngagedUsers, + AvgUserInitiatedInteractionCount: phase.AvgUserInitiatedInteractionCount, + AvgCodeGenerationActivityCount: phase.AvgCodeGenerationActivityCount, + AvgCodeAcceptanceActivityCount: phase.AvgCodeAcceptanceActivityCount, + AvgLocAddedSum: phase.AvgLocAddedSum, + AvgLocDeletedSum: phase.AvgLocDeletedSum, + AvgPullRequestsCreated: phase.AvgPullRequestsCreated, + AvgPullRequestsMerged: phase.AvgPullRequestsMerged, + AvgPullRequestsReviewed: phase.AvgPullRequestsReviewed, + AvgMedianMinutesToMerge: phase.AvgMedianMinutesToMerge, + AvgPullRequestsMinutesToReview: phase.AvgPullRequestsMinutesToReview, + AvgPullRequestsReviewCycles: phase.AvgPullRequestsReviewCycles, + TotalPullRequestsMerged: phase.TotalPullRequestsMerged, + }) + } + // By IDE for _, ide := range dt.TotalsByIde { results = append(results, &models.GhCopilotMetricsByIde{ diff --git a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go index 7671fab86a9..403b0c33160 100644 --- a/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go +++ b/backend/plugins/gh-copilot/tasks/user_metrics_extractor.go @@ -30,32 +30,47 @@ import ( // --- User report JSONL structures (one line per user) --- 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"` - UserInitiatedInteractionCount int `json:"user_initiated_interaction_count"` - CodeGenerationActivityCount int `json:"code_generation_activity_count"` - CodeAcceptanceActivityCount int `json:"code_acceptance_activity_count"` - LocSuggestedToAddSum int `json:"loc_suggested_to_add_sum"` - LocSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"` - LocAddedSum int `json:"loc_added_sum"` - 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"` - AiCreditsUsed float64 `json:"ai_credits_used"` - 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"` + 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"` + UserInitiatedInteractionCount int `json:"user_initiated_interaction_count"` + CodeGenerationActivityCount int `json:"code_generation_activity_count"` + CodeAcceptanceActivityCount int `json:"code_acceptance_activity_count"` + LocSuggestedToAddSum int `json:"loc_suggested_to_add_sum"` + LocSuggestedToDeleteSum int `json:"loc_suggested_to_delete_sum"` + LocAddedSum int `json:"loc_added_sum"` + 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"` + // UsedCopilotCodingAgent / UsedCopilotCloudAgent — added in API 2026-03-10. + // GitHub's docs use "coding agent" and "cloud agent" somewhat + // interchangeably for the same async PR-authoring surface; both fields + // are collected in case they're ever populated independently. + UsedCopilotCodingAgent bool `json:"used_copilot_coding_agent"` + UsedCopilotCloudAgent bool `json:"used_copilot_cloud_agent"` + AiCreditsUsed float64 `json:"ai_credits_used"` + AiAdoptionPhase *aiAdoptionPhase `json:"ai_adoption_phase"` + 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"` +} + +// aiAdoptionPhase is the per-user cohort classification (phase 0-3), added +// 2026-05-29. version starts at 1 and increments if GitHub changes the +// classification logic, so historical rows stay interpretable. +type aiAdoptionPhase struct { + Phase int `json:"phase"` + Version int `json:"version"` } type userTotalsByIde struct { @@ -124,6 +139,8 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { UsedCli: u.UsedCli, UsedCopilotCodeReviewActive: u.UsedCopilotCodeReviewActive, UsedCopilotCodeReviewPassive: u.UsedCopilotCodeReviewPassive, + UsedCopilotCodingAgent: u.UsedCopilotCodingAgent, + UsedCopilotCloudAgent: u.UsedCopilotCloudAgent, AiCreditsUsed: u.AiCreditsUsed, CopilotActivityMetrics: models.CopilotActivityMetrics{ UserInitiatedInteractionCount: u.UserInitiatedInteractionCount, @@ -135,15 +152,21 @@ func ExtractUserMetrics(taskCtx plugin.SubTaskContext) errors.Error { LocDeletedSum: u.LocDeletedSum, }, } + if u.AiAdoptionPhase != nil { + userMetrics.AiAdoptionPhase = u.AiAdoptionPhase.Phase + userMetrics.AiAdoptionPhaseVersion = u.AiAdoptionPhase.Version + } if u.TotalsByCli != nil { userMetrics.CopilotCliMetrics = models.CopilotCliMetrics{ - CliSessionCount: u.TotalsByCli.SessionCount, - CliRequestCount: u.TotalsByCli.RequestCount, - CliPromptCount: u.TotalsByCli.PromptCount, + CliSessionCount: u.TotalsByCli.SessionCount, + CliRequestCount: u.TotalsByCli.RequestCount, + CliPromptCount: u.TotalsByCli.PromptCount, + CliLastKnownVersion: u.TotalsByCli.LastKnownCliVersion, } if u.TotalsByCli.TokenUsage != nil { userMetrics.CopilotCliMetrics.CliOutputTokenSum = u.TotalsByCli.TokenUsage.OutputTokensSum userMetrics.CopilotCliMetrics.CliPromptTokenSum = u.TotalsByCli.TokenUsage.PromptTokensSum + userMetrics.CopilotCliMetrics.CliAvgTokensPerRequest = u.TotalsByCli.TokenUsage.AvgTokensPerRequest } } results = append(results, userMetrics)