Commit 9d639fea authored by Sean Roberts's avatar Sean Roberts Committed by GitHub

Merge pull request #194 from hypothesis/client-metrics-tracking

Add analytics tracking
parents 547c01cb 42015a5e
'use strict';
var VIA_REFERRER = /^https:\/\/(qa-)?via.hypothes.is\//;
var globalGAOptions = function(win, settings){
settings = settings || {};
var globalOpts = {
category: '',
};
var validTypes = ['chrome-extension', 'embed', 'bookmarklet', 'via'];
// The preferred method for deciding what type of app is running is
// through the setting of the appType to one of the valid types above.
// However, we also want to capture app types where we were not given
// the appType setting explicitly - these are the app types that were
// added before we added the analytics logic
if(validTypes.indexOf((settings.appType || '').toLowerCase()) > -1){
globalOpts.category = settings.appType.toLowerCase();
}else if(win.location.protocol === 'chrome-extension:'){
globalOpts.category = 'chrome-extension';
}else if(VIA_REFERRER.test(win.document.referrer)){
globalOpts.category = 'via';
}else {
globalOpts.category = 'embed';
}
return globalOpts;
};
/**
* Analytics API to simplify and standardize the values that we
* pass to the Angulartics service.
*
* These analytics are based on google analytics and need to conform to its
* requirements. Specifically, we are required to send the event and a category.
*
* We will standardize the category to be the appType of the client settings
*/
// @ngInject
function analytics($analytics, $window, settings) {
var options = globalGAOptions($window, settings);
return {
/**
* @param {string} event This is the event name that we are capturing
* in our analytics. Example: 'sidebarOpened'. Use camelCase to track multiple
* words.
*/
track: function(event){
$analytics.eventTrack(event, options);
},
};
}
module.exports = analytics;
...@@ -164,6 +164,7 @@ module.exports = angular.module('h', [ ...@@ -164,6 +164,7 @@ module.exports = angular.module('h', [
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
.directive('windowScroll', require('./directive/window-scroll')) .directive('windowScroll', require('./directive/window-scroll'))
.service('analytics', require('./analytics'))
.service('annotationMapper', require('./annotation-mapper')) .service('annotationMapper', require('./annotation-mapper'))
.service('annotationUI', require('./annotation-ui')) .service('annotationUI', require('./annotation-ui'))
.service('auth', require('./auth').service) .service('auth', require('./auth').service)
......
'use strict';
var analyticsService = require('../analytics');
describe('analytics', function () {
var $analyticsStub;
var $windowStub;
var eventTrackStub;
beforeEach(function () {
$analyticsStub = {
eventTrack: sinon.stub(),
};
eventTrackStub = $analyticsStub.eventTrack;
$windowStub = {
location: {
href: '',
protocol: 'https:',
},
document: {
referrer: '',
},
};
});
describe('applying global category based on environment contexts', function () {
it('sets the category to match the appType setting value', function(){
var validTypes = ['chrome-extension', 'embed', 'bookmarklet', 'via'];
validTypes.forEach(function(appType, index){
analyticsService($analyticsStub, $windowStub, {appType: appType}).track('event' + index);
assert.deepEqual(eventTrackStub.args[index], ['event' + index, {category: appType}]);
});
});
it('sets category as embed if no other matches can be made', function () {
analyticsService($analyticsStub, $windowStub).track('eventA');
assert.deepEqual(eventTrackStub.args[0], ['eventA', {category: 'embed'}]);
});
it('sets category as via if url matches the via uri pattern', function () {
$windowStub.document.referrer = 'https://via.hypothes.is/';
analyticsService($analyticsStub, $windowStub).track('eventA');
assert.deepEqual(eventTrackStub.args[0], ['eventA', {category: 'via'}]);
// match staging as well
$windowStub.document.referrer = 'https://qa-via.hypothes.is/';
analyticsService($analyticsStub, $windowStub).track('eventB');
assert.deepEqual(eventTrackStub.args[1], ['eventB', {category: 'via'}]);
});
it('sets category as chrome-extension if protocol matches chrome-extension:', function () {
$windowStub.location.protocol = 'chrome-extension:';
analyticsService($analyticsStub, $windowStub).track('eventA');
assert.deepEqual(eventTrackStub.args[0], ['eventA', {category: 'chrome-extension'}]);
});
});
});
...@@ -40,6 +40,7 @@ describe('WidgetController', function () { ...@@ -40,6 +40,7 @@ describe('WidgetController', function () {
var $rootScope; var $rootScope;
var $scope; var $scope;
var annotationUI; var annotationUI;
var fakeAnalytics;
var fakeAnnotationMapper; var fakeAnnotationMapper;
var fakeDrafts; var fakeDrafts;
var fakeFeatures; var fakeFeatures;
...@@ -69,6 +70,10 @@ describe('WidgetController', function () { ...@@ -69,6 +70,10 @@ describe('WidgetController', function () {
searchClients = []; searchClients = [];
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
fakeAnalytics = {
track: sandbox.spy(),
};
fakeAnnotationMapper = { fakeAnnotationMapper = {
loadAnnotations: sandbox.spy(), loadAnnotations: sandbox.spy(),
unloadAnnotations: sandbox.spy(), unloadAnnotations: sandbox.spy(),
...@@ -112,6 +117,7 @@ describe('WidgetController', function () { ...@@ -112,6 +117,7 @@ describe('WidgetController', function () {
search: sinon.stub(), search: sinon.stub(),
}; };
$provide.value('analytics', fakeAnalytics);
$provide.value('annotationMapper', fakeAnnotationMapper); $provide.value('annotationMapper', fakeAnnotationMapper);
$provide.value('drafts', fakeDrafts); $provide.value('drafts', fakeDrafts);
$provide.value('features', fakeFeatures); $provide.value('features', fakeFeatures);
......
...@@ -32,8 +32,8 @@ function groupIDFromSelection(selection, results) { ...@@ -32,8 +32,8 @@ function groupIDFromSelection(selection, results) {
// @ngInject // @ngInject
module.exports = function WidgetController( module.exports = function WidgetController(
$scope, annotationUI, annotationMapper, drafts, features, frameSync, groups, $scope, analytics, annotationUI, annotationMapper, drafts, features, frameSync,
rootThread, settings, streamer, streamFilter, store groups, rootThread, settings, streamer, streamFilter, store
) { ) {
function thread() { function thread() {
return rootThread.thread(annotationUI.getState()); return rootThread.thread(annotationUI.getState());
...@@ -201,6 +201,9 @@ module.exports = function WidgetController( ...@@ -201,6 +201,9 @@ module.exports = function WidgetController(
} }
$scope.$on('sidebarOpened', function () { $scope.$on('sidebarOpened', function () {
analytics.track('sidebarOpened');
streamer.connect(); streamer.connect();
}); });
......
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