From 03e201ed884e92765b7b15590d6e8f5a58349cc7 Mon Sep 17 00:00:00 2001 From: DiscreteTom Date: Thu, 10 Jul 2025 10:28:46 +0000 Subject: [PATCH 1/4] feat(q_dev): include all existing user activity report fields --- .../20250710_add_missing_metrics.go | 61 ++++++ .../q_dev/models/migrationscripts/register.go | 1 + backend/plugins/q_dev/models/user_data.go | 68 ++++-- .../plugins/q_dev/models/user_data_test.go | 193 ++++++++++-------- .../plugins/q_dev/tasks/s3_data_extractor.go | 40 +++- .../q_dev/tasks/s3_data_extractor_test.go | 180 +++++++++++++++- 6 files changed, 430 insertions(+), 113 deletions(-) create mode 100644 backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go 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 00000000000..e0e61294254 --- /dev/null +++ b/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go @@ -0,0 +1,61 @@ +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 4020753d51d..85a74690e3c 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 107076742c3..75907121ebf 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 857fb653721..d45d1748fec 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 a4cbbcbdf1b..bc5d16cc211 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 @@ -187,7 +188,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 +204,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 8c824f3e1f6..1e84e81ef05 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")) +} From 5891657da28802c67e8b412e896a588e9b676ba3 Mon Sep 17 00:00:00 2001 From: DiscreteTom Date: Thu, 10 Jul 2025 10:28:46 +0000 Subject: [PATCH 2/4] fix(q_dev): prevent duplicated data entries --- .../plugins/q_dev/tasks/s3_data_extractor.go | 23 +++++++++---- .../plugins/q_dev/tasks/s3_file_collector.go | 32 +++++++++++++------ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/backend/plugins/q_dev/tasks/s3_data_extractor.go b/backend/plugins/q_dev/tasks/s3_data_extractor.go index bc5d16cc211..73f770b8ace 100644 --- a/backend/plugins/q_dev/tasks/s3_data_extractor.go +++ b/backend/plugins/q_dev/tasks/s3_data_extractor.go @@ -71,20 +71,29 @@ 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 { + tx.Rollback() + 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 { + tx.Rollback() 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) } @@ -127,7 +136,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") diff --git a/backend/plugins/q_dev/tasks/s3_file_collector.go b/backend/plugins/q_dev/tasks/s3_file_collector.go index d06f066e14c..37abf184563 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, From 10364a8d65351dfbf45e4996f4972bc30f89a5ae Mon Sep 17 00:00:00 2001 From: DiscreteTom Date: Thu, 10 Jul 2025 10:28:46 +0000 Subject: [PATCH 3/4] chore(q_dev): lint and format --- .../20250710_add_missing_metrics.go | 17 +++++++++++++++++ .../plugins/q_dev/tasks/s3_data_extractor.go | 10 +++++++--- 2 files changed, 24 insertions(+), 3 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 index e0e61294254..d2c6ebe391b 100644 --- a/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go +++ b/backend/plugins/q_dev/models/migrationscripts/20250710_add_missing_metrics.go @@ -1,3 +1,20 @@ +/* +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 ( diff --git a/backend/plugins/q_dev/tasks/s3_data_extractor.go b/backend/plugins/q_dev/tasks/s3_data_extractor.go index 73f770b8ace..f091e15b90a 100644 --- a/backend/plugins/q_dev/tasks/s3_data_extractor.go +++ b/backend/plugins/q_dev/tasks/s3_data_extractor.go @@ -75,7 +75,9 @@ func ExtractQDevS3Data(taskCtx plugin.SubTaskContext) errors.Error { tx := db.Begin() csvErr := processCSVData(taskCtx, tx, getResult.Body, fileMeta) if csvErr != nil { - tx.Rollback() + 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)) } @@ -85,10 +87,12 @@ func ExtractQDevS3Data(taskCtx plugin.SubTaskContext) errors.Error { fileMeta.ProcessedTime = &now err = tx.Update(fileMeta) if err != nil { - tx.Rollback() + 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 { From 6d6d2e706ad13e9a6ffe00aef9308cdedae58ef5 Mon Sep 17 00:00:00 2001 From: DiscreteTom Date: Thu, 10 Jul 2025 11:07:10 +0000 Subject: [PATCH 4/4] feat(q_dev): update dashboard to include more metrics --- grafana/dashboards/qdev_user_data.json | 243 +++---------------------- 1 file changed, 25 insertions(+), 218 deletions(-) diff --git a/grafana/dashboards/qdev_user_data.json b/grafana/dashboards/qdev_user_data.json index 9a66d82b790..3bdf3c42791 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_acceptance_event_count) as 'Accepted Events (Inline Chat)',\n SUM(inline_chat_total_event_count) as 'Total Events (Inline Chat)',\n SUM(inline_chat_acceptance_event_count) / NULLIF(SUM(inline_chat_total_event_count), 0) as 'Acceptance Rate (Inline Chat)'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)", + "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(SUM(code_fix_acceptance_event_count), 0) as 'Acceptance Rate (Code Fix)',\n SUM(transformation_lines_ingested) as 'Ingested Lines (Java Transform)',\n SUM(transformation_lines_generated) as 'Generated Lines (Java Transform)',\n SUM(inline_chat_accepted_line_additions) as 'Accepted Lines (Inline Chat)',\n SUM(inline_chat_acceptance_event_count) / NULLIF(SUM(inline_chat_total_event_count), 0) as 'Acceptance Rate (Inline Chat)'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)", "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_line_additions) as 'Inline Chat Rejected Line Additions',\n SUM(inline_chat_rejected_line_deletions) as 'Inline Chat Rejected Line Deletions'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY date", + "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 '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_line_additions) as 'Inline Chat Rejected Line Additions',\n SUM(inline_chat_rejected_line_deletions) as 'Inline Chat Rejected Line Deletions'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY date", "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 Suggestions'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY date", + "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_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 Suggestions'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY date", "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 Review)',\n SUM(inline_chat_accepted_line_additions) as 'Accepted Line Additions (Inline Chat)',\n SUM(inline_chat_accepted_line_deletions) as 'Accepted Line Deletions (Inline Chat)',\n SUM(inline_chat_acceptance_event_count) as 'Accepted Events (Inline Chat)',\n SUM(inline_chat_total_event_count) as 'Total Events (Inline Chat)',\n CONCAT(ROUND(SUM(inline_chat_acceptance_event_count) / NULLIF(SUM(inline_chat_total_event_count), 0) * 100, 2), '%') as 'Acceptance Rate (Inline Chat)',\n MIN(date) as 'First Activity',\n MAX(date) as 'Last Activity'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY user_id, display_name\nORDER BY SUM(inline_ai_code_lines) DESC", + "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_fix_generated_lines) as 'Generated Lines (Code Fix)',\n SUM(code_fix_acceptance_event_count) as 'Accepted Count (Code Fix)',\n SUM(code_fix_generation_event_count) as 'Generated Count (Code Fix)',\n CONCAT(ROUND(SUM(code_fix_acceptance_event_count) / NULLIF(SUM(code_fix_generation_event_count), 0) * 100, 2), '%') as 'Acceptance Rate (Code Fix)',\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(inline_chat_accepted_line_additions) as 'Accepted Line Additions (Inline Chat)',\n SUM(inline_chat_accepted_line_deletions) as 'Accepted Line Deletions (Inline Chat)',\n SUM(inline_chat_acceptance_event_count) as 'Accepted Events (Inline Chat)',\n SUM(inline_chat_total_event_count) as 'Total Events (Inline Chat)',\n CONCAT(ROUND(SUM(inline_chat_acceptance_event_count) / NULLIF(SUM(inline_chat_total_event_count), 0) * 100, 2), '%') as 'Acceptance Rate (Inline Chat)',\n MIN(date) as 'First Activity',\n MAX(date) as 'Last Activity'\nFROM lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY user_id, display_name\nORDER BY SUM(inline_ai_code_lines) DESC", "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,