......@@ -10,6 +10,7 @@ plugins:
- import
- "@gitlab/i18n"
- "@gitlab/vue-i18n"
- no-jquery
settings:
import/resolver:
webpack:
......@@ -36,6 +37,11 @@ rules:
vue/no-use-v-if-with-v-for: off
vue/no-v-html: off
vue/use-v-on-exact: off
no-jquery/no-ajax: error
no-jquery/no-ajax-events: error
no-jquery/no-load: error
no-jquery/no-load-shorthand: error
no-jquery/no-serialize: error
overrides:
files:
- '**/spec/**/*'
......
......
......@@ -25,7 +25,7 @@ Then leave running while monitoring and performing some testing through web, api
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved)
## 2. Staging Trial
......@@ -41,7 +41,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 4. Production Server Version Check
......@@ -57,7 +57,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 6. Low Impact Check
......@@ -69,7 +69,7 @@ Then leave running while monitoring for at least **30 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 7. Mid Impact Trial
......@@ -81,7 +81,7 @@ Then leave running while monitoring for at least **12 hours** while performing s
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 8. Full Impact Trial
......@@ -93,7 +93,7 @@ Then leave running while monitoring for at least **1 week**.
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved)
#### Success?
......
......
......@@ -446,7 +446,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.58.0'
gem 'gitaly', '~> 1.65.0'
gem 'grpc', '~> 1.19.0'
......
......
......@@ -358,7 +358,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
gitaly (1.58.0)
gitaly (1.65.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-labkit (0.5.2)
......@@ -1168,7 +1168,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.58.0)
gitaly (~> 1.65.0)
github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
......
......
export const initSidebarTracking = () => {};
export const trackEvent = () => {};
// Noop function which has a EE counter-part
export default () => {};
......@@ -22,6 +22,7 @@ export default class FilterableList {
getPagePath() {
const action = this.filterForm.getAttribute('action');
// eslint-disable-next-line no-jquery/no-serialize
const params = $(this.filterForm).serialize();
return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`;
}
......
......
......@@ -42,6 +42,7 @@ export default class IntegrationSettingsForm {
// and test the service using provided configuration.
if (this.$form.get(0).checkValidity() && this.canTestService) {
e.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
this.testSettings(this.$form.serialize());
}
}
......
......
import Vue from 'vue';
import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
......@@ -9,9 +8,6 @@ export default function initIssueableApp() {
components: {
issuableApp,
},
mounted() {
initSidebarTracking();
},
render(createElement) {
return createElement('issuable-app', {
props: parseIssuableData(),
......
......
......@@ -314,6 +314,7 @@ document.addEventListener('DOMContentLoaded', () => {
const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
visitUrl(`${action}${$(this).serialize()}`);
});
......
......
......@@ -1461,6 +1461,7 @@ export default class Notes {
getFormData($form) {
const content = $form.find('.js-note-text').val();
return {
// eslint-disable-next-line no-jquery/no-serialize
formData: $form.serialize(),
formContent: _.escape(content),
formAction: $form.attr('action'),
......
......
......@@ -19,7 +19,9 @@ export default {
<gl-button
ref="button"
v-gl-tooltip
class="note-action-button js-note-action-reply"
class="note-action-button"
data-track-event="click_button"
data-track-label="reply_comment_button"
variant="transparent"
:title="__('Reply to comment')"
@click="$emit('startReplying')"
......
......
import Vue from 'vue';
import initNoteStats from 'ee_else_ce/event_tracking/notes';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import createStore from './stores';
......@@ -39,9 +38,6 @@ document.addEventListener('DOMContentLoaded', () => {
notesData: JSON.parse(notesDataset.notesData),
};
},
mounted() {
initNoteStats();
},
render(createElement) {
return createElement('notes-app', {
props: {
......
......
......@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines();
});
......@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
}).bindEvents();
initNotes();
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
initDiffNotes();
......
......
<script>
import { n__ } from '~/locale';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
name: 'AssigneeTitle',
......@@ -30,11 +29,6 @@ export default {
return n__('Assignee', `%d Assignees`, assignees);
},
},
methods: {
trackEdit() {
trackEvent('click_edit_button', 'assignee');
},
},
};
</script>
<template>
......@@ -45,7 +39,9 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
@click.prevent="trackEdit"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
>
{{ __('Edit') }}
</a>
......
......
......@@ -5,7 +5,6 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
......@@ -52,11 +51,6 @@ export default {
toggleForm() {
this.edit = !this.edit;
},
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'confidentiality');
},
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
......@@ -88,7 +82,10 @@ export default {
v-if="isEditable"
class="float-right confidential-edit"
href="#"
@click.prevent="onEditClick"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</a>
......
......
......@@ -6,7 +6,6 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
......@@ -66,11 +65,6 @@ export default {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'lock_issue');
},
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
......@@ -114,7 +108,10 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
type="button"
@click.prevent="onEditClick"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="lock_issue"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</button>
......
......
<script>
import { __ } from '~/locale';
import Tracking from '~/tracking';
import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
......@@ -19,6 +19,7 @@ export default {
icon,
toggleButton,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: {
loading: {
type: Boolean,
......@@ -65,7 +66,10 @@ export default {
// Component event emission.
this.$emit('toggleSubscription', this.id);
trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
this.track('toggle_button', {
property: 'notifications',
value: this.subscribed ? 0 : 1,
});
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
......
......
import $ from 'jquery';
import _ from 'underscore';
const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
......@@ -14,18 +14,31 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
linkClickTracking: false,
};
const extractData = (el, opts = {}) => {
const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset;
let trackValue = el.dataset.trackValue || el.value || '';
if (el.type === 'checkbox' && !el.checked) trackValue = false;
return [
trackEvent + (opts.suffix || ''),
{
label: trackLabel,
property: trackProperty,
value: trackValue,
},
];
const eventHandler = (e, func, opts = {}) => {
const el = e.target.closest('[data-track-event]');
const action = el && el.dataset.trackEvent;
if (!action) return;
let value = el.dataset.trackValue || el.value || undefined;
if (el.type === 'checkbox' && !el.checked) value = false;
const data = {
label: el.dataset.trackLabel,
property: el.dataset.trackProperty,
value,
context: el.dataset.trackContext,
};
func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined));
};
const eventHandlers = (category, func) => {
const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts });
const handlers = [];
handlers.push({ name: 'click', func: handler() });
handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
return handlers;
};
export default class Tracking {
......@@ -39,49 +52,43 @@ export default class Tracking {
return typeof window.snowplow === 'function' && this.trackable();
}
static event(category = document.body.dataset.page, event = 'generic', data = {}) {
static event(category = document.body.dataset.page, action = 'generic', data = {}) {
if (!this.enabled()) return false;
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.');
return window.snowplow(
'trackStructEvent',
category,
event,
Object.assign({}, { label: '', property: '', value: '' }, data),
);
const { label, property, value, context } = data;
const contexts = context ? [context] : undefined;
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
}
constructor(category = document.body.dataset.page) {
this.category = category;
}
bind(container = document) {
if (!this.constructor.enabled()) return;
container.querySelectorAll(`[data-track-event]`).forEach(el => {
if (this.customHandlingFor(el)) return;
// jquery is required for select2, so we use it always
// see: https://github.com/select2/select2/issues/4686
$(el).on('click', this.eventHandler(this.category));
});
}
static bindDocument(category = document.body.dataset.page, documentOverride = null) {
const el = documentOverride || document;
if (!this.enabled() || el.trackingBound) return [];
customHandlingFor(el) {
const classes = el.classList;
el.trackingBound = true;
// bootstrap dropdowns
if (classes.contains('dropdown')) {
$(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' }));
$(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' }));
return true;
}
return false;
const handlers = eventHandlers(category, (...args) => this.event(...args));
handlers.forEach(event => el.addEventListener(event.name, event.func));
return handlers;
}
eventHandler(category = null, opts = {}) {
return e => {
this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts));
static mixin(opts) {
return {
data() {
return {
tracking: {
// eslint-disable-next-line no-underscore-dangle
category: this.$options.name || this.$options._componentTag,
},
};
},
methods: {
track(action, data) {
const category = opts.category || data.category || this.tracking.category;
Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data });
},
},
};
}
}
......@@ -89,7 +96,7 @@ export default class Tracking {
export function initUserTracking() {
if (!Tracking.enabled()) return;
const opts = Object.assign({}, DEFAULT_SNOWPLOW_OPTIONS, window.snowplowOptions);
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
window.snowplow('enableActivityTracking', 30, 30);
......@@ -97,4 +104,6 @@ export function initUserTracking() {
if (opts.formTracking) window.snowplow('enableFormTracking');
if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
}