This is an automated email from the ASF dual-hosted git repository.
abeizn 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 44a722783 feat(q-dev): add logging data ingestion and enrich Kiro
dashboards (#8767)
44a722783 is described below
commit 44a722783f73e6c1fe9ff5d6f03695aaa896d850
Author: Warren Chen <[email protected]>
AuthorDate: Sun Mar 15 18:41:10 2026 +0800
feat(q-dev): add logging data ingestion and enrich Kiro dashboards (#8767)
* feat(q-dev): add logging data ingestion and enrich Kiro dashboards
Add support for ingesting S3 logging data (GenerateAssistantResponse and
GenerateCompletions events) into new database tables, and enrich all three
Kiro Grafana dashboards with additional metrics.
Changes:
- New models: QDevChatLog and QDevCompletionLog for logging event data
- New extractor: s3_logging_extractor.go parses JSON.gz logging files
- Updated S3 collector to also handle .json.gz files
- Added logging S3 prefixes (GenerateAssistantResponse, GenerateCompletions)
- New dashboard: "Kiro AI Activity Insights" with 10 panels including
model usage distribution, active hours, conversation depth, feature
adoption (Steering/Spec), file type usage, and prompt/response trends
- Enriched "Kiro Code Metrics Dashboard" with DocGeneration, TestGeneration,
and Dev (Agentic) metric panels
- Fixed "Kiro Usage Dashboard" per-user table to sort by user_id
- Migration script for new tables
* fix(q-dev): use separate base path for logging S3 prefixes
Logging data lives under a different S3 prefix ("logging/") than user
report data ("user-report/"). Add LoggingBasePath option (defaults to
"logging") so logging prefixes are constructed correctly.
* fix(q-dev): auto-scan logging path without extra config
Kiro exports to two well-known S3 prefixes in the same bucket:
- user-report/AWSLogs/{accountId}/KiroLogs/ (CSV reports)
- logging/AWSLogs/{accountId}/KiroLogs/ (interaction logs)
When AccountId is set, automatically scan both paths. The "logging"
prefix is hardcoded since it's a standard Kiro export convention.
No additional configuration needed.
* fix(q-dev): update scope tooltip to mention logging data scanning
* fix(q-dev): fix scope ID routing and CSV/JSON file separation
Three fixes:
1. Use *scopeId (catch-all) route pattern instead of :scopeId so scope
IDs containing "/" (e.g. "034362076319/2026") work in URL paths
2. CSV extractor now filters for .csv files only, preventing it from
trying to parse .json.gz logging files as CSV
3. Frontend scope API calls now encodeURIComponent(scopeId) for safe
URL encoding
* fix(q-dev): resolve *scopeId route conflict with dispatcher pattern
The catch-all *scopeId route conflicts with *scopeId/latest-sync-state.
Follow Jenkins/Bitbucket pattern: use a single *scopeId route with a
GetScopeDispatcher that checks for /latest-sync-state suffix and
dispatches accordingly. All scope handlers now TrimLeft "/" from scopeId.
* fix(q-dev): use URL-safe scope ID format (underscore separator)
Scope IDs like "034362076319/2026" break URL routing because "/" is a
path separator. Change ID format to "034362076319_2026" (underscore)
when AccountId is set. The Prefix field still uses "/" for S3 path
matching. Revert to standard :scopeId routes since IDs are now safe.
Note: existing scopes need to be recreated after this change.
* fix(q-dev): use NoPKModel instead of Model in archived logging models
archived.Model only has ID+timestamps, missing RawDataOrigin fields
(_raw_data_params etc.) that common.NoPKModel includes. This caused
"Unknown column '_raw_data_params'" errors at runtime.
* fix(q-dev): fix GROUP BY in per-user table to merge display_name variants
Remove display_name from GROUP BY so same user_id with different
display_name values gets merged. Use MAX(display_name) in SELECT.
* fix(q-dev): normalize logging user IDs to match CSV short UUID format
Logging data uses "d-{directoryId}.{UUID}" format while CSV user-report
uses plain "{UUID}". Strip the "d-xxx." prefix so the same user maps to
one user_id across both data sources.
* fix(q-dev): normalize user IDs in CSV extractors and sort table DESC
Apply normalizeUserId to both createUserReportData and
createUserDataWithDisplayName so user_report CSV data also strips
the "d-{directoryId}." prefix. Change per-user table sort to
ORDER BY user_id DESC.
* style(q-dev): fix gofmt formatting in chat_log models
* perf(q-dev): parallelize logging S3 downloads and batch DB writes
Optimize logging extractor performance:
- 10 goroutine workers for parallel S3 file downloads
- Batch 50 files per DB transaction instead of 1-per-file
- sync.Map cache for display name resolution (avoid repeated IAM calls)
- Parse records in memory during download, write all at once
This should improve throughput from ~1.5 files/sec to ~15+ files/sec
for typical logging file sizes.
* fix(q-dev): check tx.Rollback error return to satisfy errcheck lint
* feat(q-dev): add per-user model usage table and models column
Add "Per-User Model Usage" table (panel 11) showing each user's
request count and avg prompt/response length per model_id. Also add
"Models Used" column to the Per-User Activity table.
* fix(q-dev): remove per-user model usage table, keep models column only
* feat(q-dev): add Kiro Executive Dashboard with cross-source analytics
New dashboard "Kiro Executive Dashboard" with 12 panels covering:
- KPIs: WAU, credits efficiency, acceptance rate, steering adoption
- Trends: weekly active users, new vs returning users
- Adoption funnel:
Chat→Inline→CodeFix→Review→DocGen→TestGen→Agentic→Steering→Spec
- Cost: credits pace vs projected monthly, idle power users
- Quality: acceptance rate trends, code review findings, test generation
- Efficiency: per-user productivity table with credits/line ratio
Correlates data across user_report (credits), user_data (code metrics),
and chat_log (interaction patterns) for holistic Kiro usage insights.
* fix(q-dev): fix pie charts to show per-row slices instead of single total
Set reduceOptions.values=true so Grafana treats each SQL result row as
a separate pie slice. Fixes Model Usage Distribution, File Type Usage,
Kiro Feature Adoption, and Active File Types pie charts.
* fix(q-dev): cast Hour to string for Active Hours bar chart x-axis
* fix(q-dev): fix pie chart single-slice and GROUP BY display_name issues
1. qdev_user_report Panel 4 (Subscription Tier Distribution): set
reduceOptions.values=true to show per-tier slices
2. qdev_user_data Panel 6 (User Interactions): remove display_name
from GROUP BY, use MAX(display_name) to merge same user
* fix(q-dev): prevent data inflation in user_report JOIN user_data
user_report has multiple rows per (user_id, date) due to client_type
(KIRO_IDE, KIRO_CLI), but user_data has only one row per (user_id, date).
A direct JOIN causes user_data metrics to be counted multiple times.
Fix: pre-aggregate user_report by (user_id, date) in a subquery before
joining, so the JOIN is always 1:1.
Affects: Credits Efficiency stat and User Productivity table.
---
backend/plugins/q_dev/api/s3_slice_api.go | 41 -
backend/plugins/q_dev/impl/impl.go | 20 +-
backend/plugins/q_dev/impl/impl_test.go | 4 +-
backend/plugins/q_dev/models/chat_log.go | 51 ++
backend/plugins/q_dev/models/completion_log.go | 43 +
...{register.go => 20260314_add_logging_tables.go} | 36 +-
.../models/migrationscripts/archived/chat_log.go | 50 ++
.../migrationscripts/archived/completion_log.go | 42 +
.../q_dev/models/migrationscripts/register.go | 1 +
backend/plugins/q_dev/models/s3_slice.go | 11 +-
backend/plugins/q_dev/tasks/s3_data_extractor.go | 14 +-
backend/plugins/q_dev/tasks/s3_file_collector.go | 6 +-
.../plugins/q_dev/tasks/s3_logging_extractor.go | 438 ++++++++++
.../src/plugins/register/q-dev/data-scope.tsx | 2 +-
grafana/dashboards/qdev_executive.json | 958 +++++++++++++++++++++
grafana/dashboards/qdev_logging.json | 800 +++++++++++++++++
grafana/dashboards/qdev_user_data.json | 389 ++++++++-
grafana/dashboards/qdev_user_report.json | 8 +-
18 files changed, 2836 insertions(+), 78 deletions(-)
diff --git a/backend/plugins/q_dev/api/s3_slice_api.go
b/backend/plugins/q_dev/api/s3_slice_api.go
index 737081302..158aa4ef7 100644
--- a/backend/plugins/q_dev/api/s3_slice_api.go
+++ b/backend/plugins/q_dev/api/s3_slice_api.go
@@ -60,62 +60,21 @@ func GetScopeList(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, er
}
// GetScope returns a single scope record
-// @Summary get a Q Developer scope
-// @Description get a Q Developer scope
-// @Tags plugins/q_dev
-// @Param connectionId path int true "connection ID"
-// @Param scopeId path string true "scope id"
-// @Param blueprints query bool false "include blueprint references"
-// @Success 200 {object} ScopeDetail
-// @Failure 400 {object} shared.ApiBody "Bad Request"
-// @Failure 500 {object} shared.ApiBody "Internal Error"
-// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [GET]
func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
return dsHelper.ScopeApi.GetScopeDetail(input)
}
// PatchScope updates a scope record
-// @Summary patch a Q Developer scope
-// @Description patch a Q Developer scope
-// @Tags plugins/q_dev
-// @Accept application/json
-// @Param connectionId path int true "connection ID"
-// @Param scopeId path string true "scope id"
-// @Param scope body models.QDevS3Slice true "json"
-// @Success 200 {object} models.QDevS3Slice
-// @Failure 400 {object} shared.ApiBody "Bad Request"
-// @Failure 500 {object} shared.ApiBody "Internal Error"
-// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [PATCH]
func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
return dsHelper.ScopeApi.Patch(input)
}
// DeleteScope removes a scope and optionally associated data.
-// @Summary delete a Q Developer scope
-// @Description delete Q Developer scope data
-// @Tags plugins/q_dev
-// @Param connectionId path int true "connection ID"
-// @Param scopeId path string true "scope id"
-// @Param delete_data_only query bool false "Only delete scope data"
-// @Success 200
-// @Failure 400 {object} shared.ApiBody "Bad Request"
-// @Failure 409 {object} srvhelper.DsRefs "References exist to this scope"
-// @Failure 500 {object} shared.ApiBody "Internal Error"
-// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [DELETE]
func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput,
errors.Error) {
return dsHelper.ScopeApi.Delete(input)
}
// GetScopeLatestSyncState returns scope sync state info
-// @Summary latest sync state for a Q Developer scope
-// @Description get latest sync state for a Q Developer scope
-// @Tags plugins/q_dev
-// @Param connectionId path int true "connection ID"
-// @Param scopeId path string true "scope id"
-// @Success 200
-// @Failure 400 {object} shared.ApiBody "Bad Request"
-// @Failure 500 {object} shared.ApiBody "Internal Error"
-// @Router
/plugins/q_dev/connections/{connectionId}/scopes/{scopeId}/latest-sync-state
[GET]
func GetScopeLatestSyncState(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors.Error) {
return dsHelper.ScopeApi.GetScopeLatestSyncState(input)
}
diff --git a/backend/plugins/q_dev/impl/impl.go
b/backend/plugins/q_dev/impl/impl.go
index e38fe7ad7..3c6e1ed16 100644
--- a/backend/plugins/q_dev/impl/impl.go
+++ b/backend/plugins/q_dev/impl/impl.go
@@ -58,6 +58,8 @@ func (p QDev) GetTablesInfo() []dal.Tabler {
&models.QDevS3FileMeta{},
&models.QDevS3Slice{},
&models.QDevUserReport{},
+ &models.QDevChatLog{},
+ &models.QDevCompletionLog{},
}
}
@@ -85,6 +87,7 @@ func (p QDev) SubTaskMetas() []plugin.SubTaskMeta {
return []plugin.SubTaskMeta{
tasks.CollectQDevS3FilesMeta,
tasks.ExtractQDevS3DataMeta,
+ tasks.ExtractQDevLoggingDataMeta,
}
}
@@ -127,10 +130,21 @@ func (p QDev) PrepareTaskData(taskCtx plugin.TaskContext,
options map[string]int
if op.Month != nil {
timePart = fmt.Sprintf("%04d/%02d", op.Year, *op.Month)
}
- base := fmt.Sprintf("%s/AWSLogs/%s/KiroLogs", op.BasePath,
op.AccountId)
+ // Kiro exports data to two well-known S3 prefixes:
+ // {basePath}/AWSLogs/{accountId}/KiroLogs/ — user report
CSVs
+ // logging/AWSLogs/{accountId}/KiroLogs/ — interaction
logs (JSON.gz)
+ // When basePath is empty, default to "user-report" for CSV
data.
+ reportBase := op.BasePath
+ if reportBase == "" {
+ reportBase = "user-report"
+ }
+ csvBase := fmt.Sprintf("%s/AWSLogs/%s/KiroLogs", reportBase,
op.AccountId)
+ logBase := fmt.Sprintf("logging/AWSLogs/%s/KiroLogs",
op.AccountId)
s3Prefixes = []string{
- fmt.Sprintf("%s/by_user_analytic/%s/%s", base, region,
timePart),
- fmt.Sprintf("%s/user_report/%s/%s", base, region,
timePart),
+ fmt.Sprintf("%s/by_user_analytic/%s/%s", csvBase,
region, timePart),
+ fmt.Sprintf("%s/user_report/%s/%s", csvBase, region,
timePart),
+ fmt.Sprintf("%s/GenerateAssistantResponse/%s/%s",
logBase, region, timePart),
+ fmt.Sprintf("%s/GenerateCompletions/%s/%s", logBase,
region, timePart),
}
} else {
// Legacy scope: use S3Prefix directly
diff --git a/backend/plugins/q_dev/impl/impl_test.go
b/backend/plugins/q_dev/impl/impl_test.go
index e61b53251..7153617ab 100644
--- a/backend/plugins/q_dev/impl/impl_test.go
+++ b/backend/plugins/q_dev/impl/impl_test.go
@@ -34,11 +34,11 @@ func TestQDev_BasicPluginMethods(t *testing.T) {
// Test table info
tables := plugin.GetTablesInfo()
- assert.Len(t, tables, 5)
+ assert.Len(t, tables, 7)
// Test subtask metas
subtasks := plugin.SubTaskMetas()
- assert.Len(t, subtasks, 2)
+ assert.Len(t, subtasks, 3)
// Test API resources
apiResources := plugin.ApiResources()
diff --git a/backend/plugins/q_dev/models/chat_log.go
b/backend/plugins/q_dev/models/chat_log.go
new file mode 100644
index 000000000..06679c515
--- /dev/null
+++ b/backend/plugins/q_dev/models/chat_log.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// QDevChatLog stores parsed data from GenerateAssistantResponse logging events
+type QDevChatLog struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"
json:"scopeId"`
+ RequestId string `gorm:"primaryKey;type:varchar(255)"
json:"requestId"`
+ UserId string `gorm:"index;type:varchar(255)"
json:"userId"`
+ DisplayName string `gorm:"type:varchar(255)"
json:"displayName"`
+ Timestamp time.Time `gorm:"index" json:"timestamp"`
+ ChatTriggerType string `gorm:"type:varchar(50)"
json:"chatTriggerType"`
+ HasCustomization bool `json:"hasCustomization"`
+ ConversationId string `gorm:"type:varchar(255)"
json:"conversationId"`
+ UtteranceId string `gorm:"type:varchar(255)"
json:"utteranceId"`
+ ModelId string `gorm:"type:varchar(100)" json:"modelId"`
+ PromptLength int `json:"promptLength"`
+ ResponseLength int `json:"responseLength"`
+ OpenFileCount int `json:"openFileCount"`
+ ActiveFileName string `gorm:"type:varchar(512)"
json:"activeFileName"`
+ ActiveFileExtension string `gorm:"type:varchar(50)"
json:"activeFileExtension"`
+ HasSteering bool `json:"hasSteering"`
+ IsSpecMode bool `json:"isSpecMode"`
+}
+
+func (QDevChatLog) TableName() string {
+ return "_tool_q_dev_chat_log"
+}
diff --git a/backend/plugins/q_dev/models/completion_log.go
b/backend/plugins/q_dev/models/completion_log.go
new file mode 100644
index 000000000..0d0e0404c
--- /dev/null
+++ b/backend/plugins/q_dev/models/completion_log.go
@@ -0,0 +1,43 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+ "time"
+
+ "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// QDevCompletionLog stores parsed data from GenerateCompletions logging events
+type QDevCompletionLog struct {
+ common.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"
json:"scopeId"`
+ RequestId string `gorm:"primaryKey;type:varchar(255)"
json:"requestId"`
+ UserId string `gorm:"index;type:varchar(255)"
json:"userId"`
+ DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
+ Timestamp time.Time `gorm:"index" json:"timestamp"`
+ FileName string `gorm:"type:varchar(512)" json:"fileName"`
+ FileExtension string `gorm:"type:varchar(50)"
json:"fileExtension"`
+ HasCustomization bool `json:"hasCustomization"`
+ CompletionsCount int `json:"completionsCount"`
+}
+
+func (QDevCompletionLog) TableName() string {
+ return "_tool_q_dev_completion_log"
+}
diff --git a/backend/plugins/q_dev/models/migrationscripts/register.go
b/backend/plugins/q_dev/models/migrationscripts/20260314_add_logging_tables.go
similarity index 54%
copy from backend/plugins/q_dev/models/migrationscripts/register.go
copy to
backend/plugins/q_dev/models/migrationscripts/20260314_add_logging_tables.go
index 9c68ae8f8..cbd5943ec 100644
--- a/backend/plugins/q_dev/models/migrationscripts/register.go
+++
b/backend/plugins/q_dev/models/migrationscripts/20260314_add_logging_tables.go
@@ -18,22 +18,26 @@ limitations under the License.
package migrationscripts
import (
- "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+
"github.com/apache/incubator-devlake/plugins/q_dev/models/migrationscripts/archived"
)
-// All return all migration scripts
-func All() []plugin.MigrationScript {
- return []plugin.MigrationScript{
- new(initTables),
- new(modifyFileMetaTable),
- new(addDisplayNameFields),
- new(addMissingMetrics),
- new(addS3SliceTable),
- new(addScopeConfigIdToS3Slice),
- new(addScopeIdFields),
- new(addUserReportTable),
- new(addAccountIdToS3Slice),
- new(fixDedupUserTables),
- new(resetS3FileMetaProcessed),
- }
+type addLoggingTables struct{}
+
+func (*addLoggingTables) Up(basicRes context.BasicRes) errors.Error {
+ return migrationhelper.AutoMigrateTables(
+ basicRes,
+ &archived.QDevChatLog{},
+ &archived.QDevCompletionLog{},
+ )
+}
+
+func (*addLoggingTables) Version() uint64 {
+ return 20260314000001
+}
+
+func (*addLoggingTables) Name() string {
+ return "Add chat_log and completion_log tables for Kiro logging data"
}
diff --git a/backend/plugins/q_dev/models/migrationscripts/archived/chat_log.go
b/backend/plugins/q_dev/models/migrationscripts/archived/chat_log.go
new file mode 100644
index 000000000..8278f52ff
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/chat_log.go
@@ -0,0 +1,50 @@
+/*
+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 archived
+
+import (
+ "time"
+
+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type QDevChatLog struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"
json:"scopeId"`
+ RequestId string `gorm:"primaryKey;type:varchar(255)"
json:"requestId"`
+ UserId string `gorm:"index;type:varchar(255)"
json:"userId"`
+ DisplayName string `gorm:"type:varchar(255)"
json:"displayName"`
+ Timestamp time.Time `gorm:"index" json:"timestamp"`
+ ChatTriggerType string `gorm:"type:varchar(50)"
json:"chatTriggerType"`
+ HasCustomization bool `json:"hasCustomization"`
+ ConversationId string `gorm:"type:varchar(255)"
json:"conversationId"`
+ UtteranceId string `gorm:"type:varchar(255)"
json:"utteranceId"`
+ ModelId string `gorm:"type:varchar(100)" json:"modelId"`
+ PromptLength int `json:"promptLength"`
+ ResponseLength int `json:"responseLength"`
+ OpenFileCount int `json:"openFileCount"`
+ ActiveFileName string `gorm:"type:varchar(512)"
json:"activeFileName"`
+ ActiveFileExtension string `gorm:"type:varchar(50)"
json:"activeFileExtension"`
+ HasSteering bool `json:"hasSteering"`
+ IsSpecMode bool `json:"isSpecMode"`
+}
+
+func (QDevChatLog) TableName() string {
+ return "_tool_q_dev_chat_log"
+}
diff --git
a/backend/plugins/q_dev/models/migrationscripts/archived/completion_log.go
b/backend/plugins/q_dev/models/migrationscripts/archived/completion_log.go
new file mode 100644
index 000000000..035c13ef2
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/completion_log.go
@@ -0,0 +1,42 @@
+/*
+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 archived
+
+import (
+ "time"
+
+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+type QDevCompletionLog struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey"`
+ ScopeId string `gorm:"primaryKey;type:varchar(255)"
json:"scopeId"`
+ RequestId string `gorm:"primaryKey;type:varchar(255)"
json:"requestId"`
+ UserId string `gorm:"index;type:varchar(255)"
json:"userId"`
+ DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
+ Timestamp time.Time `gorm:"index" json:"timestamp"`
+ FileName string `gorm:"type:varchar(512)" json:"fileName"`
+ FileExtension string `gorm:"type:varchar(50)"
json:"fileExtension"`
+ HasCustomization bool `json:"hasCustomization"`
+ CompletionsCount int `json:"completionsCount"`
+}
+
+func (QDevCompletionLog) TableName() string {
+ return "_tool_q_dev_completion_log"
+}
diff --git a/backend/plugins/q_dev/models/migrationscripts/register.go
b/backend/plugins/q_dev/models/migrationscripts/register.go
index 9c68ae8f8..5480d5eaf 100644
--- a/backend/plugins/q_dev/models/migrationscripts/register.go
+++ b/backend/plugins/q_dev/models/migrationscripts/register.go
@@ -35,5 +35,6 @@ func All() []plugin.MigrationScript {
new(addAccountIdToS3Slice),
new(fixDedupUserTables),
new(resetS3FileMetaProcessed),
+ new(addLoggingTables),
}
}
diff --git a/backend/plugins/q_dev/models/s3_slice.go
b/backend/plugins/q_dev/models/s3_slice.go
index e918258a9..19ecd6920 100644
--- a/backend/plugins/q_dev/models/s3_slice.go
+++ b/backend/plugins/q_dev/models/s3_slice.go
@@ -99,7 +99,16 @@ func (s *QDevS3Slice) normalize(strict bool) error {
}
if s.Id == "" {
- s.Id = s.Prefix
+ if s.AccountId != "" {
+ // Use URL-safe ID: account_year or account_year_month
+ if s.Month != nil {
+ s.Id = fmt.Sprintf("%s_%04d_%02d", s.AccountId,
s.Year, *s.Month)
+ } else {
+ s.Id = fmt.Sprintf("%s_%04d", s.AccountId,
s.Year)
+ }
+ } else {
+ s.Id = s.Prefix
+ }
}
if s.AccountId != "" {
diff --git a/backend/plugins/q_dev/tasks/s3_data_extractor.go
b/backend/plugins/q_dev/tasks/s3_data_extractor.go
index 1cf2a9f2e..da29bca07 100644
--- a/backend/plugins/q_dev/tasks/s3_data_extractor.go
+++ b/backend/plugins/q_dev/tasks/s3_data_extractor.go
@@ -40,10 +40,11 @@ func ExtractQDevS3Data(taskCtx plugin.SubTaskContext)
errors.Error {
data := taskCtx.GetData().(*QDevTaskData)
db := taskCtx.GetDal()
- // 查询未处理的文件元数据
+ // 查询未处理的CSV文件元数据(排除.json.gz日志文件)
cursor, err := db.Cursor(
dal.From(&models.QDevS3FileMeta{}),
- dal.Where("connection_id = ? AND processed = ?",
data.Options.ConnectionId, false),
+ dal.Where("connection_id = ? AND processed = ? AND file_name
LIKE ?",
+ data.Options.ConnectionId, false, "%.csv"),
)
if err != nil {
return errors.Default.Wrap(err, "failed to get file metadata
cursor")
@@ -202,8 +203,8 @@ func createUserReportData(logger interface {
}
}
- // UserId
- report.UserId = getStringField(fieldMap, "UserId")
+ // UserId (normalize to strip "d-{directoryId}." prefix if present)
+ report.UserId = normalizeUserId(getStringField(fieldMap, "UserId"))
if report.UserId == "" {
return nil, errors.Default.New("UserId not found in CSV record")
}
@@ -303,11 +304,12 @@ func createUserDataWithDisplayName(logger interface {
var err error
var ok bool
- // 设置UserId
- userData.UserId, ok = fieldMap["UserId"]
+ // 设置UserId (normalize to strip "d-{directoryId}." prefix if present)
+ rawUserId, ok := fieldMap["UserId"]
if !ok {
return nil, errors.Default.New("UserId not found in CSV record")
}
+ userData.UserId = normalizeUserId(rawUserId)
// 设置DisplayName (new functionality)
userData.DisplayName = resolveDisplayName(logger, userData.UserId,
identityClient)
diff --git a/backend/plugins/q_dev/tasks/s3_file_collector.go
b/backend/plugins/q_dev/tasks/s3_file_collector.go
index 9d40919ae..1ab4f8f0a 100644
--- a/backend/plugins/q_dev/tasks/s3_file_collector.go
+++ b/backend/plugins/q_dev/tasks/s3_file_collector.go
@@ -59,9 +59,9 @@ func CollectQDevS3Files(taskCtx plugin.SubTaskContext)
errors.Error {
}
for _, object := range result.Contents {
- // Only process CSV files
- if !strings.HasSuffix(*object.Key, ".csv") {
- taskCtx.GetLogger().Debug("Skipping
non-CSV file: %s", *object.Key)
+ // Only process CSV and JSON.gz files
+ if !strings.HasSuffix(*object.Key, ".csv") &&
!strings.HasSuffix(*object.Key, ".json.gz") {
+ taskCtx.GetLogger().Debug("Skipping
unsupported file: %s", *object.Key)
continue
}
diff --git a/backend/plugins/q_dev/tasks/s3_logging_extractor.go
b/backend/plugins/q_dev/tasks/s3_logging_extractor.go
new file mode 100644
index 000000000..df55a663b
--- /dev/null
+++ b/backend/plugins/q_dev/tasks/s3_logging_extractor.go
@@ -0,0 +1,438 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "compress/gzip"
+ "encoding/json"
+ "path/filepath"
+ "strings"
+ "sync"
+ "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"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractQDevLoggingData
+
+const (
+ loggingBatchSize = 50 // number of files to process per DB transaction
+ s3DownloadWorkers = 10 // parallel S3 download goroutines
+ s3DownloadChanSize = 20 // buffered channel size for download results
+)
+
+// downloadResult holds the parsed records from one S3 file
+type downloadResult struct {
+ FileMeta *models.QDevS3FileMeta
+ ChatLogs []*models.QDevChatLog
+ CompLogs []*models.QDevCompletionLog
+ Err error
+}
+
+// ExtractQDevLoggingData extracts logging data from S3 JSON.gz files
+func ExtractQDevLoggingData(taskCtx plugin.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*QDevTaskData)
+ db := taskCtx.GetDal()
+
+ cursor, err := db.Cursor(
+ dal.From(&models.QDevS3FileMeta{}),
+ dal.Where("connection_id = ? AND processed = ? AND file_name
LIKE ?",
+ data.Options.ConnectionId, false, "%.json.gz"),
+ )
+ if err != nil {
+ return errors.Default.Wrap(err, "failed to get logging file
metadata cursor")
+ }
+ defer cursor.Close()
+
+ // Collect all file metas first
+ var fileMetas []*models.QDevS3FileMeta
+ for cursor.Next() {
+ fm := &models.QDevS3FileMeta{}
+ if err := db.Fetch(cursor, fm); err != nil {
+ return errors.Default.Wrap(err, "failed to fetch file
metadata")
+ }
+ fileMetas = append(fileMetas, fm)
+ }
+
+ if len(fileMetas) == 0 {
+ return nil
+ }
+
+ taskCtx.SetProgress(0, len(fileMetas))
+ taskCtx.GetLogger().Info("Processing %d logging files with %d workers",
len(fileMetas), s3DownloadWorkers)
+
+ // Display name cache to avoid repeated IAM calls
+ displayNameCache := &sync.Map{}
+
+ // Process in batches
+ for batchStart := 0; batchStart < len(fileMetas); batchStart +=
loggingBatchSize {
+ batchEnd := batchStart + loggingBatchSize
+ if batchEnd > len(fileMetas) {
+ batchEnd = len(fileMetas)
+ }
+ batch := fileMetas[batchStart:batchEnd]
+
+ // Parallel download and parse
+ results := parallelDownloadAndParse(taskCtx, data, batch,
displayNameCache)
+
+ // Check for download errors
+ for _, r := range results {
+ if r.Err != nil {
+ return
errors.Default.Wrap(errors.Convert(r.Err),
+ "failed to download/parse
"+r.FileMeta.FileName)
+ }
+ }
+
+ // Batch write to DB in a single transaction
+ tx := db.Begin()
+ var txErr errors.Error
+ for _, r := range results {
+ for _, chatLog := range r.ChatLogs {
+ if txErr = tx.CreateOrUpdate(chatLog); txErr !=
nil {
+ break
+ }
+ }
+ if txErr != nil {
+ break
+ }
+ for _, compLog := range r.CompLogs {
+ if txErr = tx.CreateOrUpdate(compLog); txErr !=
nil {
+ break
+ }
+ }
+ if txErr != nil {
+ break
+ }
+ r.FileMeta.Processed = true
+ now := time.Now()
+ r.FileMeta.ProcessedTime = &now
+ if txErr = tx.Update(r.FileMeta); txErr != nil {
+ break
+ }
+ }
+ if txErr != nil {
+ if rbErr := tx.Rollback(); rbErr != nil {
+ taskCtx.GetLogger().Error(rbErr, "failed to
rollback transaction")
+ }
+ return errors.Default.Wrap(txErr, "failed to write
logging batch")
+ }
+ if err := tx.Commit(); err != nil {
+ return errors.Default.Wrap(err, "failed to commit
batch")
+ }
+
+ taskCtx.IncProgress(len(batch))
+ }
+
+ return nil
+}
+
+// parallelDownloadAndParse downloads and parses S3 files concurrently
+func parallelDownloadAndParse(
+ taskCtx plugin.SubTaskContext,
+ data *QDevTaskData,
+ fileMetas []*models.QDevS3FileMeta,
+ displayNameCache *sync.Map,
+) []downloadResult {
+ results := make([]downloadResult, len(fileMetas))
+ jobs := make(chan int, s3DownloadChanSize)
+ var wg sync.WaitGroup
+
+ // Start workers
+ for w := 0; w < s3DownloadWorkers; w++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for idx := range jobs {
+ fm := fileMetas[idx]
+ result := downloadAndParseFile(taskCtx, data,
fm, displayNameCache)
+ results[idx] = result
+ }
+ }()
+ }
+
+ // Send jobs
+ for i := range fileMetas {
+ jobs <- i
+ }
+ close(jobs)
+ wg.Wait()
+
+ return results
+}
+
+// downloadAndParseFile downloads one S3 file and parses it into model records
+func downloadAndParseFile(
+ taskCtx plugin.SubTaskContext,
+ data *QDevTaskData,
+ fileMeta *models.QDevS3FileMeta,
+ displayNameCache *sync.Map,
+) downloadResult {
+ result := downloadResult{FileMeta: fileMeta}
+
+ getResult, err := data.S3Client.S3.GetObject(&s3.GetObjectInput{
+ Bucket: aws.String(data.S3Client.Bucket),
+ Key: aws.String(fileMeta.S3Path),
+ })
+ if err != nil {
+ result.Err = err
+ return result
+ }
+ defer getResult.Body.Close()
+
+ gzReader, err := gzip.NewReader(getResult.Body)
+ if err != nil {
+ result.Err = err
+ return result
+ }
+ defer gzReader.Close()
+
+ var logFile loggingFile
+ if err := json.NewDecoder(gzReader).Decode(&logFile); err != nil {
+ result.Err = err
+ return result
+ }
+
+ isChatLog := strings.Contains(fileMeta.S3Path,
"GenerateAssistantResponse")
+
+ for _, rawRecord := range logFile.Records {
+ if isChatLog {
+ chatLog, err := parseChatRecord(rawRecord, fileMeta,
data.IdentityClient, displayNameCache)
+ if err != nil {
+ result.Err = err
+ return result
+ }
+ if chatLog != nil {
+ result.ChatLogs = append(result.ChatLogs,
chatLog)
+ }
+ } else {
+ compLog, err := parseCompletionRecord(rawRecord,
fileMeta, data.IdentityClient, displayNameCache)
+ if err != nil {
+ result.Err = err
+ return result
+ }
+ if compLog != nil {
+ result.CompLogs = append(result.CompLogs,
compLog)
+ }
+ }
+ }
+
+ return result
+}
+
+// cachedResolveDisplayName resolves display name with caching
+func cachedResolveDisplayName(userId string, identityClient
UserDisplayNameResolver, cache *sync.Map) string {
+ if v, ok := cache.Load(userId); ok {
+ return v.(string)
+ }
+ if identityClient == nil {
+ cache.Store(userId, userId)
+ return userId
+ }
+ displayName, err := identityClient.ResolveUserDisplayName(userId)
+ if err != nil || displayName == "" {
+ cache.Store(userId, userId)
+ return userId
+ }
+ cache.Store(userId, displayName)
+ return displayName
+}
+
+// JSON structures for logging data
+
+type loggingFile struct {
+ Records []json.RawMessage `json:"records"`
+}
+
+type chatLogRecord struct {
+ Request *chatLogRequest `json:"generateAssistantResponseEventRequest"`
+ Response *chatLogResponse
`json:"generateAssistantResponseEventResponse"`
+}
+
+type chatLogRequest struct {
+ UserID string `json:"userId"`
+ Timestamp string `json:"timeStamp"`
+ ChatTriggerType string `json:"chatTriggerType"`
+ CustomizationArn *string `json:"customizationArn"`
+ ModelID string `json:"modelId"`
+ Prompt string `json:"prompt"`
+}
+
+type chatLogResponse struct {
+ RequestID string `json:"requestId"`
+ AssistantResponse string `json:"assistantResponse"`
+ MessageMetadata struct {
+ ConversationID *string `json:"conversationId"`
+ UtteranceID *string `json:"utteranceId"`
+ } `json:"messageMetadata"`
+}
+
+type completionLogRecord struct {
+ Request *completionLogRequest `json:"generateCompletionsEventRequest"`
+ Response *completionLogResponse
`json:"generateCompletionsEventResponse"`
+}
+
+type completionLogRequest struct {
+ UserID string `json:"userId"`
+ Timestamp string `json:"timeStamp"`
+ FileName string `json:"fileName"`
+ CustomizationArn *string `json:"customizationArn"`
+}
+
+type completionLogResponse struct {
+ RequestID string `json:"requestId"`
+ Completions []json.RawMessage `json:"completions"`
+}
+
+func parseChatRecord(raw json.RawMessage, fileMeta *models.QDevS3FileMeta,
identityClient UserDisplayNameResolver, cache *sync.Map) (*models.QDevChatLog,
error) {
+ var record chatLogRecord
+ if err := json.Unmarshal(raw, &record); err != nil {
+ return nil, err
+ }
+
+ if record.Request == nil || record.Response == nil {
+ return nil, nil
+ }
+
+ ts, err := time.Parse(time.RFC3339Nano, record.Request.Timestamp)
+ if err != nil {
+ ts = time.Now()
+ }
+
+ userId := normalizeUserId(record.Request.UserID)
+ chatLog := &models.QDevChatLog{
+ ConnectionId: fileMeta.ConnectionId,
+ ScopeId: fileMeta.ScopeId,
+ RequestId: record.Response.RequestID,
+ UserId: userId,
+ DisplayName: cachedResolveDisplayName(userId,
identityClient, cache),
+ Timestamp: ts,
+ ChatTriggerType: record.Request.ChatTriggerType,
+ HasCustomization: record.Request.CustomizationArn != nil &&
*record.Request.CustomizationArn != "",
+ ModelId: record.Request.ModelID,
+ PromptLength: len(record.Request.Prompt),
+ ResponseLength: len(record.Response.AssistantResponse),
+ }
+
+ // Parse structured info from prompt
+ prompt := record.Request.Prompt
+ chatLog.OpenFileCount = countOpenFiles(prompt)
+ chatLog.ActiveFileName, chatLog.ActiveFileExtension =
parseActiveFile(prompt)
+ chatLog.HasSteering = strings.Contains(prompt, ".kiro/steering")
+ chatLog.IsSpecMode = strings.Contains(prompt, "implicit-rules")
+
+ if record.Response.MessageMetadata.ConversationID != nil {
+ chatLog.ConversationId =
*record.Response.MessageMetadata.ConversationID
+ }
+ if record.Response.MessageMetadata.UtteranceID != nil {
+ chatLog.UtteranceId =
*record.Response.MessageMetadata.UtteranceID
+ }
+
+ return chatLog, nil
+}
+
+// countOpenFiles counts <file name="..."> tags within <OPEN-EDITOR-FILES>
block
+func countOpenFiles(prompt string) int {
+ start := strings.Index(prompt, "<OPEN-EDITOR-FILES>")
+ if start == -1 {
+ return 0
+ }
+ end := strings.Index(prompt, "</OPEN-EDITOR-FILES>")
+ if end == -1 {
+ return 0
+ }
+ block := prompt[start:end]
+ return strings.Count(block, "<file name=")
+}
+
+// parseActiveFile extracts the active file name and extension from prompt
+func parseActiveFile(prompt string) (string, string) {
+ start := strings.Index(prompt, "<ACTIVE-EDITOR-FILE>")
+ if start == -1 {
+ return "", ""
+ }
+ end := strings.Index(prompt[start:], "</ACTIVE-EDITOR-FILE>")
+ if end == -1 {
+ return "", ""
+ }
+ block := prompt[start : start+end]
+ nameStart := strings.Index(block, "name=\"")
+ if nameStart == -1 {
+ return "", ""
+ }
+ nameStart += len("name=\"")
+ nameEnd := strings.Index(block[nameStart:], "\"")
+ if nameEnd == -1 {
+ return "", ""
+ }
+ fileName := block[nameStart : nameStart+nameEnd]
+ ext := filepath.Ext(fileName)
+ return fileName, ext
+}
+
+func parseCompletionRecord(raw json.RawMessage, fileMeta
*models.QDevS3FileMeta, identityClient UserDisplayNameResolver, cache
*sync.Map) (*models.QDevCompletionLog, error) {
+ var record completionLogRecord
+ if err := json.Unmarshal(raw, &record); err != nil {
+ return nil, err
+ }
+
+ if record.Request == nil || record.Response == nil {
+ return nil, nil
+ }
+
+ ts, err := time.Parse(time.RFC3339Nano, record.Request.Timestamp)
+ if err != nil {
+ ts = time.Now()
+ }
+
+ userId := normalizeUserId(record.Request.UserID)
+ return &models.QDevCompletionLog{
+ ConnectionId: fileMeta.ConnectionId,
+ ScopeId: fileMeta.ScopeId,
+ RequestId: record.Response.RequestID,
+ UserId: userId,
+ DisplayName: cachedResolveDisplayName(userId,
identityClient, cache),
+ Timestamp: ts,
+ FileName: record.Request.FileName,
+ FileExtension: filepath.Ext(record.Request.FileName),
+ HasCustomization: record.Request.CustomizationArn != nil &&
*record.Request.CustomizationArn != "",
+ CompletionsCount: len(record.Response.Completions),
+ }, nil
+}
+
+// normalizeUserId strips the "d-{directoryId}." prefix from Identity Center
user IDs
+// so that logging user IDs match the short UUID format used in user-report
CSVs.
+func normalizeUserId(userId string) string {
+ if idx := strings.LastIndex(userId, "."); idx != -1 &&
strings.HasPrefix(userId, "d-") {
+ return userId[idx+1:]
+ }
+ return userId
+}
+
+var ExtractQDevLoggingDataMeta = plugin.SubTaskMeta{
+ Name: "extractQDevLoggingData",
+ EntryPoint: ExtractQDevLoggingData,
+ EnabledByDefault: true,
+ Description: "Extract logging data from S3 JSON.gz files (chat and
completion events)",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CROSS},
+ Dependencies: []*plugin.SubTaskMeta{&CollectQDevS3FilesMeta},
+}
diff --git a/config-ui/src/plugins/register/q-dev/data-scope.tsx
b/config-ui/src/plugins/register/q-dev/data-scope.tsx
index c5eff68db..657d0bdb8 100644
--- a/config-ui/src/plugins/register/q-dev/data-scope.tsx
+++ b/config-ui/src/plugins/register/q-dev/data-scope.tsx
@@ -326,7 +326,7 @@ export const QDevDataScope = ({
const timePart = meta.month ?
`${meta.year}/${ensureLeadingZero(meta.month)}` : `${meta.year}`;
return (
<Tooltip
- title={`Scans both by_user_analytic and user_report under
AWSLogs/${meta.accountId}/KiroLogs/…/${timePart}`}
+ title={`Scans user-report (by_user_analytic + user_report) and
logging (chat + completions) under
AWSLogs/${meta.accountId}/KiroLogs/…/${timePart}`}
>
<Typography.Text code>
{meta.basePath}/…/{meta.accountId}/…/{timePart}
diff --git a/grafana/dashboards/qdev_executive.json
b/grafana/dashboards/qdev_executive.json
new file mode 100644
index 000000000..c6e2524d7
--- /dev/null
+++ b/grafana/dashboards/qdev_executive.json
@@ -0,0 +1,958 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": "mysql",
+ "description": "Distinct users with chat activity in the last 7 days",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "sum"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT COUNT(DISTINCT user_id) as 'WAU'\nFROM
lake._tool_q_dev_chat_log\nWHERE timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
+ "refId": "A"
+ }
+ ],
+ "title": "Weekly Active Users",
+ "type": "stat"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Average credits spent per accepted line of code",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 6,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "sum"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT ROUND(SUM(r.credits_used) /
NULLIF(SUM(d.total_accepted), 0), 2) as 'Credits per Accepted Line'\nFROM (\n
SELECT user_id, date, SUM(credits_used) as credits_used\n FROM
lake._tool_q_dev_user_report\n WHERE $__timeFilter(date)\n GROUP BY user_id,
date\n) r\nJOIN (\n SELECT user_id, date,\n (inline_ai_code_lines +
chat_ai_code_lines + code_fix_accepted_lines + dev_accepted_lines) as
total_accepted\n FROM lake._tool_q_dev_user_data\n WHERE $__timeFilter [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Credits Efficiency",
+ "type": "stat"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Percentage of inline suggestions accepted",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 12,
+ "y": 0
+ },
+ "id": 3,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "sum"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT ROUND(SUM(inline_acceptance_count) /
NULLIF(SUM(inline_suggestions_count), 0) * 100, 1) as 'Acceptance %'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)",
+ "refId": "A"
+ }
+ ],
+ "title": "Inline Acceptance Rate",
+ "type": "stat"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Percentage of users using steering rules",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 6,
+ "x": 18,
+ "y": 0
+ },
+ "id": 4,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "sum"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT CONCAT(ROUND(COUNT(DISTINCT CASE WHEN has_steering
= 1 THEN user_id END) / NULLIF(COUNT(DISTINCT user_id), 0) * 100, 0), '%') as
'Users with Steering'\nFROM lake._tool_q_dev_chat_log\nWHERE
$__timeFilter(timestamp)",
+ "refId": "A"
+ }
+ ],
+ "title": "Steering Adoption",
+ "type": "stat"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Weekly active user count over time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 6
+ },
+ "id": 5,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n STR_TO_DATE(CONCAT(YEARWEEK(timestamp, 1), '
Monday'), '%X%V %W') as time,\n COUNT(DISTINCT user_id) as 'Active
Users'\nFROM lake._tool_q_dev_chat_log\nWHERE $__timeFilter(timestamp)\nGROUP
BY YEARWEEK(timestamp, 1)\nORDER BY time",
+ "refId": "A"
+ }
+ ],
+ "title": "Weekly Active Users Trend",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "New vs returning users by week",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 6
+ },
+ "id": 6,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n week as time,\n SUM(CASE WHEN is_new = 1 THEN
1 ELSE 0 END) as 'New Users',\n SUM(CASE WHEN is_new = 0 THEN 1 ELSE 0 END) as
'Returning Users'\nFROM (\n SELECT\n u.user_id,\n
STR_TO_DATE(CONCAT(YEARWEEK(u.timestamp, 1), ' Monday'), '%X%V %W') as week,\n
CASE WHEN STR_TO_DATE(CONCAT(YEARWEEK(u.timestamp, 1), ' Monday'), '%X%V %W')
= STR_TO_DATE(CONCAT(YEARWEEK(f.first_seen, 1), ' Monday'), '%X%V %W') THEN 1
ELSE 0 END as is_new\n FROM lake._tool [...]
+ "refId": "A"
+ }
+ ],
+ "title": "New vs Returning Users (Weekly)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Number of users who used each feature",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.8,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 14
+ },
+ "id": 7,
+ "options": {
+ "barRadius": 0.1,
+ "barWidth": 0.8,
+ "fullHighlight": false,
+ "groupWidth": 0.7,
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": false
+ },
+ "orientation": "horizontal",
+ "showValue": "auto",
+ "stacking": "none",
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ },
+ "xTickLabelRotation": 0
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n 'Chat' as Feature, COUNT(DISTINCT CASE WHEN
chat_messages_sent > 0 THEN user_id END) as Users FROM
lake._tool_q_dev_user_data WHERE $__timeFilter(date)\nUNION ALL SELECT 'Inline
Suggestions', COUNT(DISTINCT CASE WHEN inline_suggestions_count > 0 THEN
user_id END) FROM lake._tool_q_dev_user_data WHERE $__timeFilter(date)\nUNION
ALL SELECT 'Code Fix', COUNT(DISTINCT CASE WHEN code_fix_generation_event_count
> 0 THEN user_id END) FROM lake._tool_q_dev_user_dat [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Feature Adoption Funnel",
+ "type": "barchart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Cumulative credits this month vs projected total",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 14
+ },
+ "id": 8,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n SUM(SUM(credits_used)) OVER
(ORDER BY date) as 'Cumulative Credits',\n (SELECT SUM(credits_used) /
COUNT(DISTINCT date) * DAY(LAST_DAY(CURDATE()))\n FROM
lake._tool_q_dev_user_report\n WHERE YEAR(date) = YEAR(CURDATE()) AND
MONTH(date) = MONTH(CURDATE())) as 'Projected Monthly'\nFROM
lake._tool_q_dev_user_report\nWHERE YEAR(date) = YEAR(CURDATE()) AND
MONTH(date) = MONTH(CURDATE())\nGROUP BY date\nORDER BY date",
+ "refId": "A"
+ }
+ ],
+ "title": "Credits Pace vs Projected (This Month)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Acceptance rates for inline suggestions, code fix, and
inline chat over time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "percentunit"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 22
+ },
+ "id": 9,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n SUM(inline_acceptance_count) /
NULLIF(SUM(inline_suggestions_count), 0) as 'Inline Suggestions',\n
SUM(code_fix_acceptance_event_count) /
NULLIF(SUM(code_fix_generation_event_count), 0) as 'Code Fix',\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"
+ }
+ ],
+ "title": "Acceptance Rate Trends",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Code review findings and test generation metrics over
time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 22
+ },
+ "id": 10,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n
SUM(code_review_findings_count) as 'Review Findings',\n
SUM(test_generation_event_count) as 'Test Gen Events',\n
SUM(test_generation_accepted_tests) as 'Tests Accepted'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
+ "refId": "A"
+ }
+ ],
+ "title": "Code Review Findings & Test Generation",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Per-user productivity and efficiency metrics",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "cellOptions": {
+ "type": "auto"
+ },
+ "filterable": true,
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 10,
+ "w": 24,
+ "x": 0,
+ "y": 30
+ },
+ "id": 11,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": []
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n COALESCE(MAX(d.display_name), d.user_id) as
'User',\n COALESCE(MAX(r.subscription_tier), '') as 'Tier',\n
ROUND(SUM(r.credits_used), 1) as 'Credits Used',\n SUM(d.chat_ai_code_lines +
d.inline_ai_code_lines + d.code_fix_accepted_lines + d.dev_accepted_lines) as
'Total Accepted Lines',\n CASE WHEN SUM(d.chat_ai_code_lines +
d.inline_ai_code_lines + d.code_fix_accepted_lines + d.dev_accepted_lines) >
0\n THEN ROUND(SUM(r.credits_used) / SUM(d.chat_ai_c [...]
+ "refId": "A"
+ }
+ ],
+ "title": "User Productivity & Efficiency",
+ "type": "table"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Power tier users with no activity in the last 14 days",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "cellOptions": {
+ "type": "auto"
+ },
+ "filterable": true,
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 40
+ },
+ "id": 12,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": []
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n COALESCE(MAX(display_name), user_id) as
'User',\n MAX(subscription_tier) as 'Tier',\n ROUND(SUM(credits_used), 1) as
'Total Credits Used',\n MAX(date) as 'Last Activity'\nFROM
lake._tool_q_dev_user_report\nWHERE $__timeFilter(date)\n AND
subscription_tier = 'POWER'\nGROUP BY user_id\nHAVING MAX(date) <
DATE_SUB(NOW(), INTERVAL 14 DAY)\nORDER BY MAX(date)",
+ "refId": "A"
+ }
+ ],
+ "title": "Idle Power Users (No Activity in 14 Days)",
+ "type": "table"
+ }
+ ],
+ "preload": false,
+ "refresh": "5m",
+ "schemaVersion": 41,
+ "tags": [
+ "q_dev",
+ "executive",
+ "kiro"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-30d",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "utc",
+ "title": "Kiro Executive Dashboard",
+ "uid": "qdev_executive",
+ "version": 1
+}
\ No newline at end of file
diff --git a/grafana/dashboards/qdev_logging.json
b/grafana/dashboards/qdev_logging.json
new file mode 100644
index 000000000..462adcd98
--- /dev/null
+++ b/grafana/dashboards/qdev_logging.json
@@ -0,0 +1,800 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": "mysql",
+ "description": "Overview of logging event metrics",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "sum"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n (SELECT COUNT(*) FROM lake._tool_q_dev_chat_log
WHERE $__timeFilter(timestamp)) as 'Chat Events',\n (SELECT COUNT(DISTINCT
user_id) FROM lake._tool_q_dev_chat_log WHERE $__timeFilter(timestamp)) as
'Chat Users',\n (SELECT COUNT(DISTINCT conversation_id) FROM
lake._tool_q_dev_chat_log WHERE $__timeFilter(timestamp) AND conversation_id !=
'') as 'Conversations',\n (SELECT COUNT(*) FROM
lake._tool_q_dev_completion_log WHERE $__timeFilter(timestamp)) as 'Com [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Logging Overview",
+ "type": "stat"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Hourly distribution of AI usage activity (chat +
completions)",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "Hour of Day",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.8,
+ "drawStyle": "bars",
+ "fillOpacity": 80,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 6
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": [
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n LPAD(CAST(hour_of_day AS CHAR), 2, '0') as
'Hour',\n SUM(chat_count) as 'Chat Events',\n SUM(completion_count) as
'Completion Events'\nFROM (\n SELECT HOUR(timestamp) as hour_of_day, COUNT(*)
as chat_count, 0 as completion_count\n FROM lake._tool_q_dev_chat_log\n WHERE
$__timeFilter(timestamp)\n GROUP BY HOUR(timestamp)\n UNION ALL\n SELECT
HOUR(timestamp) as hour_of_day, 0 as chat_count, COUNT(*) as completion_count\n
FROM lake._tool_q_dev_complet [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Active Hours Distribution",
+ "type": "barchart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Distribution of model usage across chat events",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 14
+ },
+ "id": 3,
+ "options": {
+ "displayLabels": [
+ "name",
+ "percent"
+ ],
+ "legend": {
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true,
+ "values": [
+ "value",
+ "percent"
+ ]
+ },
+ "pieType": "donut",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "/^Requests$/",
+ "values": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n CASE\n WHEN model_id = '' OR model_id IS
NULL THEN '(unknown)'\n ELSE model_id\n END as 'Model',\n COUNT(*) as
'Requests'\nFROM lake._tool_q_dev_chat_log\nWHERE
$__timeFilter(timestamp)\nGROUP BY model_id\nORDER BY COUNT(*) DESC",
+ "refId": "A"
+ }
+ ],
+ "title": "Model Usage Distribution",
+ "type": "piechart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Top file extensions used with inline completions",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 14
+ },
+ "id": 4,
+ "options": {
+ "displayLabels": [
+ "name",
+ "percent"
+ ],
+ "legend": {
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true,
+ "values": [
+ "value",
+ "percent"
+ ]
+ },
+ "pieType": "pie",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n CASE\n WHEN file_extension = '' THEN
'(unknown)'\n ELSE file_extension\n END as 'File Type',\n COUNT(*) as
'Completions'\nFROM lake._tool_q_dev_completion_log\nWHERE
$__timeFilter(timestamp)\nGROUP BY file_extension\nORDER BY COUNT(*)
DESC\nLIMIT 15",
+ "refId": "A"
+ }
+ ],
+ "title": "File Type Usage (Completions)",
+ "type": "piechart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Average number of chat events per conversation",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 22
+ },
+ "id": 5,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "min"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n DATE(timestamp) as time,\n COUNT(*) /
NULLIF(COUNT(DISTINCT CASE WHEN conversation_id != '' THEN conversation_id
END), 0) as 'Avg Turns per Conversation',\n COUNT(DISTINCT CASE WHEN
conversation_id != '' THEN conversation_id END) as 'Unique Conversations',\n
COUNT(*) as 'Total Chat Events'\nFROM lake._tool_q_dev_chat_log\nWHERE
$__timeFilter(timestamp)\nGROUP BY DATE(timestamp)\nORDER BY DATE(timestamp)",
+ "refId": "A"
+ }
+ ],
+ "title": "Conversation Depth Analysis",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Daily chat and completion events over time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 30
+ },
+ "id": 6,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT time, SUM(chat) as 'Chat Events', SUM(completions)
as 'Completion Events'\nFROM (\n SELECT DATE(timestamp) as time, COUNT(*) as
chat, 0 as completions\n FROM lake._tool_q_dev_chat_log\n WHERE
$__timeFilter(timestamp)\n GROUP BY DATE(timestamp)\n UNION ALL\n SELECT
DATE(timestamp) as time, 0 as chat, COUNT(*) as completions\n FROM
lake._tool_q_dev_completion_log\n WHERE $__timeFilter(timestamp)\n GROUP BY
DATE(timestamp)\n) combined\nGROUP BY time\nORD [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Daily Event Trends",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Per-user logging activity summary",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "cellOptions": {
+ "type": "auto"
+ },
+ "filterable": true,
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 10,
+ "w": 24,
+ "x": 0,
+ "y": 38
+ },
+ "id": 7,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": []
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n COALESCE(u.display_name, u.user_id) as
'User',\n u.user_id as 'User ID',\n u.chat_events as 'Chat Events',\n
u.conversations as 'Conversations',\n ROUND(u.chat_events /
NULLIF(u.conversations, 0), 1) as 'Avg Turns',\n COALESCE(c.completion_events,
0) as 'Completion Events',\n COALESCE(c.files_count, 0) as 'Distinct Files',\n
ROUND(u.avg_prompt_len) as 'Avg Prompt Len',\n ROUND(u.avg_response_len) as
'Avg Response Len',\n u.steering_count as 'Steeri [...]
+ "refId": "A"
+ }
+ ],
+ "title": "Per-User Activity",
+ "type": "table"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Distribution of Kiro feature adoption: Steering, Spec
Mode, and Plain Chat",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 48
+ },
+ "id": 8,
+ "options": {
+ "displayLabels": [
+ "name",
+ "percent"
+ ],
+ "legend": {
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true,
+ "values": [
+ "value",
+ "percent"
+ ]
+ },
+ "pieType": "donut",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n SUM(CASE WHEN has_steering = 1 THEN 1 ELSE 0
END) as 'Using Steering',\n SUM(CASE WHEN is_spec_mode = 1 THEN 1 ELSE 0 END)
as 'Using Spec Mode',\n SUM(CASE WHEN has_steering = 0 AND is_spec_mode = 0
THEN 1 ELSE 0 END) as 'Plain Chat'\nFROM lake._tool_q_dev_chat_log\nWHERE
$__timeFilter(timestamp)",
+ "refId": "A"
+ }
+ ],
+ "title": "Kiro Feature Adoption",
+ "type": "piechart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Top file extensions active during chat events",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 48
+ },
+ "id": 9,
+ "options": {
+ "displayLabels": [
+ "name",
+ "percent"
+ ],
+ "legend": {
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true,
+ "values": [
+ "value",
+ "percent"
+ ]
+ },
+ "pieType": "pie",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT\n CASE\n WHEN active_file_extension = '' OR
active_file_extension IS NULL THEN '(no file active)'\n ELSE
active_file_extension\n END as 'File Type',\n COUNT(*) as 'Chat Events'\nFROM
lake._tool_q_dev_chat_log\nWHERE $__timeFilter(timestamp)\nGROUP BY
active_file_extension\nORDER BY COUNT(*) DESC\nLIMIT 15",
+ "refId": "A"
+ }
+ ],
+ "title": "Active File Types in Chat",
+ "type": "piechart"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Average and maximum prompt/response lengths over time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 56
+ },
+ "id": 10,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "rawQuery": true,
+ "rawSql": "SELECT\n DATE(timestamp) as time,\n AVG(prompt_length)
as 'Avg Prompt Length',\n AVG(response_length) as 'Avg Response Length',\n
MAX(prompt_length) as 'Max Prompt Length',\n MAX(response_length) as 'Max
Response Length'\nFROM lake._tool_q_dev_chat_log\nWHERE
$__timeFilter(timestamp)\nGROUP BY DATE(timestamp)\nORDER BY DATE(timestamp)",
+ "refId": "A"
+ }
+ ],
+ "title": "Prompt & Response Length Trends",
+ "type": "timeseries"
+ }
+ ],
+ "preload": false,
+ "refresh": "5m",
+ "schemaVersion": 41,
+ "tags": [
+ "q_dev",
+ "logging",
+ "kiro"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-30d",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "utc",
+ "title": "Kiro AI Activity Insights",
+ "uid": "qdev_logging",
+ "version": 1
+}
\ No newline at end of file
diff --git a/grafana/dashboards/qdev_user_data.json
b/grafana/dashboards/qdev_user_data.json
index d80d57bab..578cf095d 100644
--- a/grafana/dashboards/qdev_user_data.json
+++ b/grafana/dashboards/qdev_user_data.json
@@ -730,7 +730,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "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 [...]
+ "rawSql": "SELECT\n COALESCE(MAX(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 [...]
"refId": "A",
"select": [
[
@@ -771,6 +771,393 @@
],
"title": "User Interactions",
"type": "table"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Daily doc generation events and accepted/rejected lines",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 40
+ },
+ "id": 11,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "group": [],
+ "metricColumn": "none",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n
SUM(doc_generation_event_count) as 'Doc Generation Events',\n
SUM(doc_generation_accepted_line_additions) as 'Accepted Line Additions',\n
SUM(doc_generation_accepted_line_updates) as 'Accepted Line Updates',\n
SUM(doc_generation_rejected_line_additions) as 'Rejected Line Additions',\n
SUM(doc_generation_rejected_line_updates) as 'Rejected Line Updates'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
+ "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": "Doc Generation Metrics",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Daily test generation events and lines",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 40
+ },
+ "id": 12,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "group": [],
+ "metricColumn": "none",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n
SUM(test_generation_event_count) as 'Test Generation Events',\n
SUM(test_generation_accepted_tests) as 'Accepted Tests',\n
SUM(test_generation_generated_tests) as 'Generated Tests',\n
SUM(test_generation_accepted_lines) as 'Accepted Lines',\n
SUM(test_generation_generated_lines) as 'Generated Lines'\nFROM
lake._tool_q_dev_user_data\nWHERE $__timeFilter(date)\nGROUP BY date\nORDER BY
date",
+ "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": "Test Generation Metrics",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "mysql",
+ "description": "Daily agentic dev events and lines",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "smooth",
+ "lineWidth": 2,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green"
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 48
+ },
+ "id": 13,
+ "options": {
+ "legend": {
+ "calcs": [
+ "mean",
+ "max",
+ "sum"
+ ],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "11.6.2",
+ "targets": [
+ {
+ "datasource": "mysql",
+ "editorMode": "code",
+ "format": "time_series",
+ "group": [],
+ "metricColumn": "none",
+ "rawQuery": true,
+ "rawSql": "SELECT\n date as time,\n
SUM(dev_generation_event_count) as 'Dev Generation Events',\n
SUM(dev_acceptance_event_count) as 'Dev Acceptance Events',\n
SUM(dev_generated_lines) as 'Dev Generated Lines',\n SUM(dev_accepted_lines)
as 'Dev Accepted Lines'\nFROM lake._tool_q_dev_user_data\nWHERE
$__timeFilter(date)\nGROUP BY date\nORDER BY date",
+ "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": "Dev (Agentic) Metrics",
+ "type": "timeseries"
}
],
"preload": false,
diff --git a/grafana/dashboards/qdev_user_report.json
b/grafana/dashboards/qdev_user_report.json
index e1a27bc53..920fc1f6c 100644
--- a/grafana/dashboards/qdev_user_report.json
+++ b/grafana/dashboards/qdev_user_report.json
@@ -305,10 +305,10 @@
"pieType": "pie",
"reduceOptions": {
"calcs": [
- "sum"
+ "lastNotNull"
],
"fields": "",
- "values": false
+ "values": true
},
"tooltip": {
"mode": "single",
@@ -433,7 +433,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
- "rawSql": "SELECT\n COALESCE(display_name, user_id) as 'User',\n
subscription_tier as 'Tier',\n client_type as 'Client',\n SUM(credits_used)
as 'Credits Used',\n SUM(total_messages) as 'Messages',\n
SUM(chat_conversations) as 'Conversations',\n SUM(overage_credits_used) as
'Overage Credits',\n CASE WHEN MAX(CAST(overage_enabled AS UNSIGNED)) = 1 THEN
'Yes' ELSE 'No' END as 'Overage',\n MIN(date) as 'First Activity',\n
MAX(date) as 'Last Activity'\nFROM lake._tool_q_de [...]
+ "rawSql": "SELECT\n COALESCE(MAX(display_name), user_id) as
'User',\n subscription_tier as 'Tier',\n client_type as 'Client',\n
SUM(credits_used) as 'Credits Used',\n SUM(total_messages) as 'Messages',\n
SUM(chat_conversations) as 'Conversations',\n SUM(overage_credits_used) as
'Overage Credits',\n CASE WHEN MAX(CAST(overage_enabled AS UNSIGNED)) = 1 THEN
'Yes' ELSE 'No' END as 'Overage',\n MIN(date) as 'First Activity',\n
MAX(date) as 'Last Activity'\nFROM lake._tool [...]
"refId": "A"
}
],
@@ -461,4 +461,4 @@
"title": "Kiro Usage Dashboard",
"uid": "qdev_user_report",
"version": 1
-}
+}
\ No newline at end of file