Commit 455b3a38 authored by Adam Hegyi's avatar Adam Hegyi Committed by Ezekiel Kigbo

Expose stage endpoints for project-level VSA

This change backports the stage specific endpoints (count, average,
median, records) from the group-level (paid) to FOSS (project-level).
parent c6e8bde2
# frozen_string_literal: true
module Analytics
module CycleAnalytics
module StageActions
include Gitlab::Utils::StrongMemoize
extend ActiveSupport::Concern
included do
include CycleAnalyticsParams
before_action :validate_params, only: %i[median]
end
def index
result = list_service.execute
if result.success?
render json: cycle_analytics_configuration(result.payload[:stages])
else
render json: { message: result.message }, status: result.http_status
end
end
def median
render json: { value: data_collector.median.seconds }
end
def average
render json: { value: data_collector.average.seconds }
end
def records
serialized_records = data_collector.serialized_records do |relation|
add_pagination_headers(relation)
end
render json: serialized_records
end
def count
render json: { count: data_collector.count }
end
private
def parent
raise NotImplementedError
end
def value_stream_class
raise NotImplementedError
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,
per_page: relation.limit_value,
page: relation.current_page,
next_page: relation.next_page,
prev_page: relation.prev_page,
params: permitted_cycle_analytics_params
).execute(exclude_total_headers: true, data_without_counts: true)
end
def stage
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
end
def data_collector
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
stage: stage,
params: request_params.to_data_collector_params
)
end
def value_stream
@value_stream ||= value_stream_class.build_default_value_stream(parent)
end
def list_params
{ value_stream: value_stream }
end
def list_service
Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params)
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end
end
end
end
...@@ -16,9 +16,20 @@ module CycleAnalyticsParams ...@@ -16,9 +16,20 @@ module CycleAnalyticsParams
end end
def options(params) def options(params)
@options ||= { from: start_date(params), current_user: current_user }.merge(date_range(params)) @options ||= {}.tap do |opts|
opts[:current_user] = current_user
opts[:projects] = params[:project_ids] if params[:project_ids]
opts[:group] = params[:group_id] if params[:group_id]
opts[:from] = params[:from] || start_date(params)
opts[:to] = params[:to] if params[:to]
opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
opts.merge!(date_range(params))
end
end end
private
def start_date(params) def start_date(params)
case params[:start_date] case params[:start_date]
when '7' when '7'
...@@ -41,6 +52,27 @@ module CycleAnalyticsParams ...@@ -41,6 +52,27 @@ module CycleAnalyticsParams
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field) date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
date.to_time.utc date.to_time.utc
end end
def permitted_cycle_analytics_params
params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION)
end
def all_cycle_analytics_params
permitted_cycle_analytics_params.merge(current_user: current_user)
end
def request_params
@request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
end
def validate_params
if request_params.invalid?
render(
json: { message: 'Invalid parameters', errors: request_params.errors },
status: :unprocessable_entity
)
end
end
end end
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams') CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
# frozen_string_literal: true # frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
include ::Analytics::CycleAnalytics::StageActions
extend ::Gitlab::Utils::Override
respond_to :json respond_to :json
feature_category :planning_analytics feature_category :planning_analytics
...@@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat ...@@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
before_action :authorize_read_cycle_analytics! before_action :authorize_read_cycle_analytics!
before_action :only_default_value_stream_is_allowed! before_action :only_default_value_stream_is_allowed!
def index
result = list_service.execute
if result.success?
render json: cycle_analytics_configuration(result.payload[:stages])
else
render json: { message: result.message }, status: result.http_status
end
end
private private
def only_default_value_stream_is_allowed! override :parent
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME def parent
@project
end end
def value_stream override :value_stream_class
Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project) def value_stream_class
Analytics::CycleAnalytics::ProjectValueStream
end end
def list_params def only_default_value_stream_is_allowed!
{ value_stream: value_stream } render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
def list_service
Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end end
end end
...@@ -283,7 +283,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -283,7 +283,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics' resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
resources :value_streams, only: [:index] do resources :value_streams, only: [:index] do
resources :stages, only: [:index] resources :stages, only: [:index] do
member do
get :median
get :average
get :records
get :count
end
end
end end
resource :summary, controller: :summary, only: :show resource :summary, controller: :summary, only: :show
end end
......
# frozen_string_literal: true
module EE
module CycleAnalyticsParams
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
override :options
def options(params)
strong_memoize(:options) do
super.tap do |options|
options[:branch] = params[:branch_name]
options[:projects] = params[:project_ids] if params[:project_ids]
options[:group] = params[:group_id] if params[:group_id]
options[:from] = params[:from] if params[:from]
options[:to] = params[:to] if params[:to]
options[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
options.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
end
end
end
private
def permitted_cycle_analytics_params
params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION)
end
def all_cycle_analytics_params
permitted_cycle_analytics_params.merge(current_user: current_user)
end
def request_params
@request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
end
def validate_params
if request_params.invalid?
render(
json: { message: 'Invalid parameters', errors: request_params.errors },
status: :unprocessable_entity
)
end
end
end
end
...@@ -4,24 +4,18 @@ module Groups ...@@ -4,24 +4,18 @@ module Groups
module Analytics module Analytics
module CycleAnalytics module CycleAnalytics
class StagesController < Groups::Analytics::ApplicationController class StagesController < Groups::Analytics::ApplicationController
include CycleAnalyticsParams include ::Analytics::CycleAnalytics::StageActions
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
before_action :load_group before_action :load_group
before_action :load_value_stream
before_action :validate_params, only: %i[median average records average_duration_chart count] before_action :validate_params, only: %i[median average records average_duration_chart count]
before_action :authorize_read_group_stage, only: %i[median average records average_duration_chart count] before_action :authorize_read_group_stage, only: %i[median average records average_duration_chart count]
override :index
def index def index
return render_403 unless can?(current_user, :read_group_cycle_analytics, @group) return render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
result = list_service.execute super
if result.success?
render json: cycle_analytics_configuration(result.payload[:stages])
else
render json: { message: result.message }, status: result.http_status
end
end end
def create def create
...@@ -42,51 +36,20 @@ module Groups ...@@ -42,51 +36,20 @@ module Groups
render_stage_service_result(delete_service.execute) render_stage_service_result(delete_service.execute)
end end
def median
render json: { value: data_collector.median.seconds }
end
def average
render json: { value: data_collector.average.seconds }
end
def records
serialized_records = data_collector.serialized_records do |relation|
add_pagination_headers(relation)
end
render json: serialized_records
end
def average_duration_chart def average_duration_chart
render json: ::Analytics::CycleAnalytics::DurationChartAverageItemEntity.represent(data_collector.duration_chart_average_data) render json: ::Analytics::CycleAnalytics::DurationChartAverageItemEntity.represent(data_collector.duration_chart_average_data)
end end
def count
render json: { count: data_collector.count }
end
private private
def data_collector override :parent
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new( def parent
stage: stage, @group
params: request_params.to_data_collector_params
)
end
def stage
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: @group, stage_id: params[:id]).execute
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
::Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end end
def list_service override :value_stream_class
::Analytics::CycleAnalytics::Stages::ListService.new(parent: @group, current_user: current_user, params: list_params) def value_stream_class
::Analytics::CycleAnalytics::GroupValueStream
end end
def create_service def create_service
...@@ -115,10 +78,6 @@ module Groups ...@@ -115,10 +78,6 @@ module Groups
super.merge({ group: @group }) super.merge({ group: @group })
end end
def list_params
{ value_stream: @value_stream }
end
def update_params def update_params
params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden, :start_event_label_id, :end_event_label_id).merge(list_params) params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden, :start_event_label_id, :end_event_label_id).merge(list_params)
end end
...@@ -131,21 +90,12 @@ module Groups ...@@ -131,21 +90,12 @@ module Groups
params.permit(:id) params.permit(:id)
end end
def load_value_stream def value_stream
if params[:value_stream_id] && params[:value_stream_id] != ::Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME @value_stream ||= if params[:value_stream_id] && params[:value_stream_id] != ::Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
@value_stream = @group.value_streams.find(params[:value_stream_id]) @group.value_streams.find(params[:value_stream_id])
end else
end super
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,
per_page: relation.limit_value,
page: relation.current_page,
next_page: relation.next_page,
prev_page: relation.prev_page,
params: permitted_cycle_analytics_params
).execute(exclude_total_headers: true, data_without_counts: true)
end end
def authorize_read_group_stage def authorize_read_group_stage
......
...@@ -15,4 +15,8 @@ class Analytics::CycleAnalytics::GroupValueStream < ApplicationRecord ...@@ -15,4 +15,8 @@ class Analytics::CycleAnalytics::GroupValueStream < ApplicationRecord
def custom? def custom?
persisted? || name != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME persisted? || name != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end end
def self.build_default_value_stream(group)
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, group: group)
end
end end
...@@ -30,6 +30,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do ...@@ -30,6 +30,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
create(:cycle_analytics_group_stage, { create(:cycle_analytics_group_stage, {
name: 'label-based-stage', name: 'label-based-stage',
parent: group, parent: group,
value_stream: value_stream,
start_event_identifier: :issue_label_added, start_event_identifier: :issue_label_added,
start_event_label_id: label.id, start_event_label_id: label.id,
end_event_identifier: :issue_label_removed, end_event_identifier: :issue_label_removed,
...@@ -76,7 +77,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do ...@@ -76,7 +77,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end end
def additional_cycle_analytics_metrics def additional_cycle_analytics_metrics
create(:cycle_analytics_group_stage, parent: group) create(:cycle_analytics_group_stage, parent: group, value_stream: value_stream)
update_metrics update_metrics
...@@ -133,7 +134,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do ...@@ -133,7 +134,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end end
it 'analytics/value_stream_analytics/stages.json' do it 'analytics/value_stream_analytics/stages.json' do
get(:index, params: { group_id: group.name }, format: :json) get(:index, params: { group_id: group.name, value_stream_id: value_stream.id }, format: :json)
expect(response).to be_successful expect(response).to be_successful
end end
......
...@@ -44,6 +44,7 @@ module Gitlab ...@@ -44,6 +44,7 @@ module Gitlab
attribute :sort attribute :sort
attribute :direction attribute :direction
attribute :page attribute :page
attribute :project
attribute :stage_id attribute :stage_id
attribute :end_event_filter attribute :end_event_filter
...@@ -176,7 +177,7 @@ module Gitlab ...@@ -176,7 +177,7 @@ module Gitlab
return unless value_stream return unless value_stream
strong_memoize(:stage) do strong_memoize(:stage) do
::Analytics::CycleAnalytics::StageFinder.new(parent: group, stage_id: stage_id).execute if stage_id ::Analytics::CycleAnalytics::StageFinder.new(parent: project || group, stage_id: stage_id).execute if stage_id
end end
end end
end end
......
...@@ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do ...@@ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } } let(:params) do
{
namespace_id: group,
project_id: project,
value_stream_id: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
}
end
before do before do
sign_in(user) sign_in(user)
end end
describe 'GET index' do shared_examples 'project-level value stream analytics endpoint' do
context 'when user is member of the project' do before do
project.add_developer(user)
end
it 'succeeds' do
get action, params: params
expect(response).to have_gitlab_http_status(:ok)
end
end
shared_examples 'project-level value stream analytics request error examples' do
context 'when invalid value stream id is given' do
before do before do
project.add_developer(user) params[:value_stream_id] = 1
end end
it 'succeeds' do it 'renders 404' do
get :index, params: params get action, params: params
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:not_found)
end end
end
context 'when user is not member of the project' do
it 'renders 404' do
get action, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET index' do
let(:action) { :index }
it_behaves_like 'project-level value stream analytics endpoint' do
it 'exposes the default stages' do it 'exposes the default stages' do
get :index, params: params get action, params: params
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end end
...@@ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do ...@@ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
expect(list_service).to receive(:allowed?).and_return(false) expect(list_service).to receive(:allowed?).and_return(false)
end end
get :index, params: params get action, params: params
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end end
context 'when invalid value stream id is given' do it_behaves_like 'project-level value stream analytics request error examples'
before do end
params[:value_stream_id] = 1
describe 'GET median' do
let(:action) { :median }
before do
params[:id] = 'issue'
end
it_behaves_like 'project-level value stream analytics endpoint' do
it 'returns the median' do
result = 2
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Median) do |instance|
expect(instance).to receive(:seconds).and_return(result)
end
get action, params: params
expect(json_response['value']).to eq(result)
end end
end
it 'renders 404' do it_behaves_like 'project-level value stream analytics request error examples'
get :index, params: params end
expect(response).to have_gitlab_http_status(:not_found) describe 'GET average' do
let(:action) { :average }
before do
params[:id] = 'issue'
end
it_behaves_like 'project-level value stream analytics endpoint' do
it 'returns the average' do
result = 2
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Average) do |instance|
expect(instance).to receive(:seconds).and_return(result)
end
get action, params: params
expect(json_response['value']).to eq(result)
end end
end end
context 'when user is not member of the project' do it_behaves_like 'project-level value stream analytics request error examples'
it 'renders 404' do end
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found) describe 'GET count' do
let(:action) { :count }
before do
params[:id] = 'issue'
end
it_behaves_like 'project-level value stream analytics endpoint' do
it 'returns the count' do
count = 2
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::DataCollector) do |instance|
expect(instance).to receive(:count).and_return(count)
end
get action, params: params
expect(json_response['count']).to eq(count)
end end
end end
it_behaves_like 'project-level value stream analytics request error examples'
end
describe 'GET records' do
let(:action) { :records }
before do
params[:id] = 'issue'
end
it_behaves_like 'project-level value stream analytics endpoint' do
it 'returns the records' do
result = Issue.none.page(1)
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::RecordsFetcher) do |instance|
expect(instance).to receive(:serialized_records).and_yield(result).and_return([])
end
get action, params: params
expect(json_response).to eq([])
end
end
it_behaves_like 'project-level value stream analytics request error examples'
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