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 ...@@ -38,7 +38,8 @@ module CycleAnalyticsParams
end end
def to_utc_time(field) 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
end end
......
...@@ -6,7 +6,7 @@ module Analytics ...@@ -6,7 +6,7 @@ module Analytics
check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG
before_action :load_group before_action :load_group
before_action :validate_params, only: %i[median records] before_action :validate_params, only: %i[median records duration_chart]
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)
...@@ -50,6 +50,12 @@ module Analytics ...@@ -50,6 +50,12 @@ module Analytics
render json: data_collector.serialized_records render json: data_collector.serialized_records
end 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 private
def validate_params def validate_params
...@@ -62,14 +68,15 @@ module Analytics ...@@ -62,14 +68,15 @@ module Analytics
end end
def request_params 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 end
def data_collector def data_collector
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: {
current_user: current_user, current_user: current_user,
from: request_params.created_after, from: request_params.created_after,
to: request_params.created_before to: request_params.created_before,
project_ids: request_params.project_ids
}) })
end end
...@@ -108,6 +115,10 @@ module Analytics ...@@ -108,6 +115,10 @@ module Analytics
end end
end end
def data_collector_params
params.permit(:created_before, :created_after, project_ids: [])
end
def update_params def update_params
params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden) params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden)
end end
......
...@@ -13,15 +13,19 @@ module Analytics ...@@ -13,15 +13,19 @@ module Analytics
def show def show
return render_403 unless can?(current_user, :read_group_cycle_analytics, @group) 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 render json: group_level.summary
end end
private private
def allowed_group_params def group_params
params.permit(:created_after, :created_before, project_ids: []) {
created_after: request_params.created_after,
created_before: request_params.created_before,
project_ids: request_params.project_ids
}
end end
def validate_params def validate_params
...@@ -34,7 +38,11 @@ module Analytics ...@@ -34,7 +38,11 @@ module Analytics
end end
def request_params 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 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 ...@@ -10,6 +10,7 @@ namespace :analytics do
namespace :cycle_analytics do namespace :cycle_analytics do
resources :stages, only: [:index, :create, :update, :destroy] do resources :stages, only: [:index, :create, :update, :destroy] do
member do member do
get :duration_chart
get :median get :median
get :records get :records
end 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 ...@@ -8,6 +8,8 @@ module Gitlab
include ActiveModel::Validations include ActiveModel::Validations
include ActiveModel::Attributes include ActiveModel::Attributes
attr_writer :project_ids
attribute :created_after, :date attribute :created_after, :date
attribute :created_before, :date attribute :created_before, :date
...@@ -16,6 +18,10 @@ module Gitlab ...@@ -16,6 +18,10 @@ module Gitlab
validate :validate_created_before validate :validate_created_before
def project_ids
Array(@project_ids)
end
private private
def validate_created_before def validate_created_before
......
...@@ -185,43 +185,48 @@ describe Analytics::CycleAnalytics::StagesController do ...@@ -185,43 +185,48 @@ describe Analytics::CycleAnalytics::StagesController do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
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 describe 'GET `median`' do
subject { get :median, params: params } subject { get :median, params: params }
before do it 'matches the response schema' do
params[:created_after] = '2019-01-01'
params[:created_before] = '2020-01-01'
end
it 'succeeds' do
subject subject
expect(response).to be_successful
expect(response).to match_response_schema('analytics/cycle_analytics/median', dir: 'ee') expect(response).to match_response_schema('analytics/cycle_analytics/median', dir: 'ee')
end end
include_examples 'date parameter examples' include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level' include_examples 'group permission check on the controller level'
end end
describe 'GET `records`' do describe 'GET `records`' do
subject { get :records, params: params } subject { get :records, params: params }
before do include_examples 'cycle analytics data endpoint examples'
params[:created_after] = '2019-01-01' include_examples 'group permission check on the controller level'
params[:created_before] = '2020-01-01' end
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 subject
expect(response).to be_successful expect(response).to match_response_schema('analytics/cycle_analytics/duration_chart', dir: 'ee')
end end
include_examples 'date parameter examples' include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level' include_examples 'group permission check on the controller level'
end end
end end
......
...@@ -25,8 +25,7 @@ describe Analytics::CycleAnalytics::SummaryController do ...@@ -25,8 +25,7 @@ describe Analytics::CycleAnalytics::SummaryController do
expect(response).to match_response_schema('analytics/cycle_analytics/summary', dir: 'ee') expect(response).to match_response_schema('analytics/cycle_analytics/summary', dir: 'ee')
end end
include_examples 'date parameter examples' include_examples 'cycle analytics data endpoint examples'
include_examples 'group permission check on the controller level' include_examples 'group permission check on the controller level'
end end
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 ...@@ -58,6 +58,16 @@ describe Gitlab::Analytics::CycleAnalytics::DataCollector do
it 'calculates median' do it 'calculates median' do
expect(round_to_days(data_collector.median.seconds)).to eq(10) expect(round_to_days(data_collector.median.seconds)).to eq(10)
end 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 end
shared_examples 'test various start and end event combinations' do shared_examples 'test various start and end event combinations' do
......
...@@ -40,4 +40,34 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do ...@@ -40,4 +40,34 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it 'casts `created_before` to date' do it 'casts `created_before` to date' do
expect(subject.created_before).to be_a_kind_of(Date) expect(subject.created_before).to be_a_kind_of(Date)
end 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 end
...@@ -78,7 +78,7 @@ shared_context 'when invalid stage parameters are given' do ...@@ -78,7 +78,7 @@ shared_context 'when invalid stage parameters are given' do
end end
end end
shared_examples 'date parameter examples' do shared_examples 'cycle analytics data endpoint examples' do
before do before do
params[:created_after] = '2019-01-01' params[:created_after] = '2019-01-01'
params[:created_before] = '2020-01-01' params[:created_before] = '2020-01-01'
...@@ -92,6 +92,20 @@ shared_examples 'date parameter examples' do ...@@ -92,6 +92,20 @@ shared_examples 'date parameter examples' do
end end
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 shared_examples 'example for invalid parameter' do
it 'renders `unprocessable_entity`' do it 'renders `unprocessable_entity`' do
subject subject
......
...@@ -42,3 +42,5 @@ module Gitlab ...@@ -42,3 +42,5 @@ module Gitlab
end end
end end
end end
Gitlab::Analytics::CycleAnalytics::DataCollector.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::DataCollector')
...@@ -9,11 +9,11 @@ module Gitlab ...@@ -9,11 +9,11 @@ module Gitlab
end end
def zero_interval def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) Arel::Nodes::NamedFunction.new('CAST', [Arel.sql("'0' AS INTERVAL")])
end end
def round_duration_to_seconds def round_duration_to_seconds
Arel::Nodes::Extract.new(duration, :epoch) Arel::Nodes::NamedFunction.new('ROUND', [Arel::Nodes::Extract.new(duration, :epoch)])
end end
def duration 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