From d0a1a4000dc64b1edd14b61f434b128a2b33b8f7 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 8 Jul 2026 09:42:29 -0600 Subject: [PATCH 1/4] feat(sonarqube): add project-level metrics history collection Add collector, extractor, and convertor subtasks that pull historical metric snapshots from the SonarQube measures/search_history and project_analyses/search APIs into a new cq_project_metrics_history domain table, enabling trend analysis for coverage, bugs, code smells, complexity, vulnerabilities, and other quality indicators over time. Signed-off-by: Joshua Smith --- .../codequality/cq_project_metrics_history.go | 46 +++++ .../domainlayer/domaininfo/domaininfo.go | 1 + ...20260707_add_cq_project_metrics_history.go | 42 +++++ .../archived/cq_project_metrics_history.go | 42 +++++ .../core/models/migrationscripts/register.go | 1 + backend/plugins/sonarqube/impl/impl.go | 7 + .../20260707_add_project_metrics_history.go | 77 +++++++++ .../models/migrationscripts/register.go | 1 + .../models/sonarqube_project_analyses.go | 40 +++++ .../sonarqube_project_metrics_history.go | 37 ++++ .../tasks/project_analyses_collector.go | 92 ++++++++++ .../tasks/project_analyses_extractor.go | 84 ++++++++++ .../project_metrics_history_collector.go | 94 +++++++++++ .../project_metrics_history_convertor.go | 158 ++++++++++++++++++ .../project_metrics_history_extractor.go | 89 ++++++++++ 15 files changed, 811 insertions(+) create mode 100644 backend/core/models/domainlayer/codequality/cq_project_metrics_history.go create mode 100644 backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go create mode 100644 backend/core/models/migrationscripts/archived/cq_project_metrics_history.go create mode 100644 backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go create mode 100644 backend/plugins/sonarqube/models/sonarqube_project_analyses.go create mode 100644 backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go create mode 100644 backend/plugins/sonarqube/tasks/project_analyses_collector.go create mode 100644 backend/plugins/sonarqube/tasks/project_analyses_extractor.go create mode 100644 backend/plugins/sonarqube/tasks/project_metrics_history_collector.go create mode 100644 backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go create mode 100644 backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go diff --git a/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go new file mode 100644 index 00000000000..84d8f17f947 --- /dev/null +++ b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go @@ -0,0 +1,46 @@ +/* +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 codequality + +import ( + "time" + + "github.com/apache/incubator-devlake/core/models/domainlayer" +) + +type CqProjectMetricsHistory struct { + domainlayer.DomainEntity + ProjectKey string `gorm:"index;type:varchar(500)"` + AnalysisDate time.Time `gorm:"index"` + Coverage *float64 + Ncloc *int + Bugs *int + ReliabilityRating string `gorm:"type:varchar(5)"` + CodeSmells *int + SqaleRating string `gorm:"type:varchar(5)"` + Complexity *int + CognitiveComplexity *int + Vulnerabilities *int + SecurityRating string `gorm:"type:varchar(5)"` + SecurityHotspots *int + DuplicatedLinesDensity *float64 +} + +func (CqProjectMetricsHistory) TableName() string { + return "cq_project_metrics_history" +} diff --git a/backend/core/models/domainlayer/domaininfo/domaininfo.go b/backend/core/models/domainlayer/domaininfo/domaininfo.go index b2cc5431fb2..b88289e8d8f 100644 --- a/backend/core/models/domainlayer/domaininfo/domaininfo.go +++ b/backend/core/models/domainlayer/domaininfo/domaininfo.go @@ -56,6 +56,7 @@ func GetDomainTablesInfo() []dal.Tabler { &codequality.CqIssue{}, &codequality.CqIssueImpact{}, &codequality.CqProject{}, + &codequality.CqProjectMetricsHistory{}, // crossdomain &crossdomain.Account{}, &crossdomain.BoardRepo{}, diff --git a/backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go b/backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.go new file mode 100644 index 00000000000..de66ae82914 --- /dev/null +++ b/backend/core/models/migrationscripts/20260707_add_cq_project_metrics_history.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 migrationscripts + +import ( + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/models/migrationscripts/archived" + "github.com/apache/incubator-devlake/helpers/migrationhelper" +) + +type addCqProjectMetricsHistory struct{} + +func (u *addCqProjectMetricsHistory) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables( + basicRes, + &archived.CqProjectMetricsHistory{}, + ) +} + +func (*addCqProjectMetricsHistory) Version() uint64 { + return 20260707153201 +} + +func (*addCqProjectMetricsHistory) Name() string { + return "add cq_project_metrics_history domain table" +} diff --git a/backend/core/models/migrationscripts/archived/cq_project_metrics_history.go b/backend/core/models/migrationscripts/archived/cq_project_metrics_history.go new file mode 100644 index 00000000000..934627368e2 --- /dev/null +++ b/backend/core/models/migrationscripts/archived/cq_project_metrics_history.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" + +type CqProjectMetricsHistory struct { + DomainEntity + ProjectKey string `gorm:"index;type:varchar(500)"` + AnalysisDate time.Time `gorm:"index"` + Coverage *float64 + Ncloc *int + Bugs *int + ReliabilityRating string `gorm:"type:varchar(5)"` + CodeSmells *int + SqaleRating string `gorm:"type:varchar(5)"` + Complexity *int + CognitiveComplexity *int + Vulnerabilities *int + SecurityRating string `gorm:"type:varchar(5)"` + SecurityHotspots *int + DuplicatedLinesDensity *float64 +} + +func (CqProjectMetricsHistory) TableName() string { + return "cq_project_metrics_history" +} diff --git a/backend/core/models/migrationscripts/register.go b/backend/core/models/migrationscripts/register.go index b4596154e2c..76f2e96838b 100644 --- a/backend/core/models/migrationscripts/register.go +++ b/backend/core/models/migrationscripts/register.go @@ -145,5 +145,6 @@ func All() []plugin.MigrationScript { new(modifyCicdDeploymentsToText), new(increaseCqIssuesProjectKeyLength), new(addAuthSessions), + new(addCqProjectMetricsHistory), } } diff --git a/backend/plugins/sonarqube/impl/impl.go b/backend/plugins/sonarqube/impl/impl.go index a6d6bd258ab..85ee668ee31 100644 --- a/backend/plugins/sonarqube/impl/impl.go +++ b/backend/plugins/sonarqube/impl/impl.go @@ -86,6 +86,8 @@ func (p Sonarqube) GetTablesInfo() []dal.Tabler { &models.SonarqubeFileMetrics{}, &models.SonarqubeAccount{}, &models.SonarqubeScopeConfig{}, + &models.SonarqubeProjectMetricsHistory{}, + &models.SonarqubeProjectAnalysis{}, } } @@ -108,6 +110,11 @@ func (p Sonarqube) SubTaskMetas() []plugin.SubTaskMeta { tasks.ConvertHotspotsMeta, tasks.ConvertFileMetricsMeta, tasks.ConvertAccountsMeta, + tasks.CollectProjectMetricsHistoryMeta, + tasks.ExtractProjectMetricsHistoryMeta, + tasks.CollectProjectAnalysesMeta, + tasks.ExtractProjectAnalysesMeta, + tasks.ConvertProjectMetricsHistoryMeta, } } diff --git a/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go b/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go new file mode 100644 index 00000000000..d060f46310a --- /dev/null +++ b/backend/plugins/sonarqube/models/migrationscripts/20260707_add_project_metrics_history.go @@ -0,0 +1,77 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package migrationscripts + +import ( + "time" + + "github.com/apache/incubator-devlake/core/context" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/models/migrationscripts/archived" + "github.com/apache/incubator-devlake/core/plugin" + "github.com/apache/incubator-devlake/helpers/migrationhelper" +) + +var _ plugin.MigrationScript = (*addProjectMetricsHistory)(nil) + +type projectMetricsHistory20260707 struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisDate time.Time `gorm:"primaryKey"` + MetricKey string `gorm:"primaryKey;type:varchar(100)"` + MetricValue string `gorm:"type:varchar(50)"` + archived.NoPKModel +} + +func (projectMetricsHistory20260707) TableName() string { + return "_tool_sonarqube_project_metrics_history" +} + +type projectAnalyses20260707 struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisDate time.Time `gorm:"index"` + ProjectVersion string `gorm:"type:varchar(255)"` + Revision string `gorm:"type:varchar(255)"` + BuildString string `gorm:"type:varchar(255)"` + DetectedCI string `gorm:"type:varchar(100)"` + archived.NoPKModel +} + +func (projectAnalyses20260707) TableName() string { + return "_tool_sonarqube_project_analyses" +} + +type addProjectMetricsHistory struct{} + +func (script *addProjectMetricsHistory) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables( + basicRes, + &projectMetricsHistory20260707{}, + &projectAnalyses20260707{}, + ) +} + +func (*addProjectMetricsHistory) Version() uint64 { + return 20260707153200 +} + +func (*addProjectMetricsHistory) Name() string { + return "add project_metrics_history and project_analyses tables" +} diff --git a/backend/plugins/sonarqube/models/migrationscripts/register.go b/backend/plugins/sonarqube/models/migrationscripts/register.go index 7c48de84226..1133419212e 100644 --- a/backend/plugins/sonarqube/models/migrationscripts/register.go +++ b/backend/plugins/sonarqube/models/migrationscripts/register.go @@ -39,5 +39,6 @@ func All() []plugin.MigrationScript { new(addOrgToConn), new(addIssueImpacts), new(extendSonarqubeFieldSize), + new(addProjectMetricsHistory), } } diff --git a/backend/plugins/sonarqube/models/sonarqube_project_analyses.go b/backend/plugins/sonarqube/models/sonarqube_project_analyses.go new file mode 100644 index 00000000000..55dac5ff702 --- /dev/null +++ b/backend/plugins/sonarqube/models/sonarqube_project_analyses.go @@ -0,0 +1,40 @@ +/* +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" +) + +type SonarqubeProjectAnalysis struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisDate time.Time `gorm:"index"` + ProjectVersion string `gorm:"type:varchar(255)"` + Revision string `gorm:"type:varchar(255)"` + BuildString string `gorm:"type:varchar(255)"` + DetectedCI string `gorm:"type:varchar(100)"` + common.NoPKModel +} + +func (SonarqubeProjectAnalysis) TableName() string { + return "_tool_sonarqube_project_analyses" +} diff --git a/backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go b/backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go new file mode 100644 index 00000000000..ebd555252ac --- /dev/null +++ b/backend/plugins/sonarqube/models/sonarqube_project_metrics_history.go @@ -0,0 +1,37 @@ +/* +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" +) + +type SonarqubeProjectMetricsHistory struct { + ConnectionId uint64 `gorm:"primaryKey"` + ProjectKey string `gorm:"primaryKey;type:varchar(255)"` + AnalysisDate time.Time `gorm:"primaryKey"` + MetricKey string `gorm:"primaryKey;type:varchar(100)"` + MetricValue string `gorm:"type:varchar(50)"` + common.NoPKModel +} + +func (SonarqubeProjectMetricsHistory) TableName() string { + return "_tool_sonarqube_project_metrics_history" +} diff --git a/backend/plugins/sonarqube/tasks/project_analyses_collector.go b/backend/plugins/sonarqube/tasks/project_analyses_collector.go new file mode 100644 index 00000000000..e01d46906a3 --- /dev/null +++ b/backend/plugins/sonarqube/tasks/project_analyses_collector.go @@ -0,0 +1,92 @@ +/* +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 ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/sonarqube/models" +) + +const RAW_PROJECT_ANALYSES_TABLE = "sonarqube_api_project_analyses" + +var _ plugin.SubTaskEntryPoint = CollectProjectAnalyses + +func CollectProjectAnalyses(taskCtx plugin.SubTaskContext) errors.Error { + logger := taskCtx.GetLogger() + logger.Info("collect project analyses") + + data := taskCtx.GetData().(*SonarqubeTaskData) + apiCollector, err := helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Params: models.SonarqubeApiParams{ + ConnectionId: data.Options.ConnectionId, + ProjectKey: data.Options.ProjectKey, + }, + Table: RAW_PROJECT_ANALYSES_TABLE, + }) + if err != nil { + return err + } + + err = apiCollector.InitCollector(helper.ApiCollectorArgs{ + ApiClient: data.ApiClient, + PageSize: 500, + UrlTemplate: "project_analyses/search", + Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { + query := url.Values{} + query.Set("project", data.Options.ProjectKey) + query.Set("ps", fmt.Sprintf("%v", reqData.Pager.Size)) + query.Set("p", fmt.Sprintf("%v", reqData.Pager.Page)) + if apiCollector.GetSince() != nil { + query.Set("from", apiCollector.GetSince().UTC().Format("2006-01-02")) + } + return query, nil + }, + GetTotalPages: GetTotalPagesFromResponse, + ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { + var resData struct { + Analyses []json.RawMessage `json:"analyses"` + } + err := helper.UnmarshalResponse(res, &resData) + if err != nil { + return nil, err + } + return resData.Analyses, nil + }, + }) + if err != nil { + return err + } + + return apiCollector.Execute() +} + +var CollectProjectAnalysesMeta = plugin.SubTaskMeta{ + Name: "CollectProjectAnalyses", + EntryPoint: CollectProjectAnalyses, + EnabledByDefault: true, + Description: "Collect project analysis metadata from SonarQube project_analyses/search API", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} diff --git a/backend/plugins/sonarqube/tasks/project_analyses_extractor.go b/backend/plugins/sonarqube/tasks/project_analyses_extractor.go new file mode 100644 index 00000000000..ac10d8d6e09 --- /dev/null +++ b/backend/plugins/sonarqube/tasks/project_analyses_extractor.go @@ -0,0 +1,84 @@ +/* +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 ( + "encoding/json" + "time" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/sonarqube/models" +) + +var _ plugin.SubTaskEntryPoint = ExtractProjectAnalyses + +type projectAnalysisResponse struct { + Key string `json:"key"` + Date string `json:"date"` + ProjectVersion string `json:"projectVersion"` + Revision string `json:"revision"` + BuildString string `json:"buildString"` + DetectedCI string `json:"detectedCI"` +} + +func ExtractProjectAnalyses(taskCtx plugin.SubTaskContext) errors.Error { + rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RAW_PROJECT_ANALYSES_TABLE) + + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: *rawDataSubTaskArgs, + Extract: func(resData *helper.RawData) ([]interface{}, errors.Error) { + body := &projectAnalysisResponse{} + err := errors.Convert(json.Unmarshal(resData.Data, body)) + if err != nil { + return nil, err + } + + analysisDate, parseErr := time.Parse("2006-01-02T15:04:05-0700", body.Date) + if parseErr != nil { + return nil, errors.Default.Wrap(errors.Convert(parseErr), "failed to parse analysis date") + } + + analysis := &models.SonarqubeProjectAnalysis{ + ConnectionId: data.Options.ConnectionId, + ProjectKey: data.Options.ProjectKey, + AnalysisKey: body.Key, + AnalysisDate: analysisDate, + ProjectVersion: body.ProjectVersion, + Revision: body.Revision, + BuildString: body.BuildString, + DetectedCI: body.DetectedCI, + } + return []interface{}{analysis}, nil + }, + }) + if err != nil { + return err + } + + return extractor.Execute() +} + +var ExtractProjectAnalysesMeta = plugin.SubTaskMeta{ + Name: "ExtractProjectAnalyses", + EntryPoint: ExtractProjectAnalyses, + EnabledByDefault: true, + Description: "Extract raw project analyses data into tool layer table", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} diff --git a/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go new file mode 100644 index 00000000000..ba10f24b763 --- /dev/null +++ b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go @@ -0,0 +1,94 @@ +/* +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 ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/sonarqube/models" +) + +const RAW_PROJECT_METRICS_HISTORY_TABLE = "sonarqube_api_project_metrics_history" + +const metricsToCollect = "coverage,ncloc,bugs,vulnerabilities,code_smells,security_hotspots," + + "duplicated_lines_density,sqale_rating,reliability_rating,security_rating,complexity,cognitive_complexity" + +var _ plugin.SubTaskEntryPoint = CollectProjectMetricsHistory + +func CollectProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error { + logger := taskCtx.GetLogger() + logger.Info("collect project metrics history") + + data := taskCtx.GetData().(*SonarqubeTaskData) + apiCollector, err := helper.NewStatefulApiCollector(helper.RawDataSubTaskArgs{ + Ctx: taskCtx, + Params: models.SonarqubeApiParams{ + ConnectionId: data.Options.ConnectionId, + ProjectKey: data.Options.ProjectKey, + }, + Table: RAW_PROJECT_METRICS_HISTORY_TABLE, + }) + if err != nil { + return err + } + + err = apiCollector.InitCollector(helper.ApiCollectorArgs{ + ApiClient: data.ApiClient, + PageSize: 1000, + UrlTemplate: "measures/search_history", + Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { + query := url.Values{} + query.Set("component", data.Options.ProjectKey) + query.Set("metrics", metricsToCollect) + query.Set("ps", fmt.Sprintf("%v", reqData.Pager.Size)) + query.Set("p", fmt.Sprintf("%v", reqData.Pager.Page)) + if apiCollector.GetSince() != nil { + query.Set("from", apiCollector.GetSince().UTC().Format("2006-01-02")) + } + return query, nil + }, + GetTotalPages: GetTotalPagesFromResponse, + ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { + var body json.RawMessage + err := helper.UnmarshalResponse(res, &body) + if err != nil { + return nil, err + } + return []json.RawMessage{body}, nil + }, + }) + if err != nil { + return err + } + + return apiCollector.Execute() +} + +var CollectProjectMetricsHistoryMeta = plugin.SubTaskMeta{ + Name: "CollectProjectMetricsHistory", + EntryPoint: CollectProjectMetricsHistory, + EnabledByDefault: true, + Description: "Collect project-level metric history from SonarQube measures/search_history API", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} diff --git a/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go b/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go new file mode 100644 index 00000000000..bf59aa1fe98 --- /dev/null +++ b/backend/plugins/sonarqube/tasks/project_metrics_history_convertor.go @@ -0,0 +1,158 @@ +/* +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 ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/apache/incubator-devlake/core/dal" + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/models/domainlayer" + "github.com/apache/incubator-devlake/core/models/domainlayer/codequality" + "github.com/apache/incubator-devlake/core/models/domainlayer/didgen" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + sonarqubeModels "github.com/apache/incubator-devlake/plugins/sonarqube/models" +) + +var ConvertProjectMetricsHistoryMeta = plugin.SubTaskMeta{ + Name: "convertProjectMetricsHistory", + EntryPoint: ConvertProjectMetricsHistory, + EnabledByDefault: true, + Description: "Convert tool layer project metrics history into domain layer table cq_project_metrics_history", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} + +func ConvertProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error { + db := taskCtx.GetDal() + _, data := CreateRawDataSubTaskArgs(taskCtx, RAW_PROJECT_METRICS_HISTORY_TABLE) + + // Query all narrow metric rows ordered so we can group-pivot them + cursor, err := db.Cursor( + dal.From(sonarqubeModels.SonarqubeProjectMetricsHistory{}), + dal.Where("connection_id = ? AND project_key = ?", data.Options.ConnectionId, data.Options.ProjectKey), + dal.Orderby("analysis_date"), + ) + if err != nil { + return err + } + defer cursor.Close() + + projectIdGen := didgen.NewDomainIdGenerator(&sonarqubeModels.SonarqubeProject{}) + domainProjectKey := projectIdGen.Generate(data.Options.ConnectionId, data.Options.ProjectKey) + + batchSave, err := helper.NewBatchSave(taskCtx, reflect.TypeOf(&codequality.CqProjectMetricsHistory{}), 200) + if err != nil { + return err + } + defer batchSave.Close() + + // Group narrow rows by analysis_date, pivot into wide domain rows + var currentDate *time.Time + var currentDomain *codequality.CqProjectMetricsHistory + + flushCurrent := func() errors.Error { + if currentDomain != nil { + return batchSave.Add(currentDomain) + } + return nil + } + + for cursor.Next() { + row := &sonarqubeModels.SonarqubeProjectMetricsHistory{} + err = db.Fetch(cursor, row) + if err != nil { + return err + } + + if currentDate == nil || !currentDate.Equal(row.AnalysisDate) { + if flushErr := flushCurrent(); flushErr != nil { + return flushErr + } + domainId := fmt.Sprintf("%s:%s", + domainProjectKey, + row.AnalysisDate.UTC().Format("2006-01-02T15:04:05Z"), + ) + currentDomain = &codequality.CqProjectMetricsHistory{ + DomainEntity: domainlayer.DomainEntity{Id: domainId}, + ProjectKey: domainProjectKey, + AnalysisDate: row.AnalysisDate, + } + t := row.AnalysisDate + currentDate = &t + } + + applyMetricValue(currentDomain, row.MetricKey, row.MetricValue) + } + + if flushErr := flushCurrent(); flushErr != nil { + return flushErr + } + + return batchSave.Close() +} + +func applyMetricValue(d *codequality.CqProjectMetricsHistory, metricKey, value string) { + switch metricKey { + case "coverage": + if v, err := strconv.ParseFloat(value, 64); err == nil { + d.Coverage = &v + } + case "ncloc": + if v, err := strconv.Atoi(value); err == nil { + d.Ncloc = &v + } + case "bugs": + if v, err := strconv.Atoi(value); err == nil { + d.Bugs = &v + } + case "reliability_rating": + d.ReliabilityRating = alphabetMap[value] + case "code_smells": + if v, err := strconv.Atoi(value); err == nil { + d.CodeSmells = &v + } + case "sqale_rating": + d.SqaleRating = alphabetMap[value] + case "complexity": + if v, err := strconv.Atoi(value); err == nil { + d.Complexity = &v + } + case "cognitive_complexity": + if v, err := strconv.Atoi(value); err == nil { + d.CognitiveComplexity = &v + } + case "vulnerabilities": + if v, err := strconv.Atoi(value); err == nil { + d.Vulnerabilities = &v + } + case "security_rating": + d.SecurityRating = alphabetMap[value] + case "security_hotspots": + if v, err := strconv.Atoi(value); err == nil { + d.SecurityHotspots = &v + } + case "duplicated_lines_density": + if v, err := strconv.ParseFloat(value, 64); err == nil { + d.DuplicatedLinesDensity = &v + } + } +} diff --git a/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go b/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go new file mode 100644 index 00000000000..430ff196beb --- /dev/null +++ b/backend/plugins/sonarqube/tasks/project_metrics_history_extractor.go @@ -0,0 +1,89 @@ +/* +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 ( + "encoding/json" + "time" + + "github.com/apache/incubator-devlake/core/errors" + "github.com/apache/incubator-devlake/core/plugin" + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" + "github.com/apache/incubator-devlake/plugins/sonarqube/models" +) + +var _ plugin.SubTaskEntryPoint = ExtractProjectMetricsHistory + +type metricsHistoryResponse struct { + Measures []struct { + Metric string `json:"metric"` + History []struct { + Date string `json:"date"` + Value string `json:"value"` + } `json:"history"` + } `json:"measures"` +} + +func ExtractProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error { + rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RAW_PROJECT_METRICS_HISTORY_TABLE) + + extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{ + RawDataSubTaskArgs: *rawDataSubTaskArgs, + Extract: func(resData *helper.RawData) ([]interface{}, errors.Error) { + body := &metricsHistoryResponse{} + err := errors.Convert(json.Unmarshal(resData.Data, body)) + if err != nil { + return nil, err + } + + var results []interface{} + for _, measure := range body.Measures { + for _, entry := range measure.History { + if entry.Value == "" { + continue + } + analysisDate, parseErr := time.Parse("2006-01-02T15:04:05-0700", entry.Date) + if parseErr != nil { + return nil, errors.Default.Wrap(errors.Convert(parseErr), "failed to parse analysis date") + } + results = append(results, &models.SonarqubeProjectMetricsHistory{ + ConnectionId: data.Options.ConnectionId, + ProjectKey: data.Options.ProjectKey, + AnalysisDate: analysisDate, + MetricKey: measure.Metric, + MetricValue: entry.Value, + }) + } + } + return results, nil + }, + }) + if err != nil { + return err + } + + return extractor.Execute() +} + +var ExtractProjectMetricsHistoryMeta = plugin.SubTaskMeta{ + Name: "ExtractProjectMetricsHistory", + EntryPoint: ExtractProjectMetricsHistory, + EnabledByDefault: true, + Description: "Extract raw project metrics history into tool layer table", + DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY}, +} From d3dd8f7bc403b05de7add9080dc0bb746091b039 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 8 Jul 2026 10:52:54 -0600 Subject: [PATCH 2/4] fix(sonarqube): Require user token at test * Reject Global and Project analysis tokens on SonarQube Server before authentication validate runs. * Skip prefix validation for SonarCloud endpoints where token prefixes differ. --- .../plugins/sonarqube/api/connection_api.go | 3 + .../plugins/sonarqube/models/connection.go | 42 ++++++++- .../sonarqube/models/connection_test.go | 92 +++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 backend/plugins/sonarqube/models/connection_test.go diff --git a/backend/plugins/sonarqube/api/connection_api.go b/backend/plugins/sonarqube/api/connection_api.go index feb1905a63d..d387e5d460a 100644 --- a/backend/plugins/sonarqube/api/connection_api.go +++ b/backend/plugins/sonarqube/api/connection_api.go @@ -44,6 +44,9 @@ func testConnection(ctx context.Context, connection models.SonarqubeConn) (*plug return nil, errors.Default.Wrap(err, "error validating target") } } + if err := connection.ValidateUserTokenPrefix(); err != nil { + return nil, err + } apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &connection) if err != nil { return nil, err diff --git a/backend/plugins/sonarqube/models/connection.go b/backend/plugins/sonarqube/models/connection.go index 66c32f57c49..0bbe124d592 100644 --- a/backend/plugins/sonarqube/models/connection.go +++ b/backend/plugins/sonarqube/models/connection.go @@ -21,6 +21,7 @@ import ( "encoding/base64" "fmt" "net/http" + "strings" "github.com/apache/incubator-devlake/core/utils" @@ -29,6 +30,12 @@ import ( helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" ) +const ( + userTokenPrefix = "squ_" + globalAnalysisTokenPrefix = "sqa_" + projectAnalysisTokenPrefix = "sqp_" +) + type SonarqubeAccessToken helper.AccessToken // SetupAuthentication sets up the HTTP Request Authentication @@ -92,7 +99,40 @@ func (connection *SonarqubeConnection) MergeFromRequest(target *SonarqubeConnect return nil } -func (connection *SonarqubeConnection) IsCloud() bool { +// ValidateUserTokenPrefix ensures the token is a SonarQube User token on Server +// instances. Global (sqa_) and Project (sqp_) analysis tokens authenticate but +// cannot call read APIs such as measures/component_tree. +func (connection SonarqubeConn) ValidateUserTokenPrefix() errors.Error { + if connection.IsCloud() { + return nil + } + token := strings.TrimSpace(connection.Token) + if token == "" { + return errors.BadInput.New("token is required") + } + switch { + case strings.HasPrefix(token, globalAnalysisTokenPrefix): + return errors.BadInput.New( + "DevLake requires a User token (squ_ prefix). " + + "Global Analysis tokens (sqa_) can push scan results but cannot read project metrics via the Web API. " + + "Create a User token under My Account > Security in SonarQube.", + ) + case strings.HasPrefix(token, projectAnalysisTokenPrefix): + return errors.BadInput.New( + "DevLake requires a User token (squ_ prefix). " + + "Project Analysis tokens (sqp_) can push scan results but cannot read project metrics via the Web API. " + + "Create a User token under My Account > Security in SonarQube.", + ) + case !strings.HasPrefix(token, userTokenPrefix): + return errors.BadInput.New( + "DevLake requires a User token (squ_ prefix) for SonarQube Server. " + + "Create one under My Account > Security in SonarQube.", + ) + } + return nil +} + +func (connection SonarqubeConn) IsCloud() bool { return connection.Endpoint == "https://sonarcloud.io/api/" } diff --git a/backend/plugins/sonarqube/models/connection_test.go b/backend/plugins/sonarqube/models/connection_test.go new file mode 100644 index 00000000000..e92284ad5da --- /dev/null +++ b/backend/plugins/sonarqube/models/connection_test.go @@ -0,0 +1,92 @@ +/* +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 ( + "testing" + + helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" +) + +func TestValidateUserTokenPrefix(t *testing.T) { + t.Parallel() + + serverEndpoint := "https://rad-sonar.example.com/api/" + cloudEndpoint := "https://sonarcloud.io/api/" + + tests := []struct { + name string + conn SonarqubeConn + wantErr bool + }{ + { + name: "user token on server", + conn: SonarqubeConn{ + RestConnection: helper.RestConnection{Endpoint: serverEndpoint}, + SonarqubeAccessToken: SonarqubeAccessToken{Token: "squ_abc123"}, + }, + }, + { + name: "global analysis token on server", + conn: SonarqubeConn{ + RestConnection: helper.RestConnection{Endpoint: serverEndpoint}, + SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqa_abc123"}, + }, + wantErr: true, + }, + { + name: "project analysis token on server", + conn: SonarqubeConn{ + RestConnection: helper.RestConnection{Endpoint: serverEndpoint}, + SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqp_abc123"}, + }, + wantErr: true, + }, + { + name: "unknown prefix on server", + conn: SonarqubeConn{ + RestConnection: helper.RestConnection{Endpoint: serverEndpoint}, + SonarqubeAccessToken: SonarqubeAccessToken{Token: "legacy-token"}, + }, + wantErr: true, + }, + { + name: "sonarcloud skips prefix check", + conn: SonarqubeConn{ + RestConnection: helper.RestConnection{Endpoint: cloudEndpoint}, + SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqa_abc123"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.conn.ValidateUserTokenPrefix() + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} From 40cdcb5048f2fe53d0824eccefa7644fe66a952e Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Wed, 8 Jul 2026 14:29:28 -0600 Subject: [PATCH 3/4] feat(grafana): add historical trends to SonarQube dashboards * Add collapsed "Historical Trends" row with coverage, bugs, vulnerabilities, code smells, and duplication time-series panels to both Server and Cloud dashboards * Panels query cq_project_metrics_history and honor the Grafana time picker via $__timeFilter Signed-off-by: Joshua Smith --- grafana/dashboards/mysql/Sonarqube.json | 261 ++++++++++++++++- .../dashboards/mysql/sonar-qube-cloud.json | 261 ++++++++++++++++- grafana/dashboards/postgresql/Sonarqube.json | 269 +++++++++++++++++- .../postgresql/sonar-qube-cloud.json | 269 +++++++++++++++++- 4 files changed, 1052 insertions(+), 8 deletions(-) diff --git a/grafana/dashboards/mysql/Sonarqube.json b/grafana/dashboards/mysql/Sonarqube.json index d8708f36d93..2dac28c9f21 100644 --- a/grafana/dashboards/mysql/Sonarqube.json +++ b/grafana/dashboards/mysql/Sonarqube.json @@ -46,7 +46,7 @@ "showLineNumbers": false, "showMiniMap": false }, - "content": "- Use Cases: This dashboard shows the code quality metrics from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- This dashboard does not honor the time filter on the top-right side as SonarQube metrics are all from the latest scan.", + "content": "- Use Cases: This dashboard shows the code quality metrics from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- Snapshot panels (Reliability, Security, Test, etc.) show the latest scan and do not honor the time filter.\n- **Historical Trends** panels at the bottom respond to the time range selector and show metric history over time.", "mode": "markdown" }, "pluginVersion": "11.2.0", @@ -1023,6 +1023,263 @@ ], "title": "Code Quality Metrics by Files (Top 20 order by Bugs)", "type": "table" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 20, + "panels": [ + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 21, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.coverage AS 'Coverage %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Coverage Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 22, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.bugs AS 'Bugs',\n pmh.vulnerabilities AS 'Vulnerabilities'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Bugs & Vulnerabilities Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 23, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.code_smells AS 'Code Smells'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Code Smells Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 24, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.duplicated_lines_density AS 'Duplication %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Duplication Trend", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Historical Trends", + "type": "row" } ], "refresh": "", @@ -1086,7 +1343,7 @@ ] }, "time": { - "from": "now", + "from": "now-6M", "to": "now" }, "timepicker": {}, diff --git a/grafana/dashboards/mysql/sonar-qube-cloud.json b/grafana/dashboards/mysql/sonar-qube-cloud.json index 1a290703897..8cb1939a5eb 100644 --- a/grafana/dashboards/mysql/sonar-qube-cloud.json +++ b/grafana/dashboards/mysql/sonar-qube-cloud.json @@ -47,7 +47,7 @@ "showLineNumbers": false, "showMiniMap": false }, - "content": "- Use Cases: This dashboard shows the code quality metrics from SonarCloud.\n- Data Source Required: SonarCloud\n- This dashboard does not honor the time filter on the top-right side as SonarQube metrics are all from the latest scan.", + "content": "- Use Cases: This dashboard shows the code quality metrics from SonarCloud.\n- Data Source Required: SonarCloud\n- Snapshot panels (Software Quality, Security Review, Test, etc.) show the latest scan and do not honor the time filter.\n- **Historical Trends** panels at the bottom respond to the time range selector and show metric history over time.", "mode": "markdown" }, "pluginVersion": "11.2.0", @@ -1428,6 +1428,263 @@ ], "title": "Code Quality Metrics by Files (Top 20 order by Bugs)", "type": "table" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 27, + "panels": [ + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 28, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.coverage AS 'Coverage %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Coverage Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 29, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.bugs AS 'Bugs',\n pmh.vulnerabilities AS 'Vulnerabilities'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Bugs & Vulnerabilities Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 30, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.code_smells AS 'Code Smells'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Code Smells Trend", + "type": "timeseries" + }, + { + "datasource": "mysql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 31, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "mysql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT\n pmh.analysis_date AS time,\n pmh.duplicated_lines_density AS 'Duplication %'\nFROM cq_project_metrics_history pmh\nWHERE\n pmh.project_key in (${project_id})\n AND $__timeFilter(pmh.analysis_date)\nORDER BY pmh.analysis_date ASC", + "refId": "A" + } + ], + "title": "Duplication Trend", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Historical Trends", + "type": "row" } ], "refresh": "", @@ -1491,7 +1748,7 @@ ] }, "time": { - "from": "now", + "from": "now-6M", "to": "now" }, "timepicker": {}, diff --git a/grafana/dashboards/postgresql/Sonarqube.json b/grafana/dashboards/postgresql/Sonarqube.json index 02d9ccbc250..1bae97b37a0 100644 --- a/grafana/dashboards/postgresql/Sonarqube.json +++ b/grafana/dashboards/postgresql/Sonarqube.json @@ -46,7 +46,7 @@ "showLineNumbers": false, "showMiniMap": false }, - "content": "- Use Cases: This dashboard shows the code quality metrics from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- This dashboard does not honor the time filter on the top-right side as SonarQube metrics are all from the latest scan.", + "content": "- Use Cases: This dashboard shows the code quality metrics from SonarQube.\n- Data Source Required: SonarQube v8.2+\n- Snapshot panels (Reliability, Security, Test, etc.) show the latest scan and do not honor the time filter.\n- **Historical Trends** panels at the bottom respond to the time range selector and show metric history over time.", "mode": "markdown" }, "pluginVersion": "11.2.0", @@ -1023,6 +1023,271 @@ ], "title": "Code Quality Metrics by Files (Top 20 order by Bugs)", "type": "table" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 20, + "panels": [ + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 21, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.coverage AS \"Coverage %\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Coverage Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 22, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.bugs AS \"Bugs\", pmh.vulnerabilities AS \"Vulnerabilities\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Bugs & Vulnerabilities Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 23, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.code_smells AS \"Code Smells\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Code Smells Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.duplicated_lines_density AS \"Duplication %\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Duplication Trend", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Historical Trends", + "type": "row" } ], "refresh": "", @@ -1086,7 +1351,7 @@ ] }, "time": { - "from": "now", + "from": "now-6M", "to": "now" }, "timepicker": {}, diff --git a/grafana/dashboards/postgresql/sonar-qube-cloud.json b/grafana/dashboards/postgresql/sonar-qube-cloud.json index 6719a3e2d7d..08cd7359e16 100644 --- a/grafana/dashboards/postgresql/sonar-qube-cloud.json +++ b/grafana/dashboards/postgresql/sonar-qube-cloud.json @@ -47,7 +47,7 @@ "showLineNumbers": false, "showMiniMap": false }, - "content": "- Use Cases: This dashboard shows the code quality metrics from SonarCloud.\n- Data Source Required: SonarCloud\n- This dashboard does not honor the time filter on the top-right side as SonarQube metrics are all from the latest scan.", + "content": "- Use Cases: This dashboard shows the code quality metrics from SonarCloud.\n- Data Source Required: SonarCloud\n- Snapshot panels (Software Quality, Security Review, Test, etc.) show the latest scan and do not honor the time filter.\n- **Historical Trends** panels at the bottom respond to the time range selector and show metric history over time.", "mode": "markdown" }, "pluginVersion": "11.2.0", @@ -1428,6 +1428,271 @@ ], "title": "Code Quality Metrics by Files (Top 20 order by Bugs)", "type": "table" + }, + { + "collapsed": true, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 27, + "panels": [ + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 28, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.coverage AS \"Coverage %\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Coverage Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 29, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.bugs AS \"Bugs\", pmh.vulnerabilities AS \"Vulnerabilities\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Bugs & Vulnerabilities Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 30, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.code_smells AS \"Code Smells\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Code Smells Trend", + "type": "timeseries" + }, + { + "datasource": "postgresql", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 10, + "spanNulls": true, + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "pointSize": 5 + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 31, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": "postgresql", + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT pmh.analysis_date AS time, pmh.duplicated_lines_density AS \"Duplication %\" FROM cq_project_metrics_history AS pmh WHERE ('${project_id:csv}' = '' OR pmh.project_key::text = ANY(ARRAY[${project_id:singlequote}]::text[])) AND $__timeFilter(pmh.analysis_date) ORDER BY pmh.analysis_date ASC NULLS FIRST", + "refId": "A" + } + ], + "title": "Duplication Trend", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Historical Trends", + "type": "row" } ], "refresh": "", @@ -1491,7 +1756,7 @@ ] }, "time": { - "from": "now", + "from": "now-6M", "to": "now" }, "timepicker": {}, From 8e0a5548b5e9a63f430ea87e8edb62a7bd8c2915 Mon Sep 17 00:00:00 2001 From: Joshua Smith Date: Fri, 10 Jul 2026 11:58:38 -0600 Subject: [PATCH 4/4] style(sonarqube): Fix gofmt formatting in code quality files * Align struct fields and ApiCollectorArgs literals to satisfy golangci-lint gofmt checks. * Updates cq_project_metrics_history and SonarQube collector tasks flagged in CI. --- .../codequality/cq_project_metrics_history.go | 26 +++++++++---------- .../tasks/project_analyses_collector.go | 4 +-- .../project_metrics_history_collector.go | 4 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go index 84d8f17f947..90f64ce660c 100644 --- a/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go +++ b/backend/core/models/domainlayer/codequality/cq_project_metrics_history.go @@ -25,19 +25,19 @@ import ( type CqProjectMetricsHistory struct { domainlayer.DomainEntity - ProjectKey string `gorm:"index;type:varchar(500)"` - AnalysisDate time.Time `gorm:"index"` - Coverage *float64 - Ncloc *int - Bugs *int - ReliabilityRating string `gorm:"type:varchar(5)"` - CodeSmells *int - SqaleRating string `gorm:"type:varchar(5)"` - Complexity *int - CognitiveComplexity *int - Vulnerabilities *int - SecurityRating string `gorm:"type:varchar(5)"` - SecurityHotspots *int + ProjectKey string `gorm:"index;type:varchar(500)"` + AnalysisDate time.Time `gorm:"index"` + Coverage *float64 + Ncloc *int + Bugs *int + ReliabilityRating string `gorm:"type:varchar(5)"` + CodeSmells *int + SqaleRating string `gorm:"type:varchar(5)"` + Complexity *int + CognitiveComplexity *int + Vulnerabilities *int + SecurityRating string `gorm:"type:varchar(5)"` + SecurityHotspots *int DuplicatedLinesDensity *float64 } diff --git a/backend/plugins/sonarqube/tasks/project_analyses_collector.go b/backend/plugins/sonarqube/tasks/project_analyses_collector.go index e01d46906a3..37682fd7cd0 100644 --- a/backend/plugins/sonarqube/tasks/project_analyses_collector.go +++ b/backend/plugins/sonarqube/tasks/project_analyses_collector.go @@ -51,8 +51,8 @@ func CollectProjectAnalyses(taskCtx plugin.SubTaskContext) errors.Error { } err = apiCollector.InitCollector(helper.ApiCollectorArgs{ - ApiClient: data.ApiClient, - PageSize: 500, + ApiClient: data.ApiClient, + PageSize: 500, UrlTemplate: "project_analyses/search", Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { query := url.Values{} diff --git a/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go index ba10f24b763..fa2b02633b6 100644 --- a/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go +++ b/backend/plugins/sonarqube/tasks/project_metrics_history_collector.go @@ -54,8 +54,8 @@ func CollectProjectMetricsHistory(taskCtx plugin.SubTaskContext) errors.Error { } err = apiCollector.InitCollector(helper.ApiCollectorArgs{ - ApiClient: data.ApiClient, - PageSize: 1000, + ApiClient: data.ApiClient, + PageSize: 1000, UrlTemplate: "measures/search_history", Query: func(reqData *helper.RequestData) (url.Values, errors.Error) { query := url.Values{}