Commit b80b3fa6 authored by Thong Kuah's avatar Thong Kuah

Merge branch '31923-Snowplow-custom-events-Monitor' into 'master'

Snowplow custom events for Monitor: Health Product Categories

See merge request gitlab-org/gitlab!18157
parents c3898cdf 5e4781ac
......@@ -4,6 +4,8 @@ import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils';
export default {
fields: [
......@@ -21,6 +23,9 @@ export default {
Icon,
TimeAgo,
},
directives: {
TrackEvent: TrackEventDirective,
},
props: {
indexPath: {
type: String,
......@@ -53,6 +58,8 @@ export default {
},
methods: {
...mapActions(['startPolling', 'restartPolling']),
trackViewInSentryOptions,
trackClickErrorLinkToSentryOptions,
},
};
</script>
......@@ -65,7 +72,13 @@ export default {
</div>
<div v-else>
<div class="d-flex justify-content-end">
<gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank">
<gl-button
v-track-event="trackViewInSentryOptions(externalUrl)"
class="my-3 ml-auto"
variant="primary"
:href="externalUrl"
target="_blank"
>
{{ __('View in Sentry') }}
<icon name="external-link" class="flex-shrink-0" />
</gl-button>
......@@ -80,7 +93,12 @@ export default {
</template>
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
<gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank">
<gl-link
v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)"
:href="errors.item.externalUrl"
class="d-flex text-dark"
target="_blank"
>
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
......
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
/**
* Tracks snowplow event when user clicks View in Sentry btn
* @param {String} externalUrl that will be send as a property for the event
*/
export const trackViewInSentryOptions = url => ({
category: 'Error Tracking',
action: 'click_view_in_sentry',
label: 'External Url',
property: url,
});
/**
* Tracks snowplow event when User clicks on error link to Sentry
* @param {String} externalUrl that will be send as a property for the event
*/
export const trackClickErrorLinkToSentryOptions = url => ({
category: 'Error Tracking',
action: 'click_error_link_to_sentry',
label: 'Error Link',
property: url,
});
......@@ -21,7 +21,14 @@ import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
import { getTimeDiff, getTimeWindow } from '../utils';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import {
getTimeDiff,
getTimeWindow,
downloadCSVOptions,
generateLinkToChartOptions,
} from '../utils';
let sidebarMutationObserver;
......@@ -43,6 +50,7 @@ export default {
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
props: {
externalDashboardUrl: {
......@@ -322,6 +330,8 @@ export default {
groupHasData(group) {
return this.chartsWithData(group.metrics).length > 0;
},
downloadCSVOptions,
generateLinkToChartOptions,
},
addMetric: {
title: s__('Metrics|Add metric'),
......@@ -552,10 +562,19 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
<gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv(graphData)"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-track-event="
generateLinkToChartOptions(
generateLink(groupData.group, graphData.title, graphData.y_label),
)
"
class="js-chart-link"
:data-clipboard-text="
generateLink(groupData.group, graphData.title, graphData.y_label)
......
......@@ -13,6 +13,8 @@ import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
......@@ -27,6 +29,7 @@ export default {
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
props: {
clipboardText: {
......@@ -84,6 +87,8 @@ export default {
showToast() {
this.$toast.show(__('Link copied'));
},
downloadCSVOptions,
generateLinkToChartOptions,
},
};
</script>
......@@ -121,13 +126,18 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
<gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-track-event="generateLinkToChartOptions(clipboardText)"
class="js-chart-link"
:data-clipboard-text="clipboardText"
@click="showToast"
@click="showToast(clipboardText)"
>
{{ __('Generate link to chart') }}
</gl-dropdown-item>
......
......@@ -45,4 +45,47 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
);
};
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
/**
* Checks that element that triggered event is located on cluster health check dashboard
* @param {HTMLElement} element to check against
* @returns {boolean}
*/
const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show');
/**
* Tracks snowplow event when user generates link to metric chart
* @param {String} chart link that will be sent as a property for the event
* @return {Object} config object for event tracking
*/
export const generateLinkToChartOptions = chartLink => {
const isCLusterHealthBoard = isClusterHealthBoard();
const category = isCLusterHealthBoard
? 'Cluster Monitoring'
: 'Incident Management::Embedded metrics';
const action = isCLusterHealthBoard
? 'generate_link_to_cluster_metric_chart'
: 'generate_link_to_metrics_chart';
return { category, action, label: 'Chart link', property: chartLink };
};
/**
* Tracks snowplow event when user downloads CSV of cluster metric
* @param {String} chart title that will be sent as a property for the event
*/
export const downloadCSVOptions = title => {
const isCLusterHealthBoard = isClusterHealthBoard();
const category = isCLusterHealthBoard
? 'Cluster Monitoring'
: 'Incident Management::Embedded metrics';
const action = isCLusterHealthBoard
? 'download_csv_of_cluster_metric_chart'
: 'download_csv_of_metrics_dashboard_chart';
return { category, action, label: 'Chart title', property: title };
};
export default {};
import Tracking from '~/tracking';
export default {
bind(el, binding) {
el.dataset.trackingOptions = JSON.stringify(binding.value || {});
el.addEventListener('click', () => {
const { category, action, label, property, value } = JSON.parse(el.dataset.trackingOptions);
if (!category || !action) {
return;
}
Tracking.event(category, action, { label, property, value });
});
},
update(el, binding) {
if (binding.value !== binding.oldValue) {
el.dataset.trackingOptions = JSON.stringify(binding.value || {});
}
},
};
......@@ -13,9 +13,14 @@ module Projects
def update
result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
track_events(result)
render_update_response(result)
end
# overridden in EE
def track_events(result)
end
private
# overridden in EE
......
......@@ -10,6 +10,7 @@ module Issues
def add_link(link)
if can_add_link? && (link = parse_link(link))
track_meeting_added_event
success(_('Zoom meeting added'), append_to_description(link))
else
error(_('Failed to add a Zoom meeting'))
......@@ -22,6 +23,7 @@ module Issues
def remove_link
if can_remove_link?
track_meeting_removed_event
success(_('Zoom meeting removed'), remove_from_description)
else
error(_('Failed to remove a Zoom meeting'))
......@@ -44,6 +46,14 @@ module Issues
issue.description || ''
end
def track_meeting_added_event
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
end
def track_meeting_removed_event
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
end
def success(message, description)
ServiceResponse
.success(message: message, payload: { description: description })
......
---
title: 'Snowplow custom events for Monitor: Health Product Categories'
merge_request: 18157
author:
type: added
......@@ -64,7 +64,7 @@ module EE
end
if incident_management_available?
permitted_params[:incident_management_setting_attributes] = [:create_issue, :send_email, :issue_template_key]
permitted_params[:incident_management_setting_attributes] = ::Gitlab::Tracking::IncidentManagement.tracking_keys.keys
end
permitted_params
......@@ -82,6 +82,17 @@ module EE
end
end
end
override :track_events
def track_events(result)
super
if result[:status] == :success
::Gitlab::Tracking::IncidentManagement.track_from_params(
update_params[:incident_management_setting_attributes]
)
end
end
end
end
end
......
......@@ -380,6 +380,34 @@ describe Projects::Settings::OperationsController do
)
end
end
context 'updating each incident management setting' do
let(:project) { create(:project) }
let(:new_incident_management_settings) { {} }
before do
project.add_maintainer(user)
end
shared_examples 'a gitlab tracking event' do |params, event_key|
it "creates a gitlab tracking event #{event_key}" do
new_incident_management_settings = params
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::Settings', event_key, kind_of(Hash))
update_project(project,
incident_management_params: new_incident_management_settings)
end
end
it_behaves_like 'a gitlab tracking event', { create_issue: '1' }, 'enabled_issue_auto_creation_on_alerts'
it_behaves_like 'a gitlab tracking event', { create_issue: '0' }, 'disabled_issue_auto_creation_on_alerts'
it_behaves_like 'a gitlab tracking event', { issue_template_key: 'template' }, 'enabled_issue_template_on_alerts'
it_behaves_like 'a gitlab tracking event', { issue_template_key: nil }, 'disabled_issue_template_on_alerts'
it_behaves_like 'a gitlab tracking event', { send_email: '1' }, 'enabled_sending_emails'
it_behaves_like 'a gitlab tracking event', { send_email: '0' }, 'disabled_sending_emails'
end
end
context 'without a license' do
......
# frozen_string_literal: true
module Gitlab
module Tracking
module IncidentManagement
class << self
def track_from_params(incident_params)
return if incident_params.blank?
incident_params.each do |k, v|
prefix = ['', '0'].include?(v.to_s) ? 'disabled' : 'enabled'
key = tracking_keys.dig(k, :name)
label = tracking_keys.dig(k, :label)
next if key.blank?
details = label ? { label: label, property: v } : {}
::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details )
end
end
def tracking_keys
{
create_issue: {
name: 'issue_auto_creation_on_alerts'
},
issue_template_key: {
name: 'issue_template_on_alerts',
label: 'Template name'
},
send_email: {
name: 'sending_emails'
}
}.with_indifferent_access.freeze
end
end
end
end
end
import * as errorTrackingUtils from '~/error_tracking/utils';
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
describe('Error Tracking Events', () => {
describe('trackViewInSentryOptions', () => {
it('should return correct event options', () => {
expect(errorTrackingUtils.trackViewInSentryOptions(externalUrl)).toEqual({
category: 'Error Tracking',
action: 'click_view_in_sentry',
label: 'External Url',
property: externalUrl,
});
});
});
describe('trackClickErrorLinkToSentryOptions', () => {
it('should return correct event options', () => {
expect(errorTrackingUtils.trackClickErrorLinkToSentryOptions(externalUrl)).toEqual({
category: 'Error Tracking',
action: 'click_error_link_to_sentry',
label: 'Error Link',
property: externalUrl,
});
});
});
});
import * as monitoringUtils from '~/monitoring/utils';
describe('Snowplow Events', () => {
const generatedLink = 'http://chart.link.com';
const chartTitle = 'Some metric chart';
describe('trackGenerateLinkToChartEventOptions', () => {
it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
document.body.dataset.page = 'groups:clusters:show';
expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
category: 'Cluster Monitoring',
action: 'generate_link_to_cluster_metric_chart',
label: 'Chart link',
property: generatedLink,
});
});
it('should return Incident Management event options if located on Metrics Dashboard', () => {
document.body.dataset.page = 'metrics:show';
expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
category: 'Incident Management::Embedded metrics',
action: 'generate_link_to_metrics_chart',
label: 'Chart link',
property: generatedLink,
});
});
});
describe('trackDownloadCSVEvent', () => {
it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
document.body.dataset.page = 'groups:clusters:show';
expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
category: 'Cluster Monitoring',
action: 'download_csv_of_cluster_metric_chart',
label: 'Chart title',
property: chartTitle,
});
});
it('should return Incident Management event options if located on Metrics Dashboard', () => {
document.body.dataset.page = 'metriss:show';
expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
category: 'Incident Management::Embedded metrics',
action: 'download_csv_of_metrics_dashboard_chart',
label: 'Chart title',
property: chartTitle,
});
});
});
});
import Vue from 'vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking';
import TrackEvent from '~/vue_shared/directives/track_event';
jest.mock('~/tracking');
const Component = Vue.component('dummy-element', {
directives: {
TrackEvent,
},
data() {
return {
trackingOptions: null,
};
},
template: '<button id="trackable" v-track-event="trackingOptions"></button>',
});
const localVue = createLocalVue();
let wrapper;
let button;
describe('Error Tracking directive', () => {
beforeEach(() => {
wrapper = shallowMount(localVue.extend(Component), {
localVue,
});
button = wrapper.find('#trackable');
});
it('should not track the event if required arguments are not provided', () => {
button.trigger('click');
expect(Tracking.event).not.toHaveBeenCalled();
});
it('should track event on click if tracking info provided', () => {
const trackingOptions = {
category: 'Tracking',
action: 'click_trackable_btn',
label: 'Trackable Info',
};
wrapper.setData({ trackingOptions });
const { category, action, label, property, value } = trackingOptions;
button.trigger('click');
expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
});
});
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Tracking::IncidentManagement do
describe '.track_from_params' do
shared_examples 'a tracked event' do |label, value = nil|
it 'creates the tracking event with the correct details' do
expect(::Gitlab::Tracking)
.to receive(:event)
.with(
'IncidentManagement::Settings',
label,
value || kind_of(Hash)
)
end
end
after do
described_class.track_from_params(params)
end
context 'known params' do
known_params = described_class.tracking_keys
known_params.each do |key, values|
context "param #{key}" do
let(:params) { { key => '1' } }
it_behaves_like 'a tracked event', "enabled_#{known_params[key][:name]}"
end
end
context 'different input values' do
shared_examples 'the correct prefixed event name' do |input, enabled|
let(:params) { { issue_template_key: input } }
it 'matches' do
expect(::Gitlab::Tracking)
.to receive(:event)
.with(
anything,
"#{enabled}_issue_template_on_alerts",
anything
)
end
end
it_behaves_like 'the correct prefixed event name', 1, 'enabled'
it_behaves_like 'the correct prefixed event name', '1', 'enabled'
it_behaves_like 'the correct prefixed event name', 'template', 'enabled'
it_behaves_like 'the correct prefixed event name', '', 'disabled'
it_behaves_like 'the correct prefixed event name', nil, 'disabled'
end
context 'param with label' do
let(:params) { { issue_template_key: '1' } }
it_behaves_like 'a tracked event', "enabled_issue_template_on_alerts", { label: 'Template name', property: '1' }
end
context 'param without label' do
let(:params) { { create_issue: '1' } }
it_behaves_like 'a tracked event', "enabled_issue_auto_creation_on_alerts", {}
end
end
context 'unknown params' do
let(:params) { { 'unknown' => '1' } }
it 'does not create the tracking event' do
expect(::Gitlab::Tracking)
.not_to receive(:event)
end
end
end
end
......@@ -51,6 +51,12 @@ describe Issues::ZoomLinkService do
expect(result.payload[:description])
.to eq("#{issue.description}\n\n#{zoom_link}")
end
it 'tracks the add event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
end
shared_examples 'cannot add link' do
......@@ -135,6 +141,13 @@ describe Issues::ZoomLinkService do
.to eq(issue.description.delete_suffix("\n\n#{zoom_link}"))
end
it 'tracks the remove event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
context 'with insufficient permissions' do
include_context 'insufficient permissions'
include_examples 'cannot remove link'
......
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