diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index 9d7cc5dc41b8d87f58ffd8525fbec3833fb89881..119aa797b42b357be752705822bf11f772bf8cc6 100644 --- a/app/assets/javascripts/analytics/shared/constants.js +++ b/app/assets/javascripts/analytics/shared/constants.js @@ -114,6 +114,37 @@ export const DORA_METRICS = { CHANGE_FAILURE_RATE: 'change_failure_rate', }; +export const LEAD_TIME_NO_DATA_MESSAGE = s__( + 'DORA4Metrics|No merge requests were deployed during this period', +); + +export const DORA_METRICS_NULL_SERIES_TITLE = { + [DORA_METRICS.CHANGE_FAILURE_RATE]: s__('DORA4Metrics|No incidents during this period'), + [DORA_METRICS.TIME_TO_RESTORE_SERVICE]: s__('DORA4Metrics|No incidents during this period'), + [DORA_METRICS.LEAD_TIME_FOR_CHANGES]: LEAD_TIME_NO_DATA_MESSAGE, + [DORA_METRICS.DEPLOYMENT_FREQUENCY]: s__('DORA4Metrics|No deployments during this period'), +}; + +export const LEAD_TIME_FOR_CHANGES_SECONDARY_SERIES_NAME = s__( + 'DORA4Metrics|Median (last %{days}d)', +); +export const DEPLOYMENT_FREQUENCY_SECONDARY_SERIES_NAME = s__( + 'DORA4Metrics|Average (last %{days}d)', +); +export const CHANGE_FAILURE_RATE_SECONDARY_SERIES_NAME = s__( + 'DORA4Metrics|Median time (last %{days}d)', +); +export const TIME_TO_RESTORE_SERVICE_SECONDARY_SERIES_NAME = s__( + 'DORA4Metrics|Median time (last %{days}d)', +); + +export const DORA_METRICS_SECONDARY_SERIES_NAME = { + [DORA_METRICS.CHANGE_FAILURE_RATE]: CHANGE_FAILURE_RATE_SECONDARY_SERIES_NAME, + [DORA_METRICS.DEPLOYMENT_FREQUENCY]: DEPLOYMENT_FREQUENCY_SECONDARY_SERIES_NAME, + [DORA_METRICS.TIME_TO_RESTORE_SERVICE]: TIME_TO_RESTORE_SERVICE_SECONDARY_SERIES_NAME, + [DORA_METRICS.LEAD_TIME_FOR_CHANGES]: LEAD_TIME_FOR_CHANGES_SECONDARY_SERIES_NAME, +}; + export const VSA_METRICS_GROUPS = [ { key: 'lifecycle_metrics', diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/dora_metrics.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/dora_metrics.js index f5112acc37fffc39df7fda9739b731a7ac7580d6..f137d282184db803d6a292376724135a5c177f0d 100644 --- a/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/dora_metrics.js +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/data_sources/dora_metrics.js @@ -1,3 +1,5 @@ +import { getDayDifference } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '~/locale'; import { BUCKETING_INTERVAL_ALL, BUCKETING_INTERVAL_MONTHLY, @@ -5,7 +7,12 @@ import { } from '~/analytics/shared/graphql/constants'; import DoraMetricsQuery from '~/analytics/shared/graphql/dora_metrics.query.graphql'; import { extractQueryResponseFromNamespace, scaledValueForDisplay } from '~/analytics/shared/utils'; -import { VALUE_STREAM_METRIC_TILE_METADATA } from '~/analytics/shared/constants'; +import { + VALUE_STREAM_METRIC_TILE_METADATA, + DORA_METRICS_NULL_SERIES_TITLE, + DORA_METRICS_SECONDARY_SERIES_NAME, + DORA_METRICS, +} from '~/analytics/shared/constants'; import { TABLE_METRICS } from 'ee/analytics/dashboards/constants'; import { DORA_METRICS_CHARTS_ADDITIONAL_OPTS } from 'ee/analytics/analytics_dashboards/constants'; import { @@ -13,8 +20,14 @@ import { DATE_RANGE_OPTION_KEYS, DATE_RANGE_OPTIONS, } from 'ee/analytics/analytics_dashboards/components/filters/constants'; +import { buildNullSeries } from 'ee/analytics/shared/utils'; import { getStartDate } from 'ee/analytics/analytics_dashboards/components/filters/utils'; -import { startOfTomorrow } from 'ee/dora/components/static_data/shared'; +import { + startOfTomorrow, + averageSeriesOptions, + medianSeriesOptions, +} from 'ee/dora/components/static_data/shared'; +import { seriesToMedianSeries, seriesToAverageSeries } from 'ee/dora/components/util'; import { defaultClient } from '../graphql/client'; const asValue = ({ metrics, targetMetric, units }) => { @@ -24,13 +37,39 @@ const asValue = ({ metrics, targetMetric, units }) => { return scaledValueForDisplay(metricValue, units); }; -const asTimeSeries = ({ metrics, targetMetric }) => { +const calculateAdditionalSeries = ({ targetMetric, rawData, daysCount }) => { + const seriesName = DORA_METRICS_SECONDARY_SERIES_NAME[targetMetric]; + + if (targetMetric === DORA_METRICS.DEPLOYMENT_FREQUENCY) { + return { + ...averageSeriesOptions, + ...seriesToAverageSeries(rawData, sprintf(seriesName, { days: daysCount })), + }; + } + + return { + ...medianSeriesOptions, + ...seriesToMedianSeries(rawData, sprintf(seriesName, { days: daysCount })), + }; +}; + +const asTimeSeries = ({ metrics, targetMetric, daysCount, nullSeriesTitle = __('No data') }) => { // Extracts a date + value, returns an array of arrays [[date, value],[date, value]] - const data = metrics.map(({ date, ...rest }) => { + // Calculates a "null" series and returns all the series in the correct order for rendering + const rawData = metrics.map(({ date, ...rest }) => { return [date, rest[targetMetric]]; }); - return [{ name: VALUE_STREAM_METRIC_TILE_METADATA[targetMetric].label, data }]; + const data = { name: VALUE_STREAM_METRIC_TILE_METADATA[targetMetric].label, data: rawData }; + const [nullSeries, primarySeries] = buildNullSeries({ seriesData: [data], nullSeriesTitle }); + const additionalSeries = calculateAdditionalSeries({ targetMetric, rawData, daysCount }); + + return [primarySeries, additionalSeries, nullSeries].map( + ({ data: seriesData, ...seriesRest }) => ({ + data: seriesData.filter(([, n]) => !Number.isNaN(n)), + ...seriesRest, + }), + ); }; const fetchDoraMetricsQuery = async ({ metric, namespace, startDate, endDate, interval }) => { @@ -46,9 +85,15 @@ const fetchDoraMetricsQuery = async ({ metric, namespace, startDate, endDate, in const { metrics } = extractQueryResponseFromNamespace({ result, resultKey: 'dora' }); const { units } = TABLE_METRICS[metric]; + const daysCount = getDayDifference(startDate, endDate); if ([BUCKETING_INTERVAL_DAILY, BUCKETING_INTERVAL_MONTHLY].includes(interval)) { - return asTimeSeries({ metrics, targetMetric: metric }); + return asTimeSeries({ + metrics, + targetMetric: metric, + nullSeriesTitle: DORA_METRICS_NULL_SERIES_TITLE[metric] ?? null, + daysCount, + }); } return asValue({ metrics, targetMetric: metric, units }); diff --git a/ee/app/assets/javascripts/dora/components/change_failure_rate_charts.vue b/ee/app/assets/javascripts/dora/components/change_failure_rate_charts.vue index ae7cc9a7d24754c3aa63634b48743d8c739bd88e..0a33f7a4b95ff1c3a43b3c468c8287a6f99a6188 100644 --- a/ee/app/assets/javascripts/dora/components/change_failure_rate_charts.vue +++ b/ee/app/assets/javascripts/dora/components/change_failure_rate_charts.vue @@ -1,10 +1,13 @@ <script> import * as Sentry from '~/sentry/sentry_browser_wrapper'; import * as DoraApi from 'ee/api/dora_api'; +import { + DORA_METRICS_QUERY_TYPE, + CHANGE_FAILURE_RATE_SECONDARY_SERIES_NAME, +} from '~/analytics/shared/constants'; import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; -import { DORA_METRICS_QUERY_TYPE } from '~/analytics/shared/constants'; import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue'; import { buildNullSeries } from '../../analytics/shared/utils'; import ChartTooltipText from '../../analytics/shared/components/chart_tooltip_text.vue'; @@ -13,7 +16,6 @@ import { allChartDefinitions, areaChartOptions, averageSeriesOptions, - medianSeriesName, chartDescriptionText, chartDocumentationHref, LAST_WEEK, @@ -139,7 +141,9 @@ export default { ...averageSeriesOptions, ...seriesToMedianSeries( data, - sprintf(medianSeriesName, { days: this.$options.chartInDays[id] }), + sprintf(CHANGE_FAILURE_RATE_SECONDARY_SERIES_NAME, { + days: this.$options.chartInDays[id], + }), ), }; diff --git a/ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue b/ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue index b71422f9330ce0725b3e594cd78cf3f3f60d9f83..855ee03862ef649ae6951c8e11810c4200056dd9 100644 --- a/ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue +++ b/ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue @@ -7,7 +7,10 @@ import * as DoraApi from 'ee/api/dora_api'; import { linearRegression } from 'ee/analytics/shared/utils'; import SafeHtml from '~/vue_shared/directives/safe_html'; import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; -import { ALL_METRICS_QUERY_TYPE } from '~/analytics/shared/constants'; +import { + ALL_METRICS_QUERY_TYPE, + DEPLOYMENT_FREQUENCY_SECONDARY_SERIES_NAME, +} from '~/analytics/shared/constants'; import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; import { spriteIcon } from '~/lib/utils/common_utils'; @@ -20,7 +23,6 @@ import { allChartDefinitions, areaChartOptions, averageSeriesOptions, - averageSeriesName, chartDescriptionText, chartDocumentationHref, LAST_WEEK, @@ -199,7 +201,9 @@ export default { ...averageSeriesOptions, ...seriesToAverageSeries( data, - sprintf(averageSeriesName, { days: this.$options.chartInDays[id] }), + sprintf(DEPLOYMENT_FREQUENCY_SECONDARY_SERIES_NAME, { + days: this.$options.chartInDays[id], + }), ), }, ]; diff --git a/ee/app/assets/javascripts/dora/components/lead_time_charts.vue b/ee/app/assets/javascripts/dora/components/lead_time_charts.vue index c7ba9a7cc61de8b28d517d2111ea9cc9c6b75b13..ac48550d47651a8d70e4643968a5feb8b702bffa 100644 --- a/ee/app/assets/javascripts/dora/components/lead_time_charts.vue +++ b/ee/app/assets/javascripts/dora/components/lead_time_charts.vue @@ -1,5 +1,9 @@ <script> import * as DoraApi from 'ee/api/dora_api'; +import { + LEAD_TIME_NO_DATA_MESSAGE, + LEAD_TIME_FOR_CHANGES_SECONDARY_SERIES_NAME, +} from '~/analytics/shared/constants'; import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue'; @@ -10,7 +14,6 @@ import { allChartDefinitions, areaChartOptions, averageSeriesOptions, - medianSeriesTitle, chartDescriptionText, chartDocumentationHref, LAST_WEEK, @@ -18,7 +21,6 @@ import { LAST_90_DAYS, LAST_180_DAYS, CHART_TITLE, - NO_DATA_MESSAGE, } from './static_data/lead_time'; import { apiDataToChartSeries, seriesToMedianSeries, extractTimeSeriesTooltip } from './util'; @@ -95,7 +97,9 @@ export default { ...averageSeriesOptions, ...seriesToMedianSeries( data, - sprintf(medianSeriesTitle, { days: this.$options.chartInDays[id] }), + sprintf(LEAD_TIME_FOR_CHANGES_SECONDARY_SERIES_NAME, { + days: this.$options.chartInDays[id], + }), ), }; @@ -159,7 +163,7 @@ export default { alertMessage: s__('DORA4Metrics|Something went wrong while getting lead time data.'), chartHeaderText: CHART_TITLE, medianLeadTime: CHART_TITLE, - noMergeRequestsDeployed: NO_DATA_MESSAGE, + noMergeRequestsDeployed: LEAD_TIME_NO_DATA_MESSAGE, }, }; </script> diff --git a/ee/app/assets/javascripts/dora/components/static_data/change_failure_rate.js b/ee/app/assets/javascripts/dora/components/static_data/change_failure_rate.js index b27796827cd45e2a888577bf65251b3416c06129..705e0fbc3a4f7c379c688369e437d02215e33e53 100644 --- a/ee/app/assets/javascripts/dora/components/static_data/change_failure_rate.js +++ b/ee/app/assets/javascripts/dora/components/static_data/change_failure_rate.js @@ -4,8 +4,6 @@ import { formatAsPercentage } from '../util'; export * from './shared'; -export const medianSeriesName = s__('DORA4Metrics|Median time (last %{days}d)'); - export const CHART_TITLE = s__('DORA4Metrics|Change failure rate'); export const areaChartOptions = { diff --git a/ee/app/assets/javascripts/dora/components/static_data/deployment_frequency.js b/ee/app/assets/javascripts/dora/components/static_data/deployment_frequency.js index f726b7adf1d8d4a152b2eb00f7962b9365e81511..bfa775d936155ae8915f685f12c652e27aa65be5 100644 --- a/ee/app/assets/javascripts/dora/components/static_data/deployment_frequency.js +++ b/ee/app/assets/javascripts/dora/components/static_data/deployment_frequency.js @@ -3,8 +3,6 @@ import { s__ } from '~/locale'; export * from './shared'; -export const averageSeriesName = s__('DORA4Metrics|Average (last %{days}d)'); - export const CHART_TITLE = s__('DORA4Metrics|Deployment frequency'); export const areaChartOptions = { diff --git a/ee/app/assets/javascripts/dora/components/static_data/lead_time.js b/ee/app/assets/javascripts/dora/components/static_data/lead_time.js index a2e3edf4f8fb7992fd4f0256d96588447faffd8c..84eff5249f29c84fa1b670b3ddd5caf65f1ef01a 100644 --- a/ee/app/assets/javascripts/dora/components/static_data/lead_time.js +++ b/ee/app/assets/javascripts/dora/components/static_data/lead_time.js @@ -5,10 +5,6 @@ import { humanizeTimeInterval } from '~/lib/utils/datetime_utility'; export * from './shared'; export const CHART_TITLE = s__('DORA4Metrics|Lead time for changes'); -export const NO_DATA_MESSAGE = s__( - 'DORA4Metrics|No merge requests were deployed during this period', -); -export const medianSeriesTitle = s__('DORA4Metrics|Median (last %{days}d)'); export const areaChartOptions = { grid: { containLabel: true }, diff --git a/ee/app/assets/javascripts/dora/components/static_data/shared.js b/ee/app/assets/javascripts/dora/components/static_data/shared.js index a7a8bc950b17c682992d50cc70f59a45ab03d551..373991db4e061bad135a665ca1d275b8894a2200 100644 --- a/ee/app/assets/javascripts/dora/components/static_data/shared.js +++ b/ee/app/assets/javascripts/dora/components/static_data/shared.js @@ -55,6 +55,13 @@ export const averageSeriesOptions = { }, }; +export const medianSeriesOptions = { + areaStyle: { + opacity: 0, + }, + showSymbol: false, +}; + const formatDateRangeString = (startDate) => { const start = dateFormat(startDate, dateFormats.defaultDate, true); const end = dateFormat(startOfToday, dateFormats.defaultDate, true); diff --git a/ee/app/assets/javascripts/dora/components/static_data/time_to_restore_service.js b/ee/app/assets/javascripts/dora/components/static_data/time_to_restore_service.js index 3272fbc42622b07b271a432d645b521080638f10..9a57c738e440fdd382bea0cb7adba28c3a6987be 100644 --- a/ee/app/assets/javascripts/dora/components/static_data/time_to_restore_service.js +++ b/ee/app/assets/javascripts/dora/components/static_data/time_to_restore_service.js @@ -4,8 +4,6 @@ import { secondsToDays } from '~/analytics/shared/utils'; export * from './shared'; -export const medianSeriesName = s__('DORA4Metrics|Median time (last %{days}d)'); - export const CHART_TITLE = s__('DORA4Metrics|Time to restore service'); export const areaChartOptions = { diff --git a/ee/app/assets/javascripts/dora/components/time_to_restore_service_charts.vue b/ee/app/assets/javascripts/dora/components/time_to_restore_service_charts.vue index db4bb1a863618c8f1aee06781e5dd6ecccac54b7..5c62251fbd50095781102a2cfe9d7a2aa5917b6b 100644 --- a/ee/app/assets/javascripts/dora/components/time_to_restore_service_charts.vue +++ b/ee/app/assets/javascripts/dora/components/time_to_restore_service_charts.vue @@ -4,7 +4,10 @@ import * as DoraApi from 'ee/api/dora_api'; import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; -import { DORA_METRICS_QUERY_TYPE } from '~/analytics/shared/constants'; +import { + DORA_METRICS_QUERY_TYPE, + TIME_TO_RESTORE_SERVICE_SECONDARY_SERIES_NAME, +} from '~/analytics/shared/constants'; import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue'; import { buildNullSeries } from '../../analytics/shared/utils'; import ChartTooltipText from '../../analytics/shared/components/chart_tooltip_text.vue'; @@ -13,7 +16,6 @@ import { allChartDefinitions, areaChartOptions, averageSeriesOptions, - medianSeriesName, chartDescriptionText, chartDocumentationHref, LAST_WEEK, @@ -138,7 +140,9 @@ export default { ...averageSeriesOptions, ...seriesToMedianSeries( data, - sprintf(medianSeriesName, { days: this.$options.chartInDays[id] }), + sprintf(TIME_TO_RESTORE_SERVICE_SECONDARY_SERIES_NAME, { + days: this.$options.chartInDays[id], + }), ), }; diff --git a/ee/app/assets/javascripts/dora/components/util.js b/ee/app/assets/javascripts/dora/components/util.js index a08940f86a085c589105cfae9d35c2cf0330e0a2..8b0efeffd565a84bf3f64d7af4c2f276a36fe5d6 100644 --- a/ee/app/assets/javascripts/dora/components/util.js +++ b/ee/app/assets/javascripts/dora/components/util.js @@ -64,7 +64,7 @@ export const apiDataToChartSeries = (apiData, startDate, endDate, seriesName, em * @returns {Object} An object containing the series name and an array of original data keys with the average of the dataset as each value. */ export const seriesToAverageSeries = (chartSeriesData, seriesName) => { - if (!chartSeriesData) return {}; + if (!chartSeriesData || !chartSeriesData.length) return {}; const average = Math.round( diff --git a/ee/lib/gitlab/analytics/dora_metrics/visualizations/lead_time_for_changes_over_time.yaml b/ee/lib/gitlab/analytics/dora_metrics/visualizations/lead_time_for_changes_over_time.yaml index f392d26031927dc68fd572ce188e2f1b7a29416e..75f9d8740528d21ac8187037788f1e05a1d68812 100644 --- a/ee/lib/gitlab/analytics/dora_metrics/visualizations/lead_time_for_changes_over_time.yaml +++ b/ee/lib/gitlab/analytics/dora_metrics/visualizations/lead_time_for_changes_over_time.yaml @@ -12,5 +12,5 @@ options: name: Date type: time yAxis: - name: "Days from merge to deploy" + name: "Time from merge to deploy" type: value diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/dora_metrics_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/dora_metrics_spec.js index ea21a600232acb78ae3a80e378d9a1d7d2680c95..a7adb521b9f527ada56bb753f8296cd43c449fed 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/dora_metrics_spec.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/components/data_sources/dora_metrics_spec.js @@ -10,6 +10,7 @@ import { BUCKETING_INTERVAL_DAILY, BUCKETING_INTERVAL_MONTHLY, } from '~/analytics/shared/graphql/constants'; +import { dataSeries, medianSeries, nullSeries } from '../../mock_data'; describe('Dora Metrics Data Source', () => { let res; @@ -161,6 +162,8 @@ describe('Dora Metrics Data Source', () => { }); }); + const timeSeriesData = [dataSeries, medianSeries, nullSeries]; + describe.each` interval ${BUCKETING_INTERVAL_DAILY} @@ -179,7 +182,15 @@ describe('Dora Metrics Data Source', () => { }); it('returns a time series', () => { - expect(res).toEqual([{ data: [[null, 23508]], name: 'Lead time for changes' }]); + expect(res).toEqual(timeSeriesData); + }); + + it('returns the data, median and null series', () => { + expect(timeSeriesData.map(({ name }) => name)).toEqual([ + 'Lead time for changes', + 'Median (last 180d)', + 'No merge requests were deployed during this period', + ]); }); it('calls setVisualizationOverrides', () => { diff --git a/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js b/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js index a66ba012d09995a446c95bec3d4194210b377aad..9fcf7d13b661d6e42606f4a38eb2c5d5c3331c77 100644 --- a/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js +++ b/ee/spec/frontend/analytics/analytics_dashboards/mock_data.js @@ -1262,3 +1262,45 @@ export const mockProjectNamespaceMetadata = { visibilityLevelTooltip: 'Internal - The project can be accessed by any logged in user except external users.', }; + +export const dataSeries = { + areaStyle: { + opacity: 0, + }, + data: [[null, 23508]], + itemStyle: { + color: '#617ae2', + }, + lineStyle: { + color: '#617ae2', + }, + name: 'Lead time for changes', + showAllSymbol: true, + showSymbol: true, + symbolSize: 8, +}; + +export const medianSeries = { + areaStyle: { + opacity: 0, + }, + showSymbol: false, + data: [[null, 23508]], + name: 'Median (last 180d)', +}; + +export const nullSeries = { + areaStyle: { + color: 'none', + }, + data: [[null, null]], + itemStyle: { + color: '#a4a3a8', + }, + lineStyle: { + color: '#a4a3a8', + type: 'dashed', + }, + name: 'No merge requests were deployed during this period', + showSymbol: false, +}; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 701df44e8f46d31cfa5662d2d9252940b173c77e..e859b64ad12640c386e5366edac46245786b8431 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18324,6 +18324,9 @@ msgstr "" msgid "DORA4Metrics|No data available for Group: %{fullPath}" msgstr "" +msgid "DORA4Metrics|No deployments during this period" +msgstr "" + msgid "DORA4Metrics|No incidents during this period" msgstr ""