Commit febed011 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '218487-add-telemetry' into 'master'

Add Snowplow event for expanding security widget

See merge request gitlab-org/gitlab!35909
parents d6f7cbd1 52b06250
...@@ -91,6 +91,11 @@ export default { ...@@ -91,6 +91,11 @@ export default {
required: false, required: false,
default: undefined, default: undefined,
}, },
shouldEmitToggleEvent: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
...@@ -157,6 +162,9 @@ export default { ...@@ -157,6 +162,9 @@ export default {
}, },
methods: { methods: {
toggleCollapsed() { toggleCollapsed() {
if (this.shouldEmitToggleEvent) {
this.$emit('toggleEvent');
}
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
}, },
}, },
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { once } from 'lodash';
import { componentNames } from 'ee/reports/components/issue_body'; import { componentNames } from 'ee/reports/components/issue_body';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue'; import SummaryRow from '~/reports/components/summary_row.vue';
import Tracking from '~/tracking';
import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue'; import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import IssueModal from './components/modal.vue'; import IssueModal from './components/modal.vue';
...@@ -11,6 +13,7 @@ import securityReportsMixin from './mixins/security_report_mixin'; ...@@ -11,6 +13,7 @@ import securityReportsMixin from './mixins/security_report_mixin';
import createStore from './store'; import createStore from './store';
import { GlSprintf, GlLink } from '@gitlab/ui'; import { GlSprintf, GlLink } from '@gitlab/ui';
import { mrStates } from '~/mr_popover/constants'; import { mrStates } from '~/mr_popover/constants';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
export default { export default {
store: createStore(), store: createStore(),
...@@ -188,6 +191,12 @@ export default { ...@@ -188,6 +191,12 @@ export default {
dastScans() { dastScans() {
return this.dast.scans.filter(scan => scan.scanned_resources_count > 0); return this.dast.scans.filter(scan => scan.scanned_resources_count > 0);
}, },
handleToggleEvent() {
return once(() => {
const { category, action } = trackMrSecurityReportDetails;
Tracking.event(category, action);
});
},
}, },
created() { created() {
...@@ -284,8 +293,10 @@ export default { ...@@ -284,8 +293,10 @@ export default {
:loading-text="groupedSummaryText" :loading-text="groupedSummaryText"
:error-text="groupedSummaryText" :error-text="groupedSummaryText"
:has-issues="true" :has-issues="true"
:should-emit-toggle-event="true"
class="mr-widget-border-top grouped-security-reports mr-report" class="mr-widget-border-top grouped-security-reports mr-report"
data-qa-selector="vulnerability_report_grouped" data-qa-selector="vulnerability_report_grouped"
@toggleEvent="handleToggleEvent"
> >
<template v-if="pipelinePath" #actionButtons> <template v-if="pipelinePath" #actionButtons>
<div> <div>
......
export const LOADING = 'LOADING'; export const LOADING = 'LOADING';
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS'; export const SUCCESS = 'SUCCESS';
/**
* Tracks snowplow event when user views report details
*/
export const trackMrSecurityReportDetails = {
category: 'Vulnerability_Management',
action: 'mr_report_inline_details',
};
...@@ -12,6 +12,9 @@ import { trimText } from 'helpers/text_helper'; ...@@ -12,6 +12,9 @@ import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mrStates } from '~/mr_popover/constants'; import { mrStates } from '~/mr_popover/constants';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import ReportSection from '~/reports/components/report_section.vue';
import { import {
sastDiffSuccessMock, sastDiffSuccessMock,
...@@ -32,6 +35,8 @@ describe('Grouped security reports app', () => { ...@@ -32,6 +35,8 @@ describe('Grouped security reports app', () => {
let wrapper; let wrapper;
let mock; let mock;
const findReportSection = () => wrapper.find(ReportSection);
const props = { const props = {
headBlobPath: 'path', headBlobPath: 'path',
baseBlobPath: 'path', baseBlobPath: 'path',
...@@ -531,4 +536,44 @@ describe('Grouped security reports app', () => { ...@@ -531,4 +536,44 @@ describe('Grouped security reports app', () => {
}); });
}); });
}); });
describe('track report section expansion using Snowplow', () => {
let trackingSpy;
const { category, action } = trackMrSecurityReportDetails;
beforeEach(() => {
createWrapper(props);
trackingSpy = mockTracking(category, wrapper.vm.$el, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('tracks an event when toggled', () => {
expect(trackingSpy).not.toHaveBeenCalled();
findReportSection().vm.$emit('toggleEvent');
return wrapper.vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalledWith(category, action);
});
});
it('tracks an event only the first time it is toggled', () => {
const report = findReportSection();
expect(trackingSpy).not.toHaveBeenCalled();
report.vm.$emit('toggleEvent');
return wrapper.vm
.$nextTick()
.then(() => {
expect(trackingSpy).toHaveBeenCalledWith(category, action);
expect(trackingSpy).toHaveBeenCalledTimes(1);
report.vm.$emit('toggleEvent');
})
.then(wrapper.vm.$nextTick())
.then(() => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import reportSection from '~/reports/components/report_section.vue'; import reportSection from '~/reports/components/report_section.vue';
describe('Report section', () => { describe('Report section', () => {
let vm; let vm;
let wrapper;
const ReportSection = Vue.extend(reportSection); const ReportSection = Vue.extend(reportSection);
const resolvedIssues = [ const resolvedIssues = [
...@@ -16,22 +18,41 @@ describe('Report section', () => { ...@@ -16,22 +18,41 @@ describe('Report section', () => {
}, },
]; ];
const defaultProps = {
component: '',
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues,
hasIssues: false,
alwaysOpen: false,
};
const createComponent = props => {
wrapper = shallowMount(reportSection, {
propsData: {
...defaultProps,
...props,
},
});
return wrapper;
};
afterEach(() => { afterEach(() => {
vm.$destroy(); if (vm) {
vm.$destroy();
vm = null;
}
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
describe('computed', () => { describe('computed', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, defaultProps);
component: '',
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues,
hasIssues: false,
alwaysOpen: false,
});
}); });
describe('isCollapsible', () => { describe('isCollapsible', () => {
...@@ -105,12 +126,7 @@ describe('Report section', () => { ...@@ -105,12 +126,7 @@ describe('Report section', () => {
describe('with success status', () => { describe('with success status', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
component: '', ...defaultProps,
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues,
hasIssues: true, hasIssues: true,
}); });
}); });
...@@ -160,6 +176,50 @@ describe('Report section', () => { ...@@ -160,6 +176,50 @@ describe('Report section', () => {
}); });
}); });
describe('snowplow events', () => {
it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', done => {
createComponent({ hasIssues: true, shouldEmitToggleEvent: true });
expect(wrapper.emitted().toggleEvent).toBeUndefined();
wrapper.vm.$el.querySelector('button').click();
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.emitted().toggleEvent).toHaveLength(1);
})
.then(done)
.catch(done.fail);
});
it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', done => {
createComponent({ hasIssues: true });
expect(wrapper.emitted().toggleEvent).toBeUndefined();
wrapper.vm.$el.querySelector('button').click();
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.emitted().toggleEvent).toBeUndefined();
})
.then(done)
.catch(done.fail);
});
it('does not emit an event if always-open is set to true', done => {
createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true });
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.emitted().toggleEvent).toBeUndefined();
})
.then(done)
.catch(done.fail);
});
});
describe('with failed request', () => { describe('with failed request', () => {
it('should render error indicator', () => { it('should render error indicator', () => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
...@@ -199,7 +259,7 @@ describe('Report section', () => { ...@@ -199,7 +259,7 @@ describe('Report section', () => {
}); });
describe('Success and Error slots', () => { describe('Success and Error slots', () => {
const createComponent = status => { const createComponentWithSlots = status => {
vm = mountComponentWithSlots(ReportSection, { vm = mountComponentWithSlots(ReportSection, {
props: { props: {
status, status,
...@@ -214,7 +274,7 @@ describe('Report section', () => { ...@@ -214,7 +274,7 @@ describe('Report section', () => {
}; };
it('only renders success slot when status is "SUCCESS"', () => { it('only renders success slot when status is "SUCCESS"', () => {
createComponent('SUCCESS'); createComponentWithSlots('SUCCESS');
expect(vm.$el.textContent.trim()).toContain('This is a success'); expect(vm.$el.textContent.trim()).toContain('This is a success');
expect(vm.$el.textContent.trim()).not.toContain('This is an error'); expect(vm.$el.textContent.trim()).not.toContain('This is an error');
...@@ -222,7 +282,7 @@ describe('Report section', () => { ...@@ -222,7 +282,7 @@ describe('Report section', () => {
}); });
it('only renders error slot when status is "ERROR"', () => { it('only renders error slot when status is "ERROR"', () => {
createComponent('ERROR'); createComponentWithSlots('ERROR');
expect(vm.$el.textContent.trim()).toContain('This is an error'); expect(vm.$el.textContent.trim()).toContain('This is an error');
expect(vm.$el.textContent.trim()).not.toContain('This is a success'); expect(vm.$el.textContent.trim()).not.toContain('This is a success');
...@@ -230,7 +290,7 @@ describe('Report section', () => { ...@@ -230,7 +290,7 @@ describe('Report section', () => {
}); });
it('only renders loading slot when status is "LOADING"', () => { it('only renders loading slot when status is "LOADING"', () => {
createComponent('LOADING'); createComponentWithSlots('LOADING');
expect(vm.$el.textContent.trim()).toContain('This is loading'); expect(vm.$el.textContent.trim()).toContain('This is loading');
expect(vm.$el.textContent.trim()).not.toContain('This is an error'); expect(vm.$el.textContent.trim()).not.toContain('This is an error');
......
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