Skip to content

Commit

Permalink
Feature/#162 mttr backend (#163)
Browse files Browse the repository at this point in the history
* init commit'

* Added tests

* Test fix

* Test fix
  • Loading branch information
duke-b authored Mar 22, 2024
1 parent e4e979c commit 256acc6
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 12 deletions.
2 changes: 2 additions & 0 deletions devlake-go/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ func metricHandler(client sql_client.ClientInterface) func(w http.ResponseWriter
dfService := service.MetricDfService{Client: client}
mltcService := service.MetricMltcService{Client: client}
cfrService := service.MetricCfrService{Client: client}
mttrService := service.MetricMttrService{Client: client}
serviceMap := map[string]service.Service[models.MetricResponse]{
"df_count": dfService,
"df_average": dfService,
"mltc": mltcService,
"cfr": cfrService,
"mttr": mttrService,
}

return handler(validation.ValidMetricServiceParameters, serviceMap)
Expand Down
12 changes: 9 additions & 3 deletions devlake-go/api/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@ func Test_metricHandler(t *testing.T) {
name: "should throw 400 response when not specifying metric type",
req: httptest.NewRequest(http.MethodGet, "/dora/api/metric?", nil),
expectStatusCode: 400,
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr\n",
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr, mttr\n",
},
{
name: "should throw 400 response when specifying nonsense metric type",
req: httptest.NewRequest(http.MethodGet, "/dora/api/metric?type=not_metric", nil),
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr\n",
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr, mttr\n",
expectStatusCode: 400,
},
{
name: "should throw 400 response when specifying multiple metric types",
req: httptest.NewRequest(http.MethodGet, "/dora/api/metric?type=df_count&type=df_average", nil),
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr\n",
expectBody: "type should be provided as one of the following: df_count, df_average, mltc, cfr, mttr\n",
expectStatusCode: 400,
},
{
Expand All @@ -60,6 +60,12 @@ func Test_metricHandler(t *testing.T) {
expectBody: `{"aggregation":"weekly","dataPoints":[]}` + "\n",
expectStatusCode: 200,
},
{
name: "should return data response when specifying mttr",
req: httptest.NewRequest(http.MethodGet, "/dora/api/metric?type=mttr", nil),
expectBody: `{"aggregation":"weekly","dataPoints":[]}` + "\n",
expectStatusCode: 200,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
25 changes: 25 additions & 0 deletions devlake-go/api/service/metric_mttr_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package service

import (
"github.com/devoteamnl/opendora/api/models"
"github.com/devoteamnl/opendora/api/sql_client"
"github.com/devoteamnl/opendora/api/sql_client/sql_queries"
)

type MetricMttrService struct {
Client sql_client.ClientInterface
}

func (service MetricMttrService) ServeRequest(params ServiceParameters) (models.MetricResponse, error) {
aggregationQueryMap := map[string]string{
"weekly": sql_queries.WeeklyMttrSql,
"monthly": sql_queries.MonthlyMttrSql,
"quarterly": sql_queries.QuarterlyMttrSql,
}

query := aggregationQueryMap[params.Aggregation]

dataPoints, err := service.Client.QueryDeployments(query, sql_client.QueryParams{To: params.To, From: params.From, Project: params.Project})

return models.MetricResponse{Aggregation: params.Aggregation, DataPoints: dataPoints}, err
}
96 changes: 96 additions & 0 deletions devlake-go/api/service/metric_mttr_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package service

import (
"fmt"
"reflect"
"testing"

"github.com/devoteamnl/opendora/api/models"
"github.com/devoteamnl/opendora/api/sql_client"
"github.com/devoteamnl/opendora/api/sql_client/sql_queries"
)

func TestMetricMttrService_ServeRequest(t *testing.T) {

exampleWeeklyDataPoints := []models.DataPoint{{Key: "202338", Value: 0}, {Key: "202337", Value: 1}, {Key: "202336", Value: 2}}
exampleMonthlyDataPoints := []models.DataPoint{{Key: "23/04", Value: 6}, {Key: "23/03", Value: 5}, {Key: "23/02", Value: 4}}
exampleQuarterlyDataPoints := []models.DataPoint{{Key: "2024-01-01", Value: 6}, {Key: "2023-10-01", Value: 5}, {Key: "2023-07-01", Value: 4}}

dataMockMap := map[string]sql_client.MockDeploymentsDataReturn{
sql_queries.WeeklyMttrSql: {Data: exampleWeeklyDataPoints},
sql_queries.MonthlyMttrSql: {Data: exampleMonthlyDataPoints},
sql_queries.QuarterlyMttrSql: {Data: exampleQuarterlyDataPoints}}

errorMockMap := map[string]sql_client.MockDeploymentsDataReturn{
sql_queries.WeeklyMttrSql: {Err: fmt.Errorf("error from weekly query")},
sql_queries.MonthlyMttrSql: {Err: fmt.Errorf("error from monthly query")},
sql_queries.QuarterlyMttrSql: {Err: fmt.Errorf("error from quarterly query")},
}

tests := []struct {
name string
params ServiceParameters
mockClient sql_client.MockClient
expectResponse models.MetricResponse
expectError string
}{
{
name: "should return an error with an unexpected error from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "weekly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: errorMockMap},
expectResponse: models.MetricResponse{Aggregation: "weekly", DataPoints: nil},
expectError: "error from weekly query",
},
{
name: "should return an error with an unexpected error from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "monthly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: errorMockMap},
expectResponse: models.MetricResponse{Aggregation: "monthly", DataPoints: nil},
expectError: "error from monthly query",
},
{
name: "should return an error with an unexpected error from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "quarterly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: errorMockMap},
expectResponse: models.MetricResponse{Aggregation: "quarterly", DataPoints: nil},
expectError: "error from quarterly query",
},
{
name: "should return weekly data points from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "weekly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: dataMockMap},
expectResponse: models.MetricResponse{Aggregation: "weekly", DataPoints: exampleWeeklyDataPoints},
expectError: "",
},
{
name: "should return monthly data points from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "monthly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: dataMockMap},
expectResponse: models.MetricResponse{Aggregation: "monthly", DataPoints: exampleMonthlyDataPoints},
expectError: "",
},
{
name: "should return quarterly data points from the database",
params: ServiceParameters{TypeQuery: "mttr", Aggregation: "quarterly", Project: "", To: 0, From: 0},
mockClient: sql_client.MockClient{MockDeploymentsDataMap: dataMockMap},
expectResponse: models.MetricResponse{Aggregation: "quarterly", DataPoints: exampleQuarterlyDataPoints},
expectError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mttrService := MetricMttrService{Client: tt.mockClient}
got, err := mttrService.ServeRequest(tt.params)

if err == nil && tt.expectError != "" {
t.Errorf("expected '%v' got no error", tt.expectError)
}
if err != nil && err.Error() != tt.expectError {
t.Errorf("expected '%v' got '%v'", tt.expectError, err)
}
if !reflect.DeepEqual(got, tt.expectResponse) {
t.Errorf("MttrService.ServeRequest() = %v, want %v", got, tt.expectResponse)
}
})
}
}
14 changes: 7 additions & 7 deletions devlake-go/api/sql_client/sql_queries/monthly_mltc.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ WITH _pr_stats AS (
JOIN repos ON cdc.repo_id = repos.id
WHERE
(
:project = ""
OR LOWER(repos.name) LIKE CONCAT('%/', LOWER(:project))
:project = ""
OR LOWER(repos.name) LIKE CONCAT('%/', LOWER(:project))
)
AND pr.merged_date IS NOT NULL
AND ppm.pr_cycle_time IS NOT NULL
Expand All @@ -33,11 +33,11 @@ _clt as(
)

SELECT
cm.month as data_key,
case
when _clt.median_change_lead_time is null then 0
else _clt.median_change_lead_time/60
end as data_value
cm.month AS data_key,
CASE
WHEN _clt.median_change_lead_time IS NULL THEN 0
ELSE _clt.median_change_lead_time/60
END AS data_value
FROM
calendar_months cm
LEFT JOIN _clt ON cm.month = _clt.month
Expand Down
42 changes: 42 additions & 0 deletions devlake-go/api/sql_client/sql_queries/monthly_mttr.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
WITH _incidents AS (
SELECT
DISTINCT i.id,
DATE_FORMAT(i.created_date,'%y/%m') AS month,
cast(lead_time_minutes AS signed) AS lead_time_minutes
FROM
issues i
JOIN board_issues bi ON i.id = bi.issue_id
JOIN boards b ON bi.board_id = b.id
JOIN project_mapping pm ON b.id = pm.row_id AND pm.`table` = 'boards'
WHERE
(
:project = ""
OR LOWER(repos.name) LIKE CONCAT('%/', LOWER(:project))
)
AND i.type = 'INCIDENT'
AND i.lead_time_minutes IS NOT NULL
),

_find_median_mttr_each_month_ranks as(
SELECT *, percent_rank() OVER(PARTITION BY month ORDER BY lead_time_minutes) AS ranks
FROM _incidents
),

_mttr as(
SELECT month, max(lead_time_minutes) AS median_time_to_resolve
FROM _find_median_mttr_each_month_ranks
WHERE ranks <= 0.5
GROUP BY month
)

SELECT
cm.month AS data_key,
CASE
WHEN m.median_time_to_resolve IS NULL THEN 0
ELSE m.median_time_to_resolve/60
END AS data_value
FROM
calendar_months cm
LEFT JOIN _mttr m ON cm.month = m.month
WHERE cm.month_timestamp BETWEEN FROM_UNIXTIME(:from)
AND FROM_UNIXTIME(:to)
57 changes: 57 additions & 0 deletions devlake-go/api/sql_client/sql_queries/quarterly_mttr.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

WITH RECURSIVE calendar_quarters AS (
SELECT
DATE_ADD(
MAKEDATE(YEAR(FROM_UNIXTIME(:from)), 1),
INTERVAL QUARTER(FROM_UNIXTIME(:from)) -1 QUARTER
) AS quarter_date
UNION
ALL
SELECT
DATE_ADD(quarter_date, INTERVAL 1 QUARTER)
FROM
calendar_quarters
WHERE
quarter_date < FROM_UNIXTIME(:to)
), _incidents AS (
SELECT
DISTINCT i.id,
i.created_date AS quarter,
cast(lead_time_minutes AS signed) AS lead_time_minutes
FROM
issues i
JOIN board_issues bi ON i.id = bi.issue_id
JOIN boards b ON bi.board_id = b.id
JOIN project_mapping pm ON b.id = pm.row_id AND pm.`table` = 'boards'
WHERE
(
:project = ""
OR LOWER(repos.name) LIKE CONCAT('%/', LOWER(:project))
)
AND i.type = 'INCIDENT'
AND i.lead_time_minutes IS NOT NULL
),

_find_median_mttr_each_quarter_ranks as(
SELECT *, percent_rank() OVER(PARTITION BY quarter ORDER BY lead_time_minutes) AS ranks
FROM _incidents
),

_mttr as(
SELECT quarter, max(lead_time_minutes) AS median_time_to_resolve
FROM _find_median_mttr_each_quarter_ranks
WHERE ranks <= 0.5
GROUP BY quarter
)

SELECT
cq.quarter_date AS data_key,
CASE
WHEN m.median_time_to_resolve IS NULL THEN 0
ELSE m.median_time_to_resolve/60
END AS data_value
FROM
calendar_quarters cq
LEFT JOIN _mttr m ON cq.quarter_date = m.quarter
ORDER BY
cq.quarter_date DESC
9 changes: 9 additions & 0 deletions devlake-go/api/sql_client/sql_queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ var MonthlyCfrSql string

//go:embed quarterly_cfr.sql
var QuarterlyCfrSql string

//go:embed weekly_mttr.sql
var WeeklyMttrSql string

//go:embed monthly_mttr.sql
var MonthlyMttrSql string

//go:embed quarterly_mttr.sql
var QuarterlyMttrSql string
55 changes: 55 additions & 0 deletions devlake-go/api/sql_client/sql_queries/weekly_mttr.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
WITH RECURSIVE calendar_weeks AS (
SELECT
STR_TO_DATE(
CONCAT(YEARWEEK(FROM_UNIXTIME(:from)), ' Sunday'),
'%X%V %W'
) AS week_date
UNION
ALL
SELECT
DATE_ADD(week_date, INTERVAL 1 WEEK)
FROM
calendar_weeks
WHERE
week_date < FROM_UNIXTIME(:to)
),_incidents AS (
SELECT
DISTINCT i.id,
YEARWEEK(i.created_date) AS week,
cast(lead_time_minutes AS signed) AS lead_time_minutes
FROM
issues i
JOIN board_issues bi ON i.id = bi.issue_id
JOIN boards b ON bi.board_id = b.id
JOIN project_mapping pm ON b.id = pm.row_id AND pm.`table` = 'boards'
WHERE
(
:project = ""
OR LOWER(repos.name) LIKE CONCAT('%/', LOWER(:project))
)
AND i.type = 'INCIDENT'
AND i.lead_time_minutes IS NOT NULL
),

_find_median_mttr_each_week_ranks as(
SELECT *, percent_rank() OVER(PARTITION BY week ORDER BY lead_time_minutes) AS ranks
FROM _incidents
),

_mttr as(
SELECT week, max(lead_time_minutes) AS median_time_to_resolve
FROM _find_median_mttr_each_week_ranks
WHERE ranks <= 0.5
GROUP BY week
)

SELECT
YEARWEEK(cw.week_date) AS data_key,
CASE
WHEN m.median_time_to_resolve IS NULL THEN 0
ELSE m.median_time_to_resolve/60 END AS data_value
FROM
calendar_weeks cw
LEFT JOIN _mttr m ON YEARWEEK(cw.week_date) = m.week
ORDER BY
cw.week_date DESC
2 changes: 1 addition & 1 deletion devlake-go/api/validation/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func ValidMetricServiceParameters(queries url.Values) (service.ServiceParameters
return service.ServiceParameters{}, fmt.Errorf("aggregation should be provided as either weekly, monthly or quarterly")
}

serviceParameters, err := validServiceParameters(queries, []string{"df_count", "df_average", "mltc", "cfr"})
serviceParameters, err := validServiceParameters(queries, []string{"df_count", "df_average", "mltc", "cfr", "mttr"})
if err != nil {
return serviceParameters, err
}
Expand Down
2 changes: 1 addition & 1 deletion devlake-go/api/validation/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func Test_ValidMetricServiceParameters(t *testing.T) {
name: "should return an error for an invalid type parameter",
values: url.Values{"type": {"df"}},
expectServiceParameters: service.ServiceParameters{},
expectError: "type should be provided as one of the following: df_count, df_average, mltc, cfr",
expectError: "type should be provided as one of the following: df_count, df_average, mltc, cfr, mttr",
},
{
name: "should return an error for an invalid aggregation parameter",
Expand Down

0 comments on commit 256acc6

Please sign in to comment.