diff --git a/backend/plugins/gh-copilot/models/migrationscripts/20260227_add_team_tables.go b/backend/plugins/gh-copilot/models/migrationscripts/20260227_add_team_tables.go new file mode 100644 index 00000000000..5b792ca1e89 --- /dev/null +++ b/backend/plugins/gh-copilot/models/migrationscripts/20260227_add_team_tables.go @@ -0,0 +1,43 @@ +/* +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/helpers/migrationhelper" + "github.com/apache/incubator-devlake/plugins/gh-copilot/models" +) + +type addTeamTables struct{} + +func (script *addTeamTables) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables( + basicRes, + &models.GhCopilotTeam{}, + &models.GhCopilotTeamUser{}, + ) +} + +func (*addTeamTables) Version() uint64 { + return 20260227000000 +} + +func (*addTeamTables) Name() string { + return "copilot add team and team_user tables" +} diff --git a/backend/plugins/gh-copilot/models/migrationscripts/register.go b/backend/plugins/gh-copilot/models/migrationscripts/register.go index a9c1a770bfa..22e1a29b2ab 100644 --- a/backend/plugins/gh-copilot/models/migrationscripts/register.go +++ b/backend/plugins/gh-copilot/models/migrationscripts/register.go @@ -29,6 +29,9 @@ func All() []plugin.MigrationScript { new(addScopeConfig20260121), new(migrateToUsageMetricsV2), new(addPRFieldsToEnterpriseMetrics), + new(addTeamTables), + new(addOrganizationIdToUserMetrics), + new(addTeamTables), new(addOrganizationIdToUserMetrics), } } diff --git a/backend/plugins/gh-copilot/models/models.go b/backend/plugins/gh-copilot/models/models.go index f223c821827..d9f0f02b3bf 100644 --- a/backend/plugins/gh-copilot/models/models.go +++ b/backend/plugins/gh-copilot/models/models.go @@ -45,5 +45,8 @@ func GetTablesInfo() []dal.Tabler { &GhCopilotUserMetricsByModelFeature{}, // Seat assignments &GhCopilotSeat{}, + // Team / team-member tables (org-level) + &GhCopilotTeam{}, + &GhCopilotTeamUser{}, } } diff --git a/backend/plugins/gh-copilot/models/models_test.go b/backend/plugins/gh-copilot/models/models_test.go index 72ead8a65e9..428402460b2 100644 --- a/backend/plugins/gh-copilot/models/models_test.go +++ b/backend/plugins/gh-copilot/models/models_test.go @@ -22,24 +22,26 @@ import "testing" func TestGetTablesInfo(t *testing.T) { tables := GetTablesInfo() expected := map[string]bool{ - (&GhCopilotConnection{}).TableName(): false, - (&GhCopilotScope{}).TableName(): false, - (&GhCopilotScopeConfig{}).TableName(): false, - (&GhCopilotOrgMetrics{}).TableName(): false, - (&GhCopilotLanguageMetrics{}).TableName(): false, - (&GhCopilotEnterpriseDailyMetrics{}).TableName(): false, - (&GhCopilotMetricsByIde{}).TableName(): false, - (&GhCopilotMetricsByFeature{}).TableName(): false, - (&GhCopilotMetricsByLanguageFeature{}).TableName(): false, - (&GhCopilotMetricsByLanguageModel{}).TableName(): false, - (&GhCopilotMetricsByModelFeature{}).TableName(): false, - (&GhCopilotUserDailyMetrics{}).TableName(): false, - (&GhCopilotUserMetricsByIde{}).TableName(): false, - (&GhCopilotUserMetricsByFeature{}).TableName(): false, + (&GhCopilotConnection{}).TableName(): false, + (&GhCopilotScope{}).TableName(): false, + (&GhCopilotScopeConfig{}).TableName(): false, + (&GhCopilotOrgMetrics{}).TableName(): false, + (&GhCopilotLanguageMetrics{}).TableName(): false, + (&GhCopilotEnterpriseDailyMetrics{}).TableName(): false, + (&GhCopilotMetricsByIde{}).TableName(): false, + (&GhCopilotMetricsByFeature{}).TableName(): false, + (&GhCopilotMetricsByLanguageFeature{}).TableName(): false, + (&GhCopilotMetricsByLanguageModel{}).TableName(): false, + (&GhCopilotMetricsByModelFeature{}).TableName(): false, + (&GhCopilotUserDailyMetrics{}).TableName(): false, + (&GhCopilotUserMetricsByIde{}).TableName(): false, + (&GhCopilotUserMetricsByFeature{}).TableName(): false, (&GhCopilotUserMetricsByLanguageFeature{}).TableName(): false, - (&GhCopilotUserMetricsByLanguageModel{}).TableName(): false, - (&GhCopilotUserMetricsByModelFeature{}).TableName(): false, - (&GhCopilotSeat{}).TableName(): false, + (&GhCopilotUserMetricsByLanguageModel{}).TableName(): false, + (&GhCopilotUserMetricsByModelFeature{}).TableName(): false, + (&GhCopilotSeat{}).TableName(): false, + (&GhCopilotTeam{}).TableName(): false, + (&GhCopilotTeamUser{}).TableName(): false, } if len(tables) != len(expected) { diff --git a/backend/plugins/gh-copilot/models/team.go b/backend/plugins/gh-copilot/models/team.go new file mode 100644 index 00000000000..42574e74f90 --- /dev/null +++ b/backend/plugins/gh-copilot/models/team.go @@ -0,0 +1,47 @@ +/* +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" +) + +// GhCopilotTeam stores a GitHub organization team in the tool layer. +type GhCopilotTeam struct { + ConnectionId uint64 `gorm:"primaryKey"` + Id int `json:"id" gorm:"primaryKey;autoIncrement:false"` + OrgId *int `json:"organization_id" gorm:"index"` + OrgLogin string `json:"org_login" gorm:"type:varchar(255);index"` + Name string `json:"name" gorm:"type:varchar(255)"` + Slug string `json:"slug" gorm:"type:varchar(255);index"` + Description string `json:"description"` + Privacy string `json:"privacy" gorm:"type:varchar(100)"` + Permission string `json:"permission" gorm:"type:varchar(100)"` + NotificationSetting string `json:"notification_setting" gorm:"type:varchar(100)"` + ParentTeamId *int `json:"parent_team_id" gorm:"index"` + ParentTeamSlug string `json:"parent_team_slug" gorm:"type:varchar(255);index"` + GithubCreatedAt *time.Time + GithubUpdatedAt *time.Time + common.NoPKModel +} + +func (GhCopilotTeam) TableName() string { + return "_tool_copilot_teams" +} diff --git a/backend/plugins/gh-copilot/models/team_user.go b/backend/plugins/gh-copilot/models/team_user.go new file mode 100644 index 00000000000..260af089ffc --- /dev/null +++ b/backend/plugins/gh-copilot/models/team_user.go @@ -0,0 +1,38 @@ +/* +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 "github.com/apache/incubator-devlake/core/models/common" + +// GhCopilotTeamUser stores a team-member relationship in the tool layer. +type GhCopilotTeamUser struct { + ConnectionId uint64 `gorm:"primaryKey"` + TeamId int `gorm:"primaryKey;autoIncrement:false"` + UserId int `gorm:"primaryKey;autoIncrement:false"` + OrgLogin string `gorm:"type:varchar(255);index"` + TeamSlug string `gorm:"type:varchar(255);index"` + UserLogin string `json:"login" gorm:"type:varchar(255);index"` + Type string `json:"type" gorm:"type:varchar(100)"` + ViewType string `json:"user_view_type" gorm:"type:varchar(100)"` + IsSiteAdmin bool `json:"site_admin"` + common.NoPKModel +} + +func (GhCopilotTeamUser) TableName() string { + return "_tool_copilot_team_users" +} diff --git a/backend/plugins/gh-copilot/tasks/pagination.go b/backend/plugins/gh-copilot/tasks/pagination.go new file mode 100644 index 00000000000..54454984214 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/pagination.go @@ -0,0 +1,55 @@ +/* +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 ( + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/apache/incubator-devlake/core/errors" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" +) + +// getTotalPagesFromResponse parses the GitHub Link header to determine the last page number. +// This is used for paginated list endpoints (teams, team members). +func getTotalPagesFromResponse(res *http.Response, _ *helper.ApiCollectorArgs) (int, errors.Error) { + link := res.Header.Get("Link") + if link == "" { + return 0, nil + } + pagePattern := regexp.MustCompile(`page=(\d+)`) + relPattern := regexp.MustCompile(`rel="([a-z]+)"`) + for _, part := range strings.Split(link, ",") { + relMatch := relPattern.FindStringSubmatch(part) + if len(relMatch) < 2 || relMatch[1] != "last" { + continue + } + pageMatch := pagePattern.FindStringSubmatch(part) + if len(pageMatch) < 2 { + continue + } + last, err := strconv.Atoi(pageMatch[1]) + if err != nil { + return 0, errors.Default.Wrap(err, "failed to parse last page") + } + return last, nil + } + return 0, nil +} diff --git a/backend/plugins/gh-copilot/tasks/register.go b/backend/plugins/gh-copilot/tasks/register.go index ee1dcc797fc..3286e23fa9a 100644 --- a/backend/plugins/gh-copilot/tasks/register.go +++ b/backend/plugins/gh-copilot/tasks/register.go @@ -22,12 +22,17 @@ import "github.com/apache/incubator-devlake/core/plugin" // GetSubTaskMetas returns the ordered list of Copilot subtasks. func GetSubTaskMetas() []plugin.SubTaskMeta { return []plugin.SubTaskMeta{ - // Collectors + // Collectors – metrics CollectOrgMetricsMeta, CollectCopilotSeatAssignmentsMeta, CollectEnterpriseMetricsMeta, CollectUserMetricsMeta, - // Extractors + // Collectors – teams (extract teams before collecting team users) + CollectTeamsMeta, + ExtractTeamsMeta, + CollectTeamUsersMeta, + ExtractTeamUsersMeta, + // Extractors – metrics ExtractSeatsMeta, ExtractOrgMetricsMeta, ExtractEnterpriseMetricsMeta, diff --git a/backend/plugins/gh-copilot/tasks/team_collector.go b/backend/plugins/gh-copilot/tasks/team_collector.go new file mode 100644 index 00000000000..686ae1a9fcb --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/team_collector.go @@ -0,0 +1,103 @@ +/* +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" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" +) + +const rawCopilotTeamTable = "copilot_api_teams" + +var CollectTeamsMeta = plugin.SubTaskMeta{ + Name: "collectTeams", + EntryPoint: CollectTeams, + EnabledByDefault: true, + Description: "Collect teams data from GitHub API for the configured organization.", + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + DependencyTables: []string{}, + ProductTables: []string{rawCopilotTeamTable}, +} + +func CollectTeams(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() + + org := strings.TrimSpace(connection.Organization) + if org == "" { + taskCtx.GetLogger().Warn(nil, "skipping team collection: no organization configured on connection %d", connection.ID) + return nil + } + + apiClient, err := CreateApiClient(taskCtx.TaskContext(), connection) + if err != nil { + return err + } + + collector, cErr := helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: rawCopilotTeamTable, + Options: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: org, + Endpoint: connection.Endpoint, + }, + }) + if cErr != nil { + return cErr + } + + cErr = collector.InitCollector(helper.ApiCollectorArgs{ + ApiClient: apiClient, + PageSize: 100, + UrlTemplate: fmt.Sprintf("orgs/%s/teams", org), + Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { + query := url.Values{} + query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page)) + query.Set("per_page", fmt.Sprintf("%v", reqData.Pager.Size)) + return query, nil + }, + GetTotalPages: getTotalPagesFromResponse, + ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { + var items []json.RawMessage + e := helper.UnmarshalResponse(res, &items) + if e != nil { + return nil, e + } + return items, nil + }, + AfterResponse: ignore404, + }) + if cErr != nil { + return cErr + } + + return collector.Execute() +} diff --git a/backend/plugins/gh-copilot/tasks/team_extractor.go b/backend/plugins/gh-copilot/tasks/team_extractor.go new file mode 100644 index 00000000000..a4e26b2d214 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/team_extractor.go @@ -0,0 +1,118 @@ +/* +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" + "strings" + "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" +) + +var ExtractTeamsMeta = plugin.SubTaskMeta{ + Name: "extractTeams", + EntryPoint: ExtractTeams, + EnabledByDefault: true, + Description: "Extract raw team data into tool layer table _tool_copilot_teams", + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + DependencyTables: []string{rawCopilotTeamTable}, + ProductTables: []string{models.GhCopilotTeam{}.TableName()}, +} + +// githubTeamParentResponse represents the parent field in the team API response. +type githubTeamParentResponse struct { + Id int `json:"id"` + Slug string `json:"slug"` +} + +// githubTeamResponse represents a single team object from GET /orgs/{org}/teams. +type githubTeamResponse struct { + Id int `json:"id"` + Url string `json:"url"` + HtmlUrl string `json:"html_url"` + Name string `json:"name"` + Type string `json:"type"` + Slug string `json:"slug"` + Description string `json:"description"` + Privacy string `json:"privacy"` + NotificationSetting string `json:"notification_setting"` + Permission string `json:"permission"` + MembersUrl string `json:"members_url"` + RepositoriesUrl string `json:"repositories_url"` + Parent *githubTeamParentResponse `json:"parent"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + OrgId *int `json:"organization_id"` +} + +func ExtractTeams(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() + org := strings.TrimSpace(connection.Organization) + + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: rawCopilotTeamTable, + Options: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: org, + Endpoint: connection.Endpoint, + }, + }, + Extract: func(row *helper.RawData) ([]interface{}, errors.Error) { + apiTeam := &githubTeamResponse{} + if e := json.Unmarshal(row.Data, apiTeam); e != nil { + return nil, errors.Convert(e) + } + + team := &models.GhCopilotTeam{ + ConnectionId: data.Options.ConnectionId, + Id: apiTeam.Id, + Name: apiTeam.Name, + Slug: apiTeam.Slug, + Description: apiTeam.Description, + Privacy: apiTeam.Privacy, + Permission: apiTeam.Permission, + NotificationSetting: apiTeam.NotificationSetting, + GithubCreatedAt: apiTeam.CreatedAt, + GithubUpdatedAt: apiTeam.UpdatedAt, + OrgId: apiTeam.OrgId, + OrgLogin: org, + } + if apiTeam.Parent != nil { + team.ParentTeamId = &apiTeam.Parent.Id + team.ParentTeamSlug = apiTeam.Parent.Slug + } + return []interface{}{team}, nil + }, + }) + if err != nil { + return err + } + return extractor.Execute() +} diff --git a/backend/plugins/gh-copilot/tasks/team_user_collector.go b/backend/plugins/gh-copilot/tasks/team_user_collector.go new file mode 100644 index 00000000000..7dffb8934fe --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/team_user_collector.go @@ -0,0 +1,125 @@ +/* +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" + "fmt" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/apache/incubator-devlake/core/dal" + "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" +) + +const rawCopilotTeamUserTable = "copilot_api_team_users" + +// simpleCopilotTeam is the minimal struct used as input for the team-member +// collector iterator. Fields must be exported for DalCursorIterator. +type simpleCopilotTeam struct { + Id int + Slug string + OrgLogin string +} + +var CollectTeamUsersMeta = plugin.SubTaskMeta{ + Name: "collectTeamUsers", + EntryPoint: CollectTeamUsers, + EnabledByDefault: true, + Description: "Collect team members data from GitHub API.", + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + DependencyTables: []string{models.GhCopilotTeam{}.TableName()}, + ProductTables: []string{rawCopilotTeamUserTable}, +} + +func CollectTeamUsers(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() + + org := strings.TrimSpace(connection.Organization) + if org == "" { + taskCtx.GetLogger().Warn(nil, "skipping team-user collection: no organization configured on connection %d", connection.ID) + return nil + } + + db := taskCtx.GetDal() + cursor, err := db.Cursor( + dal.Select("id, slug, org_login"), + dal.From(models.GhCopilotTeam{}.TableName()), + dal.Where("connection_id = ? AND org_login = ?", data.Options.ConnectionId, org), + ) + if err != nil { + return err + } + iterator, err := helper.NewDalCursorIterator(db, cursor, reflect.TypeOf(simpleCopilotTeam{})) + if err != nil { + return err + } + + apiClient, aErr := CreateApiClient(taskCtx.TaskContext(), connection) + if aErr != nil { + return aErr + } + + collector, cErr := helper.NewApiCollector(helper.ApiCollectorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: rawCopilotTeamUserTable, + Options: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: org, + Endpoint: connection.Endpoint, + }, + }, + ApiClient: apiClient, + Input: iterator, + PageSize: 100, + UrlTemplate: "orgs/{{ .Input.OrgLogin }}/teams/{{ .Input.Slug }}/members", + Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { + query := url.Values{} + query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page)) + query.Set("per_page", fmt.Sprintf("%v", reqData.Pager.Size)) + return query, nil + }, + GetTotalPages: getTotalPagesFromResponse, + ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { + var item json.RawMessage + e := helper.UnmarshalResponse(res, &item) + if e != nil { + return nil, e + } + return []json.RawMessage{item}, nil + }, + AfterResponse: ignore404, + }) + if cErr != nil { + return cErr + } + + return collector.Execute() +} diff --git a/backend/plugins/gh-copilot/tasks/team_user_extractor.go b/backend/plugins/gh-copilot/tasks/team_user_extractor.go new file mode 100644 index 00000000000..1e54eeae945 --- /dev/null +++ b/backend/plugins/gh-copilot/tasks/team_user_extractor.go @@ -0,0 +1,114 @@ +/* +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" + "strings" + + "github.com/apache/incubator-devlake/core/dal" + "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" +) + +var ExtractTeamUsersMeta = plugin.SubTaskMeta{ + Name: "extractTeamUsers", + EntryPoint: ExtractTeamUsers, + EnabledByDefault: true, + Description: "Extract raw team member data into tool layer table _tool_copilot_team_users", + DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS}, + DependencyTables: []string{rawCopilotTeamUserTable}, + ProductTables: []string{models.GhCopilotTeamUser{}.TableName()}, +} + +// githubTeamUserResponse represents a user object from the team members API. +type githubTeamUserResponse struct { + Id int `json:"id"` + Login string `json:"login"` + NodeId string `json:"node_id"` + Type string `json:"type"` + ViewType string `json:"view_type"` + IsSiteAdmin bool `json:"site_admin"` +} + +func ExtractTeamUsers(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() + org := strings.TrimSpace(connection.Organization) + if org == "" { + return errors.BadInput.New("no organization configured on connection") + } + + db := taskCtx.GetDal() + // Delete existing team-user records for this connection/org before re-extracting. + if dErr := db.Delete( + &models.GhCopilotTeamUser{}, + dal.Where("connection_id = ? AND org_login = ?", data.Options.ConnectionId, org), + ); dErr != nil { + return dErr + } + + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Table: rawCopilotTeamUserTable, + Options: copilotRawParams{ + ConnectionId: data.Options.ConnectionId, + ScopeId: data.Options.ScopeId, + Organization: org, + Endpoint: connection.Endpoint, + }, + }, + Extract: func(row *helper.RawData) ([]interface{}, errors.Error) { + apiUsers := &[]githubTeamUserResponse{} + if e := json.Unmarshal(row.Data, apiUsers); e != nil { + return nil, errors.Convert(e) + } + team := &simpleCopilotTeam{} + if e := json.Unmarshal(row.Input, team); e != nil { + return nil, errors.Convert(e) + } + + results := make([]interface{}, 0, len(*apiUsers)) + for _, u := range *apiUsers { + results = append(results, &models.GhCopilotTeamUser{ + ConnectionId: data.Options.ConnectionId, + TeamId: team.Id, + UserId: u.Id, + OrgLogin: team.OrgLogin, + TeamSlug: team.Slug, + UserLogin: u.Login, + Type: u.Type, + ViewType: u.ViewType, + IsSiteAdmin: u.IsSiteAdmin, + }) + } + return results, nil + }, + }) + if err != nil { + return err + } + return extractor.Execute() +} diff --git a/grafana/dashboards/GithubCopilotAdoptionByTeam.json b/grafana/dashboards/GithubCopilotAdoptionByTeam.json new file mode 100644 index 00000000000..2b4c9f800ad --- /dev/null +++ b/grafana/dashboards/GithubCopilotAdoptionByTeam.json @@ -0,0 +1,1568 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": "mysql", + "description": "Daily active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Daily Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND m.day = (\n SELECT MAX(day) FROM _tool_copilot_user_daily_metrics\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}'\n )\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Daily Active Users", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Code acceptance rate for the selected team(s) in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "max": 100, + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 40 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Code Acceptance Rate", + "type": "gauge" + }, + { + "datasource": "mysql", + "description": "Total lines of code added by team members in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(m.loc_added_sum) as \"Lines Added\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Lines of Code Added", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Total user-initiated interactions in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(m.user_initiated_interaction_count) as \"User-Initiated Interactions\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "User-Initiated Interactions", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Weekly active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Weekly Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE m.day >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Weekly Active Users (WAU)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Monthly active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Monthly Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE m.day >= DATE_SUB(CURDATE(), INTERVAL 28 DAY)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Monthly Active Users (MAU)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Unique users with Copilot activity in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Unique Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Unique Users (Period)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Seat utilization for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "min": 0, + "max": 100, + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 75 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 14, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n COUNT(CASE WHEN s.last_activity_at IS NOT NULL THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0) as \"Utilization %\"\nFROM _tool_copilot_seats s\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = s.connection_id AND tu.user_login = s.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE s.connection_id = ${connection_id}\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Seat Utilization", + "type": "gauge" + }, + { + "datasource": "mysql", + "description": "Active users over time for the selected team(s)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n COUNT(DISTINCT m.user_login) as \"Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Active Users Over Time", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Code suggestions generated vs accepted over time for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.code_generation_activity_count) as \"Suggestions\",\n SUM(m.code_acceptance_activity_count) as \"Acceptances\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Code Suggestions & Acceptances", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Lines of code suggested vs added over time for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Lines of Code", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.loc_suggested_to_add_sum) as \"LOC Suggested\",\n SUM(m.loc_added_sum) as \"LOC Added\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "LOC Suggested vs LOC Added Over Time", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Daily acceptance rate trend for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "max": 1, + "min": 0, + "unit": "percentunit", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.code_acceptance_activity_count) / NULLIF(SUM(m.code_generation_activity_count), 0) as \"Acceptance Rate\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Overall Acceptance Rate Trend", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 40, + "panels": [], + "title": "Team Comparison", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Side-by-side comparison of key metrics across all teams", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 41, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT t.name as \"Team\",\n COUNT(DISTINCT m.user_login) as \"Active Users\",\n SUM(m.code_generation_activity_count) as \"Suggestions\",\n SUM(m.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"Acceptance %\",\n SUM(m.loc_added_sum) as \"LOC Added\",\n SUM(m.user_initiated_interaction_count) as \"Interactions\",\n COUNT(DISTINCT CASE WHEN m.used_agent = 1 THEN m.user_login END) as \"Agent Adopters\",\n COUNT(DISTINCT CASE WHEN m.used_chat = 1 THEN m.user_login END) as \"Chat Adopters\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY t.name\nORDER BY SUM(m.code_generation_activity_count) DESC", + "refId": "A" + } + ], + "title": "Team Metrics Comparison", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Weekly active users by team over time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "mode": "normal", + "group": "A" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + }, + "stacking": { + "mode": "normal", + "group": "A" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT DATE(DATE_SUB(m.day, INTERVAL WEEKDAY(m.day) DAY)) as time,\n t.name as metric,\n COUNT(DISTINCT m.user_login) as value\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY time, t.name\nORDER BY time", + "refId": "A" + } + ], + "title": "Weekly Active Users by Team", + "type": "timeseries", + "transformations": [ + { + "id": "prepareTimeSeries", + "options": { + "format": "many" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "^value (.+)$", + "renamePattern": "$1" + } + } + ] + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 43, + "panels": [], + "title": "Quality & Efficiency Ratios", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by language for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 44 + }, + "id": 9, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT lf.language as \"Language\",\n SUM(lf.code_generation_activity_count) as \"Suggestions\",\n SUM(lf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_language_feature lf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = lf.connection_id AND udm.user_id = lf.user_id AND udm.day = lf.day AND udm.scope_id = lf.scope_id\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = lf.connection_id AND tu.user_login = udm.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(lf.day)\n AND lf.connection_id = ${connection_id}\n AND lf.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY lf.language\nHAVING SUM(lf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) DESC\nLIMIT 15", + "refId": "A" + } + ], + "title": "Top Languages by Acceptance Rate", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by model for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 44 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT mf.model as \"Model\",\n SUM(mf.code_generation_activity_count) as \"Suggestions\",\n SUM(mf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_model_feature mf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = mf.connection_id AND udm.user_id = mf.user_id AND udm.day = mf.day AND udm.scope_id = mf.scope_id\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = mf.connection_id AND tu.user_login = udm.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(mf.day)\n AND mf.connection_id = ${connection_id}\n AND mf.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY mf.model\nHAVING SUM(mf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) DESC\nLIMIT 10", + "refId": "A" + } + ], + "title": "Top Models by Acceptance Rate", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 45, + "panels": [], + "title": "User Behavior", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Top users by code generation activity in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 29, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.user_login as \"user_login\",\n t.name as \"team\",\n SUM(m.code_generation_activity_count) as \"suggestions\",\n SUM(m.code_acceptance_activity_count) as \"acceptances\",\n ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"acceptance %\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.user_login, t.name\nHAVING SUM(m.code_generation_activity_count) > 0\nORDER BY SUM(m.code_generation_activity_count) DESC\nLIMIT 25", + "refId": "A" + } + ], + "title": "Top Users by Code Generations", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Agent vs chat user trend for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n COUNT(DISTINCT CASE WHEN m.used_agent = 1 THEN m.user_login END) as \"Agent Users\",\n COUNT(DISTINCT CASE WHEN m.used_chat = 1 THEN m.user_login END) as \"Chat Users\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Agent Users vs Chat Users Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "New active users per day for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 61 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT first_day as time,\n COUNT(*) as \"New Active Users\"\nFROM (\n SELECT m.user_login,\n MIN(m.day) as first_day\n FROM _tool_copilot_user_daily_metrics m\n INNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\n GROUP BY m.user_login\n) first_seen\nWHERE $__timeFilter(first_day)\nGROUP BY first_day\nORDER BY 1", + "refId": "A" + } + ], + "title": "New Active Users per Day", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 69 + }, + "id": 46, + "panels": [], + "title": "Seat Effectiveness", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Seat activity by editor for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 70 + }, + "id": 33, + "options": { + "displayLabels": [ + "name", + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COALESCE(NULLIF(s.last_activity_editor, ''), 'unknown') as \"Editor\",\n COUNT(*) as \"Seats\"\nFROM _tool_copilot_seats s\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = s.connection_id AND tu.user_login = s.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE s.connection_id = ${connection_id}\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\nGROUP BY COALESCE(NULLIF(s.last_activity_editor, ''), 'unknown')\nORDER BY COUNT(*) DESC", + "refId": "A" + } + ], + "title": "Seat Activity by Editor", + "type": "piechart" + }, + { + "datasource": "mysql", + "description": "Total, active, and inactive seats for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 70 + }, + "id": 34, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(*) as \"total_seats\",\n SUM(CASE WHEN s.last_activity_at IS NOT NULL AND s.last_activity_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as \"active_seats\",\n SUM(CASE WHEN s.last_activity_at IS NULL OR s.last_activity_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as \"inactive_seats\"\nFROM _tool_copilot_seats s\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = s.connection_id AND tu.user_login = s.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE s.connection_id = ${connection_id}\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')", + "refId": "A" + } + ], + "title": "Seats Summary", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Inactive seats for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 70 + }, + "id": 35, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.0.0", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT s.user_login as \"user_login\",\n t.name as \"team\",\n s.last_activity_at as \"last_activity_at\",\n s.plan_type as \"plan_type\"\nFROM _tool_copilot_seats s\nINNER JOIN _tool_copilot_team_users tu\n ON tu.connection_id = s.connection_id AND tu.user_login = s.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE s.connection_id = ${connection_id}\n AND ('${team:raw}' = 'All' OR t.slug = '${team:raw}')\n AND (s.last_activity_at IS NULL OR s.last_activity_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY))\nORDER BY s.last_activity_at IS NULL DESC, s.last_activity_at ASC\nLIMIT 100", + "refId": "A" + } + ], + "title": "Inactive Seats", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [ + "copilot", + "devlake", + "teams" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "datasource": "mysql", + "definition": "SELECT DISTINCT connection_id FROM _tool_copilot_scopes ORDER BY connection_id DESC", + "hide": 0, + "includeAll": false, + "label": "Connection ID", + "multi": false, + "name": "connection_id", + "options": [], + "query": "SELECT DISTINCT connection_id FROM _tool_copilot_scopes ORDER BY connection_id DESC", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "datasource": "mysql", + "definition": "SELECT DISTINCT id as scope_id FROM _tool_copilot_scopes WHERE connection_id = ${connection_id} ORDER BY 1", + "hide": 0, + "includeAll": false, + "label": "Scope ID", + "multi": false, + "name": "scope_id", + "options": [], + "query": "SELECT DISTINCT id as scope_id FROM _tool_copilot_scopes WHERE connection_id = ${connection_id} ORDER BY 1", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "All", + "value": "All" + }, + "datasource": "mysql", + "definition": "SELECT slug as __value, name as __text FROM _tool_copilot_teams WHERE connection_id = ${connection_id} ORDER BY name", + "hide": 0, + "includeAll": true, + "label": "Team", + "multi": false, + "name": "team", + "options": [], + "query": "SELECT slug as __value, name as __text FROM _tool_copilot_teams WHERE connection_id = ${connection_id} ORDER BY name", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query", + "allValue": "All" + } + ] + }, + "time": { + "from": "now-90d", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "GitHub Copilot Adoption by Team", + "uid": "copilot_adoption_by_team", + "version": 1, + "weekStart": "" +} diff --git a/grafana/dashboards/GithubCopilotAdoptionByTeamAndUser.json b/grafana/dashboards/GithubCopilotAdoptionByTeamAndUser.json new file mode 100644 index 00000000000..00de984fa6b --- /dev/null +++ b/grafana/dashboards/GithubCopilotAdoptionByTeamAndUser.json @@ -0,0 +1,2675 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 49, + "links": [], + "panels": [ + { + "datasource": "mysql", + "description": "Daily active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Daily Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND m.day = (\n SELECT MAX(day) FROM _tool_copilot_user_daily_metrics\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}'\n )\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Daily Active Users", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Code acceptance rate for the selected team(s) in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red" + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 40 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Code Acceptance Rate", + "type": "gauge" + }, + { + "datasource": "mysql", + "description": "Total lines of code added by team members in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(m.loc_added_sum) as \"Lines Added\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Lines of Code Added", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Total user-initiated interactions in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 0 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(m.user_initiated_interaction_count) as \"User-Initiated Interactions\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "User-Initiated Interactions", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Weekly active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 15, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Weekly Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE m.day >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Weekly Active Users (WAU)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Monthly active users in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Monthly Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE m.day >= DATE_SUB(CURDATE(), INTERVAL 28 DAY)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Monthly Active Users (MAU)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Unique users with Copilot activity in the time range", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Unique Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Unique Users (Period)", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Seat utilization for team members", + "fieldConfig": { + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red" + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 75 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 14, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n COUNT(CASE WHEN s.last_activity_at IS NOT NULL THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0) as \"Utilization %\"\nFROM _tool_copilot_seats s\nWHERE s.connection_id = ${connection_id}\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = s.connection_id\n AND tu.user_login = s.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR s.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Seat Utilization", + "type": "gauge" + }, + { + "datasource": "mysql", + "description": "Users who have used Copilot agent mode in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 47, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Agent Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND m.used_agent = 1\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Agent Mode Adopters", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Users who have used Copilot chat in the selected team(s)", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 48, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(DISTINCT m.user_login) as \"Chat Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND m.used_chat = 1\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Chat Adopters", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Total PR summaries created by Copilot for selected team(s) in the time range Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 8 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(total_pr_summaries_created) as \"PRs by Copilot\"\nFROM _tool_copilot_team_dotcom_prs\nWHERE $__timeFilter(date)\n AND connection_id = ${connection_id}\n AND scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring}))\n AND '${user:csv}' = '0'", + "refId": "A" + } + ], + "title": "PRs Created by Copilot", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Proxy metric for PR review engagement in selected team(s) (team API exposes engaged users, not reviewed count) Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 8 + }, + "id": 51, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(total_engaged_users) as \"PRs Reviewed by Copilot\"\nFROM _tool_copilot_team_dotcom_prs\nWHERE $__timeFilter(date)\n AND connection_id = ${connection_id}\n AND scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring}))\n AND '${user:csv}' = '0'", + "refId": "A" + } + ], + "title": "PRs Reviewed by Copilot", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Active users over time for the selected team(s)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n COUNT(DISTINCT m.user_login) as \"Active Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Active Users Over Time", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Code suggestions generated vs accepted over time for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Count", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.code_generation_activity_count) as \"Suggestions\",\n SUM(m.code_acceptance_activity_count) as \"Acceptances\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Code Suggestions & Acceptances", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Lines of code suggested vs added over time for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Lines of Code", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.loc_suggested_to_add_sum) as \"LOC Suggested\",\n SUM(m.loc_added_sum) as \"LOC Added\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "LOC Suggested vs LOC Added Over Time", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Daily acceptance rate trend for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 20 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n SUM(m.code_acceptance_activity_count) / NULLIF(SUM(m.code_generation_activity_count), 0) as \"Acceptance Rate\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Overall Acceptance Rate Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Daily LOC yield trend for team members (added LOC / suggested LOC)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 49, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n ROUND(SUM(m.loc_added_sum) / NULLIF(SUM(m.loc_suggested_to_add_sum), 0), 2) as \"LOC Yield\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "LOC Yield Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "PR summary activity over time for selected team(s) Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Pull Requests", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 52, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT date as time,\n SUM(total_pr_summaries_created) as \"PRs Created by Copilot\",\n SUM(total_engaged_users) as \"PR Engaged Users\"\nFROM _tool_copilot_team_dotcom_prs\nWHERE $__timeFilter(date)\n AND connection_id = ${connection_id}\n AND scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring}))\n AND '${user:csv}' = '0'\nGROUP BY date\nORDER BY 1", + "refId": "A" + } + ], + "title": "PR Activity Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 36 + }, + "id": 40, + "panels": [], + "title": "Team Comparison", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Side-by-side comparison of key metrics across all teams (users in multiple teams are counted once per team)", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 41, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT t.name as \"Team\",\n COUNT(DISTINCT m.user_login) as \"Active Users\",\n SUM(m.code_generation_activity_count) as \"Suggestions\",\n SUM(m.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"Acceptance %\",\n SUM(m.loc_added_sum) as \"LOC Added\",\n SUM(m.user_initiated_interaction_count) as \"Interactions\",\n COUNT(DISTINCT CASE WHEN m.used_agent = 1 THEN m.user_login END) as \"Agent Adopters\",\n COUNT(DISTINCT CASE WHEN m.used_chat = 1 THEN m.user_login END) as \"Chat Adopters\"\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN (\n SELECT DISTINCT connection_id, team_id, user_login\n FROM _tool_copilot_team_users\n) tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR t.slug IN (${team:sqlstring}))\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY t.name\nORDER BY SUM(m.code_generation_activity_count) DESC", + "refId": "A" + } + ], + "title": "Team Metrics Comparison", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Weekly active users by team over time (users in multiple teams appear in each team they belong to)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "stacking": { + "group": "A", + "mode": "normal" + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT DATE(DATE_SUB(m.day, INTERVAL WEEKDAY(m.day) DAY)) as time,\n t.name as metric,\n COUNT(DISTINCT m.user_login) as value\nFROM _tool_copilot_user_daily_metrics m\nINNER JOIN (\n SELECT DISTINCT connection_id, team_id, user_login\n FROM _tool_copilot_team_users\n) tu\n ON tu.connection_id = m.connection_id AND tu.user_login = m.user_login\nINNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR t.slug IN (${team:sqlstring}))\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY time, t.name\nORDER BY time", + "refId": "A" + } + ], + "title": "Weekly Active Users by Team", + "transformations": [ + { + "id": "prepareTimeSeries", + "options": { + "format": "many" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "^value (.+)$", + "renamePattern": "$1" + } + } + ], + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 55 + }, + "id": 43, + "panels": [], + "title": "Quality & Efficiency Ratios", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by language for team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 9, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT lf.language as \"Language\",\n SUM(lf.code_generation_activity_count) as \"Suggestions\",\n SUM(lf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_language_feature lf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = lf.connection_id AND udm.user_id = lf.user_id AND udm.day = lf.day AND udm.scope_id = lf.scope_id\nWHERE $__timeFilter(lf.day)\n AND lf.connection_id = ${connection_id}\n AND lf.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = udm.connection_id\n AND tu.user_login = udm.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY lf.language\nHAVING SUM(lf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) DESC\nLIMIT 15", + "refId": "A" + } + ], + "title": "Top Languages by Acceptance Rate", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by model for team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT mf.model as \"Model\",\n SUM(mf.code_generation_activity_count) as \"Suggestions\",\n SUM(mf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_model_feature mf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = mf.connection_id AND udm.user_id = mf.user_id AND udm.day = mf.day AND udm.scope_id = mf.scope_id\nWHERE $__timeFilter(mf.day)\n AND mf.connection_id = ${connection_id}\n AND mf.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = udm.connection_id\n AND tu.user_login = udm.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY mf.model\nHAVING SUM(mf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) DESC\nLIMIT 10", + "refId": "A" + } + ], + "title": "Top Models by Acceptance Rate", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 45, + "panels": [], + "title": "User Behavior", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Top users by code generation activity in the selected team(s)", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 29, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.user_login as \"user_login\",\n (SELECT GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ', ')\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n ) as \"teams\",\n SUM(m.code_generation_activity_count) as \"suggestions\",\n SUM(m.code_acceptance_activity_count) as \"acceptances\",\n ROUND(SUM(m.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(m.code_generation_activity_count), 0), 1) as \"acceptance %\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.user_login\nHAVING SUM(m.code_generation_activity_count) > 0\nORDER BY SUM(m.code_generation_activity_count) DESC\nLIMIT 25", + "refId": "A" + } + ], + "title": "Top Users by Code Generations", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Agent vs chat user trend for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT m.day as time,\n COUNT(DISTINCT CASE WHEN m.used_agent = 1 THEN m.user_login END) as \"Agent Users\",\n COUNT(DISTINCT CASE WHEN m.used_chat = 1 THEN m.user_login END) as \"Chat Users\"\nFROM _tool_copilot_user_daily_metrics m\nWHERE $__timeFilter(m.day)\n AND m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\nGROUP BY m.day\nORDER BY 1", + "refId": "A" + } + ], + "title": "Agent Users vs Chat Users Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "New active users per day for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Users", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 73 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT first_day as time,\n COUNT(*) as \"New Active Users\"\nFROM (\n SELECT m.user_login,\n MIN(m.day) as first_day\n FROM _tool_copilot_user_daily_metrics m\n WHERE m.connection_id = ${connection_id}\n AND m.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = m.connection_id\n AND tu.user_login = m.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR m.user_login IN (${user:sqlstring}))\n GROUP BY m.user_login\n) first_seen\nWHERE $__timeFilter(first_day)\nGROUP BY first_day\nORDER BY 1", + "refId": "A" + } + ], + "title": "New Active Users per Day", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 81 + }, + "id": 46, + "panels": [], + "title": "Seat Effectiveness", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Seat activity by editor for team members", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 82 + }, + "id": 33, + "options": { + "displayLabels": [ + "name", + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COALESCE(NULLIF(s.last_activity_editor, ''), 'unknown') as \"Editor\",\n COUNT(*) as \"Seats\"\nFROM _tool_copilot_seats s\nWHERE s.connection_id = ${connection_id}\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = s.connection_id\n AND tu.user_login = s.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR s.user_login IN (${user:sqlstring}))\nGROUP BY COALESCE(NULLIF(s.last_activity_editor, ''), 'unknown')\nORDER BY COUNT(*) DESC", + "refId": "A" + } + ], + "title": "Seat Activity by Editor", + "type": "piechart" + }, + { + "datasource": "mysql", + "description": "Total, active, and inactive seats for team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 82 + }, + "id": 34, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(*) as \"total_seats\",\n SUM(CASE WHEN s.last_activity_at IS NOT NULL AND s.last_activity_at >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as \"active_seats\",\n SUM(CASE WHEN s.last_activity_at IS NULL OR s.last_activity_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as \"inactive_seats\"\nFROM _tool_copilot_seats s\nWHERE s.connection_id = ${connection_id}\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = s.connection_id\n AND tu.user_login = s.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR s.user_login IN (${user:sqlstring}))", + "refId": "A" + } + ], + "title": "Seats Summary", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Inactive seats for team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 82 + }, + "id": 35, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT s.user_login as \"user_login\",\n (SELECT GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ', ')\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = s.connection_id\n AND tu.user_login = s.user_login\n ) as \"teams\",\n s.last_activity_at as \"last_activity_at\",\n s.plan_type as \"plan_type\"\nFROM _tool_copilot_seats s\nWHERE s.connection_id = ${connection_id}\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = s.connection_id\n AND tu.user_login = s.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND (s.last_activity_at IS NULL OR s.last_activity_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 30 DAY))\n AND ('${user:csv}' = '0' OR s.user_login IN (${user:sqlstring}))\nORDER BY s.last_activity_at IS NULL DESC, s.last_activity_at ASC\nLIMIT 100", + "refId": "A" + } + ], + "title": "Inactive Seats", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Weekly user-initiated interactions by top Copilot features for the selected team(s) and user(s), using per-user metrics as the source of truth.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Interactions", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 90 + }, + "id": 53, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "stacking": { + "group": "A", + "mode": "normal" + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT DATE(DATE_SUB(f.day, INTERVAL WEEKDAY(f.day) DAY)) as time,\n CASE\n WHEN f.feature IN (\n SELECT feature FROM (\n SELECT f2.feature\n FROM _tool_copilot_user_metrics_by_feature f2\n INNER JOIN _tool_copilot_user_daily_metrics udm2\n ON udm2.connection_id = f2.connection_id AND udm2.user_id = f2.user_id AND udm2.day = f2.day AND udm2.scope_id = f2.scope_id\n LEFT JOIN (\n SELECT DISTINCT connection_id, user_login\n FROM _tool_copilot_team_users\n WHERE '${team:csv}' = '0' OR team_slug IN (${team:sqlstring})\n ) tu2\n ON tu2.connection_id = udm2.connection_id AND tu2.user_login = udm2.user_login\n WHERE $__timeFilter(f2.day)\n AND f2.connection_id = ${connection_id}\n AND f2.scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR tu2.user_login IS NOT NULL)\n AND ('${user:csv}' = '0' OR udm2.user_login IN (${user:sqlstring}))\n GROUP BY f2.feature\n HAVING SUM(f2.user_initiated_interaction_count) > 0\n ORDER BY SUM(f2.user_initiated_interaction_count) DESC\n LIMIT 4\n ) top_features\n ) THEN REPLACE(REPLACE(f.feature, 'chat_panel_', ''), '_mode', '')\n ELSE 'Other'\n END as metric,\n SUM(f.user_initiated_interaction_count) as value\nFROM _tool_copilot_user_metrics_by_feature f\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = f.connection_id AND udm.user_id = f.user_id AND udm.day = f.day AND udm.scope_id = f.scope_id\nLEFT JOIN (\n SELECT DISTINCT connection_id, user_login\n FROM _tool_copilot_team_users\n WHERE '${team:csv}' = '0' OR team_slug IN (${team:sqlstring})\n) tu\n ON tu.connection_id = udm.connection_id AND tu.user_login = udm.user_login\nWHERE $__timeFilter(f.day)\n AND f.connection_id = ${connection_id}\n AND f.scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR tu.user_login IS NOT NULL)\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY time, metric\nORDER BY time", + "refId": "A" + } + ], + "title": "Feature Mix Over Time", + "transformations": [ + { + "id": "prepareTimeSeries", + "options": { + "format": "many" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "^value (.+)$", + "renamePattern": "$1" + } + } + ], + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Weekly IDE activity for selected team(s) and user(s), using per-user IDE metrics as the source of truth.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Weekly Interactions", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 0, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 90 + }, + "id": 54, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "stacking": { + "group": "A", + "mode": "normal" + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT DATE(DATE_SUB(ui.day, INTERVAL WEEKDAY(ui.day) DAY)) as time,\n ui.ide as metric,\n SUM(ui.user_initiated_interaction_count + ui.code_generation_activity_count + ui.code_acceptance_activity_count) as value\nFROM _tool_copilot_user_metrics_by_ide ui\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = ui.connection_id AND udm.user_id = ui.user_id AND udm.day = ui.day AND udm.scope_id = ui.scope_id\nLEFT JOIN (\n SELECT DISTINCT connection_id, user_login\n FROM _tool_copilot_team_users\n WHERE '${team:csv}' = '0' OR team_slug IN (${team:sqlstring})\n) tu\n ON tu.connection_id = udm.connection_id AND tu.user_login = udm.user_login\nWHERE $__timeFilter(ui.day)\n AND ui.connection_id = ${connection_id}\n AND ui.scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR tu.user_login IS NOT NULL)\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY time, ui.ide\nORDER BY time", + "refId": "A" + } + ], + "title": "IDE Adoption Over Time", + "transformations": [ + { + "id": "prepareTimeSeries", + "options": { + "format": "many" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "^value (.+)$", + "renamePattern": "$1" + } + } + ], + "type": "timeseries" + }, + { + "datasource": "mysql", + "description": "Matrix-style breakdown of completion suggestions and acceptances by language for selected team(s) and user(s), using per-user language metrics as the source of truth.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 98 + }, + "id": 55, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT lf.language as \"Language\",\n 'code_completions' as \"Feature\",\n SUM(lf.code_generation_activity_count) as \"Suggestions\",\n SUM(lf.code_acceptance_activity_count) as \"Acceptances\"\nFROM _tool_copilot_user_metrics_by_language_feature lf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = lf.connection_id AND udm.user_id = lf.user_id AND udm.day = lf.day AND udm.scope_id = lf.scope_id\nLEFT JOIN (\n SELECT DISTINCT connection_id, user_login\n FROM _tool_copilot_team_users\n WHERE '${team:csv}' = '0' OR team_slug IN (${team:sqlstring})\n) tu\n ON tu.connection_id = udm.connection_id AND tu.user_login = udm.user_login\nWHERE $__timeFilter(lf.day)\n AND lf.connection_id = ${connection_id}\n AND lf.scope_id = '${scope_id}'\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY lf.language\nHAVING SUM(lf.code_generation_activity_count) > 0\nORDER BY lf.language", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Feature x Language Matrix", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by Copilot feature for selected team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 106 + }, + "id": 56, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT f.feature as \"Feature\",\n SUM(f.code_generation_activity_count) as \"Suggestions\",\n SUM(f.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(f.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(f.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_feature f\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = f.connection_id AND udm.user_id = f.user_id AND udm.day = f.day AND udm.scope_id = f.scope_id\nWHERE $__timeFilter(f.day)\n AND f.connection_id = ${connection_id}\n AND f.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = udm.connection_id\n AND tu.user_login = udm.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY f.feature\nHAVING SUM(f.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(f.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(f.code_generation_activity_count), 0), 1) DESC, SUM(f.code_generation_activity_count) DESC\nLIMIT 20", + "refId": "A" + } + ], + "title": "Acceptance Rate by Feature", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by programming language for selected team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 106 + }, + "id": 57, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT lf.language as \"Language\",\n SUM(lf.code_generation_activity_count) as \"Suggestions\",\n SUM(lf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_language_feature lf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = lf.connection_id AND udm.user_id = lf.user_id AND udm.day = lf.day AND udm.scope_id = lf.scope_id\nWHERE $__timeFilter(lf.day)\n AND lf.connection_id = ${connection_id}\n AND lf.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = udm.connection_id\n AND tu.user_login = udm.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY lf.language\nHAVING SUM(lf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(lf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(lf.code_generation_activity_count), 0), 1) DESC, SUM(lf.code_generation_activity_count) DESC\nLIMIT 20", + "refId": "A" + } + ], + "title": "Acceptance Rate by Language", + "type": "table" + }, + { + "datasource": "mysql", + "description": "Acceptance ratio by model for selected team members", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 106 + }, + "id": 58, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT mf.model as \"Model\",\n SUM(mf.code_generation_activity_count) as \"Suggestions\",\n SUM(mf.code_acceptance_activity_count) as \"Acceptances\",\n ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) as \"Acceptance Rate %\"\nFROM _tool_copilot_user_metrics_by_model_feature mf\nINNER JOIN _tool_copilot_user_daily_metrics udm\n ON udm.connection_id = mf.connection_id AND udm.user_id = mf.user_id AND udm.day = mf.day AND udm.scope_id = mf.scope_id\nWHERE $__timeFilter(mf.day)\n AND mf.connection_id = ${connection_id}\n AND mf.scope_id = '${scope_id}'\n AND (\n '${team:csv}' = '0'\n OR EXISTS (\n SELECT 1\n FROM _tool_copilot_team_users tu\n INNER JOIN _tool_copilot_teams t\n ON t.connection_id = tu.connection_id AND t.id = tu.team_id\n WHERE tu.connection_id = udm.connection_id\n AND tu.user_login = udm.user_login\n AND t.slug IN (${team:sqlstring})\n )\n )\n AND ('${user:csv}' = '0' OR udm.user_login IN (${user:sqlstring}))\nGROUP BY mf.model\nHAVING SUM(mf.code_generation_activity_count) > 0\nORDER BY ROUND(SUM(mf.code_acceptance_activity_count) * 100.0 / NULLIF(SUM(mf.code_generation_activity_count), 0), 1) DESC, SUM(mf.code_generation_activity_count) DESC\nLIMIT 20", + "refId": "A" + } + ], + "title": "Acceptance Rate by Model", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 114 + }, + "id": 59, + "panels": [], + "title": "Diagnostics", + "type": "row" + }, + { + "datasource": "mysql", + "description": "Latest available team metrics day freshness Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 115 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COALESCE(TIMESTAMPDIFF(DAY, MAX(date), UTC_DATE()), 9999) as \"Days Since Latest Data\"\nFROM _tool_copilot_team_daily_metrics\nWHERE connection_id = ${connection_id}\n AND scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring}))\n AND '${user:csv}' = '0'", + "refId": "A" + } + ], + "title": "Data Freshness", + "type": "stat" + }, + { + "datasource": "mysql", + "description": "Share of PR summaries tied to unknown model taxonomy in selected team(s) Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 20 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 115 + }, + "id": 61, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT ROUND(\n SUM(CASE WHEN LOWER(COALESCE(model, '')) = 'unknown' THEN total_pr_summaries_created ELSE 0 END) * 100.0\n / NULLIF(SUM(total_pr_summaries_created), 0),\n 2\n) as \"Unknown Taxonomy %\"\nFROM _tool_copilot_team_dotcom_prs\nWHERE $__timeFilter(date)\n AND connection_id = ${connection_id}\n AND scope_id = '${scope_id}'\n AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring}))\n AND '${user:csv}' = '0'", + "refId": "A" + } + ], + "title": "Unknown Taxonomy Share", + "type": "gauge" + }, + { + "datasource": "mysql", + "description": "Row counts for team Copilot metrics tables Team-level aggregate; returns no data when User filter is applied.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 115 + }, + "id": 62, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.2", + "targets": [ + { + "datasource": "mysql", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT *\nFROM (\n SELECT '_tool_copilot_team_daily_metrics' as table_name,\n COUNT(*) as row_count\n FROM _tool_copilot_team_daily_metrics\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring})) AND '${user:csv}' = '0'\n\n UNION ALL\n\n SELECT '_tool_copilot_team_completions' as table_name,\n COUNT(*) as row_count\n FROM _tool_copilot_team_completions\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring})) AND '${user:csv}' = '0'\n\n UNION ALL\n\n SELECT '_tool_copilot_team_ide_chat' as table_name,\n COUNT(*) as row_count\n FROM _tool_copilot_team_ide_chat\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring})) AND '${user:csv}' = '0'\n\n UNION ALL\n\n SELECT '_tool_copilot_team_dotcom_chat' as table_name,\n COUNT(*) as row_count\n FROM _tool_copilot_team_dotcom_chat\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring})) AND '${user:csv}' = '0'\n\n UNION ALL\n\n SELECT '_tool_copilot_team_dotcom_prs' as table_name,\n COUNT(*) as row_count\n FROM _tool_copilot_team_dotcom_prs\n WHERE connection_id = ${connection_id} AND scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR team_slug IN (${team:sqlstring})) AND '${user:csv}' = '0'\n) data_volume\nORDER BY row_count DESC", + "refId": "A" + } + ], + "title": "Data Volume by Table", + "type": "table" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [ + "copilot", + "devlake", + "teams", + "users" + ], + "templating": { + "list": [ + { + "current": { + "text": "", + "value": "" + }, + "datasource": "mysql", + "definition": "SELECT DISTINCT connection_id FROM _tool_copilot_scopes ORDER BY connection_id DESC", + "includeAll": false, + "label": "Connection ID", + "name": "connection_id", + "options": [], + "query": "SELECT DISTINCT connection_id FROM _tool_copilot_scopes ORDER BY connection_id DESC", + "refresh": 1, + "regex": "", + "type": "query" + }, + { + "current": { + "text": "", + "value": "" + }, + "datasource": "mysql", + "definition": "SELECT DISTINCT id as scope_id FROM _tool_copilot_scopes WHERE connection_id = ${connection_id} ORDER BY 1", + "includeAll": false, + "label": "Scope ID", + "name": "scope_id", + "options": [], + "query": "SELECT DISTINCT id as scope_id FROM _tool_copilot_scopes WHERE connection_id = ${connection_id} ORDER BY 1", + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "allValue": "0", + "current": { + "text": "All", + "value": "0" + }, + "datasource": "mysql", + "definition": "SELECT slug as __value, name as __text FROM _tool_copilot_teams WHERE connection_id = ${connection_id} ORDER BY name", + "includeAll": true, + "label": "Team", + "multi": true, + "name": "team", + "options": [], + "query": "SELECT slug as __value, name as __text FROM _tool_copilot_teams WHERE connection_id = ${connection_id} ORDER BY name", + "refresh": 2, + "regex": "", + "type": "query" + }, + { + "allValue": "0", + "current": { + "text": "All", + "value": "0" + }, + "datasource": "mysql", + "definition": "SELECT DISTINCT m.user_login as __value, m.user_login as __text FROM _tool_copilot_user_daily_metrics m WHERE m.connection_id = ${connection_id} AND m.scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR EXISTS (SELECT 1 FROM _tool_copilot_team_users tu INNER JOIN _tool_copilot_teams t ON t.connection_id = tu.connection_id AND t.id = tu.team_id WHERE tu.connection_id = m.connection_id AND tu.user_login = m.user_login AND t.slug IN (${team:sqlstring}))) ORDER BY m.user_login", + "includeAll": true, + "label": "User", + "multi": true, + "name": "user", + "options": [], + "query": "SELECT DISTINCT m.user_login as __value, m.user_login as __text FROM _tool_copilot_user_daily_metrics m WHERE m.connection_id = ${connection_id} AND m.scope_id = '${scope_id}' AND ('${team:csv}' = '0' OR EXISTS (SELECT 1 FROM _tool_copilot_team_users tu INNER JOIN _tool_copilot_teams t ON t.connection_id = tu.connection_id AND t.id = tu.team_id WHERE tu.connection_id = m.connection_id AND tu.user_login = m.user_login AND t.slug IN (${team:sqlstring}))) ORDER BY m.user_login", + "refresh": 2, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-90d", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "GitHub Copilot Adoption by Team and User", + "uid": "copilot_adoption_by_team_and_user", + "version": 7 +} \ No newline at end of file