Commit 15b30c75 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '346395-add-snowplow-metrics-for-failing-masked-variable-validation' into 'master'

Add snowplow metrics for failing masked variable validation

See merge request gitlab-org/gitlab!75255
parents 96a73b86 471b4e8c
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Tracking from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { mapComputed } from '~/vuex_shared/bindings'; import { mapComputed } from '~/vuex_shared/bindings';
import { import {
...@@ -25,10 +26,14 @@ import { ...@@ -25,10 +26,14 @@ import {
AWS_TIP_DISMISSED_COOKIE_NAME, AWS_TIP_DISMISSED_COOKIE_NAME,
AWS_TIP_MESSAGE, AWS_TIP_MESSAGE,
CONTAINS_VARIABLE_REFERENCE_MESSAGE, CONTAINS_VARIABLE_REFERENCE_MESSAGE,
EVENT_LABEL,
EVENT_ACTION,
} from '../constants'; } from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
export default { export default {
modalId: ADD_CI_VARIABLE_MODAL_ID, modalId: ADD_CI_VARIABLE_MODAL_ID,
tokens: awsTokens, tokens: awsTokens,
...@@ -51,10 +56,11 @@ export default { ...@@ -51,10 +56,11 @@ export default {
GlModal, GlModal,
GlSprintf, GlSprintf,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin(), trackingMixin],
data() { data() {
return { return {
isTipDismissed: Cookies.get(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', isTipDismissed: Cookies.get(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
validationErrorEventProperty: '',
}; };
}, },
computed: { computed: {
...@@ -147,6 +153,14 @@ export default { ...@@ -147,6 +153,14 @@ export default {
return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState); return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
}, },
}, },
watch: {
variable: {
handler() {
this.trackVariableValidationErrors();
},
deep: true,
},
},
methods: { methods: {
...mapActions([ ...mapActions([
'addVariable', 'addVariable',
...@@ -179,6 +193,7 @@ export default { ...@@ -179,6 +193,7 @@ export default {
this.clearModal(); this.clearModal();
this.resetSelectedEnvironment(); this.resetSelectedEnvironment();
this.resetValidationErrorEvents();
}, },
updateOrAddVariable() { updateOrAddVariable() {
if (this.variableBeingEdited) { if (this.variableBeingEdited) {
...@@ -193,6 +208,31 @@ export default { ...@@ -193,6 +208,31 @@ export default {
this.setVariableProtected(); this.setVariableProtected();
} }
}, },
trackVariableValidationErrors() {
const property = this.getTrackingErrorProperty();
if (!this.validationErrorEventProperty && property) {
this.track(EVENT_ACTION, { property });
this.validationErrorEventProperty = property;
}
},
getTrackingErrorProperty() {
let property;
if (this.variable.secret_value?.length && !property) {
if (this.displayMaskedError && this.maskableRegex?.length) {
const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
const regex = new RegExp(supportedChars, 'g');
property = this.variable.secret_value.replace(regex, '');
}
if (this.containsVariableReference) {
property = '$';
}
}
return property;
},
resetValidationErrorEvents() {
this.validationErrorEventProperty = '';
},
}, },
}; };
</script> </script>
......
...@@ -19,6 +19,9 @@ export const AWS_TIP_MESSAGE = __( ...@@ -19,6 +19,9 @@ export const AWS_TIP_MESSAGE = __(
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.', '%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',
); );
export const EVENT_LABEL = 'ci_variable_modal';
export const EVENT_ACTION = 'validation_error';
// AWS TOKEN CONSTANTS // AWS TOKEN CONSTANTS
export const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID'; export const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
export const AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION'; export const AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION';
......
import { GlButton, GlFormInput } from '@gitlab/ui'; import { GlButton, GlFormInput } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import { AWS_ACCESS_KEY_ID, EVENT_LABEL, EVENT_ACTION } from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store'; import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data'; import mockData from '../services/mock_data';
import ModalStub from '../stubs'; import ModalStub from '../stubs';
...@@ -14,9 +15,12 @@ localVue.use(Vuex); ...@@ -14,9 +15,12 @@ localVue.use(Vuex);
describe('Ci variable modal', () => { describe('Ci variable modal', () => {
let wrapper; let wrapper;
let store; let store;
let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
const createComponent = (method, options = {}) => { const createComponent = (method, options = {}) => {
store = createStore({ isGroup: options.isGroup }); store = createStore({ maskableRegex, isGroup: options.isGroup });
wrapper = method(CiVariableModal, { wrapper = method(CiVariableModal, {
attachTo: document.body, attachTo: document.body,
stubs: { stubs: {
...@@ -138,6 +142,7 @@ describe('Ci variable modal', () => { ...@@ -138,6 +142,7 @@ describe('Ci variable modal', () => {
}; };
createComponent(mount); createComponent(mount);
store.state.variable = invalidKeyVariable; store.state.variable = invalidKeyVariable;
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
}); });
it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => { it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => {
...@@ -226,6 +231,7 @@ describe('Ci variable modal', () => { ...@@ -226,6 +231,7 @@ describe('Ci variable modal', () => {
}; };
createComponent(mount); createComponent(mount);
store.state.variable = invalidMaskVariable; store.state.variable = invalidMaskVariable;
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
}); });
it('disables the submit button', () => { it('disables the submit button', () => {
...@@ -235,6 +241,50 @@ describe('Ci variable modal', () => { ...@@ -235,6 +241,50 @@ describe('Ci variable modal', () => {
it('shows the correct error text', () => { it('shows the correct error text', () => {
expect(findModal().text()).toContain(maskError); expect(findModal().text()).toContain(maskError);
}); });
it('sends the correct tracking event', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
label: EVENT_LABEL,
property: ';',
});
});
});
describe.each`
value | secret | masked | eventSent | trackingErrorProperty
${'value'} | ${'secretValue'} | ${false} | ${0} | ${null}
${'shortMasked'} | ${'short'} | ${true} | ${0} | ${null}
${'withDollar$Sign'} | ${'dollar$ign'} | ${false} | ${1} | ${'$'}
${'withDollar$Sign'} | ${'dollar$ign'} | ${true} | ${1} | ${'$'}
${'unsupported'} | ${'unsupported|char'} | ${true} | ${1} | ${'|'}
${'unsupportedMasked'} | ${'unsupported|char'} | ${false} | ${0} | ${null}
`('Adding a new variable', ({ value, secret, masked, eventSent, trackingErrorProperty }) => {
beforeEach(() => {
const [variable] = mockData.mockVariables;
const invalidKeyVariable = {
...variable,
key: 'key',
value,
secret_value: secret,
masked,
};
createComponent(mount);
store.state.variable = invalidKeyVariable;
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it(`${
eventSent > 0 ? 'sends the correct' : 'does not send the'
} variable validation tracking event`, () => {
expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
if (eventSent > 0) {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
label: EVENT_LABEL,
property: trackingErrorProperty,
});
}
});
}); });
describe('when both states are valid', () => { describe('when both states are valid', () => {
...@@ -249,7 +299,6 @@ describe('Ci variable modal', () => { ...@@ -249,7 +299,6 @@ describe('Ci variable modal', () => {
}; };
createComponent(mount); createComponent(mount);
store.state.variable = validMaskandKeyVariable; store.state.variable = validMaskandKeyVariable;
store.state.maskableRegex = /^[a-zA-Z0-9_+=/@:.~-]{8,}$/;
}); });
it('does not disable the submit button', () => { it('does not disable the submit button', () => {
......
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