Commit b26ee08d authored by Adam Hegyi's avatar Adam Hegyi

Add median lead time for changes to VSA

Add median lead time for changes to value stream analytics when it's
available for the current group or project.

Changelog: added
EE: true
parent ccf9dd4a
......@@ -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.",
),
},
'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-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
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:
- **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
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:
......
......@@ -2,7 +2,8 @@
module Dora
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_INTERVAL = Dora::DailyMetrics::INTERVAL_DAILY
......@@ -52,8 +53,8 @@ module Dora
end
def validate
unless (end_date - start_date) <= MAX_RANGE
return error(_("Date range must be shorter than %{max_range} days.") % { max_range: 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.in_days.to_i },
:bad_request)
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
end
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
private
......@@ -37,6 +39,14 @@ module Gitlab
)
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)
AnalyticsSummarySerializer.new.represent(
summary_object, with_unit: with_unit)
......
......@@ -49,7 +49,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end
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)
project.add_maintainer(user)
......@@ -83,7 +83,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end
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('-')
......@@ -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('-')
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(new_issues_count)
......@@ -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('-')
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
......
......@@ -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 } }
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
......
# 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 @@
require 'spec_helper'
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_2) { create(:project, :repository, namespace: group) }
let_it_be(:project_3) { create(:project, :repository, namespace: group) }
let_it_be(:user) { create(:user) }
let(:from) { 1.day.ago }
......@@ -220,4 +221,78 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Summary::StageTimeSummary do
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
......@@ -27,7 +27,7 @@ RSpec.describe Dora::AggregateMetricsService do
let(:extra_params) { { start_date: 1.year.ago.to_date } }
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 }
end
end
......
......@@ -10225,6 +10225,9 @@ msgstr ""
msgid "CycleAnalytics|Display chart filters"
msgstr ""
msgid "CycleAnalytics|Lead Time for Changes"
msgstr ""
msgid "CycleAnalytics|No stages selected"
msgstr ""
......@@ -37550,6 +37553,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day."
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."
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