Commit a7476257 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #311 from hypothesis/auth-analytics

Adding auth metrics for login, logout, and signup requested
parents d1063fb8 955923c0
...@@ -41,7 +41,7 @@ var globalGAOptions = function(win, settings){ ...@@ -41,7 +41,7 @@ var globalGAOptions = function(win, settings){
*/ */
// @ngInject // @ngInject
function analytics($analytics, $window, settings) { function analytics($analytics, $window, settings) {
var options = globalGAOptions($window, settings); var options = $window ? globalGAOptions($window, settings) : {};
return { return {
...@@ -67,6 +67,10 @@ function analytics($analytics, $window, settings) { ...@@ -67,6 +67,10 @@ function analytics($analytics, $window, settings) {
HIGHLIGHT_CREATED: 'highlightCreated', HIGHLIGHT_CREATED: 'highlightCreated',
HIGHLIGHT_UPDATED: 'highlightUpdated', HIGHLIGHT_UPDATED: 'highlightUpdated',
HIGHLIGHT_DELETED: 'highlightDeleted', HIGHLIGHT_DELETED: 'highlightDeleted',
LOGIN_FAILURE: 'loginFailure',
LOGIN_SUCCESS: 'loginSuccessful',
LOGOUT_FAILURE: 'logoutFailure',
LOGOUT_SUCCESS: 'logoutSuccessful',
PAGE_NOTE_CREATED: 'pageNoteCreated', PAGE_NOTE_CREATED: 'pageNoteCreated',
PAGE_NOTE_UPDATED: 'pageNoteUpdated', PAGE_NOTE_UPDATED: 'pageNoteUpdated',
PAGE_NOTE_DELETED: 'pageNoteDeleted', PAGE_NOTE_DELETED: 'pageNoteDeleted',
...@@ -74,6 +78,7 @@ function analytics($analytics, $window, settings) { ...@@ -74,6 +78,7 @@ function analytics($analytics, $window, settings) {
REPLY_UPDATED: 'replyUpdated', REPLY_UPDATED: 'replyUpdated',
REPLY_DELETED: 'replyDeleted', REPLY_DELETED: 'replyDeleted',
SIDEBAR_OPENED: 'sidebarOpened', SIDEBAR_OPENED: 'sidebarOpened',
SIGN_UP_REQUESTED: 'signUpRequested',
}, },
}; };
} }
......
...@@ -26,7 +26,7 @@ function authStateFromUserID(userid) { ...@@ -26,7 +26,7 @@ function authStateFromUserID(userid) {
// @ngInject // @ngInject
module.exports = function AppController( module.exports = function AppController(
$document, $location, $rootScope, $route, $scope, $document, $location, $rootScope, $route, $scope,
$window, annotationUI, auth, bridge, drafts, features, frameSync, groups, $window, analytics, annotationUI, auth, bridge, drafts, features, frameSync, groups,
serviceUrl, session, settings, streamer serviceUrl, session, settings, streamer
) { ) {
...@@ -109,6 +109,10 @@ module.exports = function AppController( ...@@ -109,6 +109,10 @@ module.exports = function AppController(
scrollToView('login-form'); scrollToView('login-form');
}; };
$scope.signUp = function(){
analytics.track(analytics.events.SIGN_UP_REQUESTED);
};
// Display the dialog for sharing the current page // Display the dialog for sharing the current page
$scope.share = function () { $scope.share = function () {
$scope.shareDialog.visible = true; $scope.shareDialog.visible = true;
......
...@@ -24,6 +24,10 @@ module.exports = { ...@@ -24,6 +24,10 @@ module.exports = {
* Called when the user clicks on the "Log in" text. * Called when the user clicks on the "Log in" text.
*/ */
onLogin: '&', onLogin: '&',
/**
* Called when the user clicks on the "Sign Up" text.
*/
onSignUp: '&',
/** /**
* Called when the user clicks on the "Log out" text. * Called when the user clicks on the "Log out" text.
*/ */
......
...@@ -3,13 +3,14 @@ ...@@ -3,13 +3,14 @@
var angular = require('angular'); var angular = require('angular');
// @ngInject // @ngInject
function Controller($scope, $timeout, flash, session, formRespond, serviceUrl) { function Controller($scope, $timeout, analytics, flash, session, formRespond, serviceUrl) {
var pendingTimeout = null; var pendingTimeout = null;
function success(data) { function success(data) {
if (data.userid) { if (data.userid) {
$scope.$emit('auth', null, data); $scope.$emit('auth', null, data);
} }
analytics.track(analytics.events.LOGIN_SUCCESS);
angular.copy({}, $scope.model); angular.copy({}, $scope.model);
...@@ -30,6 +31,8 @@ function Controller($scope, $timeout, flash, session, formRespond, serviceUrl) { ...@@ -30,6 +31,8 @@ function Controller($scope, $timeout, flash, session, formRespond, serviceUrl) {
'Please try again later!'; 'Please try again later!';
} }
analytics.track(analytics.events.LOGIN_FAILURE);
return formRespond(form, errors, reason); return formRespond(form, errors, reason);
} }
......
...@@ -17,6 +17,10 @@ class MockSession ...@@ -17,6 +17,10 @@ class MockSession
mockFlash = info: sandbox.spy() mockFlash = info: sandbox.spy()
mockFormRespond = sandbox.spy() mockFormRespond = sandbox.spy()
mockAnalytics = {
track: sandbox.stub(),
events: require('../../analytics')().events,
}
describe 'loginForm.Controller', -> describe 'loginForm.Controller', ->
$scope = null $scope = null
...@@ -33,6 +37,7 @@ describe 'loginForm.Controller', -> ...@@ -33,6 +37,7 @@ describe 'loginForm.Controller', ->
beforeEach module ($provide) -> beforeEach module ($provide) ->
$provide.value '$timeout', sandbox.spy() $provide.value '$timeout', sandbox.spy()
$provide.value 'analytics', mockAnalytics
$provide.value 'flash', mockFlash $provide.value 'flash', mockFlash
$provide.value 'session', new MockSession() $provide.value 'session', new MockSession()
$provide.value 'formRespond', mockFormRespond $provide.value 'formRespond', mockFormRespond
...@@ -49,6 +54,7 @@ describe 'loginForm.Controller', -> ...@@ -49,6 +54,7 @@ describe 'loginForm.Controller', ->
afterEach -> afterEach ->
sandbox.restore() sandbox.restore()
mockAnalytics.track.reset()
describe '#submit()', -> describe '#submit()', ->
it 'should call session methods on submit', -> it 'should call session methods on submit', ->
...@@ -138,6 +144,48 @@ describe 'loginForm.Controller', -> ...@@ -138,6 +144,48 @@ describe 'loginForm.Controller', ->
$scope.$destroy() $scope.$destroy()
assert.calledWith $scope.$emit, 'auth', 'cancel' assert.calledWith $scope.$emit, 'auth', 'cancel'
describe 'auth analytics', ->
it 'should not track login errors for local validation errors', ->
auth.submit
$name: 'login'
$valid: false
$setValidity: sandbox.stub()
assert.notCalled mockAnalytics.track
it 'should track login error when server sends a reason', ->
# Make a mock session that returns an error response with a "reason" but
# no "errors" in the JSON object.
reason = 'Argh, crashed! :|'
myMockSession = new MockSession()
myMockSession.register = (data, callback, errback) ->
errback({data: {reason: reason}})
$promise: {finally: sandbox.stub()}
# Get an AuthController object with our mock session.
authCtrl = $controller(
'loginFormController', {$scope:$scope, session:myMockSession})
form = {$name: 'register', $valid: true}
authCtrl.submit(form)
assert.calledOnce(mockAnalytics.track)
assert.calledWith(mockAnalytics.track, mockAnalytics.events.LOGIN_FAILURE)
it 'should emit an auth event once authenticated', ->
form =
$name: 'login'
$valid: true
$setValidity: sandbox.stub()
sandbox.spy $scope, '$emit'
auth.submit(form)
assert.calledOnce(mockAnalytics.track)
assert.calledWith(mockAnalytics.track, mockAnalytics.events.LOGIN_SUCCESS)
describe 'timeout', -> describe 'timeout', ->
it 'should happen after a period of inactivity', -> it 'should happen after a period of inactivity', ->
sandbox.spy $scope, '$broadcast' sandbox.spy $scope, '$broadcast'
......
...@@ -9,6 +9,7 @@ module.exports = function () { ...@@ -9,6 +9,7 @@ module.exports = function () {
isSidebar: '<', isSidebar: '<',
onShowHelpPanel: '&', onShowHelpPanel: '&',
onLogin: '&', onLogin: '&',
onSignUp: '&',
onLogout: '&', onLogout: '&',
onSharePage: '&', onSharePage: '&',
searchController: '<', searchController: '<',
......
...@@ -43,7 +43,7 @@ function sessionActions(options) { ...@@ -43,7 +43,7 @@ function sessionActions(options) {
* *
* @ngInject * @ngInject
*/ */
function session($http, $resource, $rootScope, annotationUI, auth, function session($http, $q, $resource, $rootScope, analytics, annotationUI, auth,
flash, raven, settings, store) { flash, raven, settings, store) {
// Headers sent by every request made by the session service. // Headers sent by every request made by the session service.
var headers = {}; var headers = {};
...@@ -208,10 +208,14 @@ function session($http, $resource, $rootScope, annotationUI, auth, ...@@ -208,10 +208,14 @@ function session($http, $resource, $rootScope, annotationUI, auth,
auth.clearCache(); auth.clearCache();
}).catch(function (err) { }).catch(function (err) {
flash.error('Log out failed'); flash.error('Log out failed');
throw err; analytics.track(analytics.events.LOGOUT_FAILURE);
return $q.reject(new Error(err));
}).then(function(){
analytics.track(analytics.events.LOGOUT_SUCCESS);
}); });
} }
return { return {
dismissSidebarTutorial: dismissSidebarTutorial, dismissSidebarTutorial: dismissSidebarTutorial,
load: resource.load, load: resource.load,
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
<top-bar <top-bar
auth="auth" auth="auth"
on-login="login()" on-login="login()"
on-sign-up="signUp()"
on-logout="logout()" on-logout="logout()"
on-share-page="share()" on-share-page="share()"
on-show-help-panel="helpPanel.visible = true" on-show-help-panel="helpPanel.visible = true"
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
ng-if="vm.newStyle && vm.auth.status === 'unknown'"></span> ng-if="vm.newStyle && vm.auth.status === 'unknown'"></span>
<span class="login-text" <span class="login-text"
ng-if="vm.newStyle && vm.auth.status === 'logged-out'"> ng-if="vm.newStyle && vm.auth.status === 'logged-out'">
<a href="{{vm.serviceUrl('signup')}}" target="_blank" h-branding="highlightColor">Sign up</a> <a href="{{vm.serviceUrl('signup')}}" ng-click="vm.onSignUp()" target="_blank" h-branding="highlightColor">Sign up</a>
/ <a href="" ng-click="vm.onLogin()" h-branding="highlightColor">Log in</a> / <a href="" ng-click="vm.onLogin()" h-branding="highlightColor">Log in</a>
</span> </span>
<div ng-if="vm.newStyle" <div ng-if="vm.newStyle"
......
...@@ -56,7 +56,8 @@ ...@@ -56,7 +56,8 @@
new-style="true" new-style="true"
on-show-help-panel="onShowHelpPanel()" on-show-help-panel="onShowHelpPanel()"
on-login="onLogin()" on-login="onLogin()"
on-logout="onLogout()"> on-logout="onLogout()"
on-sign-up="onSignUp()">
</login-control> </login-control>
</div> </div>
</div> </div>
...@@ -69,7 +69,7 @@ describe('AppController', function () { ...@@ -69,7 +69,7 @@ describe('AppController', function () {
fakeAnalytics = { fakeAnalytics = {
track: sandbox.stub(), track: sandbox.stub(),
events: {}, events: require('../analytics')().events,
}; };
fakeAuth = {}; fakeAuth = {};
...@@ -256,6 +256,12 @@ describe('AppController', function () { ...@@ -256,6 +256,12 @@ describe('AppController', function () {
assert.calledOnce(fakeRoute.reload); assert.calledOnce(fakeRoute.reload);
}); });
it('tracks sign up requests in analytics', function () {
createController();
$scope.signUp();
assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.SIGN_UP_REQUESTED);
});
describe('#login()', function () { describe('#login()', function () {
it('shows the login dialog if not using a third-party service', function () { it('shows the login dialog if not using a third-party service', function () {
// If no third-party annotation service is in use then it should show the // If no third-party annotation service is in use then it should show the
......
...@@ -10,6 +10,7 @@ describe('session', function () { ...@@ -10,6 +10,7 @@ describe('session', function () {
var $httpBackend; var $httpBackend;
var $rootScope; var $rootScope;
var fakeAnalytics;
var fakeAuth; var fakeAuth;
var fakeFlash; var fakeFlash;
var fakeRaven; var fakeRaven;
...@@ -27,6 +28,10 @@ describe('session', function () { ...@@ -27,6 +28,10 @@ describe('session', function () {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
var state = {}; var state = {};
fakeAnalytics = {
track: sinon.stub(),
events: require('../analytics')().events,
};
var fakeAnnotationUI = { var fakeAnnotationUI = {
getState: function () { getState: function () {
return {session: state}; return {session: state};
...@@ -53,6 +58,7 @@ describe('session', function () { ...@@ -53,6 +58,7 @@ describe('session', function () {
}; };
mock.module('h', { mock.module('h', {
analytics: fakeAnalytics,
annotationUI: fakeAnnotationUI, annotationUI: fakeAnnotationUI,
auth: fakeAuth, auth: fakeAuth,
flash: fakeFlash, flash: fakeFlash,
...@@ -296,9 +302,10 @@ describe('session', function () { ...@@ -296,9 +302,10 @@ describe('session', function () {
}); });
describe('#logout()', function () { describe('#logout()', function () {
var postExpectation;
beforeEach(function () { beforeEach(function () {
var url = 'https://test.hypothes.is/root/app?__formid__=logout'; var logoutUrl = 'https://test.hypothes.is/root/app?__formid__=logout';
$httpBackend.expectPOST(url).respond(200, { postExpectation = $httpBackend.expectPOST(logoutUrl).respond(200, {
model: { model: {
userid: 'logged-out-id', userid: 'logged-out-id',
}, },
...@@ -318,5 +325,22 @@ describe('session', function () { ...@@ -318,5 +325,22 @@ describe('session', function () {
}); });
$httpBackend.flush(); $httpBackend.flush();
}); });
it('tracks successful logout actions in analytics', function () {
session.logout().then(function () {
assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.LOGOUT_SUCCESS);
});
$httpBackend.flush();
});
it('tracks unsuccessful logout actions in analytics', function () {
postExpectation.respond(500);
session.logout().catch(function(){
assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.LOGOUT_FAILURE);
});
$httpBackend.flush();
});
}); });
}); });
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