Commit 3006fcb9 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch 'ag-add-per-period-deploy-frequency' into 'master'

Add deploy frequency to project-level VSA Summary

See merge request gitlab-org/gitlab!28772
parents 9ebb85f4 83f9eca0
...@@ -59,16 +59,10 @@ export default () => { ...@@ -59,16 +59,10 @@ export default () => {
service: this.createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath), service: this.createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath),
}; };
}, },
defaultNumberOfSummaryItems: 3,
computed: { computed: {
currentStage() { currentStage() {
return this.store.currentActiveStage(); return this.store.currentActiveStage();
}, },
summaryTableColumnClass() {
return this.state.summary.length === this.$options.defaultNumberOfSummaryItems
? 'col-sm-3'
: 'col-sm-4';
},
}, },
created() { created() {
// Conditional check placed here to prevent this method from being called on the // Conditional check placed here to prevent this method from being called on the
......
...@@ -105,10 +105,6 @@ ...@@ -105,10 +105,6 @@
} }
} }
.js-ca-dropdown {
top: $gl-padding-top;
}
.stage-panel-body { .stage-panel-body {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
......
...@@ -4,4 +4,12 @@ class AnalyticsSummaryEntity < Grape::Entity ...@@ -4,4 +4,12 @@ class AnalyticsSummaryEntity < Grape::Entity
expose :value, safe: true expose :value, safe: true
expose :title expose :title
expose :unit, if: { with_unit: true } expose :unit, if: { with_unit: true }
private
def value
return object.value if object.value.is_a? String
object.value&.nonzero? ? object.value.to_s : '-'
end
end end
...@@ -10,27 +10,25 @@ ...@@ -10,27 +10,25 @@
.card .card
.card-header .card-header
{{ __('Recent Project Activity') }} {{ __('Recent Project Activity') }}
.content-block .d-flex.justify-content-between
.container-fluid .flex-grow.text-center{ "v-for" => "item in state.summary" }
.row %h3.header {{ item.value }}
.col-12.column{ "v-for" => "item in state.summary", ":class" => "summaryTableColumnClass" } %p.text {{ item.title }}
%h3.header {{ item.value }} .flex-grow.align-self-center.text-center
%p.text {{ item.title }} .dropdown.inline.js-ca-dropdown
.col-12.column{ ":class" => "summaryTableColumnClass" } %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
.dropdown.inline.js-ca-dropdown %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %i.fa.fa-chevron-down
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %ul.dropdown-menu.dropdown-menu-right
%i.fa.fa-chevron-down %li
%ul.dropdown-menu.dropdown-menu-right %a{ "href" => "#", "data-value" => "7" }
%li {{ n__('Last %d day', 'Last %d days', 7) }}
%a{ "href" => "#", "data-value" => "7" } %li
{{ n__('Last %d day', 'Last %d days', 7) }} %a{ "href" => "#", "data-value" => "30" }
%li {{ n__('Last %d day', 'Last %d days', 30) }}
%a{ "href" => "#", "data-value" => "30" } %li
{{ n__('Last %d day', 'Last %d days', 30) }} %a{ "href" => "#", "data-value" => "90" }
%li {{ n__('Last %d day', 'Last %d days', 90) }}
%a{ "href" => "#", "data-value" => "90" }
{{ n__('Last %d day', 'Last %d days', 90) }}
.stage-panel-container .stage-panel-container
.card.stage-panel .card.stage-panel
.card-header.border-bottom-0 .card-header.border-bottom-0
......
---
title: Add deployment frequency to Project level Value Stream Analytics summary
merge_request: 28772
author:
type: added
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
"required": ["value", "title"], "required": ["value", "title"],
"properties": { "properties": {
"value": { "value": {
"type": "number" "type": "string"
}, },
"title": { "title": {
"type": "string" "type": "string"
......
...@@ -14,6 +14,7 @@ module Gitlab ...@@ -14,6 +14,7 @@ module Gitlab
summary = [issue_stats] summary = [issue_stats]
summary << commit_stats if user_has_sufficient_access? summary << commit_stats if user_has_sufficient_access?
summary << deploy_stats summary << deploy_stats
summary << deployment_frequency_stats
end end
private private
...@@ -26,16 +27,32 @@ module Gitlab ...@@ -26,16 +27,32 @@ module Gitlab
serialize(Summary::Commit.new(project: @project, from: @from, to: @to)) serialize(Summary::Commit.new(project: @project, from: @from, to: @to))
end end
def deployments_summary
@deployments_summary ||=
Summary::Deploy.new(project: @project, from: @from, to: @to)
end
def deploy_stats def deploy_stats
serialize(Summary::Deploy.new(project: @project, from: @from, to: @to)) serialize deployments_summary
end
def deployment_frequency_stats
serialize(
Summary::DeploymentFrequency.new(
deployments: deployments_summary.value,
from: @from,
to: @to),
with_unit: true
)
end end
def user_has_sufficient_access? def user_has_sufficient_access?
@project.team.member?(@current_user, Gitlab::Access::REPORTER) @project.team.member?(@current_user, Gitlab::Access::REPORTER)
end end
def serialize(summary_object) def serialize(summary_object, with_unit: false)
AnalyticsSummarySerializer.new.represent(summary_object) AnalyticsSummarySerializer.new.represent(
summary_object, with_unit: with_unit)
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module Summary
class DeploymentFrequency < Base
include SummaryHelper
def initialize(deployments:, from:, to: nil, project: nil)
@deployments = deployments
super(project: project, from: from, to: to)
end
def title
_('Deployment Frequency')
end
def value
@value ||=
frequency(@deployments, @from, @to || Time.now)
end
def unit
_('per day')
end
end
end
end
end
...@@ -4,11 +4,14 @@ module Gitlab ...@@ -4,11 +4,14 @@ module Gitlab
module CycleAnalytics module CycleAnalytics
module SummaryHelper module SummaryHelper
def frequency(count, from, to) def frequency(count, from, to)
(count / days(from, to)).round(1) return count if count.zero?
freq = (count / days(from, to)).round(1)
freq.zero? ? '0' : freq
end end
def days(from, to) def days(from, to)
[(to.end_of_day - from.beginning_of_day) / (24 * 60 * 60), 1].max [(to.end_of_day - from.beginning_of_day).fdiv(1.day), 1].max
end end
end end
end end
......
...@@ -30,6 +30,7 @@ describe 'Value Stream Analytics', :js do ...@@ -30,6 +30,7 @@ describe 'Value Stream Analytics', :js do
expect(new_issues_counter).to have_content('-') expect(new_issues_counter).to have_content('-')
expect(commits_counter).to have_content('-') expect(commits_counter).to have_content('-')
expect(deploys_counter).to have_content('-') expect(deploys_counter).to have_content('-')
expect(deployment_frequency_counter).to have_content('-')
end end
it 'shows active stage with empty message' do it 'shows active stage with empty message' do
...@@ -53,6 +54,7 @@ describe 'Value Stream Analytics', :js do ...@@ -53,6 +54,7 @@ describe 'Value Stream Analytics', :js do
expect(new_issues_counter).to have_content('1') expect(new_issues_counter).to have_content('1')
expect(commits_counter).to have_content('2') expect(commits_counter).to have_content('2')
expect(deploys_counter).to have_content('1') expect(deploys_counter).to have_content('1')
expect(deployment_frequency_counter).to have_content('0')
end end
it 'shows data on each stage', :sidekiq_might_not_need_inline do it 'shows data on each stage', :sidekiq_might_not_need_inline do
...@@ -134,7 +136,15 @@ describe 'Value Stream Analytics', :js do ...@@ -134,7 +136,15 @@ describe 'Value Stream Analytics', :js do
end end
def deploys_counter def deploys_counter
find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3") find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3", match: :first)
end
def deployment_frequency_counter_selector
"//p[contains(text(),'Deployment Frequency')]/preceding-sibling::h3"
end
def deployment_frequency_counter
find(:xpath, deployment_frequency_counter_selector)
end end
def expect_issue_to_be_present def expect_issue_to_be_present
......
...@@ -20,7 +20,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -20,7 +20,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "finds the number of issues created after it" do it "finds the number of issues created after it" do
expect(subject.first[:value]).to eq(2) expect(subject.first[:value]).to eq('2')
end end
context 'with subgroups' do context 'with subgroups' do
...@@ -29,7 +29,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -29,7 +29,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "finds issues from them" do it "finds issues from them" do
expect(subject.first[:value]).to eq(3) expect(subject.first[:value]).to eq('3')
end end
end end
...@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data } subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data }
it 'finds issues from those projects' do it 'finds issues from those projects' do
expect(subject.first[:value]).to eq(2) expect(subject.first[:value]).to eq('2')
end end
end end
...@@ -49,7 +49,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -49,7 +49,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data } subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data }
it 'finds issues from 5 days ago' do it 'finds issues from 5 days ago' do
expect(subject.first[:value]).to eq(2) expect(subject.first[:value]).to eq('2')
end end
end end
end end
...@@ -62,7 +62,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -62,7 +62,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "doesn't find issues from them" do it "doesn't find issues from them" do
expect(subject.first[:value]).to eq(2) expect(subject.first[:value]).to eq('2')
end end
end end
end end
...@@ -77,7 +77,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -77,7 +77,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "finds the number of deploys made created after it" do it "finds the number of deploys made created after it" do
expect(subject.second[:value]).to eq(2) expect(subject.second[:value]).to eq('2')
end end
context 'with subgroups' do context 'with subgroups' do
...@@ -88,7 +88,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -88,7 +88,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "finds deploys from them" do it "finds deploys from them" do
expect(subject.second[:value]).to eq(3) expect(subject.second[:value]).to eq('3')
end end
end end
...@@ -102,7 +102,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -102,7 +102,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data } subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data }
it 'shows deploys from those projects' do it 'shows deploys from those projects' do
expect(subject.second[:value]).to eq(2) expect(subject.second[:value]).to eq('2')
end end
end end
...@@ -110,7 +110,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -110,7 +110,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data } subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data }
it 'finds deployments from 5 days ago' do it 'finds deployments from 5 days ago' do
expect(subject.second[:value]).to eq(2) expect(subject.second[:value]).to eq('2')
end end
end end
end end
...@@ -123,7 +123,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -123,7 +123,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
end end
it "doesn't find deploys from them" do it "doesn't find deploys from them" do
expect(subject.second[:value]).to eq(0) expect(subject.second[:value]).to eq('-')
end end
end end
end end
...@@ -153,7 +153,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -153,7 +153,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
context 'when `to` is nil' do context 'when `to` is nil' do
it 'includes range until now' do it 'includes range until now' do
# 1 deployment over 7 days # 1 deployment over 7 days
expect(subject[:value]).to eq(0.1) expect(subject[:value]).to eq('0.1')
end end
end end
...@@ -169,7 +169,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do ...@@ -169,7 +169,7 @@ describe Gitlab::CycleAnalytics::GroupStageSummary do
it 'returns deployment frequency within `from` and `to` range' do it 'returns deployment frequency within `from` and `to` range' do
# 2 deployments over 20 days # 2 deployments over 20 days
expect(subject[:value]).to eq(0.1) expect(subject[:value]).to eq('0.1')
end end
end end
end end
......
...@@ -20,13 +20,13 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -20,13 +20,13 @@ describe Gitlab::CycleAnalytics::StageSummary do
Timecop.freeze(5.days.ago) { create(:issue, project: project) } Timecop.freeze(5.days.ago) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project) } Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
expect(subject).to eq(1) expect(subject).to eq('1')
end end
it "doesn't find issues from other projects" do it "doesn't find issues from other projects" do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
expect(subject).to eq(0) expect(subject).to eq('-')
end end
context 'when `to` parameter is given' do context 'when `to` parameter is given' do
...@@ -38,14 +38,14 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -38,14 +38,14 @@ describe Gitlab::CycleAnalytics::StageSummary do
it "doesn't find any record" do it "doesn't find any record" do
options[:to] = Time.now options[:to] = Time.now
expect(subject).to eq(0) expect(subject).to eq('-')
end end
it "finds records created between `from` and `to` range" do it "finds records created between `from` and `to` range" do
options[:from] = 10.days.ago options[:from] = 10.days.ago
options[:to] = 10.days.from_now options[:to] = 10.days.from_now
expect(subject).to eq(2) expect(subject).to eq('2')
end end
end end
end end
...@@ -57,19 +57,19 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -57,19 +57,19 @@ describe Gitlab::CycleAnalytics::StageSummary do
Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
expect(subject).to eq(1) expect(subject).to eq('1')
end end
it "doesn't find commits from other projects" do it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') } Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') }
expect(subject).to eq(0) expect(subject).to eq('-')
end end
it "finds a large (> 100) snumber of commits if present" do it "finds a large (> 100) number of commits if present" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
expect(subject).to eq(100) expect(subject).to eq('100')
end end
context 'when `to` parameter is given' do context 'when `to` parameter is given' do
...@@ -81,14 +81,14 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -81,14 +81,14 @@ describe Gitlab::CycleAnalytics::StageSummary do
it "doesn't find any record" do it "doesn't find any record" do
options[:to] = Time.now options[:to] = Time.now
expect(subject).to eq(0) expect(subject).to eq('-')
end end
it "finds records created between `from` and `to` range" do it "finds records created between `from` and `to` range" do
options[:from] = 10.days.ago options[:from] = 10.days.ago
options[:to] = 10.days.from_now options[:to] = 10.days.from_now
expect(subject).to eq(2) expect(subject).to eq('2')
end end
end end
...@@ -118,7 +118,7 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -118,7 +118,7 @@ describe Gitlab::CycleAnalytics::StageSummary do
Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
expect(subject).to eq(1) expect(subject).to eq('1')
end end
it "doesn't find commits from other projects" do it "doesn't find commits from other projects" do
...@@ -126,7 +126,7 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -126,7 +126,7 @@ describe Gitlab::CycleAnalytics::StageSummary do
create(:deployment, :success, project: create(:project, :repository)) create(:deployment, :success, project: create(:project, :repository))
end end
expect(subject).to eq(0) expect(subject).to eq('-')
end end
context 'when `to` parameter is given' do context 'when `to` parameter is given' do
...@@ -138,14 +138,76 @@ describe Gitlab::CycleAnalytics::StageSummary do ...@@ -138,14 +138,76 @@ describe Gitlab::CycleAnalytics::StageSummary do
it "doesn't find any record" do it "doesn't find any record" do
options[:to] = Time.now options[:to] = Time.now
expect(subject).to eq(0) expect(subject).to eq('-')
end end
it "finds records created between `from` and `to` range" do it "finds records created between `from` and `to` range" do
options[:from] = 10.days.ago options[:from] = 10.days.ago
options[:to] = 10.days.from_now options[:to] = 10.days.from_now
expect(subject).to eq(2) expect(subject).to eq('2')
end
end
end
describe '#deployment_frequency' do
subject { stage_summary.fourth[:value] }
it 'includes the unit: `per day`' do
expect(stage_summary.fourth[:unit]).to eq _('per day')
end
before do
Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
end
it 'returns 0.0 when there were deploys but the frequency was too low' do
options[:from] = 30.days.ago
# 1 deployment over 30 days
# frequency of 0.03, rounded off to 0.0
expect(subject).to eq('0')
end
it 'returns `-` when there were no deploys' do
options[:from] = 4.days.ago
# 0 deployment in the last 4 days
expect(subject).to eq('-')
end
context 'when `to` is nil' do
it 'includes range until now' do
options[:from] = 6.days.ago
options[:to] = nil
# 1 deployment over 7 days
expect(subject).to eq('0.1')
end
end
context 'when `to` is given' do
before do
Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
end
it 'finds records created between `from` and `to` range' do
options[:from] = 10.days.ago
options[:to] = 10.days.from_now
# 2 deployments over 20 days
expect(subject).to eq('0.1')
end
context 'when `from` and `to` are within a day' do
it 'returns the number of deployments made on that day' do
Timecop.freeze(Time.now) do
create(:deployment, :success, project: project)
options[:from] = options[:to] = Time.now
expect(subject).to eq('1')
end
end
end end
end end
end end
......
...@@ -38,7 +38,7 @@ describe CycleAnalytics::GroupLevel do ...@@ -38,7 +38,7 @@ describe CycleAnalytics::GroupLevel do
end end
it 'returns medians for each stage for a specific group' do it 'returns medians for each stage for a specific group' do
expect(subject.summary.map { |summary| summary[:value] }).to contain_exactly(0.1, 1, 1) expect(subject.summary.map { |summary| summary[:value] }).to contain_exactly('0.1', '1', '1')
end end
end end
end end
...@@ -34,7 +34,10 @@ describe AnalyticsSummarySerializer do ...@@ -34,7 +34,10 @@ describe AnalyticsSummarySerializer do
end end
context 'when representing with unit' do context 'when representing with unit' do
let(:resource) { { title: 'frequency', value: 1.12, unit: 'per day' } } let(:resource) do
Gitlab::CycleAnalytics::Summary::DeploymentFrequency
.new(deployments: 10, from: 1.day.ago)
end
subject { described_class.new.represent(resource, with_unit: true) } subject { described_class.new.represent(resource, with_unit: true) }
......
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