import { setHTMLFixture } from 'helpers/fixtures'; import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants'; import { getExperimentData } from '~/experimentation/utils'; import Tracking, { initUserTracking, initDefaultTrackers, STANDARD_CONTEXT } from '~/tracking'; jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() })); describe('Tracking', () => { let snowplowSpy; let bindDocumentSpy; let trackLoadEventsSpy; beforeEach(() => { getExperimentData.mockReturnValue(undefined); window.snowplow = window.snowplow || (() => {}); window.snowplowOptions = { namespace: '_namespace_', hostname: 'app.gitfoo.com', cookieDomain: '.gitfoo.com', }; snowplowSpy = jest.spyOn(window, 'snowplow'); }); describe('initUserTracking', () => { it('calls through to get a new tracker with the expected options', () => { initUserTracking(); expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', { namespace: '_namespace_', hostname: 'app.gitfoo.com', cookieDomain: '.gitfoo.com', appId: '', userFingerprint: false, respectDoNotTrack: true, forceSecureTracker: true, eventMethod: 'post', contexts: { webPage: true, performanceTiming: true }, formTracking: false, linkClickTracking: false, pageUnloadTimer: 10, }); }); }); describe('initDefaultTrackers', () => { beforeEach(() => { bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null); trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null); }); it('should activate features based on what has been enabled', () => { initDefaultTrackers(); expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30); expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [STANDARD_CONTEXT]); expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking'); window.snowplowOptions = { ...window.snowplowOptions, formTracking: true, linkClickTracking: true, }; initDefaultTrackers(); expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking'); }); it('binds the document event handling', () => { initDefaultTrackers(); expect(bindDocumentSpy).toHaveBeenCalled(); }); it('tracks page loaded events', () => { initDefaultTrackers(); expect(trackLoadEventsSpy).toHaveBeenCalled(); }); }); describe('.event', () => { afterEach(() => { window.doNotTrack = undefined; navigator.doNotTrack = undefined; navigator.msDoNotTrack = undefined; }); describe('builds the standard context', () => { let standardContext; beforeAll(async () => { window.gl = window.gl || {}; window.gl.snowplowStandardContext = { schema: 'iglu:com.gitlab/gitlab_standard', data: { environment: 'testing', source: 'unknown', }, }; jest.resetModules(); ({ STANDARD_CONTEXT: standardContext } = await import('~/tracking')); }); it('uses server data', () => { expect(standardContext.schema).toBe('iglu:com.gitlab/gitlab_standard'); expect(standardContext.data.environment).toBe('testing'); }); it('overrides schema source', () => { expect(standardContext.data.source).toBe('gitlab-javascript'); }); }); it('tracks to snowplow (our current tracking system)', () => { Tracking.event('_category_', '_eventName_', { label: '_label_' }); expect(snowplowSpy).toHaveBeenCalledWith( 'trackStructEvent', '_category_', '_eventName_', '_label_', undefined, undefined, [STANDARD_CONTEXT], ); }); it('skips tracking if snowplow is unavailable', () => { window.snowplow = false; Tracking.event('_category_', '_eventName_'); expect(snowplowSpy).not.toHaveBeenCalled(); }); it('skips tracking if the user does not want to be tracked (general spec)', () => { window.doNotTrack = '1'; Tracking.event('_category_', '_eventName_'); expect(snowplowSpy).not.toHaveBeenCalled(); }); it('skips tracking if the user does not want to be tracked (firefox legacy)', () => { navigator.doNotTrack = 'yes'; Tracking.event('_category_', '_eventName_'); expect(snowplowSpy).not.toHaveBeenCalled(); }); it('skips tracking if the user does not want to be tracked (IE legacy)', () => { navigator.msDoNotTrack = '1'; Tracking.event('_category_', '_eventName_'); expect(snowplowSpy).not.toHaveBeenCalled(); }); }); describe('.flushPendingEvents', () => { it('flushes any pending events', () => { Tracking.initialized = false; Tracking.event('_category_', '_eventName_', { label: '_label_' }); expect(snowplowSpy).not.toHaveBeenCalled(); Tracking.flushPendingEvents(); expect(snowplowSpy).toHaveBeenCalledWith( 'trackStructEvent', '_category_', '_eventName_', '_label_', undefined, undefined, [STANDARD_CONTEXT], ); }); }); describe.each` term ${'event'} ${'action'} `('tracking interface events with data-track-$term', ({ term }) => { let eventSpy; beforeEach(() => { eventSpy = jest.spyOn(Tracking, 'event'); Tracking.bindDocument('_category_'); // only happens once setHTMLFixture(`
`); }); it(`binds to clicks on elements matching [data-track-${term}]`, () => { document.querySelector(`[data-track-${term}="click_input1"]`).click(); expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', { label: '_label_', value: '_value_', }); }); it(`does not bind to clicks on elements without [data-track-${term}]`, () => { document.querySelector('[data-track-bogus="click_bogusinput"]').click(); expect(eventSpy).not.toHaveBeenCalled(); }); it('allows value override with the data-track-value attribute', () => { document.querySelector(`[data-track-${term}="click_input2"]`).click(); expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', { value: '_value_override_', }); }); it('handles checkbox values correctly', () => { const checkbox = document.querySelector(`[data-track-${term}="toggle_checkbox"]`); checkbox.click(); // unchecking expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { value: false, }); checkbox.click(); // checking expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { value: '_value_', }); }); it('handles bootstrap dropdowns', () => { const dropdown = document.querySelector(`[data-track-${term}="toggle_dropdown"]`); dropdown.dispatchEvent(new Event('show.bs.dropdown', { bubbles: true })); expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {}); dropdown.dispatchEvent(new Event('hide.bs.dropdown', { bubbles: true })); expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {}); }); it('handles nested elements inside an element with tracking', () => { document.querySelector('span.nested').click(); expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {}); }); it('brings in experiment data if linked to an experiment', () => { const mockExperimentData = { variant: 'candidate', experiment: 'repo_integrations_link', key: '2bff73f6bb8cc11156c50a8ba66b9b8b', }; getExperimentData.mockReturnValue(mockExperimentData); document.querySelector(`[data-track-${term}="click_input3"]`).click(); expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input3', { value: '_value_', context: { schema: TRACKING_CONTEXT_SCHEMA, data: mockExperimentData }, }); }); }); describe.each` term ${'event'} ${'action'} `('tracking page loaded events with -$term', ({ term }) => { let eventSpy; beforeEach(() => { eventSpy = jest.spyOn(Tracking, 'event'); setHTMLFixture(` Something `); Tracking.trackLoadEvents('_category_'); // only happens once }); it(`sends tracking events when [data-track-${term}="render"] is on an element`, () => { expect(eventSpy.mock.calls).toEqual([ [ '_category_', 'render', { label: 'label1', value: '_value_', property: '_property_', }, ], [ '_category_', 'render', { label: 'label2', value: '_value_', }, ], ]); }); }); describe('tracking mixin', () => { describe('trackingOptions', () => { it('returns the options defined on initialisation', () => { const mixin = Tracking.mixin({ foo: 'bar' }); expect(mixin.computed.trackingOptions()).toEqual({ foo: 'bar' }); }); it('lets local tracking value override and extend options', () => { const mixin = Tracking.mixin({ foo: 'bar' }); // The value of this in the Vue lifecyle is different, but this serves the test's purposes mixin.computed.tracking = { foo: 'baz', baz: 'bar' }; expect(mixin.computed.trackingOptions()).toEqual({ foo: 'baz', baz: 'bar' }); }); }); describe('trackingCategory', () => { it('returns the category set in the component properties first', () => { const mixin = Tracking.mixin({ category: 'foo' }); mixin.computed.tracking = { category: 'bar', }; expect(mixin.computed.trackingCategory()).toBe('bar'); }); it('returns the category set in the options', () => { const mixin = Tracking.mixin({ category: 'foo' }); expect(mixin.computed.trackingCategory()).toBe('foo'); }); it('returns undefined if no category is selected', () => { const mixin = Tracking.mixin(); expect(mixin.computed.trackingCategory()).toBe(undefined); }); }); describe('track', () => { let eventSpy; let mixin; beforeEach(() => { eventSpy = jest.spyOn(Tracking, 'event').mockReturnValue(); mixin = Tracking.mixin(); mixin = { ...mixin.computed, ...mixin.methods, }; }); it('calls the event method with no category or action defined', () => { mixin.trackingCategory = mixin.trackingCategory(); mixin.trackingOptions = mixin.trackingOptions(); mixin.track(); expect(eventSpy).toHaveBeenCalledWith(undefined, undefined, {}); }); it('calls the event method', () => { mixin.trackingCategory = mixin.trackingCategory(); mixin.trackingOptions = mixin.trackingOptions(); mixin.track('foo'); expect(eventSpy).toHaveBeenCalledWith(undefined, 'foo', {}); }); it('gives precedence to data for category and options', () => { mixin.trackingCategory = mixin.trackingCategory(); mixin.trackingOptions = mixin.trackingOptions(); const data = { category: 'foo', label: 'baz' }; mixin.track('foo', data); expect(eventSpy).toHaveBeenCalledWith('foo', 'foo', data); }); }); }); });