mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #289 from sharelatex/afc-track-edit-sessions
Send editing session heartbeat to the analytics service
This commit is contained in:
commit
19c97cb15b
7 changed files with 118 additions and 22 deletions
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
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}"
|
||||
|
@ -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}",
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue