Merge branch 'prepare-cycle-analytics-for-group-level' into 'master'

Prepare cycle analytics for group level

See merge request gitlab-org/gitlab-ce!30356
This commit is contained in:
Douglas Barbosa Alexandre 2019-07-10 14:46:00 +00:00
commit b76d4fadd4
30 changed files with 134 additions and 95 deletions

View File

@ -50,7 +50,7 @@ module Projects
end
def cycle_analytics
@cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
@cycle_analytics ||= ::CycleAnalytics::ProjectLevel.new(project, options: options(events_params))
end
def events_params

View File

@ -9,7 +9,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics!
def show
@cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
@cycle_analytics = ::CycleAnalytics::ProjectLevel.new(@project, options: options(cycle_analytics_params))
@cycle_analytics_no_data = @cycle_analytics.no_stats?

View File

@ -1,46 +0,0 @@
# frozen_string_literal: true
class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, options)
@project = project
@options = options
end
def all_medians_per_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median
end
end
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from],
current_user: @options[:current_user]).data
end
def stats
@stats ||= stats_per_stage
end
def no_stats?
stats.all? { |hash| hash[:value].nil? }
end
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
private
def stats_per_stage
STAGES.map do |stage_name|
self[stage_name].as_json
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module CycleAnalytics
class Base
STAGES = %i[issue plan code test review staging production].freeze
def all_medians_by_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median
end
end
def stats
@stats ||= STAGES.map do |stage_name|
self[stage_name].as_json
end
end
def no_stats?
stats.all? { |hash| hash[:value].nil? }
end
def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module CycleAnalytics
class ProjectLevel < Base
attr_reader :project, :options
def initialize(project, options:)
@project = project
@options = options
end
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(project,
from: options[:from],
current_user: options[:current_user]).data
end
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Modify cycle analytics on project level
merge_request: 30356
author:
type: changed

View File

@ -5,11 +5,11 @@ module Gitlab
class BaseEventFetcher
include BaseQuery
attr_reader :projections, :query, :stage, :order
attr_reader :projections, :query, :stage, :order, :project, :options
MAX_EVENTS = 50
def initialize(project:, stage:, options:)
def initialize(project: nil, stage:, options:)
@project = project
@stage = stage
@options = options
@ -40,13 +40,13 @@ module Gitlab
end
def events_query
diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
diff_fn = subtract_datetimes_diff(base_query, options[:start_time_attrs], options[:end_time_attrs])
base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc).take(MAX_EVENTS)
end
def default_order
[@options[:start_time_attrs]].flatten.first
[options[:start_time_attrs]].flatten.first
end
def serialize(_event)
@ -59,13 +59,21 @@ module Gitlab
def allowed_ids
@allowed_ids ||= allowed_ids_finder_class
.new(@options[:current_user], project_id: @project.id)
.new(options[:current_user], allowed_ids_source)
.execute.where(id: event_result_ids).pluck(:id)
end
def event_result_ids
event_result.map { |event| event['id'] }
end
def allowed_ids_source
{ project_id: project.id }
end
def projects
[project]
end
end
end
end

View File

@ -10,7 +10,7 @@ module Gitlab
private
def base_query
@base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
@base_query ||= stage_query(projects.map(&:id))
end
def stage_query(project_ids)
@ -18,7 +18,7 @@ module Gitlab
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.where(issue_table[:created_at].gteq(options[:from]))
# Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin)

View File

@ -5,7 +5,9 @@ module Gitlab
class BaseStage
include BaseQuery
def initialize(project:, options:)
attr_reader :project, :options
def initialize(project: nil, options:)
@project = project
@options = options
end
@ -14,8 +16,8 @@ module Gitlab
event_fetcher.fetch
end
def as_json
AnalyticsStageSerializer.new.represent(self)
def as_json(serializer: AnalyticsStageSerializer)
serializer.new.represent(self)
end
def title
@ -23,21 +25,14 @@ module Gitlab
end
def median
BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
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).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage.
interval_query = Arel::Nodes::As.new(cte_table,
subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
return if project.nil?
BatchLoader.for(project.id).batch(key: name) do |project_ids, loader|
if project_ids.one?
loader.call(@project.id, median_datetime(cte_table, interval_query, name))
loader.call(project.id, median_query(project_ids))
else
begin
median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
median_datetimes(cte_table, interval_query(project_ids), name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
rescue NotSupportedError
@ -47,20 +42,42 @@ module Gitlab
end
end
def median_query(project_ids)
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage.
median_datetime(cte_table, interval_query(project_ids), name)
end
def name
raise NotImplementedError.new("Expected #{self.name} to implement name")
end
def cte_table
Arel::Table.new("cte_table_for_#{name}")
end
def interval_query(project_ids)
Arel::Nodes::As.new(cte_table,
subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
end
private
def event_fetcher
@event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
@event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: project,
stage: name,
options: event_options)
end
def event_options
@options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
end
def projects
[project]
end
end
end

View File

@ -20,7 +20,7 @@ module Gitlab
private
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
AnalyticsMergeRequestSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class

View File

@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
AnalyticsIssueSerializer.new(project: @project).represent(event)
AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class

View File

@ -7,7 +7,7 @@ module Gitlab
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
query

View File

@ -23,7 +23,7 @@ module Gitlab
end
def get
::CycleAnalytics::STAGES.each do |stage|
::CycleAnalytics::Base::STAGES.each do |stage|
@stage_permission_hash[stage] = authorized_stage?(stage)
end

View File

@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
AnalyticsIssueSerializer.new(project: @project).represent(event)
AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class

View File

@ -7,7 +7,7 @@ module Gitlab
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))

View File

@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
AnalyticsIssueSerializer.new(project: @project).represent(event)
AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class

View File

@ -6,7 +6,7 @@ module Gitlab
def stage_query(project_ids)
super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.gteq(options[:from]))
end
end
end

View File

@ -19,7 +19,7 @@ module Gitlab
private
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
AnalyticsMergeRequestSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class

View File

@ -14,7 +14,7 @@ module Gitlab
private
def branch
@branch ||= @options[:branch] # rubocop:disable Gitlab/ModuleWithInstanceVariables
@branch ||= options[:branch]
end
end
end

View File

@ -32,7 +32,7 @@ module Gitlab
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
::CycleAnalytics::ProjectLevel.new(project, options: options).all_medians_by_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end

View File

@ -7,7 +7,7 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
let(:events) do
CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user })[stage].events
end
before do

View File

@ -34,7 +34,7 @@ describe Gitlab::CycleAnalytics::UsageData do
expect(result).to have_key(:avg_cycle_analytics)
CycleAnalytics::STAGES.each do |stage|
CycleAnalytics::Base::STAGES.each do |stage|
expect(result[:avg_cycle_analytics]).to have_key(stage)
stage_values = result[:avg_cycle_analytics][stage]

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#code' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
context 'with deployment' do
generate_cycle_analytics_spec(

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#issue' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :issue,

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#plan' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :plan,

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#production' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :production,

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe CycleAnalytics do
describe CycleAnalytics::ProjectLevel do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
@ -11,9 +11,9 @@ describe CycleAnalytics do
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
subject { described_class.new(project, from: from_date) }
subject { described_class.new(project, options: { from: from_date }) }
describe '#all_medians_per_stage' do
describe '#all_medians_by_stage' do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
@ -26,7 +26,7 @@ describe CycleAnalytics do
hsh[stage_name] = subject[stage_name].median.presence
end
expect(subject.all_medians_per_stage).to eq(values)
expect(subject.all_medians_by_stage).to eq(values)
end
end
end

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#review' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :review,

View File

@ -9,7 +9,7 @@ describe 'CycleAnalytics#staging' do
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :staging,

View File

@ -8,7 +8,8 @@ describe 'CycleAnalytics#test' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :test,