Merge pull request #289 from sharelatex/afc-track-edit-sessions

Send editing session heartbeat to the analytics service
This commit is contained in:
Shane Kilkelly 2018-02-01 12:31:38 +00:00 committed by GitHub
commit 19c97cb15b
7 changed files with 118 additions and 22 deletions

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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",

View file

@ -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

View file

@ -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)

View file

@ -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")