From 1889f850ae08883232524537161fb6feb0c59022 Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 1/6] feat(core): add project_name to board_issues Several DevLake projects can share one board and each collect their own slice of it (e.g. via Jira ExtraJQL). Record which project a board/issue link belongs to by adding project_name to board_issues. It is part of the primary key so an issue that falls into two projects' slices can be linked once per project. Existing rows and plugins that do not set it get an empty project_name, which keeps the previous single-project behaviour. The e2e snapshots that verify board_issues gain the (empty) project_name column. Co-Authored-By: Claude Opus 5.5 --- .../models/domainlayer/ticket/board_issue.go | 5 +- ...0260820_add_project_name_to_board_issue.go | 46 ++++++++++++++ .../core/models/migrationscripts/register.go | 1 + .../e2e/snapshot_tables/board_issues.csv | 62 +++++++++---------- .../e2e/snapshot_tables/board_issues.csv | 18 +++--- .../e2e/snapshot_tables/board_issues.csv | 54 ++++++++-------- .../e2e/snapshot_tables/board_issues.csv | 34 +++++----- .../e2e/snapshot_tables/board_issues.csv | 10 +-- .../e2e/snapshot_tables/board_issues.csv | 8 +-- .../jira/e2e/snapshot_tables/board_issues.csv | 62 +++++++++---------- .../e2e/snapshot_tables/board_issues.csv | 12 ++-- .../e2e/snapshot_tables/board_issues.csv | 12 ++-- .../board_issues_from_epics.csv | 6 +- .../board_issues_from_issues.csv | 6 +- .../board_issues_from_tasks.csv | 6 +- .../board_issues_from_user_stories.csv | 6 +- .../e2e/snapshot_tables/board_issues_bug.csv | 42 ++++++------- .../snapshot_tables/board_issues_story.csv | 42 ++++++------- .../e2e/snapshot_tables/board_issues_task.csv | 32 +++++----- .../e2e/snapshot_tables/board_issues.csv | 32 +++++----- .../e2e/snapshot_tables/board_issues_bug.csv | 14 ++--- .../snapshot_tables/board_issues_story.csv | 20 +++--- .../e2e/snapshot_tables/board_issues_task.csv | 8 +-- 23 files changed, 293 insertions(+), 245 deletions(-) create mode 100644 backend/core/models/migrationscripts/20260820_add_project_name_to_board_issue.go diff --git a/backend/core/models/domainlayer/ticket/board_issue.go b/backend/core/models/domainlayer/ticket/board_issue.go index 522a79e6f57..8183402bf16 100644 --- a/backend/core/models/domainlayer/ticket/board_issue.go +++ b/backend/core/models/domainlayer/ticket/board_issue.go @@ -22,8 +22,9 @@ import ( ) type BoardIssue struct { - BoardId string `gorm:"primaryKey;type:varchar(255)"` - IssueId string `gorm:"primaryKey;type:varchar(255)"` + BoardId string `gorm:"primaryKey;type:varchar(255)"` + IssueId string `gorm:"primaryKey;type:varchar(255)"` + ProjectName string `gorm:"primaryKey;type:varchar(255);default:''"` common.NoPKModel } diff --git a/backend/core/models/migrationscripts/20260820_add_project_name_to_board_issue.go b/backend/core/models/migrationscripts/20260820_add_project_name_to_board_issue.go new file mode 100644 index 00000000000..2a7e64aa569 --- /dev/null +++ b/backend/core/models/migrationscripts/20260820_add_project_name_to_board_issue.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 migrationscripts + +import ( + "github.com/apache/devlake/core/context" + "github.com/apache/devlake/core/errors" + "github.com/apache/devlake/helpers/migrationhelper" +) + +type boardIssue20260820 struct { + ProjectName string `gorm:"primaryKey;type:varchar(255);default:''"` +} + +func (boardIssue20260820) TableName() string { + return "board_issues" +} + +type addProjectNameToBoardIssue struct{} + +func (*addProjectNameToBoardIssue) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables(basicRes, &boardIssue20260820{}) +} + +func (*addProjectNameToBoardIssue) Version() uint64 { + return 20260820000000 +} + +func (*addProjectNameToBoardIssue) Name() string { + return "add project_name to board_issues" +} diff --git a/backend/core/models/migrationscripts/register.go b/backend/core/models/migrationscripts/register.go index 94994b612e0..d11c11258b2 100644 --- a/backend/core/models/migrationscripts/register.go +++ b/backend/core/models/migrationscripts/register.go @@ -155,5 +155,6 @@ func All() []plugin.MigrationScript { new(backfillSubProjectFromMonorepo), new(addBlueprintIdIndexToPipelines), new(expandDomainTextColumns), + new(addProjectNameToBoardIssue), } } diff --git a/backend/plugins/bitbucket/e2e/snapshot_tables/board_issues.csv b/backend/plugins/bitbucket/e2e/snapshot_tables/board_issues.csv index 2d65162ccb1..cc77559104d 100644 --- a/backend/plugins/bitbucket/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/bitbucket/e2e/snapshot_tables/board_issues.csv @@ -1,31 +1,31 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:1,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,60, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:10,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,52, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:11,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,51, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:12,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,50, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:13,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,49, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:14,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,48, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:15,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,47, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:16,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,46, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:17,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,45, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:18,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,44, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:19,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,43, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:2,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,59, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:20,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,42, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:21,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,41, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:22,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,40, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:23,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,39, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:24,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,38, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:25,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,36, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:26,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,35, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:27,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,31, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:28,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,34, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:29,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,33, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:3,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,58, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:30,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,32, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:4,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,57, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:5,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,56, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:6,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,37, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:7,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,55, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:8,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,54, -bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:9,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,53, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:1,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,60,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:10,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,52,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:11,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,51,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:12,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,50,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:13,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,49,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:14,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,48,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:15,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,47,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:16,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,46,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:17,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,45,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:18,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,44,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:19,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,43,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:2,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,59,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:20,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,42,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:21,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,41,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:22,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,40,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:23,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,39,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:24,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,38,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:25,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,36,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:26,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,35,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:27,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,31,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:28,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,34,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:29,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,33,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:3,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,58,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:30,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,32,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:4,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,57,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:5,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,56,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:6,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,37,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:7,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,55,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:8,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,54,, +bitbucket:BitbucketRepo:1:likyh/likyhphp,bitbucket:BitbucketIssue:1:likyh/likyhphp:9,"{""ConnectionId"":1,""FullName"":""likyh/likyhphp""}",_raw_bitbucket_api_issues,53,, diff --git a/backend/plugins/customize/e2e/snapshot_tables/board_issues.csv b/backend/plugins/customize/e2e/snapshot_tables/board_issues.csv index dac06627be7..cb778b24588 100644 --- a/backend/plugins/customize/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/customize/e2e/snapshot_tables/board_issues.csv @@ -1,9 +1,9 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -csv-board,csv:1,,,0, -csv-board,csv:10,,,0, -csv-board,csv:11,,,0, -csv-board,csv:12,,,0, -csv-board,csv:13,,,0, -csv-board,csv:14,,,0, -csv-board2,csv:1,,,0, -csv-board2,csv:10,,,0, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +csv-board,csv:1,,,0,, +csv-board,csv:10,,,0,, +csv-board,csv:11,,,0,, +csv-board,csv:12,,,0,, +csv-board,csv:13,,,0,, +csv-board,csv:14,,,0,, +csv-board2,csv:1,,,0,, +csv-board2,csv:10,,,0,, diff --git a/backend/plugins/github/e2e/snapshot_tables/board_issues.csv b/backend/plugins/github/e2e/snapshot_tables/board_issues.csv index 9ee6ea40cee..e137197d18c 100644 --- a/backend/plugins/github/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/github/e2e/snapshot_tables/board_issues.csv @@ -1,27 +1,27 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -github:GithubRepo:1:134018330,github:GithubIssue:1:346842831,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,11, -github:GithubRepo:1:134018330,github:GithubIssue:1:347255859,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,12, -github:GithubRepo:1:134018330,github:GithubIssue:1:348630179,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,13, -github:GithubRepo:1:134018330,github:GithubIssue:1:356703393,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,16, -github:GithubRepo:1:134018330,github:GithubIssue:1:364361014,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,18, -github:GithubRepo:1:134018330,github:GithubIssue:1:381941219,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,23, -github:GithubRepo:1:134018330,github:GithubIssue:1:382039050,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,24, -github:GithubRepo:1:134018330,github:GithubIssue:1:382574800,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,26, -github:GithubRepo:1:134018330,github:GithubIssue:1:388907811,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,27, -github:GithubRepo:1:134018330,github:GithubIssue:1:401277739,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,28, -github:GithubRepo:1:134018330,github:GithubIssue:1:402513849,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,30, -github:GithubRepo:1:134018330,github:GithubIssue:1:405951301,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,31, -github:GithubRepo:1:134018330,github:GithubIssue:1:413968505,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,32, -github:GithubRepo:1:134018330,github:GithubIssue:1:419183961,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,33, -github:GithubRepo:1:134018330,github:GithubIssue:1:419268851,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,34, -github:GithubRepo:1:134018330,github:GithubIssue:1:424634533,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,35, -github:GithubRepo:1:134018330,github:GithubIssue:1:429972115,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,37, -github:GithubRepo:1:134018330,github:GithubIssue:1:433564955,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,38, -github:GithubRepo:1:134018330,github:GithubIssue:1:434069015,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,39, -github:GithubRepo:1:134018330,github:GithubIssue:1:435486645,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,40, -github:GithubRepo:1:134018330,github:GithubIssue:1:461280653,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,41, -github:GithubRepo:1:134018330,github:GithubIssue:1:462631417,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,43, -github:GithubRepo:1:134018330,github:GithubIssue:1:472125082,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,44, -github:GithubRepo:1:134018330,github:GithubIssue:1:483164833,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,48, -github:GithubRepo:1:134018330,github:GithubIssue:1:483736247,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,49, -github:GithubRepo:1:134018330,github:GithubIssue:1:484311063,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,50, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +github:GithubRepo:1:134018330,github:GithubIssue:1:346842831,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,11,, +github:GithubRepo:1:134018330,github:GithubIssue:1:347255859,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,12,, +github:GithubRepo:1:134018330,github:GithubIssue:1:348630179,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,13,, +github:GithubRepo:1:134018330,github:GithubIssue:1:356703393,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,16,, +github:GithubRepo:1:134018330,github:GithubIssue:1:364361014,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,18,, +github:GithubRepo:1:134018330,github:GithubIssue:1:381941219,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,23,, +github:GithubRepo:1:134018330,github:GithubIssue:1:382039050,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,24,, +github:GithubRepo:1:134018330,github:GithubIssue:1:382574800,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,26,, +github:GithubRepo:1:134018330,github:GithubIssue:1:388907811,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,27,, +github:GithubRepo:1:134018330,github:GithubIssue:1:401277739,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,28,, +github:GithubRepo:1:134018330,github:GithubIssue:1:402513849,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,30,, +github:GithubRepo:1:134018330,github:GithubIssue:1:405951301,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,31,, +github:GithubRepo:1:134018330,github:GithubIssue:1:413968505,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,32,, +github:GithubRepo:1:134018330,github:GithubIssue:1:419183961,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,33,, +github:GithubRepo:1:134018330,github:GithubIssue:1:419268851,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,34,, +github:GithubRepo:1:134018330,github:GithubIssue:1:424634533,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,35,, +github:GithubRepo:1:134018330,github:GithubIssue:1:429972115,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,37,, +github:GithubRepo:1:134018330,github:GithubIssue:1:433564955,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,38,, +github:GithubRepo:1:134018330,github:GithubIssue:1:434069015,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,39,, +github:GithubRepo:1:134018330,github:GithubIssue:1:435486645,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,40,, +github:GithubRepo:1:134018330,github:GithubIssue:1:461280653,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,41,, +github:GithubRepo:1:134018330,github:GithubIssue:1:462631417,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,43,, +github:GithubRepo:1:134018330,github:GithubIssue:1:472125082,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,44,, +github:GithubRepo:1:134018330,github:GithubIssue:1:483164833,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,48,, +github:GithubRepo:1:134018330,github:GithubIssue:1:483736247,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,49,, +github:GithubRepo:1:134018330,github:GithubIssue:1:484311063,"{""ConnectionId"":1,""Name"":""panjf2000/ants""}",_raw_github_api_issues,50,, diff --git a/backend/plugins/gitlab/e2e/snapshot_tables/board_issues.csv b/backend/plugins/gitlab/e2e/snapshot_tables/board_issues.csv index fe844ef419a..d8527ba8767 100644 --- a/backend/plugins/gitlab/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/gitlab/e2e/snapshot_tables/board_issues.csv @@ -1,17 +1,17 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:106802088,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,15, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:108497826,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,16, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:22097949,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,1, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23413488,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,2, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23577209,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,3, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23812433,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,4, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23816268,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,5, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:24172103,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,6, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:32460839,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,7, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:32667832,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,8, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:33004486,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,9, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:47175437,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,10, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:59444322,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,11, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:69025114,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,12, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:89051403,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,13, -gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:91069461,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,14, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:106802088,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,15,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:108497826,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,16,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:22097949,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,1,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23413488,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,2,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23577209,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,3,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23812433,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,4,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:23816268,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,5,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:24172103,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,6,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:32460839,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,7,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:32667832,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,8,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:33004486,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,9,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:47175437,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,10,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:59444322,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,11,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:69025114,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,12,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:89051403,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,13,, +gitlab:GitlabProject:1:12345678,gitlab:GitlabIssue:1:91069461,"{""ConnectionId"":1,""ProjectId"":12345678}",_raw_gitlab_api_issues,14,, diff --git a/backend/plugins/grafana_irm/e2e/snapshot_tables/board_issues.csv b/backend/plugins/grafana_irm/e2e/snapshot_tables/board_issues.csv index a3e5652d292..3ab2a0be196 100644 --- a/backend/plugins/grafana_irm/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/grafana_irm/e2e/snapshot_tables/board_issues.csv @@ -1,5 +1,5 @@ -board_id,issue_id -grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:4 -grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:5 -grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:6 -grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:7 +board_id,issue_id,project_name +grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:4, +grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:5, +grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:6, +grafana_irm:GrafanaIrmScope:1:default,grafana_irm:Incident:1:7, diff --git a/backend/plugins/incidentio/e2e/snapshot_tables/board_issues.csv b/backend/plugins/incidentio/e2e/snapshot_tables/board_issues.csv index 9d6a8d037bf..6d30d2a3abf 100644 --- a/backend/plugins/incidentio/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/incidentio/e2e/snapshot_tables/board_issues.csv @@ -1,4 +1,4 @@ -board_id,issue_id -incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_01 -incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_02 -incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_04 +board_id,issue_id,project_name +incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_01, +incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_02, +incidentio:IncidentType:1:type_01,incidentio:Incident:1:inc_04, diff --git a/backend/plugins/jira/e2e/snapshot_tables/board_issues.csv b/backend/plugins/jira/e2e/snapshot_tables/board_issues.csv index 79e10b4b19e..d013a357f7e 100644 --- a/backend/plugins/jira/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/jira/e2e/snapshot_tables/board_issues.csv @@ -1,31 +1,31 @@ -board_id,issue_id -jira:JiraBoard:2:8,jira:JiraIssue:2:10063 -jira:JiraBoard:2:8,jira:JiraIssue:2:10064 -jira:JiraBoard:2:8,jira:JiraIssue:2:10065 -jira:JiraBoard:2:8,jira:JiraIssue:2:10066 -jira:JiraBoard:2:8,jira:JiraIssue:2:10067 -jira:JiraBoard:2:8,jira:JiraIssue:2:10068 -jira:JiraBoard:2:8,jira:JiraIssue:2:10070 -jira:JiraBoard:2:8,jira:JiraIssue:2:10071 -jira:JiraBoard:2:8,jira:JiraIssue:2:10072 -jira:JiraBoard:2:8,jira:JiraIssue:2:10076 -jira:JiraBoard:2:8,jira:JiraIssue:2:10077 -jira:JiraBoard:2:8,jira:JiraIssue:2:10078 -jira:JiraBoard:2:8,jira:JiraIssue:2:10079 -jira:JiraBoard:2:8,jira:JiraIssue:2:10081 -jira:JiraBoard:2:8,jira:JiraIssue:2:10082 -jira:JiraBoard:2:8,jira:JiraIssue:2:10085 -jira:JiraBoard:2:8,jira:JiraIssue:2:10086 -jira:JiraBoard:2:8,jira:JiraIssue:2:10087 -jira:JiraBoard:2:8,jira:JiraIssue:2:10088 -jira:JiraBoard:2:8,jira:JiraIssue:2:10089 -jira:JiraBoard:2:8,jira:JiraIssue:2:10090 -jira:JiraBoard:2:8,jira:JiraIssue:2:10091 -jira:JiraBoard:2:8,jira:JiraIssue:2:10092 -jira:JiraBoard:2:8,jira:JiraIssue:2:10093 -jira:JiraBoard:2:8,jira:JiraIssue:2:10094 -jira:JiraBoard:2:8,jira:JiraIssue:2:10095 -jira:JiraBoard:2:8,jira:JiraIssue:2:10096 -jira:JiraBoard:2:8,jira:JiraIssue:2:10097 -jira:JiraBoard:2:8,jira:JiraIssue:2:10098 -jira:JiraBoard:2:8,jira:JiraIssue:2:10099 +board_id,issue_id,project_name +jira:JiraBoard:2:8,jira:JiraIssue:2:10063, +jira:JiraBoard:2:8,jira:JiraIssue:2:10064, +jira:JiraBoard:2:8,jira:JiraIssue:2:10065, +jira:JiraBoard:2:8,jira:JiraIssue:2:10066, +jira:JiraBoard:2:8,jira:JiraIssue:2:10067, +jira:JiraBoard:2:8,jira:JiraIssue:2:10068, +jira:JiraBoard:2:8,jira:JiraIssue:2:10070, +jira:JiraBoard:2:8,jira:JiraIssue:2:10071, +jira:JiraBoard:2:8,jira:JiraIssue:2:10072, +jira:JiraBoard:2:8,jira:JiraIssue:2:10076, +jira:JiraBoard:2:8,jira:JiraIssue:2:10077, +jira:JiraBoard:2:8,jira:JiraIssue:2:10078, +jira:JiraBoard:2:8,jira:JiraIssue:2:10079, +jira:JiraBoard:2:8,jira:JiraIssue:2:10081, +jira:JiraBoard:2:8,jira:JiraIssue:2:10082, +jira:JiraBoard:2:8,jira:JiraIssue:2:10085, +jira:JiraBoard:2:8,jira:JiraIssue:2:10086, +jira:JiraBoard:2:8,jira:JiraIssue:2:10087, +jira:JiraBoard:2:8,jira:JiraIssue:2:10088, +jira:JiraBoard:2:8,jira:JiraIssue:2:10089, +jira:JiraBoard:2:8,jira:JiraIssue:2:10090, +jira:JiraBoard:2:8,jira:JiraIssue:2:10091, +jira:JiraBoard:2:8,jira:JiraIssue:2:10092, +jira:JiraBoard:2:8,jira:JiraIssue:2:10093, +jira:JiraBoard:2:8,jira:JiraIssue:2:10094, +jira:JiraBoard:2:8,jira:JiraIssue:2:10095, +jira:JiraBoard:2:8,jira:JiraIssue:2:10096, +jira:JiraBoard:2:8,jira:JiraIssue:2:10097, +jira:JiraBoard:2:8,jira:JiraIssue:2:10098, +jira:JiraBoard:2:8,jira:JiraIssue:2:10099, diff --git a/backend/plugins/linear/e2e/snapshot_tables/board_issues.csv b/backend/plugins/linear/e2e/snapshot_tables/board_issues.csv index 9a7d32f9477..5aa5e670d95 100644 --- a/backend/plugins/linear/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/linear/e2e/snapshot_tables/board_issues.csv @@ -1,6 +1,6 @@ -board_id,issue_id -linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-1 -linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-2 -linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-3 -linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-4 -linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-5 +board_id,issue_id,project_name +linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-1, +linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-2, +linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-3, +linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-4, +linear:LinearTeam:1:team-1,linear:LinearIssue:1:issue-5, diff --git a/backend/plugins/rootly/e2e/snapshot_tables/board_issues.csv b/backend/plugins/rootly/e2e/snapshot_tables/board_issues.csv index 654e122979e..12428eb25a2 100644 --- a/backend/plugins/rootly/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/rootly/e2e/snapshot_tables/board_issues.csv @@ -1,6 +1,6 @@ -board_id,issue_id -rootly:Service:1:svc_01,rootly:Incident:1:inc_01 -rootly:Service:1:svc_01,rootly:Incident:1:inc_02 -rootly:Service:1:svc_01,rootly:Incident:1:inc_03 -rootly:Service:1:svc_01,rootly:Incident:1:inc_05 -rootly:Service:1:svc_01,rootly:Incident:1:inc_06 +board_id,issue_id,project_name +rootly:Service:1:svc_01,rootly:Incident:1:inc_01, +rootly:Service:1:svc_01,rootly:Incident:1:inc_02, +rootly:Service:1:svc_01,rootly:Incident:1:inc_03, +rootly:Service:1:svc_01,rootly:Incident:1:inc_05, +rootly:Service:1:svc_01,rootly:Incident:1:inc_06, diff --git a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_epics.csv b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_epics.csv index 118e5665334..b92587d5426 100644 --- a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_epics.csv +++ b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_epics.csv @@ -1,3 +1,3 @@ -board_id,issue_id -taiga:TaigaProject:1:1,taiga:TaigaEpic:1:401 -taiga:TaigaProject:1:1,taiga:TaigaEpic:1:402 +board_id,issue_id,project_name +taiga:TaigaProject:1:1,taiga:TaigaEpic:1:401, +taiga:TaigaProject:1:1,taiga:TaigaEpic:1:402, diff --git a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_issues.csv b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_issues.csv index afc67821289..5c2abf6a6c5 100644 --- a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_issues.csv +++ b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_issues.csv @@ -1,3 +1,3 @@ -board_id,issue_id -taiga:TaigaProject:1:1,taiga:TaigaIssue:1:301 -taiga:TaigaProject:1:1,taiga:TaigaIssue:1:302 +board_id,issue_id,project_name +taiga:TaigaProject:1:1,taiga:TaigaIssue:1:301, +taiga:TaigaProject:1:1,taiga:TaigaIssue:1:302, diff --git a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_tasks.csv b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_tasks.csv index 0dddfd05a27..791151c1065 100644 --- a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_tasks.csv +++ b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_tasks.csv @@ -1,3 +1,3 @@ -board_id,issue_id -taiga:TaigaProject:1:1,taiga:TaigaTask:1:201 -taiga:TaigaProject:1:1,taiga:TaigaTask:1:202 +board_id,issue_id,project_name +taiga:TaigaProject:1:1,taiga:TaigaTask:1:201, +taiga:TaigaProject:1:1,taiga:TaigaTask:1:202, diff --git a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_user_stories.csv b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_user_stories.csv index d54c4ccb5e0..6edc42251b4 100644 --- a/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_user_stories.csv +++ b/backend/plugins/taiga/e2e/snapshot_tables/board_issues_from_user_stories.csv @@ -1,3 +1,3 @@ -board_id,issue_id -taiga:TaigaProject:1:1,taiga:TaigaUserStory:1:101 -taiga:TaigaProject:1:1,taiga:TaigaUserStory:1:102 +board_id,issue_id,project_name +taiga:TaigaProject:1:1,taiga:TaigaUserStory:1:101, +taiga:TaigaProject:1:1,taiga:TaigaUserStory:1:102, diff --git a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_bug.csv b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_bug.csv index 67879dad382..de09ba1f83d 100644 --- a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_bug.csv +++ b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_bug.csv @@ -1,21 +1,21 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001025,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1271, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001030,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1272, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001031,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1273, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001032,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1274, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001417,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1275, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001418,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1276, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001420,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1277, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001422,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1278, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001423,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1279, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001425,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1280, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001426,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1281, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001447,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1282, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001448,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1283, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001450,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1284, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001451,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1285, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001710,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1286, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001711,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1287, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001737,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1288, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001739,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1289, -tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001740,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1290, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001025,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1271,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001030,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1272,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001031,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1273,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001032,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1274,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001417,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1275,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001418,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1276,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001420,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1277,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001422,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1278,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001423,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1279,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001425,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1280,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001426,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1281,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001447,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1282,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001448,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1283,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001450,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1284,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001451,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1285,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001710,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1286,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001711,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1287,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001737,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1288,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001739,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1289,, +tapd:TapdWorkspace:1:991,tapd:TapdBug:1:11991001001740,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_bugs,1290,, diff --git a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_story.csv b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_story.csv index 311b443d966..8c70388d11d 100644 --- a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_story.csv +++ b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_story.csv @@ -1,21 +1,21 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037563,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,1, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037696,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,2, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037697,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,3, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038322,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,4, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038323,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,5, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038697,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,6, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038911,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,7, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038912,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,8, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001039664,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,9, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001039673,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,10, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001040086,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,11, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001040088,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,12, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041163,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,13, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041164,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,14, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041165,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,15, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041166,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,16, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041788,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,17, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041789,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,18, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041899,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,19, -tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041900,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,20, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037563,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,1,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037696,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,2,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001037697,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,3,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038322,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,4,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038323,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,5,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038697,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,6,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038911,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,7,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001038912,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,8,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001039664,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,9,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001039673,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,10,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001040086,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,11,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001040088,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,12,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041163,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,13,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041164,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,14,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041165,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,15,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041166,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,16,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041788,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,17,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041789,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,18,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041899,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,19,, +tapd:TapdWorkspace:1:991,tapd:TapdStory:1:11991001041900,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_stories,20,, diff --git a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_task.csv b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_task.csv index c90f507f2ef..d7920e22b1a 100644 --- a/backend/plugins/tapd/e2e/snapshot_tables/board_issues_task.csv +++ b/backend/plugins/tapd/e2e/snapshot_tables/board_issues_task.csv @@ -1,16 +1,16 @@ -board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015107,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4339, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015121,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4340, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015142,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4341, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015184,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4342, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015203,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4343, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015207,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4344, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015253,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4345, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015307,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4346, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015309,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4347, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015340,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4348, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015361,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4349, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015431,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4350, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015441,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4351, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015452,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4352, -tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015583,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4353, +board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015107,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4339,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015121,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4340,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015142,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4341,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015184,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4342,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015203,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4343,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015207,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4344,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015253,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4345,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015307,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4346,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015309,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4347,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015340,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4348,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015361,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4349,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015431,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4350,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015441,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4351,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015452,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4352,, +tapd:TapdWorkspace:1:991,tapd:TapdTask:1:11991001015583,"{""ConnectionId"":1,""WorkspaceId"":991}",_raw_tapd_api_tasks,4353,, diff --git a/backend/plugins/youtrack/e2e/snapshot_tables/board_issues.csv b/backend/plugins/youtrack/e2e/snapshot_tables/board_issues.csv index 13c5c1bc526..90908648ce1 100644 --- a/backend/plugins/youtrack/e2e/snapshot_tables/board_issues.csv +++ b/backend/plugins/youtrack/e2e/snapshot_tables/board_issues.csv @@ -1,16 +1,16 @@ -board_id,issue_id -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-1 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-135 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-137 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-170 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-191 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-195 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-26 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-40 -youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-44 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-233 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-345 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-384 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-387 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-388 -youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-399 +board_id,issue_id,project_name +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-1, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-135, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-137, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-170, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-191, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-195, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-26, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-40, +youtrack:YoutrackProject:1:0-1,youtrack:YoutrackIssue:1:2-44, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-233, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-345, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-384, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-387, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-388, +youtrack:YoutrackProject:1:0-2,youtrack:YoutrackIssue:1:2-399, diff --git a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv index 63c8fe5c8ef..5f4bb475f2c 100644 --- a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv +++ b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_bug.csv @@ -1,7 +1,7 @@ -board_id,issue_id -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:1 -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:2 -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:3 -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:4 -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:5 -zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:6 +board_id,issue_id,project_name +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:1, +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:2, +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:3, +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:4, +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:5, +zentao:ZentaoProject:1:1,zentao:ZentaoBug:1:6, diff --git a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv index 6f93edb6348..42ab55d1a1c 100644 --- a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv +++ b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_story.csv @@ -1,10 +1,10 @@ -board_id,issue_id -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:1 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:2 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:3 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:4 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:5 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:6 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:7 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:8 -zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:9 +board_id,issue_id,project_name +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:1, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:2, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:3, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:4, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:5, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:6, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:7, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:8, +zentao:ZentaoProject:1:1,zentao:ZentaoStory:1:9, diff --git a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv index 8232c652e75..863dd9e001e 100644 --- a/backend/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv +++ b/backend/plugins/zentao/e2e/snapshot_tables/board_issues_task.csv @@ -1,4 +1,4 @@ -board_id,issue_id -zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:1 -zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:2 -zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:3 +board_id,issue_id,project_name +zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:1, +zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:2, +zentao:ZentaoProject:1:1,zentao:ZentaoTask:1:3, From 9c64e2baeee54cf6b5879adea86fa47d4e94fad3 Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 2/6] feat(jira): record the collecting project on board issues Add project_name to _tool_jira_board_issues (part of the primary key) and ProjectName to JiraApiParams. The issue collector and extractor stamp the DevLake project the pipeline runs for, so a board shared by several projects keeps each project's ExtraJQL slice apart. Co-Authored-By: Claude Opus 5.5 --- .../_tool_jira_board_issues.csv | 62 +++++++-------- ...l_jira_board_issues_for_external_epics.csv | 8 +- backend/plugins/jira/models/board.go | 3 + backend/plugins/jira/models/board_issue.go | 1 + ...0260710_add_project_name_to_board_issue.go | 46 +++++++++++ .../jira/models/migrationscripts/register.go | 1 + backend/plugins/jira/tasks/issue_collector.go | 1 + backend/plugins/jira/tasks/issue_extractor.go | 2 + .../jira/tasks/issue_extractor_test.go | 78 +++++++++++++++++++ 9 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 backend/plugins/jira/models/migrationscripts/20260710_add_project_name_to_board_issue.go diff --git a/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues.csv b/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues.csv index 9ac08ef7b40..36e400b407c 100644 --- a/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues.csv +++ b/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues.csv @@ -1,32 +1,32 @@ -connection_id,board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark -2,8,10063,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12441, -2,8,10064,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12442, -2,8,10065,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12443, -2,8,10066,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12444, -2,8,10067,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12445, -2,8,10068,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12446, -2,8,10070,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12447, -2,8,10071,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12448, -2,8,10072,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12449, -2,8,10076,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12450, -2,8,10077,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12451, -2,8,10078,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12452, -2,8,10079,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12453, -2,8,10081,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12454, -2,8,10082,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12455, -2,8,10085,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12456, -2,8,10086,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12457, -2,8,10087,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12458, -2,8,10088,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12459, -2,8,10089,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12460, -2,8,10090,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12461, -2,8,10091,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12462, -2,8,10092,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12463, -2,8,10093,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12464, -2,8,10094,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12465, -2,8,10095,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12466, -2,8,10096,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12467, -2,8,10097,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12468, -2,8,10098,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12469, -2,8,10099,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12470, +connection_id,board_id,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_name +2,8,10063,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12441,, +2,8,10064,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12442,, +2,8,10065,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12443,, +2,8,10066,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12444,, +2,8,10067,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12445,, +2,8,10068,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12446,, +2,8,10070,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12447,, +2,8,10071,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12448,, +2,8,10072,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12449,, +2,8,10076,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12450,, +2,8,10077,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12451,, +2,8,10078,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12452,, +2,8,10079,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12453,, +2,8,10081,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12454,, +2,8,10082,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12455,, +2,8,10085,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12456,, +2,8,10086,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12457,, +2,8,10087,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12458,, +2,8,10088,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12459,, +2,8,10089,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12460,, +2,8,10090,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12461,, +2,8,10091,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12462,, +2,8,10092,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12463,, +2,8,10093,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12464,, +2,8,10094,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12465,, +2,8,10095,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12466,, +2,8,10096,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12467,, +2,8,10097,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12468,, +2,8,10098,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12469,, +2,8,10099,"{""ConnectionId"":2,""BoardId"":8}",_raw_jira_api_issues,12470,, diff --git a/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv b/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv index cd0504917e8..019c1fcb971 100644 --- a/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv +++ b/backend/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv @@ -1,4 +1,4 @@ -connection_id,board_id,issue_id -1,93,20708 -1,93,20709 -1,93,20710 +connection_id,board_id,issue_id,project_name +1,93,20708, +1,93,20709, +1,93,20710, diff --git a/backend/plugins/jira/models/board.go b/backend/plugins/jira/models/board.go index 93f3a8a0469..d79cf9aa845 100644 --- a/backend/plugins/jira/models/board.go +++ b/backend/plugins/jira/models/board.go @@ -63,4 +63,7 @@ func (JiraBoard) TableName() string { type JiraApiParams struct { ConnectionId uint64 BoardId uint64 + // ProjectName is omitted from the serialized params when empty, so raw data + // collected without a project keeps its existing params. + ProjectName string `json:",omitempty"` } diff --git a/backend/plugins/jira/models/board_issue.go b/backend/plugins/jira/models/board_issue.go index ca7a8effdd8..285ad06d501 100644 --- a/backend/plugins/jira/models/board_issue.go +++ b/backend/plugins/jira/models/board_issue.go @@ -25,6 +25,7 @@ type JiraBoardIssue struct { ConnectionId uint64 `gorm:"primaryKey"` BoardId uint64 `gorm:"primaryKey"` IssueId uint64 `gorm:"primaryKey"` + ProjectName string `gorm:"primaryKey;type:varchar(255);default:''"` common.NoPKModel } diff --git a/backend/plugins/jira/models/migrationscripts/20260710_add_project_name_to_board_issue.go b/backend/plugins/jira/models/migrationscripts/20260710_add_project_name_to_board_issue.go new file mode 100644 index 00000000000..b820b68304f --- /dev/null +++ b/backend/plugins/jira/models/migrationscripts/20260710_add_project_name_to_board_issue.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 migrationscripts + +import ( + "github.com/apache/devlake/core/context" + "github.com/apache/devlake/core/errors" + "github.com/apache/devlake/helpers/migrationhelper" +) + +type JiraBoardIssue20260710 struct { + ProjectName string `gorm:"primaryKey;type:varchar(255);default:''"` +} + +func (JiraBoardIssue20260710) TableName() string { + return "_tool_jira_board_issues" +} + +type addProjectNameToBoardIssue struct{} + +func (script *addProjectNameToBoardIssue) Up(basicRes context.BasicRes) errors.Error { + return migrationhelper.AutoMigrateTables(basicRes, &JiraBoardIssue20260710{}) +} + +func (*addProjectNameToBoardIssue) Version() uint64 { + return 20260710000000 +} + +func (*addProjectNameToBoardIssue) Name() string { + return "add project_name to _tool_jira_board_issues" +} diff --git a/backend/plugins/jira/models/migrationscripts/register.go b/backend/plugins/jira/models/migrationscripts/register.go index 7716da342e9..c1a6efbc946 100644 --- a/backend/plugins/jira/models/migrationscripts/register.go +++ b/backend/plugins/jira/models/migrationscripts/register.go @@ -61,5 +61,6 @@ func All() []plugin.MigrationScript { new(addSprintReportTable), new(addRawDataColumnsToSprintReport), new(addJiraOAuth20260907), + new(addProjectNameToBoardIssue), } } diff --git a/backend/plugins/jira/tasks/issue_collector.go b/backend/plugins/jira/tasks/issue_collector.go index 20d4e38dda5..056346947de 100644 --- a/backend/plugins/jira/tasks/issue_collector.go +++ b/backend/plugins/jira/tasks/issue_collector.go @@ -56,6 +56,7 @@ func CollectIssues(taskCtx plugin.SubTaskContext) errors.Error { Params: JiraApiParams{ ConnectionId: data.Options.ConnectionId, BoardId: data.Options.BoardId, + ProjectName: data.Options.ProjectName, }, Table: RAW_ISSUE_TABLE, }) diff --git a/backend/plugins/jira/tasks/issue_extractor.go b/backend/plugins/jira/tasks/issue_extractor.go index f1db6c76d1d..173416aa8c7 100644 --- a/backend/plugins/jira/tasks/issue_extractor.go +++ b/backend/plugins/jira/tasks/issue_extractor.go @@ -69,6 +69,7 @@ func ExtractIssues(subtaskCtx plugin.SubTaskContext) errors.Error { Params: JiraApiParams{ ConnectionId: data.Options.ConnectionId, BoardId: data.Options.BoardId, + ProjectName: data.Options.ProjectName, }, SubtaskConfig: map[string]any{ "typeMappings": mappings, @@ -216,6 +217,7 @@ func extractIssues(data *JiraTaskData, mappings *typeMappings, apiIssue *apiv2mo ConnectionId: data.Options.ConnectionId, BoardId: data.Options.BoardId, IssueId: issue.IssueId, + ProjectName: data.Options.ProjectName, }) // labels labels := apiIssue.Fields.Labels diff --git a/backend/plugins/jira/tasks/issue_extractor_test.go b/backend/plugins/jira/tasks/issue_extractor_test.go index 72628a6962e..9d6508acf4b 100644 --- a/backend/plugins/jira/tasks/issue_extractor_test.go +++ b/backend/plugins/jira/tasks/issue_extractor_test.go @@ -97,6 +97,84 @@ func TestExtractIssuesEpicKeyField(t *testing.T) { } } +// TestExtractIssuesStampsProjectName verifies the earlier (tool) layer of the +// per-project isolation feature: when the Jira pipeline runs for a DevLake +// project, the extractor must stamp that project's name onto the +// _tool_jira_board_issues row. This is what later lets a shared board's issues +// be partitioned per project so DORA metrics (e.g. Change Failure Rate) only +// count the project's own slice. +// +// It calls the REAL extractIssues function (no DB, no Docker) and asserts the +// JiraBoardIssue it produces carries the expected project_name. +func TestExtractIssuesStampsProjectName(t *testing.T) { + mappings := &typeMappings{ + TypeIdMappings: map[string]string{}, + StdTypeMappings: map[string]string{}, + StandardStatusMappings: map[string]models.StatusMappings{}, + } + + tests := []struct { + name string + projectName string + wantProjectName string + }{ + { + name: "stamps the collecting project's name", + projectName: "ProjectA", + wantProjectName: "ProjectA", + }, + { + name: "empty project name is preserved (legacy single-project pipeline)", + projectName: "", + wantProjectName: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + raw := minimalIssueJSON("") + var apiIssue apiv2models.Issue + if err := json.Unmarshal(raw, &apiIssue); err != nil { + t.Fatalf("unmarshal issue: %v", err) + } + + data := &JiraTaskData{ + Options: &JiraOptions{ + ConnectionId: 1, + BoardId: 7, + ProjectName: tt.projectName, + ScopeConfig: &models.JiraScopeConfig{}, + }, + ProjectName: tt.projectName, + } + results, err := extractIssues(data, mappings, &apiIssue, &api.RawData{Data: raw}, nil) + if err != nil { + t.Fatalf("extractIssues() error = %v", err) + } + boardIssue := jiraBoardIssueFromResults(results) + if boardIssue == nil { + t.Fatal("extractIssues() did not return a JiraBoardIssue") + } + if boardIssue.ProjectName != tt.wantProjectName { + t.Errorf("JiraBoardIssue.ProjectName = %q, want %q", boardIssue.ProjectName, tt.wantProjectName) + } + // The board/issue identity must still be set correctly alongside the stamp. + if boardIssue.BoardId != 7 { + t.Errorf("JiraBoardIssue.BoardId = %d, want 7", boardIssue.BoardId) + } + }) + } +} + +func jiraBoardIssueFromResults(results []interface{}) *models.JiraBoardIssue { + for _, r := range results { + if bi, ok := r.(*models.JiraBoardIssue); ok { + return bi + } + } + return nil +} + func minimalIssueJSON(fieldsExtra string) []byte { if fieldsExtra != "" { fieldsExtra = "," + fieldsExtra From 5d7e63ee19447dffe8761327f57c70b25826b1c1 Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 3/6] feat(jira): convert only the current project's slice of a shared board The issue convertor used to read every _tool_jira_board_issues row of the board, so on a shared board one project's run converted (and stamped) the other projects' issues too. Filter its input by project_name and stamp the project on the domain board_issues row. Boards without any per-project stamps (single-project or pre-feature data) keep converting all of their issues. On a board that has stamps, unstamped leftover rows are excluded rather than attributed to every project. Adds gorm.io/driver/sqlite (already in go.sum) for the in-memory tests. Co-Authored-By: Claude Opus 5.5 --- backend/go.mod | 2 + backend/plugins/jira/tasks/issue_convertor.go | 56 ++++-- .../issue_convertor_project_filter_test.go | 171 ++++++++++++++++++ 3 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 backend/plugins/jira/tasks/issue_convertor_project_filter_test.go diff --git a/backend/go.mod b/backend/go.mod index 3b823467f9f..ea4e49775ba 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -36,6 +36,7 @@ require ( gorm.io/datatypes v1.2.7 gorm.io/driver/mysql v1.6.0 gorm.io/driver/postgres v1.6.3 + gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.2 ) @@ -100,6 +101,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/backend/plugins/jira/tasks/issue_convertor.go b/backend/plugins/jira/tasks/issue_convertor.go index 74a92328e73..8cbe126d19c 100644 --- a/backend/plugins/jira/tasks/issue_convertor.go +++ b/backend/plugins/jira/tasks/issue_convertor.go @@ -40,6 +40,43 @@ var ConvertIssuesMeta = plugin.SubTaskMeta{ DomainTypes: []string{plugin.DOMAIN_TYPE_TICKET}, } +// convertIssuesInputClauses selects the tool-layer issues that the convertor +// turns into domain issues/board_issues for one board. +// +// On a board SHARED by several DevLake projects, each project collected its own +// slice of the board (its ExtraJQL result) into _tool_jira_board_issues rows +// tagged with that project's name. The convertor therefore MUST filter by +// project_name as well as connection_id + board_id; otherwise a project's run +// would read every project's rows for the board and stamp them all with its own +// name, leaking issues across projects. +// +// The empty-project_name rows are always included so that pre-feature data +// (collected before project_name existed) and no-project pipelines keep working. +func convertIssuesInputClauses(connectionId uint64, boardId uint64, projectName string) []dal.Clause { + return []dal.Clause{ + dal.Select("_tool_jira_issues.*"), + dal.From("_tool_jira_issues"), + dal.Join(`left join _tool_jira_board_issues + on _tool_jira_board_issues.issue_id = _tool_jira_issues.issue_id + and _tool_jira_board_issues.connection_id = _tool_jira_issues.connection_id`), + dal.Where( + `_tool_jira_board_issues.connection_id = ? AND _tool_jira_board_issues.board_id = ? + AND ( + _tool_jira_board_issues.project_name = ? + OR NOT EXISTS ( + SELECT 1 FROM _tool_jira_board_issues b2 + WHERE b2.connection_id = _tool_jira_board_issues.connection_id + AND b2.board_id = _tool_jira_board_issues.board_id + AND b2.project_name IS NOT NULL AND b2.project_name <> '' + ) + )`, + connectionId, + boardId, + projectName, + ), + } +} + func ConvertIssues(subtaskCtx plugin.SubTaskContext) errors.Error { logger := subtaskCtx.GetLogger() data := subtaskCtx.GetData().(*JiraTaskData) @@ -61,22 +98,12 @@ func ConvertIssues(subtaskCtx plugin.SubTaskContext) errors.Error { Params: JiraApiParams{ ConnectionId: data.Options.ConnectionId, BoardId: data.Options.BoardId, + ProjectName: data.Options.ProjectName, }, SubtaskConfig: mappings, }, Input: func(stateManager *api.SubtaskStateManager) (dal.Rows, errors.Error) { - clauses := []dal.Clause{ - dal.Select("_tool_jira_issues.*"), - dal.From("_tool_jira_issues"), - dal.Join(`left join _tool_jira_board_issues - on _tool_jira_board_issues.issue_id = _tool_jira_issues.issue_id - and _tool_jira_board_issues.connection_id = _tool_jira_issues.connection_id`), - dal.Where( - "_tool_jira_board_issues.connection_id = ? AND _tool_jira_board_issues.board_id = ?", - data.Options.ConnectionId, - data.Options.BoardId, - ), - } + clauses := convertIssuesInputClauses(data.Options.ConnectionId, data.Options.BoardId, data.Options.ProjectName) if stateManager.IsIncremental() { since := stateManager.GetSince() if since != nil { @@ -155,8 +182,9 @@ func ConvertIssues(subtaskCtx plugin.SubTaskContext) errors.Error { result = append(result, issueAssignee) } boardIssue := &ticket.BoardIssue{ - BoardId: boardId, - IssueId: issue.Id, + BoardId: boardId, + IssueId: issue.Id, + ProjectName: data.ProjectName, } result = append(result, boardIssue) return result, nil diff --git a/backend/plugins/jira/tasks/issue_convertor_project_filter_test.go b/backend/plugins/jira/tasks/issue_convertor_project_filter_test.go new file mode 100644 index 00000000000..a26c0f05601 --- /dev/null +++ b/backend/plugins/jira/tasks/issue_convertor_project_filter_test.go @@ -0,0 +1,171 @@ +/* +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 ( + "sort" + "testing" + + "github.com/apache/devlake/core/dal" + "github.com/apache/devlake/impls/dalgorm" + "github.com/apache/devlake/plugins/jira/models" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// TestConvertIssuesInputProjectFilter reproduces (and then guards against) the +// root cause of "DORA metrics show the whole shared board instead of just the +// project's slice". +// +// The domain board_issues.project_name stamp is written by the convertor. The +// convertor reads its input from _tool_jira_board_issues. BEFORE the fix, that +// input query filtered only by connection_id + board_id -- so when ProjectA and +// ProjectB share a board, ProjectA's convertor run read BOTH projects' tool +// rows and stamped them all "ProjectA", leaking ProjectB's issues into +// ProjectA (and vice versa). The project-level DORA filter downstream was then +// meaningless because every issue carried the wrong project_name. +// +// This test seeds a shared board (board 10) whose issues were collected per +// project into _tool_jira_board_issues (A-slice tagged ProjectA, B-slice tagged +// ProjectB, plus a legacy row with empty project_name), then runs the REAL +// convertor input query (convertIssuesInputClauses) for each project and +// asserts it returns only that project's slice (+ the legacy row). On the old +// unfiltered query this test fails (ProjectA would also see issue 1002). +func TestConvertIssuesInputProjectFilter(t *testing.T) { + db := newJiraInMemoryDB(t) + + const connectionId = uint64(1) + const sharedBoard = uint64(10) + + // tool issues (the board-level identity does not distinguish project) + seedJiraIssue(t, db, connectionId, 1001) + seedJiraIssue(t, db, connectionId, 1002) + seedJiraIssue(t, db, connectionId, 1003) + + // per-project tool board-issue rows on the SAME board + seedJiraBoardIssue(t, db, connectionId, sharedBoard, 1001, "ProjectA") + seedJiraBoardIssue(t, db, connectionId, sharedBoard, 1002, "ProjectB") + // 1003 is unstamped (empty) on a board that already HAS real per-project + // stamps -> it is the ambiguous leftover pool and must NOT be attributed to + // any project (attributing it was the cross-project leak). + seedJiraBoardIssue(t, db, connectionId, sharedBoard, 1003, "") + + projectA := queryConvertIssueIds(t, db, connectionId, sharedBoard, "ProjectA") + projectB := queryConvertIssueIds(t, db, connectionId, sharedBoard, "ProjectB") + + // ProjectA converts ONLY its own slice, never ProjectB's, never the pool. + require.Equal(t, []uint64{1001}, projectA) + require.NotContains(t, projectA, uint64(1002), "ProjectA convertor must not read ProjectB's issue on a shared board") + require.NotContains(t, projectA, uint64(1003), "unstamped rows on a stamped board must be excluded") + + // ProjectB converts ONLY its own slice. + require.Equal(t, []uint64{1002}, projectB) + require.NotContains(t, projectB, uint64(1001)) + require.NotContains(t, projectB, uint64(1003)) +} + +// TestConvertIssuesInputExcludesNullPool mirrors the real staging data: ProjectA +// was collected (stamped) while an earlier unfiltered collection left tool rows +// with NULL project_name on the same board. ProjectA's convertor must read only +// its stamped slice; the NULL pool must be excluded (previously it leaked back +// in via the empty/NULL fallback). +func TestConvertIssuesInputExcludesNullPool(t *testing.T) { + db := newJiraInMemoryDB(t) + const connectionId = uint64(1) + const sharedBoard = uint64(10) + + seedJiraIssue(t, db, connectionId, 1) + seedJiraIssue(t, db, connectionId, 2) + seedJiraIssue(t, db, connectionId, 3) + seedJiraBoardIssue(t, db, connectionId, sharedBoard, 1, "ProjectA") + // leftover unfiltered pool with NULL project_name + seedJiraBoardIssueNull(t, db, connectionId, sharedBoard, 2) + seedJiraBoardIssueNull(t, db, connectionId, sharedBoard, 3) + + ids := queryConvertIssueIds(t, db, connectionId, sharedBoard, "ProjectA") + require.Equal(t, []uint64{1}, ids, "only ProjectA's stamped slice may convert; the NULL pool is excluded") +} + +// TestConvertIssuesInputNoProject is the control: a non-shared board with no +// project context (empty project_name) still converts all of its issues, so the +// fix does not regress the ordinary single-project case. +func TestConvertIssuesInputNoProject(t *testing.T) { + db := newJiraInMemoryDB(t) + const connectionId = uint64(1) + const board = uint64(20) + + seedJiraIssue(t, db, connectionId, 2001) + seedJiraIssue(t, db, connectionId, 2002) + seedJiraBoardIssue(t, db, connectionId, board, 2001, "") + seedJiraBoardIssue(t, db, connectionId, board, 2002, "") + + ids := queryConvertIssueIds(t, db, connectionId, board, "") + require.Equal(t, []uint64{2001, 2002}, ids) +} + +// --- helpers --------------------------------------------------------------- + +func newJiraInMemoryDB(t *testing.T) dal.Dal { + gormDb, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, gormDb.AutoMigrate(&models.JiraIssue{}, &models.JiraBoardIssue{})) + // start from a clean slate (shared in-memory cache can persist across tests) + require.NoError(t, gormDb.Exec("DELETE FROM _tool_jira_issues").Error) + require.NoError(t, gormDb.Exec("DELETE FROM _tool_jira_board_issues").Error) + return dalgorm.NewDalgorm(gormDb) +} + +func seedJiraIssue(t *testing.T, db dal.Dal, connectionId, issueId uint64) { + require.NoError(t, db.Create(&models.JiraIssue{ + ConnectionId: connectionId, + IssueId: issueId, + })) +} + +func seedJiraBoardIssue(t *testing.T, db dal.Dal, connectionId, boardId, issueId uint64, projectName string) { + require.NoError(t, db.Create(&models.JiraBoardIssue{ + ConnectionId: connectionId, + BoardId: boardId, + IssueId: issueId, + ProjectName: projectName, + })) +} + +func seedJiraBoardIssueNull(t *testing.T, db dal.Dal, connectionId, boardId, issueId uint64) { + // leftover unfiltered pool: project_name is SQL NULL (distinct from empty). + require.NoError(t, db.Exec( + "INSERT INTO _tool_jira_board_issues (connection_id, board_id, issue_id, project_name) VALUES (?, ?, ?, NULL)", + connectionId, boardId, issueId, + )) +} + +func queryConvertIssueIds(t *testing.T, db dal.Dal, connectionId, boardId uint64, projectName string) []uint64 { + cursor, err := db.Cursor(convertIssuesInputClauses(connectionId, boardId, projectName)...) + require.NoError(t, err) + defer cursor.Close() + + var ids []uint64 + for cursor.Next() { + row := &models.JiraIssue{} + require.NoError(t, db.Fetch(cursor, row)) + ids = append(ids, row.IssueId) + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + return ids +} From 59592b47407286f5d421c3d5e12eb6efa6b356ce Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 4/6] fix(jira): resync the board when its ExtraJQL changes The board filter collectors only compared the saved filter JQL to decide whether a full resync is needed. Fold the rendered ExtraJQL into that key so changing ExtraJQL also triggers a full resync, and issues that no longer match are unlinked from the board instead of lingering in board_issues. Co-Authored-By: Claude Opus 5.5 --- .../jira/tasks/board_filter_begin_collector.go | 11 +++++++++++ .../plugins/jira/tasks/board_filter_end_collector.go | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/backend/plugins/jira/tasks/board_filter_begin_collector.go b/backend/plugins/jira/tasks/board_filter_begin_collector.go index 6b064e2f669..6c6fcf2f11d 100644 --- a/backend/plugins/jira/tasks/board_filter_begin_collector.go +++ b/backend/plugins/jira/tasks/board_filter_begin_collector.go @@ -60,6 +60,17 @@ func CollectBoardFilterBegin(taskCtx plugin.SubTaskContext) errors.Error { logger.Info("collect board filter jql:%s", filterInfo.Jql) jql := filterInfo.Jql + // Fold the rendered ExtraJQL into the change-detection key. ExtraJQL filters + // which issues are collected; if it changes we must force a full resync so + // that issues no longer matching the filter are unlinked from the board + // (otherwise they linger in board_issues and keep appearing on dashboards). + if data.Options.ScopeConfig != nil && data.Options.ScopeConfig.ExtraJQL != "" { + renderedExtra, renderErr := renderExtraJQL(data.Options.ScopeConfig.ExtraJQL, data) + if renderErr != nil { + return renderErr + } + jql = fmt.Sprintf("%s AND (%s)", jql, renderedExtra) + } var record models.JiraBoard err = db.First(&record, dal.Where("connection_id = ? AND board_id = ? ", data.Options.ConnectionId, data.Options.BoardId)) if err != nil { diff --git a/backend/plugins/jira/tasks/board_filter_end_collector.go b/backend/plugins/jira/tasks/board_filter_end_collector.go index c9e898cca0b..3b65710f2f5 100644 --- a/backend/plugins/jira/tasks/board_filter_end_collector.go +++ b/backend/plugins/jira/tasks/board_filter_end_collector.go @@ -52,6 +52,17 @@ func CollectBoardFilterEnd(taskCtx plugin.SubTaskContext) errors.Error { return errors.Default.Wrap(err, fmt.Sprintf("error getting board filter jql for connection_id:%d board_id:%d", data.Options.ConnectionId, data.Options.BoardId)) } jql := filterInfo.Jql + // Fold the rendered ExtraJQL into the change-detection key. ExtraJQL filters + // which issues are collected; if it changes we must force a full resync so + // that issues no longer matching the filter are unlinked from the board + // (otherwise they linger in board_issues and keep appearing on dashboards). + if data.Options.ScopeConfig != nil && data.Options.ScopeConfig.ExtraJQL != "" { + renderedExtra, renderErr := renderExtraJQL(data.Options.ScopeConfig.ExtraJQL, data) + if renderErr != nil { + return renderErr + } + jql = fmt.Sprintf("%s AND (%s)", jql, renderedExtra) + } logger.Info("collect board filter jql:%s", jql) var record models.JiraBoard From 805ed9859ec92bdfd9b5b3f021eea15705d2b263 Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 5/6] feat(dora): scope incidents and deployment links to the project's slice ConvertIssuesToIncidents and ConnectIncidentToDeployment selected incidents by board-level project_mapping only, so every project sharing a board got every incident on it. Also filter on board_issues.project_name, falling back to the whole board when it has no per-project stamps. Co-Authored-By: Claude Opus 5.5 --- .../dora/tasks/incident_deploy_connector.go | 39 +++- .../tasks/incident_deploy_connector_test.go | 196 ++++++++++++++++++ .../tasks/incident_from_issue_generator.go | 88 ++++++-- .../incident_from_issue_generator_test.go | 169 +++++++++++++++ 4 files changed, 466 insertions(+), 26 deletions(-) create mode 100644 backend/plugins/dora/tasks/incident_deploy_connector_test.go create mode 100644 backend/plugins/dora/tasks/incident_from_issue_generator_test.go diff --git a/backend/plugins/dora/tasks/incident_deploy_connector.go b/backend/plugins/dora/tasks/incident_deploy_connector.go index 95351f12fa7..98d1eab6d7d 100644 --- a/backend/plugins/dora/tasks/incident_deploy_connector.go +++ b/backend/plugins/dora/tasks/incident_deploy_connector.go @@ -44,6 +44,37 @@ type simpleCicdDeploymentCommit struct { FinishedDate *time.Time } +// connectIncidentInScopeClauses selects the incidents that belong to a single +// DevLake project's slice of a (possibly shared) Jira board. +// +// An incident is scoped only by scope_id (= board id), so joining it to +// project_mapping alone matches EVERY project pointing at that board. When +// several projects share a board, that leaks one project's incidents into +// another's Change Failure Rate. We therefore additionally join board_issues +// (which carries the per-project slice stamped at collection time via ExtraJQL) +// and keep only the incidents whose underlying issue was collected for this +// project. The empty/NULL board_issues.project_name fallback preserves +// pre-feature / single-project data. +func connectIncidentInScopeClauses(db dal.Dal, projectName string) []dal.Clause { + return []dal.Clause{ + dal.From(`incidents i`), + dal.Join("join project_mapping pm on pm.row_id = i.scope_id and " + quoteTableColumn(db, "pm") + " = " + quoteTableColumn(db, "i")), + dal.Join(`left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id`), + dal.Where( + `pm.project_name = ? + and ( + bi.project_name = ? + or not exists ( + select 1 from board_issues b2 + where b2.board_id = i.scope_id + and b2.project_name is not null and b2.project_name <> '' + ) + )`, + projectName, projectName, + ), + } +} + // ConnectIncidentToDeployment will generate data to crossdomain.ProjectIncidentDeploymentRelationship. func ConnectIncidentToDeployment(taskCtx plugin.SubTaskContext) errors.Error { db := taskCtx.GetDal() @@ -55,12 +86,8 @@ func ConnectIncidentToDeployment(taskCtx plugin.SubTaskContext) errors.Error { return errors.Default.Wrap(err, "error deleting previous project_incident_deployment_relationships") } logger.Info("delete previous project_incident_deployment_relationships") - // select all issues belongs to the board - clauses := []dal.Clause{ - dal.From(`incidents i`), - dal.Join(`left join project_mapping pm on pm.row_id = i.scope_id and pm.table = i.table`), - dal.Where("pm.project_name = ?", data.Options.ProjectName), - } + // select all incidents belonging to this project's slice of the board + clauses := connectIncidentInScopeClauses(db, data.Options.ProjectName) //count, err := db.Count( // dal.From(`incidents i`), diff --git a/backend/plugins/dora/tasks/incident_deploy_connector_test.go b/backend/plugins/dora/tasks/incident_deploy_connector_test.go new file mode 100644 index 00000000000..7fea73a095c --- /dev/null +++ b/backend/plugins/dora/tasks/incident_deploy_connector_test.go @@ -0,0 +1,196 @@ +/* +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 ( + "sort" + "testing" + + "github.com/apache/devlake/core/dal" + "github.com/apache/devlake/core/models/domainlayer" + "github.com/apache/devlake/core/models/domainlayer/crossdomain" + "github.com/apache/devlake/core/models/domainlayer/ticket" + "github.com/apache/devlake/impls/dalgorm" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// TestConnectIncidentInScopeProjectFilter reproduces the bug behind "the board +// still contains incidents that relate to Jira issues that shouldn't be there". +// +// ConnectIncidentToDeployment builds project_incident_deployment_relationships +// (the table the Change Failure Rate metric reads). It used to select incidents +// by project_mapping on scope_id (= board) ONLY, so on a board shared by several +// projects, every incident on the board was attached to every project -- the +// incident objects leaked across projects even though the per-issue slice was +// correct. +// +// Shared board (board1), mapped to both ProjectA and ProjectB. Incidents carry +// scope_id = board1 (that is all an incident knows). The per-project slice lives +// on board_issues.project_name: +// +// incident/issue 1001 board_issues.project_name = ProjectA +// incident/issue 1002 board_issues.project_name = ProjectB +// incident/issue 1003 board_issues.project_name = '' (legacy) +// +// Expected selection: +// ProjectA -> {1001, 1003} (never 1002) +// ProjectB -> {1002, 1003} (never 1001) +// +// On the OLD board-only query, ProjectA would also select 1002 -> the leak. +func TestConnectIncidentInScopeProjectFilter(t *testing.T) { + db := newDoraIncidentDB(t) + + const board = "board1" + seedIncident(t, db, "1001", board) + seedIncident(t, db, "1002", board) + seedIncident(t, db, "1003", board) + + seedDoraBoardIssue(t, db, board, "1001", "ProjectA") + seedDoraBoardIssue(t, db, board, "1002", "ProjectB") + // 1003 is an unstamped (empty) row on a board that DOES have real per-project + // stamps. On such a board the empty/NULL rows are the ambiguous "unfiltered + // pool" and must NOT be attributed to any project (that was the leak). + seedDoraBoardIssue(t, db, board, "1003", "") + + seedDoraProjectMapping(t, db, "ProjectA", "boards", board) + seedDoraProjectMapping(t, db, "ProjectB", "boards", board) + + projectA := queryConnectIncidentIds(t, db, "ProjectA") + projectB := queryConnectIncidentIds(t, db, "ProjectB") + + require.Equal(t, []string{"1001"}, projectA) + require.NotContains(t, projectA, "1002", "ProjectA must not attach ProjectB's incident on a shared board") + require.NotContains(t, projectA, "1003", "unstamped rows on a stamped board must be excluded") + + require.Equal(t, []string{"1002"}, projectB) + require.NotContains(t, projectB, "1001") + require.NotContains(t, projectB, "1003") +} + +// TestConnectIncidentInScopeExcludesNullPool reproduces the real staging state: +// ProjectA was collected (its ExtraJQL slice stamped with "ProjectA"), while the +// rest of the board's incidents are left over from an earlier unfiltered +// collection and carry a NULL project_name. ProjectA's Change Failure Rate must +// include ONLY its stamped slice -- the NULL "unfiltered pool" must be excluded, +// not re-admitted. +// +// This is the case the previous fix got wrong: a NULL/empty fallback re-added +// every NULL row to every project, so ProjectA showed the whole board. +func TestConnectIncidentInScopeExcludesNullPool(t *testing.T) { + db := newDoraIncidentDB(t) + + const board = "board1" + // ProjectA's real slice + seedIncident(t, db, "A1", board) + seedIncident(t, db, "A2", board) + seedDoraBoardIssue(t, db, board, "A1", "ProjectA") + seedDoraBoardIssue(t, db, board, "A2", "ProjectA") + // leftover unfiltered pool from an earlier collection (NULL project_name) + seedIncident(t, db, "N1", board) + seedIncident(t, db, "N2", board) + seedIncident(t, db, "N3", board) + seedDoraBoardIssueNull(t, db, board, "N1") + seedDoraBoardIssueNull(t, db, board, "N2") + seedDoraBoardIssueNull(t, db, board, "N3") + + seedDoraProjectMapping(t, db, "ProjectA", "boards", board) + + ids := queryConnectIncidentIds(t, db, "ProjectA") + require.Equal(t, []string{"A1", "A2"}, ids, "only ProjectA's stamped slice may count; the NULL pool must be excluded") + require.NotContains(t, ids, "N1") + require.NotContains(t, ids, "N2") + require.NotContains(t, ids, "N3") +} + +// TestConnectIncidentInScopeNoShare is the control: a non-shared board with no +// per-project slice still attaches all of its incidents (no regression). +func TestConnectIncidentInScopeNoShare(t *testing.T) { + db := newDoraIncidentDB(t) + + const board = "boardSolo" + seedIncident(t, db, "2001", board) + seedIncident(t, db, "2002", board) + seedDoraBoardIssue(t, db, board, "2001", "") + seedDoraBoardIssue(t, db, board, "2002", "") + seedDoraProjectMapping(t, db, "SoloProject", "boards", board) + + ids := queryConnectIncidentIds(t, db, "SoloProject") + require.Equal(t, []string{"2001", "2002"}, ids) +} + +// --- helpers --------------------------------------------------------------- + +func newDoraIncidentDB(t *testing.T) dal.Dal { + gormDb, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, gormDb.AutoMigrate(&ticket.Incident{}, &ticket.BoardIssue{}, &crossdomain.ProjectMapping{})) + require.NoError(t, gormDb.Exec("DELETE FROM incidents").Error) + require.NoError(t, gormDb.Exec("DELETE FROM board_issues").Error) + require.NoError(t, gormDb.Exec("DELETE FROM project_mapping").Error) + return dalgorm.NewDalgorm(gormDb) +} + +func seedIncident(t *testing.T, db dal.Dal, id, boardId string) { + require.NoError(t, db.Create(&ticket.Incident{ + DomainEntity: domainlayer.DomainEntity{Id: id}, + ScopeId: boardId, + Table: "boards", + })) +} + +func seedDoraBoardIssue(t *testing.T, db dal.Dal, boardId, issueId, projectName string) { + require.NoError(t, db.Create(&ticket.BoardIssue{ + BoardId: boardId, + IssueId: issueId, + ProjectName: projectName, + })) +} + +func seedDoraBoardIssueNull(t *testing.T, db dal.Dal, boardId, issueId string) { + // Insert a row whose project_name is SQL NULL (the leftover unfiltered pool), + // which is distinct from the empty-string case. + require.NoError(t, db.Exec( + "INSERT INTO board_issues (board_id, issue_id, project_name) VALUES (?, ?, NULL)", + boardId, issueId, + )) +} + +func seedDoraProjectMapping(t *testing.T, db dal.Dal, projectName, table, rowId string) { + require.NoError(t, db.Create(&crossdomain.ProjectMapping{ + ProjectName: projectName, + Table: table, + RowId: rowId, + })) +} + +func queryConnectIncidentIds(t *testing.T, db dal.Dal, projectName string) []string { + cursor, err := db.Cursor(connectIncidentInScopeClauses(db, projectName)...) + require.NoError(t, err) + defer cursor.Close() + + var ids []string + for cursor.Next() { + row := &ticket.Incident{} + require.NoError(t, db.Fetch(cursor, row)) + ids = append(ids, row.Id) + } + sort.Strings(ids) + return ids +} diff --git a/backend/plugins/dora/tasks/incident_from_issue_generator.go b/backend/plugins/dora/tasks/incident_from_issue_generator.go index d0118ee85ae..e7698dfa58b 100644 --- a/backend/plugins/dora/tasks/incident_from_issue_generator.go +++ b/backend/plugins/dora/tasks/incident_from_issue_generator.go @@ -42,12 +42,56 @@ type issueWithBoardId struct { BoardId string } +// inScopeIncidentIssueClauses builds the query that selects the INCIDENT issues +// belonging to a single DevLake project's slice of a (possibly shared) Jira +// board. +// +// When several projects point at the same board, each project's Extra JQL +// records its slice on board_issues.project_name at collection time. This query +// therefore filters on BOTH the board-level project_mapping (pm.project_name) +// AND the issue-level slice (bi.project_name), so a project only turns its own +// issues into incidents. The empty/NULL bi.project_name fallback preserves +// pre-feature (single-project) data that was collected before project_name +// existed on board_issues. +func inScopeIncidentIssueClauses(db dal.Dal, projectName string) []dal.Clause { + return []dal.Clause{ + dal.Select("i.*, bi.board_id as board_id"), + dal.From(`issues i`), + dal.Join(`join board_issues bi on bi.issue_id = i.id`), + dal.Join("join project_mapping pm on pm.row_id = bi.board_id and " + quoteTableColumn(db, "pm") + " = 'boards'"), + dal.Where( + `i.type = ? and pm.project_name = ? + and ( + bi.project_name = ? + or not exists ( + select 1 from board_issues b2 + where b2.board_id = bi.board_id + and b2.project_name is not null and b2.project_name <> '' + ) + )`, + "INCIDENT", projectName, projectName, + ), + } +} + +// quoteTableColumn returns the `table` column of the given alias quoted for the +// current dialect. `table` is a reserved word: MySQL quotes identifiers with +// backticks, while Postgres (and SQLite, used by the unit tests) use double +// quotes. +func quoteTableColumn(db dal.Dal, alias string) string { + if db.Dialect() == "mysql" { + return alias + ".`table`" + } + return alias + `."table"` +} + func ConvertIssuesToIncidents(taskCtx plugin.SubTaskContext) errors.Error { db := taskCtx.GetDal() data := taskCtx.GetData().(*DoraTaskData) if data.DisableIssueToIncidentGenerator { return nil } + pmTable := quoteTableColumn(db, "pm") // clear previous incidents and incident_assignees from the project deleteIncidentsSql := ` DELETE @@ -55,13 +99,20 @@ func ConvertIssuesToIncidents(taskCtx plugin.SubTaskContext) errors.Error { WHERE id IN (SELECT i.id FROM issues i - LEFT JOIN board_issues bi ON bi.issue_id = i.id - LEFT JOIN project_mapping pm ON pm.row_id = bi.board_id + JOIN board_issues bi ON bi.issue_id = i.id + JOIN project_mapping pm ON pm.row_id = bi.board_id AND ` + pmTable + ` = 'boards' WHERE i.type = ? AND pm.project_name = ? - AND pm.table = ?) + AND ( + bi.project_name = ? + OR NOT EXISTS ( + SELECT 1 FROM board_issues b2 + WHERE b2.board_id = bi.board_id + AND b2.project_name IS NOT NULL AND b2.project_name <> '' + ) + )) ` - if err := db.Exec(deleteIncidentsSql, "INCIDENT", data.Options.ProjectName, "boards"); err != nil { + if err := db.Exec(deleteIncidentsSql, "INCIDENT", data.Options.ProjectName, data.Options.ProjectName); err != nil { return errors.Default.Wrap(err, "error deleting previous incidents") } @@ -71,28 +122,25 @@ func ConvertIssuesToIncidents(taskCtx plugin.SubTaskContext) errors.Error { WHERE incident_id IN (SELECT i.id FROM issues i - LEFT JOIN board_issues bi ON bi.issue_id = i.id - LEFT JOIN project_mapping pm ON pm.row_id = bi.board_id + JOIN board_issues bi ON bi.issue_id = i.id + JOIN project_mapping pm ON pm.row_id = bi.board_id AND ` + pmTable + ` = 'boards' WHERE i.type = ? AND pm.project_name = ? - AND pm.table = ?) + AND ( + bi.project_name = ? + OR NOT EXISTS ( + SELECT 1 FROM board_issues b2 + WHERE b2.board_id = bi.board_id + AND b2.project_name IS NOT NULL AND b2.project_name <> '' + ) + )) ` - if err := db.Exec(deleteIncidentAssigneesSql, "INCIDENT", data.Options.ProjectName, "boards"); err != nil { + if err := db.Exec(deleteIncidentAssigneesSql, "INCIDENT", data.Options.ProjectName, data.Options.ProjectName); err != nil { return errors.Default.Wrap(err, "error deleting previous incident_assignees") } - // select all issues belongs to the board - clauses := []dal.Clause{ - dal.Select("i.*, bi.board_id as board_id"), - dal.From(`issues i`), - dal.Join(`left join board_issues bi on bi.issue_id = i.id`), - dal.Join(`left join project_mapping pm on pm.row_id = bi.board_id`), - dal.Where( - "i.type = ? and pm.project_name = ? and pm.table = ?", - "INCIDENT", data.Options.ProjectName, "boards", - ), - } - cursor, err := db.Cursor(clauses...) + // select all issues belongs to the board, scoped to this project's slice + cursor, err := db.Cursor(inScopeIncidentIssueClauses(db, data.Options.ProjectName)...) if err != nil { return err } diff --git a/backend/plugins/dora/tasks/incident_from_issue_generator_test.go b/backend/plugins/dora/tasks/incident_from_issue_generator_test.go new file mode 100644 index 00000000000..07c81a77f46 --- /dev/null +++ b/backend/plugins/dora/tasks/incident_from_issue_generator_test.go @@ -0,0 +1,169 @@ +/* +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 ( + "sort" + "testing" + + "github.com/apache/devlake/core/dal" + "github.com/apache/devlake/core/models/domainlayer" + "github.com/apache/devlake/core/models/domainlayer/crossdomain" + "github.com/apache/devlake/core/models/domainlayer/ticket" + "github.com/apache/devlake/impls/dalgorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// TestInScopeIncidentIssueProjectFilter is a fast, infrastructure-free check of +// the behavior behind the "DORA Details - Change Failure Rate" panel: when a +// per-project Extra JQL is set on a Jira board that is SHARED by several DevLake +// projects, each project's DORA metrics must count only that project's slice of +// issues, not the whole board. +// +// It runs the REAL production query (inScopeIncidentIssueClauses, used by +// ConvertIssuesToIncidents) against an in-memory SQLite database wrapped in +// DevLake's Dal, so there is no MySQL/Docker dependency and the test completes +// in milliseconds. +// +// Scenario - ProjectA and ProjectB both map to the same board (board1): +// +// issue-A1 INCIDENT bi.project_name=ProjectA +// issue-A2 INCIDENT bi.project_name=ProjectA +// issue-B1 INCIDENT bi.project_name=ProjectB +// issue-N1 INCIDENT bi.project_name='' (legacy row, pre-feature) +// issue-A3 BUG bi.project_name=ProjectA (not an INCIDENT) +// +// Change Failure Rate counts distinct INCIDENT issues in project scope: +// +// ProjectA -> {A1, A2} + legacy N1 +// ProjectB -> {B1} + legacy N1 +// +// The decisive isolation guarantee is the cross-project EXCLUSION: ProjectA must +// never see issue-B1 and ProjectB must never see issue-A1/A2, even though both +// projects share board1. Before this feature both projects mapped to board1 and +// the query returned the entire board, so ProjectA would have wrongly counted +// issue-B1. +func TestInScopeIncidentIssueProjectFilter(t *testing.T) { + db := newInMemoryDB(t) + + seedIssue(t, db, "issue-A1", "INCIDENT") + seedIssue(t, db, "issue-A2", "INCIDENT") + seedIssue(t, db, "issue-B1", "INCIDENT") + seedIssue(t, db, "issue-N1", "INCIDENT") + seedIssue(t, db, "issue-A3", "BUG") + + seedBoardIssue(t, db, "board1", "issue-A1", "ProjectA") + seedBoardIssue(t, db, "board1", "issue-A2", "ProjectA") + seedBoardIssue(t, db, "board1", "issue-B1", "ProjectB") + seedBoardIssue(t, db, "board1", "issue-N1", "") // ambiguous pool row on a stamped board + seedBoardIssue(t, db, "board1", "issue-A3", "ProjectA") + + // Both projects map to the SAME board - this is the shared-board setup. + seedProjectMapping(t, db, "ProjectA", "boards", "board1") + seedProjectMapping(t, db, "ProjectB", "boards", "board1") + + projectA := queryInScopeIncidentIssueIds(t, db, "ProjectA") + projectB := queryInScopeIncidentIssueIds(t, db, "ProjectB") + + // board1 HAS real per-project stamps, so each project sees ONLY its own + // stamped slice. The unstamped issue-N1 (the ambiguous pool) is excluded, + // and ProjectB's issue never leaks into ProjectA. + assert.Equal(t, []string{"issue-A1", "issue-A2"}, projectA) + assert.NotContains(t, projectA, "issue-B1", "ProjectA must not see ProjectB's issue on the shared board") + assert.NotContains(t, projectA, "issue-N1", "unstamped pool rows are excluded on a stamped board") + assert.NotContains(t, projectA, "issue-A3", "non-INCIDENT issues never become incidents") + + // ProjectB: only its own stamped incident. + assert.Equal(t, []string{"issue-B1"}, projectB) + assert.NotContains(t, projectB, "issue-A1") + assert.NotContains(t, projectB, "issue-A2") + assert.NotContains(t, projectB, "issue-N1") + + // The two projects share a board yet get different Change-Failure-Rate + // numerators - that is the whole point of per-project isolation. + assert.NotEqual(t, projectA, projectB) +} + +// TestInScopeIncidentIssueNoExtraJql is the control: when a board is NOT shared +// and no per-project slice is recorded (bi.project_name empty), the project +// still sees all of its board's incidents. This guarantees the feature does not +// regress the ordinary single-project case. +func TestInScopeIncidentIssueNoExtraJql(t *testing.T) { + db := newInMemoryDB(t) + + seedIssue(t, db, "issue-1", "INCIDENT") + seedIssue(t, db, "issue-2", "INCIDENT") + seedBoardIssue(t, db, "boardX", "issue-1", "") + seedBoardIssue(t, db, "boardX", "issue-2", "") + seedProjectMapping(t, db, "SoloProject", "boards", "boardX") + + ids := queryInScopeIncidentIssueIds(t, db, "SoloProject") + assert.Equal(t, []string{"issue-1", "issue-2"}, ids) +} + +// --- helpers --------------------------------------------------------------- + +func newInMemoryDB(t *testing.T) dal.Dal { + gormDb, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, gormDb.AutoMigrate(&ticket.Issue{}, &ticket.BoardIssue{}, &crossdomain.ProjectMapping{})) + return dalgorm.NewDalgorm(gormDb) +} + +func seedIssue(t *testing.T, db dal.Dal, id, issueType string) { + require.NoError(t, db.Create(&ticket.Issue{ + DomainEntity: domainlayer.DomainEntity{Id: id}, + Type: issueType, + })) +} + +func seedBoardIssue(t *testing.T, db dal.Dal, boardId, issueId, projectName string) { + require.NoError(t, db.Create(&ticket.BoardIssue{ + BoardId: boardId, + IssueId: issueId, + ProjectName: projectName, + })) +} + +func seedProjectMapping(t *testing.T, db dal.Dal, projectName, table, rowId string) { + require.NoError(t, db.Create(&crossdomain.ProjectMapping{ + ProjectName: projectName, + Table: table, + RowId: rowId, + })) +} + +// queryInScopeIncidentIssueIds runs the real production selection used by +// ConvertIssuesToIncidents and returns the matched issue ids (sorted). +func queryInScopeIncidentIssueIds(t *testing.T, db dal.Dal, projectName string) []string { + cursor, err := db.Cursor(inScopeIncidentIssueClauses(db, projectName)...) + require.NoError(t, err) + defer cursor.Close() + + var ids []string + for cursor.Next() { + row := &issueWithBoardId{} + require.NoError(t, db.Fetch(cursor, row)) + ids = append(ids, row.Id) + } + sort.Strings(ids) + return ids +} From be6cb810afbe61b6435a6d5ed26c511f15e6398b Mon Sep 17 00:00:00 2001 From: Keren Date: Tue, 13 Oct 2026 11:11:07 +0300 Subject: [PATCH 6/6] feat(grafana): filter DORA incident panels by project Apply the same board_issues.project_name filter to the incident queries of the DORA dashboards (mysql: DORA, dora-by-team, dora-debug and the change-failure-rate / failed-deployment-recovery-time / time-to-restore-service details; postgresql: DORA, dora-by-team), with the same whole-board fallback. Co-Authored-By: Claude Opus 5.5 --- grafana/dashboards/mysql/DORA.json | 2 +- grafana/dashboards/mysql/dora-by-team.json | 2 +- grafana/dashboards/mysql/dora-debug.json | 2 +- grafana/dashboards/mysql/dora-details-change-failure-rate.json | 2 +- .../mysql/dora-details-failed-deployment-recovery-time.json | 2 +- .../dashboards/mysql/dora-details-timeto-restore-service.json | 2 +- grafana/dashboards/postgresql/DORA.json | 2 +- grafana/dashboards/postgresql/dora-by-team.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/grafana/dashboards/mysql/DORA.json b/grafana/dashboards/mysql/DORA.json index 089fd7995da..20cda5f7bfb 100644 --- a/grafana/dashboards/mysql/DORA.json +++ b/grafana/dashboards/mysql/DORA.json @@ -233,7 +233,7 @@ "metricColumn": "none", "queryType": "randomWalk", "rawQuery": true, - "rawSql": "-- Metric 1: Deployment Frequency\nwith last_few_calendar_months as(\n -- construct the last few calendar months within the selected time period in the top-right corner\n SELECT\n CAST(($__timeTo() - INTERVAL (H + T + U) DAY) AS date) day\n FROM\n (\n SELECT\n 0 H\n UNION\n ALL\n SELECT\n 100\n UNION\n ALL\n SELECT\n 200\n UNION\n ALL\n SELECT\n 300\n ) H\n CROSS JOIN (\n SELECT\n 0 T\n UNION\n ALL\n SELECT\n 10\n UNION\n ALL\n SELECT\n 20\n UNION\n ALL\n SELECT\n 30\n UNION\n ALL\n SELECT\n 40\n UNION\n ALL\n SELECT\n 50\n UNION\n ALL\n SELECT\n 60\n UNION\n ALL\n SELECT\n 70\n UNION\n ALL\n SELECT\n 80\n UNION\n ALL\n SELECT\n 90\n ) T\n CROSS JOIN (\n SELECT\n 0 U\n UNION\n ALL\n SELECT\n 1\n UNION\n ALL\n SELECT\n 2\n UNION\n ALL\n SELECT\n 3\n UNION\n ALL\n SELECT\n 4\n UNION\n ALL\n SELECT\n 5\n UNION\n ALL\n SELECT\n 6\n UNION\n ALL\n SELECT\n 7\n UNION\n ALL\n SELECT\n 8\n UNION\n ALL\n SELECT\n 9\n ) U\n WHERE\n ($__timeTo() - INTERVAL (H + T + U) DAY) > $__timeFrom()\n),\n_production_deployment_days as(\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(DATE(cdc.finished_date)) as day\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in (${project})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n),\n_days_weekly_deploy as(\n -- calculate the number of deployment days every week\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - WEEKDAY(last_few_calendar_months.day) DAY\n )\n ) as week,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n 0\n )\n ) as weeks_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n week\n),\n_days_monthly_deploy as(\n -- calculate the number of deployment days every month\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - DAY(last_few_calendar_months.day) + 1 DAY\n )\n ) as month,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n null\n )\n ) as months_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n month\n),\n_days_six_months_deploy AS (\n SELECT\n month,\n SUM(days_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS days_deployed_per_six_months,\n COUNT(months_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS months_deployed_count,\n ROW_NUMBER() OVER (\n PARTITION BY DATE_FORMAT(month, '%Y-%m') DIV 6\n ORDER BY\n month DESC\n ) AS rn\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_week_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_weekly_deploy\n),\n_median_number_of_deployment_days_per_week as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_week\n FROM\n _median_number_of_deployment_days_per_week_ranks\n WHERE\n ranks <= 0.5\n),\n_median_number_of_deployment_days_per_month_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_month as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_month\n FROM\n _median_number_of_deployment_days_per_month_ranks\n WHERE\n ranks <= 0.5\n),\n_days_per_six_months_deploy_by_filter AS (\n SELECT\n month,\n days_deployed_per_six_months,\n months_deployed_count\n FROM\n _days_six_months_deploy\n WHERE\n rn % 6 = 1\n),\n_median_number_of_deployment_days_per_six_months_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed_per_six_months\n ) as ranks\n FROM\n _days_per_six_months_deploy_by_filter\n),\n_median_number_of_deployment_days_per_six_months as(\n SELECT\n min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months,\n min(months_deployed_count) as is_collected\n FROM\n _median_number_of_deployment_days_per_six_months_ranks\n WHERE\n ranks >= 0.5\n),\n_metric_deployment_frequency as (\n SELECT\n 'Deployment frequency' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)'\n WHEN median_number_of_deployment_days_per_month < 1\n and is_collected is not null THEN 'Fewer than once per month(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)'\n WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)'\n WHEN median_number_of_deployment_days_per_six_months < 1\n and is_collected is not null THEN 'Fewer than once per six months(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_number_of_deployment_days_per_week,\n _median_number_of_deployment_days_per_month,\n _median_number_of_deployment_days_per_six_months\n),\n-- Metric 2: median lead time for changes\n_pr_stats as (\n -- get the cycle time of PRs deployed by the deployments finished in the selected period\n SELECT\n distinct pr.id,\n ppm.pr_cycle_time\n FROM\n pull_requests pr\n join project_pr_metrics ppm on ppm.id = pr.id\n join project_mapping pm on pr.base_repo_id = pm.row_id\n and pm.`table` = 'repos'\n join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id\n WHERE\n pm.project_name in (${project})\n and pr.merged_date is not null\n and ppm.pr_cycle_time is not null\n and $__timeFilter(cdc.finished_date)\n),\n_median_change_lead_time_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n pr_cycle_time\n ) as ranks\n FROM\n _pr_stats\n),\n_median_change_lead_time as(\n -- use median PR cycle time as the median change lead time\n SELECT\n max(pr_cycle_time) as median_change_lead_time\n FROM\n _median_change_lead_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_change_lead_time as (\n SELECT\n 'Lead time for changes' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_change_lead_time < 24 * 60 THEN \"Less than one day(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Between one day and one week(high)\"\n WHEN median_change_lead_time < 30 * 24 * 60 THEN \"Between one week and one month(medium)\"\n WHEN median_change_lead_time >= 30 * 24 * 60 THEN \"More than one month(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_change_lead_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Less than one week(high)\"\n WHEN median_change_lead_time < 180 * 24 * 60 THEN \"Between one week and six months(medium)\"\n WHEN median_change_lead_time >= 180 * 24 * 60 THEN \"More than six months(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_change_lead_time\n),\n-- Metric 3: change failure rate\n_deployments as (\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in (${project})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_failure_caused_by_deployments as (\n -- calculate the number of incidents caused by each deployment\n SELECT\n d.deployment_id,\n d.deployment_finished_date,\n count(\n distinct case\n when i.id is not null then d.deployment_id\n else null\n end\n ) as has_incident\n FROM\n _deployments d\n left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id\n left join incidents i on pim.id = i.id\n GROUP BY\n 1,\n 2\n),\n_change_failure_rate as (\n SELECT\n case\n when count(deployment_id) is null then null\n else sum(has_incident) / count(deployment_id)\n end as change_failure_rate\n FROM\n _failure_caused_by_deployments\n),\n_is_collected_data as(\n SELECT\n CASE\n WHEN COUNT(i.id) = 0\n AND COUNT(cdc.id) = 0 THEN 'No All'\n WHEN COUNT(i.id) = 0 THEN 'No Incidents'\n WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'\n END AS is_collected\n FROM\n (\n SELECT\n 1\n ) AS dummy\n LEFT JOIN incidents i ON 1 = 1\n LEFT JOIN cicd_deployment_commits cdc ON 1 = 1\n),\n_metric_cfr as (\n SELECT\n 'Change failure rate' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.05 THEN \"0-5%(elite)\"\n WHEN change_failure_rate <=.10 THEN \"5%-10%(high)\"\n WHEN change_failure_rate <=.15 THEN \"10%-15%(medium)\"\n WHEN change_failure_rate >.15 THEN \"> 15%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.15 THEN \"0-15%(elite)\"\n WHEN change_failure_rate <=.20 THEN \"16%-20%(high)\"\n WHEN change_failure_rate <=.30 THEN \"21%-30%(medium)\"\n WHEN change_failure_rate >.30 THEN \"> 30%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _change_failure_rate,\n _is_collected_data\n),\n-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_recovery_time_2023_report as(\n SELECT\n \"Failed deployment recovery time\" as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_recovery_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_recovery_time < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_recovery_time < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_recovery_time >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected deployments or incidents.\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n WHERE\n pm.project_name in (${project})\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n \"Time to restore service\" as metric,\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n),\n_metric_mrt_or_mm as(\n SELECT\n metric,\n median_recovery_time AS value\n FROM\n _metric_recovery_time_2023_report\n WHERE\n ('$dora_report') = '2023'\n UNION\n SELECT\n metric,\n median_time_to_resolve AS value\n FROM\n _metric_mttr_2021_report\n WHERE\n ('$dora_report') = '2021'\n),\n_final_results as (\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m1.metric as _metric,\n m1.value\n FROM\n dora_benchmarks db\n left join _metric_deployment_frequency m1 on db.metric = m1.metric\n WHERE\n m1.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m2.metric as _metric,\n m2.value\n FROM\n dora_benchmarks db\n left join _metric_change_lead_time m2 on db.metric = m2.metric\n WHERE\n m2.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m3.metric as _metric,\n m3.value\n FROM\n dora_benchmarks db\n left join _metric_cfr m3 on db.metric = m3.metric\n WHERE\n m3.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m4.metric as _metric,\n m4.value\n FROM\n dora_benchmarks db\n left join _metric_mrt_or_mm m4 on db.metric = m4.metric\n WHERE\n m4.metric is not null\n and db.dora_report = ('$dora_report')\n)\nSELECT\n metric,\n replace(metric, ' ', '-') as metric_hidden,\n case\n when low = value then low\n else null\n end as low,\n case\n when medium = value then medium\n else null\n end as medium,\n case\n when high = value then high\n else null\n end as high,\n case\n when elite = value then elite\n else null\n end as elite\nFROM\n _final_results\nORDER BY\n id", + "rawSql": "-- Metric 1: Deployment Frequency\nwith last_few_calendar_months as(\n -- construct the last few calendar months within the selected time period in the top-right corner\n SELECT\n CAST(($__timeTo() - INTERVAL (H + T + U) DAY) AS date) day\n FROM\n (\n SELECT\n 0 H\n UNION\n ALL\n SELECT\n 100\n UNION\n ALL\n SELECT\n 200\n UNION\n ALL\n SELECT\n 300\n ) H\n CROSS JOIN (\n SELECT\n 0 T\n UNION\n ALL\n SELECT\n 10\n UNION\n ALL\n SELECT\n 20\n UNION\n ALL\n SELECT\n 30\n UNION\n ALL\n SELECT\n 40\n UNION\n ALL\n SELECT\n 50\n UNION\n ALL\n SELECT\n 60\n UNION\n ALL\n SELECT\n 70\n UNION\n ALL\n SELECT\n 80\n UNION\n ALL\n SELECT\n 90\n ) T\n CROSS JOIN (\n SELECT\n 0 U\n UNION\n ALL\n SELECT\n 1\n UNION\n ALL\n SELECT\n 2\n UNION\n ALL\n SELECT\n 3\n UNION\n ALL\n SELECT\n 4\n UNION\n ALL\n SELECT\n 5\n UNION\n ALL\n SELECT\n 6\n UNION\n ALL\n SELECT\n 7\n UNION\n ALL\n SELECT\n 8\n UNION\n ALL\n SELECT\n 9\n ) U\n WHERE\n ($__timeTo() - INTERVAL (H + T + U) DAY) > $__timeFrom()\n),\n_production_deployment_days as(\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(DATE(cdc.finished_date)) as day\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in (${project})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n),\n_days_weekly_deploy as(\n -- calculate the number of deployment days every week\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - WEEKDAY(last_few_calendar_months.day) DAY\n )\n ) as week,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n 0\n )\n ) as weeks_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n week\n),\n_days_monthly_deploy as(\n -- calculate the number of deployment days every month\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - DAY(last_few_calendar_months.day) + 1 DAY\n )\n ) as month,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n null\n )\n ) as months_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n month\n),\n_days_six_months_deploy AS (\n SELECT\n month,\n SUM(days_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS days_deployed_per_six_months,\n COUNT(months_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS months_deployed_count,\n ROW_NUMBER() OVER (\n PARTITION BY DATE_FORMAT(month, '%Y-%m') DIV 6\n ORDER BY\n month DESC\n ) AS rn\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_week_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_weekly_deploy\n),\n_median_number_of_deployment_days_per_week as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_week\n FROM\n _median_number_of_deployment_days_per_week_ranks\n WHERE\n ranks <= 0.5\n),\n_median_number_of_deployment_days_per_month_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_month as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_month\n FROM\n _median_number_of_deployment_days_per_month_ranks\n WHERE\n ranks <= 0.5\n),\n_days_per_six_months_deploy_by_filter AS (\n SELECT\n month,\n days_deployed_per_six_months,\n months_deployed_count\n FROM\n _days_six_months_deploy\n WHERE\n rn % 6 = 1\n),\n_median_number_of_deployment_days_per_six_months_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed_per_six_months\n ) as ranks\n FROM\n _days_per_six_months_deploy_by_filter\n),\n_median_number_of_deployment_days_per_six_months as(\n SELECT\n min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months,\n min(months_deployed_count) as is_collected\n FROM\n _median_number_of_deployment_days_per_six_months_ranks\n WHERE\n ranks >= 0.5\n),\n_metric_deployment_frequency as (\n SELECT\n 'Deployment frequency' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)'\n WHEN median_number_of_deployment_days_per_month < 1\n and is_collected is not null THEN 'Fewer than once per month(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)'\n WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)'\n WHEN median_number_of_deployment_days_per_six_months < 1\n and is_collected is not null THEN 'Fewer than once per six months(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_number_of_deployment_days_per_week,\n _median_number_of_deployment_days_per_month,\n _median_number_of_deployment_days_per_six_months\n),\n-- Metric 2: median lead time for changes\n_pr_stats as (\n -- get the cycle time of PRs deployed by the deployments finished in the selected period\n SELECT\n distinct pr.id,\n ppm.pr_cycle_time\n FROM\n pull_requests pr\n join project_pr_metrics ppm on ppm.id = pr.id\n join project_mapping pm on pr.base_repo_id = pm.row_id\n and pm.`table` = 'repos'\n join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id\n WHERE\n pm.project_name in (${project})\n and pr.merged_date is not null\n and ppm.pr_cycle_time is not null\n and $__timeFilter(cdc.finished_date)\n),\n_median_change_lead_time_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n pr_cycle_time\n ) as ranks\n FROM\n _pr_stats\n),\n_median_change_lead_time as(\n -- use median PR cycle time as the median change lead time\n SELECT\n max(pr_cycle_time) as median_change_lead_time\n FROM\n _median_change_lead_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_change_lead_time as (\n SELECT\n 'Lead time for changes' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_change_lead_time < 24 * 60 THEN \"Less than one day(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Between one day and one week(high)\"\n WHEN median_change_lead_time < 30 * 24 * 60 THEN \"Between one week and one month(medium)\"\n WHEN median_change_lead_time >= 30 * 24 * 60 THEN \"More than one month(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_change_lead_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Less than one week(high)\"\n WHEN median_change_lead_time < 180 * 24 * 60 THEN \"Between one week and six months(medium)\"\n WHEN median_change_lead_time >= 180 * 24 * 60 THEN \"More than six months(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_change_lead_time\n),\n-- Metric 3: change failure rate\n_deployments as (\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in (${project})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_failure_caused_by_deployments as (\n -- calculate the number of incidents caused by each deployment\n SELECT\n d.deployment_id,\n d.deployment_finished_date,\n count(\n distinct case\n when i.id is not null then d.deployment_id\n else null\n end\n ) as has_incident\n FROM\n _deployments d\n left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id\n left join incidents i on pim.id = i.id\n GROUP BY\n 1,\n 2\n),\n_change_failure_rate as (\n SELECT\n case\n when count(deployment_id) is null then null\n else sum(has_incident) / count(deployment_id)\n end as change_failure_rate\n FROM\n _failure_caused_by_deployments\n),\n_is_collected_data as(\n SELECT\n CASE\n WHEN COUNT(i.id) = 0\n AND COUNT(cdc.id) = 0 THEN 'No All'\n WHEN COUNT(i.id) = 0 THEN 'No Incidents'\n WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'\n END AS is_collected\n FROM\n (\n SELECT\n 1\n ) AS dummy\n LEFT JOIN incidents i ON 1 = 1\n LEFT JOIN cicd_deployment_commits cdc ON 1 = 1\n),\n_metric_cfr as (\n SELECT\n 'Change failure rate' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.05 THEN \"0-5%(elite)\"\n WHEN change_failure_rate <=.10 THEN \"5%-10%(high)\"\n WHEN change_failure_rate <=.15 THEN \"10%-15%(medium)\"\n WHEN change_failure_rate >.15 THEN \"> 15%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.15 THEN \"0-15%(elite)\"\n WHEN change_failure_rate <=.20 THEN \"16%-20%(high)\"\n WHEN change_failure_rate <=.30 THEN \"21%-30%(medium)\"\n WHEN change_failure_rate >.30 THEN \"> 30%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _change_failure_rate,\n _is_collected_data\n),\n-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_recovery_time_2023_report as(\n SELECT\n \"Failed deployment recovery time\" as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_recovery_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_recovery_time < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_recovery_time < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_recovery_time >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected deployments or incidents.\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n WHERE\n pm.project_name in (${project})\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n \"Time to restore service\" as metric,\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n),\n_metric_mrt_or_mm as(\n SELECT\n metric,\n median_recovery_time AS value\n FROM\n _metric_recovery_time_2023_report\n WHERE\n ('$dora_report') = '2023'\n UNION\n SELECT\n metric,\n median_time_to_resolve AS value\n FROM\n _metric_mttr_2021_report\n WHERE\n ('$dora_report') = '2021'\n),\n_final_results as (\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m1.metric as _metric,\n m1.value\n FROM\n dora_benchmarks db\n left join _metric_deployment_frequency m1 on db.metric = m1.metric\n WHERE\n m1.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m2.metric as _metric,\n m2.value\n FROM\n dora_benchmarks db\n left join _metric_change_lead_time m2 on db.metric = m2.metric\n WHERE\n m2.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m3.metric as _metric,\n m3.value\n FROM\n dora_benchmarks db\n left join _metric_cfr m3 on db.metric = m3.metric\n WHERE\n m3.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m4.metric as _metric,\n m4.value\n FROM\n dora_benchmarks db\n left join _metric_mrt_or_mm m4 on db.metric = m4.metric\n WHERE\n m4.metric is not null\n and db.dora_report = ('$dora_report')\n)\nSELECT\n metric,\n replace(metric, ' ', '-') as metric_hidden,\n case\n when low = value then low\n else null\n end as low,\n case\n when medium = value then medium\n else null\n end as medium,\n case\n when high = value then high\n else null\n end as high,\n case\n when elite = value then elite\n else null\n end as elite\nFROM\n _final_results\nORDER BY\n id", "refId": "A", "select": [ [ diff --git a/grafana/dashboards/mysql/dora-by-team.json b/grafana/dashboards/mysql/dora-by-team.json index e799f9722ca..d37f44279a1 100644 --- a/grafana/dashboards/mysql/dora-by-team.json +++ b/grafana/dashboards/mysql/dora-by-team.json @@ -206,7 +206,7 @@ "format": "table", "hide": false, "rawQuery": true, - "rawSql": "-- Metric 1: Deployment Frequency\nwith last_few_calendar_months as(\n -- construct the last few calendar months within the selected time period in the top-right corner\n SELECT\n CAST(($__timeTo() - INTERVAL (H + T + U) DAY) AS date) day\n FROM\n (\n SELECT\n 0 H\n UNION\n ALL\n SELECT\n 100\n UNION\n ALL\n SELECT\n 200\n UNION\n ALL\n SELECT\n 300\n ) H\n CROSS JOIN (\n SELECT\n 0 T\n UNION\n ALL\n SELECT\n 10\n UNION\n ALL\n SELECT\n 20\n UNION\n ALL\n SELECT\n 30\n UNION\n ALL\n SELECT\n 40\n UNION\n ALL\n SELECT\n 50\n UNION\n ALL\n SELECT\n 60\n UNION\n ALL\n SELECT\n 70\n UNION\n ALL\n SELECT\n 80\n UNION\n ALL\n SELECT\n 90\n ) T\n CROSS JOIN (\n SELECT\n 0 U\n UNION\n ALL\n SELECT\n 1\n UNION\n ALL\n SELECT\n 2\n UNION\n ALL\n SELECT\n 3\n UNION\n ALL\n SELECT\n 4\n UNION\n ALL\n SELECT\n 5\n UNION\n ALL\n SELECT\n 6\n UNION\n ALL\n SELECT\n 7\n UNION\n ALL\n SELECT\n 8\n UNION\n ALL\n SELECT\n 9\n ) U\n WHERE\n ($__timeTo() - INTERVAL (H + T + U) DAY) > $__timeFrom()\n),\n_production_deployment_days as(\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(DATE(cdc.finished_date)) as day\n FROM\n cicd_deployment_commits cdc\n JOIN commits c on cdc.commit_sha = c.sha\n join user_accounts ua on c.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n t.name in (${team:singlequote})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n),\n_days_weekly_deploy as(\n -- calculate the number of deployment days every week\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - WEEKDAY(last_few_calendar_months.day) DAY\n )\n ) as week,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n 0\n )\n ) as weeks_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n week\n),\n_days_monthly_deploy as(\n -- calculate the number of deployment days every month\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - DAY(last_few_calendar_months.day) + 1 DAY\n )\n ) as month,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n null\n )\n ) as months_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n month\n),\n_days_six_months_deploy AS (\n SELECT\n month,\n SUM(days_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS days_deployed_per_six_months,\n COUNT(months_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS months_deployed_count,\n ROW_NUMBER() OVER (\n PARTITION BY DATE_FORMAT(month, '%Y-%m') DIV 6\n ORDER BY\n month DESC\n ) AS rn\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_week_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_weekly_deploy\n),\n_median_number_of_deployment_days_per_week as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_week\n FROM\n _median_number_of_deployment_days_per_week_ranks\n WHERE\n ranks <= 0.5\n),\n_median_number_of_deployment_days_per_month_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_month as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_month\n FROM\n _median_number_of_deployment_days_per_month_ranks\n WHERE\n ranks <= 0.5\n),\n_days_per_six_months_deploy_by_filter AS (\n SELECT\n month,\n days_deployed_per_six_months,\n months_deployed_count\n FROM\n _days_six_months_deploy\n WHERE\n rn % 6 = 1\n),\n_median_number_of_deployment_days_per_six_months_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed_per_six_months\n ) as ranks\n FROM\n _days_per_six_months_deploy_by_filter\n),\n_median_number_of_deployment_days_per_six_months as(\n SELECT\n min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months,\n min(months_deployed_count) as is_collected\n FROM\n _median_number_of_deployment_days_per_six_months_ranks\n WHERE\n ranks >= 0.5\n),\n_metric_deployment_frequency as (\n SELECT\n 'Deployment frequency' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)'\n WHEN median_number_of_deployment_days_per_month < 1\n and is_collected is not null THEN 'Fewer than once per month(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)'\n WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)'\n WHEN median_number_of_deployment_days_per_six_months < 1\n and is_collected is not null THEN 'Fewer than once per six months(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_number_of_deployment_days_per_week,\n _median_number_of_deployment_days_per_month,\n _median_number_of_deployment_days_per_six_months\n),\n-- Metric 2: median lead time for changes\n_pr_stats as (\n -- get the cycle time of PRs deployed by the deployments finished in the selected period\n SELECT\n distinct pr.id,\n ppm.pr_cycle_time\n FROM\n pull_requests pr\n join user_accounts ua on pr.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n join project_pr_metrics ppm on ppm.id = pr.id\n join project_mapping pm on pr.base_repo_id = pm.row_id\n and pm.`table` = 'repos'\n join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id\n WHERE\n t.name in (${team:singlequote})\n and pr.merged_date is not null\n and ppm.pr_cycle_time is not null\n and $__timeFilter(cdc.finished_date)\n),\n_median_change_lead_time_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n pr_cycle_time\n ) as ranks\n FROM\n _pr_stats\n),\n_median_change_lead_time as(\n -- use median PR cycle time as the median change lead time\n SELECT\n max(pr_cycle_time) as median_change_lead_time\n FROM\n _median_change_lead_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_change_lead_time as (\n SELECT\n 'Lead time for changes' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_change_lead_time < 24 * 60 THEN \"Less than one day(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Between one day and one week(high)\"\n WHEN median_change_lead_time < 30 * 24 * 60 THEN \"Between one week and one month(medium)\"\n WHEN median_change_lead_time >= 30 * 24 * 60 THEN \"More than one month(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_change_lead_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Less than one week(high)\"\n WHEN median_change_lead_time < 180 * 24 * 60 THEN \"Between one week and six months(medium)\"\n WHEN median_change_lead_time >= 180 * 24 * 60 THEN \"More than six months(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_change_lead_time\n),\n-- Metric 3: change failure rate\n_deployments as (\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN commits c on cdc.commit_sha = c.sha\n join user_accounts ua on c.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n t.name in (${team:singlequote})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_failure_caused_by_deployments as (\n -- calculate the number of incidents caused by each deployment\n SELECT\n d.deployment_id,\n d.deployment_finished_date,\n count(\n distinct case\n when i.id is not null then d.deployment_id\n else null\n end\n ) as has_incident\n FROM\n _deployments d\n left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id\n left join incidents i on pim.id = i.id\n GROUP BY\n 1,\n 2\n),\n_change_failure_rate as (\n SELECT\n case\n when count(deployment_id) is null then null\n else sum(has_incident) / count(deployment_id)\n end as change_failure_rate\n FROM\n _failure_caused_by_deployments\n),\n_is_collected_data as(\n SELECT\n CASE\n WHEN COUNT(i.id) = 0\n AND COUNT(cdc.id) = 0 THEN 'No All'\n WHEN COUNT(i.id) = 0 THEN 'No Incidents'\n WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'\n END AS is_collected\n FROM\n (\n SELECT\n 1\n ) AS dummy\n LEFT JOIN incidents i ON 1 = 1\n LEFT JOIN cicd_deployment_commits cdc ON 1 = 1\n),\n_metric_cfr as (\n SELECT\n 'Change failure rate' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.05 THEN \"0-5%(elite)\"\n WHEN change_failure_rate <=.10 THEN \"5%-10%(high)\"\n WHEN change_failure_rate <=.15 THEN \"10%-15%(medium)\"\n WHEN change_failure_rate >.15 THEN \"> 15%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.15 THEN \"0-15%(elite)\"\n WHEN change_failure_rate <=.20 THEN \"16%-20%(high)\"\n WHEN change_failure_rate <=.30 THEN \"21%-30%(medium)\"\n WHEN change_failure_rate >.30 THEN \"> 30%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _change_failure_rate,\n _is_collected_data\n),\n-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_recovery_time_2023_report as(\n SELECT\n \"Failed deployment recovery time\" as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_recovery_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_recovery_time < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_recovery_time < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_recovery_time >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected deployments or incidents.\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n join user_accounts ua on i.assignee_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n WHERE\n t.name in (${team:singlequote})\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n \"Time to restore service\" as metric,\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n),\n_metric_mrt_or_mm as(\n SELECT\n metric,\n median_recovery_time AS value\n FROM\n _metric_recovery_time_2023_report\n WHERE\n ('$dora_report') = '2023'\n UNION\n SELECT\n metric,\n median_time_to_resolve AS value\n FROM\n _metric_mttr_2021_report\n WHERE\n ('$dora_report') = '2021'\n),\n_final_results as (\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m1.metric as _metric,\n m1.value\n FROM\n dora_benchmarks db\n left join _metric_deployment_frequency m1 on db.metric = m1.metric\n WHERE\n m1.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m2.metric as _metric,\n m2.value\n FROM\n dora_benchmarks db\n left join _metric_change_lead_time m2 on db.metric = m2.metric\n WHERE\n m2.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m3.metric as _metric,\n m3.value\n FROM\n dora_benchmarks db\n left join _metric_cfr m3 on db.metric = m3.metric\n WHERE\n m3.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m4.metric as _metric,\n m4.value\n FROM\n dora_benchmarks db\n left join _metric_mrt_or_mm m4 on db.metric = m4.metric\n WHERE\n m4.metric is not null\n and db.dora_report = ('$dora_report')\n)\nSELECT\n metric,\n case\n when low = value then low\n else null\n end as low,\n case\n when medium = value then medium\n else null\n end as medium,\n case\n when high = value then high\n else null\n end as high,\n case\n when elite = value then elite\n else null\n end as elite\nFROM\n _final_results\nORDER BY\n id", + "rawSql": "-- Metric 1: Deployment Frequency\nwith last_few_calendar_months as(\n -- construct the last few calendar months within the selected time period in the top-right corner\n SELECT\n CAST(($__timeTo() - INTERVAL (H + T + U) DAY) AS date) day\n FROM\n (\n SELECT\n 0 H\n UNION\n ALL\n SELECT\n 100\n UNION\n ALL\n SELECT\n 200\n UNION\n ALL\n SELECT\n 300\n ) H\n CROSS JOIN (\n SELECT\n 0 T\n UNION\n ALL\n SELECT\n 10\n UNION\n ALL\n SELECT\n 20\n UNION\n ALL\n SELECT\n 30\n UNION\n ALL\n SELECT\n 40\n UNION\n ALL\n SELECT\n 50\n UNION\n ALL\n SELECT\n 60\n UNION\n ALL\n SELECT\n 70\n UNION\n ALL\n SELECT\n 80\n UNION\n ALL\n SELECT\n 90\n ) T\n CROSS JOIN (\n SELECT\n 0 U\n UNION\n ALL\n SELECT\n 1\n UNION\n ALL\n SELECT\n 2\n UNION\n ALL\n SELECT\n 3\n UNION\n ALL\n SELECT\n 4\n UNION\n ALL\n SELECT\n 5\n UNION\n ALL\n SELECT\n 6\n UNION\n ALL\n SELECT\n 7\n UNION\n ALL\n SELECT\n 8\n UNION\n ALL\n SELECT\n 9\n ) U\n WHERE\n ($__timeTo() - INTERVAL (H + T + U) DAY) > $__timeFrom()\n),\n_production_deployment_days as(\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(DATE(cdc.finished_date)) as day\n FROM\n cicd_deployment_commits cdc\n JOIN commits c on cdc.commit_sha = c.sha\n join user_accounts ua on c.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n t.name in (${team:singlequote})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n),\n_days_weekly_deploy as(\n -- calculate the number of deployment days every week\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - WEEKDAY(last_few_calendar_months.day) DAY\n )\n ) as week,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n 0\n )\n ) as weeks_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n week\n),\n_days_monthly_deploy as(\n -- calculate the number of deployment days every month\n SELECT\n date(\n DATE_ADD(\n last_few_calendar_months.day,\n INTERVAL - DAY(last_few_calendar_months.day) + 1 DAY\n )\n ) as month,\n MAX(\n if(\n _production_deployment_days.day is not null,\n 1,\n null\n )\n ) as months_deployed,\n COUNT(distinct _production_deployment_days.day) as days_deployed\n FROM\n last_few_calendar_months\n LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day\n GROUP BY\n month\n),\n_days_six_months_deploy AS (\n SELECT\n month,\n SUM(days_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS days_deployed_per_six_months,\n COUNT(months_deployed) OVER (\n ORDER BY\n month ROWS BETWEEN 5 PRECEDING\n AND CURRENT ROW\n ) AS months_deployed_count,\n ROW_NUMBER() OVER (\n PARTITION BY DATE_FORMAT(month, '%Y-%m') DIV 6\n ORDER BY\n month DESC\n ) AS rn\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_week_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_weekly_deploy\n),\n_median_number_of_deployment_days_per_week as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_week\n FROM\n _median_number_of_deployment_days_per_week_ranks\n WHERE\n ranks <= 0.5\n),\n_median_number_of_deployment_days_per_month_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed\n ) as ranks\n FROM\n _days_monthly_deploy\n),\n_median_number_of_deployment_days_per_month as(\n SELECT\n max(days_deployed) as median_number_of_deployment_days_per_month\n FROM\n _median_number_of_deployment_days_per_month_ranks\n WHERE\n ranks <= 0.5\n),\n_days_per_six_months_deploy_by_filter AS (\n SELECT\n month,\n days_deployed_per_six_months,\n months_deployed_count\n FROM\n _days_six_months_deploy\n WHERE\n rn % 6 = 1\n),\n_median_number_of_deployment_days_per_six_months_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n days_deployed_per_six_months\n ) as ranks\n FROM\n _days_per_six_months_deploy_by_filter\n),\n_median_number_of_deployment_days_per_six_months as(\n SELECT\n min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months,\n min(months_deployed_count) as is_collected\n FROM\n _median_number_of_deployment_days_per_six_months_ranks\n WHERE\n ranks >= 0.5\n),\n_metric_deployment_frequency as (\n SELECT\n 'Deployment frequency' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)'\n WHEN median_number_of_deployment_days_per_month < 1\n and is_collected is not null THEN 'Fewer than once per month(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)'\n WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)'\n WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)'\n WHEN median_number_of_deployment_days_per_six_months < 1\n and is_collected is not null THEN 'Fewer than once per six months(low)'\n ELSE \"N/A. Please check if you have collected deployments.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_number_of_deployment_days_per_week,\n _median_number_of_deployment_days_per_month,\n _median_number_of_deployment_days_per_six_months\n),\n-- Metric 2: median lead time for changes\n_pr_stats as (\n -- get the cycle time of PRs deployed by the deployments finished in the selected period\n SELECT\n distinct pr.id,\n ppm.pr_cycle_time\n FROM\n pull_requests pr\n join user_accounts ua on pr.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n join project_pr_metrics ppm on ppm.id = pr.id\n join project_mapping pm on pr.base_repo_id = pm.row_id\n and pm.`table` = 'repos'\n join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id\n WHERE\n t.name in (${team:singlequote})\n and pr.merged_date is not null\n and ppm.pr_cycle_time is not null\n and $__timeFilter(cdc.finished_date)\n),\n_median_change_lead_time_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n pr_cycle_time\n ) as ranks\n FROM\n _pr_stats\n),\n_median_change_lead_time as(\n -- use median PR cycle time as the median change lead time\n SELECT\n max(pr_cycle_time) as median_change_lead_time\n FROM\n _median_change_lead_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_change_lead_time as (\n SELECT\n 'Lead time for changes' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_change_lead_time < 24 * 60 THEN \"Less than one day(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Between one day and one week(high)\"\n WHEN median_change_lead_time < 30 * 24 * 60 THEN \"Between one week and one month(medium)\"\n WHEN median_change_lead_time >= 30 * 24 * 60 THEN \"More than one month(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_change_lead_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_change_lead_time < 7 * 24 * 60 THEN \"Less than one week(high)\"\n WHEN median_change_lead_time < 180 * 24 * 60 THEN \"Between one week and six months(medium)\"\n WHEN median_change_lead_time >= 180 * 24 * 60 THEN \"More than six months(low)\"\n ELSE \"N/A. Please check if you have collected deployments/pull_requests.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _median_change_lead_time\n),\n-- Metric 3: change failure rate\n_deployments as (\n -- When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date.\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN commits c on cdc.commit_sha = c.sha\n join user_accounts ua on c.author_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n t.name in (${team:singlequote})\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_failure_caused_by_deployments as (\n -- calculate the number of incidents caused by each deployment\n SELECT\n d.deployment_id,\n d.deployment_finished_date,\n count(\n distinct case\n when i.id is not null then d.deployment_id\n else null\n end\n ) as has_incident\n FROM\n _deployments d\n left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id\n left join incidents i on pim.id = i.id\n GROUP BY\n 1,\n 2\n),\n_change_failure_rate as (\n SELECT\n case\n when count(deployment_id) is null then null\n else sum(has_incident) / count(deployment_id)\n end as change_failure_rate\n FROM\n _failure_caused_by_deployments\n),\n_is_collected_data as(\n SELECT\n CASE\n WHEN COUNT(i.id) = 0\n AND COUNT(cdc.id) = 0 THEN 'No All'\n WHEN COUNT(i.id) = 0 THEN 'No Incidents'\n WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'\n END AS is_collected\n FROM\n (\n SELECT\n 1\n ) AS dummy\n LEFT JOIN incidents i ON 1 = 1\n LEFT JOIN cicd_deployment_commits cdc ON 1 = 1\n),\n_metric_cfr as (\n SELECT\n 'Change failure rate' as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.05 THEN \"0-5%(elite)\"\n WHEN change_failure_rate <=.10 THEN \"5%-10%(high)\"\n WHEN change_failure_rate <=.15 THEN \"10%-15%(medium)\"\n WHEN change_failure_rate >.15 THEN \"> 15%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN is_collected = \"No All\" THEN \"N/A. Please check if you have collected deployments/incidents.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN change_failure_rate <=.15 THEN \"0-15%(elite)\"\n WHEN change_failure_rate <=.20 THEN \"16%-20%(high)\"\n WHEN change_failure_rate <=.30 THEN \"21%-30%(medium)\"\n WHEN change_failure_rate >.30 THEN \"> 30%(low)\"\n ELSE \"N/A. Please check if you have collected deployments/incidents.\"\n END\n ELSE 'Invalid dora report'\n END AS value\n FROM\n _change_failure_rate,\n _is_collected_data\n),\n-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_recovery_time_2023_report as(\n SELECT\n \"Failed deployment recovery time\" as metric,\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN median_recovery_time < 60 THEN \"Less than one hour(elite)\"\n WHEN median_recovery_time < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_recovery_time < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_recovery_time >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected deployments or incidents.\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n join user_accounts ua on i.assignee_id = ua.account_id\n join users u on ua.user_id = u.id\n join team_users tu on u.id = tu.user_id\n join teams t on tu.team_id = t.id\n WHERE\n t.name in (${team:singlequote})\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n \"Time to restore service\" as metric,\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n),\n_metric_mrt_or_mm as(\n SELECT\n metric,\n median_recovery_time AS value\n FROM\n _metric_recovery_time_2023_report\n WHERE\n ('$dora_report') = '2023'\n UNION\n SELECT\n metric,\n median_time_to_resolve AS value\n FROM\n _metric_mttr_2021_report\n WHERE\n ('$dora_report') = '2021'\n),\n_final_results as (\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m1.metric as _metric,\n m1.value\n FROM\n dora_benchmarks db\n left join _metric_deployment_frequency m1 on db.metric = m1.metric\n WHERE\n m1.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m2.metric as _metric,\n m2.value\n FROM\n dora_benchmarks db\n left join _metric_change_lead_time m2 on db.metric = m2.metric\n WHERE\n m2.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m3.metric as _metric,\n m3.value\n FROM\n dora_benchmarks db\n left join _metric_cfr m3 on db.metric = m3.metric\n WHERE\n m3.metric is not null\n and db.dora_report = ('$dora_report')\n union\n SELECT\n distinct db.id,\n db.metric,\n db.low,\n db.medium,\n db.high,\n db.elite,\n m4.metric as _metric,\n m4.value\n FROM\n dora_benchmarks db\n left join _metric_mrt_or_mm m4 on db.metric = m4.metric\n WHERE\n m4.metric is not null\n and db.dora_report = ('$dora_report')\n)\nSELECT\n metric,\n case\n when low = value then low\n else null\n end as low,\n case\n when medium = value then medium\n else null\n end as medium,\n case\n when high = value then high\n else null\n end as high,\n case\n when elite = value then elite\n else null\n end as elite\nFROM\n _final_results\nORDER BY\n id", "refId": "A", "sql": { "columns": [ diff --git a/grafana/dashboards/mysql/dora-debug.json b/grafana/dashboards/mysql/dora-debug.json index 4f83fcd86d2..e10caf9f5c4 100644 --- a/grafana/dashboards/mysql/dora-debug.json +++ b/grafana/dashboards/mysql/dora-debug.json @@ -2930,7 +2930,7 @@ "metricColumn": "none", "queryType": "randomWalk", "rawQuery": true, - "rawSql": "-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n\n_is_collected_data as(\n SELECT\n CASE\n WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 AND EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No deployments and incidents'\n WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 THEN 'No Deployments'\n WHEN EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No Incidents'\n Else 'No incidents are mapped to deployments'\n END AS is_collected\n FROM\n _deployments d, _incidents_for_deployments i\n),\n\n_metric_recovery_time_2023_report as(\n SELECT\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No deployments and incidents\" THEN \"N/A. Please check if you have collected deployments and incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN median_recovery_time < 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(elite)\")\n WHEN median_recovery_time < 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(high)\")\n WHEN median_recovery_time < 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(medium)\")\n WHEN median_recovery_time >= 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(low)\")\n ELSE \"No data\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time,\n _is_collected_data\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n WHERE\n pm.project_name in (${project})\n and $__timeFilter(i.created_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n)\nSELECT\n median_recovery_time AS median_time_in_hour\nFROM\n _metric_recovery_time_2023_report\nWHERE\n ('$dora_report') = '2023'\nUNION\nSELECT\n median_time_to_resolve AS median_time_to_resolve\nFROM\n _metric_mttr_2021_report\nWHERE\n ('$dora_report') = '2021'", + "rawSql": "-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n_recovery_time_ranks as (\n SELECT\n *,\n percent_rank() over(\n order by\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as ranks\n FROM\n _incidents_for_deployments\n),\n_median_recovery_time as (\n SELECT\n max(\n TIMESTAMPDIFF(\n MINUTE,\n deployment_finished_date,\n incident_resolution_date\n )\n ) as median_recovery_time\n FROM\n _recovery_time_ranks\n WHERE\n ranks <= 0.5\n),\n\n_is_collected_data as(\n SELECT\n CASE\n WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 AND EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No deployments and incidents'\n WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 THEN 'No Deployments'\n WHEN EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No Incidents'\n Else 'No incidents are mapped to deployments'\n END AS is_collected\n FROM\n _deployments d, _incidents_for_deployments i\n),\n\n_metric_recovery_time_2023_report as(\n SELECT\n CASE\n WHEN ('$dora_report') = '2023' THEN CASE\n WHEN is_collected = \"No deployments and incidents\" THEN \"N/A. Please check if you have collected deployments and incidents.\"\n WHEN is_collected = \"No Deployments\" THEN \"N/A. Please check if you have collected deployments.\"\n WHEN is_collected = \"No Incidents\" THEN \"N/A. Please check if you have collected incidents.\"\n WHEN median_recovery_time < 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(elite)\")\n WHEN median_recovery_time < 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(high)\")\n WHEN median_recovery_time < 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(medium)\")\n WHEN median_recovery_time >= 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), \"(low)\")\n ELSE \"No data\"\n END\n END AS median_recovery_time\n FROM\n _median_recovery_time,\n _is_collected_data\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n WHERE\n pm.project_name in (${project})\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.created_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n),\n_metric_mttr_2021_report as(\n SELECT\n CASE\n WHEN ('$dora_report') = '2021' THEN CASE\n WHEN median_time_to_resolve < 60 THEN \"Less than one hour(elite)\"\n WHEN median_time_to_resolve < 24 * 60 THEN \"Less than one day(high)\"\n WHEN median_time_to_resolve < 7 * 24 * 60 THEN \"Between one day and one week(medium)\"\n WHEN median_time_to_resolve >= 7 * 24 * 60 THEN \"More than one week(low)\"\n ELSE \"N/A. Please check if you have collected incidents.\"\n END\n END AS median_time_to_resolve\n FROM\n _median_mttr\n)\nSELECT\n median_recovery_time AS median_time_in_hour\nFROM\n _metric_recovery_time_2023_report\nWHERE\n ('$dora_report') = '2023'\nUNION\nSELECT\n median_time_to_resolve AS median_time_to_resolve\nFROM\n _metric_mttr_2021_report\nWHERE\n ('$dora_report') = '2021'", "refId": "A", "select": [ [ diff --git a/grafana/dashboards/mysql/dora-details-change-failure-rate.json b/grafana/dashboards/mysql/dora-details-change-failure-rate.json index 878540d0fdb..c66cbedba2d 100644 --- a/grafana/dashboards/mysql/dora-details-change-failure-rate.json +++ b/grafana/dashboards/mysql/dora-details-change-failure-rate.json @@ -627,7 +627,7 @@ "metricColumn": "none", "queryType": "randomWalk", "rawQuery": true, - "rawSql": "with _deployments as(\n select\n distinct d.cicd_deployment_id as deployment_id,\n d.result,\n d.environment,\n d.finished_date,\n d.cicd_scope_id,\n pm.project_name\n from\n cicd_deployment_commits d\n join project_mapping pm on d.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n where\n -- only result needs to specified, not environment\n d.result = 'SUCCESS' -- choose your project_name\n and pm.project_name in ($project)\n and $__timeFilter(d.finished_date)\n),\n_incidents as(\n select\n distinct i.id as issue_id,\n i.created_date,\n pm.project_name\n from\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and i.`table` = pm.`table`\n where\n -- choose your project_name\n pm.project_name in ($project)\n and $__timeFilter(i.created_date)\n)\nselect\n finished_date as 'Time (Ascending)',\n deployment_id as 'Entity ID',\n 'DEPLOYMENT' as 'Entity Type (Deployment/Incident)'\nfrom\n _deployments\nunion\nselect\n created_date as 'Time (Ascending)',\n issue_id as 'Entity ID',\n 'INCIDENT' as 'Entity Type (Deployment/Incident)'\nfrom\n _incidents\norder by\n 1", + "rawSql": "with _deployments as(\n select\n distinct d.cicd_deployment_id as deployment_id,\n d.result,\n d.environment,\n d.finished_date,\n d.cicd_scope_id,\n pm.project_name\n from\n cicd_deployment_commits d\n join project_mapping pm on d.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n where\n -- only result needs to specified, not environment\n d.result = 'SUCCESS' -- choose your project_name\n and pm.project_name in ($project)\n and $__timeFilter(d.finished_date)\n),\n_incidents as(\n select\n distinct i.id as issue_id,\n i.created_date,\n pm.project_name\n from\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and i.`table` = pm.`table`\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n where\n -- choose your project_name\n pm.project_name in ($project)\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.created_date)\n)\nselect\n finished_date as 'Time (Ascending)',\n deployment_id as 'Entity ID',\n 'DEPLOYMENT' as 'Entity Type (Deployment/Incident)'\nfrom\n _deployments\nunion\nselect\n created_date as 'Time (Ascending)',\n issue_id as 'Entity ID',\n 'INCIDENT' as 'Entity Type (Deployment/Incident)'\nfrom\n _incidents\norder by\n 1", "refId": "A", "select": [ [ diff --git a/grafana/dashboards/mysql/dora-details-failed-deployment-recovery-time.json b/grafana/dashboards/mysql/dora-details-failed-deployment-recovery-time.json index aabdd461e4c..526c1dbcd91 100644 --- a/grafana/dashboards/mysql/dora-details-failed-deployment-recovery-time.json +++ b/grafana/dashboards/mysql/dora-details-failed-deployment-recovery-time.json @@ -409,7 +409,7 @@ "format": "table", "hide": false, "rawQuery": true, - "rawSql": "-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and i.`table` = pm.`table`\n and pm.`table` = 'boards'\n WHERE\n pm.project_name in (${project})\n and $__timeFilter(i.created_date)\n)\nSELECT\n COUNT(incident_id) AS total_count\nFROM\n _incidents_for_deployments", + "rawSql": "-- ***** 2023 report ***** --\n-- Metric 4: Failed deployment recovery time\nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents_for_deployments as (\n SELECT\n i.id as incident_id,\n i.created_date as incident_create_date,\n i.resolution_date as incident_resolution_date,\n fd.deployment_id as caused_by_deployment,\n fd.deployment_finished_date,\n date_format(fd.deployment_finished_date, '%y/%m') as deployment_finished_month\n FROM\n incidents i\n left join project_incident_deployment_relationships pim on i.id = pim.id\n join _deployments fd on pim.deployment_id = fd.deployment_id\n WHERE\n $__timeFilter(i.resolution_date)\n),\n-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and i.`table` = pm.`table`\n and pm.`table` = 'boards'\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n WHERE\n pm.project_name in (${project})\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.created_date)\n)\nSELECT\n COUNT(incident_id) AS total_count\nFROM\n _incidents_for_deployments", "refId": "D", "sql": { "columns": [ diff --git a/grafana/dashboards/mysql/dora-details-timeto-restore-service.json b/grafana/dashboards/mysql/dora-details-timeto-restore-service.json index 127ec734ed9..9a6e7c5e8af 100644 --- a/grafana/dashboards/mysql/dora-details-timeto-restore-service.json +++ b/grafana/dashboards/mysql/dora-details-timeto-restore-service.json @@ -121,7 +121,7 @@ "format": "table", "hide": false, "rawQuery": true, - "rawSql": "-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n WHERE\n pm.project_name in (${project})\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n)\nSELECT\n median_time_to_resolve / 60 AS median_time_to_resolve_in_hours\nFROM\n _median_mttr", + "rawSql": "-- ***** 2021 report ***** --\n-- Metric 4: Median time to restore service \nwith _deployments as (\n SELECT\n cdc.cicd_deployment_id as deployment_id,\n max(cdc.finished_date) as deployment_finished_date\n FROM\n cicd_deployment_commits cdc\n JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id\n and pm.`table` = 'cicd_scopes'\n WHERE\n pm.project_name in ($project)\n and cdc.result = 'SUCCESS'\n and cdc.environment = 'PRODUCTION'\n GROUP BY\n 1\n HAVING\n $__timeFilter(max(cdc.finished_date))\n),\n_incidents as (\n -- get the incidents created within the selected time period in the top-right corner\n SELECT\n distinct i.id,\n cast(lead_time_minutes as signed) as lead_time_minutes\n FROM\n incidents i\n join project_mapping pm on i.scope_id = pm.row_id\n and pm.`table` = i.`table`\n left join board_issues bi on bi.issue_id = i.id and bi.board_id = i.scope_id\n WHERE\n pm.project_name in (${project})\n and (\n bi.project_name in (${project})\n or not exists (\n select 1 from board_issues b2\n where b2.board_id = i.scope_id\n and b2.project_name is not null and b2.project_name <> ''\n )\n )\n and $__timeFilter(i.resolution_date)\n),\n_median_mttr_ranks as(\n SELECT\n *,\n percent_rank() over(\n order by\n lead_time_minutes\n ) as ranks\n FROM\n _incidents\n),\n_median_mttr as(\n SELECT\n max(lead_time_minutes) as median_time_to_resolve\n FROM\n _median_mttr_ranks\n WHERE\n ranks <= 0.5\n)\nSELECT\n median_time_to_resolve / 60 AS median_time_to_resolve_in_hours\nFROM\n _median_mttr", "refId": "D", "sql": { "columns": [ diff --git a/grafana/dashboards/postgresql/DORA.json b/grafana/dashboards/postgresql/DORA.json index d1aaa0dc2e9..74c3880fa80 100644 --- a/grafana/dashboards/postgresql/DORA.json +++ b/grafana/dashboards/postgresql/DORA.json @@ -221,7 +221,7 @@ "metricColumn": "none", "queryType": "randomWalk", "rawQuery": true, - "rawSql": "/* Optimized DORA Metrics Query for PostgreSQL/Grafana */\r\n\r\n/* Metric 1: Deployment Frequency */\r\nWITH last_few_calendar_months AS (\r\n /* Generate calendar days efficiently using generate_series instead of cartesian product */\r\n SELECT date::DATE AS day\r\n FROM generate_series(\r\n $__timeFrom()::timestamp,\r\n ($__timeTo()::timestamp - interval '1 day'),\r\n interval '1 day'\r\n ) AS date\r\n),\r\n\r\n_production_deployment_days AS (\r\n /* Get deployment days with single pass aggregation */\r\n SELECT\r\n cdc.cicd_deployment_id AS deployment_id,\r\n MAX(cdc.finished_date::DATE) AS day\r\n FROM cicd_deployment_commits cdc\r\n INNER JOIN project_mapping pm ON\r\n cdc.cicd_scope_id = pm.row_id\r\n AND pm.\"table\" = 'cicd_scopes'\r\n WHERE\r\n cdc.result = 'SUCCESS'\r\n AND cdc.environment = 'PRODUCTION'\r\n AND cdc.finished_date BETWEEN $__timeFrom() AND $__timeTo()\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n GROUP BY cdc.cicd_deployment_id\r\n),\r\n\r\n_deployment_stats AS (\r\n /* Consolidate weekly, monthly, and six-monthly stats in single pass */\r\n SELECT\r\n DATE_TRUNC('week', cal.day)::DATE AS week,\r\n DATE_TRUNC('month', cal.day)::DATE AS month,\r\n cal.day,\r\n dep.day AS deployment_day\r\n FROM last_few_calendar_months cal\r\n LEFT JOIN _production_deployment_days dep ON dep.day = cal.day\r\n),\r\n\r\n_weekly_summary AS (\r\n /* Aggregate weekly deployment days */\r\n SELECT\r\n week,\r\n MAX(CASE WHEN deployment_day IS NOT NULL THEN 1 ELSE 0 END) AS weeks_deployed,\r\n COUNT(DISTINCT deployment_day) AS days_deployed\r\n FROM _deployment_stats\r\n GROUP BY week\r\n),\r\n\r\n_monthly_summary AS (\r\n /* Aggregate monthly deployment days */\r\n SELECT\r\n month,\r\n MAX(CASE WHEN deployment_day IS NOT NULL THEN 1 ELSE NULL END) AS months_deployed,\r\n COUNT(DISTINCT deployment_day) AS days_deployed\r\n FROM _deployment_stats\r\n GROUP BY month\r\n),\r\n\r\n_six_months_rolling AS (\r\n /* Calculate rolling 6-month windows */\r\n SELECT\r\n month,\r\n SUM(days_deployed) OVER (\r\n ORDER BY month\r\n ROWS BETWEEN 5 PRECEDING AND CURRENT ROW\r\n ) AS days_deployed_per_six_months,\r\n COUNT(months_deployed) OVER (\r\n ORDER BY month\r\n ROWS BETWEEN 5 PRECEDING AND CURRENT ROW\r\n ) AS months_deployed_count,\r\n ROW_NUMBER() OVER (\r\n PARTITION BY (EXTRACT(YEAR FROM month)::INTEGER * 12 + EXTRACT(MONTH FROM month)::INTEGER) / 6\r\n ORDER BY month DESC\r\n ) AS rn\r\n FROM _monthly_summary\r\n),\r\n\r\n_six_months_summary AS (\r\n /* Filter to one record per 6-month period */\r\n SELECT\r\n month,\r\n days_deployed_per_six_months,\r\n months_deployed_count\r\n FROM _six_months_rolling\r\n WHERE rn = 1\r\n),\r\n\r\n_median_deployment_frequency AS (\r\n /* Calculate all medians using percentile_cont for efficiency */\r\n SELECT\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed)\r\n FROM _weekly_summary) AS median_weekly,\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed)\r\n FROM _monthly_summary) AS median_monthly,\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed_per_six_months)\r\n FROM _six_months_summary) AS median_six_monthly,\r\n (SELECT MIN(months_deployed_count)\r\n FROM _six_months_summary\r\n WHERE months_deployed_count > 0) AS is_collected\r\n),\r\n\r\n_metric_deployment_frequency AS (\r\n SELECT\r\n 'Deployment frequency' AS metric,\r\n CASE '$dora_report'\r\n WHEN '2023' THEN\r\n CASE\r\n WHEN median_weekly >= 5 THEN 'On-demand(elite)'\r\n WHEN median_weekly >= 1 THEN 'Between once per day and once per week(high)'\r\n WHEN median_monthly >= 1 THEN 'Between once per week and once per month(medium)'\r\n WHEN median_monthly < 1 AND is_collected IS NOT NULL THEN 'Fewer than once per month(low)'\r\n ELSE 'N/A. Please check if you have collected deployments.'\r\n END\r\n WHEN '2021' THEN\r\n CASE\r\n WHEN median_weekly >= 5 THEN 'On-demand(elite)'\r\n WHEN median_monthly >= 1 THEN 'Between once per day and once per month(high)'\r\n WHEN median_six_monthly >= 1 THEN 'Between once per month and once every 6 months(medium)'\r\n WHEN median_six_monthly < 1 AND is_collected IS NOT NULL THEN 'Fewer than once per six months(low)'\r\n ELSE 'N/A. Please check if you have collected deployments.'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM _median_deployment_frequency\r\n),\r\n\r\n/* Metric 2: Lead Time for Changes - Direct from PRs (Webhook-friendly) */\r\n_pr_cycle_times AS (\r\n /* Calculate lead time directly from merged PRs without deployment dependency */\r\n SELECT ppm.pr_cycle_time\r\n FROM pull_requests pr\r\n INNER JOIN project_pr_metrics ppm ON ppm.id = pr.id\r\n INNER JOIN project_mapping pm ON\r\n pr.base_repo_id = pm.row_id\r\n AND pm.\"table\" = 'repos'\r\n WHERE\r\n pr.merged_date IS NOT NULL\r\n AND ppm.pr_cycle_time IS NOT NULL\r\n AND $__timeFilter(pr.merged_date) -- Filter by PR merge date instead of deployment\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n),\r\n\r\n_metric_change_lead_time AS (\r\n SELECT\r\n 'Lead time for changes' AS metric,\r\n CASE '$dora_report'\r\n WHEN '2023' THEN\r\n CASE\r\n WHEN median_lt IS NULL THEN 'N/A. Please check if you have collected pull_requests.'\r\n WHEN median_lt < 24 * 60 THEN 'Less than one day(elite)'\r\n WHEN median_lt < 7 * 24 * 60 THEN 'Between one day and one week(high)'\r\n WHEN median_lt < 30 * 24 * 60 THEN 'Between one week and one month(medium)'\r\n ELSE 'More than one month(low)'\r\n END\r\n WHEN '2021' THEN\r\n CASE\r\n WHEN median_lt IS NULL THEN 'N/A. Please check if you have collected pull_requests.'\r\n WHEN median_lt < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_lt < 7 * 24 * 60 THEN 'Less than one week(high)'\r\n WHEN median_lt < 180 * 24 * 60 THEN 'Between one week and six months(medium)'\r\n ELSE 'More than six months(low)'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY pr_cycle_time) AS median_lt\r\n FROM _pr_cycle_times\r\n ) AS medians\r\n),\r\n\r\n/* Metric 3: Change Failure Rate */\r\n_deployments AS (\r\n SELECT\r\n cdc.cicd_deployment_id AS deployment_id,\r\n MAX(cdc.finished_date) AS deployment_finished_date\r\n FROM cicd_deployment_commits cdc\r\n INNER JOIN project_mapping pm ON\r\n cdc.cicd_scope_id = pm.row_id\r\n AND pm.\"table\" = 'cicd_scopes'\r\n WHERE\r\n cdc.result = 'SUCCESS'\r\n AND cdc.environment = 'PRODUCTION'\r\n AND cdc.finished_date BETWEEN $__timeFrom() AND $__timeTo()\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n GROUP BY cdc.cicd_deployment_id\r\n),\r\n\r\n_change_failure_rate AS (\r\n SELECT\r\n COALESCE(\r\n COUNT(DISTINCT CASE WHEN i.id IS NOT NULL THEN d.deployment_id END)::NUMERIC\r\n / NULLIF(COUNT(DISTINCT d.deployment_id), 0),\r\n 0\r\n ) AS cfr,\r\n COUNT(DISTINCT d.deployment_id) AS deployment_count,\r\n COUNT(DISTINCT i.id) AS incident_count\r\n FROM _deployments d\r\n LEFT JOIN project_incident_deployment_relationships pim ON d.deployment_id = pim.deployment_id\r\n LEFT JOIN incidents i ON pim.id = i.id\r\n),\r\n\r\n_metric_cfr AS (\r\n SELECT\r\n 'Change failure rate' AS metric,\r\n CASE\r\n WHEN deployment_count = 0 AND incident_count = 0 THEN\r\n 'N/A. Please check if you have collected deployments/incidents.'\r\n WHEN incident_count = 0 THEN\r\n 'N/A. Please check if you have collected incidents.'\r\n WHEN deployment_count = 0 THEN\r\n 'N/A. Please check if you have collected deployments.'\r\n WHEN '$dora_report' = '2023' THEN\r\n CASE\r\n WHEN cfr <= 0.05 THEN '0-5%(elite)'\r\n WHEN cfr <= 0.10 THEN '5%-10%(high)'\r\n WHEN cfr <= 0.15 THEN '10%-15%(medium)'\r\n ELSE '> 15%(low)'\r\n END\r\n WHEN '$dora_report' = '2021' THEN\r\n CASE\r\n WHEN cfr <= 0.15 THEN '0-15%(elite)'\r\n WHEN cfr <= 0.20 THEN '16%-20%(high)'\r\n WHEN cfr <= 0.30 THEN '21%-30%(medium)'\r\n ELSE '> 30%(low)'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM _change_failure_rate\r\n),\r\n\r\n/* Metric 4: Recovery Time (2023) / MTTR (2021) */\r\n_recovery_times_2023 AS (\r\n SELECT\r\n EXTRACT(EPOCH FROM (i.resolution_date - d.deployment_finished_date)) / 60 AS recovery_minutes\r\n FROM incidents i\r\n INNER JOIN project_incident_deployment_relationships pim ON i.id = pim.id\r\n INNER JOIN _deployments d ON pim.deployment_id = d.deployment_id\r\n WHERE\r\n i.resolution_date IS NOT NULL\r\n AND $__timeFilter(i.resolution_date)\r\n),\r\n\r\n_metric_recovery_time_2023 AS (\r\n SELECT\r\n 'Failed deployment recovery time' AS metric,\r\n CASE\r\n WHEN median_rt < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_rt < 24 * 60 THEN 'Less than one day(high)'\r\n WHEN median_rt < 7 * 24 * 60 THEN 'Between one day and one week(medium)'\r\n WHEN median_rt >= 7 * 24 * 60 THEN 'More than one week(low)'\r\n ELSE 'N/A. Please check if you have collected deployments or incidents.'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY recovery_minutes) AS median_rt\r\n FROM _recovery_times_2023\r\n ) AS medians\r\n WHERE '$dora_report' = '2023'\r\n),\r\n\r\n_mttr_2021 AS (\r\n SELECT\r\n 'Time to restore service' AS metric,\r\n CASE\r\n WHEN median_mttr < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_mttr < 24 * 60 THEN 'Less than one day(high)'\r\n WHEN median_mttr < 7 * 24 * 60 THEN 'Between one day and one week(medium)'\r\n WHEN median_mttr >= 7 * 24 * 60 THEN 'More than one week(low)'\r\n ELSE 'N/A. Please check if you have collected incidents.'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY lead_time_minutes) AS median_mttr\r\n FROM incidents i\r\n INNER JOIN project_mapping pm ON\r\n i.scope_id = pm.row_id\r\n AND pm.\"table\" = i.\"table\"\r\n WHERE\r\n i.lead_time_minutes IS NOT NULL\r\n AND $__timeFilter(i.resolution_date)\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n ) AS medians\r\n WHERE '$dora_report' = '2021'\r\n),\r\n\r\n_all_metrics AS (\r\n SELECT metric, value FROM _metric_deployment_frequency\r\n UNION ALL\r\n SELECT metric, value FROM _metric_change_lead_time\r\n UNION ALL\r\n SELECT metric, value FROM _metric_cfr\r\n UNION ALL\r\n SELECT metric, value FROM _metric_recovery_time_2023\r\n UNION ALL\r\n SELECT metric, value FROM _mttr_2021\r\n)\r\n\r\n/* Final output with benchmark comparison */\r\nSELECT\r\n db.metric,\r\n REPLACE(db.metric, ' ', '-') AS metric_hidden,\r\n CASE WHEN db.low = m.value THEN db.low END AS low,\r\n CASE WHEN db.medium = m.value THEN db.medium END AS medium,\r\n CASE WHEN db.high = m.value THEN db.high END AS high,\r\n CASE WHEN db.elite = m.value THEN db.elite END AS elite\r\nFROM dora_benchmarks db\r\nINNER JOIN _all_metrics m ON db.metric = m.metric\r\nWHERE db.dora_report = '$dora_report'\r\nORDER BY db.id;\r\n\r\n/* Recommended indexes for optimal performance:\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cicd_deployment_commits_lookup\r\n ON cicd_deployment_commits(cicd_scope_id, result, environment, finished_date)\r\n INCLUDE (cicd_deployment_id);\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_mapping_scope\r\n ON project_mapping(row_id, \"table\", project_name);\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pull_requests_merge\r\n ON pull_requests(base_repo_id, merged_date)\r\n WHERE merged_date IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_pr_metrics_deployment\r\n ON project_pr_metrics(deployment_commit_id, pr_cycle_time)\r\n WHERE pr_cycle_time IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_incidents_resolution\r\n ON incidents(scope_id, resolution_date, lead_time_minutes)\r\n WHERE resolution_date IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_incident_deployment\r\n ON project_incident_deployment_relationships(deployment_id, id);\r\n*/\r\n", + "rawSql": "/* Optimized DORA Metrics Query for PostgreSQL/Grafana */\r\n\r\n/* Metric 1: Deployment Frequency */\r\nWITH last_few_calendar_months AS (\r\n /* Generate calendar days efficiently using generate_series instead of cartesian product */\r\n SELECT date::DATE AS day\r\n FROM generate_series(\r\n $__timeFrom()::timestamp,\r\n ($__timeTo()::timestamp - interval '1 day'),\r\n interval '1 day'\r\n ) AS date\r\n),\r\n\r\n_production_deployment_days AS (\r\n /* Get deployment days with single pass aggregation */\r\n SELECT\r\n cdc.cicd_deployment_id AS deployment_id,\r\n MAX(cdc.finished_date::DATE) AS day\r\n FROM cicd_deployment_commits cdc\r\n INNER JOIN project_mapping pm ON\r\n cdc.cicd_scope_id = pm.row_id\r\n AND pm.\"table\" = 'cicd_scopes'\r\n WHERE\r\n cdc.result = 'SUCCESS'\r\n AND cdc.environment = 'PRODUCTION'\r\n AND cdc.finished_date BETWEEN $__timeFrom() AND $__timeTo()\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n GROUP BY cdc.cicd_deployment_id\r\n),\r\n\r\n_deployment_stats AS (\r\n /* Consolidate weekly, monthly, and six-monthly stats in single pass */\r\n SELECT\r\n DATE_TRUNC('week', cal.day)::DATE AS week,\r\n DATE_TRUNC('month', cal.day)::DATE AS month,\r\n cal.day,\r\n dep.day AS deployment_day\r\n FROM last_few_calendar_months cal\r\n LEFT JOIN _production_deployment_days dep ON dep.day = cal.day\r\n),\r\n\r\n_weekly_summary AS (\r\n /* Aggregate weekly deployment days */\r\n SELECT\r\n week,\r\n MAX(CASE WHEN deployment_day IS NOT NULL THEN 1 ELSE 0 END) AS weeks_deployed,\r\n COUNT(DISTINCT deployment_day) AS days_deployed\r\n FROM _deployment_stats\r\n GROUP BY week\r\n),\r\n\r\n_monthly_summary AS (\r\n /* Aggregate monthly deployment days */\r\n SELECT\r\n month,\r\n MAX(CASE WHEN deployment_day IS NOT NULL THEN 1 ELSE NULL END) AS months_deployed,\r\n COUNT(DISTINCT deployment_day) AS days_deployed\r\n FROM _deployment_stats\r\n GROUP BY month\r\n),\r\n\r\n_six_months_rolling AS (\r\n /* Calculate rolling 6-month windows */\r\n SELECT\r\n month,\r\n SUM(days_deployed) OVER (\r\n ORDER BY month\r\n ROWS BETWEEN 5 PRECEDING AND CURRENT ROW\r\n ) AS days_deployed_per_six_months,\r\n COUNT(months_deployed) OVER (\r\n ORDER BY month\r\n ROWS BETWEEN 5 PRECEDING AND CURRENT ROW\r\n ) AS months_deployed_count,\r\n ROW_NUMBER() OVER (\r\n PARTITION BY (EXTRACT(YEAR FROM month)::INTEGER * 12 + EXTRACT(MONTH FROM month)::INTEGER) / 6\r\n ORDER BY month DESC\r\n ) AS rn\r\n FROM _monthly_summary\r\n),\r\n\r\n_six_months_summary AS (\r\n /* Filter to one record per 6-month period */\r\n SELECT\r\n month,\r\n days_deployed_per_six_months,\r\n months_deployed_count\r\n FROM _six_months_rolling\r\n WHERE rn = 1\r\n),\r\n\r\n_median_deployment_frequency AS (\r\n /* Calculate all medians using percentile_cont for efficiency */\r\n SELECT\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed)\r\n FROM _weekly_summary) AS median_weekly,\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed)\r\n FROM _monthly_summary) AS median_monthly,\r\n (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_deployed_per_six_months)\r\n FROM _six_months_summary) AS median_six_monthly,\r\n (SELECT MIN(months_deployed_count)\r\n FROM _six_months_summary\r\n WHERE months_deployed_count > 0) AS is_collected\r\n),\r\n\r\n_metric_deployment_frequency AS (\r\n SELECT\r\n 'Deployment frequency' AS metric,\r\n CASE '$dora_report'\r\n WHEN '2023' THEN\r\n CASE\r\n WHEN median_weekly >= 5 THEN 'On-demand(elite)'\r\n WHEN median_weekly >= 1 THEN 'Between once per day and once per week(high)'\r\n WHEN median_monthly >= 1 THEN 'Between once per week and once per month(medium)'\r\n WHEN median_monthly < 1 AND is_collected IS NOT NULL THEN 'Fewer than once per month(low)'\r\n ELSE 'N/A. Please check if you have collected deployments.'\r\n END\r\n WHEN '2021' THEN\r\n CASE\r\n WHEN median_weekly >= 5 THEN 'On-demand(elite)'\r\n WHEN median_monthly >= 1 THEN 'Between once per day and once per month(high)'\r\n WHEN median_six_monthly >= 1 THEN 'Between once per month and once every 6 months(medium)'\r\n WHEN median_six_monthly < 1 AND is_collected IS NOT NULL THEN 'Fewer than once per six months(low)'\r\n ELSE 'N/A. Please check if you have collected deployments.'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM _median_deployment_frequency\r\n),\r\n\r\n/* Metric 2: Lead Time for Changes - Direct from PRs (Webhook-friendly) */\r\n_pr_cycle_times AS (\r\n /* Calculate lead time directly from merged PRs without deployment dependency */\r\n SELECT ppm.pr_cycle_time\r\n FROM pull_requests pr\r\n INNER JOIN project_pr_metrics ppm ON ppm.id = pr.id\r\n INNER JOIN project_mapping pm ON\r\n pr.base_repo_id = pm.row_id\r\n AND pm.\"table\" = 'repos'\r\n WHERE\r\n pr.merged_date IS NOT NULL\r\n AND ppm.pr_cycle_time IS NOT NULL\r\n AND $__timeFilter(pr.merged_date) -- Filter by PR merge date instead of deployment\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n),\r\n\r\n_metric_change_lead_time AS (\r\n SELECT\r\n 'Lead time for changes' AS metric,\r\n CASE '$dora_report'\r\n WHEN '2023' THEN\r\n CASE\r\n WHEN median_lt IS NULL THEN 'N/A. Please check if you have collected pull_requests.'\r\n WHEN median_lt < 24 * 60 THEN 'Less than one day(elite)'\r\n WHEN median_lt < 7 * 24 * 60 THEN 'Between one day and one week(high)'\r\n WHEN median_lt < 30 * 24 * 60 THEN 'Between one week and one month(medium)'\r\n ELSE 'More than one month(low)'\r\n END\r\n WHEN '2021' THEN\r\n CASE\r\n WHEN median_lt IS NULL THEN 'N/A. Please check if you have collected pull_requests.'\r\n WHEN median_lt < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_lt < 7 * 24 * 60 THEN 'Less than one week(high)'\r\n WHEN median_lt < 180 * 24 * 60 THEN 'Between one week and six months(medium)'\r\n ELSE 'More than six months(low)'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY pr_cycle_time) AS median_lt\r\n FROM _pr_cycle_times\r\n ) AS medians\r\n),\r\n\r\n/* Metric 3: Change Failure Rate */\r\n_deployments AS (\r\n SELECT\r\n cdc.cicd_deployment_id AS deployment_id,\r\n MAX(cdc.finished_date) AS deployment_finished_date\r\n FROM cicd_deployment_commits cdc\r\n INNER JOIN project_mapping pm ON\r\n cdc.cicd_scope_id = pm.row_id\r\n AND pm.\"table\" = 'cicd_scopes'\r\n WHERE\r\n cdc.result = 'SUCCESS'\r\n AND cdc.environment = 'PRODUCTION'\r\n AND cdc.finished_date BETWEEN $__timeFrom() AND $__timeTo()\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n GROUP BY cdc.cicd_deployment_id\r\n),\r\n\r\n_change_failure_rate AS (\r\n SELECT\r\n COALESCE(\r\n COUNT(DISTINCT CASE WHEN i.id IS NOT NULL THEN d.deployment_id END)::NUMERIC\r\n / NULLIF(COUNT(DISTINCT d.deployment_id), 0),\r\n 0\r\n ) AS cfr,\r\n COUNT(DISTINCT d.deployment_id) AS deployment_count,\r\n COUNT(DISTINCT i.id) AS incident_count\r\n FROM _deployments d\r\n LEFT JOIN project_incident_deployment_relationships pim ON d.deployment_id = pim.deployment_id\r\n LEFT JOIN incidents i ON pim.id = i.id\r\n),\r\n\r\n_metric_cfr AS (\r\n SELECT\r\n 'Change failure rate' AS metric,\r\n CASE\r\n WHEN deployment_count = 0 AND incident_count = 0 THEN\r\n 'N/A. Please check if you have collected deployments/incidents.'\r\n WHEN incident_count = 0 THEN\r\n 'N/A. Please check if you have collected incidents.'\r\n WHEN deployment_count = 0 THEN\r\n 'N/A. Please check if you have collected deployments.'\r\n WHEN '$dora_report' = '2023' THEN\r\n CASE\r\n WHEN cfr <= 0.05 THEN '0-5%(elite)'\r\n WHEN cfr <= 0.10 THEN '5%-10%(high)'\r\n WHEN cfr <= 0.15 THEN '10%-15%(medium)'\r\n ELSE '> 15%(low)'\r\n END\r\n WHEN '$dora_report' = '2021' THEN\r\n CASE\r\n WHEN cfr <= 0.15 THEN '0-15%(elite)'\r\n WHEN cfr <= 0.20 THEN '16%-20%(high)'\r\n WHEN cfr <= 0.30 THEN '21%-30%(medium)'\r\n ELSE '> 30%(low)'\r\n END\r\n ELSE 'Invalid dora report'\r\n END AS value\r\n FROM _change_failure_rate\r\n),\r\n\r\n/* Metric 4: Recovery Time (2023) / MTTR (2021) */\r\n_recovery_times_2023 AS (\r\n SELECT\r\n EXTRACT(EPOCH FROM (i.resolution_date - d.deployment_finished_date)) / 60 AS recovery_minutes\r\n FROM incidents i\r\n INNER JOIN project_incident_deployment_relationships pim ON i.id = pim.id\r\n INNER JOIN _deployments d ON pim.deployment_id = d.deployment_id\r\n WHERE\r\n i.resolution_date IS NOT NULL\r\n AND $__timeFilter(i.resolution_date)\r\n),\r\n\r\n_metric_recovery_time_2023 AS (\r\n SELECT\r\n 'Failed deployment recovery time' AS metric,\r\n CASE\r\n WHEN median_rt < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_rt < 24 * 60 THEN 'Less than one day(high)'\r\n WHEN median_rt < 7 * 24 * 60 THEN 'Between one day and one week(medium)'\r\n WHEN median_rt >= 7 * 24 * 60 THEN 'More than one week(low)'\r\n ELSE 'N/A. Please check if you have collected deployments or incidents.'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY recovery_minutes) AS median_rt\r\n FROM _recovery_times_2023\r\n ) AS medians\r\n WHERE '$dora_report' = '2023'\r\n),\r\n\r\n_mttr_2021 AS (\r\n SELECT\r\n 'Time to restore service' AS metric,\r\n CASE\r\n WHEN median_mttr < 60 THEN 'Less than one hour(elite)'\r\n WHEN median_mttr < 24 * 60 THEN 'Less than one day(high)'\r\n WHEN median_mttr < 7 * 24 * 60 THEN 'Between one day and one week(medium)'\r\n WHEN median_mttr >= 7 * 24 * 60 THEN 'More than one week(low)'\r\n ELSE 'N/A. Please check if you have collected incidents.'\r\n END AS value\r\n FROM (\r\n SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY lead_time_minutes) AS median_mttr\r\n FROM incidents i\r\n INNER JOIN project_mapping pm ON\r\n i.scope_id = pm.row_id\r\n AND pm.\"table\" = i.\"table\"\r\n LEFT JOIN board_issues AS bi ON bi.issue_id = i.id AND bi.board_id = i.scope_id\r\n WHERE\r\n i.lead_time_minutes IS NOT NULL\r\n AND $__timeFilter(i.resolution_date)\r\n AND (\r\n '${project:csv}' = ''\r\n OR pm.project_name = ANY(ARRAY[${project:singlequote}]::text[])\r\n )\r\n AND (\r\n ('${project:csv}' = '' OR bi.project_name::text = ANY(ARRAY[${project:singlequote}]::text[]))\r\n OR NOT EXISTS (\r\n SELECT 1 FROM board_issues AS b2\r\n WHERE b2.board_id = i.scope_id\r\n AND b2.project_name IS NOT NULL AND b2.project_name <> ''\r\n )\r\n )\r\n ) AS medians\r\n WHERE '$dora_report' = '2021'\r\n),\r\n\r\n_all_metrics AS (\r\n SELECT metric, value FROM _metric_deployment_frequency\r\n UNION ALL\r\n SELECT metric, value FROM _metric_change_lead_time\r\n UNION ALL\r\n SELECT metric, value FROM _metric_cfr\r\n UNION ALL\r\n SELECT metric, value FROM _metric_recovery_time_2023\r\n UNION ALL\r\n SELECT metric, value FROM _mttr_2021\r\n)\r\n\r\n/* Final output with benchmark comparison */\r\nSELECT\r\n db.metric,\r\n REPLACE(db.metric, ' ', '-') AS metric_hidden,\r\n CASE WHEN db.low = m.value THEN db.low END AS low,\r\n CASE WHEN db.medium = m.value THEN db.medium END AS medium,\r\n CASE WHEN db.high = m.value THEN db.high END AS high,\r\n CASE WHEN db.elite = m.value THEN db.elite END AS elite\r\nFROM dora_benchmarks db\r\nINNER JOIN _all_metrics m ON db.metric = m.metric\r\nWHERE db.dora_report = '$dora_report'\r\nORDER BY db.id;\r\n\r\n/* Recommended indexes for optimal performance:\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cicd_deployment_commits_lookup\r\n ON cicd_deployment_commits(cicd_scope_id, result, environment, finished_date)\r\n INCLUDE (cicd_deployment_id);\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_mapping_scope\r\n ON project_mapping(row_id, \"table\", project_name);\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_pull_requests_merge\r\n ON pull_requests(base_repo_id, merged_date)\r\n WHERE merged_date IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_pr_metrics_deployment\r\n ON project_pr_metrics(deployment_commit_id, pr_cycle_time)\r\n WHERE pr_cycle_time IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_incidents_resolution\r\n ON incidents(scope_id, resolution_date, lead_time_minutes)\r\n WHERE resolution_date IS NOT NULL;\r\n\r\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_incident_deployment\r\n ON project_incident_deployment_relationships(deployment_id, id);\r\n*/\r\n", "refId": "A", "select": [ [ diff --git a/grafana/dashboards/postgresql/dora-by-team.json b/grafana/dashboards/postgresql/dora-by-team.json index b8f227c64ac..464c9646723 100644 --- a/grafana/dashboards/postgresql/dora-by-team.json +++ b/grafana/dashboards/postgresql/dora-by-team.json @@ -206,7 +206,7 @@ "format": "table", "hide": false, "rawQuery": true, - "rawSql": "/* Metric 1: Deployment Frequency */ WITH last_few_calendar_months AS (/* construct the last few calendar months within the selected time period in the top-right corner */ SELECT CAST(($__timeTo()::timestamp - INTERVAL '1 day') AS DATE) AS day FROM (SELECT 0 AS \"H\" UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300) AS \"H\" CROSS JOIN (SELECT 0 AS \"T\" UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60 UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90) AS \"T\" CROSS JOIN (SELECT 0 AS \"U\" UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS \"U\" WHERE ($__timeTo()::timestamp - INTERVAL '1 day') > $__timeFrom()), _production_deployment_days AS (/* When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date. */ SELECT cdc.cicd_deployment_id AS deployment_id, MAX(CAST(cdc.finished_date AS DATE)) AS day FROM cicd_deployment_commits AS cdc JOIN commits AS c ON cdc.commit_sha = c.sha JOIN user_accounts AS ua ON c.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_mapping AS pm ON cdc.cicd_scope_id = pm.row_id AND pm.\"table\" = 'cicd_scopes' WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND cdc.result = 'SUCCESS' AND cdc.environment = 'PRODUCTION' GROUP BY 1), _days_weekly_deploy AS (/* calculate the number of deployment days every week */ SELECT CAST(last_few_calendar_months.day + -(EXTRACT(ISODOW FROM last_few_calendar_months.day) - 1) * INTERVAL '1 day' AS DATE) AS week, MAX(CASE WHEN NOT _production_deployment_days.day IS NULL THEN 1 ELSE 0 END) AS weeks_deployed, COUNT(DISTINCT _production_deployment_days.day) AS days_deployed FROM last_few_calendar_months LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day GROUP BY week), _days_monthly_deploy AS (/* calculate the number of deployment days every month */ SELECT CAST(last_few_calendar_months.day + INTERVAL '1 DAY' * (-EXTRACT(DAY FROM CAST(last_few_calendar_months.day AS DATE)) + 1) AS DATE) AS month, MAX(CASE WHEN NOT _production_deployment_days.day IS NULL THEN 1 ELSE NULL END) AS months_deployed, COUNT(DISTINCT _production_deployment_days.day) AS days_deployed FROM last_few_calendar_months LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day GROUP BY month), _days_six_months_deploy AS (SELECT month, SUM(days_deployed) OVER (ORDER BY month NULLS FIRST ROWS BETWEEN 5 PRECEDING AND CURRENT ROW) AS days_deployed_per_six_months, COUNT(months_deployed) OVER (ORDER BY month NULLS FIRST ROWS BETWEEN 5 PRECEDING AND CURRENT ROW) AS months_deployed_count, ROW_NUMBER() OVER (PARTITION BY ((EXTRACT(YEAR FROM CAST(month AS TIMESTAMP)) * 12 + EXTRACT(MONTH FROM CAST(month AS TIMESTAMP))) / 6) ORDER BY month DESC NULLS LAST) AS rn FROM _days_monthly_deploy), _median_number_of_deployment_days_per_week_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed NULLS FIRST) AS ranks FROM _days_weekly_deploy), _median_number_of_deployment_days_per_week AS (SELECT MAX(days_deployed) AS median_number_of_deployment_days_per_week FROM _median_number_of_deployment_days_per_week_ranks WHERE ranks <= 0.5), _median_number_of_deployment_days_per_month_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed NULLS FIRST) AS ranks FROM _days_monthly_deploy), _median_number_of_deployment_days_per_month AS (SELECT MAX(days_deployed) AS median_number_of_deployment_days_per_month FROM _median_number_of_deployment_days_per_month_ranks WHERE ranks <= 0.5), _days_per_six_months_deploy_by_filter AS (SELECT month, days_deployed_per_six_months, months_deployed_count FROM _days_six_months_deploy WHERE rn % 6 = 1), _median_number_of_deployment_days_per_six_months_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed_per_six_months NULLS FIRST) AS ranks FROM _days_per_six_months_deploy_by_filter), _median_number_of_deployment_days_per_six_months AS (SELECT MIN(days_deployed_per_six_months) AS median_number_of_deployment_days_per_six_months, MIN(months_deployed_count) AS is_collected FROM _median_number_of_deployment_days_per_six_months_ranks WHERE ranks >= 0.5), _metric_deployment_frequency AS (SELECT 'Deployment frequency' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)' WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)' WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)' WHEN median_number_of_deployment_days_per_month < 1 AND NOT is_collected IS NULL THEN 'Fewer than once per month(low)' ELSE 'N/A. Please check if you have collected deployments.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)' WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)' WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)' WHEN median_number_of_deployment_days_per_six_months < 1 AND NOT is_collected IS NULL THEN 'Fewer than once per six months(low)' ELSE 'N/A. Please check if you have collected deployments.' END ELSE 'Invalid dora report' END AS value FROM _median_number_of_deployment_days_per_week, _median_number_of_deployment_days_per_month, _median_number_of_deployment_days_per_six_months), _pr_stats /* Metric 2: median lead time for changes */ AS (/* get the cycle time of PRs deployed by the deployments finished in the selected period */ SELECT DISTINCT pr.id, ppm.pr_cycle_time FROM pull_requests AS pr JOIN user_accounts AS ua ON pr.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_pr_metrics AS ppm ON ppm.id = pr.id JOIN project_mapping AS pm ON pr.base_repo_id = pm.row_id AND pm.\"table\" = 'repos' JOIN cicd_deployment_commits AS cdc ON ppm.deployment_commit_id = cdc.id WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND NOT pr.merged_date IS NULL AND NOT ppm.pr_cycle_time IS NULL AND $__timeFilter(cdc.finished_date)), _median_change_lead_time_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY pr_cycle_time NULLS FIRST) AS ranks FROM _pr_stats), _median_change_lead_time AS (/* use median PR cycle time as the median change lead time */ SELECT MAX(pr_cycle_time) AS median_change_lead_time FROM _median_change_lead_time_ranks WHERE ranks <= 0.5), _metric_change_lead_time AS (SELECT 'Lead time for changes' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_change_lead_time < 24 * 60 THEN 'Less than one day(elite)' WHEN median_change_lead_time < 7 * 24 * 60 THEN 'Between one day and one week(high)' WHEN median_change_lead_time < 30 * 24 * 60 THEN 'Between one week and one month(medium)' WHEN median_change_lead_time >= 30 * 24 * 60 THEN 'More than one month(low)' ELSE 'N/A. Please check if you have collected deployments/pull_requests.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN median_change_lead_time < 60 THEN 'Less than one hour(elite)' WHEN median_change_lead_time < 7 * 24 * 60 THEN 'Less than one week(high)' WHEN median_change_lead_time < 180 * 24 * 60 THEN 'Between one week and six months(medium)' WHEN median_change_lead_time >= 180 * 24 * 60 THEN 'More than six months(low)' ELSE 'N/A. Please check if you have collected deployments/pull_requests.' END ELSE 'Invalid dora report' END AS value FROM _median_change_lead_time), _deployments /* Metric 3: change failure rate */ AS (/* When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date. */ SELECT cdc.cicd_deployment_id AS deployment_id, MAX(cdc.finished_date) AS deployment_finished_date FROM cicd_deployment_commits AS cdc JOIN commits AS c ON cdc.commit_sha = c.sha JOIN user_accounts AS ua ON c.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_mapping AS pm ON cdc.cicd_scope_id = pm.row_id AND pm.\"table\" = 'cicd_scopes' WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND cdc.result = 'SUCCESS' AND cdc.environment = 'PRODUCTION' GROUP BY 1 HAVING MAX(cdc.finished_date) BETWEEN $__timeFrom() AND $__timeTo()), _failure_caused_by_deployments AS (/* calculate the number of incidents caused by each deployment */ SELECT d.deployment_id, d.deployment_finished_date, COUNT(DISTINCT CASE WHEN NOT i.id IS NULL THEN d.deployment_id ELSE NULL END) AS has_incident FROM _deployments AS d LEFT JOIN project_incident_deployment_relationships AS pim ON d.deployment_id = pim.deployment_id LEFT JOIN incidents AS i ON pim.id = i.id GROUP BY 1, 2), _change_failure_rate AS (SELECT CASE WHEN COUNT(deployment_id) IS NULL THEN NULL ELSE CAST(SUM(has_incident) AS NUMERIC) / NULLIF(COUNT(deployment_id), 0) END AS change_failure_rate FROM _failure_caused_by_deployments), _is_collected_data AS (SELECT CASE WHEN COUNT(i.id) = 0 AND COUNT(cdc.id) = 0 THEN 'No All' WHEN COUNT(i.id) = 0 THEN 'No Incidents' WHEN COUNT(cdc.id) = 0 THEN 'No Deployments' END AS is_collected FROM (SELECT 1) AS dummy LEFT JOIN incidents AS i ON 1 = 1 LEFT JOIN cicd_deployment_commits AS cdc ON 1 = 1), _metric_cfr AS (SELECT 'Change failure rate' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.' WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.' WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.' WHEN change_failure_rate <= 0.05 THEN '0-5%(elite)' WHEN change_failure_rate <= 0.10 THEN '5%-10%(high)' WHEN change_failure_rate <= 0.15 THEN '10%-15%(medium)' WHEN change_failure_rate > 0.15 THEN '> 15%(low)' ELSE 'N/A. Please check if you have collected deployments/incidents.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.' WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.' WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.' WHEN change_failure_rate <= 0.15 THEN '0-15%(elite)' WHEN change_failure_rate <= 0.20 THEN '16%-20%(high)' WHEN change_failure_rate <= 0.30 THEN '21%-30%(medium)' WHEN change_failure_rate > 0.30 THEN '> 30%(low)' ELSE 'N/A. Please check if you have collected deployments/incidents.' END ELSE 'Invalid dora report' END AS value FROM _change_failure_rate, _is_collected_data), _incidents_for_deployments /* ***** 2023 report ***** -- */ /* Metric 4: Failed deployment recovery time */ AS (SELECT i.id AS incident_id, i.created_date AS incident_create_date, i.resolution_date AS incident_resolution_date, fd.deployment_id AS caused_by_deployment, fd.deployment_finished_date, TO_CHAR(CAST(fd.deployment_finished_date AS TIMESTAMP), 'YY/MM') AS deployment_finished_month FROM incidents AS i LEFT JOIN project_incident_deployment_relationships AS pim ON i.id = pim.id JOIN _deployments AS fd ON pim.deployment_id = fd.deployment_id WHERE $__timeFilter(i.resolution_date)), _recovery_time_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY (EXTRACT(EPOCH FROM (incident_resolution_date - deployment_finished_date))/60) NULLS FIRST) AS ranks FROM _incidents_for_deployments), _median_recovery_time AS (SELECT MAX((EXTRACT(EPOCH FROM (incident_resolution_date - deployment_finished_date))/60)) AS median_recovery_time FROM _recovery_time_ranks WHERE ranks <= 0.5), _metric_recovery_time_2023_report AS (SELECT 'Failed deployment recovery time' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_recovery_time < 60 THEN 'Less than one hour(elite)' WHEN median_recovery_time < 24 * 60 THEN 'Less than one day(high)' WHEN median_recovery_time < 7 * 24 * 60 THEN 'Between one day and one week(medium)' WHEN median_recovery_time >= 7 * 24 * 60 THEN 'More than one week(low)' ELSE 'N/A. Please check if you have collected deployments or incidents.' END END AS median_recovery_time FROM _median_recovery_time), _incidents /* ***** 2021 report ***** -- */ /* Metric 4: Median time to restore service */ AS (/* get the incidents created within the selected time period in the top-right corner */ SELECT DISTINCT i.id, CAST(lead_time_minutes AS BIGINT) AS lead_time_minutes FROM incidents AS i JOIN project_mapping AS pm ON i.scope_id = pm.row_id AND pm.\"table\" = i.\"table\" JOIN user_accounts AS ua ON i.assignee_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND $__timeFilter(i.resolution_date)), _median_mttr_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY lead_time_minutes NULLS FIRST) AS ranks FROM _incidents), _median_mttr AS (SELECT MAX(lead_time_minutes) AS median_time_to_resolve FROM _median_mttr_ranks WHERE ranks <= 0.5), _metric_mttr_2021_report AS (SELECT 'Time to restore service' AS metric, CASE WHEN ('$dora_report') = '2021' THEN CASE WHEN median_time_to_resolve < 60 THEN 'Less than one hour(elite)' WHEN median_time_to_resolve < 24 * 60 THEN 'Less than one day(high)' WHEN median_time_to_resolve < 7 * 24 * 60 THEN 'Between one day and one week(medium)' WHEN median_time_to_resolve >= 7 * 24 * 60 THEN 'More than one week(low)' ELSE 'N/A. Please check if you have collected incidents.' END END AS median_time_to_resolve FROM _median_mttr), _metric_mrt_or_mm AS (SELECT metric, median_recovery_time AS value FROM _metric_recovery_time_2023_report WHERE ('$dora_report') = '2023' UNION SELECT metric, median_time_to_resolve AS value FROM _metric_mttr_2021_report WHERE ('$dora_report') = '2021'), _final_results AS (SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m1.metric AS _metric, m1.value FROM dora_benchmarks AS db LEFT JOIN _metric_deployment_frequency AS m1 ON db.metric = m1.metric WHERE NOT m1.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m2.metric AS _metric, m2.value FROM dora_benchmarks AS db LEFT JOIN _metric_change_lead_time AS m2 ON db.metric = m2.metric WHERE NOT m2.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m3.metric AS _metric, m3.value FROM dora_benchmarks AS db LEFT JOIN _metric_cfr AS m3 ON db.metric = m3.metric WHERE NOT m3.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m4.metric AS _metric, m4.value FROM dora_benchmarks AS db LEFT JOIN _metric_mrt_or_mm AS m4 ON db.metric = m4.metric WHERE NOT m4.metric IS NULL AND db.dora_report = ('$dora_report')) SELECT metric, CASE WHEN low = value THEN low ELSE NULL END AS low, CASE WHEN medium = value THEN medium ELSE NULL END AS medium, CASE WHEN high = value THEN high ELSE NULL END AS high, CASE WHEN elite = value THEN elite ELSE NULL END AS elite FROM _final_results ORDER BY id NULLS FIRST", + "rawSql": "/* Metric 1: Deployment Frequency */ WITH last_few_calendar_months AS (/* construct the last few calendar months within the selected time period in the top-right corner */ SELECT CAST(($__timeTo()::timestamp - INTERVAL '1 day') AS DATE) AS day FROM (SELECT 0 AS \"H\" UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300) AS \"H\" CROSS JOIN (SELECT 0 AS \"T\" UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30 UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60 UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90) AS \"T\" CROSS JOIN (SELECT 0 AS \"U\" UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS \"U\" WHERE ($__timeTo()::timestamp - INTERVAL '1 day') > $__timeFrom()), _production_deployment_days AS (/* When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date. */ SELECT cdc.cicd_deployment_id AS deployment_id, MAX(CAST(cdc.finished_date AS DATE)) AS day FROM cicd_deployment_commits AS cdc JOIN commits AS c ON cdc.commit_sha = c.sha JOIN user_accounts AS ua ON c.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_mapping AS pm ON cdc.cicd_scope_id = pm.row_id AND pm.\"table\" = 'cicd_scopes' WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND cdc.result = 'SUCCESS' AND cdc.environment = 'PRODUCTION' GROUP BY 1), _days_weekly_deploy AS (/* calculate the number of deployment days every week */ SELECT CAST(last_few_calendar_months.day + -(EXTRACT(ISODOW FROM last_few_calendar_months.day) - 1) * INTERVAL '1 day' AS DATE) AS week, MAX(CASE WHEN NOT _production_deployment_days.day IS NULL THEN 1 ELSE 0 END) AS weeks_deployed, COUNT(DISTINCT _production_deployment_days.day) AS days_deployed FROM last_few_calendar_months LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day GROUP BY week), _days_monthly_deploy AS (/* calculate the number of deployment days every month */ SELECT CAST(last_few_calendar_months.day + INTERVAL '1 DAY' * (-EXTRACT(DAY FROM CAST(last_few_calendar_months.day AS DATE)) + 1) AS DATE) AS month, MAX(CASE WHEN NOT _production_deployment_days.day IS NULL THEN 1 ELSE NULL END) AS months_deployed, COUNT(DISTINCT _production_deployment_days.day) AS days_deployed FROM last_few_calendar_months LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day GROUP BY month), _days_six_months_deploy AS (SELECT month, SUM(days_deployed) OVER (ORDER BY month NULLS FIRST ROWS BETWEEN 5 PRECEDING AND CURRENT ROW) AS days_deployed_per_six_months, COUNT(months_deployed) OVER (ORDER BY month NULLS FIRST ROWS BETWEEN 5 PRECEDING AND CURRENT ROW) AS months_deployed_count, ROW_NUMBER() OVER (PARTITION BY ((EXTRACT(YEAR FROM CAST(month AS TIMESTAMP)) * 12 + EXTRACT(MONTH FROM CAST(month AS TIMESTAMP))) / 6) ORDER BY month DESC NULLS LAST) AS rn FROM _days_monthly_deploy), _median_number_of_deployment_days_per_week_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed NULLS FIRST) AS ranks FROM _days_weekly_deploy), _median_number_of_deployment_days_per_week AS (SELECT MAX(days_deployed) AS median_number_of_deployment_days_per_week FROM _median_number_of_deployment_days_per_week_ranks WHERE ranks <= 0.5), _median_number_of_deployment_days_per_month_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed NULLS FIRST) AS ranks FROM _days_monthly_deploy), _median_number_of_deployment_days_per_month AS (SELECT MAX(days_deployed) AS median_number_of_deployment_days_per_month FROM _median_number_of_deployment_days_per_month_ranks WHERE ranks <= 0.5), _days_per_six_months_deploy_by_filter AS (SELECT month, days_deployed_per_six_months, months_deployed_count FROM _days_six_months_deploy WHERE rn % 6 = 1), _median_number_of_deployment_days_per_six_months_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY days_deployed_per_six_months NULLS FIRST) AS ranks FROM _days_per_six_months_deploy_by_filter), _median_number_of_deployment_days_per_six_months AS (SELECT MIN(days_deployed_per_six_months) AS median_number_of_deployment_days_per_six_months, MIN(months_deployed_count) AS is_collected FROM _median_number_of_deployment_days_per_six_months_ranks WHERE ranks >= 0.5), _metric_deployment_frequency AS (SELECT 'Deployment frequency' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)' WHEN median_number_of_deployment_days_per_week >= 1 THEN 'Between once per day and once per week(high)' WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per week and once per month(medium)' WHEN median_number_of_deployment_days_per_month < 1 AND NOT is_collected IS NULL THEN 'Fewer than once per month(low)' ELSE 'N/A. Please check if you have collected deployments.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN median_number_of_deployment_days_per_week >= 5 THEN 'On-demand(elite)' WHEN median_number_of_deployment_days_per_month >= 1 THEN 'Between once per day and once per month(high)' WHEN median_number_of_deployment_days_per_six_months >= 1 THEN 'Between once per month and once every 6 months(medium)' WHEN median_number_of_deployment_days_per_six_months < 1 AND NOT is_collected IS NULL THEN 'Fewer than once per six months(low)' ELSE 'N/A. Please check if you have collected deployments.' END ELSE 'Invalid dora report' END AS value FROM _median_number_of_deployment_days_per_week, _median_number_of_deployment_days_per_month, _median_number_of_deployment_days_per_six_months), _pr_stats /* Metric 2: median lead time for changes */ AS (/* get the cycle time of PRs deployed by the deployments finished in the selected period */ SELECT DISTINCT pr.id, ppm.pr_cycle_time FROM pull_requests AS pr JOIN user_accounts AS ua ON pr.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_pr_metrics AS ppm ON ppm.id = pr.id JOIN project_mapping AS pm ON pr.base_repo_id = pm.row_id AND pm.\"table\" = 'repos' JOIN cicd_deployment_commits AS cdc ON ppm.deployment_commit_id = cdc.id WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND NOT pr.merged_date IS NULL AND NOT ppm.pr_cycle_time IS NULL AND $__timeFilter(cdc.finished_date)), _median_change_lead_time_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY pr_cycle_time NULLS FIRST) AS ranks FROM _pr_stats), _median_change_lead_time AS (/* use median PR cycle time as the median change lead time */ SELECT MAX(pr_cycle_time) AS median_change_lead_time FROM _median_change_lead_time_ranks WHERE ranks <= 0.5), _metric_change_lead_time AS (SELECT 'Lead time for changes' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_change_lead_time < 24 * 60 THEN 'Less than one day(elite)' WHEN median_change_lead_time < 7 * 24 * 60 THEN 'Between one day and one week(high)' WHEN median_change_lead_time < 30 * 24 * 60 THEN 'Between one week and one month(medium)' WHEN median_change_lead_time >= 30 * 24 * 60 THEN 'More than one month(low)' ELSE 'N/A. Please check if you have collected deployments/pull_requests.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN median_change_lead_time < 60 THEN 'Less than one hour(elite)' WHEN median_change_lead_time < 7 * 24 * 60 THEN 'Less than one week(high)' WHEN median_change_lead_time < 180 * 24 * 60 THEN 'Between one week and six months(medium)' WHEN median_change_lead_time >= 180 * 24 * 60 THEN 'More than six months(low)' ELSE 'N/A. Please check if you have collected deployments/pull_requests.' END ELSE 'Invalid dora report' END AS value FROM _median_change_lead_time), _deployments /* Metric 3: change failure rate */ AS (/* When deploying multiple commits in one pipeline, GitLab and BitBucket may generate more than one deployment. However, DevLake consider these deployments as ONE production deployment and use the last one's finished_date as the finished date. */ SELECT cdc.cicd_deployment_id AS deployment_id, MAX(cdc.finished_date) AS deployment_finished_date FROM cicd_deployment_commits AS cdc JOIN commits AS c ON cdc.commit_sha = c.sha JOIN user_accounts AS ua ON c.author_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id JOIN project_mapping AS pm ON cdc.cicd_scope_id = pm.row_id AND pm.\"table\" = 'cicd_scopes' WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[])) AND cdc.result = 'SUCCESS' AND cdc.environment = 'PRODUCTION' GROUP BY 1 HAVING MAX(cdc.finished_date) BETWEEN $__timeFrom() AND $__timeTo()), _failure_caused_by_deployments AS (/* calculate the number of incidents caused by each deployment */ SELECT d.deployment_id, d.deployment_finished_date, COUNT(DISTINCT CASE WHEN NOT i.id IS NULL THEN d.deployment_id ELSE NULL END) AS has_incident FROM _deployments AS d LEFT JOIN project_incident_deployment_relationships AS pim ON d.deployment_id = pim.deployment_id LEFT JOIN incidents AS i ON pim.id = i.id GROUP BY 1, 2), _change_failure_rate AS (SELECT CASE WHEN COUNT(deployment_id) IS NULL THEN NULL ELSE CAST(SUM(has_incident) AS NUMERIC) / NULLIF(COUNT(deployment_id), 0) END AS change_failure_rate FROM _failure_caused_by_deployments), _is_collected_data AS (SELECT CASE WHEN COUNT(i.id) = 0 AND COUNT(cdc.id) = 0 THEN 'No All' WHEN COUNT(i.id) = 0 THEN 'No Incidents' WHEN COUNT(cdc.id) = 0 THEN 'No Deployments' END AS is_collected FROM (SELECT 1) AS dummy LEFT JOIN incidents AS i ON 1 = 1 LEFT JOIN cicd_deployment_commits AS cdc ON 1 = 1), _metric_cfr AS (SELECT 'Change failure rate' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.' WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.' WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.' WHEN change_failure_rate <= 0.05 THEN '0-5%(elite)' WHEN change_failure_rate <= 0.10 THEN '5%-10%(high)' WHEN change_failure_rate <= 0.15 THEN '10%-15%(medium)' WHEN change_failure_rate > 0.15 THEN '> 15%(low)' ELSE 'N/A. Please check if you have collected deployments/incidents.' END WHEN ('$dora_report') = '2021' THEN CASE WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.' WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.' WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.' WHEN change_failure_rate <= 0.15 THEN '0-15%(elite)' WHEN change_failure_rate <= 0.20 THEN '16%-20%(high)' WHEN change_failure_rate <= 0.30 THEN '21%-30%(medium)' WHEN change_failure_rate > 0.30 THEN '> 30%(low)' ELSE 'N/A. Please check if you have collected deployments/incidents.' END ELSE 'Invalid dora report' END AS value FROM _change_failure_rate, _is_collected_data), _incidents_for_deployments /* ***** 2023 report ***** -- */ /* Metric 4: Failed deployment recovery time */ AS (SELECT i.id AS incident_id, i.created_date AS incident_create_date, i.resolution_date AS incident_resolution_date, fd.deployment_id AS caused_by_deployment, fd.deployment_finished_date, TO_CHAR(CAST(fd.deployment_finished_date AS TIMESTAMP), 'YY/MM') AS deployment_finished_month FROM incidents AS i LEFT JOIN project_incident_deployment_relationships AS pim ON i.id = pim.id JOIN _deployments AS fd ON pim.deployment_id = fd.deployment_id WHERE $__timeFilter(i.resolution_date)), _recovery_time_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY (EXTRACT(EPOCH FROM (incident_resolution_date - deployment_finished_date))/60) NULLS FIRST) AS ranks FROM _incidents_for_deployments), _median_recovery_time AS (SELECT MAX((EXTRACT(EPOCH FROM (incident_resolution_date - deployment_finished_date))/60)) AS median_recovery_time FROM _recovery_time_ranks WHERE ranks <= 0.5), _metric_recovery_time_2023_report AS (SELECT 'Failed deployment recovery time' AS metric, CASE WHEN ('$dora_report') = '2023' THEN CASE WHEN median_recovery_time < 60 THEN 'Less than one hour(elite)' WHEN median_recovery_time < 24 * 60 THEN 'Less than one day(high)' WHEN median_recovery_time < 7 * 24 * 60 THEN 'Between one day and one week(medium)' WHEN median_recovery_time >= 7 * 24 * 60 THEN 'More than one week(low)' ELSE 'N/A. Please check if you have collected deployments or incidents.' END END AS median_recovery_time FROM _median_recovery_time), _incidents /* ***** 2021 report ***** -- */ /* Metric 4: Median time to restore service */ AS (/* get the incidents created within the selected time period in the top-right corner */ SELECT DISTINCT i.id, CAST(lead_time_minutes AS BIGINT) AS lead_time_minutes FROM incidents AS i JOIN project_mapping AS pm ON i.scope_id = pm.row_id AND pm.\"table\" = i.\"table\"\n LEFT JOIN board_issues AS bi ON bi.issue_id = i.id AND bi.board_id = i.scope_id JOIN user_accounts AS ua ON i.assignee_id = ua.account_id JOIN users AS u ON ua.user_id = u.id JOIN team_users AS tu ON u.id = tu.user_id JOIN teams AS t ON tu.team_id = t.id WHERE ('${team:csv}' = '' OR t.name::text = ANY(ARRAY[${team:singlequote}]::text[]))\n AND (\n ('${project:csv}' = '' OR bi.project_name::text = ANY(ARRAY[${project:singlequote}]::text[]))\n OR NOT EXISTS (\n SELECT 1 FROM board_issues AS b2\n WHERE b2.board_id = i.scope_id\n AND b2.project_name IS NOT NULL AND b2.project_name <> ''\n )\n ) AND $__timeFilter(i.resolution_date)), _median_mttr_ranks AS (SELECT *, PERCENT_RANK() OVER (ORDER BY lead_time_minutes NULLS FIRST) AS ranks FROM _incidents), _median_mttr AS (SELECT MAX(lead_time_minutes) AS median_time_to_resolve FROM _median_mttr_ranks WHERE ranks <= 0.5), _metric_mttr_2021_report AS (SELECT 'Time to restore service' AS metric, CASE WHEN ('$dora_report') = '2021' THEN CASE WHEN median_time_to_resolve < 60 THEN 'Less than one hour(elite)' WHEN median_time_to_resolve < 24 * 60 THEN 'Less than one day(high)' WHEN median_time_to_resolve < 7 * 24 * 60 THEN 'Between one day and one week(medium)' WHEN median_time_to_resolve >= 7 * 24 * 60 THEN 'More than one week(low)' ELSE 'N/A. Please check if you have collected incidents.' END END AS median_time_to_resolve FROM _median_mttr), _metric_mrt_or_mm AS (SELECT metric, median_recovery_time AS value FROM _metric_recovery_time_2023_report WHERE ('$dora_report') = '2023' UNION SELECT metric, median_time_to_resolve AS value FROM _metric_mttr_2021_report WHERE ('$dora_report') = '2021'), _final_results AS (SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m1.metric AS _metric, m1.value FROM dora_benchmarks AS db LEFT JOIN _metric_deployment_frequency AS m1 ON db.metric = m1.metric WHERE NOT m1.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m2.metric AS _metric, m2.value FROM dora_benchmarks AS db LEFT JOIN _metric_change_lead_time AS m2 ON db.metric = m2.metric WHERE NOT m2.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m3.metric AS _metric, m3.value FROM dora_benchmarks AS db LEFT JOIN _metric_cfr AS m3 ON db.metric = m3.metric WHERE NOT m3.metric IS NULL AND db.dora_report = ('$dora_report') UNION SELECT DISTINCT db.id, db.metric, db.low, db.medium, db.high, db.elite, m4.metric AS _metric, m4.value FROM dora_benchmarks AS db LEFT JOIN _metric_mrt_or_mm AS m4 ON db.metric = m4.metric WHERE NOT m4.metric IS NULL AND db.dora_report = ('$dora_report')) SELECT metric, CASE WHEN low = value THEN low ELSE NULL END AS low, CASE WHEN medium = value THEN medium ELSE NULL END AS medium, CASE WHEN high = value THEN high ELSE NULL END AS high, CASE WHEN elite = value THEN elite ELSE NULL END AS elite FROM _final_results ORDER BY id NULLS FIRST", "refId": "A", "sql": { "columns": [