diff options
-rw-r--r-- | app/assets/javascripts/main.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/tracking.js | 28 | ||||
-rw-r--r-- | app/views/layouts/_snowplow.html.haml | 21 | ||||
-rw-r--r-- | lib/gitlab/snowplow_tracker.rb | 35 | ||||
-rw-r--r-- | lib/gitlab/tracking.rb | 44 | ||||
-rw-r--r-- | spec/frontend/tracking_spec.js | 46 | ||||
-rw-r--r-- | spec/lib/gitlab/snowplow_tracker_spec.rb | 45 | ||||
-rw-r--r-- | spec/lib/gitlab/tracking_spec.rb | 88 |
8 files changed, 204 insertions, 105 deletions
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 39f2097c174..0ddf40b0405 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -35,6 +35,7 @@ import initPerformanceBar from './performance_bar'; import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; +import { initUserTracking } from './tracking'; import { __ } from './locale'; import 'ee_else_ce/main_ee'; @@ -94,6 +95,7 @@ function deferredInitialisation() { initLogoAnimation(); initUsagePingConsent(); initUserPopovers(); + initUserTracking(); if (document.querySelector('.search')) initSearchAutocomplete(); diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index a852f937eec..03281b5ef49 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -1,5 +1,23 @@ import $ from 'jquery'; +const DEFAULT_SNOWPLOW_OPTIONS = { + namespace: 'gl', + hostname: window.location.hostname, + cookieDomain: window.location.hostname, + appId: '', + userFingerprint: false, + respectDoNotTrack: true, + forceSecureTracker: true, + eventMethod: 'post', + contexts: { webPage: true }, + // Page tracking tracks a single event when the page loads. + pageTrackingEnabled: false, + // Activity tracking tracks when a user is still interacting with the page. + // Events like scrolling and mouse movements are used to determine if the + // user has the tab active and is still actively engaging. + activityTrackingEnabled: false, +}; + const extractData = (el, opts = {}) => { const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset; let trackValue = el.dataset.trackValue || el.value || ''; @@ -71,3 +89,13 @@ export default class Tracking { }; } } + +export function initUserTracking() { + if (!Tracking.enabled()) return; + + const opts = Object.assign({}, DEFAULT_SNOWPLOW_OPTIONS, window.snowplowOptions); + window.snowplow('newTracker', opts.namespace, opts.hostname, opts); + + if (opts.activityTrackingEnabled) window.snowplow('enableActivityTracking', 30, 30); + if (opts.pageTrackingEnabled) window.snowplow('trackPageView'); // must be after enableActivityTracking +} diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml index 5f5c5e984c5..d7ff5ad1094 100644 --- a/app/views/layouts/_snowplow.html.haml +++ b/app/views/layouts/_snowplow.html.haml @@ -7,23 +7,4 @@ };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); - window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_hostname}', { - appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', - cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', - userFingerprint: false, - respectDoNotTrack: true, - forceSecureTracker: true, - post: true, - contexts: { webPage: true }, - stateStorageStrategy: "localStorage" - }); - - window.snowplow('enableActivityTracking', 30, 30); - window.snowplow('trackPageView'); - -- return unless Feature.enabled?(:additional_snowplow_tracking, @group) - -= javascript_tag nonce: true do - :plain - window.snowplow('enableFormTracking'); - window.snowplow('enableLinkClickTracking'); + window.snowplowOptions = #{Gitlab::Tracking.snowplow_options(@group).to_json} diff --git a/lib/gitlab/snowplow_tracker.rb b/lib/gitlab/snowplow_tracker.rb deleted file mode 100644 index 9f12513e09e..00000000000 --- a/lib/gitlab/snowplow_tracker.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'snowplow-tracker' - -module Gitlab - module SnowplowTracker - NAMESPACE = 'cf' - - class << self - def track_event(category, action, label: nil, property: nil, value: nil, context: nil) - tracker&.track_struct_event(category, action, label, property, value, context, Time.now.to_i) - end - - private - - def tracker - return unless enabled? - - @tracker ||= ::SnowplowTracker::Tracker.new(emitter, subject, NAMESPACE, Gitlab::CurrentSettings.snowplow_site_id) - end - - def subject - ::SnowplowTracker::Subject.new - end - - def emitter - ::SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname) - end - - def enabled? - Gitlab::CurrentSettings.snowplow_enabled? - end - end - end -end diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb new file mode 100644 index 00000000000..ef669b03c87 --- /dev/null +++ b/lib/gitlab/tracking.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'snowplow-tracker' + +module Gitlab + module Tracking + SNOWPLOW_NAMESPACE = 'gl' + + class << self + def enabled? + Gitlab::CurrentSettings.snowplow_enabled? + end + + def event(category, action, label: nil, property: nil, value: nil, context: nil) + return unless enabled? + + snowplow.track_struct_event(category, action, label, property, value, context, Time.now.to_i) + end + + def snowplow_options(group) + additional_features = Feature.enabled?(:additional_snowplow_tracking, group) + { + namespace: SNOWPLOW_NAMESPACE, + hostname: Gitlab::CurrentSettings.snowplow_collector_hostname, + cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain, + app_id: Gitlab::CurrentSettings.snowplow_site_id, + page_tracking_enabled: additional_features, + activity_tracking_enabled: additional_features + }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym } + end + + private + + def snowplow + @snowplow ||= SnowplowTracker::Tracker.new( + SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname), + SnowplowTracker::Subject.new, + SNOWPLOW_NAMESPACE, + Gitlab::CurrentSettings.snowplow_site_id + ) + end + end + end +end diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js index 0e862c683d3..7c98a1a66c9 100644 --- a/spec/frontend/tracking_spec.js +++ b/spec/frontend/tracking_spec.js @@ -1,20 +1,56 @@ import $ from 'jquery'; import { setHTMLFixture } from './helpers/fixtures'; -import Tracking from '~/tracking'; +import Tracking, { initUserTracking } from '~/tracking'; describe('Tracking', () => { + let snowplowSpy; + beforeEach(() => { window.snowplow = window.snowplow || (() => {}); + window.snowplowOptions = { + namespace: '_namespace_', + hostname: 'app.gitfoo.com', + cookieDomain: '.gitfoo.com', + }; + snowplowSpy = jest.spyOn(window, 'snowplow'); }); - describe('.event', () => { - let snowplowSpy = null; + 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 }, + activityTrackingEnabled: false, + pageTrackingEnabled: false, + }); + }); - beforeEach(() => { - snowplowSpy = jest.spyOn(window, 'snowplow'); + it('should activate features based on what has been enabled', () => { + initUserTracking(); + expect(snowplowSpy).not.toHaveBeenCalledWith('enableActivityTracking', 30, 30); + expect(snowplowSpy).not.toHaveBeenCalledWith('trackPageView'); + + window.snowplowOptions = Object.assign({}, window.snowplowOptions, { + activityTrackingEnabled: true, + pageTrackingEnabled: true, + }); + + initUserTracking(); + expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30); + expect(snowplowSpy).toHaveBeenCalledWith('trackPageView'); }); + }); + describe('.event', () => { afterEach(() => { window.doNotTrack = undefined; navigator.doNotTrack = undefined; diff --git a/spec/lib/gitlab/snowplow_tracker_spec.rb b/spec/lib/gitlab/snowplow_tracker_spec.rb deleted file mode 100644 index 073a33e5973..00000000000 --- a/spec/lib/gitlab/snowplow_tracker_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -describe Gitlab::SnowplowTracker do - let(:timestamp) { Time.utc(2017, 3, 22) } - - around do |example| - Timecop.freeze(timestamp) { example.run } - end - - subject { described_class.track_event('epics', 'action', property: 'what', value: 'doit') } - - context '.track_event' do - context 'when Snowplow tracker is disabled' do - it 'does not track the event' do - expect(SnowplowTracker::Tracker).not_to receive(:new) - - subject - end - end - - context 'when Snowplow tracker is enabled' do - before do - stub_application_setting(snowplow_enabled: true) - stub_application_setting(snowplow_site_id: 'awesome gitlab') - stub_application_setting(snowplow_collector_hostname: 'url.com') - end - - it 'tracks the event' do - tracker = double - - expect(::SnowplowTracker::Tracker).to receive(:new) - .with( - an_instance_of(::SnowplowTracker::Emitter), - an_instance_of(::SnowplowTracker::Subject), - 'cf', 'awesome gitlab' - ).and_return(tracker) - expect(tracker).to receive(:track_struct_event) - .with('epics', 'action', nil, 'what', 'doit', nil, timestamp.to_i) - - subject - end - end - end -end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb new file mode 100644 index 00000000000..f14e74427e1 --- /dev/null +++ b/spec/lib/gitlab/tracking_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Tracking do + let(:timestamp) { Time.utc(2017, 3, 22) } + + before do + stub_application_setting(snowplow_enabled: true) + stub_application_setting(snowplow_collector_hostname: 'gitfoo.com') + stub_application_setting(snowplow_cookie_domain: '.gitfoo.com') + stub_application_setting(snowplow_site_id: '_abc123_') + end + + describe '.snowplow_options' do + subject(&method(:described_class)) + + it 'returns useful client options' do + expect(subject.snowplow_options(nil)).to eq( + namespace: 'gl', + hostname: 'gitfoo.com', + cookieDomain: '.gitfoo.com', + appId: '_abc123_', + pageTrackingEnabled: true, + activityTrackingEnabled: true + ) + end + + it 'enables features using feature flags' do + stub_feature_flags(additional_snowplow_tracking: true) + allow(Feature).to receive(:enabled?).with( + :additional_snowplow_tracking, + '_group_' + ).and_return(false) + + expect(subject.snowplow_options('_group_')).to include( + pageTrackingEnabled: false, + activityTrackingEnabled: false + ) + end + end + + describe '.event' do + subject(&method(:described_class)) + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + it 'can track events' do + tracker = double + + expect(SnowplowTracker::Emitter).to receive(:new).with( + 'gitfoo.com' + ).and_return('_emitter_') + + expect(SnowplowTracker::Tracker).to receive(:new).with( + '_emitter_', + an_instance_of(SnowplowTracker::Subject), + 'gl', + '_abc123_' + ).and_return(tracker) + + expect(tracker).to receive(:track_struct_event).with( + 'category', + 'action', + '_label_', + '_property_', + '_value_', + '_context_', + timestamp.to_i + ) + + subject.event('category', 'action', + label: '_label_', + property: '_property_', + value: '_value_', + context: '_context_' + ) + end + + it 'does not track when not enabled' do + stub_application_setting(snowplow_enabled: false) + expect(SnowplowTracker::Tracker).not_to receive(:new) + + subject.event('epics', 'action', property: 'what', value: 'doit') + end + end +end |