Google Analytics, track page unload event - javascript

I'm trying to accomplish tracking an event when a user leaves the page with Google Analytics (analytics.js). Though it is unknown how the user will leave, it may be because of an external link or just closing the tab. So my thought was to hook onto the beforeunload or unload event and then:
window.addEventListener("beforeunload", function() {
ga('send', 'event', 'some', 'other', 'data');
});
Now my question is, will the request to the GA server be synchronous or can I somehow force that behaviour with the hitCallback property? If that is not possible, how else can I achieve this? Preferably without having to set a timeout or fixed waiting time for the user!

There is a way to make sure the request will be sent to GA. Simo Ahava wrote a very good blog post titled - "Leverage useBeacon And beforeunload In Google Analytics".
Utilizing the brilliant sendBeacon solution. Here's quote which addresses the selected answer of this question:
User agents will typically ignore asynchronous XMLHttpRequests made in
an unload handler. To solve this problem, analytics and diagnostics
code will typically make a synchronous XMLHttpRequest in an unload or
beforeunload handler to submit the data. The synchronous
XMLHttpRequest forces the User Agent to delay unloading the document,
and makes the next navigation appear to be slower. There is nothing
the next page can do to avoid this perception of poor page load
performance.
There are other techniques used to ensure that data is submitted. One
such technique is to delay the unload in order to submit data by
creating an Image element and setting its src attribute within the
unload handler. As most user agents will delay the unload to complete
the pending image load, data can be submitted during the unload.
Another technique is to create a no-op loop for several seconds within
the unload handler to delay the unload and submit data to a server.
Not only do these techniques represent poor coding patterns, some of
them are unreliable and also result in the perception of poor page
load performance for the next navigation.

The request will not be synchronous, GA tracking calls never are.
The only way to ensure the call completes is to make sure the page stays open long enough - for an event on a link you would normally do this with a timeout potentially combined with a hitCallback, as you mentioned.
The only way to keep a window open when the user closes a tab is to return a value from your beforeunload handler, which will prompt a "Confirm Navigation" alert. That would be a really bad solution just to track a GA event, obviously.

Set transport to beacon, with ga:
window.addEventListener("beforeunload", function() {
ga('send', 'event', 'page_unload', 'bye bye', {transport: 'beacon'});
});
Or transport_type to beacon, with gtag:
window.addEventListener("beforeunload", function() {
gtag('event', 'page_unload', {
'event_category': 'my cat',
'event_label': 'my label',
'transport_type': 'beacon' // <--- important part here
});
});
For what is worth, beacon should become the default transport mode anyway. As of 2020-09:
Currently, gtag.js only uses navigator.sendBeacon if the transport
mechanism is set to 'beacon'. However, in the future, gtag.js will
likely switch to using 'beacon' as the default mechanism in browsers
that support it.

As pointed out by tomconnors, this does NOT work. I'm leaving the answer to help warn anyone thinking about doing it this way. Beacon transport is probably the way to go, but wasn't widely supported at the time of the original answer.
You can wait for a hit to be sent to Google Analytics in the page onunload, but it does require a busy loop. In my case this did not delay user navigation, as the page was a popup window that is dedicated to a webapp. I'd be more concerned about doing this inline with normal web page navigation. Still, I had to take 2 showers to get the filth off after committing code with a busy loop.
var MAX_WAIT_MS = 1000;
var _waitForFinalHit = false;
function recordFinalHit() {
_waitForFinalHit = true;
ga('send', 'event', 'some', 'other', 'data', {
'hitCallback': function() {
_waitForFinalHit = false;
}
});
}
function waitForFinalHit() {
var waitStart = new Date().getTime();
while (_waitForFinalHit
&& (new Date().getTime() - waitStart < MAX_WAIT_MS)) { }
}
function myOnUnload() {
recordFinalHit();
// Do your other final stuff here...
waitForFinalHit();
}
window.onunload = myOnUnload;

Related

Why doesn't window.open get blocked on a setTimeout <= 1000ms?

document.querySelector('#ontime').onclick = function() {
setTimeout(() => {
window.open('https://www.google.com');
}, 1000);
};
When using window.open after a user click with a timeout <= 1000ms (or a Promise.resolve().then(...)) it doesn't get blocked by the browser.
If you do the same using a timeout > 1000ms or requestAnimationFrame, the popup gets blocked.
Full example with the 4 cases is available clicking the link below:
https://jsfiddle.net/kouty79/rcwgbfxy/
Does anybody know why? Is there any documentation or w3c spec about this?
From HTML 5.2:
An algorithm is allowed to show a popup if any of the following conditions is true:
…
event listener for a trusted event
…
… queued by an algorithm that was allowed to show a popup, and the chain of such algorithms started within a user-agent defined timeframe.
onclick is a trusted event, but setTimeout put it in a queue (so it wasn't called directly) so the popup has to come within a certain time.
That time is up to the browser to decide.

How to reliably send tracking event on link click?

I have a tracking event (in the form of an http call to a tracking server) that fires on the clicking of a link, fired by an onclick event.
However, it appears that fairly often, the event is not registered by the tracking server because the browser cuts off the (long-running) event call when it loads the new page.
I'm reluctant to wait for a reply before forwarding the user to the new page, in case the reply is delayed and the user has to wait.
Is there a way to ensure the event call completes and forward the user on immediately?
Maybe preventDefault helps you. You don't share your code, it's because I explain you this way:
$('a.link').on('click', function(e) {
e.preventDefault();
var trackingLink = $(this).attr('href');
// do ajax stuff
$.ajax().success(function() {
window.location.href = trackingLink;
});
});
With preventDefault() you avoid links to load the page, then make the ajax requests and if it's successful redirects to the page of the link
One option is to end an 'unload' event handler, and send the data it the handler. This will delay the unload of the page.
Example:
$(window).on('unload', function() {
$.ajax({
method: "POST",
url: "serverUrl",
data: { logData: '....' }
})
});
Maybe you can use the .sendBeacon method. It's only supported by Chrome and Firefox right now, but the description from MDN seems to fit you needs:
This method addresses the needs of analytics and diagnostics code that
typically attempt to send data to a web server prior to the unloading
of the document.
Example (from the MDN article):
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon("/log", analyticsData);
}

How to properly handle chrome extension updates from content scripts

In background page we're able to detect extension updates using chrome.runtime.onInstalled.addListener.
But after extension has been updated all content scripts can't connect to the background page. And we get an error: Error connecting to extension ....
It's possible to re-inject content scripts using chrome.tabs.executeScript... But what if we have a sensitive data that should be saved before an update and used after update? What could we do?
Also if we re-inject all content scripts we should properly tear down previous content scripts.
What is the proper way to handle extension updates from content scripts without losing the user data?
If you've established a communication through var port = chrome.runtime.connect(...) (as described on
https://developer.chrome.com/extensions/messaging#connect), it should be possible to listen to the runtime.Port.onDisconnect event:
tport.onDisconnect.addListener(function(msg) {...})
There you can react and, e.g. apply some sort of memoization, let's say via localStorage. But in general, I would suggest to keep content scripts as tiny as possible and perform all the data manipulations in the background, letting content only to collect/pass data and render some state, if needed.
Once Chrome extension update happens, the "orphaned" content script is cut off from the extension completely. The only way it can still communicate is through shared DOM. If you're talking about really sensitive data, this is not secure from the page. More on that later.
First off, you can delay an update. In your background script, add a handler for the chrome.runtime.onUpdateAvailable event. As long as the listener is there, you have a chance to do cleanup.
// Background script
chrome.runtime.onUpdateAvailable.addListener(function(details) {
// Do your work, call the callback when done
syncRemainingData(function() {
chrome.runtime.reload();
});
});
Second, suppose the worst happens and you are cut off. You can still communicate using DOM events:
// Content script
// Get ready for data
window.addEventListener("SendRemainingData", function(evt) {
processData(evt.detail);
}, false);
// Request data
var event = new CustomEvent("RequestRemainingData");
window.dispatchEvent(event);
// Be ready to send data if asked later
window.addEventListener("RequestRemainingData", function(evt) {
var event = new CustomEvent("SendRemainingData", {detail: data});
window.dispatchEvent(event);
}, false);
However, this communication channel is potentially eavesdropped on by the host page. And, as said previously, that eavesdropping is not something you can bypass.
Yet, you can have some out-of-band pre-shared data. Suppose that you generate a random key on first install and keep it in chrome.storage - this is not accessible by web pages by any means. Of course, once orphaned you can't read it, but you can at the moment of injection.
var PSK;
chrome.storage.local.get("preSharedKey", function(data) {
PSK = data.preSharedKey;
// ...
window.addEventListener("SendRemainingData", function(evt) {
processData(decrypt(evt.detail, PSK));
}, false);
// ...
window.addEventListener("RequestRemainingData", function(evt) {
var event = new CustomEvent("SendRemainingData", {detail: encrypt(data, PSK)});
window.dispatchEvent(event);
}, false);
});
This is of course proof-of-concept code. I doubt that you will need more than an onUpdateAvailable listener.

Cancel/Abort/Confirm an HTML 5 state change event (onpopstate)

With unload event of window, it is possible to show the user a confirmation dialog , let's say in a situation where there is an ongoing request you are waiting for to finish and navigating away from the page will terminate that request.
Is there a way to accomplish this with onpopstate of HTML5 history API? Or any other way with the same outcome?
I guess you could modify the behavior of pushState to ask for confirmation before pushing a new state :
// Store original pushState
var _pushState = window.history.pushState;
// Some bad global variable to determine if confirmation is needed
var askForConfirm = true;
// Modify pushState behavior
window.history.pushState = function() {
if(!askForConfirm || confirm('Are you sure you want to quit this page ?')) {
// Call original pushState
_pushState.apply(window.history,arguments);
}
};
I don't know if it helps in your situation, but Sammy.js, a popular hash-routing library has a before handler. I've used it in my application to record the previously accessed hash, and if it's the hash I want to stop them from leaving, return false will keep them on that page. You still need to rewrite the URL to display the previous page, but it seems to be working.
See my answer in this other thread for more info.

Accessing every document that a user currently views from an extension

I'm writing an extension that checks every document a user views on certain data structures, does some back-end server calls and displays the results as a dialog.The problem is starting and continuing the sequence properly with event listeners. My actual idea is:
Load: function()
{
var Listener = function(){ Fabogore.Start();};
var ListenerTab = window.gBrowser.selectedTab;
ListenerTab.addEventListener("load",Listener,true);
}
(...)
ListenerTab.removeEventListener("load", Listener, true);
Fabogore.Load();
The Fabogore.Load function is first initialized when the browser gets opened. It works only once I get these data structures, but not afterwards. But theoretically the script should initialize a new listener, so maybe it's the selectedTab. I also tried listening to focus events.
If someone has got an alternative solution how to access a page a user is currently viewing I would feel comfortable as well.
The common approach is using a progress listener. If I understand correctly, you want to get a notification whenever a browser tab finished loading. So the important method in your progress listener would be onStateChange (it needs to have all the other methods as well however):
onStateChange: function(aWebProgress, aRequest, aFlag, aStatus)
{
if ((aFlag & Components.interfaces.nsIWebProgressListener.STATE_STOP) &&
(aFlag & Components.interfaces.nsIWebProgressListener.STATE_IS_WINDOW) &&
aWebProgress.DOMWindow == aWebProgress.DOMWindow.top)
{
// A window finished loading and it is the top-level frame in its tab
Fabogore.Start(aWebProgress.DOMWindow);
}
},
Ok, I found a way which works from the MDN documentation, and achieves that every document a user opens can be accessed by your extension. Accessing every document a user focuses is too much, I want the code to be executed only once. So I start with initializing the Exentsion, and Listen to DOMcontentloaded Event
window.addEventListener("load", function() { Fabogore.init(); }, false);
var Fabogore = {
init: function() {
var appcontent = document.getElementById("appcontent"); // browser
if(appcontent)
appcontent.addEventListener("DOMContentLoaded", Fabogore.onPageLoad, true);
},
This executes the code every Time a page is loaded. Now what's important is, that you execute your code with the new loaded page, and not with the old one. You can acces this one with the variable aEvent:
onPageLoad: function(aEvent)
{
var doc = aEvent.originalTarget;//Document which initiated the event
With the variable "doc" you can check data structures using XPCNativeWrapper etc. Thanks Wladimir for bringing me in the right direction, I suppose if you need a more sophisticated event listening choose his way with the progress listeners.

Categories

Resources