Commit bb1539bc authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '38317-rename-production-stage-in-cycle-analytics-to-total-time' into 'master'

Rename "Production" stage in Cycle Analytics to "Total"

Closes #38317

See merge request gitlab-org/gitlab!21450
parents 22056975 52e77038
......@@ -24,7 +24,7 @@ const EMPTY_STAGE_TEXTS = {
'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.',
'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.',
),
};
......
......@@ -50,7 +50,7 @@
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
%li.total-time-header.pr-5.text-right
%span.stage-name.font-weight-bold
{{ __('Total Time') }}
{{ __('Time') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
......
......@@ -77,8 +77,8 @@ end
Some start/end event pairs are not "compatible" with each other. For example:
- "Issue created" to "Merge Request created": The event classes are defined on different domain models, the `object_type` method is different.
- "Issue closed" to "Issue created": Issue must be created first before it can be closed.
- "Issue created" to "Merge Request created": The event classes are defined on different domain models, the `object_type` method is different.
- "Issue closed" to "Issue created": Issue must be created first before it can be closed.
- "Issue closed" to "Issue closed": Duration is always 0.
The `StageEvents` module describes the allowed `start_event` and `end_event` pairings (`PAIRING_RULES` constant). If a new event is added, it needs to be registered in this module.
......
......@@ -44,8 +44,8 @@ There are seven stages that are tracked as part of the Cycle Analytics calculati
- Time spent on code review
- **Staging** (Continuous Deployment)
- Time between merging and deploying to production
- **Production** (Total)
- Total lifecycle time; i.e. the velocity of the project or team
- **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**.
## Date ranges
......@@ -60,12 +60,12 @@ GitLab provides the ability to filter analytics based on a date range. To filter
## How the data is measured
Cycle Analytics records cycle time and data based on the project issues with the
exception of the staging and production stages, where only data deployed to
exception of the staging and total stages, where only data deployed to
production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
or `production/*` [environment](../../ci/yaml/README.md#environment), then you will not have any
data for those stages.
data for this stage.
Each stage of Cycle Analytics is further described in the table below.
......@@ -77,7 +77,7 @@ Each stage of Cycle 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 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 closing issue pattern, between its creation and until it's merged. |
| Staging | Measures the median time between merging the merge request with 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 configuration. If there isn't a production environment, this is not tracked. |
| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
| 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:
......@@ -124,7 +124,7 @@ environments is configured.
1. Now that the merge request is merged, a deployment to the `production`
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 **Production** stage. That is the time between creating an
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
......@@ -136,10 +136,10 @@ as long as their total time:
- **Test**: 5min
- **Review**: 5h (19:00 - 14:00)
- **Staging**: 30min (19:30 - 19:00)
- **Production**: Since this stage measures the sum of median time off all
- **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 **Production** time is 10h 30min (19:30 - 09:00)
then the **Total** time is 10h 30min (19:30 - 09:00)
A few notes:
......
......@@ -112,7 +112,7 @@ export default {
displayHeader: !this.customStageFormActive,
},
{
title: __('Total Time'),
title: __('Time'),
description: __('The time taken by each data entry gathered by that stage.'),
classes: 'total-time-header pr-5 text-right',
displayHeader: !this.customStageFormActive,
......
......@@ -41,3 +41,8 @@ export const STAGE_ACTIONS = {
CREATE: 'createStage',
UPDATE: 'updateStage',
};
export const STAGE_NAME = {
TOTAL: 'total',
PRODUCTION: 'production',
};
......@@ -4,6 +4,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { newDate, dayAfter, secondsToDays } from '~/lib/utils/datetime_utility';
import { dateFormats } from '../shared/constants';
import { STAGE_NAME } from './constants';
const EVENT_TYPE_LABEL = 'label';
......@@ -46,6 +47,25 @@ export const getLabelEventsIdentifiers = (events = []) =>
*/
export const isPersistedStage = ({ custom, id }) => custom || isNumber(id);
/**
* Returns the the correct slug to use for a stage
* default stages use the snakecased title of the stage, while custom
* stages will have a numeric id
*
* @param {Object} obj
* @param {string} obj.title - title of the stage
* @param {number} obj.id - numerical object id available for custom stages
* @param {boolean} obj.custom - boolean flag indicating a custom stage
* @returns {(number|string)} Returns a numerical id for customs stages and string for default stages
*/
const stageUrlSlug = ({ id, title, custom = false }) => {
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
? STAGE_NAME.PRODUCTION
: convertToSnakeCase(title);
};
export const transformRawStages = (stages = []) =>
stages
.map(({ id, title, name = '', custom = false, ...rest }) => ({
......@@ -53,7 +73,7 @@ export const transformRawStages = (stages = []) =>
id,
title,
custom,
slug: isPersistedStage({ custom, id }) ? id : convertToSnakeCase(title),
slug: isPersistedStage({ custom, id }) ? id : stageUrlSlug({ custom, id, title }),
// the name field is used to create a stage, but the get request returns title
name: name.length ? name : title,
}))
......
......@@ -29,7 +29,7 @@ module Analytics
description: -> { _('From merge request merge until deploy to production') }
}.freeze,
production: {
title: -> { s_('CycleAnalyticsStage|Production') },
title: -> { s_('CycleAnalyticsStage|Total') },
description: -> { _('From issue creation until deploy to production') }
}.freeze
}.freeze
......
---
title: Rename "Production" stage in Cycle Analytics to "Total"
merge_request: 21450
author:
type: changed
......@@ -127,7 +127,7 @@ describe 'Group Cycle Analytics', :js do
it 'displays the default list of stages' do
stage_nav = page.find(stage_nav_selector)
%w[Issue Plan Code Test Review Staging Production].each do |item|
%w[Issue Plan Code Test Review Staging Total].each do |item|
expect(stage_nav).to have_content(item)
end
end
......@@ -172,7 +172,7 @@ describe 'Group Cycle Analytics', :js do
{ 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: 0, median: "Not enough data" },
{ title: "Staging", description: "From merge request merge until deploy to production", events_count: 0, median: "Not enough data" },
{ title: "Production", description: "From issue creation until deploy to production", events_count: 1, median: "5 days" }
{ 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
......
......@@ -14,8 +14,8 @@ import {
testEvents,
stagingStage,
stagingEvents,
productionStage,
productionEvents,
totalStage,
totalEvents,
codeStage,
codeEvents,
} from '../mock_data';
......@@ -83,32 +83,33 @@ describe('Stage', () => {
describe('Default stages', () => {
it.each`
name | stage
${'Issue'} | ${issueStage}
${'Plan'} | ${planStage}
${'Review'} | ${reviewStage}
${'Test'} | ${testStage}
${'Code'} | ${codeStage}
${'Staging'} | ${stagingStage}
${'Production'} | ${productionStage}
name | stage
${'Issue'} | ${issueStage}
${'Plan'} | ${planStage}
${'Review'} | ${reviewStage}
${'Test'} | ${testStage}
${'Code'} | ${codeStage}
${'Staging'} | ${stagingStage}
${'Total'} | ${totalStage}
`('$name stage will render the stage description', ({ stage }) => {
wrapper = createComponent({ props: { stage, events: [] } });
expect(wrapper.find($sel.description).text()).toEqual(stage.description);
});
it.each`
name | stage | eventList
${'Issue'} | ${issueStage} | ${issueEvents}
${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents}
${'Production'} | ${productionStage} | ${productionEvents}
name | stage | eventList
${'Issue'} | ${issueStage} | ${issueEvents}
${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents}
${'Total'} | ${totalStage} | ${totalEvents}
`('$name stage will render the list of events', ({ stage, eventList }) => {
// stages generated from fixtures may not have events
const events = eventList.length ? eventList : generateEvents(5);
wrapper = createComponent({
props: { stage, events },
});
events.forEach((item, index) => {
const elem = wrapper.findAll($sel.item).at(index);
expect(elem.find($sel.title).text()).toContain(item.title);
......@@ -116,12 +117,12 @@ describe('Stage', () => {
});
it.each`
name | stage | eventList
${'Issue'} | ${issueStage} | ${issueEvents}
${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents}
${'Production'} | ${productionStage} | ${productionEvents}
name | stage | eventList
${'Issue'} | ${issueStage} | ${issueEvents}
${'Plan'} | ${planStage} | ${planEvents}
${'Review'} | ${reviewStage} | ${reviewEvents}
${'Code'} | ${codeStage} | ${codeEvents}
${'Total'} | ${totalStage} | ${totalEvents}
`('$name stage will render the items as StageEventItems', ({ stage, eventList }) => {
wrapper = createComponent({ props: { events: eventList, stage }, stubs: mockStubs });
expect(wrapper.find(StageEventItem).exists()).toBe(true);
......
......@@ -23,7 +23,7 @@ const $sel = {
illustration: '.empty-state .svg-content',
};
const headers = ['Stage', 'Median', issueStage.legend, 'Total Time'];
const headers = ['Stage', 'Median', issueStage.legend, 'Time'];
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
......
......@@ -48,7 +48,7 @@ export const reviewStage = getStageByTitle(dummyState.stages, 'review');
export const codeStage = getStageByTitle(dummyState.stages, 'code');
export const testStage = getStageByTitle(dummyState.stages, 'test');
export const stagingStage = getStageByTitle(dummyState.stages, 'staging');
export const productionStage = getStageByTitle(dummyState.stages, 'production');
export const totalStage = getStageByTitle(dummyState.stages, 'total');
export const allowedStages = [issueStage, planStage, codeStage];
......@@ -82,7 +82,7 @@ export const reviewEvents = deepCamelCase(stageFixtures.review);
export const codeEvents = deepCamelCase(stageFixtures.code);
export const testEvents = deepCamelCase(stageFixtures.test);
export const stagingEvents = deepCamelCase(stageFixtures.staging);
export const productionEvents = deepCamelCase(stageFixtures.production);
export const totalEvents = deepCamelCase(stageFixtures.production);
export const rawCustomStage = {
title: 'Coolest beans stage',
hidden: false,
......
......@@ -11,7 +11,7 @@ import {
codeStage,
stagingStage,
reviewStage,
productionStage,
totalStage,
groupLabels,
startDate,
endDate,
......@@ -157,11 +157,9 @@ describe('Cycle analytics mutations', () => {
});
it('will convert the stats object to stages', () => {
[issueStage, planStage, codeStage, stagingStage, reviewStage, productionStage].forEach(
stage => {
expect(state.stages).toContainEqual(stage);
},
);
[issueStage, planStage, codeStage, stagingStage, reviewStage, totalStage].forEach(stage => {
expect(state.stages).toContainEqual(stage);
});
});
});
});
......
......@@ -18,7 +18,7 @@ module Gitlab
end
def title
s_('CycleAnalyticsStage|Production')
s_('CycleAnalyticsStage|Total')
end
def legend
......
......@@ -5508,9 +5508,6 @@ msgstr ""
msgid "CycleAnalyticsStage|Plan"
msgstr ""
msgid "CycleAnalyticsStage|Production"
msgstr ""
msgid "CycleAnalyticsStage|Review"
msgstr ""
......@@ -5520,6 +5517,9 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
msgid "CycleAnalyticsStage|Total"
msgstr ""
msgid "CycleAnalyticsStage|is not available for the selected group"
msgstr ""
......@@ -18158,6 +18158,9 @@ msgstr ""
msgid "The time taken by each data entry gathered by that stage."
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 unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url` with a trailing slash"
msgstr ""
......@@ -19146,9 +19149,6 @@ msgstr ""
msgid "Total Contributions"
msgstr ""
msgid "Total Time"
msgstr ""
msgid "Total artifacts size: %{total_size}"
msgstr ""
......
......@@ -76,7 +76,7 @@ describe 'Cycle Analytics', :js do
click_stage('Staging')
expect_build_to_be_present
click_stage('Production')
click_stage('Total')
expect_issue_to_be_present
end
......
......@@ -4,7 +4,7 @@ require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::ProductionStage do
let(:stage_name) { :production }
let(:stage_name) { 'Total' }
it_behaves_like 'base stage'
end
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