Commit 8bba4e5a authored by Dallas Reedy's avatar Dallas Reedy Committed by Rémy Coutable

Add tracking for active trial status reminders experiment

parent cb26a88c
...@@ -4,7 +4,9 @@ const CLICK_BUTTON_ACTION = 'click_button'; ...@@ -4,7 +4,9 @@ const CLICK_BUTTON_ACTION = 'click_button';
const RESIZE_EVENT_DEBOUNCE_MS = 150; const RESIZE_EVENT_DEBOUNCE_MS = 150;
export const RESIZE_EVENT = 'resize'; export const RESIZE_EVENT = 'resize';
export const TRACKING_PROPERTY = 'experiment:show_trial_status_in_sidebar'; export const EXPERIMENT_KEY = 'forcibly_show_trial_status_popover';
export const TRACKING_PROPERTY_WHEN_FORCED = 'forced';
export const TRACKING_PROPERTY_WHEN_VOLUNTARY = 'voluntary';
export const WIDGET = { export const WIDGET = {
i18n: { i18n: {
...@@ -19,7 +21,11 @@ export const WIDGET = { ...@@ -19,7 +21,11 @@ export const WIDGET = {
}, },
}, },
trackingEvents: { trackingEvents: {
widgetClick: { action: 'click_link', label: 'trial_status_widget' }, widgetClick: {
action: 'click_link',
label: 'trial_status_widget',
property: TRACKING_PROPERTY_WHEN_VOLUNTARY,
},
}, },
}; };
......
...@@ -6,7 +6,13 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,7 +6,13 @@ import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { POPOVER, RESIZE_EVENT, TRACKING_PROPERTY } from './constants'; import {
POPOVER,
RESIZE_EVENT,
EXPERIMENT_KEY,
TRACKING_PROPERTY_WHEN_FORCED,
TRACKING_PROPERTY_WHEN_VOLUNTARY,
} from './constants';
const { const {
i18n, i18n,
...@@ -15,7 +21,7 @@ const { ...@@ -15,7 +21,7 @@ const {
resizeEventDebounceMS, resizeEventDebounceMS,
disabledBreakpoints, disabledBreakpoints,
} = POPOVER; } = POPOVER;
const trackingMixin = Tracking.mixin({ property: TRACKING_PROPERTY }); const trackingMixin = Tracking.mixin({ experiment: EXPERIMENT_KEY });
export default { export default {
components: { components: {
...@@ -26,6 +32,7 @@ export default { ...@@ -26,6 +32,7 @@ export default {
mixins: [trackingMixin], mixins: [trackingMixin],
inject: { inject: {
containerId: {}, containerId: {},
daysRemaining: {}, // for tracking purposes
groupName: {}, groupName: {},
planName: {}, planName: {},
plansHref: {}, plansHref: {},
...@@ -45,7 +52,6 @@ export default { ...@@ -45,7 +52,6 @@ export default {
}; };
}, },
i18n, i18n,
trackingEvents,
computed: { computed: {
formattedTrialEndDate() { formattedTrialEndDate() {
return formatDate(this.trialEndDate, trialEndDateFormatString); return formatDate(this.trialEndDate, trialEndDateFormatString);
...@@ -79,8 +85,8 @@ export default { ...@@ -79,8 +85,8 @@ export default {
this.forciblyShowing = false; this.forciblyShowing = false;
this.show = false; this.show = false;
const { action, ...options } = this.$options.trackingEvents.closeBtnClick; const { action, ...options } = trackingEvents.closeBtnClick;
this.track(action, options); this.track(action, { ...options, ...this.trackingPropertyAndValue() });
}, },
onForciblyShown() { onForciblyShown() {
if (this.userCalloutsPath && this.userCalloutsFeatureId) { if (this.userCalloutsPath && this.userCalloutsFeatureId) {
...@@ -98,16 +104,24 @@ export default { ...@@ -98,16 +104,24 @@ export default {
this.updateDisabledState(); this.updateDisabledState();
}, },
onShown() { onShown() {
const { action, ...options } = this.$options.trackingEvents.popoverShown; const { action, ...options } = trackingEvents.popoverShown;
this.track(action, options); this.track(action, { ...options, ...this.trackingPropertyAndValue() });
}, },
onUpgradeBtnClick() { onUpgradeBtnClick() {
const { action, ...options } = this.$options.trackingEvents.upgradeBtnClick; const { action, ...options } = trackingEvents.upgradeBtnClick;
this.track(action, options); this.track(action, { ...options, ...this.trackingPropertyAndValue() });
}, },
onCompareBtnClick() { onCompareBtnClick() {
const { action, ...options } = this.$options.trackingEvents.compareBtnClick; const { action, ...options } = trackingEvents.compareBtnClick;
this.track(action, options); this.track(action, { ...options, ...this.trackingPropertyAndValue() });
},
trackingPropertyAndValue() {
return {
property: this.forciblyShowing
? TRACKING_PROPERTY_WHEN_FORCED
: TRACKING_PROPERTY_WHEN_VOLUNTARY,
value: this.daysRemaining,
};
}, },
updateDisabledState() { updateDisabledState() {
this.disabled = disabledBreakpoints.includes(bp.getBreakpointSize()); this.disabled = disabledBreakpoints.includes(bp.getBreakpointSize());
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
import { GlLink, GlProgressBar } from '@gitlab/ui'; import { GlLink, GlProgressBar } from '@gitlab/ui';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { TRACKING_PROPERTY, WIDGET } from './constants'; import { EXPERIMENT_KEY, WIDGET } from './constants';
const { i18n, trackingEvents } = WIDGET; const { i18n, trackingEvents } = WIDGET;
const trackingMixin = Tracking.mixin({ property: TRACKING_PROPERTY }); const trackingMixin = Tracking.mixin({ experiment: EXPERIMENT_KEY });
export default { export default {
components: { components: {
...@@ -21,13 +21,9 @@ export default { ...@@ -21,13 +21,9 @@ export default {
planName: {}, planName: {},
plansHref: {}, plansHref: {},
}, },
i18n,
trackingEvents,
computed: { computed: {
widgetTitle() { widgetTitle() {
const i18nWidgetTitle = this.$options.i18n.widgetTitle.countableTranslator( const i18nWidgetTitle = i18n.widgetTitle.countableTranslator(this.daysRemaining);
this.daysRemaining,
);
return sprintf(i18nWidgetTitle, { return sprintf(i18nWidgetTitle, {
planName: this.planName, planName: this.planName,
...@@ -38,8 +34,8 @@ export default { ...@@ -38,8 +34,8 @@ export default {
}, },
methods: { methods: {
onWidgetClick() { onWidgetClick() {
const { action, ...options } = this.$options.trackingEvents.widgetClick; const { action, ...options } = trackingEvents.widgetClick;
this.track(action, options); this.track(action, { ...options, value: this.daysRemaining });
}, },
}, },
}; };
......
...@@ -37,6 +37,7 @@ export const initTrialStatusPopover = () => { ...@@ -37,6 +37,7 @@ export const initTrialStatusPopover = () => {
const { const {
containerId, containerId,
daysRemaining, // for tracking purposes
groupName, groupName,
planName, planName,
plansHref, plansHref,
...@@ -52,6 +53,7 @@ export const initTrialStatusPopover = () => { ...@@ -52,6 +53,7 @@ export const initTrialStatusPopover = () => {
el, el,
provide: { provide: {
containerId, containerId,
daysRemaining, // for tracking purposes
groupName, groupName,
planName, planName,
plansHref, plansHref,
......
...@@ -21,6 +21,7 @@ module TrialStatusWidgetHelper ...@@ -21,6 +21,7 @@ module TrialStatusWidgetHelper
def trial_status_popover_data_attrs(group) def trial_status_popover_data_attrs(group)
base_attrs = trial_status_common_data_attrs(group) base_attrs = trial_status_common_data_attrs(group)
base_attrs.merge( base_attrs.merge(
days_remaining: group.trial_days_remaining, # for experiment tracking
group_name: group.name, group_name: group.name,
purchase_href: ultimate_subscription_path_for_group(group), purchase_href: ultimate_subscription_path_for_group(group),
start_initially_shown: force_popover_to_be_shown?(group), start_initially_shown: force_popover_to_be_shown?(group),
...@@ -57,6 +58,7 @@ module TrialStatusWidgetHelper ...@@ -57,6 +58,7 @@ module TrialStatusWidgetHelper
experiment(:forcibly_show_trial_status_popover, group: group) do |e| experiment(:forcibly_show_trial_status_popover, group: group) do |e|
e.use { false } e.use { false }
e.try { !dismissed_feature_callout?(current_user_callout_feature_id(group.trial_days_remaining)) } e.try { !dismissed_feature_callout?(current_user_callout_feature_id(group.trial_days_remaining)) }
e.record!
e.run e.run
end end
end end
......
...@@ -2,7 +2,11 @@ import { GlPopover } from '@gitlab/ui'; ...@@ -2,7 +2,11 @@ import { GlPopover } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import { POPOVER, TRACKING_PROPERTY } from 'ee/contextual_sidebar/components/constants'; import {
POPOVER,
TRACKING_PROPERTY_WHEN_FORCED,
TRACKING_PROPERTY_WHEN_VOLUNTARY,
} from 'ee/contextual_sidebar/components/constants';
import TrialStatusPopover from 'ee/contextual_sidebar/components/trial_status_popover.vue'; import TrialStatusPopover from 'ee/contextual_sidebar/components/trial_status_popover.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -15,13 +19,15 @@ describe('TrialStatusPopover component', () => { ...@@ -15,13 +19,15 @@ describe('TrialStatusPopover component', () => {
let trackingSpy; let trackingSpy;
const { trackingEvents } = POPOVER; const { trackingEvents } = POPOVER;
const defaultDaysRemaining = 20;
const findGlPopover = () => wrapper.findComponent(GlPopover); const findGlPopover = () => wrapper.findComponent(GlPopover);
const expectTracking = ({ action, ...options } = {}) => { const expectTracking = ({ action, ...options } = {}) => {
return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, { return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
property: TRACKING_PROPERTY_WHEN_VOLUNTARY,
value: defaultDaysRemaining,
...options, ...options,
property: TRACKING_PROPERTY,
}); });
}; };
...@@ -30,6 +36,7 @@ describe('TrialStatusPopover component', () => { ...@@ -30,6 +36,7 @@ describe('TrialStatusPopover component', () => {
mountFn(TrialStatusPopover, { mountFn(TrialStatusPopover, {
provide: { provide: {
containerId: undefined, containerId: undefined,
daysRemaining: defaultDaysRemaining,
groupName: 'Some Test Group', groupName: 'Some Test Group',
planName: 'Ultimate', planName: 'Ultimate',
plansHref: 'billing/path-for/group', plansHref: 'billing/path-for/group',
...@@ -210,4 +217,26 @@ describe('TrialStatusPopover component', () => { ...@@ -210,4 +217,26 @@ describe('TrialStatusPopover component', () => {
}); });
}); });
}); });
describe('trackingPropertyAndValue', () => {
it.each`
daysRemaining | startInitiallyShown | property
${14} | ${false} | ${TRACKING_PROPERTY_WHEN_VOLUNTARY}
${14} | ${true} | ${TRACKING_PROPERTY_WHEN_FORCED}
`(
'sets the expected values for `property` & `value`',
({ daysRemaining, startInitiallyShown, property }) => {
wrapper = createComponent({ daysRemaining, startInitiallyShown });
// We'll use the "onShown" method to exercise trackingPropertyAndValue
findGlPopover().vm.$emit('shown');
expectTracking({
...trackingEvents.popoverShown,
property,
value: daysRemaining,
});
},
);
});
}); });
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { TRACKING_PROPERTY, WIDGET } from 'ee/contextual_sidebar/components/constants'; import { WIDGET } from 'ee/contextual_sidebar/components/constants';
import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_widget.vue'; import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_widget.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
...@@ -8,13 +8,14 @@ describe('TrialStatusWidget component', () => { ...@@ -8,13 +8,14 @@ describe('TrialStatusWidget component', () => {
let wrapper; let wrapper;
const { trackingEvents } = WIDGET; const { trackingEvents } = WIDGET;
const defaultDaysRemaining = 20;
const findGlLink = () => wrapper.findComponent(GlLink); const findGlLink = () => wrapper.findComponent(GlLink);
const createComponent = (providers = {}) => { const createComponent = (providers = {}) => {
return shallowMount(TrialStatusWidget, { return shallowMount(TrialStatusWidget, {
provide: { provide: {
daysRemaining: 20, daysRemaining: defaultDaysRemaining,
navIconImagePath: 'illustrations/golden_tanuki.svg', navIconImagePath: 'illustrations/golden_tanuki.svg',
percentageComplete: 10, percentageComplete: 10,
planName: 'Ultimate', planName: 'Ultimate',
...@@ -58,7 +59,7 @@ describe('TrialStatusWidget component', () => { ...@@ -58,7 +59,7 @@ describe('TrialStatusWidget component', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, action, { expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
...options, ...options,
property: TRACKING_PROPERTY, value: defaultDaysRemaining,
}); });
unmockTracking(); unmockTracking();
......
...@@ -8,24 +8,15 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -8,24 +8,15 @@ RSpec.describe TrialStatusWidgetHelper do
let(:today_for_specs) { Date.parse('2021-01-15') } let(:today_for_specs) { Date.parse('2021-01-15') }
let(:trial_days_remaining) { 18 } let(:trial_days_remaining) { 18 }
let(:trial_end_date) { Date.current.advance(days: trial_days_remaining) } let(:trial_end_date) { Date.current.advance(days: trial_days_remaining) }
let(:trial_start_date) { Date.current.advance(days: trial_days_remaining - trial_length) }
let(:trial_percentage_complete) { (trial_length - trial_days_remaining) * 100 / trial_length } let(:trial_percentage_complete) { (trial_length - trial_days_remaining) * 100 / trial_length }
let(:subscription) { instance_double(GitlabSubscription, plan_title: 'Ultimate') }
let_it_be(:group) { create(:group) }
let(:group) do
instance_double(Group,
id: 123,
name: 'Pants Group',
to_param: 'pants-group',
gitlab_subscription: subscription,
trial_days_remaining: trial_days_remaining,
trial_ends_on: trial_end_date,
trial_percentage_complete: trial_percentage_complete
)
end
let(:shared_expected_attrs) do let(:shared_expected_attrs) do
{ {
container_id: 'trial-status-sidebar-widget', container_id: 'trial-status-sidebar-widget',
days_remaining: trial_days_remaining,
plan_name: 'Ultimate', plan_name: 'Ultimate',
plans_href: group_billings_path(group) plans_href: group_billings_path(group)
} }
...@@ -33,6 +24,11 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -33,6 +24,11 @@ RSpec.describe TrialStatusWidgetHelper do
before do before do
travel_to today_for_specs travel_to today_for_specs
build(:gitlab_subscription, :active_trial,
namespace: group,
trial_starts_on: trial_start_date,
trial_ends_on: trial_end_date
)
stub_experiments(forcibly_show_trial_status_popover: :candidate) stub_experiments(forcibly_show_trial_status_popover: :candidate)
end end
...@@ -46,6 +42,9 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -46,6 +42,9 @@ RSpec.describe TrialStatusWidgetHelper do
d14_callout_id = described_class::D14_CALLOUT_ID d14_callout_id = described_class::D14_CALLOUT_ID
d3_callout_id = described_class::D3_CALLOUT_ID d3_callout_id = described_class::D3_CALLOUT_ID
let(:user_callouts_feature_id) { nil }
let(:dismissed_callout) { true }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
...@@ -126,6 +125,10 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -126,6 +125,10 @@ RSpec.describe TrialStatusWidgetHelper do
with_them { include_examples 'has correct data attributes' } with_them { include_examples 'has correct data attributes' }
end end
it 'records the experiment subject' do
expect { data_attrs }.to change { ExperimentSubject.count }
end
end end
describe '#trial_status_widget_data_attrs' do describe '#trial_status_widget_data_attrs' do
...@@ -138,7 +141,6 @@ RSpec.describe TrialStatusWidgetHelper do ...@@ -138,7 +141,6 @@ RSpec.describe TrialStatusWidgetHelper do
it 'returns the needed data attributes for mounting the widget Vue component' do it 'returns the needed data attributes for mounting the widget Vue component' do
expect(data_attrs).to match( expect(data_attrs).to match(
shared_expected_attrs.merge( shared_expected_attrs.merge(
days_remaining: trial_days_remaining,
nav_icon_image_path: '/image-path/for-file.svg', nav_icon_image_path: '/image-path/for-file.svg',
percentage_complete: trial_percentage_complete percentage_complete: trial_percentage_complete
) )
......
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