diff options
Diffstat (limited to 'spec/frontend/tracking_spec.js')
-rw-r--r-- | spec/frontend/tracking_spec.js | 693 |
1 files changed, 0 insertions, 693 deletions
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js deleted file mode 100644 index 21fed51ff10..00000000000 --- a/spec/frontend/tracking_spec.js +++ /dev/null @@ -1,693 +0,0 @@ -import { setHTMLFixture } from 'helpers/fixtures'; -import { TEST_HOST } from 'helpers/test_constants'; -import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants'; -import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils'; -import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking'; -import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants'; -import getStandardContext from '~/tracking/get_standard_context'; - -jest.mock('~/experimentation/utils', () => ({ - getExperimentData: jest.fn(), - getAllExperimentContexts: jest.fn(), -})); - -describe('Tracking', () => { - let standardContext; - let snowplowSpy; - let bindDocumentSpy; - let trackLoadEventsSpy; - let enableFormTracking; - let setAnonymousUrlsSpy; - - beforeAll(() => { - window.gl = window.gl || {}; - window.gl.snowplowUrls = {}; - window.gl.snowplowStandardContext = { - schema: 'iglu:com.gitlab/gitlab_standard', - data: { - environment: 'testing', - source: 'unknown', - extra: {}, - }, - }; - - standardContext = getStandardContext(); - }); - - beforeEach(() => { - getExperimentData.mockReturnValue(undefined); - getAllExperimentContexts.mockReturnValue([]); - - 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, - formTrackingConfig: { - fields: { allow: [] }, - forms: { allow: [] }, - }, - }); - }); - }); - - describe('initDefaultTrackers', () => { - beforeEach(() => { - bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null); - trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null); - enableFormTracking = jest - .spyOn(Tracking, 'enableFormTracking') - .mockImplementation(() => null); - setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null); - }); - - it('should activate features based on what has been enabled', () => { - initDefaultTrackers(); - expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30); - expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]); - expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking'); - expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking'); - - window.snowplowOptions = { - ...window.snowplowOptions, - formTracking: true, - linkClickTracking: true, - formTrackingConfig: { forms: { whitelist: ['foo'] }, fields: { whitelist: ['bar'] } }, - }; - - initDefaultTrackers(); - expect(enableFormTracking).toHaveBeenCalledWith(window.snowplowOptions.formTrackingConfig); - expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking'); - }); - - it('binds the document event handling', () => { - initDefaultTrackers(); - expect(bindDocumentSpy).toHaveBeenCalled(); - }); - - it('tracks page loaded events', () => { - initDefaultTrackers(); - expect(trackLoadEventsSpy).toHaveBeenCalled(); - }); - - it('calls the anonymized URLs method', () => { - initDefaultTrackers(); - expect(setAnonymousUrlsSpy).toHaveBeenCalled(); - }); - - describe('when there are experiment contexts', () => { - const experimentContexts = [ - { - schema: TRACKING_CONTEXT_SCHEMA, - data: { experiment: 'experiment1', variant: 'control' }, - }, - { - schema: TRACKING_CONTEXT_SCHEMA, - data: { experiment: 'experiment_two', variant: 'candidate' }, - }, - ]; - - beforeEach(() => { - getAllExperimentContexts.mockReturnValue(experimentContexts); - }); - - it('includes those contexts alongside the standard context', () => { - initDefaultTrackers(); - expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [ - standardContext, - ...experimentContexts, - ]); - }); - }); - }); - - describe('.event', () => { - afterEach(() => { - window.doNotTrack = undefined; - navigator.doNotTrack = undefined; - navigator.msDoNotTrack = undefined; - }); - - it('tracks to snowplow (our current tracking system)', () => { - Tracking.event('_category_', '_eventName_', { label: '_label_' }); - - expect(snowplowSpy).toHaveBeenCalledWith( - 'trackStructEvent', - '_category_', - '_eventName_', - '_label_', - undefined, - undefined, - [standardContext], - ); - }); - - it('allows adding extra data to the default context', () => { - const extra = { foo: 'bar' }; - - Tracking.event('_category_', '_eventName_', { extra }); - - expect(snowplowSpy).toHaveBeenCalledWith( - 'trackStructEvent', - '_category_', - '_eventName_', - undefined, - undefined, - undefined, - [ - { - ...standardContext, - data: { - ...standardContext.data, - extra, - }, - }, - ], - ); - }); - - 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('.enableFormTracking', () => { - it('tells snowplow to enable form tracking, with only explicit contexts', () => { - const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } }; - Tracking.enableFormTracking(config, ['_passed_context_', standardContext]); - - expect(snowplowSpy).toHaveBeenCalledWith( - 'enableFormTracking', - { forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } }, - ['_passed_context_'], - ); - }); - - it('throws an error if no allow rules are provided', () => { - const expectedError = new Error('Unable to enable form event tracking without allow rules.'); - - expect(() => Tracking.enableFormTracking()).toThrow(expectedError); - expect(() => Tracking.enableFormTracking({ fields: { allow: true } })).toThrow(expectedError); - expect(() => Tracking.enableFormTracking({ fields: { allow: [] } })).not.toThrow( - expectedError, - ); - }); - - it('does not add empty form whitelist rules', () => { - Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } }); - - expect(snowplowSpy).toHaveBeenCalledWith( - 'enableFormTracking', - { fields: { whitelist: ['input-class1'] } }, - [], - ); - }); - - describe('when `document.readyState` does not equal `complete`', () => { - const originalReadyState = document.readyState; - const setReadyState = (value) => { - Object.defineProperty(document, 'readyState', { - value, - configurable: true, - }); - }; - const fireReadyStateChangeEvent = () => { - document.dispatchEvent(new Event('readystatechange')); - }; - - beforeEach(() => { - setReadyState('interactive'); - }); - - afterEach(() => { - setReadyState(originalReadyState); - }); - - it('does not call `window.snowplow` until `readystatechange` is fired and `document.readyState` equals `complete`', () => { - Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } }); - - expect(snowplowSpy).not.toHaveBeenCalled(); - - fireReadyStateChangeEvent(); - - expect(snowplowSpy).not.toHaveBeenCalled(); - - setReadyState('complete'); - fireReadyStateChangeEvent(); - - expect(snowplowSpy).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, - [standardContext], - ); - }); - }); - - describe('.setAnonymousUrls', () => { - afterEach(() => { - window.gl.snowplowPseudonymizedPageUrl = ''; - localStorage.removeItem(URLS_CACHE_STORAGE_KEY); - }); - - it('does nothing if URLs are not provided', () => { - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).not.toHaveBeenCalled(); - expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).toBe(null); - }); - - it('sets the page URL when provided and populates the cache', () => { - window.gl.snowplowPseudonymizedPageUrl = TEST_HOST; - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', TEST_HOST); - expect(JSON.parse(localStorage.getItem(URLS_CACHE_STORAGE_KEY))[0]).toStrictEqual({ - url: TEST_HOST, - referrer: '', - originalUrl: window.location.href, - timestamp: Date.now(), - }); - }); - - it('appends the hash/fragment to the pseudonymized URL', () => { - const hash = 'first-heading'; - window.gl.snowplowPseudonymizedPageUrl = TEST_HOST; - window.location.hash = hash; - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', `${TEST_HOST}#${hash}`); - }); - - it('does not set the referrer URL by default', () => { - window.gl.snowplowPseudonymizedPageUrl = TEST_HOST; - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String)); - }); - - describe('with referrers cache', () => { - const testUrl = '/namespace:1/project:2/-/merge_requests/5'; - const testOriginalUrl = '/my-namespace/my-project/-/merge_requests/'; - const setUrlsCache = (data) => - localStorage.setItem(URLS_CACHE_STORAGE_KEY, JSON.stringify(data)); - - beforeEach(() => { - window.gl.snowplowPseudonymizedPageUrl = TEST_HOST; - Object.defineProperty(document, 'referrer', { value: '', configurable: true }); - }); - - it('does nothing if a referrer can not be found', () => { - setUrlsCache([ - { - url: testUrl, - originalUrl: TEST_HOST, - timestamp: Date.now(), - }, - ]); - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String)); - }); - - it('sets referrer URL from the page URL found in cache', () => { - Object.defineProperty(document, 'referrer', { value: testOriginalUrl }); - setUrlsCache([ - { - url: testUrl, - originalUrl: testOriginalUrl, - timestamp: Date.now(), - }, - ]); - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).toHaveBeenCalledWith('setReferrerUrl', testUrl); - }); - - it('ignores and removes old entries from the cache', () => { - const oldTimestamp = Date.now() - (REFERRER_TTL + 1); - Object.defineProperty(document, 'referrer', { value: testOriginalUrl }); - setUrlsCache([ - { - url: testUrl, - originalUrl: testOriginalUrl, - timestamp: oldTimestamp, - }, - ]); - - Tracking.setAnonymousUrls(); - - expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', testUrl); - expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).not.toContain(oldTimestamp); - }); - }); - }); - - 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(` - <input data-track-${term}="click_input1" data-track-label="_label_" value=0 /> - <input data-track-${term}="click_input2" data-track-value=0 value=0/> - <input type="checkbox" data-track-${term}="toggle_checkbox" value=1 checked/> - <input class="dropdown" data-track-${term}="toggle_dropdown"/> - <div data-track-${term}="nested_event"><span class="nested"></span></div> - <input data-track-bogus="click_bogusinput" data-track-label="_label_" value="_value_"/> - <input data-track-${term}="click_input3" data-track-experiment="example" value="_value_"/> - <input data-track-${term}="event_with_extra" data-track-extra='{ "foo": "bar" }' /> - <input data-track-${term}="event_with_invalid_extra" data-track-extra="invalid_json" /> - `); - }); - - 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: '0', - }); - }); - - 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: '0', - }); - }); - - it('handles checkbox values correctly', () => { - const checkbox = document.querySelector(`[data-track-${term}="toggle_checkbox"]`); - - checkbox.click(); // unchecking - - expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { - value: 0, - }); - - checkbox.click(); // checking - - expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { - value: '1', - }); - }); - - 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('includes experiment data if linked to an experiment', () => { - const mockExperimentData = { - variant: 'candidate', - experiment: 'example', - 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 }, - }); - }); - - it('supports extra data as JSON', () => { - document.querySelector(`[data-track-${term}="event_with_extra"]`).click(); - - expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_extra', { - extra: { foo: 'bar' }, - }); - }); - - it('ignores extra if provided JSON is invalid', () => { - document.querySelector(`[data-track-${term}="event_with_invalid_extra"]`).click(); - - expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_invalid_extra', {}); - }); - }); - - describe.each` - term - ${'event'} - ${'action'} - `('tracking page loaded events with -$term', ({ term }) => { - let eventSpy; - - beforeEach(() => { - eventSpy = jest.spyOn(Tracking, 'event'); - setHTMLFixture(` - <div data-track-${term}="click_link" data-track-label="all_nested_links"> - <input data-track-${term}="render" data-track-label="label1" value=1 data-track-property="_property_"/> - <span data-track-${term}="render" data-track-label="label2" data-track-value=1> - <a href="#" id="link">Something</a> - </span> - <input data-track-${term}="_render_bogus_" data-track-label="label3" value="_value_" data-track-property="_property_"/> - </div> - `); - 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: '1', - property: '_property_', - }, - ], - [ - '_category_', - 'render', - { - label: 'label2', - value: '1', - }, - ], - ]); - }); - - describe.each` - event | actionSuffix - ${'click'} | ${''} - ${'show.bs.dropdown'} | ${'_show'} - ${'hide.bs.dropdown'} | ${'_hide'} - `(`auto-tracking $event events on nested elements`, ({ event, actionSuffix }) => { - let link; - - beforeEach(() => { - link = document.querySelector('#link'); - eventSpy.mockClear(); - }); - - it(`avoids using ancestor [data-track-${term}="render"] tracking configurations`, () => { - link.dispatchEvent(new Event(event, { bubbles: true })); - - expect(eventSpy).not.toHaveBeenCalledWith( - '_category_', - `render${actionSuffix}`, - expect.any(Object), - ); - expect(eventSpy).toHaveBeenCalledWith( - '_category_', - `click_link${actionSuffix}`, - expect.objectContaining({ label: 'all_nested_links' }), - ); - }); - }); - }); - - 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' }); - }); - - it('includes experiment data if linked to an experiment', () => { - const mockExperimentData = { - variant: 'candidate', - experiment: 'darkMode', - }; - getExperimentData.mockReturnValue(mockExperimentData); - - const mixin = Tracking.mixin({ foo: 'bar', experiment: 'darkMode' }); - expect(mixin.computed.trackingOptions()).toEqual({ - foo: 'bar', - context: { - schema: TRACKING_CONTEXT_SCHEMA, - data: mockExperimentData, - }, - }); - }); - - it('does not include experiment data if experiment data does not exist', () => { - const mixin = Tracking.mixin({ foo: 'bar', experiment: 'lightMode' }); - expect(mixin.computed.trackingOptions()).toEqual({ - foo: '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); - }); - }); - }); -}); |