Commit ac40d6d7 authored by Adam Hegyi's avatar Adam Hegyi

Merge branch '340150-expose-lead-time-for-changes-in-vsa' into 'master'

Add median lead time for changes to VSA

See merge request gitlab-org/gitlab!72283
parents 8e1fbdf4 b26ee08d
...@@ -47,6 +47,11 @@ export const METRICS_POPOVER_CONTENT = { ...@@ -47,6 +47,11 @@ export const METRICS_POPOVER_CONTENT = {
"ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
), ),
}, },
'lead-time-for-changes': {
description: s__(
'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.',
),
},
'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') }, 'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') }, 'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') }, deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') },
......
...@@ -87,6 +87,12 @@ The "Time" metrics near the top of the page are measured as follows: ...@@ -87,6 +87,12 @@ The "Time" metrics near the top of the page are measured as follows:
- **Lead time**: median time from issue created to issue closed. - **Lead time**: median time from issue created to issue closed.
- **Cycle time**: median time from first commit to issue closed. (You can associate a commit with an - **Cycle time**: median time from first commit to issue closed. (You can associate a commit with an
issue by [crosslinking in the commit message](../../project/issues/crosslinking_issues.md#from-commit-messages).) issue by [crosslinking in the commit message](../../project/issues/crosslinking_issues.md#from-commit-messages).)
- **Lead Time for Changes**: median time between when a merge request is merged and deployed to a
production environment for all merge requests deployed in the given time period.
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340150) in GitLab 14.5 (**Ultimate**
tier only).
- **Lead Time for Changes**: median duration between merge request merge and deployment to a production environment for all MRs deployed in the given time period. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340150) in GitLab 14.5 (**Ultimate** tier only).
The "Recent Activity" metrics near the top of the page are measured as follows: The "Recent Activity" metrics near the top of the page are measured as follows:
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
module Dora module Dora
class AggregateMetricsService < ::BaseContainerService class AggregateMetricsService < ::BaseContainerService
MAX_RANGE = 92 # the maximum number of days in 3 months MAX_RANGE = Gitlab::Analytics::CycleAnalytics::RequestParams::MAX_RANGE_DAYS # same range as Value Stream Analytics
DEFAULT_ENVIRONMENT_TIER = 'production' DEFAULT_ENVIRONMENT_TIER = 'production'
DEFAULT_INTERVAL = Dora::DailyMetrics::INTERVAL_DAILY DEFAULT_INTERVAL = Dora::DailyMetrics::INTERVAL_DAILY
...@@ -52,8 +53,8 @@ module Dora ...@@ -52,8 +53,8 @@ module Dora
end end
def validate def validate
unless (end_date - start_date) <= MAX_RANGE unless (end_date - start_date).days <= MAX_RANGE
return error(_("Date range must be shorter than %{max_range} days.") % { max_range: MAX_RANGE }, return error(_("Date range must be shorter than %{max_range} days.") % { max_range: MAX_RANGE.in_days.to_i },
:bad_request) :bad_request)
end end
......
# frozen_string_literal: true
module Gitlab
module Analytics
module CycleAnalytics
module Summary
class LeadTimeForChanges
def initialize(stage:, current_user:, options:)
@stage = stage
@current_user = current_user
@options = options
@from = options[:from].to_date
@to = (options[:to] || Date.today).to_date
end
def title
s_('CycleAnalytics|Lead Time for Changes')
end
def value
@value ||= dora_lead_time_for_changes
end
def unit
n_('day', 'days', value)
end
private
attr_reader :stage, :current_user, :options, :from, :to
def dora_lead_time_for_changes
params = {
start_date: from,
end_date: to,
interval: 'all',
environment_tier: 'production',
metric: 'lead_time_for_changes'
}
params[:group_project_ids] = options[:projects] if options[:projects].present?
result = Dora::AggregateMetricsService.new(
container: stage.parent,
current_user: current_user,
params: params
).execute
return convert_to_days(result[:data]) if result[:status] == :success
# this signals the summary class to not even try to serialize the result
nil
end
def convert_to_days(median_seconds)
return Gitlab::CycleAnalytics::Summary::Value::None.new if median_seconds.to_i == 0
median_days = median_seconds.fdiv(1.day).round(1)
Gitlab::CycleAnalytics::Summary::Value::Numeric.new(median_days)
end
end
end
end
end
end
...@@ -14,7 +14,9 @@ module Gitlab ...@@ -14,7 +14,9 @@ module Gitlab
end end
def data def data
[lead_time, cycle_time] [lead_time, cycle_time].tap do |array|
array << serialize(lead_time_for_changes, with_unit: true) if lead_time_for_changes.value.present?
end
end end
private private
...@@ -37,6 +39,14 @@ module Gitlab ...@@ -37,6 +39,14 @@ module Gitlab
) )
end end
def lead_time_for_changes
@lead_time_for_changes ||= Summary::LeadTimeForChanges.new(
stage: stage,
current_user: current_user,
options: options
)
end
def serialize(summary_object, with_unit: false) def serialize(summary_object, with_unit: false)
AnalyticsSummarySerializer.new.represent( AnalyticsSummarySerializer.new.represent(
summary_object, with_unit: with_unit) summary_object, with_unit: with_unit)
......
...@@ -49,7 +49,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -49,7 +49,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end end
before do before do
stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true) stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true, dora4_analytics: true)
group.add_owner(user) group.add_owner(user)
project.add_maintainer(user) project.add_maintainer(user)
...@@ -83,7 +83,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -83,7 +83,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end end
it 'displays the recent activity' do it 'displays the recent activity' do
deploys_count = page.all(card_metric_selector)[3] deploys_count = page.all(card_metric_selector)[4]
expect(deploys_count).to have_content(n_('Deploy', 'Deploys', 0)) expect(deploys_count).to have_content(n_('Deploy', 'Deploys', 0))
expect(deploys_count).to have_content('-') expect(deploys_count).to have_content('-')
...@@ -93,7 +93,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -93,7 +93,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
expect(deployment_frequency).to have_content(_('Deployment Frequency')) expect(deployment_frequency).to have_content(_('Deployment Frequency'))
expect(deployment_frequency).to have_content('-') expect(deployment_frequency).to have_content('-')
issue_count = page.all(card_metric_selector)[2] issue_count = page.all(card_metric_selector)[3]
expect(issue_count).to have_content(n_('New Issue', 'New Issues', 3)) expect(issue_count).to have_content(n_('New Issue', 'New Issues', 3))
expect(issue_count).to have_content(new_issues_count) expect(issue_count).to have_content(new_issues_count)
...@@ -109,6 +109,11 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -109,6 +109,11 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
expect(cycle_time).to have_content(_('Cycle Time')) expect(cycle_time).to have_content(_('Cycle Time'))
expect(cycle_time).to have_content('-') expect(cycle_time).to have_content('-')
median_lead_time_for_changes = page.all(card_metric_selector)[2]
expect(median_lead_time_for_changes).to have_content(s_('CycleAnalytics|Lead Time for Changes'))
expect(median_lead_time_for_changes).to have_content('-')
end end
end end
......
...@@ -163,7 +163,7 @@ RSpec.describe Resolvers::DoraMetricsResolver do ...@@ -163,7 +163,7 @@ RSpec.describe Resolvers::DoraMetricsResolver do
let(:args) { { metric: 'deployment_frequency', start_date: '2020-01-01'.to_datetime, end_date: '2021-05-01'.to_datetime } } let(:args) { { metric: 'deployment_frequency', start_date: '2020-01-01'.to_datetime, end_date: '2021-05-01'.to_datetime } }
it 'raises an error' do it 'raises an error' do
expect { resolve_metrics }.to raise_error('Date range must be shorter than 92 days.') expect { resolve_metrics }.to raise_error('Date range must be shorter than 180 days.')
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::LeadTimeForChanges do
let(:stage) { build(:cycle_analytics_group_stage) }
let(:user) { build(:user) }
let(:options) do
{
from: 5.days.ago,
to: 2.days.ago
}
end
subject(:result) { described_class.new(stage: stage, current_user: user, options: options).value }
context 'when the DORA service returns non-successful status' do
it 'returns nil' do
expect_next_instance_of(Dora::AggregateMetricsService) do |service|
expect(service).to receive(:execute).and_return({ status: :error })
end
expect(result).to eq(nil)
end
end
context 'when the DORA service returns 0 as the lead time for changes' do
it 'returns "none" value' do
expect_next_instance_of(Dora::AggregateMetricsService) do |service|
expect(service).to receive(:execute).and_return({ status: :success, data: 0 })
end
expect(result.to_s).to eq('-')
end
end
context 'when the DORA service returns the lead time for changes as seconds' do
it 'returns the value in days' do
expect_next_instance_of(Dora::AggregateMetricsService) do |service|
expect(service).to receive(:execute).and_return({ status: :success, data: 5.days.to_i })
end
expect(result.to_s).to eq('5.0')
end
end
end
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::StageTimeSummary do RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::StageTimeSummary do
let_it_be(:group) { create(:group) } let_it_be_with_refind(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, namespace: group) } let_it_be(:project) { create(:project, :repository, namespace: group) }
let_it_be(:project_2) { create(:project, :repository, namespace: group) } let_it_be(:project_2) { create(:project, :repository, namespace: group) }
let_it_be(:project_3) { create(:project, :repository, namespace: group) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:from) { 1.day.ago } let(:from) { 1.day.ago }
...@@ -220,4 +221,78 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::StageTimeSummary do ...@@ -220,4 +221,78 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::StageTimeSummary do
end end
end end
end end
describe '#lead_time_for_changes' do
let(:lead_time_for_changes_title) { s_('CycleAnalytics|Lead Time for Changes') }
context 'when dora4_analytics feature is not available' do
before do
stub_licensed_features(dora4_analytics: false)
end
it 'does not include lead_time_for_changes in the result array' do
expect(subject.size).to eq(2)
titles = subject.pluck(:title)
expect(titles).not_to include(lead_time_for_changes_title)
end
end
context 'when dora4_analytics feature is available' do
let(:lead_time_for_changes) { subject.third }
before do
stub_licensed_features(dora4_analytics: true)
end
context 'when no aggregated data available' do
it 'returns no data' do
expect(lead_time_for_changes[:title]).to eq(lead_time_for_changes_title)
expect(lead_time_for_changes[:value]).to eq('-')
end
end
context 'when data is available' do
let(:environment_1) { create(:environment, :production, project: project) }
let(:environment_2) { create(:environment, :production, project: project_2) }
let(:environment_3) { create(:environment, :production, project: project_3) }
before do
create(:dora_daily_metrics,
environment: environment_1,
date: from,
lead_time_for_changes_in_seconds: 2.hours.seconds.to_i)
create(:dora_daily_metrics,
environment: environment_2,
date: from,
lead_time_for_changes_in_seconds: 5.hours.seconds.to_i) # median
create(:dora_daily_metrics,
environment: environment_3,
date: from,
lead_time_for_changes_in_seconds: 7.hours.seconds.to_i)
end
it 'returns the median lead time for changes in days' do
expected_value = 5.hours.fdiv(1.day).round(1) # 0.2
expect(lead_time_for_changes[:value]).to eq(expected_value.to_s)
end
context 'when project ids filter is given' do
before do
options[:projects] = [project]
end
it 'returns the median lead time for changes in days for the selected project' do
expected_value = 2.hours.fdiv(1.day).round(1) # 0.1
expect(lead_time_for_changes[:value]).to eq(expected_value.to_s)
end
end
end
end
end
end end
...@@ -27,7 +27,7 @@ RSpec.describe Dora::AggregateMetricsService do ...@@ -27,7 +27,7 @@ RSpec.describe Dora::AggregateMetricsService do
let(:extra_params) { { start_date: 1.year.ago.to_date } } let(:extra_params) { { start_date: 1.year.ago.to_date } }
it_behaves_like 'request failure' do it_behaves_like 'request failure' do
let(:message) { "Date range must be shorter than #{described_class::MAX_RANGE} days." } let(:message) { "Date range must be shorter than #{described_class::MAX_RANGE.in_days.to_i} days." }
let(:http_status) { :bad_request } let(:http_status) { :bad_request }
end end
end end
......
...@@ -10225,6 +10225,9 @@ msgstr "" ...@@ -10225,6 +10225,9 @@ msgstr ""
msgid "CycleAnalytics|Display chart filters" msgid "CycleAnalytics|Display chart filters"
msgstr "" msgstr ""
msgid "CycleAnalytics|Lead Time for Changes"
msgstr ""
msgid "CycleAnalytics|No stages selected" msgid "CycleAnalytics|No stages selected"
msgstr "" msgstr ""
...@@ -37550,6 +37553,9 @@ msgstr "" ...@@ -37550,6 +37553,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day." msgid "ValueStreamAnalytics|Average number of deployments to production per day."
msgstr "" msgstr ""
msgid "ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period."
msgstr ""
msgid "ValueStreamAnalytics|Median time from issue created to issue closed." msgid "ValueStreamAnalytics|Median time from issue created to issue closed."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment