summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Hegyi <ahegyi@gitlab.com>2019-08-29 14:02:32 +0200
committerAdam Hegyi <ahegyi@gitlab.com>2019-09-02 12:01:21 +0200
commitcd3d0b28243f4707ae6f5c2d637e90d1953e5053 (patch)
treeac29d19d7d1118cba5b59af5b3e1ac66d8d15ce9
parent12b749be08857afb9134d054b01347ad26146d23 (diff)
downloadgitlab-ce-new-cycle-analytics-services-and-endpoints.tar.gz
Cycle Analytics backend services and endpointsnew-cycle-analytics-services-and-endpoints
- Service for finding stages - Adding Adapter class to make new stages behave fit into the old API
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb8
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb55
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb14
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb1
-rw-r--r--app/models/cycle_analytics/project_level.rb20
-rw-r--r--app/serializers/analytics/cycle_analytics/stage_decorator.rb64
-rw-r--r--app/services/analytics/cycle_analytics/stage_find_service.rb28
-rw-r--r--changelogs/unreleased/new-cycle-analytics-services-and-endpoints.yml5
-rw-r--r--config/routes/project.rb14
-rw-r--r--lib/gitlab/analytics/cycle_analytics/base_query_builder.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/default_stages.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb7
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events.rb6
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb33
-rw-r--r--lib/gitlab/cycle_analytics/legacy_stage_adapter.rb34
-rw-r--r--spec/controllers/projects/cycle_analytics/events_controller_spec.rb4
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb35
-rw-r--r--spec/features/cycle_analytics_spec.rb1
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb12
-rw-r--r--spec/models/cycle_analytics/project_level_spec.rb5
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb33
-rw-r--r--spec/serializers/analytics/cycle_analytics/stage_decorator_spec.rb42
-rw-r--r--spec/services/analytics/cycle_analytics/stage_find_service_spec.rb20
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb9
24 files changed, 340 insertions, 116 deletions
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index c1ef848e1e7..eca4405037f 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -4,7 +4,7 @@ module CycleAnalyticsParams
extend ActiveSupport::Concern
def options(params)
- @options ||= { from: start_date(params), current_user: current_user }
+ @options ||= { from: start_date(cycle_analytics_params), current_user: current_user }
end
def start_date(params)
@@ -17,4 +17,10 @@ module CycleAnalyticsParams
90.days.ago
end
end
+
+ def cycle_analytics_params
+ return {} unless params[:cycle_analytics].present?
+
+ params[:cycle_analytics].permit(:start_date)
+ end
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index 926592b9681..ffcc523bb9c 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -6,57 +6,24 @@ module Projects
include CycleAnalyticsParams
before_action :authorize_read_cycle_analytics!
- before_action :authorize_read_build!, only: [:test, :staging]
- before_action :authorize_read_issue!, only: [:issue, :production]
- before_action :authorize_read_merge_request!, only: [:code, :review]
+ before_action :authorize_read_issue!, if: -> { stage.subject_model.eql?(Issue) }
+ before_action :authorize_read_merge_request!, if: -> { stage.subject_model.eql?(MergeRequest) }
- def issue
- render_events(cycle_analytics[:issue].events)
- end
-
- def plan
- render_events(cycle_analytics[:plan].events)
- end
-
- def code
- render_events(cycle_analytics[:code].events)
- end
-
- def test
- options(cycle_analytics_params)[:branch] = cycle_analytics_params[:branch_name]
-
- render_events(cycle_analytics[:test].events)
- end
-
- def review
- render_events(cycle_analytics[:review].events)
- end
-
- def staging
- render_events(cycle_analytics[:staging].events)
- end
-
- def production
- render_events(cycle_analytics[:production].events)
+ def show
+ render json: { events: data_collector.records_fetcher.serialized_records }
end
private
- def render_events(events)
- respond_to do |format|
- format.html
- format.json { render json: { events: events } }
- end
- end
-
- def cycle_analytics
- @cycle_analytics ||= ::CycleAnalytics::ProjectLevel.new(project, options: options(cycle_analytics_params))
+ def stage
+ @stage ||= Analytics::CycleAnalytics::StageFindService.new(parent: project, id: params[:stage_id]).execute
end
- def cycle_analytics_params
- return {} unless params[:cycle_analytics].present?
-
- params[:cycle_analytics].permit(:start_date, :branch_name)
+ def data_collector
+ @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
+ stage: stage,
+ params: options(params)
+ )
end
end
end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 3b0abecf2c9..e904bcd60c1 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -5,16 +5,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
- before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_read_cycle_analytics!
def show
@cycle_analytics = ::CycleAnalytics::ProjectLevel.new(@project, options: options(cycle_analytics_params))
- @cycle_analytics_no_data = @cycle_analytics.no_stats?
-
respond_to do |format|
format.html do
+ @cycle_analytics_no_data = @cycle_analytics.no_stats?
Gitlab::UsageDataCounters::CycleAnalyticsCounter.count(:views)
render :show
@@ -27,12 +25,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
- def cycle_analytics_params
- return {} unless params[:cycle_analytics].present?
-
- params[:cycle_analytics].permit(:start_date)
- end
-
def cycle_analytics_json
{
summary: @cycle_analytics.summary,
@@ -40,8 +32,4 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
permissions: @cycle_analytics.permissions(user: current_user)
}
end
-
- def whitelist_query_limiting
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42671')
- end
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 8ec9e1888d1..73e7d43f668 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -52,6 +52,7 @@ module Analytics
def matches_with_stage_params?(stage_params)
default_stage? &&
+ name.eql?(stage_params[:name]) &&
start_event_identifier.to_s.eql?(stage_params[:start_event_identifier].to_s) &&
end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
end
diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb
index 4aa426c58a1..cf11fe53cdd 100644
--- a/app/models/cycle_analytics/project_level.rb
+++ b/app/models/cycle_analytics/project_level.rb
@@ -10,6 +10,16 @@ module CycleAnalytics
@options = options.merge(project: project)
end
+ def all_medians_by_stage
+ stages.each_with_object({}) do |stage, medians_per_stage|
+ medians_per_stage[stage.name.to_sym] = stage.project_median
+ end
+ end
+
+ def stats
+ stages.map(&:as_json)
+ end
+
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(project,
from: options[:from],
@@ -19,5 +29,15 @@ module CycleAnalytics
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
end
+
+ def [](identifier)
+ stages.find { |s| s.name.to_s.eql?(identifier.to_s) }
+ end
+
+ def stages
+ @stages ||= Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |params|
+ Gitlab::CycleAnalytics::LegacyStageAdapter.new(project.cycle_analytics_stages.build(params), options)
+ end
+ end
end
end
diff --git a/app/serializers/analytics/cycle_analytics/stage_decorator.rb b/app/serializers/analytics/cycle_analytics/stage_decorator.rb
new file mode 100644
index 00000000000..f23d8a346b0
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/stage_decorator.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageDecorator < SimpleDelegator
+ DEFAULT_STAGE_ATTRIBUTES = {
+ issue: {
+ title: -> { s_('CycleAnalyticsStage|Issue') },
+ description: -> { _("Time before an issue gets scheduled") }
+ },
+ plan: {
+ title: -> { s_('CycleAnalyticsStage|Plan') },
+ description: -> { _("Time before an issue starts implementation") }
+ },
+ code: {
+ title: -> { s_('CycleAnalyticsStage|Code') },
+ description: -> { _("Time until first merge request") }
+ },
+ test: {
+ title: -> { s_('CycleAnalyticsStage|Test') },
+ description: -> { _("Total test time for all commits/merges") }
+ },
+ review: {
+ title: -> { s_('CycleAnalyticsStage|Review') },
+ description: -> { _("Time between merge request creation and merge/close") }
+ },
+ staging: {
+ title: -> { s_('CycleAnalyticsStage|Staging') },
+ description: -> { _("From merge request merge until deploy to production") }
+ },
+ production: {
+ title: -> { s_('CycleAnalyticsStage|Production') },
+ description: -> { _("From issue creation until deploy to production") }
+ }
+ }.freeze
+
+ def title
+ extract_default_stage_attribute(:title) || name
+ end
+
+ def description
+ extract_default_stage_attribute(:description) || ''
+ end
+
+ def legend
+ if matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage)
+ _("Related Jobs")
+ elsif matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_staging_stage)
+ _("Related Deployed Jobs")
+ elsif subject_model.eql?(Issue)
+ _("Related Issues")
+ elsif subject_model.eql?(MergeRequest)
+ _("Related Merged Requests")
+ end
+ end
+
+ private
+
+ def extract_default_stage_attribute(attribute)
+ DEFAULT_STAGE_ATTRIBUTES.dig(name.to_sym, attribute.to_sym)&.call
+ end
+ end
+ end
+end
diff --git a/app/services/analytics/cycle_analytics/stage_find_service.rb b/app/services/analytics/cycle_analytics/stage_find_service.rb
new file mode 100644
index 00000000000..60bca3d9342
--- /dev/null
+++ b/app/services/analytics/cycle_analytics/stage_find_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageFindService
+ def initialize(parent:, id:)
+ @parent = parent
+ @id = id
+ end
+
+ def execute
+ find_in_memory_stage_by_name!
+ end
+
+ private
+
+ attr_reader :parent, :id
+
+ def find_in_memory_stage_by_name!
+ raw_stage = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.find do |hash|
+ hash[:name].eql?(id.to_s)
+ end || raise(ActiveRecord::RecordNotFound)
+
+ parent.cycle_analytics_stages.build(raw_stage)
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/new-cycle-analytics-services-and-endpoints.yml b/changelogs/unreleased/new-cycle-analytics-services-and-endpoints.yml
new file mode 100644
index 00000000000..005114b7323
--- /dev/null
+++ b/changelogs/unreleased/new-cycle-analytics-services-and-endpoints.yml
@@ -0,0 +1,5 @@
+---
+title: Cycle Analytics query backend is reworked to be more performant and support customization.
+merge_request: 31850
+author:
+type: changed
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 29e462f904d..73ecdf335aa 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -433,18 +433,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resource :cycle_analytics, only: [:show]
-
- namespace :cycle_analytics do
- scope :events, controller: 'events' do
- get :issue
- get :plan
- get :code
- get :test
- get :review
- get :staging
- get :production
- end
+ resource :cycle_analytics, only: [:show] do
+ resources :events, controller: 'cycle_analytics/events', only: [:show], param: :stage_id
end
namespace :serverless do
diff --git a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
index 0afe2dbd021..279c3deedf5 100644
--- a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
+++ b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
@@ -30,7 +30,7 @@ module Gitlab
attr_reader :stage, :params
def exclude_negative_durations(query)
- query.where(duration.gt(zero_interval))
+ query.where(duration.gteq(zero_interval))
end
def filter_by_parent_model(query)
diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
index 286c393005f..08c2f4c441d 100644
--- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb
+++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
@@ -88,8 +88,8 @@ module Gitlab
name: 'production',
custom: false,
relative_position: 7,
- start_event_identifier: :merge_request_merged,
- end_event_identifier: :merge_request_first_deployed_to_production
+ start_event_identifier: :issue_created,
+ end_event_identifier: :production_stage_end
}
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
index 24ccaa0c922..2d4377099aa 100644
--- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
@@ -14,13 +14,13 @@ module Gitlab
Issue => {
finder_class: IssuesFinder,
serializer_class: AnalyticsIssueSerializer,
- includes_for_query: { project: [:namespace] },
+ includes_for_query: { project: [:namespace], author: [] },
columns_for_select: %I[title iid id created_at author_id project_id]
},
MergeRequest => {
finder_class: MergeRequestsFinder,
serializer_class: AnalyticsMergeRequestSerializer,
- includes_for_query: { target_project: [:namespace] },
+ includes_for_query: { target_project: [:namespace], author: [] },
columns_for_select: %I[title iid id created_at author_id state target_project_id]
}
}.freeze
@@ -43,7 +43,8 @@ module Gitlab
project = record.project
attributes = record.attributes.merge({
project_path: project.path,
- namespace_path: project.namespace.path
+ namespace_path: project.namespace.path,
+ author: record.author
})
serializer.represent(attributes)
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
index d21f344f483..58572446de6 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
@@ -18,7 +18,8 @@ module Gitlab
StageEvents::MergeRequestMerged => 104,
StageEvents::CodeStageStart => 1_000,
StageEvents::IssueStageEnd => 1_001,
- StageEvents::PlanStageStart => 1_002
+ StageEvents::PlanStageStart => 1_002,
+ StageEvents::ProductionStageEnd => 1_003
}.freeze
EVENTS = ENUM_MAPPING.keys.freeze
@@ -32,7 +33,8 @@ module Gitlab
StageEvents::MergeRequestCreated
],
StageEvents::IssueCreated => [
- StageEvents::IssueStageEnd
+ StageEvents::IssueStageEnd,
+ StageEvents::ProductionStageEnd
],
StageEvents::MergeRequestCreated => [
StageEvents::MergeRequestMerged
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
new file mode 100644
index 00000000000..2fd71216e11
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module StageEvents
+ class ProductionStageEnd < SimpleStageEvent
+ def self.name
+ PlanStageStart.name
+ end
+
+ def self.identifier
+ :production_stage_end
+ end
+
+ def object_type
+ Issue
+ end
+
+ def timestamp_projection
+ mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(merge_requests_closing_issues: { merge_request: [:metrics] })
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/legacy_stage_adapter.rb b/lib/gitlab/cycle_analytics/legacy_stage_adapter.rb
new file mode 100644
index 00000000000..046a4a25197
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/legacy_stage_adapter.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CycleAnalytics
+ # Translates Analytics::CycleAnalytics::ProjectStage to mimic the old interface (Gitlab::CycleAnalytics::BaseStage)
+ class LegacyStageAdapter < SimpleDelegator
+ def initialize(stage, options)
+ @stage = ::Analytics::CycleAnalytics::StageDecorator.new(stage)
+ @options = options
+ super(@stage)
+ end
+
+ def project_median
+ @project_median ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options).median.seconds
+ end
+
+ def events
+ @events ||= data_collector.records_fetcher.serialized_records
+ end
+
+ def as_json(serializer: AnalyticsStageSerializer)
+ serializer.new.represent(self)
+ end
+
+ private
+
+ attr_reader :stage, :options
+
+ def data_collector
+ @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
index b828c678d0c..a02bd39e040 100644
--- a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
@@ -58,7 +58,7 @@ describe Projects::CycleAnalytics::EventsController do
end
def get_issue(additional_params: {})
- params = additional_params.merge(namespace_id: project.namespace, project_id: project)
- get(:issue, params: params, format: :json)
+ params = additional_params.merge(namespace_id: project.namespace, project_id: project, stage_id: 'issue')
+ get(:show, params: params, format: :json)
end
end
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 65eee7b8ead..9c3c214b689 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -11,15 +11,32 @@ describe Projects::CycleAnalyticsController do
project.add_maintainer(user)
end
+ describe "GET 'show'" do
+ it 'provides the list of available stages' do
+ get(:show,
+ format: :json,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project
+ })
+
+ expect(response).to be_successful
+
+ stage_names = json_response["stats"].map { |r| r["title"] }
+ expect(stage_names.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
+ expect(stage_names.first).to eq('Issue')
+ end
+ end
+
context "counting page views for 'show'" do
it 'increases the counter' do
expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views)
get(:show,
params: {
- namespace_id: project.namespace,
- project_id: project
- })
+ namespace_id: project.namespace,
+ project_id: project
+ })
expect(response).to be_successful
end
@@ -30,9 +47,9 @@ describe Projects::CycleAnalyticsController do
it 'is true' do
get(:show,
params: {
- namespace_id: project.namespace,
- project_id: project
- })
+ namespace_id: project.namespace,
+ project_id: project
+ })
expect(response).to be_successful
expect(assigns(:cycle_analytics_no_data)).to eq(true)
@@ -51,9 +68,9 @@ describe Projects::CycleAnalyticsController do
it 'is false' do
get(:show,
params: {
- namespace_id: project.namespace,
- project_id: project
- })
+ namespace_id: project.namespace,
+ project_id: project
+ })
expect(response).to be_successful
expect(assigns(:cycle_analytics_no_data)).to eq(false)
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 07f0864fb3b..9dfdd4ba59b 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -45,6 +45,7 @@ describe 'Cycle Analytics', :js do
@build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
+ pipeline.succeed!
sign_in(user)
visit project_cycle_analytics_path(project)
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index a163de07967..630cf6426dc 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -98,6 +98,8 @@ describe 'cycle analytics events' do
before do
create_commit_referencing_issue(context)
+ # Issue mention should happen before MR creation
+ context.metrics.update(first_mentioned_in_commit_at: merge_request.created_at - 1.minute)
end
it 'has the total time' do
@@ -349,9 +351,13 @@ describe 'cycle analytics events' do
end
def setup(context)
- milestone = create(:milestone, project: project)
- context.update(milestone: milestone)
- mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
+ Timecop.travel(2.days.ago) do
+ milestone = create(:milestone, project: project)
+ context.update(milestone: milestone)
+ end
+ mr = Timecop.travel(1.day.ago) do
+ create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
+ end
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
diff --git a/spec/models/cycle_analytics/project_level_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb
index 4de01b1c679..364ea743ebf 100644
--- a/spec/models/cycle_analytics/project_level_spec.rb
+++ b/spec/models/cycle_analytics/project_level_spec.rb
@@ -22,8 +22,9 @@ describe CycleAnalytics::ProjectLevel do
end
it 'returns every median for each stage for a specific project' do
- values = described_class::STAGES.each_with_object({}) do |stage_name, hsh|
- hsh[stage_name] = subject[stage_name].project_median.presence
+ stage_names = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map { |s| s[:name] }
+ values = stage_names.each_with_object({}) do |stage_name, hsh|
+ hsh[stage_name.to_sym] = subject[stage_name].project_median.presence
end
expect(subject.all_medians_by_stage).to eq(values)
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 25390f8a23e..b3f459d9f0f 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -21,7 +21,7 @@ describe 'cycle analytics events' do
end
it 'lists the issue events' do
- get project_cycle_analytics_issue_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'issue', format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
@@ -30,7 +30,7 @@ describe 'cycle analytics events' do
end
it 'lists the plan events' do
- get project_cycle_analytics_plan_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'plan', format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
@@ -39,7 +39,7 @@ describe 'cycle analytics events' do
end
it 'lists the code events' do
- get project_cycle_analytics_code_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'code', format: :json)
expect(json_response['events']).not_to be_empty
@@ -49,14 +49,14 @@ describe 'cycle analytics events' do
end
it 'lists the test events' do
- get project_cycle_analytics_test_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'test', format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the review events' do
- get project_cycle_analytics_review_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'review', format: :json)
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
@@ -65,14 +65,14 @@ describe 'cycle analytics events' do
end
it 'lists the staging events' do
- get project_cycle_analytics_staging_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'staging', format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the production events' do
- get project_cycle_analytics_production_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'production', format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
@@ -80,36 +80,25 @@ describe 'cycle analytics events' do
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
- context 'specific branch' do
- it 'lists the test events' do
- branch = project.merge_requests.first.source_branch
-
- get project_cycle_analytics_test_path(project, format: :json, branch: branch)
-
- expect(json_response['events']).not_to be_empty
- expect(json_response['events'].first['date']).not_to be_empty
- end
- end
-
context 'with private project and builds' do
before do
project.members.last.update(access_level: Gitlab::Access::GUEST)
end
it 'does not list the test events' do
- get project_cycle_analytics_test_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'test', format: :json)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'does not list the staging events' do
- get project_cycle_analytics_staging_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'staging', format: :json)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'lists the issue events' do
- get project_cycle_analytics_issue_path(project, format: :json)
+ get project_cycle_analytics_event_path(project, stage_id: 'issue', format: :json)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -127,6 +116,8 @@ describe 'cycle analytics events' do
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ pipeline.succeed!
+
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
diff --git a/spec/serializers/analytics/cycle_analytics/stage_decorator_spec.rb b/spec/serializers/analytics/cycle_analytics/stage_decorator_spec.rb
new file mode 100644
index 00000000000..befd6f51e4e
--- /dev/null
+++ b/spec/serializers/analytics/cycle_analytics/stage_decorator_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Analytics::CycleAnalytics::StageDecorator do
+ let(:params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_issue_stage }
+
+ it 'decorates default stage attributes with localized text' do
+ issue_stage = Analytics::CycleAnalytics::ProjectStage.new(params)
+
+ decorator = described_class.new(issue_stage)
+
+ expect(decorator.title).to eq(described_class::DEFAULT_STAGE_ATTRIBUTES[:issue][:title].call)
+ expect(decorator.description).to eq(described_class::DEFAULT_STAGE_ATTRIBUTES[:issue][:description].call)
+ end
+
+ describe 'custom stage' do
+ let(:custom_stage) { Analytics::CycleAnalytics::ProjectStage.new(params) }
+ let(:decorator) { described_class.new(custom_stage) }
+
+ before do
+ params[:name] = 'My Stage'
+ end
+
+ it 'uses name attribute for the title' do
+ expect(decorator.title).to eq(params[:name])
+ end
+
+ it 'uses empty string for description' do
+ expect(decorator.description).to eq('')
+ end
+ end
+
+ it 'infers legend from #subject_model' do
+ issue_stage = Analytics::CycleAnalytics::ProjectStage.new(params)
+
+ expect(issue_stage.subject_model).to eq(Issue)
+
+ decorator = described_class.new(issue_stage)
+ expect(decorator.legend).to eq(_("Related Issues"))
+ end
+end
diff --git a/spec/services/analytics/cycle_analytics/stage_find_service_spec.rb b/spec/services/analytics/cycle_analytics/stage_find_service_spec.rb
new file mode 100644
index 00000000000..ba62545cfef
--- /dev/null
+++ b/spec/services/analytics/cycle_analytics/stage_find_service_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Analytics::CycleAnalytics::StageFindService do
+ it 'finds in-memory default stage' do
+ found_stage = described_class.new(parent: build(:project), id: 'code').execute # code (default) stage name
+
+ code_stage_params = Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_code_stage
+ expect(found_stage.name).to eq(code_stage_params[:name])
+ expect(found_stage.start_event_identifier.to_sym).to eq(code_stage_params[:start_event_identifier])
+ expect(found_stage.end_event_identifier.to_sym).to eq(code_stage_params[:end_event_identifier])
+ end
+
+ it 'raises ActiveRecord::RecordNotFound when in-memory default stage cannot be found' do
+ expect do
+ described_class.new(parent: build(:project), id: 'UnknownDefaultStage').execute
+ end.to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 575b2e779c5..4774ecd0588 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -33,7 +33,14 @@ module CycleAnalyticsHelpers
end
def create_cycle(user, project, issue, mr, milestone, pipeline)
- issue.update(milestone: milestone)
+ if Timecop.frozen?
+ issue.update(milestone: milestone)
+ else
+ Timecop.travel(mr.commits.last.committed_date - 1.minute) do
+ issue.update(milestone: milestone)
+ end
+ end
+
pipeline.run
ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)