Race condition and using Google Analytics Asynchronous (_gaq) synchronously - google-analytics

I have a website which is using Google Analytics newer asynchronous tracking method (_gaq). The problem I've run into is that I want to institute some specific link tracking and am worried that I will be creating a race condition.
Basically, it's a news website so it has headlines which link to stories all over the place. A headline for a story might appear in 3 different places on a page, and appear on hundreds of other pages. Thus, in order to understand how our audience is interacting with the site we have to track how each specific headline block is used, and not just the destination. Because of those two stipulations tracking individual pages, nor tracking referred pages won't be enough, we have to track individual links.
So if I have a link.
Here
Because _gaq.push() is an asynchronous call, isn't it possible that the page change will occur prior to Google's completion of the click tracking? If so is there a way to prevent that, or do I have a misunderstanding about the way that Google Analytics Async functions (http://code.google.com/apis/analytics/docs/tracking/asyncUsageGuide.html).

You're right. If the browser leaves the page before it sends the GA tracking beacon (gif hit) for the event, the event will not be recorded. This is not new to the async code however, because the process of sending the tracking beacon is asynchronous; the old code worked the same way in that respect. If tracking is really that important, you could do something like this:
function track(link) {
if (!_gat) return true;
_gaq.push(['_trackEvent', 'stuff']);
setTimeout(function() {location.href=link.href'}, 200);
return false;
}
...
This will stop the browser from going to the next page when the link is clicked if GA has been loaded already (it's probably best to not make the user wait that long). Then it sends the event and waits 200 milliseconds to send the user to the href of the link they clicked on. This increases the likelihood that the event will be recorded. You can increase the likelihood even more by making the timeout longer, but that also may be hurting user-experience in the process. It's a balance you'll have to experiment with.

I've got this problem too, and am determined to find a real solution.
What about pushing the function into the queue?
// Log a pageview to GA for a conversion
_gaq.push(['_trackPageview', url]);
// Push the redirect to make sure it happens AFTER we track the pageview
_gaq.push(function() { document.location = url; });

From Google's documentation for universal analytics (new version since most other answers for this question). You can now easily specify a callback.
var trackOutboundLink = function(url) {
ga('send', 'event', 'outbound', 'click', url, {'hitCallback':
function () {
document.location = url;
}
});
}
For clarity I'd recommend using this syntax, which makes it clearer which properties you're sending and easier to add more :
ga('send', 'event', {
'eventCategory': 'Homepage',
'eventAction': 'Video Play',
'eventLabel': label,
'eventValue': null,
'hitCallback': function()
{
// redirect here
},
'transport': 'beacon',
'nonInteraction': (interactive || true ? 0 : 1)
});
[Here's a complete list of parameters for all possible ga calls.]
In addition I've added the transport parameter set to beacon (not actually needed because it's automatically set if appropriate):
This specifies the transport mechanism with which hits will be sent.
The options are 'beacon', 'xhr', or 'image'. By default, analytics.js
will try to figure out the best method based on the hit size and
browser capabilities. If you specify 'beacon' and the user's browser
does not support the navigator.sendBeacon method, it will fall back
to 'image' or 'xhr' depending on hit size.
So when using navigator.beacon the navigation won't interrupt the tracking . Unfortunately Microsoft's support for beacon is non existent so you should still put the redirect in a callback.

In event handler you should setup hit callback:
_gaq.push(['_set', 'hitCallback', function(){
document.location = ...
}]);
send you data
_gaq.push(['_trackEvent'
and stop event event processing
e.preventDefault();
e.stopPropagation();

I'm trying out a new approach where we build the URL for utm.gif ourselves, and request it, then only once we've received the response (the gif) we send the user on their way:
Usage:
trackPageview(url, function() { document.location = url; });
Code (CrumbleCookie from: http://www.dannytalk.com/read-google-analytics-cookie-script/)
/**
* Use this to log a pageview to google and make sure it gets through
* See: http://www.google.com/support/forum/p/Google%20Analytics/thread?tid=5f11a529100f1d47&hl=en
*/
function trackPageview(url, fn) {
var utmaCookie = crumbleCookie('__utma');
var utmzCookie = crumbleCookie('__utmz');
var cookies = '__utma=' + utmaCookie + ';__utmz=' + utmzCookie;
var requestId = '' + (Math.floor((9999999999-999999999)*Math.random()) + 1000000000);
var hId = '' + (Math.floor((9999999999-999999999)*Math.random()) + 1000000000);
var utmUrl = 'http://www.google-analytics.com/__utm.gif';
utmUrl += '?utmwv=4.8.9';
utmUrl += '&utmn=' + requestId;
utmUrl += '&utmhn=' + encodeURIComponent(window.location.hostname);
utmUrl += '&utmhid=' + hId;
utmUrl += '&utmr=-';
utmUrl += '&utmp=' + encodeURIComponent(url);
utmUrl += '&utmac=' + encodeURIComponent(_gaProfileId);
utmUrl += '&utmcc=' + encodeURIComponent(cookies);
var image = new Image();
image.onload = function() { fn(); };
image.src = utmUrl;
}
/**
* #author: Danny Ng (http://www.dannytalk.com/read-google-analytics-cookie-script/)
* #modified: 19/08/10
* #notes: Free to use and distribute without altering this comment. Would appreciate a link back :)
*/
// Strip leading and trailing white-space
String.prototype.trim = function() { return this.replace(/^\s*|\s*$/g, ''); }
// Check if string is empty
String.prototype.empty = function() {
if (this.length == 0)
return true;
else if (this.length > 0)
return /^\s*$/.test(this);
}
// Breaks cookie into an object of keypair cookie values
function crumbleCookie(c)
{
var cookie_array = document.cookie.split(';');
var keyvaluepair = {};
for (var cookie = 0; cookie < cookie_array.length; cookie++)
{
var key = cookie_array[cookie].substring(0, cookie_array[cookie].indexOf('=')).trim();
var value = cookie_array[cookie].substring(cookie_array[cookie].indexOf('=')+1, cookie_array[cookie].length).trim();
keyvaluepair[key] = value;
}
if (c)
return keyvaluepair[c] ? keyvaluepair[c] : null;
return keyvaluepair;
}

Using onmousedown instead of onclick may also help. It doesn't eliminate the race condition, but it gives GA a head start. There's also the concern of someone clicking on a link and dragging away before letting go of the mouse button, but that's probably a negligible case.

Related

How do you obtain the Google Analytics _ga linker parameter required for cross-domain tracking when using gtag.js

My code uses Javascript window.open to link to another domain. The url of the target page is dynamically generated. I want to track the analytics across the domains but cannot use the standard "linker" automatic method of gtag,js which automatically adds the _ga parameter to the 'a' tag href attribute or action attribute for a 'form'. I need to be able to manually add the_ga attribute to the dynamically generated URL which is then used in the window.open.
I have tried creating a dummy 'a href' and then simulating a 'click' using dispatchEvent with a preventDefault in the event handler. This works for a real click event (the _ga parameter is appended to the href URL which I can then extract and use) but not for a simulated click event with dispatchEvent.
The client ID can be easily obtained using the old analytics.js implementation but I can't find how to do it using gtag.js.
To put it simply, using analytics.js the following code can be used to obtain the "linkerParm" field:
ga(function(tracker) {
var linkerParam = tracker.get('linkerParam');
});
What is the gtag.js equivalent?
Can anyone help?
OK, I’ve figured out how to do this and thought I’d share it here. This is working for me but I can’t guarantee that it’s the best way.
It appears that gtag.js also loads analytics.js automatically, however it does not do the ‘create’ to set up the tracking ID.
In this example I am assuming that the standard global objects are used, window.dataLayer for gtag.js and window.ga for analytics.js.
function getLinkerParam(){
// our global variables
window.glpReady = false; // set to true when glpLinkerParam is set
window.glpLinkerParam = null; // the linkerParam
// define variables will be used in this function
var analyticsTimeout = null;
var timeoutFired = false;
var timeoutPeriod = 1000;
// Code to initialse analytics.js if necessary
function initialiseAnalytics(){
if (!window.GoogleAnalyticsObject){
window.GoogleAnalyticsObject = 'ga';
window.ga = window.ga || function(){
(window.ga.q = window.ga.q || []).push(arguments);
};
window.ga.l = 1 * new Date();
}
}
// initialise analytics.js if necessary
checker: {
// is gtag.js installed?
if (window.dataLayer) { // Yes
// obtain the tracking ID from the dataLayer
for (var i = 0; i < window.dataLayer.length; i++){
if (window.dataLayer[i][0] == 'config') {
// initialise the tracking with a create
console.log('trackingID='+window.dataLayer[i][1]);
initialiseAnalytics();
window.ga('create', window.dataLayer[i][1]);
break checker;
}
}
}
else {
// gtag.js is NOT installed
// check for analytics.js installed
if (window.GoogleAnalyticsObject){
// analytics.js is installed in it;s own right
// so the create should already be done.
break checker;
}
}
console.log('analytics not installed or config tracker id not found');
window.glpReady = true;
return;
} // end checker
// here analytics.js is defined either in it's own right
// or as part of gtag.js. In either case the tracking ID should have been 'created'.
ga('require', 'linker');
ga(function(tracker) {
console.log('Client ID: '+tracker.get('clientId'));
});
// set a timeout for the linkerParam
analyticsTimeout = setTimeout(function(){
timeoutFired = true;
console.log('get linkerParam timeout fired');
window.glpReady = true;
return;
}, timeoutPeriod);
// get the linkerParam
console.log('Get linkerParam');
ga(function(tracker) {
var linkerParam = tracker.get('linkerParam');
console.log('got linkerParam: '+linkerParam);
clearTimeout(analyticsTimeout);
window.glpLinkerParam = linkerParam;
window.glpReady = true;
return;
});
}
I hope this helps someone.

I am noticing double entry (cpc and organic) for the same user ? little confused

I'm noticing double entry in google analytics. I have multiple ocurrences where it looks like the user came from the CPC campaign (which always has a 0s session duration) but that very same user also has an entry for "organic" and all the activities are logged under that.
My site is not ranked organically for those keywords. Unless a so many users come to my site, leave, and google for my "brand name" on google and revisits, this doesn't make sense.
I'm a little confused. Here's the report:
preview from google analytics dashboard
Based on the additional information in your comment, that the sites is a Single Page Application (SPA), you are most likely facing the problem of 'Rogue Referral'.
If this is the case, what happens, is that you overwrite the location field in the Analytics hit, losing the original UTM parameters, whereas referral is still sent with the hit, so Analytics recognizes the second hit as a new traffic source. One of the solutions is to store the original page URL and send it as the location, while sending the actual visited URL in the page field.
A very good article on this topic with further tips, by Simo Ahava, is available for your help.
Also please note, that as you have mentioned, that the first hit shows 0 second time on page, you might need to check, whether the first visited page is sent twice. E.g. sending a hit on the traditional page load event, and sending a hit for the same page as a virtual page view.
I have come up with a solution to this problem in a Gatsby website (a SPA), by writing the main logic in the gatsby-browser.js file, inside the onRouteUpdate function.
You can use this solution in other contexts, but please note that the code needs to run at the first load of the page and at every route change.
If you want the solution to work in browsers that do not support URLSearchParams I think you can easily find a polyfill.
Function to retrieve the parameters
// return the whole parameters only if at least one of the desired parameters exists
const retrieveParams = () => {
let storedParams;
if ('URLSearchParams' in window) {
// Browser supports URLSearchParams
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const requestedParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'gclid'];
const hasRequestedParams = requestedParams.some((param) => {
// true if it exists
return !!params.get(param);
});
if (hasRequestedParams) {
storedParams = params;
}
}
return storedParams;
}
Create the full URL
// look at existing parameters (from previous page navigations) or retrieve new ones
const storedParams = window.storedParams || retrieveParams();
let storedParamsUrl;
if (storedParams) {
// update window value
window.storedParams = storedParams;
// create the url
const urlWithoutParams = document.location.protocol + '//' + document.location.hostname + document.location.pathname;
storedParamsUrl = `${urlWithoutParams}?${storedParams}`;
}
Send the value to analytics (using gtag)
// gtag
gtag('config', 'YOUR_GA_ID', {
// ... other parameters
page_location: storedParamsUrl ?? window.location.href
});
or
gtag('event', 'page_view', {
// ... other parameters
page_location: storedParamsUrl ?? window.location.href,
send_to: 'YOUR_GA_ID'
})

Google Form make POST request on submission

Is there a way to call an external API Endpoint on Google Forms every time the form is filled out?
First:
you'll need to set up your App script project and you'll do that by:
Visit script.google.com to open the script editor. (You'll need to be signed in to your Google account.) If this is the first time you've been to script.google.com, you'll be redirected to a page that introduces Apps Script. Click Start Scripting to proceed to the script editor.
A welcome screen will ask what kind of script you want to create. Click Blank Project or Close.
Delete any code in the script editor and paste in the code below.
This video and the doc will help
Second
you'll need to create an installable trigger, you can add it to the form directly or to the spreadsheet that has the responses
function setUpTrigger(){
ScriptApp.newTrigger('sendPostRequest') /* this has the name of the function that will have the post request */
.forForm('formkey') // you'll find it in the url
.onFormSubmit()
.create();
}
Check the doc
Third
create the sendPostRequest function and add the UrlFetchApp to it
function sendPostRequest(e){
// Make a POST request with form data.
var resumeBlob = Utilities.newBlob('Hire me!', 'text/plain', 'resume.txt');
var formData = {
'name': 'Bob Smith',
'email': 'bob#example.com',
'resume': resumeBlob
};
// Because payload is a JavaScript object, it is interpreted as
// as form data. (No need to specify contentType; it automatically
// defaults to either 'application/x-www-form-urlencoded'
// or 'multipart/form-data')
var options = {
'method' : 'post',
'payload' : formData
};
UrlFetchApp.fetch('https://httpbin.org/post', options);
}
Check the doc
Try something like this in your app script:
var POST_URL = "enter your webhook URL";
function onSubmit(e) {
var form = FormApp.getActiveForm();
var allResponses = form.getResponses();
var latestResponse = allResponses[allResponses.length - 1];
var response = latestResponse.getItemResponses();
var payload = {};
for (var i = 0; i < response.length; i++) {
var question = response[i].getItem().getTitle();
var answer = response[i].getResponse();
payload[question] = answer;
}
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload)
};
UrlFetchApp.fetch(POST_URL, options);
};
Be sure to replace the POST_URL variable with your webhook, you can use requestcatcher.com to test this out.
Add a trigger to the script by clicking "Triggers" in the side menu
Open the menu (top-right dots)
Click in Script Editor
Paste the above code (changing the POST_URL)
Click in the clock icon (left-side menu), which means Triggers.
On the right-bottom corner, click in the blue Add trigger button (a form will show as the image below).
It should show onSubmit under Choose which function to run.
Make sure Select event type is set as On form submit.
Click Save button.
After that, submit your form and watch for the request to come in.
This is pretty straightforward with Google Scripts.
Just create a new project bound to your spreadsheet and create 2 elements:
A function that will contain all relevant data to make the call (see docs for making a HTTP request from Google Apps Script)
A trigger linked to the spreadsheet. You can set it to run each time an edit occurs or form is submitted
Voilà, your sheet will call whatever endpoint you wish on submission. You can even parse the spreadsheet to return that data to your endpoint

Trigger a button click from a URL

We need to scrape VEEC Website for the total number once a week.
As an example, for the week of 17/10/2016 - 23/10/2016 the URL returns the number Total 167,356 when the search button is clicked. We want this number to be stored in our database.
I'm using coldfusion to generate the weekly dates as params and have been passing them like the above URL. But I'm unable to find a query param so that the "Search" button click event is triggered.
I've tried like this & this but nothing seems to be working.
Any pointers?
It seems like for every form submission, a CRSF token is added, which prevents malicious activity. To make matters worse for you, the CRSF token is changed for each form submission, not just for each user, which makes it virtually impossible to circumvent.
When I make a CFHTTP POST request to this form, I get HTML FileContent back, but there is no DB data within the results table cell placeholders. It seems to me that the form owner allows form submission from an HTTP request, but if the CRSF token cannot be validated, no DB data is returned.
It maybe worth asking the website owner, if there is any kind of REST API, that you can hook into...
If you want to use a headless browser PhantomJS (https://en.wikipedia.org/wiki/PhantomJS) for this, here is a script that will save the total to a text file.
At command prompt, after you install PhantomJS, run phantomjs.exe main.js.
main.js
"use strict";
var firstLoad = true;
var url = 'https://www.veet.vic.gov.au/Public/PublicRegister/Search.aspx?CreatedFrom=17%2F10%2F2016&CreatedTo=23%2F10%2F2016';
var page = require("webpage").create();
page.viewportSize = {
width: 1280,
height: 800
};
page.onCallback = function (result) {
var fs = require('fs');
fs.write('veet.txt', result, 'w');
};
page.onLoadStarted = function () {
console.log("page.onLoadStarted, firstLoad", firstLoad);
};
page.onLoadFinished = function () {
console.log("page.onLoadFinished, firstLoad", firstLoad);
if (firstLoad) {
firstLoad = false;
page.evaluate(function () {
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, true);
document.querySelectorAll(".dx-vam")[3].dispatchEvent(event);
});
} else {
page.evaluate(function () {
var element = document.querySelectorAll('.dxgv')[130];
window.callPhantom(element.textContent);
});
setTimeout(function () {
page.render('veet.png');
phantom.exit();
}, 3000);
}
};
page.open(url);
The script is not perfect, you can work on it if you're interested, but as is it will save the total to a file veet.txt and also save a screenshot veet.png.

How can you get a url for every piece of data?

I am building an IM platform based on Firebase and I would like that every user got an address that directed them to the chat room.
http://chatt3r.sitecloud.cytanium.com/
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://cdn.firebase.com/v0/firebase.js"></script>
<script>
var hash; // global incase needed elsewhere
$(function() {
hash = window.location.hash.replace("#", "");;
myRootRef = new Firebase("http://xxxx.firebaseio.com");
var postingRef;
if (hash) {
// user has been given a hashed URL from their friend, so sets firebase root to that place
console.log(hash);
postingRef = new Firebase("http://xxxx.firebaseio.com/chatrooms/" + hash);
} else {
// there is no hash, so the user is looking to share a URL for a new room
console.log("no hash");
postingRef = new Firebase("http://xxxx.firebaseio.com/chatrooms/");
// push for a unique ID for the chatroom
postingRef = postingRef.push();
// exploit this unique ID to provide a unique ID host for you app
window.location.hash = postingRef.toString().slice(34);
}
// listener
// will pull all old messages up once bound
postingRef.on("child_added", function(data) {
console.log(data.val().user + ": " + data.val().message);
});
// later:
postingRef.push({"message": "etc", "user": "Jimmybobjimbobjimbobjim"});
});
</script>
</head>
That's working for me locally. You need to change xxxx to whatever URL yours is at, and add on however many characters that first part is at the .slice() bit.
Hashes.
If I understand your question correctly, you want to be able to share a URL that will allow anyone who clicks on the URL to log onto the same chatroom.
I did this for a Firebase application I made once. The first thing you need to be doing is using the .push() method. Push the room to Firebase, then use the toString() method to get the URL of the push. Some quick JS string manipulation - window.location.hash = pushRef.toString().slice(x) - where 'x' is whatever place you want to snip the URL at. window.location.hash will set the hash for you. Add the hash to the sharing URL, and then for the next step:
You will want a hash listener to check if there is already a hash when you open the page, so $(window).bind('hashchange', function() {UpdateHash()}) goes into a doc.ready function, and then...
function UpdateHash() {
global_windowHash = window.hash.replace("#", "");
// to assign the hash to a global hash variable
if (global_windowHash) {
// if there was already a hash
chatRef = new Firebase("[Your Firebase URL here]" + "/chatrooms/" + global_windowHash);
// chatRef is the global that you append the chat data to, and listen from.
} else {
// there wasn't a hash, so you can auto-create a new one here, in which case:
chatRef = new Firebase("[Your Firebase URL here]" + "/chatrooms/");
chatRef = chatRef.push();
window.location.hash = chatRef.toString().slice(x);
}
}
I hope that helps (and works :P ). If there are any questions or problems, then just ask!

Resources