diff options
author | Adam Hegyi <ahegyi@gitlab.com> | 2019-08-29 14:02:32 +0200 |
---|---|---|
committer | Adam Hegyi <ahegyi@gitlab.com> | 2019-09-02 12:01:21 +0200 |
commit | cd3d0b28243f4707ae6f5c2d637e90d1953e5053 (patch) | |
tree | ac29d19d7d1118cba5b59af5b3e1ac66d8d15ce9 | |
parent | 12b749be08857afb9134d054b01347ad26146d23 (diff) | |
download | gitlab-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
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) |