Commit c5307cca authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '33646-eliminate-cycle-analytics-total-stage' into 'master'

Remove Value Stream Total stage

Closes #33646

See merge request gitlab-org/gitlab!42345
parents 7f9acc4a d96236c6
...@@ -23,9 +23,6 @@ const EMPTY_STAGE_TEXTS = { ...@@ -23,9 +23,6 @@ const EMPTY_STAGE_TEXTS = {
staging: __( staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
), ),
production: __(
'The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
),
}; };
export default { export default {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module CycleAnalytics module CycleAnalytics
module LevelBase module LevelBase
STAGES = %i[issue plan code test review staging production].freeze STAGES = %i[issue plan code test review staging].freeze
def all_medians_by_stage def all_medians_by_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage| STAGES.each_with_object({}) do |stage_name, medians_per_stage|
......
---
title: Remove Value Stream Total stage
merge_request: 42345
author:
type: removed
# frozen_string_literal: true
class RemoveCycleAnalyticsTotalStageData < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
execute("DELETE FROM analytics_cycle_analytics_group_stages WHERE name='production'")
execute("DELETE FROM analytics_cycle_analytics_project_stages WHERE name='production'")
end
def down
# Migration is irreversible
end
end
dde7a29268d925044d59455db87bfc1aa617eec6e30df1cc9dc531b52c909fe1
\ No newline at end of file
...@@ -45,8 +45,6 @@ There are seven stages that are tracked as part of the Value Stream Analytics ca ...@@ -45,8 +45,6 @@ There are seven stages that are tracked as part of the Value Stream Analytics ca
- Time spent on code review - Time spent on code review
- **Staging** (Continuous Deployment) - **Staging** (Continuous Deployment)
- Time between merging and deploying to production - Time between merging and deploying to production
- **Total** (Total)
- Total lifecycle time. That is, the velocity of the project or team. [Previously known](https://gitlab.com/gitlab-org/gitlab/-/issues/38317) as **Production**.
## Filter the analytics data ## Filter the analytics data
...@@ -95,7 +93,7 @@ Note: A commit is associated with an issue by [crosslinking](../project/issues/c ...@@ -95,7 +93,7 @@ Note: A commit is associated with an issue by [crosslinking](../project/issues/c
## How the stages are measured ## How the stages are measured
Value Stream Analytics records stage time and data based on the project issues with the Value Stream Analytics records stage time and data based on the project issues with the
exception of the staging and total stages, where only data deployed to exception of the staging stage, where only data deployed to
production are measured. production are measured.
Specifically, if your CI is not set up and you have not defined a `production` Specifically, if your CI is not set up and you have not defined a `production`
...@@ -112,7 +110,6 @@ Each stage of Value Stream Analytics is further described in the table below. ...@@ -112,7 +110,6 @@ Each stage of Value Stream Analytics is further described in the table below.
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI/CD takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. | | Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI/CD takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. |
| Review | Measures the median time taken to review the merge request that has a closing issue pattern, between its creation and until it's merged. | | Review | Measures the median time taken to review the merge request that has a closing issue pattern, between its creation and until it's merged. |
| Staging | Measures the median time between merging the merge request with a closing issue pattern until the very first deployment to production. It's tracked by the environment set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI/CD configuration. If there isn't a production environment, this is not tracked. | | Staging | Measures the median time between merging the merge request with a closing issue pattern until the very first deployment to production. It's tracked by the environment set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI/CD configuration. If there isn't a production environment, this is not tracked. |
| Total | The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. [Previously known](https://gitlab.com/gitlab-org/gitlab/-/issues/38317) as **Production**. |
How this works, behind the scenes: How this works, behind the scenes:
...@@ -131,7 +128,7 @@ Value Stream Analytics dashboard will not present any data for: ...@@ -131,7 +128,7 @@ Value Stream Analytics dashboard will not present any data for:
- Merge requests that do not close an issue. - Merge requests that do not close an issue.
- Issues not labeled with a label present in the Issue Board or for issues not assigned a milestone. - Issues not labeled with a label present in the Issue Board or for issues not assigned a milestone.
- Staging and production stages, if the project has no `production` or `production/*` - Staging stage, if the project has no `production` or `production/*`
environment. environment.
## Example workflow ## Example workflow
...@@ -158,9 +155,6 @@ environments is configured. ...@@ -158,9 +155,6 @@ environments is configured.
request at 19:00. (stop of **Review** stage / start of **Staging** stage). request at 19:00. (stop of **Review** stage / start of **Staging** stage).
1. Now that the merge request is merged, a deployment to the `production` 1. Now that the merge request is merged, a deployment to the `production`
environment starts and finishes at 19:30 (stop of **Staging** stage). environment starts and finishes at 19:30 (stop of **Staging** stage).
1. The cycle completes and the sum of the median times of the previous stages
is recorded to the **Total** stage. That is the time between creating an
issue and deploying its relevant merge request to production.
From the above example you can conclude the time it took each stage to complete From the above example you can conclude the time it took each stage to complete
as long as their total time: as long as their total time:
...@@ -171,10 +165,6 @@ as long as their total time: ...@@ -171,10 +165,6 @@ as long as their total time:
- **Test**: 5min - **Test**: 5min
- **Review**: 5h (19:00 - 14:00) - **Review**: 5h (19:00 - 14:00)
- **Staging**: 30min (19:30 - 19:00) - **Staging**: 30min (19:30 - 19:00)
- **Total**: Since this stage measures the sum of median time of all
previous stages, we cannot calculate it if we don't know the status of the
stages before. In case this is the very first cycle that is run in the project,
then the **Total** time is 10h 30min (19:30 - 09:00)
A few notes: A few notes:
......
import { gray10 } from '@gitlab/ui/scss_to_js/scss_variables'; import { gray10 } from '@gitlab/ui/scss_to_js/scss_variables';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const PROJECTS_PER_PAGE = 50; export const PROJECTS_PER_PAGE = 50;
...@@ -27,12 +26,9 @@ export const EMPTY_STAGE_TEXT = { ...@@ -27,12 +26,9 @@ export const EMPTY_STAGE_TEXT = {
staging: __( staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
), ),
production: __(
'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
),
}; };
export const DEFAULT_STAGE_NAMES = [...Object.keys(EMPTY_STAGE_TEXT), 'total']; export const DEFAULT_STAGE_NAMES = [...Object.keys(EMPTY_STAGE_TEXT)];
export const TASKS_BY_TYPE_SUBJECT_ISSUE = 'Issue'; export const TASKS_BY_TYPE_SUBJECT_ISSUE = 'Issue';
export const TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST = 'MergeRequest'; export const TASKS_BY_TYPE_SUBJECT_MERGE_REQUEST = 'MergeRequest';
...@@ -59,25 +55,6 @@ export const STAGE_ACTIONS = { ...@@ -59,25 +55,6 @@ export const STAGE_ACTIONS = {
ADD_STAGE: 'showAddStageForm', ADD_STAGE: 'showAddStageForm',
}; };
export const STAGE_NAME = {
TOTAL: 'total',
PRODUCTION: 'production',
OVERVIEW: 'overview',
};
/**
* An object containing capitalized stages names
* i.e. { TOTAL: 'total' } => { TOTAL: 'Total' }
*/
export const CAPITALIZED_STAGE_NAME = Object.keys(STAGE_NAME).reduce((acc, stage) => {
return {
...acc,
[stage]: capitalizeFirstCharacter(STAGE_NAME[stage]),
};
}, {});
export const PATH_HOME_ICON = 'home';
export const DEFAULT_VALUE_STREAM_ID = 'default'; export const DEFAULT_VALUE_STREAM_ID = 'default';
export const OVERVIEW_METRICS = { export const OVERVIEW_METRICS = {
......
import { isNumber, sortBy } from 'lodash'; import { isNumber } from 'lodash';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -13,7 +13,6 @@ import { ...@@ -13,7 +13,6 @@ import {
parseSeconds, parseSeconds,
} from '~/lib/utils/datetime_utility'; } from '~/lib/utils/datetime_utility';
import { dateFormats } from '../shared/constants'; import { dateFormats } from '../shared/constants';
import { STAGE_NAME, CAPITALIZED_STAGE_NAME, PATH_HOME_ICON } from './constants';
import { toYmd } from '../shared/utils'; import { toYmd } from '../shared/utils';
const EVENT_TYPE_LABEL = 'label'; const EVENT_TYPE_LABEL = 'label';
...@@ -85,10 +84,8 @@ export const isPersistedStage = ({ custom, id }) => custom || isNumber(id); ...@@ -85,10 +84,8 @@ export const isPersistedStage = ({ custom, id }) => custom || isNumber(id);
*/ */
const stageUrlSlug = ({ id, title, custom = false }) => { const stageUrlSlug = ({ id, title, custom = false }) => {
if (custom) return id; if (custom) return id;
// We still use 'production' as the id to access this stage, even though the title is 'Total'
return title.toLowerCase() === STAGE_NAME.TOTAL return convertToSnakeCase(title);
? STAGE_NAME.PRODUCTION
: convertToSnakeCase(title);
}; };
export const transformRawStages = (stages = []) => export const transformRawStages = (stages = []) =>
...@@ -342,9 +339,6 @@ export const isStageNameExistsError = ({ status, errors }) => ...@@ -342,9 +339,6 @@ export const isStageNameExistsError = ({ status, errors }) =>
* Takes the stages and median data, combined with the selected stage, to build an * Takes the stages and median data, combined with the selected stage, to build an
* array which is formatted to proivde the data required for the path navigation. * array which is formatted to proivde the data required for the path navigation.
* *
* The stage named 'Total' is renamed to 'Overview', it's configured to have
* the 'home' icon - and is moved to the front of the array.
*
* @param {Array} stages - The stages available to the group / project * @param {Array} stages - The stages available to the group / project
* @param {Object} medians - The median values for the stages available to the group / project * @param {Object} medians - The median values for the stages available to the group / project
* @param {Object} selectedStage - The currently selected stage * @param {Object} selectedStage - The currently selected stage
...@@ -357,20 +351,17 @@ export const transformStagesForPathNavigation = ({ stages, medians, selectedStag ...@@ -357,20 +351,17 @@ export const transformStagesForPathNavigation = ({ stages, medians, selectedStag
hoursPerDay: 24, hoursPerDay: 24,
limitToDays: true, limitToDays: true,
}); });
const isTotalStage = stage.title === CAPITALIZED_STAGE_NAME.TOTAL;
return { return {
...stage, ...stage,
metric: days ? sprintf(s__('ValueStreamAnalytics|%{days}d'), { days }) : null, metric: days ? sprintf(s__('ValueStreamAnalytics|%{days}d'), { days }) : null,
selected: stage.title === selectedStage.title, selected: stage.title === selectedStage.title,
title: isTotalStage ? CAPITALIZED_STAGE_NAME.OVERVIEW : stage.title, title: stage.title,
icon: isTotalStage ? PATH_HOME_ICON : null, icon: null,
}; };
}); });
return sortBy(formattedStages, stage => return formattedStages;
stage.title === CAPITALIZED_STAGE_NAME.OVERVIEW ? 0 : 1,
);
}; };
/** /**
......
...@@ -129,11 +129,11 @@ RSpec.describe 'Group Value Stream Analytics', :js do ...@@ -129,11 +129,11 @@ RSpec.describe 'Group Value Stream Analytics', :js do
to_index: to) to_index: to)
end end
default_stage_order = %w[Issue Plan Code Test Review Staging Total].freeze default_stage_order = %w[Issue Plan Code Test Review Staging].freeze
default_custom_stage_order = %w[Issue Plan Code Test Review Staging Total Cool\ beans].freeze default_custom_stage_order = %w[Issue Plan Code Test Review Staging Cool\ beans].freeze
stages_near_middle_swapped = %w[Issue Plan Test Code Review Staging Total Cool\ beans].freeze stages_near_middle_swapped = %w[Issue Plan Test Code Review Staging Cool\ beans].freeze
stage_dragged_to_top = %w[Review Issue Plan Code Test Staging Total Cool\ beans].freeze stage_dragged_to_top = %w[Review Issue Plan Code Test Staging Cool\ beans].freeze
stage_dragged_to_bottom = %w[Issue Plan Code Test Staging Total Cool\ beans Review].freeze stage_dragged_to_bottom = %w[Issue Plan Code Test Staging Cool\ beans Review].freeze
shared_examples 'manual ordering disabled' do shared_examples 'manual ordering disabled' do
it 'does not allow stages to be draggable', :js do it 'does not allow stages to be draggable', :js do
......
...@@ -142,7 +142,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -142,7 +142,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
it 'displays the default list of stages' do it 'displays the default list of stages' do
stage_nav = page.find(stage_nav_selector) stage_nav = page.find(stage_nav_selector)
%w[Issue Plan Code Test Review Staging Total].each do |item| %w[Issue Plan Code Test Review Staging].each do |item|
string_id = "CycleAnalytics|#{item}" string_id = "CycleAnalytics|#{item}"
expect(stage_nav).to have_content(s_(string_id)) expect(stage_nav).to have_content(s_(string_id))
end end
...@@ -163,7 +163,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -163,7 +163,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
it 'displays the default list of stages' do it 'displays the default list of stages' do
path_nav = page.find(path_nav_selector) path_nav = page.find(path_nav_selector)
%w[Issue Plan Code Test Review Staging Overview].each do |item| %w[Issue Plan Code Test Review Staging].each do |item|
string_id = "CycleAnalytics|#{item}" string_id = "CycleAnalytics|#{item}"
expect(path_nav).to have_content(s_(string_id)) expect(path_nav).to have_content(s_(string_id))
end end
...@@ -303,8 +303,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -303,8 +303,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
{ title: 'Code', description: 'Time until first merge request', events_count: 1, median: 'about 5 hours' }, { title: 'Code', description: 'Time until first merge request', events_count: 1, median: 'about 5 hours' },
{ title: 'Test', description: 'Total test time for all commits/merges', events_count: 0, median: 'Not enough data' }, { title: 'Test', description: 'Total test time for all commits/merges', events_count: 0, median: 'Not enough data' },
{ title: 'Review', description: 'Time between merge request creation and merge/close', events_count: 1, median: 'about 1 hour' }, { title: 'Review', description: 'Time between merge request creation and merge/close', events_count: 1, median: 'about 1 hour' },
{ title: 'Staging', description: 'From merge request merge until deploy to production', events_count: 1, median: 'about 1 hour' }, { title: 'Staging', description: 'From merge request merge until deploy to production', events_count: 1, median: 'about 1 hour' }
{ title: 'Total', description: 'From issue creation until deploy to production', events_count: 1, median: '5 days' }
] ]
it 'each stage will have median values', :sidekiq_might_not_need_inline do it 'each stage will have median values', :sidekiq_might_not_need_inline do
......
...@@ -14,8 +14,6 @@ import { ...@@ -14,8 +14,6 @@ import {
testEvents, testEvents,
stagingStage, stagingStage,
stagingEvents, stagingEvents,
totalStage,
totalEvents,
codeStage, codeStage,
codeEvents, codeEvents,
} from '../mock_data'; } from '../mock_data';
...@@ -88,7 +86,6 @@ describe('Stage', () => { ...@@ -88,7 +86,6 @@ describe('Stage', () => {
${'Test'} | ${testStage} ${'Test'} | ${testStage}
${'Code'} | ${codeStage} ${'Code'} | ${codeStage}
${'Staging'} | ${stagingStage} ${'Staging'} | ${stagingStage}
${'Total'} | ${totalStage}
`('$name stage will render the stage description', ({ stage }) => { `('$name stage will render the stage description', ({ stage }) => {
wrapper = createComponent({ props: { stage, events: [] } }); wrapper = createComponent({ props: { stage, events: [] } });
expect(wrapper.find($sel.description).text()).toEqual(stage.description); expect(wrapper.find($sel.description).text()).toEqual(stage.description);
...@@ -100,7 +97,6 @@ describe('Stage', () => { ...@@ -100,7 +97,6 @@ describe('Stage', () => {
${'Plan'} | ${planStage} | ${planEvents} ${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents} ${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents} ${'Code'} | ${codeStage} | ${codeEvents}
${'Total'} | ${totalStage} | ${totalEvents}
`('$name stage will render the list of events', ({ stage, eventList }) => { `('$name stage will render the list of events', ({ stage, eventList }) => {
// stages generated from fixtures may not have events // stages generated from fixtures may not have events
const events = eventList.length ? eventList : generateEvents(5); const events = eventList.length ? eventList : generateEvents(5);
...@@ -120,7 +116,6 @@ describe('Stage', () => { ...@@ -120,7 +116,6 @@ describe('Stage', () => {
${'Plan'} | ${planStage} | ${planEvents} ${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents} ${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents} ${'Code'} | ${codeStage} | ${codeEvents}
${'Total'} | ${totalStage} | ${totalEvents}
`('$name stage will render the items as StageEventItems', ({ stage, eventList }) => { `('$name stage will render the items as StageEventItems', ({ stage, eventList }) => {
wrapper = createComponent({ props: { events: eventList, stage }, stubs: mockStubs }); wrapper = createComponent({ props: { events: eventList, stage }, stubs: mockStubs });
expect(wrapper.find(StageEventItem).exists()).toBe(true); expect(wrapper.find(StageEventItem).exists()).toBe(true);
......
...@@ -75,13 +75,12 @@ export const reviewStage = getStageByTitle(dummyState.stages, 'review'); ...@@ -75,13 +75,12 @@ export const reviewStage = getStageByTitle(dummyState.stages, 'review');
export const codeStage = getStageByTitle(dummyState.stages, 'code'); export const codeStage = getStageByTitle(dummyState.stages, 'code');
export const testStage = getStageByTitle(dummyState.stages, 'test'); export const testStage = getStageByTitle(dummyState.stages, 'test');
export const stagingStage = getStageByTitle(dummyState.stages, 'staging'); export const stagingStage = getStageByTitle(dummyState.stages, 'staging');
export const totalStage = getStageByTitle(dummyState.stages, 'total');
export const allowedStages = [issueStage, planStage, codeStage]; export const allowedStages = [issueStage, planStage, codeStage];
const deepCamelCase = obj => convertObjectPropsToCamelCase(obj, { deep: true }); const deepCamelCase = obj => convertObjectPropsToCamelCase(obj, { deep: true });
export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging', 'production']; export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
const stageFixtures = defaultStages.reduce((acc, stage) => { const stageFixtures = defaultStages.reduce((acc, stage) => {
const events = getJSONFixture(fixtureEndpoints.stageEvents(stage)); const events = getJSONFixture(fixtureEndpoints.stageEvents(stage));
...@@ -111,14 +110,12 @@ export const stageMediansWithNumericIds = defaultStages.reduce((acc, stage) => { ...@@ -111,14 +110,12 @@ export const stageMediansWithNumericIds = defaultStages.reduce((acc, stage) => {
export const endDate = new Date(2019, 0, 14); export const endDate = new Date(2019, 0, 14);
export const startDate = getDateInPast(endDate, DEFAULT_DAYS_IN_PAST); export const startDate = getDateInPast(endDate, DEFAULT_DAYS_IN_PAST);
export const rawIssueEvents = stageFixtures.issue;
export const issueEvents = deepCamelCase(stageFixtures.issue); export const issueEvents = deepCamelCase(stageFixtures.issue);
export const planEvents = deepCamelCase(stageFixtures.plan); export const planEvents = deepCamelCase(stageFixtures.plan);
export const reviewEvents = deepCamelCase(stageFixtures.review); export const reviewEvents = deepCamelCase(stageFixtures.review);
export const codeEvents = deepCamelCase(stageFixtures.code); export const codeEvents = deepCamelCase(stageFixtures.code);
export const testEvents = deepCamelCase(stageFixtures.test); export const testEvents = deepCamelCase(stageFixtures.test);
export const stagingEvents = deepCamelCase(stageFixtures.staging); export const stagingEvents = deepCamelCase(stageFixtures.staging);
export const totalEvents = deepCamelCase(stageFixtures.production);
export const rawCustomStage = { export const rawCustomStage = {
title: 'Coolest beans stage', title: 'Coolest beans stage',
hidden: false, hidden: false,
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
codeStage, codeStage,
stagingStage, stagingStage,
reviewStage, reviewStage,
totalStage,
startDate, startDate,
endDate, endDate,
selectedProjects, selectedProjects,
...@@ -130,7 +129,7 @@ describe('Cycle analytics mutations', () => { ...@@ -130,7 +129,7 @@ describe('Cycle analytics mutations', () => {
}); });
it('will convert the stats object to stages', () => { it('will convert the stats object to stages', () => {
[issueStage, planStage, codeStage, stagingStage, reviewStage, totalStage].forEach(stage => { [issueStage, planStage, codeStage, stagingStage, reviewStage].forEach(stage => {
expect(state.stages).toContainEqual(stage); expect(state.stages).toContainEqual(stage);
}); });
}); });
......
...@@ -18,7 +18,6 @@ import { ...@@ -18,7 +18,6 @@ import {
prepareTimeMetricsData, prepareTimeMetricsData,
} from 'ee/analytics/cycle_analytics/utils'; } from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils'; import { toYmd } from 'ee/analytics/shared/utils';
import { CAPITALIZED_STAGE_NAME, PATH_HOME_ICON } from 'ee/analytics/cycle_analytics/constants';
import { getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { slugify } from '~/lib/utils/text_utility'; import { slugify } from '~/lib/utils/text_utility';
import { import {
...@@ -36,7 +35,6 @@ import { ...@@ -36,7 +35,6 @@ import {
rawTasksByTypeData, rawTasksByTypeData,
allowedStages, allowedStages,
stageMediansWithNumericIds, stageMediansWithNumericIds,
totalStage,
pathNavIssueMetric, pathNavIssueMetric,
timeMetricsData, timeMetricsData,
} from './mock_data'; } from './mock_data';
...@@ -315,7 +313,7 @@ describe('Cycle analytics utils', () => { ...@@ -315,7 +313,7 @@ describe('Cycle analytics utils', () => {
}); });
describe('transformStagesForPathNavigation', () => { describe('transformStagesForPathNavigation', () => {
const stages = [...allowedStages, totalStage]; const stages = allowedStages;
const response = transformStagesForPathNavigation({ const response = transformStagesForPathNavigation({
stages, stages,
medians: stageMediansWithNumericIds, medians: stageMediansWithNumericIds,
...@@ -339,22 +337,6 @@ describe('Cycle analytics utils', () => { ...@@ -339,22 +337,6 @@ describe('Cycle analytics utils', () => {
expect(issue.metric).toEqual(pathNavIssueMetric); expect(issue.metric).toEqual(pathNavIssueMetric);
}); });
describe(`${CAPITALIZED_STAGE_NAME.OVERVIEW} stage specific changes`, () => {
const overview = response.filter(stage => stage.name === CAPITALIZED_STAGE_NAME.TOTAL)[0];
it(`renames '${CAPITALIZED_STAGE_NAME.TOTAL}' stage title to '${CAPITALIZED_STAGE_NAME.OVERVIEW}'`, () => {
expect(overview.title).toEqual(CAPITALIZED_STAGE_NAME.OVERVIEW);
});
it('includes the correct icon', () => {
expect(overview.icon).toEqual(PATH_HOME_ICON);
});
it(`moves the stage to the front`, () => {
expect(response[0]).toEqual(overview);
});
});
}); });
}); });
......
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RemoveCycleAnalyticsTotalStageData, :migration do
let(:group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
let(:project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:group_value_streams_table) { table(:analytics_cycle_analytics_group_value_streams) }
let(:project) { projects.create!(id: 12058473, namespace_id: group.id, name: 'gitlab', path: 'gitlab') }
let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
let(:group_value_stream) { group_value_streams_table.create!(group_id: group.id, name: 'default') }
before do
group_stages_table.create!(
id: 1,
name: 'production',
group_value_stream_id: group_value_stream.id,
group_id: group.id,
start_event_identifier: 1,
end_event_identifier: 2
)
group_stages_table.create!(
id: 2,
name: 'plan',
group_value_stream_id: group_value_stream.id,
group_id: group.id,
start_event_identifier: 1,
end_event_identifier: 2
)
project_stages_table.create!(
id: 1,
name: 'production',
project_id: project.id,
start_event_identifier: 1,
end_event_identifier: 2
)
project_stages_table.create!(
id: 2,
name: 'plan',
project_id: project.id,
start_event_identifier: 1,
end_event_identifier: 2
)
end
it 'removes "production" stage info and keeps other stages' do
migrate!
expect(group_stages_table.all.map(&:id)).to eq [2]
expect(project_stages_table.all.map(&:id)).to eq [2]
end
end
...@@ -18,8 +18,7 @@ module Gitlab ...@@ -18,8 +18,7 @@ module Gitlab
params_for_code_stage, params_for_code_stage,
params_for_test_stage, params_for_test_stage,
params_for_review_stage, params_for_review_stage,
params_for_staging_stage, params_for_staging_stage
params_for_production_stage
] ]
end end
...@@ -86,16 +85,6 @@ module Gitlab ...@@ -86,16 +85,6 @@ module Gitlab
end_event_identifier: :merge_request_first_deployed_to_production end_event_identifier: :merge_request_first_deployed_to_production
} }
end end
def self.params_for_production_stage
{
name: 'production',
custom: false,
relative_position: 7,
start_event_identifier: :issue_created,
end_event_identifier: :production_stage_end
}
end
end end
end end
end end
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
module StageEvents module StageEvents
class ProductionStageEnd < StageEvent class ProductionStageEnd < StageEvent
def self.name def self.name
_("Issue first depoloyed to production") _("Issue first deployed to production")
end end
def self.identifier def self.identifier
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
class ProductionStage < BaseStage
include ProductionHelper
def start_time_attrs
@start_time_attrs ||= issue_table[:created_at]
end
def end_time_attrs
@end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
end
def name
:production
end
def title
s_('CycleAnalyticsStage|Total')
end
def legend
_("Related Issues")
end
def description
_("From issue creation until deploy to production")
end
def query
# Limit to merge requests that have been deployed to production after `@from`
query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
end
end
end
end
...@@ -13905,7 +13905,7 @@ msgstr "" ...@@ -13905,7 +13905,7 @@ msgstr ""
msgid "Issue events" msgid "Issue events"
msgstr "" msgstr ""
msgid "Issue first depoloyed to production" msgid "Issue first deployed to production"
msgstr "" msgstr ""
msgid "Issue label" msgid "Issue label"
...@@ -25306,9 +25306,6 @@ msgstr "" ...@@ -25306,9 +25306,6 @@ msgstr ""
msgid "The private key to use when a client certificate is provided. This value is encrypted at rest." msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
msgstr "" msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
msgid "The project can be accessed by any logged in user." msgid "The project can be accessed by any logged in user."
msgstr "" msgstr ""
...@@ -25399,9 +25396,6 @@ msgstr "" ...@@ -25399,9 +25396,6 @@ msgstr ""
msgid "The time taken by each data entry gathered by that stage." msgid "The time taken by each data entry gathered by that stage."
msgstr "" msgstr ""
msgid "The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination." msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination."
msgstr "" msgstr ""
......
...@@ -76,9 +76,6 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -76,9 +76,6 @@ RSpec.describe 'Value Stream Analytics', :js do
click_stage('Staging') click_stage('Staging')
expect_build_to_be_present expect_build_to_be_present
click_stage('Total')
expect_issue_to_be_present
end end
context "when I change the time period observed" do context "when I change the time period observed" do
......
...@@ -306,48 +306,6 @@ RSpec.describe 'cycle analytics events' do ...@@ -306,48 +306,6 @@ RSpec.describe 'cycle analytics events' do
end end
end end
describe '#production_events', :sidekiq_might_not_need_inline do
let(:stage) { :production }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
merge_merge_requests_closing_issue(user, project, context)
deploy_master(user, project)
end
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[:url]).not_to be_nil
end
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
expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
expect(events.first[:author][:name]).to eq(context.author.name)
end
end
def setup(context) def setup(context)
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
context.update(milestone: milestone) context.update(milestone: milestone)
......
...@@ -21,10 +21,6 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do ...@@ -21,10 +21,6 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do
expect(subject[:staging]).to eq(false) expect(subject[:staging]).to eq(false)
end end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
it 'has no permissions to code stage' do it 'has no permissions to code stage' do
expect(subject[:code]).to eq(false) expect(subject[:code]).to eq(false)
end end
...@@ -55,10 +51,6 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do ...@@ -55,10 +51,6 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do
expect(subject[:staging]).to eq(true) expect(subject[:staging]).to eq(true)
end end
it 'has permissions to production stage' do
expect(subject[:production]).to eq(true)
end
it 'has permissions to code stage' do it 'has permissions to code stage' do
expect(subject[:code]).to eq(true) expect(subject[:code]).to eq(true)
end end
...@@ -121,9 +113,5 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do ...@@ -121,9 +113,5 @@ RSpec.describe Gitlab::CycleAnalytics::Permissions do
it 'has no permissions to issue stage' do it 'has no permissions to issue stage' do
expect(subject[:issue]).to eq(false) expect(subject[:issue]).to eq(false)
end end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::CycleAnalytics::ProductionStage do
let(:stage_name) { 'Total' }
it_behaves_like 'base stage'
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'CycleAnalytics#production' do
extend CycleAnalyticsHelpers::TestGeneration
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
generate_cycle_analytics_spec(
phase: :production,
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save! }]],
before_end_fn: lambda do |context, data|
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end,
end_time_conditions:
[["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
context.project.repository.commit("sha_that_does_not_matter")
context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project)
expect(subject[:production].project_median).to be_nil
end
end
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = build(:issue, project: project)
merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project, environment: 'staging')
expect(subject[:production].project_median).to be_nil
end
end
end
...@@ -73,15 +73,6 @@ RSpec.describe 'value stream analytics events' do ...@@ -73,15 +73,6 @@ RSpec.describe 'value stream analytics events' do
expect(json_response['events'].first['date']).not_to be_empty expect(json_response['events'].first['date']).not_to be_empty
end end
it 'lists the production events', :sidekiq_might_not_need_inline do
get project_cycle_analytics_production_path(project, format: :json)
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['iid']).to eq(first_issue_iid)
end
context 'specific branch' do context 'specific branch' do
it 'lists the test events', :sidekiq_might_not_need_inline do it 'lists the test events', :sidekiq_might_not_need_inline do
branch = project.merge_requests.first.source_branch branch = project.merge_requests.first.source_branch
......
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