Skip to content

Commit 44a7227

Browse files
authored
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.
1 parent 99376a8 commit 44a7227

18 files changed

Lines changed: 2859 additions & 62 deletions

File tree

backend/plugins/q_dev/api/s3_slice_api.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -60,62 +60,21 @@ func GetScopeList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, er
6060
}
6161

6262
// GetScope returns a single scope record
63-
// @Summary get a Q Developer scope
64-
// @Description get a Q Developer scope
65-
// @Tags plugins/q_dev
66-
// @Param connectionId path int true "connection ID"
67-
// @Param scopeId path string true "scope id"
68-
// @Param blueprints query bool false "include blueprint references"
69-
// @Success 200 {object} ScopeDetail
70-
// @Failure 400 {object} shared.ApiBody "Bad Request"
71-
// @Failure 500 {object} shared.ApiBody "Internal Error"
72-
// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [GET]
7363
func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
7464
return dsHelper.ScopeApi.GetScopeDetail(input)
7565
}
7666

7767
// PatchScope updates a scope record
78-
// @Summary patch a Q Developer scope
79-
// @Description patch a Q Developer scope
80-
// @Tags plugins/q_dev
81-
// @Accept application/json
82-
// @Param connectionId path int true "connection ID"
83-
// @Param scopeId path string true "scope id"
84-
// @Param scope body models.QDevS3Slice true "json"
85-
// @Success 200 {object} models.QDevS3Slice
86-
// @Failure 400 {object} shared.ApiBody "Bad Request"
87-
// @Failure 500 {object} shared.ApiBody "Internal Error"
88-
// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [PATCH]
8968
func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
9069
return dsHelper.ScopeApi.Patch(input)
9170
}
9271

9372
// DeleteScope removes a scope and optionally associated data.
94-
// @Summary delete a Q Developer scope
95-
// @Description delete Q Developer scope data
96-
// @Tags plugins/q_dev
97-
// @Param connectionId path int true "connection ID"
98-
// @Param scopeId path string true "scope id"
99-
// @Param delete_data_only query bool false "Only delete scope data"
100-
// @Success 200
101-
// @Failure 400 {object} shared.ApiBody "Bad Request"
102-
// @Failure 409 {object} srvhelper.DsRefs "References exist to this scope"
103-
// @Failure 500 {object} shared.ApiBody "Internal Error"
104-
// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId} [DELETE]
10573
func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
10674
return dsHelper.ScopeApi.Delete(input)
10775
}
10876

10977
// GetScopeLatestSyncState returns scope sync state info
110-
// @Summary latest sync state for a Q Developer scope
111-
// @Description get latest sync state for a Q Developer scope
112-
// @Tags plugins/q_dev
113-
// @Param connectionId path int true "connection ID"
114-
// @Param scopeId path string true "scope id"
115-
// @Success 200
116-
// @Failure 400 {object} shared.ApiBody "Bad Request"
117-
// @Failure 500 {object} shared.ApiBody "Internal Error"
118-
// @Router /plugins/q_dev/connections/{connectionId}/scopes/{scopeId}/latest-sync-state [GET]
11978
func GetScopeLatestSyncState(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
12079
return dsHelper.ScopeApi.GetScopeLatestSyncState(input)
12180
}

backend/plugins/q_dev/impl/impl.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ func (p QDev) GetTablesInfo() []dal.Tabler {
5858
&models.QDevS3FileMeta{},
5959
&models.QDevS3Slice{},
6060
&models.QDevUserReport{},
61+
&models.QDevChatLog{},
62+
&models.QDevCompletionLog{},
6163
}
6264
}
6365

@@ -85,6 +87,7 @@ func (p QDev) SubTaskMetas() []plugin.SubTaskMeta {
8587
return []plugin.SubTaskMeta{
8688
tasks.CollectQDevS3FilesMeta,
8789
tasks.ExtractQDevS3DataMeta,
90+
tasks.ExtractQDevLoggingDataMeta,
8891
}
8992
}
9093

@@ -127,10 +130,21 @@ func (p QDev) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]int
127130
if op.Month != nil {
128131
timePart = fmt.Sprintf("%04d/%02d", op.Year, *op.Month)
129132
}
130-
base := fmt.Sprintf("%s/AWSLogs/%s/KiroLogs", op.BasePath, op.AccountId)
133+
// Kiro exports data to two well-known S3 prefixes:
134+
// {basePath}/AWSLogs/{accountId}/KiroLogs/ — user report CSVs
135+
// logging/AWSLogs/{accountId}/KiroLogs/ — interaction logs (JSON.gz)
136+
// When basePath is empty, default to "user-report" for CSV data.
137+
reportBase := op.BasePath
138+
if reportBase == "" {
139+
reportBase = "user-report"
140+
}
141+
csvBase := fmt.Sprintf("%s/AWSLogs/%s/KiroLogs", reportBase, op.AccountId)
142+
logBase := fmt.Sprintf("logging/AWSLogs/%s/KiroLogs", op.AccountId)
131143
s3Prefixes = []string{
132-
fmt.Sprintf("%s/by_user_analytic/%s/%s", base, region, timePart),
133-
fmt.Sprintf("%s/user_report/%s/%s", base, region, timePart),
144+
fmt.Sprintf("%s/by_user_analytic/%s/%s", csvBase, region, timePart),
145+
fmt.Sprintf("%s/user_report/%s/%s", csvBase, region, timePart),
146+
fmt.Sprintf("%s/GenerateAssistantResponse/%s/%s", logBase, region, timePart),
147+
fmt.Sprintf("%s/GenerateCompletions/%s/%s", logBase, region, timePart),
134148
}
135149
} else {
136150
// Legacy scope: use S3Prefix directly

backend/plugins/q_dev/impl/impl_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ func TestQDev_BasicPluginMethods(t *testing.T) {
3434

3535
// Test table info
3636
tables := plugin.GetTablesInfo()
37-
assert.Len(t, tables, 5)
37+
assert.Len(t, tables, 7)
3838

3939
// Test subtask metas
4040
subtasks := plugin.SubTaskMetas()
41-
assert.Len(t, subtasks, 2)
41+
assert.Len(t, subtasks, 3)
4242

4343
// Test API resources
4444
apiResources := plugin.ApiResources()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package models
19+
20+
import (
21+
"time"
22+
23+
"github.com/apache/incubator-devlake/core/models/common"
24+
)
25+
26+
// QDevChatLog stores parsed data from GenerateAssistantResponse logging events
27+
type QDevChatLog struct {
28+
common.NoPKModel
29+
ConnectionId uint64 `gorm:"primaryKey"`
30+
ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
31+
RequestId string `gorm:"primaryKey;type:varchar(255)" json:"requestId"`
32+
UserId string `gorm:"index;type:varchar(255)" json:"userId"`
33+
DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
34+
Timestamp time.Time `gorm:"index" json:"timestamp"`
35+
ChatTriggerType string `gorm:"type:varchar(50)" json:"chatTriggerType"`
36+
HasCustomization bool `json:"hasCustomization"`
37+
ConversationId string `gorm:"type:varchar(255)" json:"conversationId"`
38+
UtteranceId string `gorm:"type:varchar(255)" json:"utteranceId"`
39+
ModelId string `gorm:"type:varchar(100)" json:"modelId"`
40+
PromptLength int `json:"promptLength"`
41+
ResponseLength int `json:"responseLength"`
42+
OpenFileCount int `json:"openFileCount"`
43+
ActiveFileName string `gorm:"type:varchar(512)" json:"activeFileName"`
44+
ActiveFileExtension string `gorm:"type:varchar(50)" json:"activeFileExtension"`
45+
HasSteering bool `json:"hasSteering"`
46+
IsSpecMode bool `json:"isSpecMode"`
47+
}
48+
49+
func (QDevChatLog) TableName() string {
50+
return "_tool_q_dev_chat_log"
51+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package models
19+
20+
import (
21+
"time"
22+
23+
"github.com/apache/incubator-devlake/core/models/common"
24+
)
25+
26+
// QDevCompletionLog stores parsed data from GenerateCompletions logging events
27+
type QDevCompletionLog struct {
28+
common.NoPKModel
29+
ConnectionId uint64 `gorm:"primaryKey"`
30+
ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
31+
RequestId string `gorm:"primaryKey;type:varchar(255)" json:"requestId"`
32+
UserId string `gorm:"index;type:varchar(255)" json:"userId"`
33+
DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
34+
Timestamp time.Time `gorm:"index" json:"timestamp"`
35+
FileName string `gorm:"type:varchar(512)" json:"fileName"`
36+
FileExtension string `gorm:"type:varchar(50)" json:"fileExtension"`
37+
HasCustomization bool `json:"hasCustomization"`
38+
CompletionsCount int `json:"completionsCount"`
39+
}
40+
41+
func (QDevCompletionLog) TableName() string {
42+
return "_tool_q_dev_completion_log"
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package migrationscripts
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/errors"
23+
"github.com/apache/incubator-devlake/helpers/migrationhelper"
24+
"github.com/apache/incubator-devlake/plugins/q_dev/models/migrationscripts/archived"
25+
)
26+
27+
type addLoggingTables struct{}
28+
29+
func (*addLoggingTables) Up(basicRes context.BasicRes) errors.Error {
30+
return migrationhelper.AutoMigrateTables(
31+
basicRes,
32+
&archived.QDevChatLog{},
33+
&archived.QDevCompletionLog{},
34+
)
35+
}
36+
37+
func (*addLoggingTables) Version() uint64 {
38+
return 20260314000001
39+
}
40+
41+
func (*addLoggingTables) Name() string {
42+
return "Add chat_log and completion_log tables for Kiro logging data"
43+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package archived
19+
20+
import (
21+
"time"
22+
23+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
24+
)
25+
26+
type QDevChatLog struct {
27+
archived.NoPKModel
28+
ConnectionId uint64 `gorm:"primaryKey"`
29+
ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
30+
RequestId string `gorm:"primaryKey;type:varchar(255)" json:"requestId"`
31+
UserId string `gorm:"index;type:varchar(255)" json:"userId"`
32+
DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
33+
Timestamp time.Time `gorm:"index" json:"timestamp"`
34+
ChatTriggerType string `gorm:"type:varchar(50)" json:"chatTriggerType"`
35+
HasCustomization bool `json:"hasCustomization"`
36+
ConversationId string `gorm:"type:varchar(255)" json:"conversationId"`
37+
UtteranceId string `gorm:"type:varchar(255)" json:"utteranceId"`
38+
ModelId string `gorm:"type:varchar(100)" json:"modelId"`
39+
PromptLength int `json:"promptLength"`
40+
ResponseLength int `json:"responseLength"`
41+
OpenFileCount int `json:"openFileCount"`
42+
ActiveFileName string `gorm:"type:varchar(512)" json:"activeFileName"`
43+
ActiveFileExtension string `gorm:"type:varchar(50)" json:"activeFileExtension"`
44+
HasSteering bool `json:"hasSteering"`
45+
IsSpecMode bool `json:"isSpecMode"`
46+
}
47+
48+
func (QDevChatLog) TableName() string {
49+
return "_tool_q_dev_chat_log"
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package archived
19+
20+
import (
21+
"time"
22+
23+
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
24+
)
25+
26+
type QDevCompletionLog struct {
27+
archived.NoPKModel
28+
ConnectionId uint64 `gorm:"primaryKey"`
29+
ScopeId string `gorm:"primaryKey;type:varchar(255)" json:"scopeId"`
30+
RequestId string `gorm:"primaryKey;type:varchar(255)" json:"requestId"`
31+
UserId string `gorm:"index;type:varchar(255)" json:"userId"`
32+
DisplayName string `gorm:"type:varchar(255)" json:"displayName"`
33+
Timestamp time.Time `gorm:"index" json:"timestamp"`
34+
FileName string `gorm:"type:varchar(512)" json:"fileName"`
35+
FileExtension string `gorm:"type:varchar(50)" json:"fileExtension"`
36+
HasCustomization bool `json:"hasCustomization"`
37+
CompletionsCount int `json:"completionsCount"`
38+
}
39+
40+
func (QDevCompletionLog) TableName() string {
41+
return "_tool_q_dev_completion_log"
42+
}

backend/plugins/q_dev/models/migrationscripts/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ func All() []plugin.MigrationScript {
3535
new(addAccountIdToS3Slice),
3636
new(fixDedupUserTables),
3737
new(resetS3FileMetaProcessed),
38+
new(addLoggingTables),
3839
}
3940
}

backend/plugins/q_dev/models/s3_slice.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,16 @@ func (s *QDevS3Slice) normalize(strict bool) error {
9999
}
100100

101101
if s.Id == "" {
102-
s.Id = s.Prefix
102+
if s.AccountId != "" {
103+
// Use URL-safe ID: account_year or account_year_month
104+
if s.Month != nil {
105+
s.Id = fmt.Sprintf("%s_%04d_%02d", s.AccountId, s.Year, *s.Month)
106+
} else {
107+
s.Id = fmt.Sprintf("%s_%04d", s.AccountId, s.Year)
108+
}
109+
} else {
110+
s.Id = s.Prefix
111+
}
103112
}
104113

105114
if s.AccountId != "" {

0 commit comments

Comments
 (0)