Commit efb60fef authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '335953-expose-project-level-stage-data' into 'master'

Add project level VSA endpoints

See merge request gitlab-org/gitlab!66439
parents 464302e2 455b3a38
# 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,8 +16,19 @@ module CycleAnalyticsParams
end
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
private
def start_date(params)
case params[:start_date]
......@@ -41,6 +52,27 @@ module CycleAnalyticsParams
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
date.to_time.utc
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
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
include ::Analytics::CycleAnalytics::StageActions
extend ::Gitlab::Utils::Override
respond_to :json
feature_category :planning_analytics
......@@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
before_action :authorize_read_cycle_analytics!
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
def only_default_value_stream_is_allowed!
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
def value_stream
Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
end
def list_params
{ value_stream: value_stream }
override :parent
def parent
@project
end
def list_service
Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
override :value_stream_class
def value_stream_class
Analytics::CycleAnalytics::ProjectValueStream
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
def only_default_value_stream_is_allowed!
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
end
......@@ -283,7 +283,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' 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
resource :summary, controller: :summary, only: :show
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
module Analytics
module CycleAnalytics
class StagesController < Groups::Analytics::ApplicationController
include CycleAnalyticsParams
include ::Analytics::CycleAnalytics::StageActions
extend ::Gitlab::Utils::Override
before_action :load_group
before_action :load_value_stream
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]
override :index
def index
return render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
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
super
end
def create
......@@ -42,51 +36,20 @@ module Groups
render_stage_service_result(delete_service.execute)
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
render json: ::Analytics::CycleAnalytics::DurationChartAverageItemEntity.represent(data_collector.duration_chart_average_data)
end
def count
render json: { count: data_collector.count }
end
private
def data_collector
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
stage: stage,
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)
override :parent
def parent
@group
end
def list_service
::Analytics::CycleAnalytics::Stages::ListService.new(parent: @group, current_user: current_user, params: list_params)
override :value_stream_class
def value_stream_class
::Analytics::CycleAnalytics::GroupValueStream
end
def create_service
......@@ -115,10 +78,6 @@ module Groups
super.merge({ group: @group })
end
def list_params
{ value_stream: @value_stream }
end
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)
end
......@@ -131,21 +90,12 @@ module Groups
params.permit(:id)
end
def load_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])
end
def value_stream
@value_stream ||= if params[:value_stream_id] && params[:value_stream_id] != ::Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
@group.value_streams.find(params[:value_stream_id])
else
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
def authorize_read_group_stage
......
......@@ -15,4 +15,8 @@ class Analytics::CycleAnalytics::GroupValueStream < ApplicationRecord
def custom?
persisted? || name != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
def self.build_default_value_stream(group)
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, group: group)
end
end
......@@ -30,6 +30,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
create(:cycle_analytics_group_stage, {
name: 'label-based-stage',
parent: group,
value_stream: value_stream,
start_event_identifier: :issue_label_added,
start_event_label_id: label.id,
end_event_identifier: :issue_label_removed,
......@@ -76,7 +77,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end
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
......@@ -133,7 +134,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end
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
end
......
......@@ -44,6 +44,7 @@ module Gitlab
attribute :sort
attribute :direction
attribute :page
attribute :project
attribute :stage_id
attribute :end_event_filter
......@@ -176,7 +177,7 @@ module Gitlab
return unless value_stream
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
......
......@@ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
let_it_be(:group) { create(: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
sign_in(user)
end
describe 'GET index' do
context 'when user is member of the project' do
shared_examples 'project-level value stream analytics endpoint' do
before do
project.add_developer(user)
end
it 'succeeds' do
get :index, params: params
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
params[:value_stream_id] = 1
end
it 'renders 404' do
get action, params: params
expect(response).to have_gitlab_http_status(:not_found)
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
get :index, params: params
get action, params: params
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end
......@@ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
expect(list_service).to receive(:allowed?).and_return(false)
end
get :index, params: params
get action, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when invalid value stream id is given' do
it_behaves_like 'project-level value stream analytics request error examples'
end
describe 'GET median' do
let(:action) { :median }
before do
params[:value_stream_id] = 1
params[:id] = 'issue'
end
it 'renders 404' do
get :index, params: params
it_behaves_like 'project-level value stream analytics endpoint' do
it 'returns the median' do
result = 2
expect(response).to have_gitlab_http_status(:not_found)
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
context 'when user is not member of the project' do
it 'renders 404' do
get :index, params: params
it_behaves_like 'project-level value stream analytics request error examples'
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
it_behaves_like 'project-level value stream analytics request error examples'
end
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
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
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