diff --git a/src/core/server/base.js b/src/core/server/base.js index 1a39a92378d..f03fc8c0c8d 100644 --- a/src/core/server/base.js +++ b/src/core/server/base.js @@ -79,7 +79,10 @@ export function getPageProps({ noScriptStyles = '', store, req, res }) { noScriptStyles, sriData, store, - trackingEnabled: convertBoolean(config.get('trackingEnabled')), + trackingEnabled: ( + convertBoolean(config.get('trackingEnabled')) && + req.header('dnt') !== '1' // A DNT header set to "1" means Do Not Track + ), }; } diff --git a/src/core/tracking.js b/src/core/tracking.js index 5729f5cbe02..47279717fe7 100644 --- a/src/core/tracking.js +++ b/src/core/tracking.js @@ -1,5 +1,6 @@ -/* global window */ +/* global navigator, window */ /* eslint-disable no-underscore-dangle */ +import { oneLine } from 'common-tags'; import config from 'config'; import { convertBoolean } from 'core/utils'; @@ -105,8 +106,35 @@ export function getAction(type) { }[type] || TRACKING_TYPE_INVALID; } +export function isDoNotTrackEnabled({ + _log = log, + _navigator = typeof navigator !== 'undefined' ? navigator : null, + _window = typeof window !== 'undefined' ? window : null, +} = {}) { + if (!_navigator || !_window) { + return false; + } + + // We ignore things like `msDoNotTrack` because they are for older, + // unsupported browsers and don't really respect the DNT spec. This + // covers new versions of IE/Edge, Firefox from 32+, Chrome, Safari, and + // any browsers built on these stacks (Chromium, Tor Browser, etc.). + const dnt = _navigator.doNotTrack || _window.doNotTrack; + if (dnt === '1') { + _log.log(oneLine`[TRACKING]: Do Not Track Enabled; Google Analytics not + loaded and tracking disabled.`); + return true; + } + + // Known DNT values not set, so we will assume it's off. + return false; +} + export default new Tracking({ - trackingEnabled: convertBoolean(config.get('trackingEnabled')), + trackingEnabled: ( + convertBoolean(config.get('trackingEnabled')) && + doNotTrackEnabled() + ), trackingId: config.get('trackingId'), trackingSendInitPageView: config.get('trackingSendInitPageView'), }); diff --git a/tests/unit/core/test_tracking.js b/tests/unit/core/test_tracking.js index b05ac9ba081..f7125e31588 100644 --- a/tests/unit/core/test_tracking.js +++ b/tests/unit/core/test_tracking.js @@ -1,6 +1,7 @@ /* global window */ +import { oneLine } from 'common-tags'; -import { Tracking, getAction } from 'core/tracking'; +import { Tracking, isDoNotTrackEnabled, getAction } from 'core/tracking'; import { ADDON_TYPE_EXTENSION, ADDON_TYPE_THEME, @@ -31,7 +32,7 @@ describe('Tracking', () => { info: sinon.stub(), }, }); - expect(tracking._log.info.calledWith(sinon.match(/OFF/), 'Tracking init')).toBeTruthy(); + expect(tracking._log.info.calledWith(sinon.match(/OFF/), 'Tracking init')).toBe(true); }); it('should log OFF when not enabled due to missing id', () => { @@ -44,7 +45,7 @@ describe('Tracking', () => { }); expect( tracking._log.info.secondCall.calledWith(sinon.match(/OFF/), 'Missing tracking id') - ).toBeTruthy(); + ).toBe(true); }); it('should send initial page view when enabled', () => { @@ -56,7 +57,7 @@ describe('Tracking', () => { info: sinon.stub(), }, }); - expect(window.ga.calledWith('send', 'pageview')).toBeTruthy(); + expect(window.ga.calledWith('send', 'pageview')).toBe(true); }); it('should not send initial page view when disabled', () => { @@ -68,7 +69,7 @@ describe('Tracking', () => { info: sinon.stub(), }, }); - expect(window.ga.calledWith('send', 'pageview')).toBeFalsy(); + expect(window.ga.calledWith('send', 'pageview')).toBe(false); }); it('should throw if page not set', () => { @@ -79,7 +80,7 @@ describe('Tracking', () => { it('should call ga with setPage', () => { tracking.setPage('whatever'); - expect(window.ga.called).toBeTruthy(); + expect(window.ga.called).toBe(true); }); it('should throw if category not set', () => { @@ -101,7 +102,7 @@ describe('Tracking', () => { category: 'whatever', action: 'some-action', }); - expect(window.ga.called).toBeTruthy(); + expect(window.ga.called).toBe(true); }); it('should call _ga when pageView is called', () => { @@ -110,7 +111,7 @@ describe('Tracking', () => { dimension2: 'whatever2', }; tracking.pageView(data); - expect(window.ga.calledWith('send', 'pageview', data)).toBeTruthy(); + expect(window.ga.calledWith('send', 'pageview', data)).toBe(true); }); }); @@ -127,3 +128,68 @@ describe('getAction', () => { expect(getAction('whatever')).toEqual('invalid'); }); }); + +describe('Do Not Track', () => { + it('should respect DNT when enabled', () => { + expect(isDoNotTrackEnabled({ + _navigator: { doNotTrack: '1' }, + _window: {}, + })).toBe(true); + expect(isDoNotTrackEnabled({ + _navigator: {}, + _window: { doNotTrack: '1' }, + })).toBe(true); + }); + + it('should respect not enabled DNT', () => { + expect(isDoNotTrackEnabled({ + _navigator: { doNotTrack: '0' }, + _window: {}, + })).toBe(false); + expect(isDoNotTrackEnabled({ + _navigator: {}, + _window: { doNotTrack: '0' }, + })).toBe(false); + }); + + it('should treat unknown values as no DNT', () => { + expect(isDoNotTrackEnabled({ + _navigator: { doNotTrack: 'leave me alone' }, + _window: {}, + })).toBe(false); + expect(isDoNotTrackEnabled({ + _navigator: {}, + _window: { doNotTrack: 'leave me alone' }, + })).toBe(false); + }); + + it('should handle missing navigator and window', () => { + expect(isDoNotTrackEnabled({ _navigator: null })).toBe(false); + expect(isDoNotTrackEnabled({ _window: null })).toBe(false); + }); + + it('should log that DNT disabled tracking', () => { + const fakeLog = { log: sinon.stub() }; + isDoNotTrackEnabled({ + _log: fakeLog, + _navigator: { doNotTrack: '1' }, + _window: {}, + }); + + sinon.assert.calledWith(fakeLog.log, oneLine`[TRACKING]: Do Not Track + Enabled; Google Analytics not loaded and tracking disabled.`); + sinon.assert.calledOnce(fakeLog.log); + + // Check with `window.doNotTrack` as well, just for completeness. + fakeLog.log.reset(); + isDoNotTrackEnabled({ + _log: fakeLog, + _navigator: {}, + _window: { doNotTrack: '1' }, + }); + + sinon.assert.calledWith(fakeLog.log, oneLine`[TRACKING]: Do Not Track + Enabled; Google Analytics not loaded and tracking disabled.`); + sinon.assert.calledOnce(fakeLog.log); + }); +});