Commit 18f462ab authored by Alexander Turinske's avatar Alexander Turinske

Add Snowplow event for expanding security widget

- the defend/secure team wants to track how many users view the security
  report widget in merge requests
- added a new snowplow event to track the expansion of the widget
- ensured that it only tracked one event per page load
- add/update tests
parent 13c165bf
......@@ -91,6 +91,11 @@ export default {
required: false,
default: undefined,
},
shouldEmitToggleEvent: {
type: Boolean,
required: false,
default: false,
},
},
data() {
......@@ -157,6 +162,9 @@ export default {
},
methods: {
toggleCollapsed() {
if (this.shouldEmitToggleEvent) {
this.$emit('toggleEvent');
}
this.isCollapsed = !this.isCollapsed;
},
},
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { once } from 'lodash';
import { componentNames } from 'ee/reports/components/issue_body';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue';
import Tracking from '~/tracking';
import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
import Icon from '~/vue_shared/components/icon.vue';
import IssueModal from './components/modal.vue';
......@@ -11,6 +13,7 @@ import securityReportsMixin from './mixins/security_report_mixin';
import createStore from './store';
import { GlSprintf, GlLink } from '@gitlab/ui';
import { mrStates } from '~/mr_popover/constants';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
export default {
store: createStore(),
......@@ -188,6 +191,12 @@ export default {
dastScans() {
return this.dast.scans.filter(scan => scan.scanned_resources_count > 0);
},
handleToggleEvent() {
return once(() => {
const { category, action } = trackMrSecurityReportDetails;
Tracking.event(category, action);
});
},
},
created() {
......@@ -284,8 +293,10 @@ export default {
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="true"
:should-emit-toggle-event="true"
class="mr-widget-border-top grouped-security-reports mr-report"
data-qa-selector="vulnerability_report_grouped"
@toggleEvent="handleToggleEvent"
>
<template v-if="pipelinePath" #actionButtons>
<div>
......
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
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';
import axios from '~/lib/utils/axios_utils';
import { mrStates } from '~/mr_popover/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 {
sastDiffSuccessMock,
......@@ -32,6 +35,8 @@ describe('Grouped security reports app', () => {
let wrapper;
let mock;
const findReportSection = () => wrapper.find(ReportSection);
const props = {
headBlobPath: 'path',
baseBlobPath: 'path',
......@@ -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 { shallowMount } from '@vue/test-utils';
import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import reportSection from '~/reports/components/report_section.vue';
describe('Report section', () => {
let vm;
let wrapper;
const ReportSection = Vue.extend(reportSection);
const resolvedIssues = [
......@@ -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(() => {
vm.$destroy();
if (vm) {
vm.$destroy();
vm = null;
}
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('computed', () => {
beforeEach(() => {
vm = mountComponent(ReportSection, {
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,
});
vm = mountComponent(ReportSection, defaultProps);
});
describe('isCollapsible', () => {
......@@ -105,12 +126,7 @@ describe('Report section', () => {
describe('with success status', () => {
beforeEach(() => {
vm = mountComponent(ReportSection, {
component: '',
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues,
...defaultProps,
hasIssues: true,
});
});
......@@ -147,12 +163,46 @@ describe('Report section', () => {
.catch(done.fail);
});
it('emits 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('emits 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('is always expanded, if always-open is set to true', done => {
vm.alwaysOpen = true;
createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true });
Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
expect(vm.$el.querySelector('button')).toBeNull();
expect(wrapper.vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(
hiddenCss,
);
expect(wrapper.vm.$el.querySelector('button')).toBeNull();
expect(wrapper.emitted().toggleEvent).toBeUndefined();
})
.then(done)
.catch(done.fail);
......@@ -199,7 +249,7 @@ describe('Report section', () => {
});
describe('Success and Error slots', () => {
const createComponent = status => {
const createComponentWithSlots = status => {
vm = mountComponentWithSlots(ReportSection, {
props: {
status,
......@@ -214,7 +264,7 @@ describe('Report section', () => {
};
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()).not.toContain('This is an error');
......@@ -222,7 +272,7 @@ describe('Report section', () => {
});
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()).not.toContain('This is a success');
......@@ -230,7 +280,7 @@ describe('Report section', () => {
});
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()).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