diff --git a/boomerang.js b/boomerang.js index a4cb5b17c..7f0afdcec 100644 --- a/boomerang.js +++ b/boomerang.js @@ -113,6 +113,10 @@ BOOMR.window = w; impl = { // properties beacon_url: "", + // beacon request method, either GET, POST or AUTO. AUTO will check the + // request size then use GET if the request URL is less than 2000 chars + // otherwise it will fall back to a POST request. + beacon_type: "AUTO", // strip out everything except last two parts of hostname. // This doesn't work well for domains that end with a country tld, // but we allow the developer to override site_domain for that. @@ -358,12 +362,69 @@ boomr = { } else { el.detachEvent('on' + type, fn); } + }, + + pushVars: function (arr, vars, prefix) { + var k, i, n=0; + + for(k in vars) { + if(vars.hasOwnProperty(k)) { + if(Object.prototype.toString.call(vars[k]) === "[object Array]") { + for(i = 0; i < vars[k].length; ++i) { + n += BOOMR.utils.pushVars(arr, vars[k][i], k + "[" + i + "]"); + } + } else { + ++n; + arr.push( + encodeURIComponent(prefix ? (prefix + "[" + k + "]") : k) + + "=" + + (vars[k]===undefined || vars[k]===null ? '' : encodeURIComponent(vars[k])) + ); + } + } + } + + return n; + }, + + postData: function (urlenc) { + var iframe = document.createElement("iframe"), + form = document.createElement("form"), + input = document.createElement("input"); + + iframe.name = "boomerang_post"; + iframe.style.display = form.style.display = "none"; + + form.method = "POST"; + form.action = impl.beacon_url; + form.target = iframe.name; + + input.name = "data"; + + if (window.JSON) { + form.enctype = "text/plain"; + input.value = JSON.stringify(impl.vars); + } else { + form.enctype = "application/x-www-form-urlencoded"; + input.value = urlenc; + } + + document.body.appendChild(iframe); + form.appendChild(input); + document.body.appendChild(form); + + BOOMR.utils.addListener(iframe, "load", function() { + document.body.removeChild(form); + document.body.removeChild(iframe); + }); + + form.submit(); } }, init: function(config) { var i, k, - properties = ["beacon_url", "site_domain", "user_ip", "strip_query_string"]; + properties = ["beacon_url", "beacon_type", "site_domain", "user_ip", "strip_query_string"]; BOOMR_check_doc_domain(); @@ -605,7 +666,7 @@ boomr = { }, sendBeacon: function() { - var k, url, img, nparams=0; + var k, data, url, img, nparams; BOOMR.debug("Checking if we can send beacon"); @@ -642,33 +703,32 @@ boomr = { return this; } - // if there are already url parameters in the beacon url, - // change the first parameter prefix for the boomerang url parameters to & + data = []; + nparams = BOOMR.utils.pushVars(data, impl.vars); - url = []; - - for(k in impl.vars) { - if(impl.vars.hasOwnProperty(k)) { - nparams++; - url.push(encodeURIComponent(k) - + "=" - + ( - impl.vars[k]===undefined || impl.vars[k]===null - ? '' - : encodeURIComponent(impl.vars[k]) - ) - ); - } + if(!nparams) { + // do not make the request if there is no data + return this; } - url = impl.beacon_url + ((impl.beacon_url.indexOf('?') > -1)?'&':'?') + url.join('&'); + data = data.join('&'); - BOOMR.debug("Sending url: " + url.replace(/&/g, "\n\t")); + if(impl.beacon_type === 'POST') { + BOOMR.utils.postData(data); + } else { + // if there are already url parameters in the beacon url, + // change the first parameter prefix for the boomerang url parameters to & + url = impl.beacon_url + ((impl.beacon_url.indexOf('?') > -1)?'&':'?') + data; - // only send beacon if we actually have something to beacon back - if(nparams) { - img = new Image(); - img.src=url; + // using 2000 here as a de facto maximum URL length based on: + // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + if(url.length > 2000 && impl.beacon_type === "AUTO") { + BOOMR.utils.postData(data); + } else { + BOOMR.debug("Sending url: " + url.replace(/&/g, "\n\t")); + img = new Image(); + img.src=url; + } } return this; diff --git a/doc/api/BOOMR.html b/doc/api/BOOMR.html index f35b16fc3..9f2634a68 100644 --- a/doc/api/BOOMR.html +++ b/doc/api/BOOMR.html @@ -73,6 +73,20 @@
'GET'
, 'POST'
or 'AUTO'
(the default).
+The AUTO setting will make a GET request unless the combined beacon URL plus query string
+exceeds 2000 characters, in which case it will issue a POST. In modern browsers, POST requests
+will encode the data as JSON, with a Content-Type
of text/plain
.
+In browsers where JSON.stringify
is not available, the data will be
+application/x-www-form-urlencoded
. To ensure reliable POST behaviour in old browsers, a
+hidden form submission
+is used, meaning that the request body will look like data={"foo":"bar","baz":"qux"}
+or data=foo%3Dbar%26baz%3Dqux
, depending on the content type.
+
+The Resource Timing plugin collects metrics
+from modern user agents that support the W3C
+Resource Timing specification.
+The Resource Timing API is encapsulated
+within the BOOMR.plugins.ResourceTiming
namespace.
+
+Note that the Resource Timing plugin isn't included by default in boomerang.js. +See Howto #9 +for details on how to include the plugin in your boomerang deployment. +
+ +
+Called by BOOMR.init()
+to configure the Resource Timing plugin.
+The Resource Timing plugin doesn't require any configuration parameters,
+since it simply reads values from
+the browser's window.performance
object (if available)
+and adds them to the beacon request data.
+
+a reference to the BOOMR.plugins.ResourceTiming
object.
+
+If the executing user agent +doesn't implement the Resource Timing specification, +the plugin won't add any data to the beacon. +
++Called by BOOMR.sendBeacon() +to determine if the Resource Timing plugin has finished. +
+true
if the plugin has completed.false
if the plugin has not completed.
+The ResourceTiming plugin adds an array named restiming
to the beacon data,
+items in the array representing resources on the page in the order that they were loaded.
+Each item contains properties that correspond to attributes from the
+PerformanceResourceTiming interface.
+Note that some of the metrics are restricted and will not be provided cross-origin
+unless the Timing-Allow-Origin header permits.
+
restiming array item property |
+ Resource Timing attribute | +
---|---|
rt_name | name |
rt_st | startTime |
rt_dur | duration |
rt_red_st | redirectStart |
rt_red_end | redirectEnd |
rt_fet_st | fetchStart |
rt_dns_st | domainLookupStart |
rt_dns_end | domainLookupEnd |
rt_con_st | connectStart |
rt_con_end | connectEnd |
rt_scon_st | secureConnectionStart |
rt_req_st | requestStart |
rt_res_st | responseStart |
rt_res_end | responseEnd |
+The latest code and docs are available on github.com/lognormal/boomerang +
+ + + diff --git a/plugins/restiming.js b/plugins/restiming.js new file mode 100644 index 000000000..135c59702 --- /dev/null +++ b/plugins/restiming.js @@ -0,0 +1,75 @@ +/** +\file restiming.js +Plugin to collect metrics from the W3C Resource Timing API. +For more information about Resource Timing, +see: http://www.w3.org/TR/resource-timing/ +*/ + +(function() { + +BOOMR = BOOMR || {}; +BOOMR.plugins = BOOMR.plugins || {}; + +var restricted = { + redirectStart: "rt_red_st", + redirectEnd: "rt_red_end", + domainLookupStart: "rt_dns_st", + domainLookupEnd: "rt_dns_end", + connectStart: "rt_con_st", + connectEnd: "rt_con_end", + secureConnectionStart: "rt_scon_st", + requestStart: "rt_req_st", + responseStart: "rt_res_st" +}, + +impl = { + complete: false, + done: function() { + var p = BOOMR.window.performance, r, data, i, k; + if(impl.complete) { + return; + } + BOOMR.removeVar("restiming"); + if(p && typeof p.getEntriesByType === "function") { + r = p.getEntriesByType("resource"); + if(r) { + BOOMR.info("Client supports Resource Timing API", "restiming"); + data = { + restiming: new Array(r.length) + }; + for(i = 0; i < r.length; ++i) { + data.restiming[i] = { + rt_name: r[i].name, + // reinstate this if entryType is ever something other than "resource" + //rt_type: r[i].entryType, + rt_st: r[i].startTime, + rt_dur: r[i].duration, + rt_fet_st: r[i].fetchStart, + rt_res_end: r[i].responseEnd + }; + for(k in restricted) { + if(restricted.hasOwnProperty(k) && r[i][k] > 0) { + data.restiming[i][restricted[k]] = r[i][k]; + } + } + } + BOOMR.addVar(data); + } + } + this.complete = true; + BOOMR.sendBeacon(); + } +}; + +BOOMR.plugins.ResourceTiming = { + init: function() { + BOOMR.subscribe("page_ready", impl.done, null, impl); + return this; + }, + is_complete: function() { + return impl.complete; + } +}; + +}()); +