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

Reply via email to