This is an automated email from the ASF dual-hosted git repository.
warren pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new ce89adebe fix(q_dev): include more metrics, prevent data duplication
(#8498)
ce89adebe is described below
commit ce89adebe28c1beb245a2d205f8ef84b10b3464f
Author: DiscreteTom <[email protected]>
AuthorDate: Thu Jul 10 19:51:07 2025 +0800
fix(q_dev): include more metrics, prevent data duplication (#8498)
---
.../20250710_add_missing_metrics.go | 78 +++++++
.../q_dev/models/migrationscripts/register.go | 1 +
backend/plugins/q_dev/models/user_data.go | 68 ++++--
backend/plugins/q_dev/models/user_data_test.go | 193 ++++++++--------
backend/plugins/q_dev/tasks/s3_data_extractor.go | 67 +++++-
.../plugins/q_dev/tasks/s3_data_extractor_test.go | 180 ++++++++++++++-
backend/plugins/q_dev/tasks/s3_file_collector.go | 32 ++-
grafana/dashboards/qdev_user_data.json | 243 +++------------------
8 files changed, 515 insertions(+), 347 deletions(-)
diff --git
a/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go
b/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go
new file mode 100644
index 000000000..d2c6ebe39
--- /dev/null
+++
b/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go
@@ -0,0 +1,78 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+)
+
+var _ plugin.MigrationScript = (*addMissingMetrics)(nil)
+
+type addMissingMetrics struct{}
+
+func (*addMissingMetrics) Up(basicRes context.BasicRes) errors.Error {
+ db := basicRes.GetDal()
+
+ // Add all missing metrics columns to _tool_q_dev_user_data table
+ // All columns are integer type with default value 0
+ // Using snake_case column names to match GORM's default naming
convention
+ _ = db.Exec(`
+ ALTER TABLE _tool_q_dev_user_data
+ ADD COLUMN chat_ai_code_lines INT DEFAULT 0,
+ ADD COLUMN chat_messages_interacted INT DEFAULT 0,
+ ADD COLUMN chat_messages_sent INT DEFAULT 0,
+ ADD COLUMN code_fix_acceptance_event_count INT DEFAULT 0,
+ ADD COLUMN code_fix_accepted_lines INT DEFAULT 0,
+ ADD COLUMN code_fix_generated_lines INT DEFAULT 0,
+ ADD COLUMN code_fix_generation_event_count INT DEFAULT 0,
+ ADD COLUMN code_review_failed_event_count INT DEFAULT 0,
+ ADD COLUMN dev_acceptance_event_count INT DEFAULT 0,
+ ADD COLUMN dev_accepted_lines INT DEFAULT 0,
+ ADD COLUMN dev_generated_lines INT DEFAULT 0,
+ ADD COLUMN dev_generation_event_count INT DEFAULT 0,
+ ADD COLUMN doc_generation_accepted_file_updates INT DEFAULT 0,
+ ADD COLUMN doc_generation_accepted_files_creations INT DEFAULT 0,
+ ADD COLUMN doc_generation_accepted_line_additions INT DEFAULT 0,
+ ADD COLUMN doc_generation_accepted_line_updates INT DEFAULT 0,
+ ADD COLUMN doc_generation_event_count INT DEFAULT 0,
+ ADD COLUMN doc_generation_rejected_file_creations INT DEFAULT 0,
+ ADD COLUMN doc_generation_rejected_file_updates INT DEFAULT 0,
+ ADD COLUMN doc_generation_rejected_line_additions INT DEFAULT 0,
+ ADD COLUMN doc_generation_rejected_line_updates INT DEFAULT 0,
+ ADD COLUMN test_generation_accepted_lines INT DEFAULT 0,
+ ADD COLUMN test_generation_accepted_tests INT DEFAULT 0,
+ ADD COLUMN test_generation_event_count INT DEFAULT 0,
+ ADD COLUMN test_generation_generated_lines INT DEFAULT 0,
+ ADD COLUMN test_generation_generated_tests INT DEFAULT 0,
+ ADD COLUMN transformation_event_count INT DEFAULT 0,
+ ADD COLUMN transformation_lines_generated INT DEFAULT 0,
+ ADD COLUMN transformation_lines_ingested INT DEFAULT 0
+ `)
+
+ return nil
+}
+
+func (*addMissingMetrics) Version() uint64 {
+ return 20250710000001
+}
+
+func (*addMissingMetrics) Name() string {
+ return "add missing metrics columns to QDevUserData table"
+}
diff --git a/backend/plugins/q_dev/models/migrationscripts/register.go
b/backend/plugins/q_dev/models/migrationscripts/register.go
index 4020753d5..85a74690e 100644
--- a/backend/plugins/q_dev/models/migrationscripts/register.go
+++ b/backend/plugins/q_dev/models/migrationscripts/register.go
@@ -27,5 +27,6 @@ func All() []plugin.MigrationScript {
new(initTables),
new(modifyFileMetaTable),
new(addDisplayNameFields),
+ new(addMissingMetrics),
}
}
diff --git a/backend/plugins/q_dev/models/user_data.go
b/backend/plugins/q_dev/models/user_data.go
index 107076742..75907121e 100644
--- a/backend/plugins/q_dev/models/user_data.go
+++ b/backend/plugins/q_dev/models/user_data.go
@@ -26,25 +26,55 @@ import (
// QDevUserData 存储从CSV中提取的原始数据
type QDevUserData struct {
common.Model
- ConnectionId uint64 `gorm:"primaryKey"`
- UserId string `gorm:"index" json:"userId"`
- Date time.Time `gorm:"index" json:"date"`
- DisplayName string `gorm:"type:varchar(255)"
json:"displayName"` // New field for user display name
- CodeReview_FindingsCount int
- CodeReview_SucceededEventCount int
- InlineChat_AcceptanceEventCount int
- InlineChat_AcceptedLineAdditions int
- InlineChat_AcceptedLineDeletions int
- InlineChat_DismissalEventCount int
- InlineChat_DismissedLineAdditions int
- InlineChat_DismissedLineDeletions int
- InlineChat_RejectedLineAdditions int
- InlineChat_RejectedLineDeletions int
- InlineChat_RejectionEventCount int
- InlineChat_TotalEventCount int
- Inline_AICodeLines int
- Inline_AcceptanceCount int
- Inline_SuggestionsCount int
+ ConnectionId uint64 `gorm:"primaryKey"`
+ UserId string `gorm:"index" json:"userId"`
+ Date time.Time `gorm:"index" json:"date"`
+ DisplayName string `gorm:"type:varchar(255)" json:"displayName"` //
New field for user display name
+
+ CodeReview_FindingsCount int
+ CodeReview_SucceededEventCount int
+ InlineChat_AcceptanceEventCount int
+ InlineChat_AcceptedLineAdditions int
+ InlineChat_AcceptedLineDeletions int
+ InlineChat_DismissalEventCount int
+ InlineChat_DismissedLineAdditions int
+ InlineChat_DismissedLineDeletions int
+ InlineChat_RejectedLineAdditions int
+ InlineChat_RejectedLineDeletions int
+ InlineChat_RejectionEventCount int
+ InlineChat_TotalEventCount int
+ Inline_AICodeLines int
+ Inline_AcceptanceCount int
+ Inline_SuggestionsCount int
+ Chat_AICodeLines int
+ Chat_MessagesInteracted int
+ Chat_MessagesSent int
+ CodeFix_AcceptanceEventCount int
+ CodeFix_AcceptedLines int
+ CodeFix_GeneratedLines int
+ CodeFix_GenerationEventCount int
+ CodeReview_FailedEventCount int
+ Dev_AcceptanceEventCount int
+ Dev_AcceptedLines int
+ Dev_GeneratedLines int
+ Dev_GenerationEventCount int
+ DocGeneration_AcceptedFileUpdates int
+ DocGeneration_AcceptedFilesCreations int
+ DocGeneration_AcceptedLineAdditions int
+ DocGeneration_AcceptedLineUpdates int
+ DocGeneration_EventCount int
+ DocGeneration_RejectedFileCreations int
+ DocGeneration_RejectedFileUpdates int
+ DocGeneration_RejectedLineAdditions int
+ DocGeneration_RejectedLineUpdates int
+ TestGeneration_AcceptedLines int
+ TestGeneration_AcceptedTests int
+ TestGeneration_EventCount int
+ TestGeneration_GeneratedLines int
+ TestGeneration_GeneratedTests int
+ Transformation_EventCount int
+ Transformation_LinesGenerated int
+ Transformation_LinesIngested int
}
func (QDevUserData) TableName() string {
diff --git a/backend/plugins/q_dev/models/user_data_test.go
b/backend/plugins/q_dev/models/user_data_test.go
index 857fb6537..d45d1748f 100644
--- a/backend/plugins/q_dev/models/user_data_test.go
+++ b/backend/plugins/q_dev/models/user_data_test.go
@@ -20,99 +20,118 @@ package models
import (
"testing"
"time"
-
+
"github.com/stretchr/testify/assert"
)
-func TestQDevUserData_WithDisplayName(t *testing.T) {
- userData := QDevUserData{
- ConnectionId: 1,
- UserId: "uuid-123",
- DisplayName: "John Doe",
- Date: time.Now(),
- CodeReview_FindingsCount: 5,
- Inline_AcceptanceCount: 10,
- }
-
- assert.Equal(t, "John Doe", userData.DisplayName)
- assert.Equal(t, "uuid-123", userData.UserId)
- assert.Equal(t, uint64(1), userData.ConnectionId)
- assert.Equal(t, 5, userData.CodeReview_FindingsCount)
- assert.Equal(t, 10, userData.Inline_AcceptanceCount)
-}
-
-func TestQDevUserData_WithFallbackDisplayName(t *testing.T) {
- userData := QDevUserData{
- ConnectionId: 1,
- UserId: "uuid-456",
- DisplayName: "uuid-456", // Fallback case when display name
resolution fails
- Date: time.Now(),
- }
-
- assert.Equal(t, "uuid-456", userData.DisplayName)
- assert.Equal(t, userData.UserId, userData.DisplayName) // Should match
when fallback
-}
-
-func TestQDevUserData_EmptyDisplayName(t *testing.T) {
- userData := QDevUserData{
+func TestQDevUserDataAllMetrics(t *testing.T) {
+ // Create a test user data object with all metrics
+ userData := &QDevUserData{
ConnectionId: 1,
- UserId: "uuid-789",
- DisplayName: "", // Empty display name
- Date: time.Now(),
+ UserId: "test-user-id",
+ Date: time.Now(),
+ DisplayName: "Test User",
+
+ // Set values for existing metrics
+ CodeReview_FindingsCount: 10,
+ CodeReview_SucceededEventCount: 11,
+ InlineChat_AcceptanceEventCount: 12,
+ InlineChat_AcceptedLineAdditions: 13,
+ InlineChat_AcceptedLineDeletions: 14,
+ InlineChat_DismissalEventCount: 15,
+ InlineChat_DismissedLineAdditions: 16,
+ InlineChat_DismissedLineDeletions: 17,
+ InlineChat_RejectedLineAdditions: 18,
+ InlineChat_RejectedLineDeletions: 19,
+ InlineChat_RejectionEventCount: 20,
+ InlineChat_TotalEventCount: 21,
+ Inline_AICodeLines: 22,
+ Inline_AcceptanceCount: 23,
+ Inline_SuggestionsCount: 24,
+
+ // Set values for new metrics
+ Chat_AICodeLines: 25,
+ Chat_MessagesInteracted: 26,
+ Chat_MessagesSent: 27,
+ CodeFix_AcceptanceEventCount: 28,
+ CodeFix_AcceptedLines: 29,
+ CodeFix_GeneratedLines: 30,
+ CodeFix_GenerationEventCount: 31,
+ CodeReview_FailedEventCount: 32,
+ Dev_AcceptanceEventCount: 33,
+ Dev_AcceptedLines: 34,
+ Dev_GeneratedLines: 35,
+ Dev_GenerationEventCount: 36,
+ DocGeneration_AcceptedFileUpdates: 37,
+ DocGeneration_AcceptedFilesCreations: 38,
+ DocGeneration_AcceptedLineAdditions: 39,
+ DocGeneration_AcceptedLineUpdates: 40,
+ DocGeneration_EventCount: 41,
+ DocGeneration_RejectedFileCreations: 42,
+ DocGeneration_RejectedFileUpdates: 43,
+ DocGeneration_RejectedLineAdditions: 44,
+ DocGeneration_RejectedLineUpdates: 45,
+ TestGeneration_AcceptedLines: 46,
+ TestGeneration_AcceptedTests: 47,
+ TestGeneration_EventCount: 48,
+ TestGeneration_GeneratedLines: 49,
+ TestGeneration_GeneratedTests: 50,
+ Transformation_EventCount: 51,
+ Transformation_LinesGenerated: 52,
+ Transformation_LinesIngested: 53,
}
-
- assert.Equal(t, "", userData.DisplayName)
- assert.Equal(t, "uuid-789", userData.UserId)
- assert.NotEqual(t, userData.UserId, userData.DisplayName)
+
+ // Verify that all metrics are accessible
+ // Existing metrics
+ assert.Equal(t, 10, userData.CodeReview_FindingsCount)
+ assert.Equal(t, 11, userData.CodeReview_SucceededEventCount)
+ assert.Equal(t, 12, userData.InlineChat_AcceptanceEventCount)
+ assert.Equal(t, 13, userData.InlineChat_AcceptedLineAdditions)
+ assert.Equal(t, 14, userData.InlineChat_AcceptedLineDeletions)
+ assert.Equal(t, 15, userData.InlineChat_DismissalEventCount)
+ assert.Equal(t, 16, userData.InlineChat_DismissedLineAdditions)
+ assert.Equal(t, 17, userData.InlineChat_DismissedLineDeletions)
+ assert.Equal(t, 18, userData.InlineChat_RejectedLineAdditions)
+ assert.Equal(t, 19, userData.InlineChat_RejectedLineDeletions)
+ assert.Equal(t, 20, userData.InlineChat_RejectionEventCount)
+ assert.Equal(t, 21, userData.InlineChat_TotalEventCount)
+ assert.Equal(t, 22, userData.Inline_AICodeLines)
+ assert.Equal(t, 23, userData.Inline_AcceptanceCount)
+ assert.Equal(t, 24, userData.Inline_SuggestionsCount)
+
+ // New metrics
+ assert.Equal(t, 25, userData.Chat_AICodeLines)
+ assert.Equal(t, 26, userData.Chat_MessagesInteracted)
+ assert.Equal(t, 27, userData.Chat_MessagesSent)
+ assert.Equal(t, 28, userData.CodeFix_AcceptanceEventCount)
+ assert.Equal(t, 29, userData.CodeFix_AcceptedLines)
+ assert.Equal(t, 30, userData.CodeFix_GeneratedLines)
+ assert.Equal(t, 31, userData.CodeFix_GenerationEventCount)
+ assert.Equal(t, 32, userData.CodeReview_FailedEventCount)
+ assert.Equal(t, 33, userData.Dev_AcceptanceEventCount)
+ assert.Equal(t, 34, userData.Dev_AcceptedLines)
+ assert.Equal(t, 35, userData.Dev_GeneratedLines)
+ assert.Equal(t, 36, userData.Dev_GenerationEventCount)
+ assert.Equal(t, 37, userData.DocGeneration_AcceptedFileUpdates)
+ assert.Equal(t, 38, userData.DocGeneration_AcceptedFilesCreations)
+ assert.Equal(t, 39, userData.DocGeneration_AcceptedLineAdditions)
+ assert.Equal(t, 40, userData.DocGeneration_AcceptedLineUpdates)
+ assert.Equal(t, 41, userData.DocGeneration_EventCount)
+ assert.Equal(t, 42, userData.DocGeneration_RejectedFileCreations)
+ assert.Equal(t, 43, userData.DocGeneration_RejectedFileUpdates)
+ assert.Equal(t, 44, userData.DocGeneration_RejectedLineAdditions)
+ assert.Equal(t, 45, userData.DocGeneration_RejectedLineUpdates)
+ assert.Equal(t, 46, userData.TestGeneration_AcceptedLines)
+ assert.Equal(t, 47, userData.TestGeneration_AcceptedTests)
+ assert.Equal(t, 48, userData.TestGeneration_EventCount)
+ assert.Equal(t, 49, userData.TestGeneration_GeneratedLines)
+ assert.Equal(t, 50, userData.TestGeneration_GeneratedTests)
+ assert.Equal(t, 51, userData.Transformation_EventCount)
+ assert.Equal(t, 52, userData.Transformation_LinesGenerated)
+ assert.Equal(t, 53, userData.Transformation_LinesIngested)
}
-func TestQDevUserData_TableName(t *testing.T) {
- userData := QDevUserData{}
+func TestQDevUserDataTableName(t *testing.T) {
+ userData := &QDevUserData{}
assert.Equal(t, "_tool_q_dev_user_data", userData.TableName())
}
-
-func TestQDevUserData_AllFields(t *testing.T) {
- now := time.Now()
- userData := QDevUserData{
- ConnectionId: 1,
- UserId: "test-user",
- DisplayName: "Test User",
- Date: now,
- CodeReview_FindingsCount: 1,
- CodeReview_SucceededEventCount: 2,
- InlineChat_AcceptanceEventCount: 3,
- InlineChat_AcceptedLineAdditions: 4,
- InlineChat_AcceptedLineDeletions: 5,
- InlineChat_DismissalEventCount: 6,
- InlineChat_DismissedLineAdditions: 7,
- InlineChat_DismissedLineDeletions: 8,
- InlineChat_RejectedLineAdditions: 9,
- InlineChat_RejectedLineDeletions: 10,
- InlineChat_RejectionEventCount: 11,
- InlineChat_TotalEventCount: 12,
- Inline_AICodeLines: 13,
- Inline_AcceptanceCount: 14,
- Inline_SuggestionsCount: 15,
- }
-
- // Verify all fields are properly set
- assert.Equal(t, uint64(1), userData.ConnectionId)
- assert.Equal(t, "test-user", userData.UserId)
- assert.Equal(t, "Test User", userData.DisplayName)
- assert.Equal(t, now, userData.Date)
- assert.Equal(t, 1, userData.CodeReview_FindingsCount)
- assert.Equal(t, 2, userData.CodeReview_SucceededEventCount)
- assert.Equal(t, 3, userData.InlineChat_AcceptanceEventCount)
- assert.Equal(t, 4, userData.InlineChat_AcceptedLineAdditions)
- assert.Equal(t, 5, userData.InlineChat_AcceptedLineDeletions)
- assert.Equal(t, 6, userData.InlineChat_DismissalEventCount)
- assert.Equal(t, 7, userData.InlineChat_DismissedLineAdditions)
- assert.Equal(t, 8, userData.InlineChat_DismissedLineDeletions)
- assert.Equal(t, 9, userData.InlineChat_RejectedLineAdditions)
- assert.Equal(t, 10, userData.InlineChat_RejectedLineDeletions)
- assert.Equal(t, 11, userData.InlineChat_RejectionEventCount)
- assert.Equal(t, 12, userData.InlineChat_TotalEventCount)
- assert.Equal(t, 13, userData.Inline_AICodeLines)
- assert.Equal(t, 14, userData.Inline_AcceptanceCount)
- assert.Equal(t, 15, userData.Inline_SuggestionsCount)
-}
diff --git a/backend/plugins/q_dev/tasks/s3_data_extractor.go
b/backend/plugins/q_dev/tasks/s3_data_extractor.go
index a4cbbcbdf..f091e15b9 100644
--- a/backend/plugins/q_dev/tasks/s3_data_extractor.go
+++ b/backend/plugins/q_dev/tasks/s3_data_extractor.go
@@ -20,16 +20,17 @@ package tasks
import (
"encoding/csv"
"fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/plugins/q_dev/models"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
- "io"
- "strconv"
- "strings"
- "time"
)
var _ plugin.SubTaskEntryPoint = ExtractQDevS3Data
@@ -70,21 +71,34 @@ func ExtractQDevS3Data(taskCtx plugin.SubTaskContext)
errors.Error {
return errors.Convert(err)
}
- // 处理CSV文件
- err = processCSVData(taskCtx, db, getResult.Body, fileMeta)
- if err != nil {
- return errors.Default.Wrap(err, fmt.Sprintf("failed to
process CSV file %s", fileMeta.FileName))
+ // Use a transaction to process the file and update its status
+ tx := db.Begin()
+ csvErr := processCSVData(taskCtx, tx, getResult.Body, fileMeta)
+ if csvErr != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ taskCtx.GetLogger().Error(rollbackErr, "failed
to rollback transaction")
+ }
+ return errors.Default.Wrap(csvErr, fmt.Sprintf("failed
to process CSV file %s", fileMeta.FileName))
}
- // 更新文件处理状态
+ // Update file processing status within the same transaction
fileMeta.Processed = true
now := time.Now()
fileMeta.ProcessedTime = &now
- err = db.Update(fileMeta)
+ err = tx.Update(fileMeta)
if err != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ taskCtx.GetLogger().Error(rollbackErr, "failed
to rollback transaction")
+ }
return errors.Default.Wrap(err, "failed to update file
metadata")
}
+ // Commit the transaction
+ err = tx.Commit()
+ if err != nil {
+ return errors.Default.Wrap(err, "failed to commit
transaction")
+ }
+
taskCtx.IncProgress(1)
}
@@ -126,7 +140,7 @@ func processCSVData(taskCtx plugin.SubTaskContext, db
dal.Dal, reader io.ReadClo
return errors.Default.Wrap(err, "failed to create user
data")
}
- // 保存到数据库
+ // Save to database - no need to check for duplicates since
we're processing each file only once
err = db.Create(userData)
if err != nil {
return errors.Default.Wrap(err, "failed to save user
data")
@@ -187,7 +201,7 @@ func createUserDataWithDisplayName(headers []string, record
[]string, fileMeta *
return nil, errors.Default.Wrap(err, "failed to parse date")
}
- // 设置指标字段
+ // 设置所有指标字段
userData.CodeReview_FindingsCount = parseInt(fieldMap,
"CodeReview_FindingsCount")
userData.CodeReview_SucceededEventCount = parseInt(fieldMap,
"CodeReview_SucceededEventCount")
userData.InlineChat_AcceptanceEventCount = parseInt(fieldMap,
"InlineChat_AcceptanceEventCount")
@@ -203,6 +217,35 @@ func createUserDataWithDisplayName(headers []string,
record []string, fileMeta *
userData.Inline_AICodeLines = parseInt(fieldMap, "Inline_AICodeLines")
userData.Inline_AcceptanceCount = parseInt(fieldMap,
"Inline_AcceptanceCount")
userData.Inline_SuggestionsCount = parseInt(fieldMap,
"Inline_SuggestionsCount")
+ userData.Chat_AICodeLines = parseInt(fieldMap, "Chat_AICodeLines")
+ userData.Chat_MessagesInteracted = parseInt(fieldMap,
"Chat_MessagesInteracted")
+ userData.Chat_MessagesSent = parseInt(fieldMap, "Chat_MessagesSent")
+ userData.CodeFix_AcceptanceEventCount = parseInt(fieldMap,
"CodeFix_AcceptanceEventCount")
+ userData.CodeFix_AcceptedLines = parseInt(fieldMap,
"CodeFix_AcceptedLines")
+ userData.CodeFix_GeneratedLines = parseInt(fieldMap,
"CodeFix_GeneratedLines")
+ userData.CodeFix_GenerationEventCount = parseInt(fieldMap,
"CodeFix_GenerationEventCount")
+ userData.CodeReview_FailedEventCount = parseInt(fieldMap,
"CodeReview_FailedEventCount")
+ userData.Dev_AcceptanceEventCount = parseInt(fieldMap,
"Dev_AcceptanceEventCount")
+ userData.Dev_AcceptedLines = parseInt(fieldMap, "Dev_AcceptedLines")
+ userData.Dev_GeneratedLines = parseInt(fieldMap, "Dev_GeneratedLines")
+ userData.Dev_GenerationEventCount = parseInt(fieldMap,
"Dev_GenerationEventCount")
+ userData.DocGeneration_AcceptedFileUpdates = parseInt(fieldMap,
"DocGeneration_AcceptedFileUpdates")
+ userData.DocGeneration_AcceptedFilesCreations = parseInt(fieldMap,
"DocGeneration_AcceptedFilesCreations")
+ userData.DocGeneration_AcceptedLineAdditions = parseInt(fieldMap,
"DocGeneration_AcceptedLineAdditions")
+ userData.DocGeneration_AcceptedLineUpdates = parseInt(fieldMap,
"DocGeneration_AcceptedLineUpdates")
+ userData.DocGeneration_EventCount = parseInt(fieldMap,
"DocGeneration_EventCount")
+ userData.DocGeneration_RejectedFileCreations = parseInt(fieldMap,
"DocGeneration_RejectedFileCreations")
+ userData.DocGeneration_RejectedFileUpdates = parseInt(fieldMap,
"DocGeneration_RejectedFileUpdates")
+ userData.DocGeneration_RejectedLineAdditions = parseInt(fieldMap,
"DocGeneration_RejectedLineAdditions")
+ userData.DocGeneration_RejectedLineUpdates = parseInt(fieldMap,
"DocGeneration_RejectedLineUpdates")
+ userData.TestGeneration_AcceptedLines = parseInt(fieldMap,
"TestGeneration_AcceptedLines")
+ userData.TestGeneration_AcceptedTests = parseInt(fieldMap,
"TestGeneration_AcceptedTests")
+ userData.TestGeneration_EventCount = parseInt(fieldMap,
"TestGeneration_EventCount")
+ userData.TestGeneration_GeneratedLines = parseInt(fieldMap,
"TestGeneration_GeneratedLines")
+ userData.TestGeneration_GeneratedTests = parseInt(fieldMap,
"TestGeneration_GeneratedTests")
+ userData.Transformation_EventCount = parseInt(fieldMap,
"Transformation_EventCount")
+ userData.Transformation_LinesGenerated = parseInt(fieldMap,
"Transformation_LinesGenerated")
+ userData.Transformation_LinesIngested = parseInt(fieldMap,
"Transformation_LinesIngested")
return userData, nil
}
diff --git a/backend/plugins/q_dev/tasks/s3_data_extractor_test.go
b/backend/plugins/q_dev/tasks/s3_data_extractor_test.go
index 8c824f3e1..1e84e81ef 100644
--- a/backend/plugins/q_dev/tasks/s3_data_extractor_test.go
+++ b/backend/plugins/q_dev/tasks/s3_data_extractor_test.go
@@ -118,7 +118,7 @@ func TestCreateUserDataWithDisplayName_EmptyDisplayName(t
*testing.T) {
mockIdentityClient.AssertExpectations(t)
}
-func TestCreateUserDataWithDisplayName_AllFields(t *testing.T) {
+func TestCreateUserDataWithDisplayName_AllExistingMetrics(t *testing.T) {
headers := []string{
"UserId", "Date", "CodeReview_FindingsCount",
"CodeReview_SucceededEventCount",
"InlineChat_AcceptanceEventCount",
"InlineChat_AcceptedLineAdditions",
@@ -152,7 +152,7 @@ func TestCreateUserDataWithDisplayName_AllFields(t
*testing.T) {
expectedDate, _ := time.Parse("2006-01-02", "2025-06-23")
assert.Equal(t, expectedDate, userData.Date)
- // Verify all metric fields
+ // Verify all existing metric fields
assert.Equal(t, 1, userData.CodeReview_FindingsCount)
assert.Equal(t, 2, userData.CodeReview_SucceededEventCount)
assert.Equal(t, 3, userData.InlineChat_AcceptanceEventCount)
@@ -172,6 +172,139 @@ func TestCreateUserDataWithDisplayName_AllFields(t
*testing.T) {
mockIdentityClient.AssertExpectations(t)
}
+func TestCreateUserDataWithDisplayName_AllNewMetrics(t *testing.T) {
+ headers := []string{
+ "UserId", "Date",
+ "Chat_AICodeLines", "Chat_MessagesInteracted",
"Chat_MessagesSent",
+ "CodeFix_AcceptanceEventCount", "CodeFix_AcceptedLines",
"CodeFix_GeneratedLines", "CodeFix_GenerationEventCount",
+ "CodeReview_FailedEventCount",
+ "Dev_AcceptanceEventCount", "Dev_AcceptedLines",
"Dev_GeneratedLines", "Dev_GenerationEventCount",
+ "DocGeneration_AcceptedFileUpdates",
"DocGeneration_AcceptedFilesCreations", "DocGeneration_AcceptedLineAdditions",
+ "DocGeneration_AcceptedLineUpdates",
"DocGeneration_EventCount", "DocGeneration_RejectedFileCreations",
+ "DocGeneration_RejectedFileUpdates",
"DocGeneration_RejectedLineAdditions", "DocGeneration_RejectedLineUpdates",
+ "TestGeneration_AcceptedLines", "TestGeneration_AcceptedTests",
"TestGeneration_EventCount",
+ "TestGeneration_GeneratedLines",
"TestGeneration_GeneratedTests",
+ "Transformation_EventCount", "Transformation_LinesGenerated",
"Transformation_LinesIngested",
+ }
+
+ record := []string{
+ "test-user", "2025-06-23",
+ "101", "102", "103", "104", "105", "106", "107", "108", "109",
"110",
+ "111", "112", "113", "114", "115", "116", "117", "118", "119",
"120",
+ "121", "122", "123", "124", "125", "126", "127", "128", "129",
+ }
+
+ fileMeta := &models.QDevS3FileMeta{
+ ConnectionId: 123,
+ }
+
+ mockIdentityClient := &MockIdentityClient{}
+ mockIdentityClient.On("ResolveUserDisplayName",
"test-user").Return("Test User", nil)
+
+ userData, err := createUserDataWithDisplayName(headers, record,
fileMeta, mockIdentityClient)
+
+ assert.NoError(t, err)
+ assert.NotNil(t, userData)
+
+ // Verify basic fields
+ assert.Equal(t, "test-user", userData.UserId)
+ assert.Equal(t, "Test User", userData.DisplayName)
+
+ // Verify all new metric fields
+ assert.Equal(t, 101, userData.Chat_AICodeLines)
+ assert.Equal(t, 102, userData.Chat_MessagesInteracted)
+ assert.Equal(t, 103, userData.Chat_MessagesSent)
+ assert.Equal(t, 104, userData.CodeFix_AcceptanceEventCount)
+ assert.Equal(t, 105, userData.CodeFix_AcceptedLines)
+ assert.Equal(t, 106, userData.CodeFix_GeneratedLines)
+ assert.Equal(t, 107, userData.CodeFix_GenerationEventCount)
+ assert.Equal(t, 108, userData.CodeReview_FailedEventCount)
+ assert.Equal(t, 109, userData.Dev_AcceptanceEventCount)
+ assert.Equal(t, 110, userData.Dev_AcceptedLines)
+ assert.Equal(t, 111, userData.Dev_GeneratedLines)
+ assert.Equal(t, 112, userData.Dev_GenerationEventCount)
+ assert.Equal(t, 113, userData.DocGeneration_AcceptedFileUpdates)
+ assert.Equal(t, 114, userData.DocGeneration_AcceptedFilesCreations)
+ assert.Equal(t, 115, userData.DocGeneration_AcceptedLineAdditions)
+ assert.Equal(t, 116, userData.DocGeneration_AcceptedLineUpdates)
+ assert.Equal(t, 117, userData.DocGeneration_EventCount)
+ assert.Equal(t, 118, userData.DocGeneration_RejectedFileCreations)
+ assert.Equal(t, 119, userData.DocGeneration_RejectedFileUpdates)
+ assert.Equal(t, 120, userData.DocGeneration_RejectedLineAdditions)
+ assert.Equal(t, 121, userData.DocGeneration_RejectedLineUpdates)
+ assert.Equal(t, 122, userData.TestGeneration_AcceptedLines)
+ assert.Equal(t, 123, userData.TestGeneration_AcceptedTests)
+ assert.Equal(t, 124, userData.TestGeneration_EventCount)
+ assert.Equal(t, 125, userData.TestGeneration_GeneratedLines)
+ assert.Equal(t, 126, userData.TestGeneration_GeneratedTests)
+ assert.Equal(t, 127, userData.Transformation_EventCount)
+ assert.Equal(t, 128, userData.Transformation_LinesGenerated)
+ assert.Equal(t, 129, userData.Transformation_LinesIngested)
+
+ mockIdentityClient.AssertExpectations(t)
+}
+
+func TestCreateUserDataWithDisplayName_MissingMetrics(t *testing.T) {
+ // Only provide a few metrics in the CSV
+ headers := []string{"UserId", "Date", "CodeReview_FindingsCount",
"Chat_AICodeLines"}
+ record := []string{"test-user", "2025-06-23", "42", "99"}
+
+ fileMeta := &models.QDevS3FileMeta{
+ ConnectionId: 123,
+ }
+
+ mockIdentityClient := &MockIdentityClient{}
+ mockIdentityClient.On("ResolveUserDisplayName",
"test-user").Return("Test User", nil)
+
+ userData, err := createUserDataWithDisplayName(headers, record,
fileMeta, mockIdentityClient)
+
+ assert.NoError(t, err)
+ assert.NotNil(t, userData)
+
+ // Verify provided metrics are set correctly
+ assert.Equal(t, 42, userData.CodeReview_FindingsCount)
+ assert.Equal(t, 99, userData.Chat_AICodeLines)
+
+ // Verify missing metrics are set to 0
+ assert.Equal(t, 0, userData.CodeReview_SucceededEventCount)
+ assert.Equal(t, 0, userData.InlineChat_AcceptanceEventCount)
+ assert.Equal(t, 0, userData.Chat_MessagesInteracted)
+ assert.Equal(t, 0, userData.TestGeneration_AcceptedTests)
+ assert.Equal(t, 0, userData.Transformation_LinesIngested)
+
+ mockIdentityClient.AssertExpectations(t)
+}
+
+func TestCreateUserDataWithDisplayName_InvalidMetricValues(t *testing.T) {
+ headers := []string{
+ "UserId", "Date", "CodeReview_FindingsCount",
"Chat_AICodeLines",
+ "InlineChat_AcceptanceEventCount",
"TestGeneration_AcceptedTests",
+ }
+ record := []string{"test-user", "2025-06-23", "42", "not-a-number",
"abc", ""}
+
+ fileMeta := &models.QDevS3FileMeta{
+ ConnectionId: 123,
+ }
+
+ mockIdentityClient := &MockIdentityClient{}
+ mockIdentityClient.On("ResolveUserDisplayName",
"test-user").Return("Test User", nil)
+
+ userData, err := createUserDataWithDisplayName(headers, record,
fileMeta, mockIdentityClient)
+
+ assert.NoError(t, err)
+ assert.NotNil(t, userData)
+
+ // Verify valid metric is set correctly
+ assert.Equal(t, 42, userData.CodeReview_FindingsCount)
+
+ // Verify invalid metrics are set to 0
+ assert.Equal(t, 0, userData.Chat_AICodeLines)
+ assert.Equal(t, 0, userData.InlineChat_AcceptanceEventCount)
+ assert.Equal(t, 0, userData.TestGeneration_AcceptedTests)
+
+ mockIdentityClient.AssertExpectations(t)
+}
+
func TestCreateUserDataWithDisplayName_MissingUserId(t *testing.T) {
headers := []string{"Date", "CodeReview_FindingsCount"}
record := []string{"2025-06-23", "5"}
@@ -199,3 +332,46 @@ func TestCreateUserDataWithDisplayName_MissingDate(t
*testing.T) {
assert.Nil(t, userData)
assert.Contains(t, err.Error(), "Date not found")
}
+
+func TestParseDate(t *testing.T) {
+ testCases := []struct {
+ dateStr string
+ expectedDate time.Time
+ expectError bool
+ }{
+ {"2025-07-10", time.Date(2025, 7, 10, 0, 0, 0, 0, time.UTC),
false},
+ {"2025/07/10", time.Date(2025, 7, 10, 0, 0, 0, 0, time.UTC),
false},
+ {"07/10/2025", time.Date(2025, 7, 10, 0, 0, 0, 0, time.UTC),
false},
+ {"07-10-2025", time.Date(2025, 7, 10, 0, 0, 0, 0, time.UTC),
false},
+ {"2025-07-10T15:04:05Z", time.Date(2025, 7, 10, 15, 4, 5, 0,
time.UTC), false},
+ {"invalid-date", time.Time{}, true},
+ }
+
+ for _, tc := range testCases {
+ date, err := parseDate(tc.dateStr)
+
+ if tc.expectError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.expectedDate, date)
+ }
+ }
+}
+
+func TestParseInt(t *testing.T) {
+ fieldMap := map[string]string{
+ "ValidInt": "42",
+ "ZeroInt": "0",
+ "NegativeInt": "-10",
+ "InvalidInt": "not-a-number",
+ "EmptyString": "",
+ }
+
+ assert.Equal(t, 42, parseInt(fieldMap, "ValidInt"))
+ assert.Equal(t, 0, parseInt(fieldMap, "ZeroInt"))
+ assert.Equal(t, -10, parseInt(fieldMap, "NegativeInt"))
+ assert.Equal(t, 0, parseInt(fieldMap, "InvalidInt"))
+ assert.Equal(t, 0, parseInt(fieldMap, "EmptyString"))
+ assert.Equal(t, 0, parseInt(fieldMap, "NonExistentField"))
+}
diff --git a/backend/plugins/q_dev/tasks/s3_file_collector.go
b/backend/plugins/q_dev/tasks/s3_file_collector.go
index d06f066e1..37abf1845 100644
--- a/backend/plugins/q_dev/tasks/s3_file_collector.go
+++ b/backend/plugins/q_dev/tasks/s3_file_collector.go
@@ -18,13 +18,14 @@ limitations under the License.
package tasks
import (
+ "strings"
+
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/plugins/q_dev/models"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
- "strings"
)
var _ plugin.SubTaskEntryPoint = CollectQDevS3Files
@@ -43,12 +44,6 @@ func CollectQDevS3Files(taskCtx plugin.SubTaskContext)
errors.Error {
taskCtx.SetProgress(0, -1)
- // 清空以前的元数据记录
- err := db.Delete(&models.QDevS3FileMeta{}, dal.Where("connection_id =
?", data.Options.ConnectionId))
- if err != nil {
- return errors.Default.Wrap(err, "failed to clean previous file
metadata")
- }
-
for {
input := &s3.ListObjectsV2Input{
Bucket: aws.String(data.S3Client.Bucket),
@@ -63,12 +58,31 @@ func CollectQDevS3Files(taskCtx plugin.SubTaskContext)
errors.Error {
// 处理每个CSV文件
for _, object := range result.Contents {
- // 只处理CSV文件
+ // Only process CSV files
if !strings.HasSuffix(*object.Key, ".csv") {
+ taskCtx.GetLogger().Debug("Skipping non-CSV
file: %s", *object.Key)
+ continue
+ }
+
+ // Check if this file already exists in our database
+ existingFile := &models.QDevS3FileMeta{}
+ err = db.First(existingFile, dal.Where("connection_id =
? AND s3_path = ?",
+ data.Options.ConnectionId, *object.Key))
+
+ if err == nil {
+ // File already exists in database, skip it if
it's already processed
+ if existingFile.Processed {
+ taskCtx.GetLogger().Debug("Skipping
already processed file: %s", *object.Key)
+ continue
+ }
+ // Otherwise, we'll keep the existing record
(which is still marked as unprocessed)
+ taskCtx.GetLogger().Debug("Found existing
unprocessed file: %s", *object.Key)
continue
+ } else if !db.IsErrorNotFound(err) {
+ return errors.Default.Wrap(err, "failed to
query existing file metadata")
}
- // 保存文件元数据
+ // This is a new file, save its metadata
fileMeta := &models.QDevS3FileMeta{
ConnectionId: data.Options.ConnectionId,
FileName: *object.Key,
diff --git a/grafana/dashboards/qdev_user_data.json
b/grafana/dashboards/qdev_user_data.json
index 9a66d82b7..3bdf3c427 100644
--- a/grafana/dashboards/qdev_user_data.json
+++ b/grafana/dashboards/qdev_user_data.json
@@ -72,7 +72,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n COUNT(DISTINCT user_id) as 'Active Users',\n
SUM(inline_ai_code_lines) as 'Accepted Lines (Inline Suggestion)',\n
SUM(inline_acceptance_count) as 'Accepted Count (Inline Suggestion)',\n
SUM(inline_suggestions_count) as 'Total Count (Inline Suggestion)',\n
SUM(inline_acceptance_count) / NULLIF(SUM(inline_suggestions_count), 0) as
'Acceptance Rate (Inline Suggestion)',\n SUM(code_review_findings_count) as
'Findings (Code Review)',\n SUM(inline_chat_acce [...]
+ "rawSql": "SELECT\n COUNT(DISTINCT user_id) as 'Active Users',\n
SUM(chat_ai_code_lines) as 'Accepted Lines (Chat)',\n
SUM(inline_ai_code_lines) as 'Accepted Lines (Inline Suggestion)',\n
SUM(inline_acceptance_count) / NULLIF(SUM(inline_suggestions_count), 0) as
'Acceptance Rate (Inline Suggestion)',\n SUM(code_review_findings_count) as
'Findings (Code Review)',\n SUM(code_fix_accepted_lines) as 'Accepted Lines
(Code Fix)',\n SUM(code_fix_generation_event_count) / NULLIF [...]
"refId": "A",
"select": [
[
@@ -201,7 +201,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n date as time,\n SUM(inline_ai_code_lines) as
'Inline Suggestion Accepted Lines',\n SUM(inline_chat_accepted_line_additions)
as 'Inline Chat Accepted Line Additions',\n
SUM(inline_chat_accepted_line_deletions) as 'Inline Chat Accepted Line
Deletions',\n SUM(inline_chat_dismissed_line_additions) as 'Inline Chat
Dismissed Line Additions',\n SUM(inline_chat_dismissed_line_deletions) as
'Inline Chat Dismissed Line Deletions',\n SUM(inline_chat_rejected_lin [...]
+ "rawSql": "SELECT\n date as time,\n SUM(chat_ai_code_lines) as
'Chat Accepted Lines',\n SUM(code_fix_accepted_lines) as 'Code Fix Accepted
Lines',\n SUM(code_fix_generated_lines) as 'Code Fix Generated Lines',\n
SUM(transformation_lines_ingested) as 'Java Transform Ingested Lines',\n
SUM(transformation_lines_generated) as 'Java Transform Generated Lines',\n
SUM(inline_ai_code_lines) as 'Inline Suggestion Accepted Lines',\n
SUM(inline_chat_accepted_line_additions) as 'In [...]
"refId": "A",
"select": [
[
@@ -245,7 +245,7 @@
},
{
"datasource": "mysql",
- "description": "Daily AI code interaction trends across all users",
+ "description": "Daily AI interaction trends across all users",
"fieldConfig": {
"defaults": {
"color": {
@@ -330,7 +330,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n date as time,\n SUM(inline_acceptance_count)
as 'Inline Suggestion Accepted Suggestions',\n SUM(inline_suggestions_count)
as 'Inline Suggestion Count',\n SUM(inline_chat_total_event_count) as 'Inline
Chat Total Suggestions',\n SUM(inline_chat_acceptance_event_count) as 'Inline
Chat Accepted Suggestions',\n SUM(inline_chat_dismissal_event_count) as
'Inline Chat Dismissed Suggestions',\n SUM(inline_chat_rejection_event_count)
as 'Inline Chat Rejected Su [...]
+ "rawSql": "SELECT\n date as time,\n SUM(chat_messages_sent) as
'Chat Messages Sent',\n SUM(code_fix_acceptance_event_count) as 'Code Fix
Accepted Event Count',\n SUM(code_fix_generation_event_count) as 'Code Fix
Generated Event Count',\n SUM(transformation_event_count) as 'Java Transform
Event Count',\n SUM(inline_acceptance_count) as 'Inline Suggestion Accepted
Suggestions',\n SUM(inline_suggestions_count) as 'Inline Suggestion Count',\n
SUM(inline_chat_total_event_cou [...]
"refId": "A",
"select": [
[
@@ -369,7 +369,7 @@
]
}
],
- "title": "Daily AI Suggestion Interactions",
+ "title": "Daily AI Interactions",
"type": "timeseries"
},
{
@@ -458,7 +458,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n date as time,\n
SUM(code_review_findings_count) as 'Code Review Findings',\n
SUM(code_review_succeeded_event_count) as 'Code Review Succeeded Events'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
+ "rawSql": "SELECT\n date as time,\n
SUM(code_fix_acceptance_event_count) as 'Code Fix Accepted Event Count',\n
SUM(code_fix_generation_event_count) as 'Code Fix Generated Event Count',\n
SUM(code_review_findings_count) as 'Total Findings'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
"refId": "A",
"select": [
[
@@ -470,6 +470,23 @@
}
]
],
+ "sql": {
+ "columns": [
+ {
+ "parameters": [],
+ "type": "function"
+ }
+ ],
+ "groupBy": [
+ {
+ "property": {
+ "type": "string"
+ },
+ "type": "groupBy"
+ }
+ ],
+ "limit": 50
+ },
"timeColumn": "time",
"where": [
{
@@ -570,7 +587,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n date as time,\n SUM(inline_acceptance_count) /
NULLIF(SUM(inline_suggestions_count), 0) as 'Inline Suggestions Acceptance
Rate',\n SUM(inline_chat_acceptance_event_count) /
NULLIF(SUM(inline_chat_total_event_count), 0) as 'Inline Chat Acceptance
Rate'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY
date\nORDER BY date",
+ "rawSql": "SELECT\n date as time,\n
SUM(code_fix_acceptance_event_count) /
NULLIF(SUM(code_fix_generation_event_count), 0) as 'Code Fix',\n
SUM(inline_acceptance_count) / NULLIF(SUM(inline_suggestions_count), 0) as
'Inline Suggestions',\n SUM(inline_chat_acceptance_event_count) /
NULLIF(SUM(inline_chat_total_event_count), 0) as 'Inline Chat'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
"refId": "A",
"select": [
[
@@ -713,7 +730,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n COALESCE(display_name, user_id) as 'User',\n
SUM(inline_ai_code_lines) as 'Accepted Lines (Inline Suggestion)',\n
SUM(inline_acceptance_count) as 'Accepted Count (Inline Suggestion)',\n
SUM(inline_suggestions_count) as 'Total Count (Inline Suggestion)',\n
CONCAT(ROUND(SUM(inline_acceptance_count) /
NULLIF(SUM(inline_suggestions_count), 0) * 100, 2), '%') as 'Acceptance Rate
(Inline Suggestion)',\n SUM(code_review_findings_count) as 'Findings (Code
Revi [...]
+ "rawSql": "SELECT\n COALESCE(display_name, user_id) as 'User',\n
SUM(chat_ai_code_lines) as 'Accepted Lines (Chat)',\n
SUM(transformation_lines_ingested) as 'Lines Ingested (Java Transform)',\n
SUM(transformation_lines_generated) as 'Lines Generated (Java Transform)',\n
SUM(transformation_event_count) as 'Event Count (Java Transform)',\n
SUM(code_review_findings_count) as 'Findings (Code Review)',\n
SUM(code_fix_accepted_lines) as 'Accepted Lines (Code Fix)',\n SUM(code [...]
"refId": "A",
"select": [
[
@@ -754,216 +771,6 @@
],
"title": "User Interactions",
"type": "table"
- },
- {
- "datasource": "mysql",
- "description": "Distribution of AI suggestion types across users",
- "fieldConfig": {
- "defaults": {
- "color": {
- "mode": "palette-classic"
- },
- "custom": {
- "hideFrom": {
- "legend": false,
- "tooltip": false,
- "viz": false
- }
- },
- "mappings": []
- },
- "overrides": []
- },
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 0,
- "y": 40
- },
- "id": 7,
- "options": {
- "displayLabels": [
- "percent"
- ],
- "legend": {
- "displayMode": "list",
- "placement": "right",
- "showLegend": true,
- "values": [
- "value"
- ]
- },
- "pieType": "pie",
- "reduceOptions": {
- "calcs": [
- "sum"
- ],
- "fields": "",
- "values": false
- },
- "tooltip": {
- "hideZeros": false,
- "mode": "single",
- "sort": "none"
- }
- },
- "pluginVersion": "11.6.2",
- "targets": [
- {
- "datasource": "mysql",
- "format": "table",
- "group": [],
- "metricColumn": "none",
- "rawQuery": true,
- "rawSql": "SELECT\n 'Accepted' as category,\n
SUM(inline_chat_acceptance_event_count) as value\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nUNION ALL\nSELECT\n
'Dismissed' as category,\n SUM(inline_chat_dismissal_event_count) as
value\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nUNION
ALL\nSELECT\n 'Rejected' as category,\n
SUM(inline_chat_rejection_event_count) as value\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)",
- "refId": "A",
- "select": [
- [
- {
- "params": [
- "value"
- ],
- "type": "column"
- }
- ]
- ],
- "timeColumn": "time",
- "where": [
- {
- "name": "$__timeFilter",
- "params": [],
- "type": "macro"
- }
- ]
- }
- ],
- "title": "Inline Chat Response Distribution",
- "type": "piechart"
- },
- {
- "datasource": "mysql",
- "description": "Weekly trends in AI code interactions",
- "fieldConfig": {
- "defaults": {
- "color": {
- "mode": "palette-classic"
- },
- "custom": {
- "axisBorderShow": false,
- "axisCenteredZero": false,
- "axisColorMode": "text",
- "axisLabel": "",
- "axisPlacement": "auto",
- "axisSoftMin": 0,
- "fillOpacity": 80,
- "gradientMode": "none",
- "hideFrom": {
- "legend": false,
- "tooltip": false,
- "viz": false
- },
- "lineWidth": 1,
- "scaleDistribution": {
- "type": "linear"
- },
- "thresholdsStyle": {
- "mode": "off"
- }
- },
- "mappings": [],
- "thresholds": {
- "mode": "absolute",
- "steps": [
- {
- "color": "green"
- }
- ]
- }
- },
- "overrides": []
- },
- "gridPos": {
- "h": 8,
- "w": 12,
- "x": 12,
- "y": 40
- },
- "id": 8,
- "options": {
- "barRadius": 0,
- "barWidth": 0.6,
- "fullHighlight": false,
- "groupWidth": 0.7,
- "legend": {
- "calcs": [],
- "displayMode": "list",
- "placement": "right",
- "showLegend": true
- },
- "orientation": "auto",
- "showValue": "auto",
- "stacking": "none",
- "text": {
- "valueSize": 12
- },
- "tooltip": {
- "hideZeros": false,
- "mode": "single",
- "sort": "none"
- },
- "xTickLabelRotation": 0,
- "xTickLabelSpacing": 0
- },
- "pluginVersion": "11.6.2",
- "targets": [
- {
- "datasource": "mysql",
- "editorMode": "code",
- "format": "time_series",
- "group": [],
- "metricColumn": "none",
- "rawQuery": true,
- "rawSql": "SELECT\n DATE_FORMAT(date, '%Y-%U') as metric,\n
MIN(date) as time,\n SUM(inline_chat_acceptance_event_count) as 'Accepted',\n
SUM(inline_chat_dismissal_event_count) as 'Dismissed',\n
SUM(inline_chat_rejection_event_count) as 'Rejected'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY
DATE_FORMAT(date, '%Y-%U')\nORDER BY time",
- "refId": "A",
- "select": [
- [
- {
- "params": [
- "value"
- ],
- "type": "column"
- }
- ]
- ],
- "sql": {
- "columns": [
- {
- "parameters": [],
- "type": "function"
- }
- ],
- "groupBy": [
- {
- "property": {
- "type": "string"
- },
- "type": "groupBy"
- }
- ],
- "limit": 50
- },
- "timeColumn": "time",
- "where": [
- {
- "name": "$__timeFilter",
- "params": [],
- "type": "macro"
- }
- ]
- }
- ],
- "title": "Weekly Inline Chat Responses",
- "type": "barchart"
}
],
"preload": false,