diff --git a/ee/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader.rb b/ee/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader.rb index c48d485c8dffb7c9f8dd422400b77b7ec5b77ccb..155ca0c1bcd009c92a3955e5afce5ea33a8f68ed 100644 --- a/ee/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader.rb +++ b/ee/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader.rb @@ -19,8 +19,8 @@ module CycleAnalytics # Stage 1, Stage 2 # # The class also adds two "in-memory" stages into the distinct calculation which - # represent the CycleTime and LeadTime metrics. These metrics are calculated as - # pre-defined VSA stages. + # represent the CycleTime and LeadTime metric for issues and TimeToMerge metric for MRs. + # These metrics are calculated as pre-defined VSA stages. class DistinctStageLoader def initialize(group:) @group = group @@ -29,8 +29,7 @@ def initialize(group:) def stages [ *persisted_stages, - add_stage_event_hash_id(in_memory_lead_time_stage), - add_stage_event_hash_id(in_memory_cycle_time_stage) + *in_memory_stages.map { |stage| add_stage_event_hash_id(stage) } ].uniq(&:stage_event_hash_id) end @@ -42,6 +41,14 @@ def persisted_stages @persisted_stages ||= ::Analytics::CycleAnalytics::Stage.distinct_stages_within_hierarchy(group) end + def in_memory_stages + [ + in_memory_lead_time_stage, + in_memory_cycle_time_stage, + in_memory_time_to_merge_stage + ] + end + def in_memory_lead_time_stage ::Analytics::CycleAnalytics::Stage.new( name: 'lead time', # not visible to the user @@ -60,6 +67,15 @@ def in_memory_cycle_time_stage ) end + def in_memory_time_to_merge_stage + ::Analytics::CycleAnalytics::Stage.new( + name: 'time to merge', + start_event_identifier: Summary::TimeToMerge.start_event_identifier, + end_event_identifier: Summary::TimeToMerge.end_event_identifier, + namespace: group + ) + end + # rubocop: disable CodeReuse/ActiveRecord def add_stage_event_hash_id(stage) # find or create the stage event hash diff --git a/ee/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary.rb b/ee/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary.rb index b5fdba4e7cdd19ad69c01bd46b3ac4b21660ceb9..ac78a239af629f4783197b6d87ad1d5cb3c6b592 100644 --- a/ee/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary.rb +++ b/ee/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary.rb @@ -14,7 +14,7 @@ def initialize(stage, options:) end def data - [lead_time, cycle_time].tap do |array| + [lead_time, cycle_time, time_to_merge].tap do |array| array << serialize(lead_time_for_changes, with_unit: true) if lead_time_for_changes.value.present? array << serialize(time_to_restore_service, with_unit: true) if time_to_restore_service.value.present? array << serialize(change_failure_rate, with_unit: true) if change_failure_rate.value.present? @@ -41,6 +41,15 @@ def cycle_time ) end + def time_to_merge + serialize( + Summary::TimeToMerge.new( + stage: stage, current_user: current_user, options: options + ), + with_unit: true + ) + end + def lead_time_for_changes @lead_time_for_changes ||= Summary::LeadTimeForChanges.new( stage: stage, diff --git a/ee/lib/gitlab/analytics/cycle_analytics/summary/time_to_merge.rb b/ee/lib/gitlab/analytics/cycle_analytics/summary/time_to_merge.rb new file mode 100644 index 0000000000000000000000000000000000000000..e14603b033b19e5ed813ef763744ce566c95ad5e --- /dev/null +++ b/ee/lib/gitlab/analytics/cycle_analytics/summary/time_to_merge.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module Summary + class TimeToMerge < BaseTime + def title + _('Time to Merge') + end + + def self.start_event_identifier + :merge_request_created + end + + def self.end_event_identifier + :merge_request_merged + end + + def links + namespace = @stage.namespace + return [] if namespace.is_a?(::Group) + + helpers = Gitlab::Routing.url_helpers + dashboard_link = helpers.project_analytics_merge_request_analytics_path(namespace.project) + + [ + { "name" => title, "url" => dashboard_link, + "label" => s_('ValueStreamAnalytics|Merge request analytics') }, + { "name" => title, + "url" => helpers.help_page_path('user/analytics/index', anchor: 'definitions'), + "docs_link" => true, + "label" => s_('ValueStreamAnalytics|Go to docs') } + ] + end + end + end + end + end +end diff --git a/ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb b/ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb index c518c3130a6ecd396948980c20f24f95718efed0..51cc848754b4313c7f776f642ee9eec608edd428 100644 --- a/ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb +++ b/ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb @@ -62,7 +62,7 @@ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute # Calculating Cycle Time and Lead Time - expect(Gitlab::Analytics::CycleAnalytics::Aggregated::DataCollector).to receive(:new).twice.and_call_original + expect(Gitlab::Analytics::CycleAnalytics::Aggregated::DataCollector).to receive(:new).thrice.and_call_original subject diff --git a/ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb b/ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb index 481de04d3839c442a90a39ef5a6efcf04d81df52..8a2045872ad6811be50b99ed6e5bc1b69e1917b0 100644 --- a/ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb +++ b/ee/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb @@ -41,11 +41,12 @@ end it 'returns correct value' do - expected_cycle_time = (closed_at - first_mentioned_in_commit_at).to_i + expected_cycle_time = (closed_at - first_mentioned_in_commit_at).to_f subject - expect(json_response.last["value"].to_i).to eq(expected_cycle_time) + lead_time = json_response.find { |result| result['identifier'] == 'cycle_time' }.symbolize_keys + expect(lead_time).to include({ value: expected_cycle_time.to_s, title: 'Cycle Time', unit: 'days' }) end context 'when analytics_disabled features are disabled' do diff --git a/ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb b/ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb index 494a141615a24f6f12db28b86a271d2457db465d..8ded2da516e357aadda8aa9c8adacbe409e5e0d4 100644 --- a/ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb +++ b/ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb @@ -2,15 +2,20 @@ require 'spec_helper' -RSpec.describe Gitlab::Analytics::CycleAnalytics::DistinctStageLoader do +RSpec.describe Gitlab::Analytics::CycleAnalytics::DistinctStageLoader, feature_category: :value_stream_management do let_it_be(:group) { create(:group) } - let_it_be(:stage_1) { create(:cycle_analytics_stage, namespace: group, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) } + let_it_be(:stage_1) { create(:cycle_analytics_stage, namespace: group, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_closed) } let_it_be(:common_stage_params) { { start_event_identifier: :issue_created, end_event_identifier: :issue_first_associated_with_milestone } } let_it_be(:stage_2) { create(:cycle_analytics_stage, namespace: group, **common_stage_params) } let_it_be(:stage_duplicate) { create(:cycle_analytics_stage, namespace: group, **common_stage_params) } let_it_be(:stage_triplicate) { create(:cycle_analytics_stage, namespace: group, **common_stage_params) } - let_it_be(:project) { create(:project, group: group) } + let(:lead_time_name) { 'lead time' } + let(:cycle_time_name) { 'cycle time' } + let(:time_to_merge_name) { 'time to merge' } + + let(:in_memory_stages_names) { [lead_time_name, cycle_time_name, time_to_merge_name] } + let(:in_memory_stages_count) { in_memory_stages_names.count } subject(:distinct_stages) { described_class.new(group: group).stages } @@ -20,45 +25,53 @@ expect(distinct_stage_hash_ids).to eq(distinct_stage_hash_ids.uniq) end - context 'when lead time and cycle time are not defined as stages' do - it 'returns in-memory stages' do - lead_time = distinct_stages.find { |stage| stage.name == 'lead time' } - cycle_time = distinct_stages.find { |stage| stage.name == 'cycle time' } + context 'when in-memory stages are not defined as stages', :aggregate_failures do + it 'creates three stage event hash records' do + expect { distinct_stages }.to change { Analytics::CycleAnalytics::StageEventHash.count }.by(3) + end + end + + context 'when all in-memory stages have been defined' do + let(:lead_time) { distinct_stages.find { |stage| stage.name == lead_time_name } } + let(:cycle_time) { distinct_stages.find { |stage| stage.name == cycle_time_name } } + let(:time_to_merge) { distinct_stages.find { |stage| stage.name == time_to_merge_name } } + let(:in_memory_stages) { [lead_time, cycle_time, time_to_merge] } - expect(lead_time).to be_present - expect(cycle_time).to be_present - expect(lead_time.stage_event_hash_id).not_to be_nil - expect(cycle_time.stage_event_hash_id).not_to be_nil + it 'returns in-memory stages' do + # all should be present + expect(in_memory_stages.compact.count).to eq in_memory_stages_count - expect(lead_time.stage_event_hash_id).not_to eq(cycle_time.stage_event_hash_id) + # all should have unique stage event hash IDs + expect(in_memory_stages.map(&:stage_event_hash_id).count).to eq in_memory_stages_count end - it 'creates two stage event hash records' do - expect { distinct_stages }.to change { Analytics::CycleAnalytics::StageEventHash.count }.by(2) + it 'has distinct values for all in-memory stages' do + expect(in_memory_stages.map(&:stage_event_hash_id).uniq.count).to eq in_memory_stages_count end - it 'returns 4 stages' do - expect(distinct_stages.size).to eq(4) + it 'returns total number of stages - in-memory + persisted' do + expect(distinct_stages.size).to eq(in_memory_stages_count + 2) end end - context 'when lead time and cycle time are persisted stages' do + context 'when a subset of in-memory stages are already defined' do let_it_be(:cycle_time) do create(:cycle_analytics_stage, namespace: group, - start_event_identifier: :issue_created, - end_event_identifier: :issue_first_associated_with_milestone) + start_event_identifier: Gitlab::Analytics::CycleAnalytics::Summary::LeadTime.start_event_identifier, + end_event_identifier: Gitlab::Analytics::CycleAnalytics::Summary::LeadTime.end_event_identifier) end - let_it_be(:lead_tiem) do + let_it_be(:lead_time) do create(:cycle_analytics_stage, namespace: group, - start_event_identifier: :issue_created, - end_event_identifier: :issue_first_associated_with_milestone) + start_event_identifier: Gitlab::Analytics::CycleAnalytics::Summary::CycleTime.start_event_identifier, + end_event_identifier: Gitlab::Analytics::CycleAnalytics::Summary::CycleTime.end_event_identifier) end it 'does not create extra stage event hash records' do - expect { distinct_stages }.to change { Analytics::CycleAnalytics::StageEventHash.count } + # only creates time_to_merge because that hasn't been defined yet + expect { distinct_stages }.to change { Analytics::CycleAnalytics::StageEventHash.count }.by(1) end end end diff --git a/ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb b/ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb index a56995b1b11e203e17bdab4e746e88ca845d1f6e..aba0edf1f9418bbd3b94d1007b5778711ca67f3b 100644 --- a/ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb +++ b/ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb @@ -13,7 +13,9 @@ let(:options) { { from: from, to: to, current_user: user } } let(:stage) { Analytics::CycleAnalytics::Stage.new(namespace: group) } - subject { described_class.new(stage, options: options).data } + subject do + described_class.new(stage, options: options).data + end around do |example| freeze_time { example.run } @@ -23,20 +25,10 @@ group.add_owner(user) end - describe '#identifier' do - it 'returns identifiers for each metric' do - identifiers = subject.pluck(:identifier) - expect(identifiers).to eq(%i[lead_time cycle_time]) - end - end - context 'when the use_aggregated_data_collector option is given' do - context 'when aggregated data is available yet' do + context 'when aggregated data is not available yet' do it 'shows no value' do - lead_time, cycle_time, * = subject - - expect(lead_time[:value]).to eq('-') - expect(cycle_time[:value]).to eq('-') + expect_values(lead_time: '-', cycle_time: '-', time_to_merge: '-') end end @@ -45,21 +37,64 @@ issue = create(:closed_issue, project: project, created_at: 1.day.ago, closed_at: Time.current) issue.metrics.update!(first_mentioned_in_commit_at: 2.days.ago) + merge_request = create(:merge_request, :merged, created_at: 5.days.ago, project: project) + merge_request.metrics.update!(merged_at: 1.day.ago) + options[:use_aggregated_data_collector] = true stub_licensed_features(cycle_analytics_for_groups: true) - Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: model).execute end - it 'loads the lead and cycle time' do - lead_time, cycle_time, * = subject + context 'when only Issue model is specified' do + let(:model) { Issue } - expect(lead_time[:value]).to eq('1.0') - expect(cycle_time[:value]).to eq('2.0') + it 'loads the lead time, cycle time and time to merge' do + # this only loads Issue models, so Time to Merge is not filled in. + expect_values(lead_time: '1.0', cycle_time: '2.0', time_to_merge: '-') + end end + + context 'when only MR model is specified' do + let(:model) { MergeRequest } + + it 'shows time to merge' do + # this only shows MergeRequest models, so the first two are not filled in. + expect_values(lead_time: '-', cycle_time: '-', time_to_merge: '4.0') + end + end + + context 'when some other model is specified' do + let(:model) { Epic } + + it 'shows none of the values' do + expect_values(lead_time: '-', cycle_time: '-', time_to_merge: '-') + end + end + end + + def expect_values(lead_time:, cycle_time:, time_to_merge:) + expect(subject.as_json).to contain_exactly( + a_hash_including({ + "identifier" => 'lead_time', + "value" => lead_time + }), + a_hash_including({ + "identifier" => 'cycle_time', + "value" => cycle_time + }), + a_hash_including({ + "identifier" => 'time_to_merge', + "value" => time_to_merge + }) + ) end end describe '#lead_time' do + let(:lead_time) do + subject.find { |result| result[:identifier] == :lead_time } + end + describe 'issuable filter parameters' do let_it_be(:label) { create(:group_label, group: group) } @@ -73,7 +108,7 @@ end it 'returns the correct lead time' do - expect(subject.first[:value]).to eq('1.0') + expect(lead_time[:value]).to eq('1.0') end end @@ -83,7 +118,7 @@ end it 'returns `-`' do - expect(subject.first[:value]).to eq('-') + expect(lead_time[:value]).to eq('-') end end @@ -93,7 +128,7 @@ end it 'returns the correct lead time' do - expect(subject.first[:value]).to eq('1.0') + expect(lead_time[:value]).to eq('1.0') end end @@ -103,7 +138,7 @@ end it 'returns `-`' do - expect(subject.first[:value]).to eq('-') + expect(lead_time[:value]).to eq('-') end end end @@ -118,7 +153,7 @@ end it 'finds the lead time of issues created after it' do - expect(subject.first[:value]).to eq('2.0') + expect(lead_time[:value]).to eq('2.0') end context 'with subgroups' do @@ -131,7 +166,7 @@ end it 'finds the lead time of issues from them' do - expect(subject.first[:value]).to eq('3.0') + expect(lead_time[:value]).to eq('3.0') end end @@ -144,7 +179,7 @@ it 'finds the lead time of issues from those projects' do # Median of 1, 2, 4, not including new issue - expect(subject.first[:value]).to eq('2.0') + expect(lead_time[:value]).to eq('2.0') end end @@ -153,7 +188,7 @@ let(:to) { Time.zone.now } it 'finds the lead time of issues from 3 days ago' do - expect(subject.first[:value]).to eq('1.5') + expect(lead_time[:value]).to eq('1.5') end end end @@ -169,13 +204,18 @@ it 'does not find the lead time of issues from them' do # Median of 2, 3, not including first issue - expect(subject.first[:value]).to eq('2.5') + expect(lead_time[:value]).to eq('2.5') end end end describe '#cycle_time' do let(:created_at) { 6.days.ago } + let(:cycle_time) do + subject.find do |result| + result[:identifier] == :cycle_time + end + end context 'with `from` date' do let(:from) { 7.days.ago } @@ -191,7 +231,7 @@ end it 'finds the cycle time of issues created after it' do - expect(subject.second[:value]).to eq('2.0') + expect(cycle_time[:value]).to eq('2.0') end context 'with subgroups' do @@ -207,7 +247,7 @@ end it 'finds the cycle time of issues from them' do - expect(subject.second[:value]).to eq('3.0') + expect(cycle_time[:value]).to eq('3.0') end end @@ -221,7 +261,7 @@ it 'finds the cycle time of issues from those projects' do # Median of 1, 2, 4, not including new issue - expect(subject.second[:value]).to eq('2.0') + expect(cycle_time[:value]).to eq('2.0') end end @@ -232,7 +272,7 @@ it 'finds the cycle time of issues created between `from` and `to`' do # Median of 1, 2, 4 - expect(subject.second[:value]).to eq('2.0') + expect(cycle_time[:value]).to eq('2.0') end end end @@ -253,15 +293,101 @@ it 'does not find the cycle time of issues from them' do # Median of 2, 3, not including first issue - expect(subject.second[:value]).to eq('2.5') + expect(cycle_time[:value]).to eq('2.5') + end + end + end + + describe '#time_to_merge' do + let(:created_at) { 6.days.ago } + let(:time_to_merge) do + subject.find { |result| result[:identifier] == :time_to_merge } + end + + context 'with `from` date' do + let(:from) { 7.days.ago } + + before do + mr1 = create(:merge_request, :merged, project: project, created_at: created_at) + mr2 = create(:merge_request, :merged, project: project, created_at: created_at) + mr3 = create(:merge_request, :merged, project: project_2, created_at: created_at) + + mr1.metrics.update!(merged_at: 1.day.ago) + mr2.metrics.update!(merged_at: 2.days.ago) + mr3.metrics.update!(merged_at: 4.days.ago) + end + + it 'finds the time to merge of MRs created after it' do + expect(time_to_merge).to include({ value: '4.0', title: "Time to Merge", unit: "days" }) + end + + context 'with subgroups' do + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project_3) { create(:project, namespace: subgroup) } + + before do + mr4 = create(:merge_request, :merged, created_at: created_at, project: project_3) + mr5 = create(:merge_request, :merged, created_at: created_at, project: project_3) + + mr4.metrics.update!(merged_at: 3.days.ago) + mr5.metrics.update!(merged_at: 5.days.ago) + end + + it 'finds the time to merge of MRs from them' do + expect(time_to_merge).to include({ value: '3.0', title: "Time to Merge", unit: "days" }) + end + end + + context 'with projects specified in options' do + before do + mr4 = create(:merge_request, :merged, created_at: created_at, project: create(:project, namespace: group)) + mr4.metrics.update!(merged_at: 3.days.ago) + end + + subject { described_class.new(stage, options: { from: from, current_user: user, projects: [project.id, project_2.id] }).data } + + it 'finds the time to merge of MRs from those projects' do + # Median of 1, 2, 4, not including new issue + expect(time_to_merge).to include({ value: '4.0', title: "Time to Merge", unit: "days" }) + end + end + + context 'when `from` and `to` parameters are provided' do + let(:from) { 5.days.ago } + let(:to) { 2.days.ago } + let(:created_at) { from } + + it 'finds the time to merge of MRs created between `from` and `to`' do + expect(time_to_merge).to include({ value: '3.0', title: "Time to Merge", unit: "days" }) + end + end + end + + context 'with other projects' do + let(:from) { 4.days.ago } + let(:created_at) { from } + + before do + mr1 = create(:merge_request, :merged, created_at: created_at, project: create(:project, namespace: create(:group))) + mr2 = create(:merge_request, :merged, created_at: created_at, project: project) + mr3 = create(:merge_request, :merged, created_at: created_at, project: project_2) + + mr1.metrics.update!(merged_at: 1.day.ago) + mr2.metrics.update!(merged_at: 2.days.ago) + mr3.metrics.update!(merged_at: 3.days.ago) + end + + it 'does not find the time to merge of MRs from them' do + # Median of 2, 3, not including first MR + expect(time_to_merge).to include({ value: '1.5', title: "Time to Merge", unit: "days" }) end end end describe 'dora4 metrics' do - let(:lead_time_for_changes) { subject[2] } - let(:time_to_restore_service) { subject[3] } - let(:change_failure_rate) { subject[4] } + let(:lead_time_for_changes) { subject.find { |result| result[:identifier] == :lead_time_for_changes } } + let(:time_to_restore_service) { subject.find { |result| result[:identifier] == :time_to_restore_service } } + let(:change_failure_rate) { subject.find { |result| result[:identifier] == :change_failure_rate } } before do stub_licensed_features(dora4_analytics: true) @@ -284,7 +410,7 @@ end it 'does not return any dora4 metrics' do - expect(subject.size).to eq 2 + expect(subject.size).to eq 3 end end diff --git a/ee/spec/models/analytics/cycle_analytics/group_level_spec.rb b/ee/spec/models/analytics/cycle_analytics/group_level_spec.rb index 14c18cb64b6a0d6b6a46f09bc85bfbfda03b537d..e5158954185273d06f25a9c581fff32cabe67e0f 100644 --- a/ee/spec/models/analytics/cycle_analytics/group_level_spec.rb +++ b/ee/spec/models/analytics/cycle_analytics/group_level_spec.rb @@ -55,19 +55,19 @@ describe '#time_summary' do let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, :merged, created_at: 9.days.ago, project: project) } before do - # lead_time: 1 day, cycle_time: 2 days - + # lead_time: 1 day, cycle_time: 2 days, time_to_merge: 8 days issue.update!(created_at: 5.days.ago) - issue.metrics.update!(first_mentioned_in_commit_at: 4.days.ago) - issue.update!(closed_at: 3.days.ago) + + merge_request.metrics.update!(merged_at: 1.day.ago) end - it 'returns medians for lead time and cycle type' do - expect(subject.time_summary.map { |summary| summary[:value] }).to contain_exactly('1.0', '2.0') + it 'returns medians for lead time, cycle time and time to merge' do + expect(subject.time_summary.map { |summary| summary[:value] }).to contain_exactly('1.0', '2.0', '8.0') end end end diff --git a/ee/spec/services/analytics/cycle_analytics/consistency_check_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/consistency_check_service_spec.rb index 838e67d4de845b4828e5f6e0ef66c9a9906e5782..a648c59889e7dda428855d255dadff4278ec0d45 100644 --- a/ee/spec/services/analytics/cycle_analytics/consistency_check_service_spec.rb +++ b/ee/spec/services/analytics/cycle_analytics/consistency_check_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Analytics::CycleAnalytics::ConsistencyCheckService, :aggregate_failures do +RSpec.describe Analytics::CycleAnalytics::ConsistencyCheckService, :aggregate_failures, feature_category: :value_stream_management do let_it_be_with_refind(:group) { create(:group) } let_it_be_with_refind(:subgroup) { create(:group, parent: group) } @@ -27,9 +27,11 @@ expect(service_response).to be_success expect(service_response.payload[:reason]).to eq(:group_processed) - all_stage_events = event_model.all - expect(all_stage_events.size).to eq(1) - expect(all_stage_events.first[event_model.issuable_id_column]).to eq(record2.id) + # stage events where the end criteria are not met are excluded. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/408320 + target_stage_events = event_model.where.not(end_event_timestamp: nil) + expect(target_stage_events.size).to eq(1) + expect(target_stage_events.first[event_model.issuable_id_column]).to eq(record2.id) end context 'when running out of allotted time' do @@ -69,9 +71,11 @@ } end - all_stage_events = event_model.all - expect(all_stage_events.size).to eq(1) - expect(all_stage_events.first[event_model.issuable_id_column]).to eq(record2.id) + # stage events where the end criteria are not met are excluded. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/408320 + target_stage_events = event_model.where.not(end_event_timestamp: nil) + expect(target_stage_events.size).to eq(1) + expect(target_stage_events.first[event_model.issuable_id_column]).to eq(record2.id) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 91a5ecff377b937fc1ad492eb0671d203ec6e738..f9c49af131eecc1f6b12f17f74adb0143bff5df6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -46613,6 +46613,9 @@ msgstr "" msgid "Time spent must be formatted correctly. For example: 1h 30m." msgstr "" +msgid "Time to Merge" +msgstr "" + msgid "Time to Restore Service" msgstr "" @@ -49292,6 +49295,9 @@ msgstr "" msgid "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed." msgstr "" +msgid "ValueStreamAnalytics|Merge request analytics" +msgstr "" + msgid "ValueStreamAnalytics|New Value Stream" msgstr ""