Commit 0a016946 authored by Robert Speicher's avatar Robert Speicher

Merge branch '12183-data-for-ca-duration-chart' into 'master'

Add duration_chart endpoint for Cycle Analytics

Closes #12183

See merge request gitlab-org/gitlab!19293
parents 7ea30d16 a581411e
......@@ -38,7 +38,8 @@ module CycleAnalyticsParams
end
def to_utc_time(field)
Date.parse(field).to_time.utc
date = field.is_a?(Date) ? field : Date.parse(field)
date.to_time.utc
end
end
......
......@@ -6,7 +6,7 @@ module Analytics
check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG
before_action :load_group
before_action :validate_params, only: %i[median records]
before_action :validate_params, only: %i[median records duration_chart]
def index
return render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
......@@ -50,6 +50,12 @@ module Analytics
render json: data_collector.serialized_records
end
def duration_chart
return render_403 unless can?(current_user, :read_group_stage, @group)
render json: Analytics::CycleAnalytics::DurationChartItemEntity.represent(data_collector.duration_chart_data)
end
private
def validate_params
......@@ -62,14 +68,15 @@ module Analytics
end
def request_params
@request_params ||= Gitlab::Analytics::CycleAnalytics::RequestParams.new(params.permit(:created_before, :created_after))
@request_params ||= Gitlab::Analytics::CycleAnalytics::RequestParams.new(data_collector_params)
end
def data_collector
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: {
current_user: current_user,
from: request_params.created_after,
to: request_params.created_before
to: request_params.created_before,
project_ids: request_params.project_ids
})
end
......@@ -108,6 +115,10 @@ module Analytics
end
end
def data_collector_params
params.permit(:created_before, :created_after, project_ids: [])
end
def update_params
params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden)
end
......
......@@ -13,15 +13,19 @@ module Analytics
def show
return render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
group_level = ::CycleAnalytics::GroupLevel.new(group: @group, options: options(allowed_group_params))
group_level = ::CycleAnalytics::GroupLevel.new(group: @group, options: options(group_params))
render json: group_level.summary
end
private
def allowed_group_params
params.permit(:created_after, :created_before, project_ids: [])
def group_params
{
created_after: request_params.created_after,
created_before: request_params.created_before,
project_ids: request_params.project_ids
}
end
def validate_params
......@@ -34,7 +38,11 @@ module Analytics
end
def request_params
@request_params ||= Gitlab::Analytics::CycleAnalytics::RequestParams.new(params.permit(:created_before, :created_after))
@request_params ||= Gitlab::Analytics::CycleAnalytics::RequestParams.new(allowed_params)
end
def allowed_params
params.permit(:created_after, :created_before, project_ids: [])
end
end
end
......
# frozen_string_literal: true
module Analytics
module CycleAnalytics
class DurationChartItemEntity < Grape::Entity
expose :finished_at
expose :duration_in_seconds
end
end
end
......@@ -10,6 +10,7 @@ namespace :analytics do
namespace :cycle_analytics do
resources :stages, only: [:index, :create, :update, :destroy] do
member do
get :duration_chart
get :median
get :records
end
......
# frozen_string_literal: true
module EE
module Gitlab
module Analytics
module CycleAnalytics
module DataCollector
def duration_chart_data
strong_memoize(:duration_chart) do
::Gitlab::Analytics::CycleAnalytics::DataForDurationChart.new(stage: stage, query: query).load
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Analytics
module CycleAnalytics
class DataForDurationChart
include StageQueryHelpers
MAX_RESULTS = 500
def initialize(stage:, query:)
@stage = stage
@query = query
end
# rubocop: disable CodeReuse/ActiveRecord
def load
query
.select(round_duration_to_seconds.as('duration_in_seconds'), stage.end_event.timestamp_projection.as('finished_at'))
.reorder(stage.end_event.timestamp_projection.desc)
.limit(MAX_RESULTS)
end
# rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :stage, :query
end
end
end
end
......@@ -8,6 +8,8 @@ module Gitlab
include ActiveModel::Validations
include ActiveModel::Attributes
attr_writer :project_ids
attribute :created_after, :date
attribute :created_before, :date
......@@ -16,6 +18,10 @@ module Gitlab
validate :validate_created_before
def project_ids
Array(@project_ids)
end
private
def validate_created_before
......
......@@ -185,43 +185,48 @@ describe Analytics::CycleAnalytics::StagesController do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'data endpoints' do
let(:stage) { create(:cycle_analytics_group_stage, parent: group) }
before do
params[:id] = stage.id
end
describe 'GET `median`' do
subject { get :median, params: params }
before do
params[:created_after] = '2019-01-01'
params[:created_before] = '2020-01-01'
end
it 'succeeds' do
it 'matches the response schema' do
subject
expect(response).to be_successful
expect(response).to match_response_schema('analytics/cycle_analytics/median', dir: 'ee')
end
include_examples 'date parameter examples'
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
describe 'GET `records`' do
subject { get :records, params: params }
before do
params[:created_after] = '2019-01-01'
params[:created_before] = '2020-01-01'
end
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
describe 'GET `duration_chart`' do
subject { get :duration_chart, params: params }
it 'matches the response schema' do
fake_result = [double(MergeRequest, duration_in_seconds: 10, finished_at: Time.now)]
expect_any_instance_of(Gitlab::Analytics::CycleAnalytics::DataForDurationChart).to receive(:load).and_return(fake_result)
it 'succeeds' do
subject
expect(response).to be_successful
expect(response).to match_response_schema('analytics/cycle_analytics/duration_chart', dir: 'ee')
end
include_examples 'date parameter examples'
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
end
......
......@@ -25,8 +25,7 @@ describe Analytics::CycleAnalytics::SummaryController do
expect(response).to match_response_schema('analytics/cycle_analytics/summary', dir: 'ee')
end
include_examples 'date parameter examples'
include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level'
end
end
{
"type": "array",
"items": {
"type": "object",
"required": ["duration_in_seconds", "finished_at"],
"properties": {
"duration_in_seconds": {
"type": "integer"
},
"finished_at": {
"type": "string"
}
},
"additionalProperties": false
}
}
......@@ -58,6 +58,16 @@ describe Gitlab::Analytics::CycleAnalytics::DataCollector do
it 'calculates median' do
expect(round_to_days(data_collector.median.seconds)).to eq(10)
end
describe '#duration_chart_data' do
subject { data_collector.duration_chart_data }
it 'loads data ordered by event time' do
days = subject.map { |item| round_to_days(item.duration_in_seconds) }
expect(days).to eq([15, 10, 5])
end
end
end
shared_examples 'test various start and end event combinations' do
......
......@@ -40,4 +40,34 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it 'casts `created_before` to date' do
expect(subject.created_before).to be_a_kind_of(Date)
end
describe 'optional `project_ids`' do
it { expect(subject.project_ids).to eq([]) }
context 'when `project_ids` is not empty' do
let(:project_ids) { [1, 2, 3] }
before do
params[:project_ids] = project_ids
end
it { expect(subject.project_ids).to eq(project_ids) }
end
context 'when `project_ids` is not an array' do
before do
params[:project_ids] = 1
end
it { expect(subject.project_ids).to eq([1]) }
end
context 'when `project_ids` is nil' do
before do
params[:project_ids] = nil
end
it { expect(subject.project_ids).to eq([]) }
end
end
end
......@@ -78,7 +78,7 @@ shared_context 'when invalid stage parameters are given' do
end
end
shared_examples 'date parameter examples' do
shared_examples 'cycle analytics data endpoint examples' do
before do
params[:created_after] = '2019-01-01'
params[:created_before] = '2020-01-01'
......@@ -92,6 +92,20 @@ shared_examples 'date parameter examples' do
end
end
context 'accepts optional `project_ids` array' do
before do
params[:project_ids] = [1, 2, 3]
end
it 'succeeds' do
expect_any_instance_of(Gitlab::Analytics::CycleAnalytics::RequestParams).to receive(:project_ids=).with(%w[1 2 3]).and_call_original
subject
expect(response).to be_successful
end
end
shared_examples 'example for invalid parameter' do
it 'renders `unprocessable_entity`' do
subject
......
......@@ -42,3 +42,5 @@ module Gitlab
end
end
end
Gitlab::Analytics::CycleAnalytics::DataCollector.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::DataCollector')
......@@ -9,11 +9,11 @@ module Gitlab
end
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
Arel::Nodes::NamedFunction.new('CAST', [Arel.sql("'0' AS INTERVAL")])
end
def round_duration_to_seconds
Arel::Nodes::Extract.new(duration, :epoch)
Arel::Nodes::NamedFunction.new('ROUND', [Arel::Nodes::Extract.new(duration, :epoch)])
end
def duration
......
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