diff --git a/doc/user/analytics/ai_impact_analytics.md b/doc/user/analytics/ai_impact_analytics.md index fd606222c64cd7621b228ed1ce2df6a6a15ef22d..1e3d5f4e95fbb56631bfad9e680c4c466c1307a5 100644 --- a/doc/user/analytics/ai_impact_analytics.md +++ b/doc/user/analytics/ai_impact_analytics.md @@ -67,11 +67,12 @@ The **Metric trends** table displays metrics for the last six months, with month ### Lifecycle metrics -- [**Cycle time**](../group/value_stream_analytics/_index.md#lifecycle-metrics) - [**Lead time**](../group/value_stream_analytics/_index.md#lifecycle-metrics) +- [**Median time to merge**](merge_request_analytics.md) - [**Deployment frequency**](dora_metrics.md#deployment-frequency) -- [**Change failure rate**](dora_metrics.md#change-failure-rate) +- [**Merge request throughput**](merge_request_analytics.md#view-the-number-of-merge-requests-in-a-date-range) - [**Critical vulnerabilities over time**](../application_security/vulnerability_report/_index.md) +- [**Contributor count**](../profile/contributions_calendar.md#user-contribution-events) ### AI usage metrics diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metric_table.vue b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metric_table.vue index a1b068aedf6a54bcb8eb5a4be279dec0cbb0973a..9223332828f11de762ba701bcc853d8e4c6475c7 100644 --- a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metric_table.vue +++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metric_table.vue @@ -21,6 +21,9 @@ import VulnerabilitiesQuery from '../graphql/vulnerabilities.query.graphql'; import FlowMetricsQuery from '../graphql/flow_metrics.query.graphql'; import DoraMetricsQuery from '../graphql/dora_metrics.query.graphql'; import AiMetricsQuery from '../graphql/ai_metrics.query.graphql'; +import MergeRequestsQuery from '../../graphql/merge_requests.query.graphql'; +import ContributorCountQuery from '../../graphql/contributor_count.query.graphql'; +import { MERGE_REQUESTS_STATE_MERGED } from '../../graphql/constants'; import MetricTableCell from '../../components/metric_table_cell.vue'; import TrendIndicator from '../../components/trend_indicator.vue'; import { @@ -48,7 +51,9 @@ import { import { SUPPORTED_DORA_METRICS, SUPPORTED_FLOW_METRICS, + SUPPORTED_MERGE_REQUEST_METRICS, SUPPORTED_VULNERABILITY_METRICS, + SUPPORTED_CONTRIBUTOR_METRICS, SUPPORTED_AI_METRICS, HIDE_METRIC_DRILL_DOWN, AI_IMPACT_TABLE_METRICS, @@ -58,6 +63,8 @@ import { extractGraphqlVulnerabilitiesData, extractGraphqlDoraData, extractGraphqlFlowData, + extractGraphqlMergeRequestsData, + extractGraphqlContributorCountData, } from '../../api'; import { extractGraphqlAiData } from '../api'; @@ -110,10 +117,15 @@ export default { { metrics: SUPPORTED_DORA_METRICS, queryFn: this.fetchDoraMetricsQuery }, { metrics: SUPPORTED_FLOW_METRICS, queryFn: this.fetchFlowMetricsQuery }, { metrics: SUPPORTED_AI_METRICS, queryFn: this.fetchAiMetricsQuery }, + { metrics: SUPPORTED_MERGE_REQUEST_METRICS, queryFn: this.fetchMergeRequestsMetricsQuery }, { metrics: SUPPORTED_VULNERABILITY_METRICS, queryFn: this.fetchVulnerabilitiesMetricsQuery, }, + { + metrics: SUPPORTED_CONTRIBUTOR_METRICS, + queryFn: this.fetchContributorsCountQuery, + }, ].filter(({ metrics }) => !this.areAllMetricsSkipped(metrics)); }, duoRcaUsageRateEnabled() { @@ -244,6 +256,27 @@ export default { }; }, + async fetchMergeRequestsMetricsQuery({ startDate, endDate }, timePeriod) { + const result = await this.$apollo.query({ + query: MergeRequestsQuery, + variables: { + fullPath: this.namespace, + startDate: toYmd(startDate), + endDate: toYmd(endDate), + state: MERGE_REQUESTS_STATE_MERGED, + }, + }); + + const metrics = extractQueryResponseFromNamespace({ + result, + resultKey: 'mergeRequests', + }); + return { + ...timePeriod, + ...extractGraphqlMergeRequestsData(metrics || {}), + }; + }, + async fetchVulnerabilitiesMetricsQuery({ endDate }, timePeriod) { const result = await this.$apollo.query({ query: VulnerabilitiesQuery, @@ -269,6 +302,27 @@ export default { }; }, + async fetchContributorsCountQuery({ startDate, endDate }, timePeriod) { + const result = await this.$apollo.query({ + query: ContributorCountQuery, + variables: { + fullPath: this.namespace, + startDate: toYmd(startDate), + endDate: toYmd(endDate), + }, + }); + + const responseData = extractQueryResponseFromNamespace({ + result, + resultKey: 'contributors', + }); + + return { + ...timePeriod, + ...extractGraphqlContributorCountData(responseData || {}), + }; + }, + async fetchAiMetricsQuery({ startDate, endDate }, timePeriod) { const result = await this.$apollo.query({ query: AiMetricsQuery, diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js index 550e5b572b8a3c6b57b58770dae0adbd79687957..04a171129f9022df7deded0839b83a8b2157ee26 100644 --- a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js +++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js @@ -3,7 +3,9 @@ import { s__ } from '~/locale'; import { FLOW_METRICS, DORA_METRICS, + MERGE_REQUEST_METRICS, VULNERABILITY_METRICS, + CONTRIBUTOR_METRICS, AI_METRICS, UNITS, } from '~/analytics/shared/constants'; @@ -11,14 +13,20 @@ import { import { helpPagePath } from '~/helpers/help_page_helper'; import { TABLE_METRICS as VSD_TABLE_METRICS } from '../constants'; -export const SUPPORTED_FLOW_METRICS = [FLOW_METRICS.CYCLE_TIME, FLOW_METRICS.LEAD_TIME]; +export const SUPPORTED_FLOW_METRICS = [ + FLOW_METRICS.CYCLE_TIME, + FLOW_METRICS.LEAD_TIME, + FLOW_METRICS.MEDIAN_TIME_TO_MERGE, +]; export const SUPPORTED_DORA_METRICS = [ DORA_METRICS.DEPLOYMENT_FREQUENCY, DORA_METRICS.CHANGE_FAILURE_RATE, ]; +export const SUPPORTED_MERGE_REQUEST_METRICS = [MERGE_REQUEST_METRICS.THROUGHPUT]; export const SUPPORTED_VULNERABILITY_METRICS = [VULNERABILITY_METRICS.CRITICAL]; +export const SUPPORTED_CONTRIBUTOR_METRICS = [CONTRIBUTOR_METRICS.COUNT]; export const SUPPORTED_AI_METRICS = [ AI_METRICS.CODE_SUGGESTIONS_USAGE_RATE, @@ -65,7 +73,9 @@ export const AI_IMPACT_TABLE_METRICS = { ...pick(VSD_TABLE_METRICS, [ ...SUPPORTED_FLOW_METRICS, ...SUPPORTED_DORA_METRICS, + ...SUPPORTED_MERGE_REQUEST_METRICS, ...SUPPORTED_VULNERABILITY_METRICS, + ...SUPPORTED_CONTRIBUTOR_METRICS, ]), ...pick(AI_IMPACT_USAGE_METRICS, SUPPORTED_AI_METRICS), }; diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/graphql/flow_metrics.query.graphql b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/graphql/flow_metrics.query.graphql index 1bbfb133038156b220bdbdaee3ee135c71435649..7f4079f4d53eec76f6aa5fbf3db31457255540ad 100644 --- a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/graphql/flow_metrics.query.graphql +++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/graphql/flow_metrics.query.graphql @@ -10,6 +10,9 @@ query aiImpactFlowMetricsQuery($fullPath: ID!, $startDate: Time!, $endDate: Time lead_time: leadTime(from: $startDate, to: $endDate) { ...FlowMetricItem } + median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) { + ...FlowMetricItem + } } } group(fullPath: $fullPath) { @@ -21,6 +24,9 @@ query aiImpactFlowMetricsQuery($fullPath: ID!, $startDate: Time!, $endDate: Time lead_time: leadTime(from: $startDate, to: $endDate) { ...FlowMetricItem } + median_time_to_merge: timeToMerge(from: $startDate, to: $endDate) { + ...FlowMetricItem + } } } } diff --git a/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_ai_metrics_table.yaml b/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_ai_metrics_table.yaml index d3dce16e57d523f186686c497a55693d883bb8ee..786c4a2038186babebef8f9d464fc2955d98e367 100644 --- a/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_ai_metrics_table.yaml +++ b/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_ai_metrics_table.yaml @@ -11,4 +11,7 @@ data: - deployment_frequency - change_failure_rate - vulnerability_critical + - merge_request_throughput + - median_time_to_merge + - contributor_count options: {} diff --git a/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_lifecycle_metrics_table.yaml b/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_lifecycle_metrics_table.yaml index f86223a29edd9334adddc5382900d9bc2ce9a004..6f6efb9085c1d35f57b65446bab0a17a26592572 100644 --- a/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_lifecycle_metrics_table.yaml +++ b/ee/lib/gitlab/analytics/ai_impact_dashboard/visualizations/ai_impact_lifecycle_metrics_table.yaml @@ -6,6 +6,8 @@ data: query: filters: excludeMetrics: + - cycle_time + - change_failure_rate - code_suggestions_usage_rate - code_suggestions_acceptance_rate - duo_chat_usage_rate diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/__snapshots__/utils_spec.js.snap b/ee/spec/frontend/analytics/dashboards/ai_impact/__snapshots__/utils_spec.js.snap index 82ed4ce19c554dbfa959b6c8aaec39bc796c42ec..c50d47fcc420187f04820f5e2157245a267c9159 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/__snapshots__/utils_spec.js.snap +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/__snapshots__/utils_spec.js.snap @@ -157,6 +157,13 @@ exports[`AI impact Dashboard utils generateSkeletonTableData returns the skeleto "value": "Lead time", }, }, + { + "invertTrendColor": true, + "metric": { + "identifier": "median_time_to_merge", + "value": "Median time to merge", + }, + }, { "invertTrendColor": undefined, "metric": { @@ -171,6 +178,13 @@ exports[`AI impact Dashboard utils generateSkeletonTableData returns the skeleto "value": "Change failure rate", }, }, + { + "invertTrendColor": undefined, + "metric": { + "identifier": "merge_request_throughput", + "value": "Merge request throughput", + }, + }, { "invertTrendColor": true, "metric": { @@ -178,6 +192,13 @@ exports[`AI impact Dashboard utils generateSkeletonTableData returns the skeleto "value": "Critical vulnerabilities over time", }, }, + { + "invertTrendColor": undefined, + "metric": { + "identifier": "contributor_count", + "value": "Contributor count", + }, + }, { "invertTrendColor": undefined, "metric": { diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/components/__snapshots__/metric_table_spec.js.snap b/ee/spec/frontend/analytics/dashboards/ai_impact/components/__snapshots__/metric_table_spec.js.snap index 194d2604a8f682e758c4021cb9f25a1eae09368f..1e36cac72b0de6d9a4f4fb17e9b5d727cf5ccea6 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/components/__snapshots__/metric_table_spec.js.snap +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/components/__snapshots__/metric_table_spec.js.snap @@ -186,6 +186,67 @@ exports[`Metric table for the code_suggestions_usage_rate table row when the dat } `; +exports[`Metric table for the contributor_count table row when the data is loaded renders the metric values 1`] = ` +"Contributor count + Number of monthly unique users with contributions. + + Go to docs + + 4 + + 4 + + 4 + + 4 + + 4 + + 4 + + 0.0%" +`; + +exports[`Metric table for the contributor_count table row when the data is loaded renders the sparkline chart with expected props 1`] = ` +{ + "connectNulls": true, + "data": [ + [ + "Jan 6 - Feb 6", + 4, + ], + [ + "Feb 6 - Mar 6", + 4, + ], + [ + "Mar 6 - Apr 6", + 4, + ], + [ + "Apr 6 - May 6", + 4, + ], + [ + "May 6 - Jun 6", + 4, + ], + [ + "Jun 6 - Jul 6", + 4, + ], + ], + "gradient": [ + "#499767", + "#5252B5", + ], + "height": 30, + "showLastYValue": false, + "smooth": 0.2, + "tooltipLabel": "", +} +`; + exports[`Metric table for the cycle_time table row when the data is loaded renders the metric values 1`] = ` "Cycle time Median time between when an issue is first referenced in the commit message of a merge request and when that referenced issue is closed. @@ -496,6 +557,129 @@ exports[`Metric table for the lead_time table row when the data is loaded render } `; +exports[`Metric table for the median_time_to_merge table row when the data is loaded renders the metric values 1`] = ` +"Median time to merge + Median time between merge request created and merge request merged. + + Go to docs + + 0.1 d + + 0.2 d + + 0.3 d + + 0.3 d + + 0.2 d + + 0.1 d + + 100.0% + days" +`; + +exports[`Metric table for the median_time_to_merge table row when the data is loaded renders the sparkline chart with expected props 1`] = ` +{ + "connectNulls": true, + "data": [ + [ + "Jan 6 - Feb 6", + 0.1, + ], + [ + "Feb 6 - Mar 6", + 0.2, + ], + [ + "Mar 6 - Apr 6", + 0.3, + ], + [ + "Apr 6 - May 6", + 0.3, + ], + [ + "May 6 - Jun 6", + 0.2, + ], + [ + "Jun 6 - Jul 6", + 0.1, + ], + ], + "gradient": [ + "#5252B5", + "#499767", + ], + "height": 30, + "showLastYValue": false, + "smooth": 0.2, + "tooltipLabel": "days", +} +`; + +exports[`Metric table for the merge_request_throughput table row when the data is loaded renders the metric values 1`] = ` +"Merge request throughput + Number of merge requests merged by month. + + Go to docs + + 10 + + 10 + + 10 + + 10 + + 10 + + 10 + + 0.0%" +`; + +exports[`Metric table for the merge_request_throughput table row when the data is loaded renders the sparkline chart with expected props 1`] = ` +{ + "connectNulls": true, + "data": [ + [ + "Jan 6 - Feb 6", + 10, + ], + [ + "Feb 6 - Mar 6", + 10, + ], + [ + "Mar 6 - Apr 6", + 10, + ], + [ + "Apr 6 - May 6", + 10, + ], + [ + "May 6 - Jun 6", + 10, + ], + [ + "Jun 6 - Jul 6", + 10, + ], + ], + "gradient": [ + "#499767", + "#5252B5", + ], + "height": 30, + "showLastYValue": false, + "smooth": 0.2, + "tooltipLabel": "", +} +`; + exports[`Metric table for the vulnerability_critical table row when the data is loaded renders the metric values 1`] = ` "Critical vulnerabilities over time Number of critical vulnerabilities identified per month. diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js b/ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js index e6066067d214275d841510850ed9a2f0fcab2e50..a42469482762878beeb83a97bf53e0045e46760e 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/components/metric_table_spec.js @@ -6,6 +6,8 @@ import { FLOW_METRICS, DORA_METRICS, VULNERABILITY_METRICS, + MERGE_REQUEST_METRICS, + CONTRIBUTOR_METRICS, AI_METRICS, } from '~/analytics/shared/constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -19,18 +21,27 @@ import { import FlowMetricsQuery from 'ee/analytics/dashboards/ai_impact/graphql/flow_metrics.query.graphql'; import DoraMetricsQuery from 'ee/analytics/dashboards/ai_impact/graphql/dora_metrics.query.graphql'; import VulnerabilitiesQuery from 'ee/analytics/dashboards/ai_impact/graphql/vulnerabilities.query.graphql'; +import MergeRequestsQuery from 'ee/analytics/dashboards/graphql/merge_requests.query.graphql'; +import ContributorCountQuery from 'ee/analytics/dashboards/graphql/contributor_count.query.graphql'; import AiMetricsQuery from 'ee/analytics/dashboards/ai_impact/graphql/ai_metrics.query.graphql'; import MetricTable from 'ee/analytics/dashboards/ai_impact/components/metric_table.vue'; import { SUPPORTED_FLOW_METRICS, SUPPORTED_DORA_METRICS, SUPPORTED_VULNERABILITY_METRICS, + SUPPORTED_MERGE_REQUEST_METRICS, + SUPPORTED_CONTRIBUTOR_METRICS, SUPPORTED_AI_METRICS, } from 'ee/analytics/dashboards/ai_impact/constants'; import MetricTableCell from 'ee/analytics/dashboards/components/metric_table_cell.vue'; import TrendIndicator from 'ee/analytics/dashboards/components/trend_indicator.vue'; import { setLanguage } from 'jest/__helpers__/locale_helper'; import { AI_IMPACT_TABLE_TRACKING_PROPERTY } from 'ee/analytics/analytics_dashboards/constants'; +import { + mockGraphqlMergeRequestsResponse, + mockGraphqlContributorCountResponse, +} from '../../helpers'; +import { mockMergeRequestsResponseData, mockContributorCountResponseData } from '../../mock_data'; import { mockDoraMetricsResponse, mockFlowMetricsResponse, @@ -66,6 +77,10 @@ describe('Metric table', () => { flowMetricsRequest = mockFlowMetricsResponse(mockTableAndChartValues), doraMetricsRequest = mockDoraMetricsResponse(mockTableAndChartValues), vulnerabilityMetricsRequest = mockVulnerabilityMetricsResponse(mockTableAndChartValues), + mrMetricsRequest = mockGraphqlMergeRequestsResponse(mockMergeRequestsResponseData), + contributorMetricsRequest = mockGraphqlContributorCountResponse( + mockContributorCountResponseData, + ), aiMetricsRequest = mockAiMetricsResponse(mockTableAndChartValues), } = {}) => { return createMockApollo( @@ -73,6 +88,8 @@ describe('Metric table', () => { [FlowMetricsQuery, flowMetricsRequest], [DoraMetricsQuery, doraMetricsRequest], [VulnerabilitiesQuery, vulnerabilityMetricsRequest], + [MergeRequestsQuery, mrMetricsRequest], + [ContributorCountQuery, contributorMetricsRequest], [AiMetricsQuery, aiMetricsRequest], ], {}, @@ -86,6 +103,10 @@ describe('Metric table', () => { flowMetricsRequest = mockFlowMetricsResponse(mockTableLargeValues), doraMetricsRequest = mockDoraMetricsResponse(mockTableLargeValues), vulnerabilityMetricsRequest = mockVulnerabilityMetricsResponse(mockTableLargeValues), + mrMetricsRequest = mockGraphqlMergeRequestsResponse(mockMergeRequestsResponseData), + contributorMetricsRequest = mockGraphqlContributorCountResponse( + mockContributorCountResponseData, + ), aiMetricsRequest = mockAiMetricsResponse(mockTableLargeValues), } = {}) => { return createMockApollo( @@ -93,6 +114,8 @@ describe('Metric table', () => { [FlowMetricsQuery, flowMetricsRequest], [DoraMetricsQuery, doraMetricsRequest], [VulnerabilitiesQuery, vulnerabilityMetricsRequest], + [MergeRequestsQuery, mrMetricsRequest], + [ContributorCountQuery, contributorMetricsRequest], [AiMetricsQuery, aiMetricsRequest], ], {}, @@ -137,7 +160,10 @@ describe('Metric table', () => { const changeFailureRateTestId = 'ai-impact-metric-change-failure-rate'; const cycleTimeTestId = 'ai-impact-metric-cycle-time'; const leadTimeTestId = 'ai-impact-metric-lead-time'; + const medianTimeToMergeTestId = 'ai-impact-metric-median-time-to-merge'; const vulnerabilityCriticalTestId = 'ai-impact-metric-vulnerability-critical'; + const mergeRequestThroughputTestId = 'ai-impact-metric-merge-request-throughput'; + const contributorCountTestId = 'ai-impact-metric-contributor-count'; const codeSuggestionsUsageRateTestId = 'ai-impact-metric-code-suggestions-usage-rate'; const codeSuggestionsAcceptanceRateTestId = 'ai-impact-metric-code-suggestions-acceptance-rate'; const duoChatUsageRateTestId = 'ai-impact-metric-duo-chat-usage-rate'; @@ -171,7 +197,10 @@ describe('Metric table', () => { ${DORA_METRICS.CHANGE_FAILURE_RATE} | ${changeFailureRateTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} ${FLOW_METRICS.CYCLE_TIME} | ${cycleTimeTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} ${FLOW_METRICS.LEAD_TIME} | ${leadTimeTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} + ${FLOW_METRICS.MEDIAN_TIME_TO_MERGE} | ${medianTimeToMergeTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} ${VULNERABILITY_METRICS.CRITICAL} | ${vulnerabilityCriticalTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} + ${MERGE_REQUEST_METRICS.THROUGHPUT} | ${mergeRequestThroughputTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} + ${CONTRIBUTOR_METRICS.COUNT} | ${contributorCountTestId} | ${namespace} | ${AI_IMPACT_TABLE_TRACKING_PROPERTY} ${AI_METRICS.CODE_SUGGESTIONS_USAGE_RATE} | ${codeSuggestionsUsageRateTestId} | ${''} | ${''} ${AI_METRICS.CODE_SUGGESTIONS_ACCEPTANCE_RATE} | ${codeSuggestionsAcceptanceRateTestId} | ${''} | ${''} ${AI_METRICS.DUO_CHAT_USAGE_RATE} | ${duoChatUsageRateTestId} | ${''} | ${''} @@ -194,7 +223,10 @@ describe('Metric table', () => { ${DORA_METRICS.CHANGE_FAILURE_RATE} | ${'Change failure rate'} | ${changeFailureRateTestId} ${FLOW_METRICS.CYCLE_TIME} | ${'Cycle time'} | ${cycleTimeTestId} ${FLOW_METRICS.LEAD_TIME} | ${'Lead time'} | ${leadTimeTestId} + ${FLOW_METRICS.MEDIAN_TIME_TO_MERGE} | ${'Median time to merge'} | ${medianTimeToMergeTestId} ${VULNERABILITY_METRICS.CRITICAL} | ${'Critical vulnerabilities over time'} | ${vulnerabilityCriticalTestId} + ${MERGE_REQUEST_METRICS.THROUGHPUT} | ${'Merge request throughput'} | ${mergeRequestThroughputTestId} + ${CONTRIBUTOR_METRICS.COUNT} | ${'Contributor count'} | ${contributorCountTestId} ${AI_METRICS.CODE_SUGGESTIONS_USAGE_RATE} | ${'Code Suggestions usage'} | ${codeSuggestionsUsageRateTestId} ${AI_METRICS.CODE_SUGGESTIONS_ACCEPTANCE_RATE} | ${'Code Suggestions acceptance rate'} | ${codeSuggestionsAcceptanceRateTestId} ${AI_METRICS.DUO_CHAT_USAGE_RATE} | ${'Duo Chat: Unique users'} | ${duoChatUsageRateTestId} @@ -223,6 +255,8 @@ describe('Metric table', () => { flowMetricsRequest: jest.fn().mockRejectedValue({}), doraMetricsRequest: jest.fn().mockRejectedValue({}), vulnerabilityMetricsRequest: jest.fn().mockRejectedValue({}), + mrMetricsRequest: jest.fn().mockRejectedValue({}), + contributorMetricsRequest: jest.fn().mockRejectedValue({}), aiMetricsRequest: jest.fn().mockRejectedValue({}), }), }); @@ -380,6 +414,8 @@ describe('Metric table', () => { const flowMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); const doraMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); const vulnerabilityMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); + const mrMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); + const contributorMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); const aiMetricsRequest = jest.fn().mockImplementation(() => Promise.resolve()); let apolloProvider; @@ -388,6 +424,8 @@ describe('Metric table', () => { flowMetricsRequest, doraMetricsRequest, vulnerabilityMetricsRequest, + mrMetricsRequest, + contributorMetricsRequest, aiMetricsRequest, }); }); @@ -402,7 +440,7 @@ describe('Metric table', () => { { group: 'Flow metrics', excludeMetrics: SUPPORTED_FLOW_METRICS, - testIds: [cycleTimeTestId, leadTimeTestId], + testIds: [cycleTimeTestId, leadTimeTestId, medianTimeToMergeTestId], apiRequest: flowMetricsRequest, }, { @@ -411,6 +449,18 @@ describe('Metric table', () => { testIds: [vulnerabilityCriticalTestId], apiRequest: vulnerabilityMetricsRequest, }, + { + group: 'MR metrics', + excludeMetrics: SUPPORTED_MERGE_REQUEST_METRICS, + testIds: [mergeRequestThroughputTestId], + apiRequest: mrMetricsRequest, + }, + { + group: 'Contribution metrics', + excludeMetrics: SUPPORTED_CONTRIBUTOR_METRICS, + testIds: [contributorCountTestId], + apiRequest: contributorMetricsRequest, + }, { group: 'AI metrics', excludeMetrics: SUPPORTED_AI_METRICS, diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/helpers.js b/ee/spec/frontend/analytics/dashboards/ai_impact/helpers.js index 9e5371e5a24f03f3b3738a4edb4c90fa045d3c2a..6b6392c636ce2d241cd7fa4bd750deed8d141265 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/helpers.js +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/helpers.js @@ -25,7 +25,7 @@ export const mockDoraMetricsResponse = (values = []) => export const mockFlowMetricsResponse = (values = []) => values.reduce( - (acc, { cycleTime, leadTime }) => + (acc, { cycleTime, leadTime, medianTimeToMerge }) => acc.mockResolvedValueOnce({ data: { project: null, @@ -63,6 +63,29 @@ export const mockFlowMetricsResponse = (values = []) => title: 'Lead time', __typename: 'ValueStreamAnalyticsMetric', }, + median_time_to_merge: { + unit: 'days', + value: medianTimeToMerge, + identifier: 'median_time_to_merge', + links: [ + { + label: 'Dashboard', + name: 'Median time to merge', + docsLink: null, + url: '/groups/test-graphql-dora/-/issues_analytics', + __typename: 'ValueStreamMetricLinkType', + }, + { + label: 'Go to docs', + name: 'Median time to merge', + docsLink: true, + url: '/help/user/analytics/index#definitions', + __typename: 'ValueStreamMetricLinkType', + }, + ], + title: 'Median time to merge', + __typename: 'ValueStreamAnalyticsMetric', + }, __typename: 'GroupValueStreamAnalyticsFlowMetrics', }, }, diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/mock_data.js b/ee/spec/frontend/analytics/dashboards/ai_impact/mock_data.js index 0560102d2c057f7f1cdf81e9a1b67dc225fec9c8..155bbf9a33b20ed120ab1c9db07151be60a8d148 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/mock_data.js +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/mock_data.js @@ -153,6 +153,7 @@ const mockTableRow = ( changeFailureRate, cycleTime, leadTime, + medianTimeToMerge, criticalVulnerabilities, [codeSuggestionsContributorsCount, codeContributorsCount], [codeSuggestionsAcceptedCount, codeSuggestionsShownCount], @@ -166,6 +167,7 @@ const mockTableRow = ( changeFailureRate, cycleTime, leadTime, + medianTimeToMerge, criticalVulnerabilities, codeSuggestionsContributorsCount, codeContributorsCount, @@ -178,39 +180,39 @@ const mockTableRow = ( }); export const mockTableValues = [ - mockTableRow(10, 0.2, 1, 1, 40, [1, 20], [3, 15], 3, 5, 15, 100), - mockTableRow(20, 0.4, 2, 2, 20, [1, 10], [3, 7], 3, 6, 7, 100), - mockTableRow(40, 0.6, 4, 4, 10, [1, 4], [10, 15], 10, 11, 15, 100), - mockTableRow(10, 0.2, 1, 1, 40, [1, 20], [9, 18], 9, 12, 18, 100), - mockTableRow(20, 0.4, 2, 2, 20, [1, 10], [3, 17], 3, 7, 17, 100), - mockTableRow(40, 0.6, 4, 4, 10, [1, 4], [4, 12], 4, 6, 12, 100), + mockTableRow(10, 0.2, 1, 1, 0.1, 40, [1, 20], [3, 15], 3, 5, 15, 100), + mockTableRow(20, 0.4, 2, 2, 0.2, 20, [1, 10], [3, 7], 3, 6, 7, 100), + mockTableRow(40, 0.6, 4, 4, 0.3, 10, [1, 4], [10, 15], 10, 11, 15, 100), + mockTableRow(10, 0.2, 1, 1, 0.3, 40, [1, 20], [9, 18], 9, 12, 18, 100), + mockTableRow(20, 0.4, 2, 2, 0.2, 20, [1, 10], [3, 17], 3, 7, 17, 100), + mockTableRow(40, 0.6, 4, 4, 0.1, 10, [1, 4], [4, 12], 4, 6, 12, 100), ]; export const mockTableLargeValues = [ - mockTableRow(10000, 0.1, 4, 0, 4000, [500, 1000], [800, 2000], 800, 1000, 2000, 10000), - mockTableRow(20000, 0.2, 2, 2, 2000, [1000, 2000], [1000, 1500], 1000, 1200, 1500, 10000), - mockTableRow(40000, 0.4, 1, 4, 1000, [2500, 5000], [1200, 2400], 1200, 2000, 2400, 10000), - mockTableRow(10000, 0.1, 4, 1, 4000, [5000, 10000], [2000, 6000], 2000, 4000, 6000, 10000), - mockTableRow(20000, 0.2, 2, 2, 2000, [1000, 2000], [8000, 9000], 8000, 7000, 9000, 10000), - mockTableRow(40, 0.4, 1, 4, 5000, [2500, 5000], [7000, 8500], 7000, 8000, 8500, 10000), + mockTableRow(10000, 0.1, 4, 0, 10, 4000, [500, 1000], [800, 2000], 800, 1000, 2000, 10000), + mockTableRow(20000, 0.2, 2, 2, 20, 2000, [1000, 2000], [1000, 1500], 1000, 1200, 1500, 10000), + mockTableRow(40000, 0.4, 1, 4, 30, 1000, [2500, 5000], [1200, 2400], 1200, 2000, 2400, 10000), + mockTableRow(10000, 0.1, 4, 1, 30, 4000, [5000, 10000], [2000, 6000], 2000, 4000, 6000, 10000), + mockTableRow(20000, 0.2, 2, 2, 20, 2000, [1000, 2000], [8000, 9000], 8000, 7000, 9000, 10000), + mockTableRow(40, 0.4, 1, 4, 10, 5000, [2500, 5000], [7000, 8500], 7000, 8000, 8500, 10000), ]; export const mockTableBlankValues = [ - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), - mockTableRow('-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), + mockTableRow('-', '-', '-', '-', '-', '-', ['-', '-'], ['-', '-'], '-', '-', '-', '-'), ]; export const mockTableZeroValues = [ - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), - mockTableRow(0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), + mockTableRow(0, 0, 0, 0, 0, 0, [0, 0], [0, 0], 0, 0, 0, 0), ]; export const mockTableAndChartValues = [...mockTableValues, ...mockTableValues]; diff --git a/ee/spec/frontend/analytics/dashboards/ai_impact/utils_spec.js b/ee/spec/frontend/analytics/dashboards/ai_impact/utils_spec.js index 89debcdbf4261c0ee81f33a2c7cce97aed1cccc0..311ff8ee1b948535eef117ff8c05ae7beec92c5f 100644 --- a/ee/spec/frontend/analytics/dashboards/ai_impact/utils_spec.js +++ b/ee/spec/frontend/analytics/dashboards/ai_impact/utils_spec.js @@ -130,7 +130,7 @@ describe('AI impact Dashboard utils', () => { ['no error', []], ]), ).toEqual([ - `${errors}: Cycle time, Lead time`, + `${errors}: Cycle time, Lead time, Median time to merge`, `${warnings}: Deployment frequency, Change failure rate`, ]); });