Commit b8056669 authored by James Lopez's avatar James Lopez

refactor cycle analytics - updated based on MR feedback

parent 8183e848
...@@ -9,33 +9,33 @@ module Projects ...@@ -9,33 +9,33 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review] before_action :authorize_read_merge_request!, only: [:code, :review]
def issue def issue
render_events(cycle_analytics.events_for(:issue)) render_events(cycle_analytics[:issue].events)
end end
def plan def plan
render_events(cycle_analytics.events_for(:plan)) render_events(cycle_analytics[:plan].events)
end end
def code def code
render_events(cycle_analytics.events_for(:code)) render_events(cycle_analytics[:code].events)
end end
def test def test
options(events_params)[:branch] = events_params[:branch_name] options(events_params)[:branch] = events_params[:branch_name]
render_events(cycle_analytics.events_for(:test)) render_events(cycle_analytics[:test].events)
end end
def review def review
render_events(cycle_analytics.events_for(:review)) render_events(cycle_analytics[:review].events)
end end
def staging def staging
render_events(cycle_analytics.events_for(:staging)) render_events(cycle_analytics[:staging].events)
end end
def production def production
render_events(cycle_analytics.events_for(:production)) render_events(cycle_analytics[:production].events)
end end
private private
...@@ -54,7 +54,7 @@ module Projects ...@@ -54,7 +54,7 @@ module Projects
def events_params def events_params
return {} unless params[:events].present? return {} unless params[:events].present?
params[:events].slice(:start_date, :branch_name) params[:events].permit(:start_date, :branch_name)
end end
end end
end end
......
...@@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics! before_action :authorize_read_cycle_analytics!
def show def show
@cycle_analytics = ::CycleAnalytics.new(@project, options: options(cycle_analytics_params)) @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
@cycle_analytics_no_data = @cycle_analytics.no_stats? @cycle_analytics_no_data = @cycle_analytics.no_stats?
...@@ -21,7 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -21,7 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def cycle_analytics_params def cycle_analytics_params
return {} unless params[:cycle_analytics].present? return {} unless params[:cycle_analytics].present?
params[:cycle_analytics].slice(:start_date) params[:cycle_analytics].permit(:start_date)
end end
def cycle_analytics_json def cycle_analytics_json
......
class CycleAnalytics class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, options:) def initialize(project, options)
@project = project @project = project
@options = options @options = options
end end
...@@ -22,19 +22,15 @@ class CycleAnalytics ...@@ -22,19 +22,15 @@ class CycleAnalytics
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end end
def events_for(stage) def [](stage_name)
classify_stage(stage).new(project: @project, options: @options, stage: stage).events Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end end
private private
def stats_per_stage def stats_per_stage
STAGES.map do |stage_name| STAGES.map do |stage_name|
classify_stage(stage_name).new(project: @project, options: @options, stage: stage_name).median_data Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options).median_data
end end
end end
def classify_stage(stage_name)
"Gitlab::CycleAnalytics::#{stage_name.to_s.capitalize}Stage".constantize
end
end end
class AnalyticsStageEntity < Grape::Entity class AnalyticsStageEntity < Grape::Entity
include EntityDateHelper include EntityDateHelper
expose :stage, as: :title do |object| expose :title
object.stage.to_s.capitalize
end
expose :description expose :description
expose :median, as: :value do |stage| expose :median, as: :value do |stage|
......
...@@ -2,13 +2,13 @@ module Gitlab ...@@ -2,13 +2,13 @@ module Gitlab
module CycleAnalytics module CycleAnalytics
class BaseEvent class BaseEvent
include MetricsTables include MetricsTables
include ClassNameUtil
attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query attr_reader :start_time_attrs, :end_time_attrs, :projections, :query
def initialize(fetcher:, stage:, options:) def initialize(fetcher:, options:)
@query = EventsQuery.new(fetcher: fetcher) @query = EventsQuery.new(fetcher: fetcher)
@project = fetcher.project @project = fetcher.project
@stage = stage
@options = options @options = options
end end
...@@ -26,6 +26,10 @@ module Gitlab ...@@ -26,6 +26,10 @@ module Gitlab
@order || @start_time_attrs @order || @start_time_attrs
end end
def stage
class_name_for('Event')
end
private private
def update_author! def update_author!
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class BaseStage class BaseStage
attr_reader :stage, :description include ClassNameUtil
def initialize(project:, options:, stage:) def initialize(project:, options:)
@project = project @project = project
@options = options @options = options
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project,
from: options[:from], from: options[:from],
branch: options[:branch]) branch: options[:branch])
@stage = stage
end end
def events def events
event_class.new(fetcher: @fetcher, stage: @stage, options: @options).fetch Gitlab::CycleAnalytics::Event[stage].new(fetcher: @fetcher, options: @options).fetch
end end
def median_data def median_data
AnalyticsStageSerializer.new.represent(self).as_json AnalyticsStageSerializer.new.represent(self).as_json
end end
def title
stage.to_s.capitalize
end
def median
raise NotImplementedError.new("Expected #{self.name} to implement median")
end
private private
def event_class def stage
"Gitlab::CycleAnalytics::#{@stage.to_s.capitalize}Event".constantize class_name_for('Stage')
end end
end end
end end
......
module Gitlab
module CycleAnalytics
module ClassNameUtil
def class_name_for(type)
class_name.split(type).first.to_sym
end
def class_name
self.class.name.demodulize
end
end
end
end
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class CodeStage < BaseStage class CodeStage < BaseStage
def initialize(*args) def description
super(*args) "Time until first merge request"
@description = "Time until first merge request"
end end
def median def median
@fetcher.calculate_metric(:code, @fetcher.median(:code,
Issue::Metrics.arel_table[:first_mentioned_in_commit_at], Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
MergeRequest.arel_table[:created_at]) MergeRequest.arel_table[:created_at])
end end
end end
end end
......
module Gitlab
module CycleAnalytics
module Event
def self.[](stage_name)
const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Event")
end
end
end
end
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class IssueStage < BaseStage class IssueStage < BaseStage
def initialize(*args) def description
super(*args) "Time before an issue gets scheduled"
@description = "Time before an issue gets scheduled"
end end
def median def median
@fetcher.calculate_metric(:issue, @fetcher.median(:issue,
Issue.arel_table[:created_at], Issue.arel_table[:created_at],
[Issue::Metrics.arel_table[:first_associated_with_milestone_at], [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]]) Issue::Metrics.arel_table[:first_added_to_board_at]])
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
@branch = branch @branch = branch
end end
def calculate_metric(name, start_time_attrs, end_time_attrs) def median(name, start_time_attrs, end_time_attrs)
cte_table = Arel::Table.new("cte_table_for_#{name}") cte_table = Arel::Table.new("cte_table_for_#{name}")
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class PlanStage < BaseStage class PlanStage < BaseStage
def initialize(*args) def description
super(*args) "Time before an issue starts implementation"
@description = "Time before an issue starts implementation"
end end
def median def median
@fetcher.calculate_metric(:plan, @fetcher.median(:plan,
[Issue::Metrics.arel_table[:first_associated_with_milestone_at], [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]], Issue::Metrics.arel_table[:first_added_to_board_at]],
Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
end end
end end
end end
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class ProductionStage < BaseStage class ProductionStage < BaseStage
def initialize(*args) def description
super(*args) "From issue creation until deploy to production"
@description = "From issue creation until deploy to production"
end end
def median def median
@fetcher.calculate_metric(:production, @fetcher.median(:production,
Issue.arel_table[:created_at], Issue.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end end
end end
end end
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class ReviewStage < BaseStage class ReviewStage < BaseStage
def initialize(*args) def description
super(*args) "Time between merge request creation and merge/close"
@description = "Time between merge request creation and merge/close"
end end
def median def median
@fetcher.calculate_metric(:review, @fetcher.median(:review,
MergeRequest.arel_table[:created_at], MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at]) MergeRequest::Metrics.arel_table[:merged_at])
end end
end end
end end
......
module Gitlab
module CycleAnalytics
module Stage
def self.[](stage_name)
const_get("::Gitlab::CycleAnalytics::#{stage_name.to_s.camelize}Stage")
end
end
end
end
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class StagingStage < BaseStage class StagingStage < BaseStage
def initialize(*args) def description
super(*args) "From merge request merge until deploy to production"
@description = "From merge request merge until deploy to production"
end end
def median def median
@fetcher.calculate_metric(:staging, @fetcher.median(:staging,
MergeRequest::Metrics.arel_table[:merged_at], MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end end
end end
end end
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class TestStage < BaseStage class TestStage < BaseStage
def initialize(*args) def description
super(*args) "Total test time for all commits/merges"
@description = "Total test time for all commits/merges"
end end
def median def median
@fetcher.calculate_metric(:test, @fetcher.median(:test,
MergeRequest::Metrics.arel_table[:latest_build_started_at], MergeRequest::Metrics.arel_table[:latest_build_started_at],
MergeRequest::Metrics.arel_table[:latest_build_finished_at]) MergeRequest::Metrics.arel_table[:latest_build_finished_at])
end end
end end
end end
......
...@@ -7,7 +7,7 @@ describe 'cycle analytics events' do ...@@ -7,7 +7,7 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
let(:events) do let(:events) do
CycleAnalytics.new(project, options: { from: from_date, current_user: user }).events_for(stage) CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
end end
before do before do
......
...@@ -4,7 +4,7 @@ shared_examples 'base stage' do ...@@ -4,7 +4,7 @@ shared_examples 'base stage' do
let(:stage) { described_class.new(project: double, options: {}, stage: stage_name) } let(:stage) { described_class.new(project: double, options: {}, stage: stage_name) }
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12)
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({})
end end
......
...@@ -10,7 +10,7 @@ describe AnalyticsStageSerializer do ...@@ -10,7 +10,7 @@ describe AnalyticsStageSerializer do
let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}, stage: :code) } let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}, stage: :code) }
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:calculate_metric).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::MetricsFetcher).to receive(:median).and_return(1.12)
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({}) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEvent).to receive(:event_result).and_return({})
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