Commit efdc6a99 authored by Adam Hegyi's avatar Adam Hegyi

Optionally use new VSA backend on the project level

This change introduces a feature flag (new_project_level_vsa_backend)
where the new backend implementation is going to be used on the project
level value stream analytics page.
parent 32f335e3
...@@ -4,9 +4,52 @@ module CycleAnalytics ...@@ -4,9 +4,52 @@ module CycleAnalytics
module LevelBase module LevelBase
STAGES = %i[issue plan code test review staging].freeze STAGES = %i[issue plan code test review staging].freeze
# This is a temporary adapter class which makes the new value stream (cycle analytics)
# backend compatible with the old implementation.
class StageAdapter
def initialize(stage, options)
@stage = stage
@options = options
end
# rubocop: disable CodeReuse/Presenter
def as_json(serializer: AnalyticsStageSerializer)
presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
serializer.new.represent(OpenStruct.new(
title: presenter.title,
description: presenter.description,
legend: presenter.legend,
name: stage.name,
project_median: median,
group_median: median
))
end
# rubocop: enable CodeReuse/Presenter
def events
data_collector.records_fetcher.serialized_records
end
def median
data_collector.median.seconds
end
alias_method :project_median, :median
alias_method :group_median, :median
private
attr_reader :stage, :options
def data_collector
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options)
end
end
def all_medians_by_stage def all_medians_by_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage| STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].project_median medians_per_stage[stage_name] = self[stage_name].median
end end
end end
...@@ -21,7 +64,15 @@ module CycleAnalytics ...@@ -21,7 +64,15 @@ module CycleAnalytics
end end
def [](stage_name) def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(options: options) if Feature.enabled?(:new_project_level_vsa_backend, resource_parent)
StageAdapter.new(build_stage(stage_name), options)
else
Gitlab::CycleAnalytics::Stage[stage_name].new(options: options)
end
end
def stage_params_by_name(name)
Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name)
end end
end end
end end
...@@ -20,5 +20,14 @@ module CycleAnalytics ...@@ -20,5 +20,14 @@ module CycleAnalytics
def permissions(user:) def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: project) Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
end end
def build_stage(stage_name)
stage_params = stage_params_by_name(stage_name).merge(project: project)
Analytics::CycleAnalytics::ProjectStage.new(stage_params)
end
def resource_parent
project
end
end end
end end
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
%span.has-tooltip{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" } %span.has-tooltip{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
= sprite_icon('question-o', css_class: 'gl-text-gray-500') = sprite_icon('question-o', css_class: 'gl-text-gray-500')
%li.event-header.pl-3 %li.event-header.pl-3
%span.stage-name.font-weight-bold %span.stage-name.font-weight-bold{ "v-if" => "currentStage && currentStage.legend" }
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }} {{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
%span.has-tooltip{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" } %span.has-tooltip{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
= sprite_icon('question-o', css_class: 'gl-text-gray-500') = sprite_icon('question-o', css_class: 'gl-text-gray-500')
......
---
name: new_project_level_vsa_backend
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282435
milestone: '13.7'
type: development
group: group::optimize
default_enabled: false
...@@ -37,6 +37,15 @@ module Analytics ...@@ -37,6 +37,15 @@ module Analytics
self[stage_name].as_json(serializer: GroupAnalyticsStageSerializer) self[stage_name].as_json(serializer: GroupAnalyticsStageSerializer)
end end
end end
def build_stage(stage_name)
stage_params = stage_params_by_name(stage_name).merge(group: group)
Analytics::CycleAnalytics::GroupStage.new(stage_params)
end
def resource_parent
group
end
end end
end end
end end
...@@ -22,6 +22,10 @@ module Gitlab ...@@ -22,6 +22,10 @@ module Gitlab
] ]
end end
def self.find_by_name!(name)
all.find { |raw_stage| raw_stage[:name].to_s.eql?(name.to_s) } || raise("Default stage '#{name}' not found")
end
def self.names def self.names
all.map { |stage| stage[:name] } all.map { |stage| stage[:name] }
end end
......
...@@ -48,6 +48,16 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -48,6 +48,16 @@ RSpec.describe 'Value Stream Analytics', :js do
@build = create_cycle(user, project, issue, mr, milestone, pipeline) @build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project) deploy_master(user, project)
issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.day)
merge_request = issue.merge_requests_closing_issues.first.merge_request
merge_request.update!(created_at: issue.metrics.first_associated_with_milestone_at + 1.day)
merge_request.metrics.update!(
latest_build_started_at: 4.hours.ago,
latest_build_finished_at: 3.hours.ago,
merged_at: merge_request.created_at + 1.hour,
first_deployed_to_production_at: merge_request.created_at + 2.hours
)
sign_in(user) sign_in(user)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
end end
......
...@@ -40,6 +40,9 @@ RSpec.describe 'value stream analytics events', :aggregate_failures do ...@@ -40,6 +40,9 @@ RSpec.describe 'value stream analytics events', :aggregate_failures do
before do before do
create_commit_referencing_issue(context) create_commit_referencing_issue(context)
# Adding extra duration because the new VSA backend filters out 0 durations between these columns
context.metrics.update!(first_mentioned_in_commit_at: context.metrics.first_associated_with_milestone_at + 1.day)
end end
it 'has correct attributes' do it 'has correct attributes' do
......
...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#code' do ...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#code' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
subject { project_level } subject { project_level }
......
...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#issue' do ...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#issue' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
subject { project_level } subject { project_level }
......
...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#plan' do ...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#plan' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
subject { project_level } subject { project_level }
......
...@@ -9,7 +9,7 @@ RSpec.describe 'CycleAnalytics#review' do ...@@ -9,7 +9,7 @@ RSpec.describe 'CycleAnalytics#review' do
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :review, phase: :review,
......
...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#staging' do ...@@ -8,7 +8,7 @@ RSpec.describe 'CycleAnalytics#staging' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
subject { project_level } subject { project_level }
......
...@@ -9,7 +9,7 @@ RSpec.describe 'CycleAnalytics#test' do ...@@ -9,7 +9,7 @@ RSpec.describe 'CycleAnalytics#test' do
let_it_be(:from_date) { 10.days.ago } let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) } let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) }
let!(:merge_request) { create_merge_request_closing_issue(user, project, issue) } let!(:merge_request) { create_merge_request_closing_issue(user, project, issue) }
subject { project_level } subject { project_level }
......
...@@ -7,108 +7,115 @@ RSpec.describe 'value stream analytics events' do ...@@ -7,108 +7,115 @@ RSpec.describe 'value stream analytics events' do
let(:project) { create(:project, :repository, public_builds: false) } let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do shared_examples 'value stream analytics events examples' do
before do describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
project.add_developer(user) before do
project.add_developer(user)
3.times do |count| 3.times do |count|
travel_to(Time.now + count.days) do travel_to(Time.now + count.days) do
create_cycle create_cycle
end
end end
end
deploy_master(user, project)
login_as(user)
end
it 'lists the issue events' do
get project_cycle_analytics_issue_path(project, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s deploy_master(user, project)
expect(json_response['events']).not_to be_empty login_as(user)
expect(json_response['events'].first['iid']).to eq(first_issue_iid) end
end
it 'lists the plan events' do it 'lists the issue events' do
get project_cycle_analytics_plan_path(project, format: :json) get project_cycle_analytics_issue_path(project, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid) expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end end
it 'lists the code events' do it 'lists the plan events' do
get project_cycle_analytics_code_path(project, format: :json) get project_cycle_analytics_plan_path(project, format: :json)
expect(json_response['events']).not_to be_empty first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
expect(json_response['events'].first['iid']).to eq(first_mr_iid) it 'lists the code events' do
end get project_cycle_analytics_code_path(project, format: :json)
it 'lists the test events', :sidekiq_might_not_need_inline do expect(json_response['events']).not_to be_empty
get project_cycle_analytics_test_path(project, format: :json)
expect(json_response['events']).not_to be_empty first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the review events' do expect(json_response['events'].first['iid']).to eq(first_mr_iid)
get project_cycle_analytics_review_path(project, format: :json) end
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s it 'lists the test events', :sidekiq_inline do
get project_cycle_analytics_test_path(project, format: :json)
expect(json_response['events']).not_to be_empty expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid) expect(json_response['events'].first['date']).not_to be_empty
end end
it 'lists the staging events', :sidekiq_might_not_need_inline do it 'lists the review events' do
get project_cycle_analytics_staging_path(project, format: :json) get project_cycle_analytics_review_path(project, format: :json)
expect(json_response['events']).not_to be_empty first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['date']).not_to be_empty
end
context 'specific branch' do expect(json_response['events']).not_to be_empty
it 'lists the test events', :sidekiq_might_not_need_inline do expect(json_response['events'].first['iid']).to eq(first_mr_iid)
branch = project.merge_requests.first.source_branch end
get project_cycle_analytics_test_path(project, format: :json, branch: branch) it 'lists the staging events', :sidekiq_inline do
get project_cycle_analytics_staging_path(project, format: :json)
expect(json_response['events']).not_to be_empty expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty expect(json_response['events'].first['date']).not_to be_empty
end end
end
context 'with private project and builds' do context 'with private project and builds' do
before do before do
project.members.last.update(access_level: Gitlab::Access::GUEST) project.members.last.update(access_level: Gitlab::Access::GUEST)
end end
it 'does not list the test events' do it 'does not list the test events' do
get project_cycle_analytics_test_path(project, format: :json) get project_cycle_analytics_test_path(project, format: :json)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'does not list the staging events' do it 'does not list the staging events' do
get project_cycle_analytics_staging_path(project, format: :json) get project_cycle_analytics_staging_path(project, format: :json)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'lists the issue events' do it 'lists the issue events' do
get project_cycle_analytics_issue_path(project, format: :json) get project_cycle_analytics_issue_path(project, format: :json)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end
end end
end end
end end
describe 'when new_project_level_vsa_backend feature flag is off' do
before do
stub_feature_flags(new_project_level_vsa_backend: false, thing: project)
end
it_behaves_like 'value stream analytics events examples'
end
describe 'when new_project_level_vsa_backend feature flag is on' do
before do
stub_feature_flags(new_project_level_vsa_backend: true, thing: project)
end
it_behaves_like 'value stream analytics events examples'
end
def create_cycle def create_cycle
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
issue.update(milestone: milestone) issue.update(milestone: milestone)
...@@ -123,5 +130,7 @@ RSpec.describe 'value stream analytics events' do ...@@ -123,5 +130,7 @@ RSpec.describe 'value stream analytics events' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
mr.metrics.update!(latest_build_started_at: 1.hour.ago, latest_build_finished_at: Time.now)
end end
end end
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