Don’t let page_views slow down page unload

See this? https://cl.ly/68e723db54c8 that’s a beforeuload in
page_views.js that is taking 335ms in prod to run _before_ the browser 
can navigate to any other pages.

what we are trying to do there in page_views is log the last page view
And we can’t use an xhr to do so because it would get aborted when we
go to a new page. Where there is a browser API to solve that exact 
problem: 

https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon

This is a screenshot of after: https://cl.ly/fed0076a15eb 
that sendBeacon call only takes 0.4ms

Oh, and we don’t need to do any of this unless ENV.page_view_update_url
Is set, so we can defer loading any of it unless that is present

Test plan:
* page views should work exactly as before
* turn on page views
* go to a page and sit there for more than 30 seconds
* clear your rails log and your browser log
* click to go to a new page
* you should see a request in your browser Network log to
  page_views/xxxx-xxxx-xxx-xx-xxx?page_view_token=xxxx
* look at your rails server logs, you should see something like:
  Processing by PageViewsController#update as */*
  Parameters: {"interaction_seconds"=>"1005”, …
  ”id"=>"36bcbd62-3a11-44bc-8dd2-69410050ff74"}
   

Change-Id: Ie67cc01dd1524c75916a26fbeffa67a01f490adc
Reviewed-on: https://gerrit.instructure.com/196416
Tested-by: Jenkins
Reviewed-by: Clay Diffrient <cdiffrient@instructure.com>
QA-Review: Clay Diffrient <cdiffrient@instructure.com>
Product-Review: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
Ryan Shaw 2019-06-04 11:40:33 -06:00
parent 3a9c4a833c
commit cd0467a1b1
2 changed files with 69 additions and 77 deletions

View File

@ -31,7 +31,6 @@ import preventDefault from 'compiled/fn/preventDefault'
import 'media_comments'
import 'reminders'
import 'instructure'
import 'page_views'
import 'compiled/behaviors/authenticity_token'
import 'compiled/behaviors/ujsLinks'
import 'compiled/behaviors/admin-links'
@ -42,6 +41,8 @@ import 'compiled/behaviors/ping'
import 'compiled/behaviors/broken-images'
import 'LtiThumbnailLauncher'
if (ENV.page_view_update_url) import('page_views')
// preventDefault so we dont change the hash
// this will make nested apps that use the hash happy
$('#skip_navigation_link').on(

View File

@ -18,91 +18,82 @@
import INST from './INST'
import $ from 'jquery'
import authenticity_token from 'compiled/behaviors/authenticity_token'
import './jquery.ajaxJSON'
$(document).ready(function() {
let interactionSeconds = 0,
update_url = window.ENV.page_view_update_url
eventInTime = false
let update_url = window.ENV.page_view_update_url
if (update_url) {
$(() => {
let interactionSeconds = 0
INST.interaction_contexts = {}
INST.interaction_contexts = {}
if (document.cookie && document.cookie.match(/last_page_view/)) {
const match = document.cookie.match(/last_page_view=([^;]+)/)
if (match && match[1]) {
try {
const data = $.parseJSON(unescape(match[1]))
if (data && data.url && data.seconds) {
setTimeout(function() {
$.ajaxJSON(
data.url,
'PUT',
{interaction_seconds: data.seconds},
function() {},
function() {},
3000
)
if (update_url) {
let secondsSinceLastEvent = 0
const intervalInSeconds = 60 * 5
$(document).bind('page_view_update_url_received', function(event, new_update_url) {
update_url = new_update_url
})
let updateTrigger
$(document).bind('page_view_update', function(event, force) {
const data = {}
if (force || (interactionSeconds > 10 && secondsSinceLastEvent < intervalInSeconds)) {
data.interaction_seconds = interactionSeconds
$.ajaxJSON(update_url, 'PUT', data, null, function(result, xhr) {
if (xhr.status === 422) {
clearInterval(updateTrigger)
}
})
interactionSeconds = 0
}
} catch (e) {}
}
document.cookie = 'last_page_view=; Path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT'
}
})
if (update_url) {
let secondsSinceLastEvent = 0
const intervalInSeconds = 60 * 5
updateTrigger = setInterval(function() {
$(document).triggerHandler('page_view_update')
}, 1000 * intervalInSeconds)
$(document).bind('page_view_update_url_received', function(event, new_update_url) {
update_url = new_update_url
})
let updateTrigger
$(document).bind('page_view_update', function(event, force) {
const data = {}
if (force || (interactionSeconds > 10 && secondsSinceLastEvent < intervalInSeconds)) {
data.interaction_seconds = interactionSeconds
$.ajaxJSON(update_url, 'PUT', data, null, function(result, xhr) {
if (xhr.status === 422) {
clearInterval(updateTrigger)
window.addEventListener(
'unload',
() => {
if (interactionSeconds > 30) {
// Use sendBeacon so the request doesn't get cancelled as we navigate away like a normal XHR would,
// but because sendBeacon only sends POST requests, we have to use FormData to fake the "_method" to PUT
// like Rail's `form_for` does
const formData = new FormData()
formData.append('interaction_seconds', interactionSeconds)
formData.append('_method', 'put')
formData.append('authenticity_token', authenticity_token())
formData.append('utf8', '&#x2713')
navigator.sendBeacon(update_url, formData)
}
})
interactionSeconds = 0
}
})
},
false
)
updateTrigger = setInterval(function() {
$(document).triggerHandler('page_view_update')
}, 1000 * intervalInSeconds)
window.addEventListener('beforeunload', function(e) {
if (interactionSeconds > 30) {
const value = JSON.stringify({url: update_url, seconds: interactionSeconds})
document.cookie = `last_page_view=${escape(value)}; Path=/;`
}
})
var eventInTime = false
$(document).bind('mousemove keypress mousedown focus', function() {
eventInTime = true
})
setInterval(function() {
if (eventInTime) {
interactionSeconds++
if (INST && INST.interaction_context && INST.interaction_contexts) {
INST.interaction_contexts[INST.interaction_context] =
(INST.interaction_contexts[INST.interaction_context] || 0) + 1
}
eventInTime = false
if (secondsSinceLastEvent >= intervalInSeconds) {
let eventInTime = false
$(document).bind('mousemove keypress mousedown focus', function() {
eventInTime = true
})
setInterval(function() {
if (eventInTime) {
interactionSeconds++
if (INST && INST.interaction_context && INST.interaction_contexts) {
INST.interaction_contexts[INST.interaction_context] =
(INST.interaction_contexts[INST.interaction_context] || 0) + 1
}
eventInTime = false
if (secondsSinceLastEvent >= intervalInSeconds) {
secondsSinceLastEvent = 0
$(document).triggerHandler('page_view_update')
}
secondsSinceLastEvent = 0
$(document).triggerHandler('page_view_update')
} else {
secondsSinceLastEvent++
}
secondsSinceLastEvent = 0
} else {
secondsSinceLastEvent++
}
}, 1000)
}
})
}, 1000)
}
})
}