466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import * as monitoringUtils from '~/monitoring/utils';
 | |
| import * as urlUtils from '~/lib/utils/url_utility';
 | |
| import { TEST_HOST } from 'jest/helpers/test_constants';
 | |
| import { mockProjectDir, barMockData } from './mock_data';
 | |
| import { singleStatGraphData, anomalyGraphData } from './graph_data';
 | |
| import { metricsDashboardViewModel, graphData } from './fixture_data';
 | |
| 
 | |
| const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
 | |
| 
 | |
| const generatedLink = 'http://chart.link.com';
 | |
| 
 | |
| const chartTitle = 'Some metric chart';
 | |
| 
 | |
| const range = {
 | |
|   start: '2019-01-01T00:00:00.000Z',
 | |
|   end: '2019-01-10T00:00:00.000Z',
 | |
| };
 | |
| 
 | |
| const rollingRange = {
 | |
|   duration: { seconds: 120 },
 | |
| };
 | |
| 
 | |
| describe('monitoring/utils', () => {
 | |
|   describe('trackGenerateLinkToChartEventOptions', () => {
 | |
|     it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
 | |
|       document.body.dataset.page = 'groups:clusters:show';
 | |
| 
 | |
|       expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
 | |
|         category: 'Cluster Monitoring',
 | |
|         action: 'generate_link_to_cluster_metric_chart',
 | |
|         label: 'Chart link',
 | |
|         property: generatedLink,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should return Incident Management event options if located on Metrics Dashboard', () => {
 | |
|       document.body.dataset.page = 'metrics:show';
 | |
| 
 | |
|       expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
 | |
|         category: 'Incident Management::Embedded metrics',
 | |
|         action: 'generate_link_to_metrics_chart',
 | |
|         label: 'Chart link',
 | |
|         property: generatedLink,
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('trackDownloadCSVEvent', () => {
 | |
|     it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
 | |
|       document.body.dataset.page = 'groups:clusters:show';
 | |
| 
 | |
|       expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
 | |
|         category: 'Cluster Monitoring',
 | |
|         action: 'download_csv_of_cluster_metric_chart',
 | |
|         label: 'Chart title',
 | |
|         property: chartTitle,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should return Incident Management event options if located on Metrics Dashboard', () => {
 | |
|       document.body.dataset.page = 'metriss:show';
 | |
| 
 | |
|       expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
 | |
|         category: 'Incident Management::Embedded metrics',
 | |
|         action: 'download_csv_of_metrics_dashboard_chart',
 | |
|         label: 'Chart title',
 | |
|         property: chartTitle,
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('graphDataValidatorForValues', () => {
 | |
|     /*
 | |
|      * When dealing with a metric using the query format, e.g.
 | |
|      * query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024'
 | |
|      * the validator will look for the `value` key instead of `values`
 | |
|      */
 | |
|     it('validates data with the query format', () => {
 | |
|       const validGraphData = monitoringUtils.graphDataValidatorForValues(
 | |
|         true,
 | |
|         singleStatGraphData(),
 | |
|       );
 | |
| 
 | |
|       expect(validGraphData).toBe(true);
 | |
|     });
 | |
| 
 | |
|     /*
 | |
|      * When dealing with a metric using the query?range format, e.g.
 | |
|      * query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job)  /1024/1024/1024',
 | |
|      * the validator will look for the `values` key instead of `value`
 | |
|      */
 | |
|     it('validates data with the query_range format', () => {
 | |
|       const validGraphData = monitoringUtils.graphDataValidatorForValues(false, graphData);
 | |
| 
 | |
|       expect(validGraphData).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('graphDataValidatorForAnomalyValues', () => {
 | |
|     let oneMetric;
 | |
|     let threeMetrics;
 | |
|     let fourMetrics;
 | |
|     beforeEach(() => {
 | |
|       oneMetric = singleStatGraphData();
 | |
|       threeMetrics = anomalyGraphData();
 | |
| 
 | |
|       const metrics = [...threeMetrics.metrics];
 | |
|       metrics.push(threeMetrics.metrics[0]);
 | |
|       fourMetrics = {
 | |
|         ...anomalyGraphData(),
 | |
|         metrics,
 | |
|       };
 | |
|     });
 | |
|     /*
 | |
|      * Anomaly charts can accept results for exactly 3 metrics,
 | |
|      */
 | |
|     it('validates passes with the right query format', () => {
 | |
|       expect(monitoringUtils.graphDataValidatorForAnomalyValues(threeMetrics)).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('validation fails for wrong format, 1 metric', () => {
 | |
|       expect(monitoringUtils.graphDataValidatorForAnomalyValues(oneMetric)).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('validation fails for wrong format, more than 3 metrics', () => {
 | |
|       expect(monitoringUtils.graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('timeRangeFromUrl', () => {
 | |
|     beforeEach(() => {
 | |
|       jest.spyOn(urlUtils, 'queryToObject');
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       urlUtils.queryToObject.mockRestore();
 | |
|     });
 | |
| 
 | |
|     const { timeRangeFromUrl } = monitoringUtils;
 | |
| 
 | |
|     it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
 | |
|       urlUtils.queryToObject.mockReturnValueOnce(range);
 | |
|       expect(timeRangeFromUrl()).toEqual(range);
 | |
|     });
 | |
| 
 | |
|     it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
 | |
|       const { seconds } = rollingRange.duration;
 | |
| 
 | |
|       urlUtils.queryToObject.mockReturnValueOnce({
 | |
|         dashboard: '.gitlab/dashboard/my_dashboard.yml',
 | |
|         duration_seconds: `${seconds}`,
 | |
|       });
 | |
| 
 | |
|       expect(timeRangeFromUrl()).toEqual(rollingRange);
 | |
|     });
 | |
| 
 | |
|     it('returns null when no time range parameters are given', () => {
 | |
|       urlUtils.queryToObject.mockReturnValueOnce({
 | |
|         dashboard: '.gitlab/dashboards/custom_dashboard.yml',
 | |
|         param1: 'value1',
 | |
|         param2: 'value2',
 | |
|       });
 | |
| 
 | |
|       expect(timeRangeFromUrl()).toBe(null);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('templatingVariablesFromUrl', () => {
 | |
|     const { templatingVariablesFromUrl } = monitoringUtils;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       jest.spyOn(urlUtils, 'queryToObject');
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       urlUtils.queryToObject.mockRestore();
 | |
|     });
 | |
| 
 | |
|     it('returns an object with only the custom variables', () => {
 | |
|       urlUtils.queryToObject.mockReturnValueOnce({
 | |
|         dashboard: '.gitlab/dashboards/custom_dashboard.yml',
 | |
|         y_label: 'memory usage',
 | |
|         group: 'kubernetes',
 | |
|         title: 'Kubernetes memory total',
 | |
|         start: '2020-05-06',
 | |
|         end: '2020-05-07',
 | |
|         duration_seconds: '86400',
 | |
|         direction: 'left',
 | |
|         anchor: 'top',
 | |
|         pod: 'POD',
 | |
|         'var-pod': 'POD',
 | |
|       });
 | |
| 
 | |
|       expect(templatingVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
 | |
|     });
 | |
| 
 | |
|     it('returns an empty object when no custom variables are present', () => {
 | |
|       urlUtils.queryToObject.mockReturnValueOnce({
 | |
|         dashboard: '.gitlab/dashboards/custom_dashboard.yml',
 | |
|       });
 | |
| 
 | |
|       expect(templatingVariablesFromUrl()).toStrictEqual({});
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('removeTimeRangeParams', () => {
 | |
|     const { removeTimeRangeParams } = monitoringUtils;
 | |
| 
 | |
|     it('returns when query contains `start` and `end` parameters are given', () => {
 | |
|       expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
 | |
|         mockPath,
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('timeRangeToUrl', () => {
 | |
|     const { timeRangeToUrl } = monitoringUtils;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       jest.spyOn(urlUtils, 'mergeUrlParams');
 | |
|       jest.spyOn(urlUtils, 'removeParams');
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       urlUtils.mergeUrlParams.mockRestore();
 | |
|       urlUtils.removeParams.mockRestore();
 | |
|     });
 | |
| 
 | |
|     it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
 | |
|       const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
 | |
|       const fromUrl = mockPath;
 | |
| 
 | |
|       urlUtils.removeParams.mockReturnValueOnce(fromUrl);
 | |
|       urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
 | |
| 
 | |
|       expect(timeRangeToUrl(range)).toEqual(toUrl);
 | |
|       expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
 | |
|     });
 | |
| 
 | |
|     it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
 | |
|       const { seconds } = rollingRange.duration;
 | |
| 
 | |
|       const toUrl = `${mockPath}?duration_seconds=${seconds}`;
 | |
|       const fromUrl = mockPath;
 | |
| 
 | |
|       urlUtils.removeParams.mockReturnValueOnce(fromUrl);
 | |
|       urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
 | |
| 
 | |
|       expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
 | |
|       expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(
 | |
|         { duration_seconds: `${seconds}` },
 | |
|         fromUrl,
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('expandedPanelPayloadFromUrl', () => {
 | |
|     const { expandedPanelPayloadFromUrl } = monitoringUtils;
 | |
|     const [panelGroup] = metricsDashboardViewModel.panelGroups;
 | |
|     const [panel] = panelGroup.panels;
 | |
| 
 | |
|     const { group } = panelGroup;
 | |
|     const { title, y_label: yLabel } = panel;
 | |
| 
 | |
|     it('returns payload for a panel when query parameters are given', () => {
 | |
|       const search = `?group=${group}&title=${title}&y_label=${yLabel}`;
 | |
| 
 | |
|       expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toEqual({
 | |
|         group: panelGroup.group,
 | |
|         panel,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('returns null when no parameters are given', () => {
 | |
|       expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, '')).toBe(null);
 | |
|     });
 | |
| 
 | |
|     it('throws an error when no group is provided', () => {
 | |
|       const search = `?title=${panel.title}&y_label=${yLabel}`;
 | |
|       expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
 | |
|     });
 | |
| 
 | |
|     it('throws an error when no title is provided', () => {
 | |
|       const search = `?title=${title}&y_label=${yLabel}`;
 | |
|       expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
 | |
|     });
 | |
| 
 | |
|     it('throws an error when no y_label group is provided', () => {
 | |
|       const search = `?group=${group}&title=${title}`;
 | |
|       expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
 | |
|     });
 | |
| 
 | |
|     test.each`
 | |
|       group            | title            | yLabel             | missingField
 | |
|       ${'NOT_A_GROUP'} | ${title}         | ${yLabel}          | ${'group'}
 | |
|       ${group}         | ${'NOT_A_TITLE'} | ${yLabel}          | ${'title'}
 | |
|       ${group}         | ${title}         | ${'NOT_A_Y_LABEL'} | ${'y_label'}
 | |
|     `('throws an error when $missingField is incorrect', params => {
 | |
|       const search = `?group=${params.group}&title=${params.title}&y_label=${params.yLabel}`;
 | |
|       expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('panelToUrl', () => {
 | |
|     const { panelToUrl } = monitoringUtils;
 | |
| 
 | |
|     const dashboard = 'metrics.yml';
 | |
|     const [panelGroup] = metricsDashboardViewModel.panelGroups;
 | |
|     const [panel] = panelGroup.panels;
 | |
| 
 | |
|     const getUrlParams = url => urlUtils.queryToObject(url.split('?')[1]);
 | |
| 
 | |
|     it('returns URL for a panel when query parameters are given', () => {
 | |
|       const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, panel));
 | |
| 
 | |
|       expect(params).toEqual(
 | |
|         expect.objectContaining({
 | |
|           dashboard,
 | |
|           group: panelGroup.group,
 | |
|           title: panel.title,
 | |
|           y_label: panel.y_label,
 | |
|         }),
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('returns a dashboard only URL if group is missing', () => {
 | |
|       const params = getUrlParams(panelToUrl(dashboard, {}, null, panel));
 | |
|       expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
 | |
|     });
 | |
| 
 | |
|     it('returns a dashboard only URL if panel is missing', () => {
 | |
|       const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, null));
 | |
|       expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
 | |
|     });
 | |
| 
 | |
|     it('returns URL for a panel when query paramters are given including custom variables', () => {
 | |
|       const params = getUrlParams(panelToUrl(dashboard, { pod: 'pod' }, panelGroup.group, null));
 | |
|       expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml', pod: 'pod' }));
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('barChartsDataParser', () => {
 | |
|     const singleMetricExpected = {
 | |
|       SLA: [
 | |
|         ['0.9935198135198128', 'api'],
 | |
|         ['0.9975296513504401', 'git'],
 | |
|         ['0.9994716394716395', 'registry'],
 | |
|         ['0.9948251748251747', 'sidekiq'],
 | |
|         ['0.9535664335664336', 'web'],
 | |
|         ['0.9335664335664336', 'postgresql_database'],
 | |
|       ],
 | |
|     };
 | |
| 
 | |
|     const multipleMetricExpected = {
 | |
|       ...singleMetricExpected,
 | |
|       SLA_2: Object.values(singleMetricExpected)[0],
 | |
|     };
 | |
| 
 | |
|     const barMockDataWithMultipleMetrics = {
 | |
|       ...barMockData,
 | |
|       metrics: [
 | |
|         barMockData.metrics[0],
 | |
|         {
 | |
|           ...barMockData.metrics[0],
 | |
|           label: 'SLA_2',
 | |
|         },
 | |
|       ],
 | |
|     };
 | |
| 
 | |
|     [
 | |
|       {
 | |
|         input: { metrics: undefined },
 | |
|         output: {},
 | |
|         testCase: 'barChartsDataParser returns {} with undefined',
 | |
|       },
 | |
|       {
 | |
|         input: { metrics: null },
 | |
|         output: {},
 | |
|         testCase: 'barChartsDataParser returns {} with null',
 | |
|       },
 | |
|       {
 | |
|         input: { metrics: [] },
 | |
|         output: {},
 | |
|         testCase: 'barChartsDataParser returns {} with []',
 | |
|       },
 | |
|       {
 | |
|         input: barMockData,
 | |
|         output: singleMetricExpected,
 | |
|         testCase: 'barChartsDataParser returns single series object with single metrics',
 | |
|       },
 | |
|       {
 | |
|         input: barMockDataWithMultipleMetrics,
 | |
|         output: multipleMetricExpected,
 | |
|         testCase: 'barChartsDataParser returns multiple series object with multiple metrics',
 | |
|       },
 | |
|     ].forEach(({ input, output, testCase }) => {
 | |
|       it(testCase, () => {
 | |
|         expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
 | |
|           expect.objectContaining(output),
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('removePrefixFromLabel', () => {
 | |
|     it.each`
 | |
|       input               | expected
 | |
|       ${undefined}        | ${''}
 | |
|       ${null}             | ${''}
 | |
|       ${''}               | ${''}
 | |
|       ${'    '}           | ${'    '}
 | |
|       ${'pod-1'}          | ${'pod-1'}
 | |
|       ${'pod-var-1'}      | ${'pod-var-1'}
 | |
|       ${'pod-1-var'}      | ${'pod-1-var'}
 | |
|       ${'podvar--1'}      | ${'podvar--1'}
 | |
|       ${'povar-d-1'}      | ${'povar-d-1'}
 | |
|       ${'var-pod-1'}      | ${'pod-1'}
 | |
|       ${'var-var-pod-1'}  | ${'var-pod-1'}
 | |
|       ${'varvar-pod-1'}   | ${'varvar-pod-1'}
 | |
|       ${'var-pod-1-var-'} | ${'pod-1-var-'}
 | |
|     `('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
 | |
|       expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('convertVariablesForURL', () => {
 | |
|     it.each`
 | |
|       input                                                               | expected
 | |
|       ${[]}                                                               | ${{}}
 | |
|       ${[{ name: 'env', value: 'prod' }]}                                 | ${{ 'var-env': 'prod' }}
 | |
|       ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }}
 | |
|       ${[{ name: 'var-env', value: 'prod' }]}                             | ${{ 'var-var-env': 'prod' }}
 | |
|     `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
 | |
|       expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('setCustomVariablesFromUrl', () => {
 | |
|     beforeEach(() => {
 | |
|       jest.spyOn(urlUtils, 'updateHistory');
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       urlUtils.updateHistory.mockRestore();
 | |
|     });
 | |
| 
 | |
|     it.each`
 | |
|       input                                                               | urlParams
 | |
|       ${[]}                                                               | ${''}
 | |
|       ${[{ name: 'env', value: 'prod' }]}                                 | ${'?var-env=prod'}
 | |
|       ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env=prod&var-env1=prod'}
 | |
|     `(
 | |
|       'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
 | |
|       ({ input, urlParams }) => {
 | |
|         monitoringUtils.setCustomVariablesFromUrl(input);
 | |
| 
 | |
|         expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
 | |
|         expect(urlUtils.updateHistory).toHaveBeenCalledWith({
 | |
|           url: `${TEST_HOST}/${urlParams}`,
 | |
|           title: '',
 | |
|         });
 | |
|       },
 | |
|     );
 | |
|   });
 | |
| });
 |