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:
commit
b76d4fadd4
30 changed files with 134 additions and 95 deletions
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
27
app/models/cycle_analytics/base.rb
Normal file
27
app/models/cycle_analytics/base.rb
Normal 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
|
22
app/models/cycle_analytics/project_level.rb
Normal file
22
app/models/cycle_analytics/project_level.rb
Normal 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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Modify cycle analytics on project level
|
||||
merge_request: 30356
|
||||
author:
|
||||
type: changed
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,7 +14,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def branch
|
||||
@branch ||= @options[:branch] # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
@branch ||= options[:branch]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue