feat: Add front end metrics gathering.
* Use SpeedTrap to collect client side metrics * Add screen views and errors to an event stream. * Metrics are sent to the `/metrics` endpoint on document unload or after 10 minutes of inactivity. * Data that is sent to the backend is filtered - only data we expect is sent. * Data collection sample rate is set by server configuration. Other logged events: * users who cancel login from the browser. * complete_reset_password:link_damaged * complete_reset_password:link_expired * complete_signn_up:link_damaged * confirm:too_many_attempts * confirm:resend * confirm_reset_password_resend * login:canceled Others changes: * AuthErrors.toMessage now accepts `forceMessage` * Change AuthErrors.toCode to accept an error object. * Give `Session expired` an error code of 1002. issue #1119
This commit is contained in:
Родитель
e6adb592eb
Коммит
084fce06ae
|
@ -28,7 +28,9 @@ define([
|
|||
'lib/url',
|
||||
'lib/channels/web',
|
||||
'lib/channels/fx-desktop',
|
||||
'lib/config-loader'
|
||||
'lib/config-loader',
|
||||
'lib/metrics',
|
||||
'lib/null-metrics'
|
||||
],
|
||||
function (
|
||||
_,
|
||||
|
@ -41,7 +43,9 @@ function (
|
|||
Url,
|
||||
WebChannel,
|
||||
FxDesktopChannel,
|
||||
ConfigLoader
|
||||
ConfigLoader,
|
||||
Metrics,
|
||||
NullMetrics
|
||||
) {
|
||||
|
||||
function getChannel() {
|
||||
|
@ -75,11 +79,24 @@ function (
|
|||
setSessionValueFromUrl('context');
|
||||
}
|
||||
|
||||
function isMetricsCollectionEnabled (sampleRate) {
|
||||
return Math.random() <= sampleRate;
|
||||
}
|
||||
|
||||
function createMetrics(sampleRate) {
|
||||
if (isMetricsCollectionEnabled(sampleRate)) {
|
||||
return new Metrics();
|
||||
}
|
||||
|
||||
return new NullMetrics();
|
||||
}
|
||||
|
||||
function Start(options) {
|
||||
options = options || {};
|
||||
|
||||
this._window = options.window || window;
|
||||
this._window.router = this._router = options.router || new Router();
|
||||
this._router = options.router;
|
||||
|
||||
this._history = options.history || Backbone.history;
|
||||
this._configLoader = new ConfigLoader();
|
||||
}
|
||||
|
@ -101,11 +118,19 @@ function (
|
|||
.then(_.bind(this.useConfig, this));
|
||||
},
|
||||
|
||||
useConfig: function(config) {
|
||||
useConfig: function (config) {
|
||||
this._config = config;
|
||||
this._configLoader.useConfig(config);
|
||||
Session.set('config', config);
|
||||
Session.set('language', config.language);
|
||||
|
||||
this._metrics = createMetrics(config.metricsSampleRate);
|
||||
this._metrics.init();
|
||||
|
||||
if (! this._router) {
|
||||
this._router = new Router({ metrics: this._metrics });
|
||||
}
|
||||
this._window.router = this._router;
|
||||
},
|
||||
|
||||
initializeL10n: function () {
|
||||
|
|
|
@ -36,13 +36,16 @@ function () {
|
|||
SERVICE_UNAVAILABLE: 998,
|
||||
SERVER_BUSY: 201,
|
||||
ENDPOINT_NOT_SUPPORTED: 116,
|
||||
USER_CANCELED_LOGIN: 1001 // local only error code for when user cancels desktop login
|
||||
|
||||
// local only error codes
|
||||
USER_CANCELED_LOGIN: 1001,
|
||||
SESSION_EXPIRED: 1002
|
||||
};
|
||||
|
||||
var CODE_TO_MESSAGES = {
|
||||
// errors returned by the auth server
|
||||
999: t('Unexpected error'),
|
||||
110: t('Invalid authentication token in request signature'),
|
||||
110: t('Invalid token'),
|
||||
111: t('Invalid timestamp in request signature'),
|
||||
115: t('Invalid nonce in request signature'),
|
||||
101: t('Account already exists'),
|
||||
|
@ -60,7 +63,10 @@ function () {
|
|||
114: t('Attempt limit exceeded.'),
|
||||
998: t('System unavailable, try again soon'),
|
||||
201: t('Server busy, try again soon'),
|
||||
116: t('This endpoint is no longer supported')
|
||||
116: t('This endpoint is no longer supported'),
|
||||
|
||||
// local only error messages
|
||||
1002: t('Session expired. Sign in to continue.')
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -72,6 +78,8 @@ function () {
|
|||
|
||||
if (typeof err === 'number') {
|
||||
code = err;
|
||||
} else if (err && err.forceMessage) {
|
||||
return err.forceMessage;
|
||||
// error from backend
|
||||
} else if (err && typeof err.errno === 'number') {
|
||||
code = err.errno;
|
||||
|
@ -118,10 +126,10 @@ function () {
|
|||
},
|
||||
|
||||
/**
|
||||
* Convert a text type from ERROR_TO_CODE to a numeric code
|
||||
* Convert an error or a text type from ERROR_TO_CODE to a numeric code
|
||||
*/
|
||||
toCode: function (type) {
|
||||
return ERROR_TO_CODE[type];
|
||||
return type.errno || ERROR_TO_CODE[type] || type;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* A metrics module!
|
||||
*
|
||||
* An instantiated metrics object has two primary APIs:
|
||||
*
|
||||
* metrics.logEvent(<event_name>);
|
||||
* metrics.startTimer(<timer_name>)/metrics.stopTimer(<timer_name);
|
||||
*
|
||||
* Metrics are automatically sent to the server on window.unload
|
||||
* but can also be sent by calling metrics.flush();
|
||||
*/
|
||||
|
||||
define([
|
||||
'underscore',
|
||||
'backbone',
|
||||
'jquery',
|
||||
'speedTrap',
|
||||
'p-promise',
|
||||
'lib/url'
|
||||
], function (_, Backbone, $, speedTrap, p, Url) {
|
||||
'use strict';
|
||||
|
||||
// Speed trap is a singleton, convert it
|
||||
// to an instantiable function.
|
||||
var SpeedTrap = function() {};
|
||||
SpeedTrap.prototype = speedTrap;
|
||||
|
||||
var ALLOWED_FIELDS = [
|
||||
'date',
|
||||
'navigationTiming',
|
||||
'referrer',
|
||||
'duration',
|
||||
'timers',
|
||||
'events',
|
||||
'context',
|
||||
'service'
|
||||
];
|
||||
|
||||
var TEN_MINS_MS = 10 * 60 * 1000;
|
||||
|
||||
function getDate() {
|
||||
var roundedDate = new Date();
|
||||
roundedDate.setHours(0, 0, 0, 0);
|
||||
|
||||
return roundedDate;
|
||||
}
|
||||
|
||||
function Metrics (options) {
|
||||
options = options || {};
|
||||
|
||||
// by default, send the metrics to the content server.
|
||||
this._collector = options.collector || '';
|
||||
|
||||
this._ajax = options.ajax || $.ajax;
|
||||
|
||||
this._speedTrap = new SpeedTrap();
|
||||
this._speedTrap.init();
|
||||
|
||||
// `timers` and `events` are part of the public API
|
||||
this.timers = this._speedTrap.timers;
|
||||
this.events = this._speedTrap.events;
|
||||
|
||||
this._date = getDate();
|
||||
|
||||
this._window = options.window || window;
|
||||
|
||||
var searchParams = this._window.location.search;
|
||||
this._context = Url.searchParam('context', searchParams);
|
||||
this._service = Url.searchParam('service', searchParams);
|
||||
|
||||
this._inactivityFlushMs = options.inactivityFlushMs || TEN_MINS_MS;
|
||||
}
|
||||
|
||||
_.extend(Metrics.prototype, Backbone.Events, {
|
||||
ALLOWED_FIELDS: ALLOWED_FIELDS,
|
||||
|
||||
init: function () {
|
||||
this._flush = _.bind(this.flush, this);
|
||||
$(this._window).on('unload', this._flush);
|
||||
|
||||
// Set the initial inactivity timeout to clear navigation timing data.
|
||||
this._resetInactivityFlushTimeout();
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
$(this._window).off('unload', this._flush);
|
||||
this._clearInactivityFlushTimeout();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send the collected data to the backend.
|
||||
*/
|
||||
flush: function () {
|
||||
// Inactivity timer is restarted when the next event/timer comes in.
|
||||
// This avoids sending empty result sets if the tab is
|
||||
// just sitting there open with no activity.
|
||||
this._clearInactivityFlushTimeout();
|
||||
|
||||
var filteredData = this.getFilteredData();
|
||||
this._speedTrap.events.clear();
|
||||
this._speedTrap.timers.clear();
|
||||
|
||||
var url = this._collector + '/metrics';
|
||||
|
||||
// use a synchronous request to block the page from unloading
|
||||
// until the request is complete.
|
||||
return this._send(filteredData, url, false);
|
||||
},
|
||||
|
||||
_clearInactivityFlushTimeout: function () {
|
||||
clearTimeout(this._inactivityFlushTimeout);
|
||||
},
|
||||
|
||||
_resetInactivityFlushTimeout: function () {
|
||||
this._clearInactivityFlushTimeout();
|
||||
|
||||
var self = this;
|
||||
this._inactivityFlushTimeout =
|
||||
setTimeout(function () {
|
||||
self.logEvent('inactivity:flush');
|
||||
self.flush();
|
||||
}, this._inactivityFlushMs);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all the data, whether it's allowed to be sent or not.
|
||||
*/
|
||||
getAllData: function () {
|
||||
var loadData = this._speedTrap.getLoad();
|
||||
var unloadData = this._speedTrap.getUnload();
|
||||
|
||||
var allData = _.extend({
|
||||
date: this._date.toISOString(),
|
||||
context: this._context,
|
||||
service: this._service
|
||||
}, loadData, unloadData);
|
||||
|
||||
return allData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the filtered data.
|
||||
* Filtered data is data that is allowed to be sent,
|
||||
* that is defined and not an empty string.
|
||||
*/
|
||||
getFilteredData: function () {
|
||||
var allData = this.getAllData();
|
||||
|
||||
var filteredData = {};
|
||||
_.forEach(ALLOWED_FIELDS, function (itemName) {
|
||||
if (typeof allData[itemName] !== 'undefined' &&
|
||||
allData[itemName] !== '') {
|
||||
filteredData[itemName] = allData[itemName];
|
||||
}
|
||||
});
|
||||
|
||||
return filteredData;
|
||||
},
|
||||
|
||||
_send: function (data, url, async) {
|
||||
var deferred = p.defer();
|
||||
|
||||
var self = this;
|
||||
this._ajax({
|
||||
async: async !== false,
|
||||
type: 'POST',
|
||||
url: url,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
self.trigger('flush:error');
|
||||
deferred.reject(errorThrown);
|
||||
},
|
||||
success: function () {
|
||||
self.trigger('flush:success', data);
|
||||
deferred.resolve(data);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an event
|
||||
*/
|
||||
logEvent: function (eventName) {
|
||||
this._resetInactivityFlushTimeout();
|
||||
this.events.capture(eventName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a timer
|
||||
*/
|
||||
startTimer: function (timerName) {
|
||||
this._resetInactivityFlushTimeout();
|
||||
this.timers.start(timerName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop a timer
|
||||
*/
|
||||
stopTimer: function (timerName) {
|
||||
this._resetInactivityFlushTimeout();
|
||||
this.timers.stop(timerName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert an error to an identifier
|
||||
*/
|
||||
errorToId: function (err, errors) {
|
||||
return 'error:' + errors.toCode(err);
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a pathname from a URL to an identifier
|
||||
*/
|
||||
pathToId: function (path) {
|
||||
return 'screen:' + Url.pathToScreenName(path);
|
||||
}
|
||||
});
|
||||
|
||||
return Metrics;
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* A null metrics module. For use as a standin if metrics are disabled
|
||||
* or for unit tests.
|
||||
*/
|
||||
|
||||
define([
|
||||
'underscore',
|
||||
'p-promise',
|
||||
'lib/metrics'
|
||||
], function (_, p, Metrics) {
|
||||
'use strict';
|
||||
|
||||
function NullMetrics () {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
_.forEach(_.keys(Metrics.prototype), function(key) {
|
||||
NullMetrics.prototype[key] = function () {
|
||||
// do nothing
|
||||
};
|
||||
});
|
||||
|
||||
// Metrics.flush returns a promise.
|
||||
NullMetrics.prototype.flush = function () {
|
||||
return p();
|
||||
};
|
||||
|
||||
return NullMetrics;
|
||||
});
|
||||
|
||||
|
|
@ -31,7 +31,17 @@ function (_) {
|
|||
var terms = searchParams(str);
|
||||
|
||||
return terms[name];
|
||||
},
|
||||
|
||||
pathToScreenName: function (path) {
|
||||
// strip leading /
|
||||
return path.replace(/^\//, '')
|
||||
// strip trailing /
|
||||
.replace(/\/$/, '')
|
||||
// search params can contain sensitive info
|
||||
.replace(/\?.*/, '');
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ require.config({
|
|||
text: '../bower_components/requirejs-text/text',
|
||||
mustache: '../bower_components/mustache/mustache',
|
||||
stache: '../bower_components/requirejs-mustache/stache',
|
||||
'p-promise': '../bower_components/p/p'
|
||||
'p-promise': '../bower_components/p/p',
|
||||
speedTrap: '../bower_components/speed-trap/dist/speed-trap'
|
||||
},
|
||||
shim: {
|
||||
underscore: {
|
||||
|
|
|
@ -57,7 +57,15 @@ function (
|
|||
|
||||
function showView(View, options) {
|
||||
return function () {
|
||||
this.showView(new View(options || {}));
|
||||
// passed in options block can override
|
||||
// default options.
|
||||
options = _.extend({
|
||||
metrics: this.metrics,
|
||||
window: this.window,
|
||||
router: this
|
||||
}, options || {});
|
||||
|
||||
this.showView(new View(options));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -92,6 +100,8 @@ function (
|
|||
|
||||
this.window = options.window || window;
|
||||
|
||||
this.metrics = options.metrics;
|
||||
|
||||
this.$stage = $('#stage');
|
||||
|
||||
this.watchAnchors();
|
||||
|
|
|
@ -14,9 +14,10 @@ define([
|
|||
'lib/fxa-client',
|
||||
'lib/url',
|
||||
'lib/strings',
|
||||
'lib/ephemeral-messages'
|
||||
'lib/ephemeral-messages',
|
||||
'lib/null-metrics'
|
||||
],
|
||||
function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, EphemeralMessages) {
|
||||
function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, EphemeralMessages, NullMetrics) {
|
||||
var ENTER_BUTTON_CODE = 13;
|
||||
var DEFAULT_TITLE = window.document.title;
|
||||
var EPHEMERAL_MESSAGE_ANIMATION_MS = 150;
|
||||
|
@ -25,6 +26,11 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
// intialized with an ephemeralMessages for testing.
|
||||
var ephemeralMessages = new EphemeralMessages();
|
||||
|
||||
// A null metrics instance is created for unit tests. In the app,
|
||||
// when a view is initialized, an initialized Metrics instance
|
||||
// is passed in to the contstructor.
|
||||
var nullMetrics = new NullMetrics();
|
||||
|
||||
var BaseView = Backbone.View.extend({
|
||||
constructor: function (options) {
|
||||
options = options || {};
|
||||
|
@ -34,6 +40,7 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
this.translator = options.translator || this.window.translator;
|
||||
this.router = options.router || this.window.router;
|
||||
this.ephemeralMessages = options.ephemeralMessages || ephemeralMessages;
|
||||
this.metrics = options.metrics || nullMetrics;
|
||||
|
||||
this.fxaClient = new FxaClient();
|
||||
|
||||
|
@ -53,6 +60,9 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
*/
|
||||
render: function () {
|
||||
var self = this;
|
||||
|
||||
self.logScreen();
|
||||
|
||||
return p()
|
||||
.then(function () {
|
||||
return self.isUserAuthorized();
|
||||
|
@ -60,8 +70,9 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
.then(function (isUserAuthorized) {
|
||||
if (! isUserAuthorized) {
|
||||
// user is not authorized, make them sign in.
|
||||
var err = AuthErrors.toError('SESSION_EXPIRED');
|
||||
self.navigate('signin', {
|
||||
error: t('Session expired. Sign in to continue.')
|
||||
error: err
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
@ -291,6 +302,7 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
this.hideSuccess();
|
||||
this.$('.spinner').hide();
|
||||
|
||||
this.logError(err, errors);
|
||||
var translated = this.translateError(err, errors);
|
||||
|
||||
if (translated) {
|
||||
|
@ -305,6 +317,39 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
return translated;
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an error to the event stream
|
||||
*/
|
||||
logError: function (err, errors) {
|
||||
// The error could already be logged, if so, abort mission.
|
||||
// This can occur when `navigate` redirects a user to a different
|
||||
// screen and an error is passed. The error is logged before the screen
|
||||
// transition, the new screen is rendered, then the original error is
|
||||
// displayed. This avoids duplicate entries.
|
||||
if (err.logged) {
|
||||
return;
|
||||
}
|
||||
err.logged = true;
|
||||
|
||||
errors = errors || AuthErrors;
|
||||
this.logEvent(this.metrics.errorToId(err, errors));
|
||||
},
|
||||
|
||||
/**
|
||||
* Log the current screen
|
||||
*/
|
||||
logScreen: function () {
|
||||
var path = this.window.location.pathname;
|
||||
this.logEvent(this.metrics.pathToId(path));
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an event to the event stream
|
||||
*/
|
||||
logEvent: function (eventName) {
|
||||
this.metrics.logEvent(eventName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display an error message that may contain HTML. Marked unsafe
|
||||
* because msg could contain XSS. Use with caution and never
|
||||
|
@ -322,6 +367,7 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
this.hideSuccess();
|
||||
this.$('.spinner').hide();
|
||||
|
||||
this.logError(err, errors);
|
||||
var translated = this.translateError(err, errors);
|
||||
|
||||
if (translated) {
|
||||
|
@ -369,6 +415,9 @@ function (_, Backbone, $, p, Session, AuthErrors, FxaClient, Url, Strings, Ephem
|
|||
}
|
||||
|
||||
if (options.error) {
|
||||
// log the error entry before the new screen is rendered so events
|
||||
// stay in the correct order.
|
||||
this.logError(options.error);
|
||||
this.ephemeralMessages.set('error', options.error);
|
||||
}
|
||||
this.router.navigate(page, { trigger: true });
|
||||
|
|
|
@ -60,8 +60,8 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, AuthErrors) {
|
|||
});
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'UNVERIFIED_ACCOUNT')) {
|
||||
var msg = t('Unverified account. <a href="#" id="resend">Resend verification email</a>.');
|
||||
return self.displayErrorUnsafe(msg);
|
||||
err.forceMessage = t('Unverified account. <a href="#" id="resend">Resend verification email</a>.');
|
||||
return self.displayErrorUnsafe(err);
|
||||
}
|
||||
|
||||
throw err;
|
||||
|
@ -76,7 +76,9 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, AuthErrors) {
|
|||
self.navigate('confirm');
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||
return self.navigate('signup');
|
||||
return self.navigate('signup', {
|
||||
error: err
|
||||
});
|
||||
}
|
||||
|
||||
throw self.displayError(err);
|
||||
|
|
|
@ -36,12 +36,14 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, Validate, Aut
|
|||
} catch(e) {
|
||||
// This is an invalid link. Abort and show an error message
|
||||
// before doing any more checks.
|
||||
this.logEvent('complete_reset_password:link_damaged');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! this._doesLinkValidate()) {
|
||||
// One or more parameters fails validation. Abort and show an
|
||||
// error message before doing any more checks.
|
||||
this.logEvent('complete_reset_password:link_damaged');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -49,6 +51,7 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, Validate, Aut
|
|||
return this.fxaClient.isPasswordResetComplete(this.token)
|
||||
.then(function (isComplete) {
|
||||
self._isLinkExpired = isComplete;
|
||||
self.logEvent('complete_reset_password:link_expired');
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
@ -95,6 +98,7 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, Validate, Aut
|
|||
self.navigate('reset_password_complete');
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||
self.logError(err);
|
||||
// The token has expired since the first check, re-render to
|
||||
// show a screen that allows the user to receive a new link.
|
||||
return self.render();
|
||||
|
|
|
@ -23,6 +23,7 @@ function (_, FormView, BaseView, CompleteSignUpTemplate, FxaClient, AuthErrors,
|
|||
this.importSearchParam('uid');
|
||||
this.importSearchParam('code');
|
||||
} catch(e) {
|
||||
this.logEvent('complete_sign_up:link_damaged');
|
||||
// This is an invalid link. Abort and show an error message
|
||||
// before doing any more checks.
|
||||
return true;
|
||||
|
@ -31,6 +32,7 @@ function (_, FormView, BaseView, CompleteSignUpTemplate, FxaClient, AuthErrors,
|
|||
if (! this._doesLinkValidate()) {
|
||||
// One or more parameters fails validation. Abort and show an
|
||||
// error message before doing any more checks.
|
||||
this.logEvent('complete_sign_up:link_damaged');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,7 @@ function (_, FormView, BaseView, CompleteSignUpTemplate, FxaClient, AuthErrors,
|
|||
AuthErrors.is(err, 'INVALID_PARAMETER')) {
|
||||
// These errors show a link damaged screen
|
||||
self._isLinkDamaged = true;
|
||||
self.logEvent('complete_sign_up:link_damaged');
|
||||
} else {
|
||||
// all other errors show the standard error box.
|
||||
self._error = self.translateError(err);
|
||||
|
|
|
@ -12,7 +12,6 @@ define([
|
|||
'lib/auth-errors'
|
||||
],
|
||||
function (FormView, BaseView, Template, Session, AuthErrors) {
|
||||
var t = BaseView.t;
|
||||
var SHOW_RESEND_IN_MS = 5 * 60 * 1000; // 5 minutes.
|
||||
var VERIFICATION_POLL_IN_MS = 4000; // 4 seconds
|
||||
|
||||
|
@ -110,6 +109,7 @@ function (FormView, BaseView, Template, Session, AuthErrors) {
|
|||
var self = this;
|
||||
// Hide the button after 4 attempts. Redisplay button after a delay.
|
||||
if (self._attemptedSubmits === 4) {
|
||||
self.logEvent('confirm:too_many_attempts');
|
||||
self.$('#resend').hide();
|
||||
self._displayResendTimeout = setTimeout(function () {
|
||||
self._displayResendTimeout = null;
|
||||
|
@ -122,13 +122,14 @@ function (FormView, BaseView, Template, Session, AuthErrors) {
|
|||
submit: function () {
|
||||
var self = this;
|
||||
|
||||
self.logEvent('confirm:resend');
|
||||
return this.fxaClient.signUpResend()
|
||||
.then(function () {
|
||||
self.displaySuccess();
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||
return self.navigate('signup', {
|
||||
error: t('Invalid token')
|
||||
error: err
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -78,13 +78,14 @@ function (_, ConfirmView, BaseView, Template, Session, Constants, AuthErrors) {
|
|||
submit: function () {
|
||||
var self = this;
|
||||
|
||||
self.logEvent('confirm_reset_password:resend');
|
||||
return this.fxaClient.passwordResetResend()
|
||||
.then(function () {
|
||||
self.displaySuccess();
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
|
||||
return self.navigate('reset_password', {
|
||||
error: t('Invalid token')
|
||||
error: err
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -66,9 +66,10 @@ function (_, BaseView, FormView, Template, Session, Url, AuthErrors) {
|
|||
// email indicates the signed in email. Use prefillEmail
|
||||
// to avoid collisions across sessions.
|
||||
Session.set('prefillEmail', email);
|
||||
var msg = t('Unknown account. <a href="/signup">Sign up</a>');
|
||||
return self.displayErrorUnsafe(msg);
|
||||
err.forceMessage = t('Unknown account. <a href="/signup">Sign up</a>');
|
||||
return self.displayErrorUnsafe(err);
|
||||
} else if (AuthErrors.is(err, 'USER_CANCELED_LOGIN')) {
|
||||
self.logEvent('login:canceled');
|
||||
// if user canceled login, just stop
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -74,8 +74,9 @@ function (_, p, BaseView, FormView, SignInTemplate, Constants, Session, Password
|
|||
})
|
||||
.then(null, function (err) {
|
||||
if (AuthErrors.is(err, 'UNKNOWN_ACCOUNT')) {
|
||||
return self._suggestSignUp();
|
||||
return self._suggestSignUp(err);
|
||||
} else if (AuthErrors.is(err, 'USER_CANCELED_LOGIN')) {
|
||||
self.logEvent('login:canceled');
|
||||
// if user canceled login, just stop
|
||||
return;
|
||||
}
|
||||
|
@ -94,9 +95,9 @@ function (_, p, BaseView, FormView, SignInTemplate, Constants, Session, Password
|
|||
return true;
|
||||
},
|
||||
|
||||
_suggestSignUp: function () {
|
||||
var msg = t('Unknown account. <a href="/signup">Sign up</a>');
|
||||
return this.displayErrorUnsafe(msg);
|
||||
_suggestSignUp: function (err) {
|
||||
err.forceMessage = t('Unknown account. <a href="/signup">Sign up</a>');
|
||||
return this.displayErrorUnsafe(err);
|
||||
},
|
||||
|
||||
_savePrefillInfo: function () {
|
||||
|
@ -125,7 +126,7 @@ function (_, p, BaseView, FormView, SignInTemplate, Constants, Session, Password
|
|||
self.navigate('confirm_reset_password');
|
||||
}, function (err) {
|
||||
if (AuthErrors.is(err, 'UNKNOWN_ACCOUNT')) {
|
||||
return self._suggestSignUp();
|
||||
return self._suggestSignUp(err);
|
||||
}
|
||||
|
||||
// resetPassword is not called from `submit` and must
|
||||
|
|
|
@ -143,8 +143,9 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, AuthErrors) {
|
|||
// user in directly, instead, point the user to the signin page
|
||||
// where the entered email/password will be prefilled.
|
||||
if (AuthErrors.is(err, 'ACCOUNT_ALREADY_EXISTS')) {
|
||||
return self._suggestSignIn();
|
||||
return self._suggestSignIn(err);
|
||||
} else if (AuthErrors.is(err, 'USER_CANCELED_LOGIN')) {
|
||||
self.logEvent('login:canceled');
|
||||
// if user canceled login, just stop
|
||||
return;
|
||||
}
|
||||
|
@ -162,9 +163,9 @@ function (_, BaseView, FormView, Template, Session, PasswordMixin, AuthErrors) {
|
|||
}
|
||||
},
|
||||
|
||||
_suggestSignIn: function () {
|
||||
var msg = t('Account already exists. <a href="/signin">Sign in</a>');
|
||||
return this.displayErrorUnsafe(msg);
|
||||
_suggestSignIn: function (err) {
|
||||
err.forceMessage = t('Account already exists. <a href="/signin">Sign in</a>');
|
||||
return this.displayErrorUnsafe(err);
|
||||
},
|
||||
|
||||
_savePrefillInfo: function () {
|
||||
|
|
|
@ -77,6 +77,19 @@ define([
|
|||
return email.split('@')[0];
|
||||
}
|
||||
|
||||
function isEventLogged(metrics, eventName) {
|
||||
var events = metrics.getFilteredData().events;
|
||||
|
||||
for (var i = 0; i < events.length; ++i) {
|
||||
var event = events[i];
|
||||
if (event.type === eventName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
requiresFocus: requiresFocus,
|
||||
addFxaClientSpy: addFxaClientSpy,
|
||||
|
@ -84,6 +97,7 @@ define([
|
|||
wrapAssertion: wrapAssertion,
|
||||
createRandomHexString: createRandomHexString,
|
||||
createEmail: createEmail,
|
||||
emailToUser: emailToUser
|
||||
emailToUser: emailToUser,
|
||||
isEventLogged: isEventLogged
|
||||
};
|
||||
});
|
||||
|
|
|
@ -14,7 +14,8 @@ require.config({
|
|||
stache: '/bower_components/requirejs-mustache/stache',
|
||||
chai: '/bower_components/chai/chai',
|
||||
'p-promise': '/bower_components/p/p',
|
||||
sinon: '/bower_components/sinon/index'
|
||||
sinon: '/bower_components/sinon/index',
|
||||
speedTrap: '/bower_components/speed-trap/dist/speed-trap'
|
||||
},
|
||||
shim: {
|
||||
underscore: {
|
||||
|
@ -56,6 +57,8 @@ require([
|
|||
'../tests/spec/lib/app-start',
|
||||
'../tests/spec/lib/validate',
|
||||
'../tests/spec/lib/service-name',
|
||||
'../tests/spec/lib/metrics',
|
||||
'../tests/spec/lib/null-metrics',
|
||||
'../tests/spec/views/base',
|
||||
'../tests/spec/views/tooltip',
|
||||
'../tests/spec/views/form',
|
||||
|
|
|
@ -30,6 +30,13 @@ function (chai, AuthErrors) {
|
|||
AuthErrors.toMessage('this is an error'), 'this is an error');
|
||||
});
|
||||
|
||||
it('uses forceMessage as the message if it exists', function () {
|
||||
assert.equal(AuthErrors.toMessage({
|
||||
errno: 102,
|
||||
forceMessage: 'this is my message'
|
||||
}), 'this is my message');
|
||||
});
|
||||
|
||||
it('converts an error from the backend containing an errno to a message', function () {
|
||||
assert.equal(
|
||||
AuthErrors.toMessage({
|
||||
|
@ -88,9 +95,18 @@ function (chai, AuthErrors) {
|
|||
});
|
||||
|
||||
describe('toCode', function () {
|
||||
it('converts a string type to a numeric code', function () {
|
||||
it('returns the errno from an error object', function () {
|
||||
var err = AuthErrors.toError('INVALID_TOKEN', 'bad token, man');
|
||||
assert.equal(AuthErrors.toCode(err), 110);
|
||||
});
|
||||
|
||||
it('converts a string type to a numeric code, if valid code', function () {
|
||||
assert.equal(AuthErrors.toCode('UNKNOWN_ACCOUNT'), 102);
|
||||
});
|
||||
|
||||
it('returns the string if an invalid code', function () {
|
||||
assert.equal(AuthErrors.toCode('this is an invalid code'), 'this is an invalid code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', function () {
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// test the metrics library
|
||||
|
||||
define([
|
||||
'chai',
|
||||
'lib/metrics',
|
||||
'../../mocks/window'
|
||||
],
|
||||
function (chai, Metrics, WindowMock) {
|
||||
'use strict';
|
||||
|
||||
/*global describe, it*/
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('lib/metrics', function () {
|
||||
var metrics, windowMock;
|
||||
|
||||
beforeEach(function () {
|
||||
windowMock = new WindowMock();
|
||||
|
||||
windowMock.location.search = '?service=sync&context=fxa_desktop_v1';
|
||||
metrics = new Metrics({
|
||||
window: windowMock
|
||||
});
|
||||
metrics.init();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
metrics = null;
|
||||
});
|
||||
|
||||
describe('getFilteredData', function () {
|
||||
it('gets data that is allowed to be sent to the server', function () {
|
||||
var filteredData = metrics.getFilteredData();
|
||||
|
||||
// ensure results are filtered and no unexpected data makes it through.
|
||||
for (var key in filteredData) {
|
||||
assert.isTrue(metrics.ALLOWED_FIELDS.indexOf(key) > -1);
|
||||
}
|
||||
});
|
||||
|
||||
it('gets non-optional fields', function () {
|
||||
var filteredData = metrics.getFilteredData();
|
||||
|
||||
assert.isTrue(filteredData.hasOwnProperty('date'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('events'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('timers'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('navigationTiming'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('duration'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('context'));
|
||||
assert.isTrue(filteredData.hasOwnProperty('service'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('logEvent', function () {
|
||||
it('adds events to output data', function () {
|
||||
metrics.logEvent('event1');
|
||||
metrics.logEvent('event2');
|
||||
metrics.logEvent('event3');
|
||||
|
||||
var filteredData = metrics.getFilteredData();
|
||||
assert.equal(filteredData.events.length, 3);
|
||||
assert.equal(filteredData.events[0].type, 'event1');
|
||||
assert.equal(filteredData.events[1].type, 'event2');
|
||||
assert.equal(filteredData.events[2].type, 'event3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startTimer/stopTimer', function () {
|
||||
it('adds a timer to output data', function () {
|
||||
metrics.startTimer('timer1');
|
||||
metrics.stopTimer('timer1');
|
||||
|
||||
var filteredData = metrics.getFilteredData();
|
||||
assert.equal(filteredData.timers.timer1.length, 1);
|
||||
|
||||
var timerData = filteredData.timers.timer1[0];
|
||||
assert.ok(timerData.hasOwnProperty('start'));
|
||||
assert.ok(timerData.hasOwnProperty('stop'));
|
||||
assert.ok(timerData.hasOwnProperty('elapsed'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush', function () {
|
||||
var sentData, serverError;
|
||||
|
||||
function ajaxMock(options) {
|
||||
sentData = options.data;
|
||||
|
||||
if (serverError) {
|
||||
options.error({}, 'bad jiji', serverError);
|
||||
} else {
|
||||
options.success();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
metrics = new Metrics({
|
||||
ajax: ajaxMock,
|
||||
window: windowMock,
|
||||
inactivityFlushMs: 100
|
||||
});
|
||||
metrics.init();
|
||||
|
||||
sentData = serverError = null;
|
||||
});
|
||||
|
||||
it('sends filtered data to the server and clears the event stream', function () {
|
||||
metrics.logEvent('event1');
|
||||
metrics.logEvent('event2');
|
||||
|
||||
return metrics.flush()
|
||||
.then(function (data) {
|
||||
var parsedSentData = JSON.parse(sentData);
|
||||
assert.deepEqual(data, parsedSentData);
|
||||
|
||||
var events = metrics.getFilteredData().events;
|
||||
assert.equal(events.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('sends filtered data to the server on window unload', function (done) {
|
||||
metrics.logEvent('event10');
|
||||
metrics.logEvent('event20');
|
||||
|
||||
var filteredData = metrics.getFilteredData();
|
||||
metrics.on('flush:success', function () {
|
||||
var parsedSentData = JSON.parse(sentData);
|
||||
|
||||
// `duration` fields are different if the above `getFilteredData`
|
||||
// is called in a different millisecond than the one used to
|
||||
// generate data that is sent to the server.
|
||||
// Ensure `duration` is in the results, but do not compare the two.
|
||||
assert.isTrue(parsedSentData.hasOwnProperty('duration'));
|
||||
|
||||
delete parsedSentData.duration;
|
||||
delete filteredData.duration;
|
||||
|
||||
assert.deepEqual(filteredData, parsedSentData);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
$(windowMock).trigger('unload');
|
||||
});
|
||||
|
||||
it('handles server errors', function () {
|
||||
metrics.logEvent('event100');
|
||||
metrics.logEvent('event200');
|
||||
|
||||
serverError = 'server down';
|
||||
|
||||
return metrics.flush()
|
||||
.then(null, function (err) {
|
||||
// to ensure the failure branch is called, pass the error
|
||||
// on to the next success callback which is called on
|
||||
// success or failure.
|
||||
return err;
|
||||
})
|
||||
.then(function(err) {
|
||||
assert.equal(err, 'server down');
|
||||
});
|
||||
});
|
||||
|
||||
it('automatically flushes after inactivityFlushMs', function (done) {
|
||||
metrics.events.clear();
|
||||
metrics.logEvent('event-is-autoflushed');
|
||||
|
||||
metrics.on('flush:success', function (sentData) {
|
||||
assert.equal(sentData.events[0].type, 'event-is-autoflushed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// test the metrics library
|
||||
|
||||
define([
|
||||
'chai',
|
||||
'lib/null-metrics',
|
||||
'lib/metrics'
|
||||
],
|
||||
function (chai, NullMetrics, Metrics) {
|
||||
'use strict';
|
||||
|
||||
/*global describe, it*/
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('lib/null-metrics', function () {
|
||||
var nullMetrics;
|
||||
|
||||
beforeEach(function () {
|
||||
nullMetrics = new NullMetrics();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
nullMetrics = null;
|
||||
});
|
||||
|
||||
it('has the same function signature as Metrics', function () {
|
||||
for (var key in Metrics.prototype) {
|
||||
if (typeof Metrics.prototype[key] === 'function') {
|
||||
assert.isFunction(nullMetrics[key], key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('flush returns a promise', function () {
|
||||
return nullMetrics.flush()
|
||||
.then(function () {
|
||||
assert.isTrue(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,15 +13,17 @@ define([
|
|||
'views/sign_in',
|
||||
'views/sign_up',
|
||||
'lib/session',
|
||||
'lib/constants',
|
||||
'lib/metrics',
|
||||
'../../mocks/window',
|
||||
'lib/constants'
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock, Constants) {
|
||||
function (chai, _, Backbone, Router, SignInView, SignUpView, Session, Constants, Metrics, WindowMock, TestHelpers) {
|
||||
/*global describe, beforeEach, afterEach, it*/
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('lib/router', function () {
|
||||
var router, windowMock, origNavigate, navigateUrl, navigateOptions;
|
||||
var router, windowMock, origNavigate, navigateUrl, navigateOptions, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
navigateUrl = navigateOptions = null;
|
||||
|
@ -29,8 +31,11 @@ function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock
|
|||
$('#container').html('<div id="stage"></div>');
|
||||
|
||||
windowMock = new WindowMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
router = new Router({
|
||||
window: windowMock
|
||||
window: windowMock,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
origNavigate = Backbone.Router.prototype.navigate;
|
||||
|
@ -41,7 +46,8 @@ function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
windowMock = router = navigateUrl = navigateOptions = null;
|
||||
metrics.destroy();
|
||||
windowMock = router = navigateUrl = navigateOptions = metrics = null;
|
||||
Backbone.Router.prototype.navigate = origNavigate;
|
||||
$('#container').empty();
|
||||
});
|
||||
|
@ -85,8 +91,14 @@ function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock
|
|||
var signInView, signUpView;
|
||||
|
||||
beforeEach(function () {
|
||||
signInView = new SignInView({});
|
||||
signUpView = new SignUpView({});
|
||||
signInView = new SignInView({
|
||||
metrics: metrics,
|
||||
window: windowMock
|
||||
});
|
||||
signUpView = new SignUpView({
|
||||
metrics: metrics,
|
||||
window: windowMock
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
@ -94,6 +106,7 @@ function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock
|
|||
});
|
||||
|
||||
it('shows a view, then shows the new view', function () {
|
||||
windowMock.location.pathname = '/signin';
|
||||
return router.showView(signInView)
|
||||
.then(function () {
|
||||
assert.ok($('#fxa-signin-header').length);
|
||||
|
@ -101,12 +114,16 @@ function (chai, _, Backbone, Router, SignInView, SignUpView, Session, WindowMock
|
|||
// session was cleared in beforeEach, simulating a user
|
||||
// visiting their first page. The user cannot go back.
|
||||
assert.equal(Session.canGoBack, false);
|
||||
windowMock.location.pathname = '/signup';
|
||||
return router.showView(signUpView);
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok($('#fxa-signup-header').length);
|
||||
// if there is a back button, it can be shown now.
|
||||
assert.equal(Session.canGoBack, true);
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics, 'screen:signin'));
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics, 'screen:signup'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,24 @@ function (chai, _, Url) {
|
|||
assert.isUndefined(Url.searchParam('animal'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('pathToScreenName', function () {
|
||||
it('strips leading /', function () {
|
||||
assert.equal(Url.pathToScreenName('/signin'), 'signin');
|
||||
});
|
||||
|
||||
it('strips trailing /', function () {
|
||||
assert.equal(Url.pathToScreenName('signup/'), 'signup');
|
||||
});
|
||||
|
||||
it('leaves middle / alone', function () {
|
||||
assert.equal(Url.pathToScreenName('/legal/tos/'), 'legal/tos');
|
||||
});
|
||||
|
||||
it('strips search parameters', function () {
|
||||
assert.equal(Url.pathToScreenName('complete_sign_up?email=testuser@testuser.com'), 'complete_sign_up');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -11,21 +11,24 @@ define([
|
|||
'views/base',
|
||||
'lib/translator',
|
||||
'lib/ephemeral-messages',
|
||||
'lib/metrics',
|
||||
'lib/auth-errors',
|
||||
'stache!templates/test_template',
|
||||
'../../mocks/dom-event',
|
||||
'../../mocks/router',
|
||||
'../../mocks/window',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
||||
Template, DOMEventMock, RouterMock, WindowMock, TestHelpers) {
|
||||
function (chai, jQuery, BaseView, Translator, EphemeralMessages, Metrics,
|
||||
AuthErrors, Template, DOMEventMock, RouterMock, WindowMock,
|
||||
TestHelpers) {
|
||||
var requiresFocus = TestHelpers.requiresFocus;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('views/base', function () {
|
||||
var view, router, windowMock, ephemeralMessages, translator;
|
||||
var view, router, windowMock, ephemeralMessages, translator, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
translator = new Translator('en-US', ['en-US']);
|
||||
|
@ -36,8 +39,8 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
|
||||
router = new RouterMock();
|
||||
windowMock = new WindowMock();
|
||||
|
||||
ephemeralMessages = new EphemeralMessages();
|
||||
metrics = new Metrics();
|
||||
|
||||
var View = BaseView.extend({
|
||||
template: Template
|
||||
|
@ -47,7 +50,8 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
translator: translator,
|
||||
router: router,
|
||||
window: windowMock,
|
||||
ephemeralMessages: ephemeralMessages
|
||||
ephemeralMessages: ephemeralMessages,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
return view.render()
|
||||
|
@ -57,11 +61,14 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
if (view) {
|
||||
view.destroy();
|
||||
jQuery(view.el).remove();
|
||||
view = router = windowMock = null;
|
||||
}
|
||||
|
||||
view = router = windowMock = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -186,6 +193,14 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
view.displayError('an error message<div>with html</div>');
|
||||
assert.equal(view.$('.error').html(), 'an error message<div>with html</div>');
|
||||
});
|
||||
|
||||
it('adds an entry into the event stream', function () {
|
||||
var err = AuthErrors.toError('INVALID_TOKEN', 'bad token, man');
|
||||
view.displayError(err);
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
metrics.errorToId('INVALID_TOKEN', AuthErrors)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayErrorUnsafe', function () {
|
||||
|
@ -199,6 +214,14 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
view.hideError();
|
||||
assert.isFalse(view.isErrorVisible());
|
||||
});
|
||||
|
||||
it('adds an entry into the event stream', function () {
|
||||
var err = AuthErrors.toError('INVALID_TOKEN', 'bad token, man');
|
||||
view.displayError(err);
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
metrics.errorToId('INVALID_TOKEN', AuthErrors)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('displaySuccess', function () {
|
||||
|
@ -224,6 +247,15 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
});
|
||||
view.navigate('signin');
|
||||
});
|
||||
|
||||
it('logs an error if an error is passed in the options', function () {
|
||||
view.navigate('signin', {
|
||||
error: AuthErrors.toError('SESSION_EXPIRED')
|
||||
});
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
metrics.errorToId('SESSION_EXPIRED', AuthErrors)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus', function () {
|
||||
|
@ -346,6 +378,32 @@ function (chai, jQuery, BaseView, Translator, EphemeralMessages,
|
|||
assert.ok(err);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logEvent', function () {
|
||||
it('logs an event to the event stream', function () {
|
||||
view.logEvent('event1');
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics, 'event1'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('logError', function () {
|
||||
it('logs an error to the event stream', function () {
|
||||
view.logError(AuthErrors.toError('INVALID_TOKEN'));
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
metrics.errorToId('INVALID_TOKEN', AuthErrors)));
|
||||
});
|
||||
|
||||
it('does not log already logged errors', function () {
|
||||
view.metrics.events.clear();
|
||||
|
||||
var err = AuthErrors.toError('INVALID_TOKEN');
|
||||
view.logError(err);
|
||||
view.logError(err);
|
||||
|
||||
var events = view.metrics.getFilteredData().events;
|
||||
assert.equal(events.length, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,26 +9,34 @@ define([
|
|||
'chai',
|
||||
'p-promise',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'views/complete_reset_password',
|
||||
'../../mocks/router',
|
||||
'../../mocks/window',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
||||
function (chai, p, AuthErrors, Metrics, View, RouterMock, WindowMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
describe('views/complete_reset_password', function () {
|
||||
var view, routerMock, windowMock, isPasswordResetComplete;
|
||||
var view, routerMock, windowMock, isPasswordResetComplete, metrics;
|
||||
|
||||
function testEventLogged(eventName) {
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics, eventName));
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
routerMock = new RouterMock();
|
||||
windowMock = new WindowMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
windowMock.location.search = '?code=dea0fae1abc2fab3bed4dec5eec6ace7&email=testuser@testuser.com&token=feed';
|
||||
|
||||
view = new View({
|
||||
router: routerMock,
|
||||
window: windowMock
|
||||
window: windowMock,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
// mock in isPasswordResetComplete
|
||||
|
@ -44,9 +52,12 @@ function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = windowMock = null;
|
||||
|
||||
view = windowMock = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -57,6 +68,9 @@ function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
|||
it('shows malformed screen if the token is missing', function () {
|
||||
windowMock.location.search = '?code=faea&email=testuser@testuser.com';
|
||||
return view.render()
|
||||
.then(function () {
|
||||
testEventLogged('complete_reset_password:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok(view.$('#fxa-verification-link-damaged-header').length);
|
||||
});
|
||||
|
@ -65,6 +79,9 @@ function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
|||
it('shows malformed screen if the code is missing', function () {
|
||||
windowMock.location.search = '?token=feed&email=testuser@testuser.com';
|
||||
return view.render()
|
||||
.then(function () {
|
||||
testEventLogged('complete_reset_password:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok(view.$('#fxa-verification-link-damaged-header').length);
|
||||
});
|
||||
|
@ -73,6 +90,9 @@ function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
|||
it('shows malformed screen if the email is missing', function () {
|
||||
windowMock.location.search = '?token=feed&code=dea0fae1abc2fab3bed4dec5eec6ace7';
|
||||
return view.render()
|
||||
.then(function () {
|
||||
testEventLogged('complete_reset_password:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok(view.$('#fxa-verification-link-damaged-header').length);
|
||||
});
|
||||
|
@ -81,6 +101,9 @@ function (chai, p, AuthErrors, View, RouterMock, WindowMock, TestHelpers) {
|
|||
it('shows the expired screen if the token has already been verified', function () {
|
||||
isPasswordResetComplete = true;
|
||||
return view.render()
|
||||
.then(function () {
|
||||
testEventLogged('complete_reset_password:link_expired');
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok(view.$('#fxa-verification-link-expired-header').length);
|
||||
});
|
||||
|
|
|
@ -10,16 +10,17 @@ define([
|
|||
'p-promise',
|
||||
'views/complete_sign_up',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'lib/constants',
|
||||
'../../mocks/router',
|
||||
'../../mocks/window',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, p, View, AuthErrors, Constants, RouterMock, WindowMock, TestHelpers) {
|
||||
function (chai, p, View, AuthErrors, Metrics, Constants, RouterMock, WindowMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('views/complete_sign_up', function () {
|
||||
var view, routerMock, windowMock, verificationError;
|
||||
var view, routerMock, windowMock, verificationError, metrics;
|
||||
var validCode = TestHelpers.createRandomHexString(Constants.CODE_LENGTH);
|
||||
var validUid = TestHelpers.createRandomHexString(Constants.UID_LENGTH);
|
||||
|
||||
|
@ -31,13 +32,19 @@ function (chai, p, View, AuthErrors, Constants, RouterMock, WindowMock, TestHelp
|
|||
});
|
||||
}
|
||||
|
||||
function testEventLogged(eventName) {
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics, eventName));
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
routerMock = new RouterMock();
|
||||
windowMock = new WindowMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
view = new View({
|
||||
router: routerMock,
|
||||
window: windowMock
|
||||
window: windowMock,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
verificationError = null;
|
||||
|
@ -53,14 +60,20 @@ function (chai, p, View, AuthErrors, Constants, RouterMock, WindowMock, TestHelp
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = windowMock = null;
|
||||
|
||||
view = windowMock = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
it('shows an error if uid is not available on the URL', function () {
|
||||
return testShowsDamagedScreen('?code=' + validCode)
|
||||
.then(function () {
|
||||
testEventLogged('complete_sign_up:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.isFalse(view.fxaClient.verifyCode.called);
|
||||
});
|
||||
|
@ -68,6 +81,9 @@ function (chai, p, View, AuthErrors, Constants, RouterMock, WindowMock, TestHelp
|
|||
|
||||
it('shows an error if code is not available on the URL', function () {
|
||||
return testShowsDamagedScreen('?uid=' + validUid)
|
||||
.then(function () {
|
||||
testEventLogged('complete_sign_up:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.isFalse(view.fxaClient.verifyCode.called);
|
||||
});
|
||||
|
@ -76,6 +92,9 @@ function (chai, p, View, AuthErrors, Constants, RouterMock, WindowMock, TestHelp
|
|||
it('INVALID_PARAMETER error displays the verification link damaged screen', function () {
|
||||
verificationError = AuthErrors.toError('INVALID_PARAMETER', 'code');
|
||||
return testShowsDamagedScreen()
|
||||
.then(function () {
|
||||
testEventLogged('complete_sign_up:link_damaged');
|
||||
})
|
||||
.then(function () {
|
||||
assert.isTrue(view.fxaClient.verifyCode.called);
|
||||
});
|
||||
|
|
|
@ -7,25 +7,30 @@ define([
|
|||
'p-promise',
|
||||
'lib/session',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'views/confirm',
|
||||
'../../mocks/router',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
||||
function (chai, p, Session, AuthErrors, Metrics, View, RouterMock, TestHelpers) {
|
||||
'use strict';
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('views/confirm', function () {
|
||||
var view, routerMock;
|
||||
var view, routerMock, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
Session.set('sessionToken', 'fake session token');
|
||||
|
||||
routerMock = new RouterMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
view = new View({
|
||||
router: routerMock
|
||||
router: routerMock,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
return view.render()
|
||||
.then(function () {
|
||||
$('#container').html(view.el);
|
||||
|
@ -33,8 +38,12 @@ function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
|
||||
view = metrics = null;
|
||||
});
|
||||
|
||||
describe('constructor creates it', function () {
|
||||
|
@ -52,7 +61,7 @@ function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
|||
});
|
||||
|
||||
describe('submit', function () {
|
||||
it('resends the confirmation email, shows success message', function () {
|
||||
it('resends the confirmation email, shows success message, logs the event', function () {
|
||||
var email = TestHelpers.createEmail();
|
||||
|
||||
return view.fxaClient.signUp(email, 'password')
|
||||
|
@ -61,6 +70,8 @@ function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
|||
})
|
||||
.then(function () {
|
||||
assert.isTrue(view.$('.success').is(':visible'));
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'confirm:resend'));
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -75,6 +86,9 @@ function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
|||
return view.submit()
|
||||
.then(function () {
|
||||
assert.equal(routerMock.page, 'signup');
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'confirm:resend'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -137,6 +151,11 @@ function (chai, p, Session, AuthErrors, View, RouterMock, TestHelpers) {
|
|||
}).then(function () {
|
||||
assert.equal(count, 2);
|
||||
assert.equal(view.$('#resend:visible').length, 0);
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'confirm:resend'));
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'confirm:too_many_attempts'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,27 +8,31 @@ define([
|
|||
'lib/auth-errors',
|
||||
'views/confirm_reset_password',
|
||||
'lib/session',
|
||||
'lib/metrics',
|
||||
'../../mocks/router',
|
||||
'../../mocks/window'
|
||||
'../../mocks/window',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, p, AuthErrors, View, Session, RouterMock, WindowMock) {
|
||||
function (chai, p, AuthErrors, View, Session, Metrics, RouterMock, WindowMock, TestHelpers) {
|
||||
'use strict';
|
||||
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('views/confirm_reset_password', function () {
|
||||
var view, routerMock, windowMock;
|
||||
var view, routerMock, windowMock, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
routerMock = new RouterMock();
|
||||
windowMock = new WindowMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
Session.set('passwordForgotToken', 'fake password reset token');
|
||||
Session.set('email', 'testuser@testuser.com');
|
||||
|
||||
view = new View({
|
||||
router: routerMock,
|
||||
window: windowMock
|
||||
window: windowMock,
|
||||
metrics: metrics
|
||||
});
|
||||
return view.render()
|
||||
.then(function () {
|
||||
|
@ -37,8 +41,12 @@ function (chai, p, AuthErrors, View, Session, RouterMock, WindowMock) {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
|
||||
view = metrics = null;
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
|
@ -130,6 +138,9 @@ function (chai, p, AuthErrors, View, Session, RouterMock, WindowMock) {
|
|||
return view.submit()
|
||||
.then(function () {
|
||||
assert.equal(routerMock.page, 'reset_password');
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'confirm_reset_password:resend'));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,23 +7,29 @@
|
|||
|
||||
define([
|
||||
'chai',
|
||||
'p-promise',
|
||||
'lib/session',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'views/reset_password',
|
||||
'../../mocks/window',
|
||||
'../../mocks/router',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, Session, View, WindowMock, RouterMock, TestHelpers) {
|
||||
function (chai, p, Session, AuthErrors, Metrics, View, WindowMock, RouterMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
describe('views/reset_password', function () {
|
||||
var view, router;
|
||||
var view, router, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
router = new RouterMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
view = new View({
|
||||
router: router
|
||||
router: router,
|
||||
metrics: metrics
|
||||
});
|
||||
return view.render()
|
||||
.then(function () {
|
||||
|
@ -32,10 +38,13 @@ function (chai, Session, View, WindowMock, RouterMock, TestHelpers) {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = router = null;
|
||||
$('#container').empty();
|
||||
|
||||
view = router = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -98,7 +107,7 @@ function (chai, Session, View, WindowMock, RouterMock, TestHelpers) {
|
|||
});
|
||||
|
||||
describe('submit with unknown email address', function () {
|
||||
it('rejects the promise', function () {
|
||||
it('shows an error message', function () {
|
||||
var email = 'unknown' + Math.random() + '@testuser.com';
|
||||
view.$('input[type=email]').val(email);
|
||||
|
||||
|
@ -109,6 +118,51 @@ function (chai, Session, View, WindowMock, RouterMock, TestHelpers) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('submit when user cancelled login', function () {
|
||||
it('logs an error', function () {
|
||||
view.fxaClient.passwordReset = function (email) {
|
||||
return p()
|
||||
.then(function () {
|
||||
throw AuthErrors.toError('USER_CANCELED_LOGIN');
|
||||
});
|
||||
};
|
||||
|
||||
return view.submit()
|
||||
.then(null, function(err) {
|
||||
assert.isTrue(false, 'unexpected failure');
|
||||
})
|
||||
.then(function (err) {
|
||||
assert.isFalse(view.isErrorVisible());
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'login:canceled'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit with other error', function () {
|
||||
it('passes other errors along', function () {
|
||||
view.fxaClient.passwordReset = function (email) {
|
||||
return p()
|
||||
.then(function () {
|
||||
throw AuthErrors.toError('INVALID_JSON');
|
||||
});
|
||||
};
|
||||
|
||||
return view.submit()
|
||||
.then(null, function(err) {
|
||||
// The errorback will not be called if the submit
|
||||
// succeeds, but the following callback always will
|
||||
// be. To ensure the errorback was called, pass
|
||||
// the error along and check its type.
|
||||
return err;
|
||||
})
|
||||
.then(function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_JSON'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('views/reset_password with email specified as query param', function () {
|
||||
|
|
|
@ -12,23 +12,29 @@ define([
|
|||
'views/sign_in',
|
||||
'lib/session',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'../../mocks/window',
|
||||
'../../mocks/router',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, $, p, View, Session, AuthErrors, WindowMock, RouterMock, TestHelpers) {
|
||||
function (chai, $, p, View, Session, AuthErrors, Metrics, WindowMock, RouterMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
describe('views/sign_in', function () {
|
||||
var view, email, routerMock;
|
||||
var view, email, routerMock, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
email = 'testuser.' + Math.random() + '@testuser.com';
|
||||
|
||||
routerMock = new RouterMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
view = new View({
|
||||
router: routerMock
|
||||
router: routerMock,
|
||||
metrics: metrics
|
||||
});
|
||||
|
||||
return view.render()
|
||||
.then(function () {
|
||||
$('#container').html(view.el);
|
||||
|
@ -36,8 +42,12 @@ function (chai, $, p, View, Session, AuthErrors, WindowMock, RouterMock, TestHel
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
|
||||
view = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -113,7 +123,7 @@ function (chai, $, p, View, Session, AuthErrors, WindowMock, RouterMock, TestHel
|
|||
});
|
||||
});
|
||||
|
||||
it('does nothing if user cancels login', function () {
|
||||
it('logs an error if user cancels login', function () {
|
||||
view.fxaClient.signIn = function () {
|
||||
return p()
|
||||
.then(function () {
|
||||
|
@ -125,6 +135,9 @@ function (chai, $, p, View, Session, AuthErrors, WindowMock, RouterMock, TestHel
|
|||
return view.submit()
|
||||
.then(function () {
|
||||
assert.isFalse(view.isErrorVisible());
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'login:canceled'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -151,6 +164,27 @@ function (chai, $, p, View, Session, AuthErrors, WindowMock, RouterMock, TestHel
|
|||
assert.ok(msg.indexOf('/signup') > -1);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes other errors along', function () {
|
||||
view.fxaClient.signIn = function (email) {
|
||||
return p()
|
||||
.then(function () {
|
||||
throw AuthErrors.toError('INVALID_JSON');
|
||||
});
|
||||
};
|
||||
|
||||
return view.submit()
|
||||
.then(null, function(err) {
|
||||
// The errorback will not be called if the submit
|
||||
// succeeds, but the following callback always will
|
||||
// be. To ensure the errorback was called, pass
|
||||
// the error along and check its type.
|
||||
return err;
|
||||
})
|
||||
.then(function (err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'INVALID_JSON'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showValidationErrors', function () {
|
||||
|
|
|
@ -13,10 +13,11 @@ define([
|
|||
'views/sign_up',
|
||||
'lib/session',
|
||||
'lib/auth-errors',
|
||||
'lib/metrics',
|
||||
'../../mocks/router',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
||||
function (chai, _, $, p, View, Session, AuthErrors, Metrics, RouterMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
|
||||
|
@ -34,14 +35,17 @@ function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
|||
}
|
||||
|
||||
describe('views/sign_up', function () {
|
||||
var view, router, email;
|
||||
var view, router, email, metrics;
|
||||
|
||||
beforeEach(function () {
|
||||
email = 'testuser.' + Math.random() + '@testuser.com';
|
||||
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
|
||||
router = new RouterMock();
|
||||
metrics = new Metrics();
|
||||
|
||||
view = new View({
|
||||
router: router
|
||||
router: router,
|
||||
metrics: metrics
|
||||
});
|
||||
return view.render()
|
||||
.then(function () {
|
||||
|
@ -50,11 +54,13 @@ function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
router = null;
|
||||
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
|
||||
|
||||
view = router = metrics = null;
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -321,7 +327,7 @@ function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
|||
});
|
||||
});
|
||||
|
||||
it('does nothing if user cancels signup', function () {
|
||||
it('logs an error if user cancels signup', function () {
|
||||
view.fxaClient.signUp = function () {
|
||||
return p()
|
||||
.then(function () {
|
||||
|
@ -335,6 +341,9 @@ function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
|||
return view.submit()
|
||||
.then(function () {
|
||||
assert.isFalse(view.isErrorVisible());
|
||||
|
||||
assert.isTrue(TestHelpers.isEventLogged(metrics,
|
||||
'login:canceled'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -351,6 +360,13 @@ function (chai, _, $, p, View, Session, AuthErrors, RouterMock, TestHelpers) {
|
|||
|
||||
return view.submit()
|
||||
.then(null, function (err) {
|
||||
// The errorback will not be called if the submit
|
||||
// succeeds, but the following callback always will
|
||||
// be. To ensure the errorback was called, pass
|
||||
// the error along and check its type.
|
||||
return err;
|
||||
})
|
||||
.then(function(err) {
|
||||
assert.isTrue(AuthErrors.is(err, 'SERVER_BUSY'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"requirejs-mustache": "https://github.com/jfparadis/requirejs-mustache.git#5fb8c0a3e8560526e8f9617a689e2924a8532d0a",
|
||||
"requirejs-text": "2.0.10",
|
||||
"sinon": "http://sinonjs.org/releases/sinon-1.7.1.js",
|
||||
"speed-trap": "0.0.2",
|
||||
"tos-pp": "https://github.com/mozilla/legal-docs.git",
|
||||
"underscore": "1.5.2"
|
||||
},
|
||||
|
|
|
@ -5,5 +5,8 @@
|
|||
"static_max_age" : 0,
|
||||
"i18n": {
|
||||
"supportedLanguages": ["af", "an", "ar", "as", "ast", "be", "bg", "bn-BD", "bn-IN", "br", "bs", "ca", "cs", "cy", "da", "de", "el", "en-GB", "en-US", "en-ZA", "eo", "es", "es-AR", "es-CL", "es-MX", "et", "eu", "fa", "ff", "fi", "fr", "fy", "fy-NL", "ga", "ga-IE", "gd", "gl", "gu", "gu-IN", "he", "hi-IN", "hr", "ht", "hu", "hy-AM", "id", "is", "it", "it-CH", "ja", "kk", "km", "kn", "ko", "ku", "lij", "lt", "lv", "mai", "mk", "ml", "mr", "ms", "nb-NO", "ne-NP", "nl", "nn-NO", "or", "pa", "pa-IN", "pl", "pt", "pt-BR", "pt-PT", "rm", "ro", "ru", "si", "sk", "sl", "son", "sq", "sr", "sr-LATN", "sv", "sv-SE", "ta", "te", "th", "tr", "uk", "ur", "vi", "xh", "zh-CN", "zh-TW", "zu"]
|
||||
},
|
||||
"metrics": {
|
||||
"sample_rate": 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,5 +13,8 @@
|
|||
"supportedLanguages": ["af", "an", "ar", "as", "ast", "be", "bg", "bn-BD", "bn-IN", "br", "bs", "ca", "cs", "cy", "da", "de", "el", "en-GB", "en-US", "en-ZA", "eo", "es", "es-AR", "es-CL", "es-MX", "et", "eu", "fa", "ff", "fi", "fr", "fy", "fy-NL", "ga", "ga-IE", "gd", "gl", "gu", "gu-IN", "he", "hi-IN", "hr", "ht", "hu", "hy-AM", "id", "is", "it", "it-CH", "ja", "kk", "km", "kn", "ko", "ku", "lij", "lt", "lv", "mai", "mk", "ml", "mr", "ms", "nb-NO", "ne-NP", "nl", "nn-NO", "or", "pa", "pa-IN", "pl", "pt", "pt-BR", "pt-PT", "rm", "ro", "ru", "si", "sk", "sl", "son", "sq", "sr", "sr-LATN", "sv", "sv-SE", "ta", "te", "th", "tr", "uk", "ur", "vi", "xh", "zh-CN", "zh-TW", "zu"]
|
||||
},
|
||||
"route_log_format": "dev_fxa",
|
||||
"static_directory": "app"
|
||||
"static_directory": "app",
|
||||
"metrics": {
|
||||
"sample_rate": 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,6 +159,14 @@ var conf = module.exports = convict({
|
|||
default: 'key-value-json',
|
||||
env: 'I18N_TRANSLATION_TYPE'
|
||||
}
|
||||
},
|
||||
metrics: {
|
||||
sample_rate: {
|
||||
doc: 'Front end metrics sample rate - must be between 0 and 1',
|
||||
format: Number,
|
||||
default: 0,
|
||||
env: 'METRICS_SAMPLE_RATE'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ module.exports = function (config, templates, i18n) {
|
|||
require('./routes/get-ver.json'),
|
||||
require('./routes/get-terms-privacy')(i18n),
|
||||
require('./routes/get-config')(i18n),
|
||||
require('./routes/get-client.json')(i18n)
|
||||
require('./routes/get-client.json')(i18n),
|
||||
require('./routes/post-metrics')()
|
||||
];
|
||||
|
||||
var authServerHost = url.parse(config.get('fxaccount_url')).hostname;
|
||||
|
|
|
@ -32,7 +32,8 @@ module.exports = function(i18n) {
|
|||
fxaccountUrl: config.get('fxaccount_url'),
|
||||
oauthUrl: config.get('oauth_url'),
|
||||
// req.lang is set by abide in a previous middleware.
|
||||
language: req.lang
|
||||
language: req.lang,
|
||||
metricsSampleRate: config.get('metrics.sample_rate')
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
var logger = require('intel').getLogger('server.metrics');
|
||||
|
||||
module.exports = function() {
|
||||
var route = {};
|
||||
|
||||
route.method = 'post';
|
||||
route.path = '/metrics';
|
||||
|
||||
route.process = function(req, res) {
|
||||
|
||||
logger.info('metrics:\n%s', JSON.stringify(req.body, null, 2));
|
||||
|
||||
res.json({ success: true });
|
||||
};
|
||||
|
||||
return route;
|
||||
};
|
|
@ -16,7 +16,8 @@ define([
|
|||
'tests/server/routes',
|
||||
'tests/server/ver.json.js',
|
||||
'tests/server/cookies_disabled',
|
||||
'tests/server/l10n'
|
||||
'tests/server/l10n',
|
||||
'tests/server/metrics'
|
||||
];
|
||||
|
||||
return intern;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'intern/dojo/node!../../server/lib/configuration',
|
||||
'intern/dojo/node!request'
|
||||
], function (registerSuite, assert, config, request) {
|
||||
'use strict';
|
||||
|
||||
var serverUrl = config.get('public_url');
|
||||
|
||||
var suite = {
|
||||
name: 'metrics'
|
||||
};
|
||||
|
||||
suite['#get /config returns a `metricsSampleRate`'] = function () {
|
||||
var dfd = this.async(1000);
|
||||
|
||||
request(serverUrl + '/config',
|
||||
dfd.callback(function (err, res) {
|
||||
var results = JSON.parse(res.body);
|
||||
|
||||
assert.equal(results.metricsSampleRate, config.get('metrics.sample_rate'));
|
||||
}, dfd.reject.bind(dfd)));
|
||||
};
|
||||
|
||||
suite['#post /metrics - does nothing yet'] = function () {
|
||||
var dfd = this.async(1000);
|
||||
|
||||
request.post(serverUrl + '/metrics', {
|
||||
data: {
|
||||
events: [ { type: 'event1', offset: 1 } ]
|
||||
}
|
||||
},
|
||||
dfd.callback(function (err, res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
}, dfd.reject.bind(dfd)));
|
||||
};
|
||||
|
||||
registerSuite(suite);
|
||||
});
|
Загрузка…
Ссылка в новой задаче