Commit ca494e5d authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Track Snowplow events for Error tracking & Alert Management

parent bb6a848d
......@@ -18,10 +18,15 @@ import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_SEVERITY_LABELS } from '../constants';
import {
ALERTS_SEVERITY_LABELS,
trackAlertsDetailsViewsOptions,
trackAlertStatusUpdateOptions,
} from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
export default {
statuses: {
......@@ -108,6 +113,9 @@ export default {
return this.errored && !this.isErrorDismissed;
},
},
mounted() {
this.trackPageViews();
},
methods: {
dismissError() {
this.isErrorDismissed = true;
......@@ -122,6 +130,9 @@ export default {
projectPath: this.projectPath,
},
})
.then(() => {
this.trackStatusUpdate(status);
})
.catch(() => {
createFlash(
s__(
......@@ -157,6 +168,14 @@ export default {
issuePath(issueId) {
return joinPaths(this.projectIssuesPath, issueId);
},
trackPageViews() {
const { category, action } = trackAlertsDetailsViewsOptions;
Tracking.event(category, action);
},
trackStatusUpdate(status) {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
};
</script>
......
......@@ -19,9 +19,16 @@ import { fetchPolicies } from '~/lib/graphql';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
import {
ALERTS_STATUS,
ALERTS_STATUS_TABS,
ALERTS_SEVERITY_LABELS,
trackAlertListViewsOptions,
trackAlertStatusUpdateOptions,
} from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import { capitalizeFirstCharacter, convertToSnakeCase } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
const bodyTrClass =
......@@ -182,6 +189,7 @@ export default {
},
mounted() {
findDefaultSortColumn().ariaSort = 'ascending';
this.trackPageViews();
},
methods: {
filterAlertsByStatus(tabIndex) {
......@@ -208,6 +216,7 @@ export default {
},
})
.then(() => {
this.trackStatusUpdate(status);
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
})
......@@ -222,6 +231,14 @@ export default {
navigateToAlertDetails({ iid }) {
return visitUrl(joinPaths(window.location.pathname, iid, 'details'));
},
trackPageViews() {
const { category, action } = trackAlertListViewsOptions;
Tracking.event(category, action);
},
trackStatusUpdate(status) {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
};
</script>
......
......@@ -44,3 +44,30 @@ export const ALERTS_STATUS_TABS = [
filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED, ALERTS_STATUS.RESOLVED],
},
];
/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when user views alerts list
*/
export const trackAlertListViewsOptions = {
category: 'Alert Management',
action: 'view_alerts_list',
};
/**
* Tracks snowplow event when user views alert details
*/
export const trackAlertsDetailsViewsOptions = {
category: 'Alert Management',
action: 'view_alert_details',
};
/**
* Tracks snowplow event when alert status is updated
*/
export const trackAlertStatusUpdateOptions = {
category: 'Alert Management',
action: 'update_alert_status',
label: 'Status',
};
......@@ -20,8 +20,13 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
import { severityLevel, severityLevelVariant, errorStatus } from './constants';
import Tracking from '~/tracking';
import {
trackClickErrorLinkToSentryOptions,
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '../utils';
import query from '../queries/details.query.graphql';
......@@ -172,6 +177,7 @@ export default {
},
},
mounted() {
this.trackPageViews();
this.startPollingStacktrace(this.issueStackTracePath);
this.errorPollTimeout = Date.now() + SENTRY_TIMEOUT;
this.$apollo.queries.error.setOptions({
......@@ -194,7 +200,10 @@ export default {
onIgnoreStatusUpdate() {
const status =
this.errorStatus === errorStatus.IGNORED ? errorStatus.UNRESOLVED : errorStatus.IGNORED;
this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status });
// eslint-disable-next-line promise/catch-or-return
this.updateIgnoreStatus({ endpoint: this.issueUpdatePath, status }).then(() => {
this.trackStatusUpdate(status);
});
},
onResolveStatusUpdate() {
const status =
......@@ -206,6 +215,7 @@ export default {
if (this.closedIssueId) {
this.isAlertVisible = true;
}
this.trackStatusUpdate(status);
});
},
onNoApolloResult() {
......@@ -218,6 +228,14 @@ export default {
formatDate(date) {
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
trackPageViews() {
const { category, action } = trackErrorDetailsViewsOptions;
Tracking.event(category, action);
},
trackStatusUpdate(status) {
const { category, action, label } = trackErrorStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
};
</script>
......@@ -259,7 +277,7 @@ export default {
<div class="d-inline-flex bv-d-sm-down-none">
<gl-deprecated-button
:loading="updatingIgnoreStatus"
data-qa-selector="update_ignore_status_button"
data-testid="update-ignore-status-btn"
@click="onIgnoreStatusUpdate"
>
{{ ignoreBtnLabel }}
......@@ -267,7 +285,7 @@ export default {
<gl-deprecated-button
class="btn-outline-info ml-2"
:loading="updatingResolveStatus"
data-qa-selector="update_resolve_status_button"
data-testid="update-resolve-status-btn"
@click="onResolveStatusUpdate"
>
{{ resolveBtnLabel }}
......@@ -275,7 +293,7 @@ export default {
<gl-deprecated-button
v-if="error.gitlabIssuePath"
class="ml-2"
data-qa-selector="view_issue_button"
data-testid="view_issue_button"
:href="error.gitlabIssuePath"
variant="success"
>
......@@ -375,6 +393,7 @@ export default {
v-track-event="trackClickErrorLinkToSentryOptions(error.externalUrl)"
:href="error.externalUrl"
target="_blank"
data-testid="external-url-link"
>
<span class="text-truncate">{{ error.externalUrl }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
......
......@@ -19,6 +19,8 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import { isEmpty } from 'lodash';
import ErrorTrackingActions from './error_tracking_actions.vue';
import Tracking from '~/tracking';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
......@@ -150,6 +152,9 @@ export default {
this.startPolling();
}
},
mounted() {
this.trackPageViews();
},
methods: {
...mapActions('list', [
'startPolling',
......@@ -197,13 +202,25 @@ export default {
this.filterValue = label;
return this.filterByStatus(status);
},
updateIssueStatus({ errorId, status }) {
updateErrosStatus({ errorId, status }) {
// eslint-disable-next-line promise/catch-or-return
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
status,
}).then(() => {
this.trackStatusUpdate(status);
});
this.removeIgnoredResolvedErrors(errorId);
},
trackPageViews() {
const { category, action } = trackErrorListViewsOptions;
Tracking.event(category, action);
},
trackStatusUpdate(status) {
const { category, action, label } = trackErrorStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
};
</script>
......@@ -359,7 +376,7 @@ export default {
</div>
</template>
<template #cell(status)="errors">
<error-tracking-actions :error="errors.item" @update-issue-status="updateIssueStatus" />
<error-tracking-actions :error="errors.item" @update-issue-status="updateErrosStatus" />
</template>
<template #empty>
{{ __('No errors to display.') }}
......
/* eslint-disable @gitlab/require-i18n-strings, import/prefer-default-export */
/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when User clicks on error link to Sentry
......@@ -10,3 +10,28 @@ export const trackClickErrorLinkToSentryOptions = url => ({
label: 'Error Link',
property: url,
});
/**
* Tracks snowplow event when user views error list
*/
export const trackErrorListViewsOptions = {
category: 'Error Tracking',
action: 'view_errors_list',
};
/**
* Tracks snowplow event when user views error details
*/
export const trackErrorDetailsViewsOptions = {
category: 'Error Tracking',
action: 'view_error_details',
};
/**
* Tracks snowplow event when error status is updated
*/
export const trackErrorStatusUpdateOptions = {
category: 'Error Tracking',
action: 'update_error_status',
label: 'Status',
};
---
title: Monitor:Health metrics instrumenation
merge_request: 32846
author:
type: added
......@@ -5,6 +5,11 @@ import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert
import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql';
import createFlash from '~/flash';
import { joinPaths } from '~/lib/utils/url_utility';
import {
trackAlertsDetailsViewsOptions,
trackAlertStatusUpdateOptions,
} from '~/alert_management/constants';
import Tracking from '~/tracking';
import mockAlerts from '../mocks/alerts.json';
......@@ -253,7 +258,7 @@ describe('AlertDetails', () => {
});
});
describe('updating the alert status', () => {
describe('Updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
......@@ -298,4 +303,31 @@ describe('AlertDetails', () => {
});
});
});
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alert: mockAlert },
loading: false,
});
});
it('should track alert details page views', () => {
const { category, action } = trackAlertsDetailsViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
it('should track alert status updates', () => {
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findStatusDropdownItem().vm.$emit('click');
const status = findStatusDropdownItem().text();
setImmediate(() => {
const { category, action, label } = trackAlertStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
});
});
});
});
......@@ -14,9 +14,14 @@ import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash from '~/flash';
import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants';
import {
ALERTS_STATUS_TABS,
trackAlertListViewsOptions,
trackAlertStatusUpdateOptions,
} from '~/alert_management/constants';
import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql';
import mockAlerts from '../mocks/alerts.json';
import Tracking from '~/tracking';
jest.mock('~/flash');
......@@ -94,7 +99,7 @@ describe('AlertManagementList', () => {
}
});
describe('alert management feature renders empty state', () => {
describe('Empty state', () => {
it('shows empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
......@@ -363,4 +368,31 @@ describe('AlertManagementList', () => {
});
});
});
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount },
loading: false,
});
});
it('should track alert list page views', () => {
const { category, action } = trackAlertListViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
it('should track alert status updates', () => {
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
const status = findFirstStatusOption().text();
setImmediate(() => {
const { category, action, label } = trackAlertStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
});
});
});
});
......@@ -18,6 +18,12 @@ import {
severityLevelVariant,
errorStatus,
} from '~/error_tracking/components/constants';
import Tracking from '~/tracking';
import {
trackClickErrorLinkToSentryOptions,
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';
jest.mock('~/flash');
......@@ -30,12 +36,19 @@ describe('ErrorDetails', () => {
let actions;
let getters;
let mocks;
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
const findInput = name => {
const inputs = wrapper.findAll(GlFormInput).filter(c => c.attributes('name') === name);
return inputs.length ? inputs.at(0) : inputs;
};
const findUpdateIgnoreStatusButton = () =>
wrapper.find('[data-testid="update-ignore-status-btn"]');
const findUpdateResolveStatusButton = () =>
wrapper.find('[data-testid="update-resolve-status-btn"]');
const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]');
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
stubs: { GlDeprecatedButton, GlSprintf },
......@@ -57,7 +70,7 @@ describe('ErrorDetails', () => {
beforeEach(() => {
actions = {
startPollingStacktrace: () => {},
updateIgnoreStatus: jest.fn(),
updateIgnoreStatus: jest.fn().mockResolvedValue({}),
updateResolveStatus: jest.fn().mockResolvedValue({ closed_issue_iid: 1 }),
};
......@@ -302,11 +315,6 @@ describe('ErrorDetails', () => {
});
describe('Status update', () => {
const findUpdateIgnoreStatusButton = () =>
wrapper.find('[data-qa-selector="update_ignore_status_button"]');
const findUpdateResolveStatusButton = () =>
wrapper.find('[data-qa-selector="update_resolve_status_button"]');
afterEach(() => {
actions.updateIgnoreStatus.mockClear();
actions.updateResolveStatus.mockClear();
......@@ -491,4 +499,55 @@ describe('ErrorDetails', () => {
});
});
});
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mocks.$apollo.queries.error.loading = false;
mountComponent();
wrapper.setData({
error: { externalUrl },
});
});
it('should track detail page views', () => {
const { category, action } = trackErrorDetailsViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
it('should track IGNORE status update', () => {
Tracking.event.mockClear();
findUpdateIgnoreStatusButton().vm.$emit('click');
setImmediate(() => {
const { category, action, label } = trackErrorStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, {
label,
property: 'ignored',
});
});
});
it('should track RESOLVE status update', () => {
Tracking.event.mockClear();
findUpdateResolveStatusButton().vm.$emit('click');
setImmediate(() => {
const { category, action, label } = trackErrorStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, {
label,
property: 'resolved',
});
});
});
it('should track external Sentry link views', () => {
Tracking.event.mockClear();
findExternalUrl().trigger('click');
setImmediate(() => {
const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
externalUrl,
);
expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
});
});
});
});
......@@ -4,7 +4,9 @@ import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } fr
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
import errorsList from './list_mock.json';
import Tracking from '~/tracking';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -460,4 +462,41 @@ describe('ErrorTrackingList', () => {
});
});
});
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
store.state.list.loading = false;
store.state.list.errors = errorsList;
mountComponent({
stubs: {
GlTable: false,
GlLink: false,
GlDeprecatedButton: false,
},
});
});
it('should track list views', () => {
const { category, action } = trackErrorListViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
it('should track status updates', () => {
Tracking.event.mockClear();
const status = 'ignored';
findErrorActions().vm.$emit('update-issue-status', {
errorId: 1,
status,
});
setImmediate(() => {
const { category, action, label } = trackErrorStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, {
label,
property: status,
});
});
});
});
});
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