Commit be593755 authored by James Lopez's avatar James Lopez

Merge branch 'ee-30138-display-cycle-analytics-issue-logic-fixes' into 'master'

EE backport for Resolve "Display and logic improvements for cycle analytics"

See merge request gitlab-org/gitlab-ee!14143
parents 07e7fe04 5594ba6b
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
export default {
components: {
userAvatarImage,
totalTime,
limitWarning,
},
props: {
items: {
type: Array,
default: () => [],
},
stage: {
type: Object,
default: () => ({}),
},
},
computed: {
iconCommit() {
return iconCommit;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="(commit, i) in items" :key="i" class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl" />
<h5 class="item-title commit-title">
<a :href="commit.commitUrl"> {{ commit.title }} </a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }} <span class="commit-icon" v-html="iconCommit"> </span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{
commit.shortSha
}}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time"><total-time :time="commit.totalTime" /></div>
</li>
</ul>
</div>
</template>
......@@ -5,7 +5,6 @@ import Flash from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
......@@ -26,7 +25,7 @@ export default () => {
components: {
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-plan-component': stageComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
......
---
title: Change logic behind cycle analytics
merge_request: 29018
author:
type: changed
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module BuildsEventHelper
def initialize(*args)
@projections = [build_table[:id]]
@order = build_table[:created_at]
super(*args)
end
def fetch
Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
super
end
def events_query
base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
super
end
private
def allowed_ids
nil
end
def serialize(event)
AnalyticsBuildSerializer.new.represent(event['build'])
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class CodeEventFetcher < BaseEventFetcher
include CodeHelper
def initialize(*args)
@projections = [mr_table[:title],
mr_table[:iid],
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module CodeHelper
def stage_query(project_ids)
super(project_ids).where(mr_table[:created_at].gteq(issue_metrics_table[:first_mentioned_in_commit_at]))
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class CodeStage < BaseStage
include CodeHelper
def start_time_attrs
@start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
end
......
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class IssueEventFetcher < BaseEventFetcher
include IssueHelper
def initialize(*args)
@projections = [issue_table[:title],
issue_table[:iid],
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module IssueHelper
def stage_query(project_ids)
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_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
query
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class IssueStage < BaseStage
include IssueHelper
def start_time_attrs
@start_time_attrs ||= issue_table[:created_at]
end
......
......@@ -3,60 +3,26 @@
module Gitlab
module CycleAnalytics
class PlanEventFetcher < BaseEventFetcher
include PlanHelper
def initialize(*args)
@projections = [mr_diff_table[:id],
issue_metrics_table[:first_mentioned_in_commit_at]]
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
issue_table[:created_at],
issue_table[:author_id]]
super(*args)
end
def events_query
base_query
.join(mr_diff_table)
.on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
super
end
private
def allowed_ids
nil
end
def merge_request_diff_commits
@merge_request_diff_commits ||=
MergeRequestDiffCommit
.where(merge_request_diff_id: event_result.map { |event| event['id'] })
.group_by(&:merge_request_diff_id)
end
def serialize(event)
commit = first_time_reference_commit(event)
return unless commit
serialize_commit(event, commit, query)
end
def first_time_reference_commit(event)
return unless event && merge_request_diff_commits
commits = merge_request_diff_commits[event['id'].to_i]
return if commits.blank?
commits.find do |commit|
next unless commit[:committed_date] && event['first_mentioned_in_commit_at']
commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i
end
AnalyticsIssueSerializer.new(project: @project).represent(event)
end
def serialize_commit(event, commit, query)
commit = Commit.from_hash(commit.to_hash, @project)
AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
def allowed_ids_finder_class
IssuesFinder
end
end
end
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module PlanHelper
def stage_query(project_ids)
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_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))
query
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class PlanStage < BaseStage
include PlanHelper
def start_time_attrs
@start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
issue_metrics_table[:first_added_to_board_at]]
......@@ -21,7 +23,7 @@ module Gitlab
end
def legend
_("Related Commits")
_("Related Issues")
end
def description
......
......@@ -2,7 +2,28 @@
module Gitlab
module CycleAnalytics
class ProductionEventFetcher < IssueEventFetcher
class ProductionEventFetcher < BaseEventFetcher
include ProductionHelper
def initialize(*args)
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
issue_table[:created_at],
issue_table[:author_id]]
super(*args)
end
private
def serialize(event)
AnalyticsIssueSerializer.new(project: @project).represent(event)
end
def allowed_ids_finder_class
IssuesFinder
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class ReviewEventFetcher < BaseEventFetcher
include ReviewHelper
def initialize(*args)
@projections = [mr_table[:title],
mr_table[:iid],
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module ReviewHelper
def stage_query(project_ids)
super(project_ids).where(mr_metrics_table[:merged_at].not_eq(nil))
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class ReviewStage < BaseStage
include ReviewHelper
def start_time_attrs
@start_time_attrs ||= mr_table[:created_at]
end
......
......@@ -3,34 +3,8 @@
module Gitlab
module CycleAnalytics
class StagingEventFetcher < BaseEventFetcher
def initialize(*args)
@projections = [build_table[:id]]
@order = build_table[:created_at]
super(*args)
end
def fetch
Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
super
end
def events_query
base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
super
end
private
def allowed_ids
nil
end
def serialize(event)
AnalyticsBuildSerializer.new.represent(event['build'])
end
include ProductionHelper
include BuildsEventHelper
end
end
end
......@@ -4,6 +4,7 @@ module Gitlab
module CycleAnalytics
class StagingStage < BaseStage
include ProductionHelper
def start_time_attrs
@start_time_attrs ||= mr_metrics_table[:merged_at]
end
......
......@@ -2,7 +2,9 @@
module Gitlab
module CycleAnalytics
class TestEventFetcher < StagingEventFetcher
class TestEventFetcher < BaseEventFetcher
include TestHelper
include BuildsEventHelper
end
end
end
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module TestHelper
def stage_query(project_ids)
if branch
super(project_ids).where(build_table[:ref].eq(branch))
else
super(project_ids)
end
end
private
def branch
@branch ||= @options[:branch] # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
end
end
......@@ -3,6 +3,8 @@
module Gitlab
module CycleAnalytics
class TestStage < BaseStage
include TestHelper
def start_time_attrs
@start_time_attrs ||= mr_metrics_table[:latest_build_started_at]
end
......@@ -26,14 +28,6 @@ module Gitlab
def description
_("Total test time for all commits/merges")
end
def stage_query(project_ids)
if @options[:branch]
super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else
super(project_ids)
end
end
end
end
end
......@@ -5714,12 +5714,6 @@ msgstr ""
msgid "First day of the week"
msgstr ""
msgid "FirstPushedBy|First"
msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
msgid "Fixed date"
msgstr ""
......@@ -10939,9 +10933,6 @@ msgstr ""
msgid "Registry"
msgstr ""
msgid "Related Commits"
msgstr ""
msgid "Related Deployed Jobs"
msgstr ""
......
......@@ -58,7 +58,7 @@ describe 'Cycle Analytics', :js do
expect_issue_to_be_present
click_stage('Plan')
expect(find('.stage-events')).to have_content(mr.commits.last.title)
expect_issue_to_be_present
click_stage('Code')
expect_merge_request_to_be_present
......
......@@ -4,5 +4,41 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::CodeStage do
let(:stage_name) { :code }
let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, source_project: project, created_at: 15.minutes.ago) }
let!(:mr_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago)
issue_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
issue_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1)
create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2)
end
it_behaves_like 'base stage'
describe '#median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes merge requests that closes issues' do
result = stage.events
expect(result.count).to eq(2)
expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title)
end
end
end
......@@ -53,20 +53,28 @@ describe 'cycle analytics events' do
describe '#plan_events' do
let(:stage) { :plan }
it 'has a title' do
expect(events.first[:title]).not_to be_nil
before do
create_commit_referencing_issue(context)
end
it 'has a sha short ID' do
expect(events.first[:short_sha]).not_to be_nil
it 'has the total time' do
expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
expect(events.first[:title]).to eq(context.title)
end
it 'has the URL' do
expect(events.first[:commit_url]).not_to be_nil
expect(events.first[:url]).not_to be_nil
end
it 'has the total time' do
expect(events.first[:total_time]).not_to be_empty
it 'has an iid' do
expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
......@@ -78,12 +86,13 @@ describe 'cycle analytics events' do
end
it "has the author's name" do
expect(events.first[:author][:name]).not_to be_nil
expect(events.first[:author][:name]).to eq(context.author.name)
end
end
describe '#code_events' do
let(:stage) { :code }
let!(:merge_request) { MergeRequest.first }
before do
create_commit_referencing_issue(context)
......@@ -122,6 +131,7 @@ describe 'cycle analytics events' do
let(:stage) { :test }
let(:merge_request) { MergeRequest.first }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
let!(:pipeline) do
create(:ci_pipeline,
......@@ -137,6 +147,7 @@ describe 'cycle analytics events' do
pipeline.run!
pipeline.succeed!
merge_merge_requests_closing_issue(user, project, context)
end
it 'has the name' do
......@@ -180,6 +191,10 @@ describe 'cycle analytics events' do
let(:stage) { :review }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
merge_merge_requests_closing_issue(user, project, context)
end
it 'has the total time' do
expect(events.first[:total_time]).not_to be_empty
end
......
......@@ -3,6 +3,37 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::IssueStage do
let(:stage_name) { :issue }
let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago )
issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago)
issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago)
end
it_behaves_like 'base stage'
describe '#median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes issues with metrics' do
result = stage.events
expect(result.count).to eq(3)
expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title, issue_3.title)
end
end
end
......@@ -3,6 +3,37 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::PlanStage do
let(:stage_name) { :plan }
let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago)
issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago)
issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago)
end
it_behaves_like 'base stage'
describe '#median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes issues with metrics' do
result = stage.events
expect(result.count).to eq(2)
expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title)
end
end
end
......@@ -3,6 +3,43 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::ReviewStage do
let(:stage_name) { :review }
let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
let!(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
let!(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do
mr_1.metrics.update!(merged_at: 30.minutes.ago)
mr_2.metrics.update!(merged_at: 10.minutes.ago)
create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1)
create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2)
create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3)
end
it_behaves_like 'base stage'
describe '#median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes merge requests that close issues' do
result = stage.events
expect(result.count).to eq(2)
expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title)
end
end
end
require 'spec_helper'
shared_examples 'base stage' do
ISSUES_MEDIAN = 30.minutes.to_i
let(:stage) { described_class.new(project: double, options: {}) }
before do
......
......@@ -4,5 +4,46 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::StagingStage do
let(:stage_name) { :staging }
let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
let!(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
let(:build_1) { create(:ci_build, project: project) }
let(:build_2) { create(:ci_build, project: project) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do
mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id)
mr_2.metrics.update!(merged_at: 60.minutes.ago, first_deployed_to_production_at: 30.minutes.ago, pipeline_id: build_2.commit_id)
mr_3.metrics.update!(merged_at: 10.minutes.ago, first_deployed_to_production_at: 3.days.ago, pipeline_id: create(:ci_build, project: project).commit_id)
create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1)
create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2)
create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3)
end
it_behaves_like 'base stage'
describe '#median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
it 'exposes builds connected to merge request' do
result = stage.events
expect(result.count).to eq(2)
expect(result.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name)
end
end
end
......@@ -32,10 +32,10 @@ describe 'cycle analytics events' do
it 'lists the plan events' do
get project_cycle_analytics_plan_path(project, format: :json)
first_mr_short_sha = project.merge_requests.sort_by_attribute(:created_asc).first.commits.first.short_id
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['short_sha']).to eq(first_mr_short_sha)
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
it 'lists the code events' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment