diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee index 18a7de5e24..a57118723e 100644 --- a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee +++ b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee @@ -3,13 +3,26 @@ Errors = require "../Errors/Errors" AuthenticationController = require("../Authentication/AuthenticationController") module.exports = AnalyticsController = + updateEditingSession: (req, res, next) -> + userId = AuthenticationController.getLoggedInUserId(req) + projectId = req.params.projectId + + if userId? + AnalyticsManager.updateEditingSession userId, projectId, {}, (error) -> + respondWith(error, res, next) + else + res.send 204 + recordEvent: (req, res, next) -> user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) -> - if error instanceof Errors.ServiceNotConfiguredError - # ignore, no-op - return res.send(204) - else if error? - return next(error) - else - return res.send 204 + respondWith(error, res, next) + +respondWith = (error, res, next) -> + if error instanceof Errors.ServiceNotConfiguredError + # ignore, no-op + res.send(204) + else if error? + next(error) + else + res.send 204 diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee index 12e5b68c84..085d9d19b3 100644 --- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee +++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee @@ -39,6 +39,21 @@ module.exports = url: "/user/#{user_id}/event" makeRequest opts, callback + updateEditingSession: (userId, projectId, segmentation = {}, callback = (error) ->) -> + if userId+"" == settings.smokeTest?.userId+"" + return callback() + opts = + body: + segmentation: segmentation + json: true + method: "PUT" + timeout: 1000 + url: "/editingSession" + qs: + userId: userId + projectId: projectId + makeRequest opts, callback + getLastOccurance: (user_id, event, callback = (error) ->) -> opts = @@ -49,7 +64,7 @@ module.exports = timeout:1000 url: "/user/#{user_id}/event/last_occurnace" makeRequest opts, (err, response, body)-> - if err? + if err? console.log response, opts logger.err {user_id, err}, "error getting last occurance of event" return callback err diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee index 4fcba0c748..e7a6051c41 100644 --- a/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee +++ b/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee @@ -5,6 +5,10 @@ AnalyticsProxy = require('./AnalyticsProxy') module.exports = apply: (webRouter, privateApiRouter, publicApiRouter) -> webRouter.post '/event/:event', AnalyticsController.recordEvent + + webRouter.put '/editingSession/:projectId', + AnalyticsController.updateEditingSession + publicApiRouter.use '/analytics/graphs', AuthenticationController.httpAuth, AnalyticsProxy.call('/graphs') diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index f2bae223c5..6f49b4bce8 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -170,6 +170,8 @@ define [ $scope.$broadcast('ide:loaded') _loaded = true + $scope.$on 'cursor:editor:update', event_tracking.editingSessionHeartbeat + DARK_THEMES = [ "ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers", "merbivore", "merbivore_soft", "mono_industrial", "monokai", diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index ff5d1554e2..3b43a05afd 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -36,7 +36,7 @@ define [ url = ace.config._moduleUrl(args...) return url - App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) -> + App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q, $window) -> monkeyPatchSearch($rootScope, $compile) return { @@ -300,6 +300,7 @@ define [ updateCount = 0 onChange = () -> updateCount++ + if updateCount == 100 event_tracking.send 'editor-interaction', 'multi-doc-update' scope.$emit "#{scope.name}:change" @@ -375,6 +376,16 @@ define [ # deletes and then inserts document content session.setAnnotations scope.annotations + + session.on "changeScrollTop", event_tracking.editingSessionHeartbeat + + angular.element($window).on('click', + event_tracking.editingSessionHeartbeat) + + scope.$on "$destroy", () -> + angular.element($window).off('click', + event_tracking.editingSessionHeartbeat) + if scope.eventsBridge? session.on "changeScrollTop", onScroll diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index b788057b77..e03a0a8afb 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -1,22 +1,29 @@ define [ + "moment" "base" "modules/localStorage" -], (App) -> + +], (moment, App) -> CACHE_KEY = "mbEvents" + # keep track of how many heartbeats we've sent so we can calculate how + # long wait until the next one + heartbeatsSent = 0 + nextHeartbeat = new Date() + send = (category, action, attributes = {})-> ga('send', 'event', category, action) event_name = "#{action}-#{category}" Intercom?("trackEvent", event_name, attributes) - + App.factory "event_tracking", ($http, localStorage) -> - _getEventCache = () -> + _getEventCache = () -> eventCache = localStorage CACHE_KEY # Initialize as an empy object if the event cache is still empty. if !eventCache? eventCache = {} - localStorage CACHE_KEY, eventCache + localStorage CACHE_KEY, eventCache return eventCache @@ -30,10 +37,39 @@ define [ localStorage CACHE_KEY, curCache + _sendEditingSessionHeartbeat = (segmentation) -> + $http({ + url: "/editingSession/#{window.project_id}", + method: "PUT", + data: segmentation, + headers: { + "X-CSRF-Token": window.csrfToken + } + }) + return { send: (category, action, label, value)-> ga('send', 'event', category, action, label, value) + + editingSessionHeartbeat: (segmentation = {}) -> + return unless nextHeartbeat <= new Date() + + _sendEditingSessionHeartbeat(segmentation) + + heartbeatsSent++ + + # send two first heartbeats at 0 and 30s then increase the backoff time + # 1min per call until we reach 5 min + backoffSecs = if heartbeatsSent <= 2 + 30 + else if heartbeatsSent <= 6 + (heartbeatsSent - 2) * 60 + else + 300 + + nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate() + sendMB: (key, segmentation = {}) -> $http { url: "/event/#{key}", @@ -45,7 +81,7 @@ define [ } sendMBSampled: (key, segmentation) -> - @sendMB key, segmentation if Math.random() < .01 + @sendMB key, segmentation if Math.random() < .01 sendMBOnce: (key, segmentation) -> if ! _eventInCache(key) diff --git a/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee b/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee index 2c52b8b5f0..8ac73c5af1 100644 --- a/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee +++ b/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee @@ -14,23 +14,38 @@ describe 'AnalyticsController', -> getLoggedInUserId: sinon.stub() @AnalyticsManager = + updateEditingSession: sinon.stub().callsArgWith(3) recordEvent: sinon.stub().callsArgWith(3) - @req = - params: - event:"i_did_something" - body:"stuff" - sessionID: "sessionIDHere" - - @res = - send:-> @controller = SandboxedModule.require modulePath, requires: "./AnalyticsManager":@AnalyticsManager "../Authentication/AuthenticationController":@AuthenticationController "logger-sharelatex": log:-> + @res = + send:-> + + describe "updateEditingSession", -> + beforeEach -> + @req = + params: + projectId: "a project id" + + it "delegates to the AnalyticsManager", (done) -> + @AuthenticationController.getLoggedInUserId.returns("1234") + @controller.updateEditingSession @req, @res + + @AnalyticsManager.updateEditingSession.calledWith("1234", "a project id", {}).should.equal true + done() + describe "recordEvent", -> + beforeEach -> + @req = + params: + event:"i_did_something" + body:"stuff" + sessionID: "sessionIDHere" it "should use the user_id", (done)-> @AuthenticationController.getLoggedInUserId.returns("1234")