Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/analytics/cycle_analytics/aggregated')
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb114
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb54
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb31
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/median.rb36
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb116
-rw-r--r--lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb41
6 files changed, 392 insertions, 0 deletions
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
new file mode 100644
index 00000000000..1e50c980a3a
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ # rubocop: disable CodeReuse/ActiveRecord
+ class BaseQueryBuilder
+ include StageQueryHelpers
+
+ MODEL_CLASSES = {
+ MergeRequest.to_s => ::Analytics::CycleAnalytics::MergeRequestStageEvent,
+ Issue.to_s => ::Analytics::CycleAnalytics::IssueStageEvent
+ }.freeze
+
+ # Allowed params:
+ # * from - stage end date filter start date
+ # * to - stage end date filter to date
+ # * author_username
+ # * milestone_title
+ # * label_name (array)
+ # * assignee_username (array)
+ # * project_ids (array)
+ def initialize(stage:, params: {})
+ @stage = stage
+ @params = params
+ @root_ancestor = stage.parent.root_ancestor
+ @stage_event_model = MODEL_CLASSES.fetch(stage.subject_class.to_s)
+ end
+
+ def build
+ query = base_query
+ query = filter_by_stage_parent(query)
+ query = filter_author(query)
+ query = filter_milestone_ids(query)
+ query = filter_label_names(query)
+ filter_assignees(query)
+ end
+
+ def filter_author(query)
+ return query if params[:author_username].blank?
+
+ user = User.by_username(params[:author_username]).first
+
+ return query.none if user.blank?
+
+ query.authored(user)
+ end
+
+ def filter_milestone_ids(query)
+ return query if params[:milestone_title].blank?
+
+ milestone = MilestonesFinder
+ .new(group_ids: root_ancestor.self_and_descendant_ids, project_ids: root_ancestor.all_projects.select(:id), title: params[:milestone_title])
+ .execute
+ .first
+
+ return query.none if milestone.blank?
+
+ query.with_milestone_id(milestone.id)
+ end
+
+ def filter_label_names(query)
+ return query if params[:label_name].blank?
+
+ LabelFilter.new(
+ stage: stage,
+ params: params,
+ project: nil,
+ group: root_ancestor
+ ).filter(query)
+ end
+
+ def filter_assignees(query)
+ return query if params[:assignee_username].blank?
+
+ Issuables::AssigneeFilter
+ .new(params: { assignee_username: params[:assignee_username] })
+ .filter(query)
+ end
+
+ def filter_by_stage_parent(query)
+ query.by_project_id(stage.parent_id)
+ end
+
+ def base_query
+ query = stage_event_model
+ .by_stage_event_hash_id(stage.stage_event_hash_id)
+
+ from = params[:from] || 30.days.ago
+ if in_progress?
+ query = query
+ .end_event_is_not_happened_yet
+ .opened_state
+ .start_event_timestamp_after(from)
+ query = query.start_event_timestamp_before(params[:to]) if params[:to]
+ else
+ query = query.end_event_timestamp_after(from)
+ query = query.end_event_timestamp_before(params[:to]) if params[:to]
+ end
+
+ query
+ end
+
+ private
+
+ attr_reader :stage, :params, :root_ancestor, :stage_event_model
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
+Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder.prepend_mod_with('Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder')
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
new file mode 100644
index 00000000000..c8b11ecb4a8
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/data_collector.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ # Arguments:
+ # stage - an instance of CycleAnalytics::ProjectStage or CycleAnalytics::GroupStage
+ # params:
+ # current_user: an instance of User
+ # from: DateTime
+ # to: DateTime
+ class DataCollector
+ include Gitlab::Utils::StrongMemoize
+
+ MAX_COUNT = 10001
+
+ delegate :serialized_records, to: :records_fetcher
+
+ def initialize(stage:, params: {})
+ @stage = stage
+ @params = params
+ end
+
+ def median
+ strong_memoize(:median) { Median.new(stage: stage, query: query, params: params) }
+ end
+
+ def count
+ strong_memoize(:count) { limit_count }
+ end
+
+ def records_fetcher
+ strong_memoize(:records_fetcher) do
+ RecordsFetcher.new(stage: stage, query: query, params: params)
+ end
+ end
+
+ private
+
+ attr_reader :stage, :params
+
+ def query
+ BaseQueryBuilder.new(stage: stage, params: params).build
+ end
+
+ def limit_count
+ query.limit(MAX_COUNT).count
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb
new file mode 100644
index 00000000000..6d87ae91a9c
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/label_filter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ # This class makes it possible to add label filters to stage event tables
+ class LabelFilter < Issuables::LabelFilter
+ extend ::Gitlab::Utils::Override
+
+ def initialize(stage:, project:, group:, **kwargs)
+ @stage = stage
+
+ super(project: project, group: group, **kwargs)
+ end
+
+ private
+
+ attr_reader :stage
+
+ override :label_link_query
+ def label_link_query(target_model, label_ids: nil)
+ join_column = target_model.arel_table[target_model.issuable_id_column]
+
+ LabelLink.by_target_for_exists_query(stage.subject_class.name, join_column, label_ids)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb
new file mode 100644
index 00000000000..181ee20948b
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/median.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ class Median
+ include StageQueryHelpers
+
+ def initialize(stage:, query:, params:)
+ @stage = stage
+ @query = query
+ @params = params
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def seconds
+ @query = @query.select(median_duration_in_seconds.as('median')).reorder(nil)
+ result = @query.take || {}
+
+ result['median'] || nil
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def days
+ seconds ? seconds.fdiv(1.day) : nil
+ end
+
+ private
+
+ attr_reader :stage, :query, :params
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
new file mode 100644
index 00000000000..7dce757cdc8
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ class RecordsFetcher
+ include Gitlab::Utils::StrongMemoize
+ include StageQueryHelpers
+
+ MAX_RECORDS = 20
+
+ MAPPINGS = {
+ Issue => {
+ serializer_class: AnalyticsIssueSerializer,
+ includes_for_query: { project: { namespace: [:route] }, author: [] },
+ columns_for_select: %I[title iid id created_at author_id project_id]
+ },
+ MergeRequest => {
+ serializer_class: AnalyticsMergeRequestSerializer,
+ includes_for_query: { target_project: [:namespace], author: [] },
+ columns_for_select: %I[title iid id created_at author_id state_id target_project_id]
+ }
+ }.freeze
+
+ def initialize(stage:, query:, params: {})
+ @stage = stage
+ @query = query
+ @params = params
+ @sort = params[:sort] || :end_event
+ @direction = params[:direction] || :desc
+ @page = params[:page] || 1
+ @per_page = MAX_RECORDS
+ @stage_event_model = query.model
+ end
+
+ def serialized_records
+ strong_memoize(:serialized_records) do
+ records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration.as('total_time'))
+
+ yield records if block_given?
+ issuables_and_records = load_issuables(records)
+
+ preload_associations(issuables_and_records.map(&:first))
+
+ issuables_and_records.map do |issuable, record|
+ project = issuable.project
+ attributes = issuable.attributes.merge({
+ project_path: project.path,
+ namespace_path: project.namespace.route.path,
+ author: issuable.author,
+ total_time: record.total_time
+ })
+ serializer.represent(attributes)
+ end
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def ordered_and_limited_query
+ sorting_options = {
+ end_event: {
+ asc: -> { query.order(end_event_timestamp: :asc) },
+ desc: -> { query.order(end_event_timestamp: :desc) }
+ },
+ duration: {
+ asc: -> { query.order(duration.asc) },
+ desc: -> { query.order(duration.desc) }
+ }
+ }
+
+ sort_lambda = sorting_options.dig(sort, direction) || sorting_options.dig(:end_event, :desc)
+
+ sort_lambda.call
+ .page(page)
+ .per(per_page)
+ .without_count
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ attr_reader :stage, :query, :sort, :direction, :params, :page, :per_page, :stage_event_model
+
+ delegate :subject_class, to: :stage
+
+ def load_issuables(stage_event_records)
+ stage_event_records_by_issuable_id = stage_event_records.index_by(&:issuable_id)
+
+ issuable_model = stage_event_model.issuable_model
+ issuables_by_id = issuable_model.id_in(stage_event_records_by_issuable_id.keys).index_by(&:id)
+
+ stage_event_records_by_issuable_id.map do |issuable_id, record|
+ [issuables_by_id[issuable_id], record] if issuables_by_id[issuable_id]
+ end.compact
+ end
+
+ def serializer
+ MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def preload_associations(records)
+ ActiveRecord::Associations::Preloader.new.preload(
+ records,
+ MAPPINGS.fetch(subject_class).fetch(:includes_for_query)
+ )
+
+ records
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb
new file mode 100644
index 00000000000..f23d1832df9
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/aggregated/stage_query_helpers.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module Aggregated
+ module StageQueryHelpers
+ def percentile_cont
+ percentile_cont_ordering = Arel::Nodes::UnaryOperation.new(Arel::Nodes::SqlLiteral.new('ORDER BY'), duration)
+ Arel::Nodes::NamedFunction.new(
+ 'percentile_cont(0.5) WITHIN GROUP',
+ [percentile_cont_ordering]
+ )
+ end
+
+ def duration
+ if in_progress?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new('TO_TIMESTAMP', [Time.current.to_i]),
+ query.model.arel_table[:start_event_timestamp]
+ )
+ else
+ Arel::Nodes::Subtraction.new(
+ query.model.arel_table[:end_event_timestamp],
+ query.model.arel_table[:start_event_timestamp]
+ )
+ end
+ end
+
+ def median_duration_in_seconds
+ Arel::Nodes::Extract.new(percentile_cont, :epoch)
+ end
+
+ def in_progress?
+ params[:end_event_filter] == :in_progress
+ end
+ end
+ end
+ end
+ end
+end