Commit c20c01c5 authored by Chad Woolley's avatar Chad Woolley

Add snowplow analytics for projects/groups dropdowns

See https://gitlab.com/gitlab-org/gitlab/-/issues/275968
parent cdd241d2
...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor'; import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import store from '../store';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants'; import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils'; import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue'; import FrequentItemsSearchInput from './frequent_items_search_input.vue';
...@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue'; ...@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
export default { export default {
store,
components: { components: {
FrequentItemsSearchInput, FrequentItemsSearchInput,
FrequentItemsList, FrequentItemsList,
......
<script> <script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */ /* eslint-disable vue/require-default-prop, vue/no-v-html */
import { mapState } from 'vuex';
import Identicon from '~/vue_shared/components/identicon.vue'; import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight'; import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility'; import { truncateNamespace } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default { export default {
components: { components: {
Identicon, Identicon,
}, },
mixins: [trackingMixin],
props: { props: {
matcher: { matcher: {
type: String, type: String,
...@@ -37,6 +42,7 @@ export default { ...@@ -37,6 +42,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['dropdownType']),
truncatedNamespace() { truncatedNamespace() {
return truncateNamespace(this.namespace); return truncateNamespace(this.namespace);
}, },
...@@ -49,7 +55,11 @@ export default { ...@@ -49,7 +55,11 @@ export default {
<template> <template>
<li class="frequent-items-list-item-container"> <li class="frequent-items-list-item-container">
<a :href="webUrl" class="clearfix"> <a
:href="webUrl"
class="clearfix"
@click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })"
>
<div <div
ref="frequentItemsItemAvatarContainer" ref="frequentItemsItemAvatarContainer"
class="frequent-items-item-avatar-container avatar-container rect-avatar s32" class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
......
<script> <script>
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin'; import frequentItemsMixin from './frequent_items_mixin';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default { export default {
components: { components: {
GlIcon, GlIcon,
}, },
mixins: [frequentItemsMixin], mixins: [frequentItemsMixin, trackingMixin],
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
}; };
}, },
computed: { computed: {
...mapState(['dropdownType']),
translations() { translations() {
return this.getTranslations(['searchInputPlaceholder']); return this.getTranslations(['searchInputPlaceholder']);
}, },
}, },
watch: { watch: {
searchQuery: debounce(function debounceSearchQuery() { searchQuery: debounce(function debounceSearchQuery() {
this.track('type_search_query', {
label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
});
this.setSearchQuery(this.searchQuery); this.setSearchQuery(this.searchQuery);
}, 500), }, 500),
}, },
......
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { createStore } from '~/frequent_items/store';
Vue.use(Translate); Vue.use(Translate);
...@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() { ...@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() {
return; return;
} }
const dropdownType = namespace;
const store = createStore({ dropdownType });
import('./components/app.vue') import('./components/app.vue')
.then(({ default: FrequentItems }) => { .then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
store,
data() { data() {
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
const item = { const item = {
......
...@@ -7,10 +7,11 @@ import state from './state'; ...@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex); Vue.use(Vuex);
export default () => export const createStore = (initState = {}) => {
new Vuex.Store({ return new Vuex.Store({
actions, actions,
getters, getters,
mutations, mutations,
state: state(), state: state(initState),
}); });
};
export default () => ({ export default ({ dropdownType = '' } = {}) => ({
namespace: '', namespace: '',
dropdownType,
storageKey: '', storageKey: '',
searchQuery: '', searchQuery: '',
isLoadingItems: false, isLoadingItems: false,
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar .frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul %ul
= nav_link(path: 'dashboard/groups#index') do = nav_link(path: 'dashboard/groups#index') do
= link_to dashboard_groups_path, class: 'qa-your-groups-link' do = link_to dashboard_groups_path, class: 'qa-your-groups-link', data: { track_label: "groups_dropdown_your_groups", track_event: "click_link" } do
= _('Your groups') = _('Your groups')
= nav_link(path: 'groups#explore') do = nav_link(path: 'groups#explore') do
= link_to explore_groups_path do = link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
= _('Explore groups') = _('Explore groups')
.frequent-items-dropdown-content .frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } } #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar .frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul %ul
= nav_link(path: 'dashboard/projects#index') do = nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path, class: 'qa-your-projects-link' do = link_to dashboard_projects_path, class: 'qa-your-projects-link', data: { track_label: "projects_dropdown_your_projects", track_event: "click_link" } do
= _('Your projects') = _('Your projects')
= nav_link(path: 'projects#starred') do = nav_link(path: 'projects#starred') do
= link_to starred_dashboard_projects_path do = link_to starred_dashboard_projects_path, data: { track_label: "projects_dropdown_starred_projects", track_event: "click_link" } do
= _('Starred projects') = _('Starred projects')
= nav_link(path: 'projects#trending') do = nav_link(path: 'projects#trending') do
= link_to explore_root_path do = link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects') = _('Explore projects')
.frequent-items-dropdown-content .frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } } #js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
...@@ -6,10 +6,10 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -6,10 +6,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import appComponent from '~/frequent_items/components/app.vue'; import appComponent from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub'; import eventHub from '~/frequent_items/event_hub';
import store from '~/frequent_items/store';
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants'; import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
import { getTopFrequentItems } from '~/frequent_items/utils'; import { getTopFrequentItems } from '~/frequent_items/utils';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data'; import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
import { createStore } from '~/frequent_items/store';
useLocalStorageSpy(); useLocalStorageSpy();
...@@ -18,6 +18,7 @@ const createComponentWithStore = (namespace = 'projects') => { ...@@ -18,6 +18,7 @@ const createComponentWithStore = (namespace = 'projects') => {
session = currentSession[namespace]; session = currentSession[namespace];
gon.api_version = session.apiVersion; gon.api_version = session.apiVersion;
const Component = Vue.extend(appComponent); const Component = Vue.extend(appComponent);
const store = createStore();
return mountComponentWithStore(Component, { return mountComponentWithStore(Component, {
store, store,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here import { createStore } from '~/frequent_items/store';
import { mockProject } from '../mock_data';
describe('FrequentItemsListItemComponent', () => { describe('FrequentItemsListItemComponent', () => {
let wrapper; let wrapper;
let trackingSpy;
let store = createStore();
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' }); const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' }); const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' });
...@@ -18,6 +22,7 @@ describe('FrequentItemsListItemComponent', () => { ...@@ -18,6 +22,7 @@ describe('FrequentItemsListItemComponent', () => {
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(frequentItemsListItemComponent, { wrapper = shallowMount(frequentItemsListItemComponent, {
store,
propsData: { propsData: {
itemId: mockProject.id, itemId: mockProject.id,
itemName: mockProject.name, itemName: mockProject.name,
...@@ -29,7 +34,14 @@ describe('FrequentItemsListItemComponent', () => { ...@@ -29,7 +34,14 @@ describe('FrequentItemsListItemComponent', () => {
}); });
}; };
beforeEach(() => {
store = createStore({ dropdownType: 'project' });
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
});
afterEach(() => { afterEach(() => {
unmockTracking();
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
...@@ -97,5 +109,18 @@ describe('FrequentItemsListItemComponent', () => { ...@@ -97,5 +109,18 @@ describe('FrequentItemsListItemComponent', () => {
`('should render $expected $name', ({ selector, expected }) => { `('should render $expected $name', ({ selector, expected }) => {
expect(selector()).toHaveLength(expected); expect(selector()).toHaveLength(expected);
}); });
it('tracks when item link is clicked', () => {
const link = wrapper.find('a');
// NOTE: this listener is required to prevent the click from going through and causing:
// `Error: Not implemented: navigation ...`
link.element.addEventListener('click', e => {
e.preventDefault();
});
link.trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
label: 'project_dropdown_frequent_items_list_item',
});
});
}); });
}); });
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { createStore } from '~/frequent_items/store';
import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue'; import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { mockFrequentProjects } from '../mock_data'; import { mockFrequentProjects } from '../mock_data';
...@@ -8,6 +9,7 @@ describe('FrequentItemsListComponent', () => { ...@@ -8,6 +9,7 @@ describe('FrequentItemsListComponent', () => {
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = mount(frequentItemsListComponent, { wrapper = mount(frequentItemsListComponent, {
store: createStore(),
propsData: { propsData: {
namespace: 'projects', namespace: 'projects',
items: mockFrequentProjects, items: mockFrequentProjects,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import { createStore } from '~/frequent_items/store';
import eventHub from '~/frequent_items/event_hub'; import eventHub from '~/frequent_items/event_hub';
const createComponent = (namespace = 'projects') =>
shallowMount(searchComponent, {
propsData: { namespace },
});
describe('FrequentItemsSearchInputComponent', () => { describe('FrequentItemsSearchInputComponent', () => {
let wrapper; let wrapper;
let trackingSpy;
let vm; let vm;
let store;
const createComponent = (namespace = 'projects') =>
shallowMount(searchComponent, {
store,
propsData: { namespace },
});
beforeEach(() => { beforeEach(() => {
store = createStore({ dropdownType: 'project' });
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
wrapper = createComponent(); wrapper = createComponent();
({ vm } = wrapper); ({ vm } = wrapper);
}); });
afterEach(() => { afterEach(() => {
unmockTracking();
vm.$destroy(); vm.$destroy();
}); });
...@@ -76,4 +88,24 @@ describe('FrequentItemsSearchInputComponent', () => { ...@@ -76,4 +88,24 @@ describe('FrequentItemsSearchInputComponent', () => {
); );
}); });
}); });
describe('tracking', () => {
it('tracks when search query is entered', async () => {
expect(trackingSpy).not.toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalled();
const value = 'my project';
const input = wrapper.find('input');
input.setValue(value);
input.trigger('input');
await wrapper.vm.$nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'type_search_query', {
label: 'project_dropdown_frequent_items_search_input',
});
expect(store.dispatch).toHaveBeenCalledWith('setSearchQuery', value);
});
});
}); });
...@@ -30,7 +30,6 @@ export const currentSession = { ...@@ -30,7 +30,6 @@ export const currentSession = {
}; };
export const mockNamespace = 'projects'; export const mockNamespace = 'projects';
export const mockStorageKey = 'test-user/frequent-projects'; export const mockStorageKey = 'test-user/frequent-projects';
export const mockGroup = { export const mockGroup = {
......
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