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';
import { GlLoadingIcon } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
......@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
export default {
store,
components: {
FrequentItemsSearchInput,
FrequentItemsList,
......
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
import { mapState } from 'vuex';
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default {
components: {
Identicon,
},
mixins: [trackingMixin],
props: {
matcher: {
type: String,
......@@ -37,6 +42,7 @@ export default {
},
},
computed: {
...mapState(['dropdownType']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
......@@ -49,7 +55,11 @@ export default {
<template>
<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
ref="frequentItemsItemAvatarContainer"
class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
......
<script>
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default {
components: {
GlIcon,
},
mixins: [frequentItemsMixin],
mixins: [frequentItemsMixin, trackingMixin],
data() {
return {
searchQuery: '',
};
},
computed: {
...mapState(['dropdownType']),
translations() {
return this.getTranslations(['searchInputPlaceholder']);
},
},
watch: {
searchQuery: debounce(function debounceSearchQuery() {
this.track('type_search_query', {
label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
});
this.setSearchQuery(this.searchQuery);
}, 500),
},
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import eventHub from './event_hub';
import { createStore } from '~/frequent_items/store';
Vue.use(Translate);
......@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() {
return;
}
const dropdownType = namespace;
const store = createStore({ dropdownType });
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
store,
data() {
const { dataset } = this.$options.el;
const item = {
......
......@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
export const createStore = (initState = {}) => {
return new Vuex.Store({
actions,
getters,
mutations,
state: state(),
state: state(initState),
});
};
export default () => ({
export default ({ dropdownType = '' } = {}) => ({
namespace: '',
dropdownType,
storageKey: '',
searchQuery: '',
isLoadingItems: false,
......
......@@ -3,10 +3,10 @@
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
= 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')
= 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')
.frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
......@@ -3,13 +3,13 @@
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= 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')
= 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')
= 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')
.frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
......@@ -6,10 +6,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import appComponent from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub';
import store from '~/frequent_items/store';
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
import { getTopFrequentItems } from '~/frequent_items/utils';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
import { createStore } from '~/frequent_items/store';
useLocalStorageSpy();
......@@ -18,6 +18,7 @@ const createComponentWithStore = (namespace = 'projects') => {
session = currentSession[namespace];
gon.api_version = session.apiVersion;
const Component = Vue.extend(appComponent);
const store = createStore();
return mountComponentWithStore(Component, {
store,
......
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trimText } from 'helpers/text_helper';
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', () => {
let wrapper;
let trackingSpy;
let store = createStore();
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' });
......@@ -18,6 +22,7 @@ describe('FrequentItemsListItemComponent', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(frequentItemsListItemComponent, {
store,
propsData: {
itemId: mockProject.id,
itemName: mockProject.name,
......@@ -29,7 +34,14 @@ describe('FrequentItemsListItemComponent', () => {
});
};
beforeEach(() => {
store = createStore({ dropdownType: 'project' });
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
});
afterEach(() => {
unmockTracking();
wrapper.destroy();
wrapper = null;
});
......@@ -97,5 +109,18 @@ describe('FrequentItemsListItemComponent', () => {
`('should render $expected $name', ({ selector, 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 { createStore } from '~/frequent_items/store';
import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { mockFrequentProjects } from '../mock_data';
......@@ -8,6 +9,7 @@ describe('FrequentItemsListComponent', () => {
const createComponent = (props = {}) => {
wrapper = mount(frequentItemsListComponent, {
store: createStore(),
propsData: {
namespace: 'projects',
items: mockFrequentProjects,
......
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 { createStore } from '~/frequent_items/store';
import eventHub from '~/frequent_items/event_hub';
const createComponent = (namespace = 'projects') =>
shallowMount(searchComponent, {
propsData: { namespace },
});
describe('FrequentItemsSearchInputComponent', () => {
let wrapper;
let trackingSpy;
let vm;
let store;
const createComponent = (namespace = 'projects') =>
shallowMount(searchComponent, {
store,
propsData: { namespace },
});
beforeEach(() => {
store = createStore({ dropdownType: 'project' });
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
wrapper = createComponent();
({ vm } = wrapper);
});
afterEach(() => {
unmockTracking();
vm.$destroy();
});
......@@ -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 = {
};
export const mockNamespace = 'projects';
export const mockStorageKey = 'test-user/frequent-projects';
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