diff --git a/loop/auth.js b/loop/auth.js index a532020..1f2e50f 100644 --- a/loop/auth.js +++ b/loop/auth.js @@ -7,7 +7,7 @@ var hawk = require('express-hawkauth'); var encrypt = require("./encrypt").encrypt; -var errors = require('./errno.json'); +var errors = require('./errno'); var hmac = require('./hmac'); var sendError = require('./utils').sendError; var fxa = require('./fxa'); diff --git a/loop/config.js b/loop/config.js index 3c8cfba..3b295fc 100644 --- a/loop/config.js +++ b/loop/config.js @@ -529,6 +529,17 @@ var conf = convict({ default: "", env: "ROOMS_HKDF_SECRET" } + }, + ga: { + activated: { + doc: "Should we send POST /events data to Google Analytics while true.", + default: false, + format: Boolean + }, + id: { + doc: "Google analytics ID.", + format: String + } } }); diff --git a/loop/fxa.js b/loop/fxa.js index 52c93c5..29392c8 100644 --- a/loop/fxa.js +++ b/loop/fxa.js @@ -9,7 +9,7 @@ var request = require('request'); var conf = require('./config').conf; var atob = require('atob'); var sendError = require("./utils").sendError; -var errors = require("./errno.json"); +var errors = require("./errno"); // Don't be limited by the default node.js HTTP agent. var agent = new https.Agent(); diff --git a/loop/index.js b/loop/index.js index 957149d..4768a16 100644 --- a/loop/index.js +++ b/loop/index.js @@ -144,6 +144,9 @@ var rooms = require("./routes/rooms"); rooms(apiRouter, conf, logError, storage, filestorage, auth, validators, tokBox, simplePush, notifications); +var analytics = require("./routes/analytics").analytics; +analytics(apiRouter, conf, auth, validators); + var session = require("./routes/session"); session(apiRouter, conf, storage, auth); diff --git a/loop/middlewares.js b/loop/middlewares.js index cd7511e..9ab6193 100644 --- a/loop/middlewares.js +++ b/loop/middlewares.js @@ -5,7 +5,7 @@ "use strict"; var conf = require("./config").conf; -var loopPackageData = require('../package.json'); +var loopPackageData = require('../package'); var os = require("os"); // Assume the hostname will not change once the server is launched. diff --git a/loop/routes/analytics.js b/loop/routes/analytics.js new file mode 100644 index 0000000..e17ecfc --- /dev/null +++ b/loop/routes/analytics.js @@ -0,0 +1,29 @@ +/* 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 errors = require('../errno'); +var sendError = require('../utils').sendError; + +exports.sendAnalytics = require('../utils').sendAnalytics; + +exports.analytics = function (app, conf, auth, validators) { + /** + * Delete an account and all data associated with it. + **/ + app.post('/event', validators.requireParams('event', 'action', 'label'), + auth.requireHawkSession, function(req, res) { + var ga = conf.get("ga"); + if (ga.activated) { + module.exports.sendAnalytics(ga.id, req.user, req.body); + res.status(204).json({}); + } else { + sendError( + res, 405, errors.UNDEFINED, + "Google Analytics events are not configured for this server." + ); + } + }); +}; diff --git a/loop/routes/call-url.js b/loop/routes/call-url.js index c829bcd..c17351c 100644 --- a/loop/routes/call-url.js +++ b/loop/routes/call-url.js @@ -4,7 +4,7 @@ "use strict"; -var errors = require('../errno.json'); +var errors = require('../errno'); var sendError = require('../utils').sendError; module.exports = function (app, conf, logError, storage, auth, validators) { diff --git a/loop/routes/calls.js b/loop/routes/calls.js index 7d4911a..06a1774 100644 --- a/loop/routes/calls.js +++ b/loop/routes/calls.js @@ -6,7 +6,7 @@ var async = require('async'); var randomBytes = require('crypto').randomBytes; -var errors = require('../errno.json'); +var errors = require('../errno'); var hmac = require('../hmac'); var getProgressURL = require('../utils').getProgressURL; var sendError = require('../utils').sendError; diff --git a/loop/routes/fxa-oauth.js b/loop/routes/fxa-oauth.js index 347cbab..8d44747 100644 --- a/loop/routes/fxa-oauth.js +++ b/loop/routes/fxa-oauth.js @@ -7,7 +7,7 @@ var randomBytes = require('crypto').randomBytes; var request = require('request'); var sendError = require('../utils').sendError; -var errors = require('../errno.json'); +var errors = require('../errno'); var hmac = require('../hmac'); var encrypt = require('../encrypt').encrypt; diff --git a/loop/routes/registration.js b/loop/routes/registration.js index cb298cb..7621b51 100644 --- a/loop/routes/registration.js +++ b/loop/routes/registration.js @@ -4,7 +4,7 @@ "use strict"; -var errors = require("../errno.json"); +var errors = require("../errno"); var sendError = require('../utils').sendError; var getSimplePushURLS = require('../utils').getSimplePushURLS; diff --git a/loop/routes/rooms.js b/loop/routes/rooms.js index fc81791..0173d74 100644 --- a/loop/routes/rooms.js +++ b/loop/routes/rooms.js @@ -10,7 +10,7 @@ var uuid = require('node-uuid'); var decrypt = require('../encrypt').decrypt; var encrypt = require('../encrypt').encrypt; -var errors = require('../errno.json'); +var errors = require('../errno'); var getUserAccount = require('../utils').getUserAccount; var sendError = require('../utils').sendError; var tokenlib = require('../tokenlib'); diff --git a/loop/routes/validators.js b/loop/routes/validators.js index 759266c..f32f908 100644 --- a/loop/routes/validators.js +++ b/loop/routes/validators.js @@ -4,7 +4,7 @@ "use strict"; -var errors = require("../errno.json"); +var errors = require("../errno"); var sendError = require('../utils').sendError; var getSimplePushURLS = require('../utils').getSimplePushURLS; var tokenlib = require('../tokenlib'); diff --git a/loop/utils.js b/loop/utils.js index fae92e7..bf81f27 100644 --- a/loop/utils.js +++ b/loop/utils.js @@ -6,6 +6,8 @@ var conf = require('./config').conf; var decrypt = require('./encrypt').decrypt; +var ua = require('universal-analytics'); + function sendError(res, code, errno, error, message, info) { var errmap = {}; @@ -92,6 +94,14 @@ function getSimplePushURLS(req, callback) { callback(null, simplePushURLs); } +/** + * Create a UA instance and sent an event to it. + **/ +function sendAnalytics(gaID, userID, data) { + var userAnalytics = ua(gaID, userID, {strictCidFormat: false, https: true}); + userAnalytics.event(data.event, data.action, data.label).send(); +} + /** * Return a unix timestamp in seconds. **/ @@ -140,6 +150,7 @@ module.exports = { time: time, getUserAccount: getUserAccount, getSimplePushURLS: getSimplePushURLS, + sendAnalytics: sendAnalytics, dedupeArray: dedupeArray, encode: encode, decode: decode, diff --git a/package.json b/package.json index 0ed9bd2..b6cb9a8 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "sodium": "1.0.13", "statsd-node": "0.2.3", "strftime": "0.8.2", + "universal-analytics": "0.3.10", "urlsafe-base64": "1.0.0", "ws": "1.0.1", "yargs": "3.0.4" diff --git a/test/functional_test.js b/test/functional_test.js index bb3b0d9..1b5e7d8 100644 --- a/test/functional_test.js +++ b/test/functional_test.js @@ -22,8 +22,10 @@ var conf = loop.conf; var tokBox = loop.tokBox; var storage = loop.storage; var statsdClient = loop.statsdClient; -var getProgressURL = require("../loop/utils").getProgressURL; -var time = require('../loop/utils').time; + +var utils = require("../loop/utils"); +var getProgressURL = utils.getProgressURL; +var time = utils.time; var Token = require("express-hawkauth").Token; var tokenlib = require("../loop/tokenlib"); @@ -32,6 +34,7 @@ var tokBoxConfig = conf.get("tokBox"); var hmac = require("../loop/hmac"); var pjson = require("../package.json"); +var analytics = require("../loop/routes/analytics"); var getMiddlewares = require("./support").getMiddlewares; var expectFormattedError = require("./support").expectFormattedError; var errors = require("../loop/errno.json"); @@ -1720,6 +1723,97 @@ function runOnPrefix(apiPrefix) { }); }); }); + + describe("POST /event", function() { + it("should fail if does not have event/label/action.", function(done) { + supertest(app) + .post('/event') + .type('json') + .expect('Content-Type', /json/) + .expect(400) + .end(function(err, res) { + if (err) throw err; + expectFormattedError(res, 400, errors.MISSING_PARAMETERS, + "Missing: event, action, label"); + done(); + }); + }); + + it("should returns a 401 if no hawk session.", function(done) { + supertest(app) + .post('/event') + .type('json') + .expect('Content-Type', /json/) + .send({'event': 'tab_shared', + 'action': 'clicked', + 'label': 'Tab shared'}) + .expect(401) + .end(done); + }); + + it("should returns a 405 if disabled in settings.", function(done) { + supertest(app) + .post('/event') + .type('json') + .expect('Content-Type', /json/) + .send({'event': 'tab_shared', + 'action': 'clicked', + 'label': 'Tab shared'}) + .hawk(hawkCredentials) + .expect(405) + .end(done); + }); + + context("enabled", function() { + var sendAnalyticsStub; + beforeEach(function() { + sendAnalyticsStub = sandbox.stub(analytics, "sendAnalytics"); + conf.set('ga', { + activated: true, + id: "fake-ga-id" + }); + }); + + afterEach(function() { + conf.set('ga', { + activated: false, + id: null + }); + }); + + it("should returns a 204 if everything went well.", function(done) { + supertest(app) + .post('/event') + .type('json') + .send({'event': 'tab_shared', + 'action': 'clicked', + 'label': 'Tab shared'}) + .hawk(hawkCredentials) + .expect(204) + .end(done); + }); + + it("should pass gaID, userID and body to sendAnalytics.", function(done) { + supertest(app) + .post('/event') + .type('json') + .send({'event': 'tab_shared', + 'action': 'clicked', + 'label': 'Tab shared'}) + .hawk(hawkCredentials) + .expect(204) + .end(function(err) { + if (err) throw err; + assert.calledWithExactly(sendAnalyticsStub, "fake-ga-id", userHmac, { + event: 'tab_shared', + action: 'clicked', + label: 'Tab shared' + }); + done(); + }); + }); + }); + }); }); describe("GET /api-specs", function() { @@ -1750,8 +1844,6 @@ function runOnPrefix(apiPrefix) { }); }); }); - - } describe("HTTP API exposed by the server", function() {