Commit f24ef52f authored by Mark Florian's avatar Mark Florian

Merge branch '291748-group-level-deployment-frequency-ci-cd-chart' into 'master'

Update deployment frequency graphs to support group-level data

See merge request gitlab-org/gitlab!60945
parents 5f2ccc89 a113cdc7
......@@ -8,11 +8,27 @@ export const ALL_METRIC_TYPES = Object.freeze([
LEAD_TIME_FOR_CHANGES,
]);
const PROJECTS_DORA_METRICS_PATH = '/api/:version/projects/:id/dora/metrics';
export const PROJECTS_DORA_METRICS_PATH = '/api/:version/projects/:id/dora/metrics';
export const GROUPS_DORA_METRICS_PATH = '/api/:version/groups/:id/dora/metrics';
function getDoraMetrics(apiUrl, projectOrGroupId, metric, params) {
if (!ALL_METRIC_TYPES.includes(metric)) {
throw new Error(`Unsupported metric type: "${metric}"`);
}
const url = buildApiUrl(apiUrl).replace(':id', encodeURIComponent(projectOrGroupId));
return axios.get(url, {
params: {
metric,
...params,
},
});
}
/**
* Gets DORA 4 metrics data from a project
* See https://docs.gitlab.com/ee/api/dora/metrics.html
* See https://docs.gitlab.com/ee/api/dora/metrics.html#get-project-level-dora-metrics
*
* @param {String|Number} projectId The ID or path of the project
* @param {String} metric The name of the metric to fetch. Must be one of:
......@@ -24,16 +40,22 @@ const PROJECTS_DORA_METRICS_PATH = '/api/:version/projects/:id/dora/metrics';
* @returns {Promise} A `Promise` that resolves to an array of data points.
*/
export function getProjectDoraMetrics(projectId, metric, params = {}) {
if (!ALL_METRIC_TYPES.includes(metric)) {
throw new Error(`Unsupported metric type provided to getProjectDoraMetrics(): "${metric}"`);
}
const url = buildApiUrl(PROJECTS_DORA_METRICS_PATH).replace(':id', encodeURIComponent(projectId));
return getDoraMetrics(PROJECTS_DORA_METRICS_PATH, projectId, metric, params);
}
return axios.get(url, {
params: {
metric,
...params,
},
});
/**
* Gets DORA 4 metrics data from a group
* See https://docs.gitlab.com/ee/api/dora/metrics.html#get-group-level-dora-metrics
*
* @param {String|Number} groupId The ID or path of the group
* @param {String} metric The name of the metric to fetch. Must be one of:
* `["deployment_frequency", "lead_time_for_changes"]`
* @param {Object} params Any additional query parameters that should be
* included with the request. These parameters are optional. See
* https://docs.gitlab.com/ee/api/dora/metrics.html for a list of available options.
*
* @returns {Promise} A `Promise` that resolves to an array of data points.
*/
export function getGroupDoraMetrics(groupId, metric, params = {}) {
return getDoraMetrics(GROUPS_DORA_METRICS_PATH, groupId, metric, params);
}
......@@ -28,6 +28,10 @@ export default {
type: String,
default: '',
},
groupPath: {
type: String,
default: '',
},
},
data() {
return {
......@@ -49,11 +53,28 @@ export default {
async mounted() {
const results = await Promise.allSettled(
allChartDefinitions.map(async ({ id, requestParams, startDate, endDate }) => {
const { data: apiData } = await DoraApi.getProjectDoraMetrics(
let apiData;
if (this.projectPath && this.groupPath) {
throw new Error('Both projectPath and groupPath were provided');
} else if (this.projectPath) {
apiData = (
await DoraApi.getProjectDoraMetrics(
this.projectPath,
DoraApi.DEPLOYMENT_FREQUENCY_METRIC_TYPE,
requestParams,
);
)
).data;
} else if (this.groupPath) {
apiData = (
await DoraApi.getGroupDoraMetrics(
this.groupPath,
DoraApi.DEPLOYMENT_FREQUENCY_METRIC_TYPE,
requestParams,
)
).data;
} else {
throw new Error('Either projectPath or groupPath must be provided');
}
this.chartData[id] = apiDataToChartSeries(apiData, startDate, endDate, CHART_TITLE);
}),
......
import * as DoraApi from 'ee/api/dora_api';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/axios_utils', () => ({
get: jest.fn(),
}));
describe('dora_api.js', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
};
let originalGon;
beforeEach(() => {
originalGon = window.gon;
window.gon = { ...dummyGon };
});
afterEach(() => {
window.gon = originalGon;
});
describe.each`
functionName | baseUrl
${'getProjectDoraMetrics'} | ${`${dummyUrlRoot}/api/${dummyApiVersion}/projects`}
${'getGroupDoraMetrics'} | ${`${dummyUrlRoot}/api/${dummyApiVersion}/groups`}
`('$functionName', ({ functionName, baseUrl }) => {
it.each`
id | metric | params | url
${1} | ${DoraApi.DEPLOYMENT_FREQUENCY_METRIC_TYPE} | ${undefined} | ${`${baseUrl}/1/dora/metrics`}
${1} | ${DoraApi.LEAD_TIME_FOR_CHANGES} | ${undefined} | ${`${baseUrl}/1/dora/metrics`}
${1} | ${DoraApi.DEPLOYMENT_FREQUENCY_METRIC_TYPE} | ${{ another: 'param' }} | ${`${baseUrl}/1/dora/metrics`}
${'name with spaces'} | ${DoraApi.DEPLOYMENT_FREQUENCY_METRIC_TYPE} | ${undefined} | ${`${baseUrl}/name%20with%20spaces/dora/metrics`}
`(`makes a call to $url with the correct params`, ({ id, metric, params, url }) => {
DoraApi[functionName](id, metric, params);
expect(axios.get.mock.calls).toEqual([
[
url,
{
params: {
metric,
...params,
},
},
],
]);
});
it('throws an error when an invalid metric type is provided', () => {
const callFunction = () => DoraApi[functionName](1, 'invalid_metric_type');
expect(callFunction).toThrowError('Unsupported metric type: "invalid_metric_type"');
expect(axios.get).not.toHaveBeenCalled();
});
});
});
......@@ -36,13 +36,14 @@ describe('deployment_frequency_charts.vue', () => {
let wrapper;
let mock;
const createComponent = () => {
wrapper = shallowMount(DeploymentFrequencyCharts, {
const defaultMountOptions = {
provide: {
projectPath: 'test/project',
},
});
};
const createComponent = (mountOptions = defaultMountOptions) => {
wrapper = shallowMount(DeploymentFrequencyCharts, mountOptions);
};
// Initializes the mock endpoint to return a specific set of deployment
......@@ -143,4 +144,86 @@ describe('deployment_frequency_charts.vue', () => {
expect(captureExceptionSpy).toHaveBeenCalledWith(new Error(expectedErrorMessage));
});
});
describe('group/project behavior', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/projects\/test%2Fproject\/dora\/metrics/).reply(httpStatus.OK, lastWeekData);
mock.onGet(/groups\/test%2Fgroup\/dora\/metrics/).reply(httpStatus.OK, lastWeekData);
});
describe('when projectPath is provided', () => {
beforeEach(async () => {
createComponent({
provide: {
projectPath: 'test/project',
},
});
await axios.waitForAll();
});
it('makes a call to the project API endpoint', () => {
expect(mock.history.get.length).toBe(3);
expect(mock.history.get[0].url).toMatch('/projects/test%2Fproject/dora/metrics');
});
it('does not throw an error', () => {
expect(createFlash).not.toHaveBeenCalled();
});
});
describe('when groupPath is provided', () => {
beforeEach(async () => {
createComponent({
provide: {
groupPath: 'test/group',
},
});
await axios.waitForAll();
});
it('makes a call to the group API endpoint', () => {
expect(mock.history.get.length).toBe(3);
expect(mock.history.get[0].url).toMatch('/groups/test%2Fgroup/dora/metrics');
});
it('does not throw an error', () => {
expect(createFlash).not.toHaveBeenCalled();
});
});
describe('when both projectPath and groupPath are provided', () => {
beforeEach(async () => {
createComponent({
provide: {
projectPath: 'test/project',
groupPath: 'test/group',
},
});
await axios.waitForAll();
});
it('throws an error (which shows a flash message)', () => {
expect(createFlash).toHaveBeenCalled();
});
});
describe('when neither projectPath nor groupPath are provided', () => {
beforeEach(async () => {
createComponent({
provide: {},
});
await axios.waitForAll();
});
it('throws an error (which shows a flash message)', () => {
expect(createFlash).toHaveBeenCalled();
});
});
});
});
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