<commit_before>/**
\file boomerang.js
boomerang measures various performance characteristics of your user's browsing
experience and beacons it back to your server.

\details
To use this you'll need a web site, lots of users and the ability to do
something with the data you collect.  How you collect the data is up to
you, but we have a few ideas.

Copyright (c) 2010 Yahoo! Inc. All rights reserved.
Code licensed under the BSD License.  See the file LICENSE.txt
for the full license text.
*/

// beaconing section
// the two parameters are the window and document objects
(function(w, d) {

// don't allow this code to be included twice
if(typeof w.BOOMR !== "undefined" && typeof w.BOOMR.version !== "undefined") {
	return;
}

// Short namespace because I don't want to keep typing BOOMERANG
if(typeof w.BOOMR === "undefined" || !w.BOOMR) {
	BOOMR = {};
}

BOOMR.version = "1.0";


// bmr is a private object not reachable from outside the impl
// users can set properties by passing in to the init() method
var bmr = {
	// properties
	beacon_url: "",
	site_domain: d.location.hostname.replace(/.*?([^.]+\.[^.]+)\.?$/, '$1').toLowerCase(),	// strip out everything except last two parts of hostname.
	user_ip: '',		//! User's ip address determined on the server

	events: {
		"script_load": [],
		"page_ready": [],
		"page_unload": [],
		"before_beacon": []
	},

	vars: {},


	// We fire events with a setTimeout so that they run asynchronously
	// and don't block each other.  This is most useful on a multi-threaded
	// JS engine.  Don't use this for onbeforeunload though
	fireEvent: function(e, i, data) {
		setTimeout(function() {
			bmr.events[e][i][0].call(bmr.events[e][i][2], data, bmr.events[e][i][1]);
		}, 10);
	}
};

// O is the boomerang object.  We merge it into BOOMR later.  Do it this way so that plugins
// may be defined before including the script.

var O = {
	// Utility functions
	utils: {
		getCookie: function(name) {
			name = ' ' + name + '=';
		
			var i, cookies;
			cookies = ' ' + d.cookie + ';';
			if ( (i=cookies.indexOf(name)) >= 0 ) {
				i += name.length;
				cookies = cookies.substring(i, cookies.indexOf(';', i));
				return cookies;
			}
		
			return "";
		},
		
		setCookie: function(name, subcookies, max_age, path, domain, sec) {
			var value = "",
			    k, nameval, c,
			    exp = "";

			if(!name) {
				return;
			}
		
			for(k in subcookies) {
				if(subcookies.hasOwnProperty(k)) {
					value += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(subcookies[k]);
				}
			}
			value = value.replace(/^&/, '');
		
			if(max_age) {
				exp = new Date();
				exp.setTime(exp.getTime() + max_age*1000);
				exp = exp.toGMTString();
			}
		
			nameval = name + '=' + value;
			c = nameval +
				((max_age) ? "; expires=" + exp : "" ) +
				((path) ? "; path=" + path : "") +
				((typeof domain !== "undefined") ? "; domain=" + (domain === null ? bmr.site_domain : domain) : "") +
				((sec) ? "; secure" : "");
		
			if ( nameval.length < 4000 ) {
				d.cookie = c;
				return ( value === this.getCookie(name) );    // confirm it was set (could be blocked by user's settings, etc.)
			}
		
			return false;
		},
		
		getSubCookies: function(cookie) {
			var cookies_a,
			    i, l, kv,
			    cookies={};

			if(!cookie) {
				return null;
			}
		
			cookies_a = cookie.split('&');
		
			if(cookies_a.length === 0) {
				return null;
			}
		
			for(i=0, l=cookies_a.length; i<l; i++) {
				kv = cookies_a[i].split('=');
				kv.push("");	// just in case there's no value
				cookies[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
			}
		
			return cookies;
		},
		
		removeCookie: function(name) {
			return this.setCookie(name, {}, 0, "/", null);
		},
		
		addListener: function(el, sType, fn, capture) {
			if(el.addEventListener) {
				el.addEventListener(sType, fn, (capture));
			}
			else if(el.attachEvent) {
				el.attachEvent("on" + sType, fn);
			}
		},

		pluginConfig: function(o, config, plugin_name, properties) {
			var i, props=0;

			if(typeof config === "undefined" || typeof config[plugin_name] === "undefined") {
				return false;
			}

			for(i=0; i<properties.length; i++) {
				if(typeof(config[plugin_name][properties[i]]) !== "undefined") {
					o[properties[i]] = config[plugin_name][properties[i]];
					props++;
				}
			}

			return (props>0);
		}
	},

	init: function(config) {
		var i, k,
		    properties = ["beacon_url", "site_domain", "user_ip"],
		    that=this;
	
		if(!config) {
			config = {};
		}

		for(i=0; i<properties.length; i++) {
			if(typeof config[properties[i]] !== "undefined") {
				bmr[properties[i]] = config[properties[i]];
			}
		}

		if(typeof config.log  !== "undefined") {
			this.log = config.log;
		}
	
		for(k in this.plugins) {
			if(this.plugins.hasOwnProperty(k) && typeof this.plugins[k].init === "function") {
				this.plugins[k].init(config);
			}
		}
	
		// The developer can override onload by setting autorun to false
		if(typeof config.autorun === "undefined" || config.autorun !== false) {
			this.utils.addListener(w, "load", function() { that.fireEvent("page_ready"); that=null; });
		}
	
		return this;
	},

	// The page dev calls this method when they determine the page is usable.  Only call this if autorun is explicitly set to false
	page_ready: function() {
		this.fireEvent("page_ready");
		return this;
	},

	subscribe: function(e, fn, cb_data, cb_scope) {
		if(e === 'page_unload') {
			this.utils.addListener(w, "unload", function() { fn.call(cb_scope, null, cb_data); });
		}
		else if(bmr.events.hasOwnProperty(e)) {
			bmr.events[e].push([ fn, cb_data || {}, cb_scope || null ]);
		}

		return this;
	},

	fireEvent: function(e, data) {
		var i;
		if(bmr.events.hasOwnProperty(e)) {
			for(i=0; i<bmr.events[e].length; i++) {
				bmr.fireEvent(e, i, data);
			}
		}
	},

	addVar: function(name, value) {
		if(typeof name === "string") {
			bmr.vars[name] = value;
		}
		else if(typeof name === "object") {
			var o = name, k;
			for(k in o) {
				if(o.hasOwnProperty(k)) {
					bmr.vars[k] = o[k];
				}
			}
		}
		return this;
	},

	removeVar: function(name) {
		if(bmr.hasOwnProperty(name)) {
			delete bmr[name];
		}

		return this;
	},

	sendBeacon: function() {
		var k, url, img;
	
		// At this point someone is ready to send the beacon.  We send
		// the beacon only if all plugins have finished doing what they
		// wanted to do
		for(k in this.plugins) {
			if(this.plugins.hasOwnProperty(k)) {
				if(!this.plugins[k].is_complete()) {
					return;
				}
			}
		}
	
		// If we reach here, all plugins have completed
		this.fireEvent("before_beacon", bmr.vars);

		// Don't send a beacon if no beacon_url has been set
		if(!this.beacon_url) {
			return this;
		}

		url = this.beacon_url + '?v=' + encodeURIComponent(BOOMR.version);
		for(k in bmr.vars) {
			if(bmr.vars.hasOwnProperty(k)) {
				url += "&" + encodeURIComponent(k) + "=" + encodeURIComponent(bmr.vars[k]);
			}
		}
	
		img = new Image();
		img.src=url;

		return this;
	},

	log: function(m,l,s) {} // create a logger - we'll try to use the YUI logger if it exists or firebug if it exists, or just fall back to nothing.
};

if(typeof w.YAHOO !== "undefined" && typeof w.YAHOO.log !== "undefined") {
	O.log = w.YAHOO.log;
}
else if(typeof w.Y !== "undefined" && typeof w.Y.log !== "undefined") {
	O.log = w.Y.log;
}
else if(typeof console !== "undefined" && typeof console.log !== "undefined") {
	O.log = function(m,l,s) { console.log(s + ": [" + l + "] ", m); };
}


for(var k in O) {
	if(O.hasOwnProperty(k)) {
		BOOMR[k] = O[k];
	}
}

if(typeof BOOMR.plugins === "undefined" || !BOOMR.plugins) {
	BOOMR.plugins = {};
}

}(this, this.document));

// end of boomerang beaconing section
// Now we start built in plugins.  I might move them into separate source files at some point


// This is the RT plugin
// the two parameters are the window and document objects
(function(w, d) {

if(!BOOMR) {
	BOOMR = {};
}
if(!BOOMR.plugins) {
	BOOMR.plugins = {};
}

// private object
var rt = {
	complete: false,//! Set when this plugin has completed

	timers: {},	//! Custom timers that the developer can use
			// Format for each timer is { start: XXX, end: YYY, delta: YYY-XXX }
	cookie: 'BRT',	//! Name of the cookie that stores the start time and referrer
	cookie_exp:600	//! Cookie expiry in seconds
};

BOOMR.plugins.RT = {
	// Methods

	init: function(config) {
		rt.complete = false;
		rt.timers = {};

		BOOMR.utils.pluginConfig(rt, config, "RT", ["cookie", "cookie_exp"]);

		return this;
	},

	// The start method is fired on page unload
	start: function() {
		var t_start = new Date().getTime();

		BOOMR.utils.setCookie(rt.cookie, { s: t_start, r: d.location }, rt.cookie_exp, "/", null);

		if(new Date().getTime() - t_start > 20) {
			// It took > 20ms to set the cookie
			// Most likely user has cookie prompting turned on so t_start won't be the actual unload time
			// We bail at this point since we can't reliably tell t_done
			BOOMR.utils.removeCookie(rt.cookie);

			// at some point we may want to log this info on the server side
		}

		return this;
	},

	startTimer: function(timer_name) {
		rt.timers[timer_name] = { start: new Date().getTime() };

		return this;
	},

	endTimer: function(timer_name, time_value) {
		if(typeof rt.timers[timer_name] === "undefined") {
			rt.timers[timer_name] = {};
		}
		rt.timers[timer_name].end = (typeof time_value === "number" ? time_value : new Date().getTime());

		return this;
	},

	setTimer: function(timer_name, time_delta) {
		rt.timers[timer_name] = { delta: time_delta };

		return this;
	},

	error: function(msg) {
		BOOMR.log(msg, "error", "boomerang.rt");
		return this;
	},

	// Called when the page has reached a "usable" state.  This may be when the onload event fires,
	// or it could be at some other moment during/after page load when the page is usable by the user
	done: function() {
		var t_start, u, r, r2, t_other=[],
		    subcookies,
		    basic_timers = { t_done: 1, t_rtpage: 1, t_resp: 1 },
		    ntimers = 0, timer;


		if(rt.complete) {
			return this;
		}

		this.endTimer("t_done");

		// A beacon may be fired automatically on page load or if the page dev fires it
		// manually with their own timers.  It may not always contain a referrer (eg: XHR calls)
		// We set default values for these cases

		u = d.location.href.replace(/#.*/, '');
		r = r2 = d.referrer.replace(/#.*/, '');

		subcookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(rt.cookie));
		BOOMR.utils.removeCookie(rt.cookie);

		if(subcookies !== null && typeof subcookies.s !== "undefined" && typeof subcookies.r !== "undefined") {
			t_start = parseInt(subcookies.s, 10);
			r = subcookies.r;
		}

		for(timer in rt.timers) {
			if(!rt.timers.hasOwnProperty(timer)) {
				continue;
			}

			if(typeof rt.timers[timer].delta !== "number") {
				rt.timers[timer].delta = rt.timers[timer].end - ( typeof rt.timers[timer].start === "number" ? rt.timers[timer].start : t_start );
			}

			if(isNaN(rt.timers[timer].delta)) {
				continue;
			}

			if(basic_timers[timer]) {
				BOOMR.addVar(timer, rt.timers[timer].delta);
			}
			else {
				t_other.push(encodeURIComponent(timer) + "|" + encodeURIComponent(rt.timers[timer].delta));
			}
			ntimers++;
		}

		// make sure an old t_other doesn't stick around
		BOOMR.removeVar('t_other');

		// At this point we decide whether the beacon should be sent or not
		if(ntimers === 0) {
			return this.error("no timers");
		}<commit_msg>Only beacon if the document.referrer matches the URL of the page that set the tstart cookie. This protects us from situations where the user went to page A on your site, then some 3rd party site and then came back to page B.<commit_after>/**
\file boomerang.js
boomerang measures various performance characteristics of your user's browsing
experience and beacons it back to your server.

\details
To use this you'll need a web site, lots of users and the ability to do
something with the data you collect.  How you collect the data is up to
you, but we have a few ideas.

Copyright (c) 2010 Yahoo! Inc. All rights reserved.
Code licensed under the BSD License.  See the file LICENSE.txt
for the full license text.
*/

// beaconing section
// the two parameters are the window and document objects
(function(w, d) {

// don't allow this code to be included twice
if(typeof w.BOOMR !== "undefined" && typeof w.BOOMR.version !== "undefined") {
	return;
}

// Short namespace because I don't want to keep typing BOOMERANG
if(typeof w.BOOMR === "undefined" || !w.BOOMR) {
	BOOMR = {};
}

BOOMR.version = "1.0";


// bmr is a private object not reachable from outside the impl
// users can set properties by passing in to the init() method
var bmr = {
	// properties
	beacon_url: "",
	site_domain: d.location.hostname.replace(/.*?([^.]+\.[^.]+)\.?$/, '$1').toLowerCase(),	// strip out everything except last two parts of hostname.
	user_ip: '',		//! User's ip address determined on the server

	events: {
		"script_load": [],
		"page_ready": [],
		"page_unload": [],
		"before_beacon": []
	},

	vars: {},


	// We fire events with a setTimeout so that they run asynchronously
	// and don't block each other.  This is most useful on a multi-threaded
	// JS engine.  Don't use this for onbeforeunload though
	fireEvent: function(e, i, data) {
		setTimeout(function() {
			bmr.events[e][i][0].call(bmr.events[e][i][2], data, bmr.events[e][i][1]);
		}, 10);
	}
};

// O is the boomerang object.  We merge it into BOOMR later.  Do it this way so that plugins
// may be defined before including the script.

var O = {
	// Utility functions
	utils: {
		getCookie: function(name) {
			name = ' ' + name + '=';
		
			var i, cookies;
			cookies = ' ' + d.cookie + ';';
			if ( (i=cookies.indexOf(name)) >= 0 ) {
				i += name.length;
				cookies = cookies.substring(i, cookies.indexOf(';', i));
				return cookies;
			}
		
			return "";
		},
		
		setCookie: function(name, subcookies, max_age, path, domain, sec) {
			var value = "",
			    k, nameval, c,
			    exp = "";

			if(!name) {
				return;
			}
		
			for(k in subcookies) {
				if(subcookies.hasOwnProperty(k)) {
					value += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(subcookies[k]);
				}
			}
			value = value.replace(/^&/, '');
		
			if(max_age) {
				exp = new Date();
				exp.setTime(exp.getTime() + max_age*1000);
				exp = exp.toGMTString();
			}
		
			nameval = name + '=' + value;
			c = nameval +
				((max_age) ? "; expires=" + exp : "" ) +
				((path) ? "; path=" + path : "") +
				((typeof domain !== "undefined") ? "; domain=" + (domain === null ? bmr.site_domain : domain) : "") +
				((sec) ? "; secure" : "");
		
			if ( nameval.length < 4000 ) {
				d.cookie = c;
				return ( value === this.getCookie(name) );    // confirm it was set (could be blocked by user's settings, etc.)
			}
		
			return false;
		},
		
		getSubCookies: function(cookie) {
			var cookies_a,
			    i, l, kv,
			    cookies={};

			if(!cookie) {
				return null;
			}
		
			cookies_a = cookie.split('&');
		
			if(cookies_a.length === 0) {
				return null;
			}
		
			for(i=0, l=cookies_a.length; i<l; i++) {
				kv = cookies_a[i].split('=');
				kv.push("");	// just in case there's no value
				cookies[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
			}
		
			return cookies;
		},
		
		removeCookie: function(name) {
			return this.setCookie(name, {}, 0, "/", null);
		},
		
		addListener: function(el, sType, fn, capture) {
			if(el.addEventListener) {
				el.addEventListener(sType, fn, (capture));
			}
			else if(el.attachEvent) {
				el.attachEvent("on" + sType, fn);
			}
		},

		pluginConfig: function(o, config, plugin_name, properties) {
			var i, props=0;

			if(typeof config === "undefined" || typeof config[plugin_name] === "undefined") {
				return false;
			}

			for(i=0; i<properties.length; i++) {
				if(typeof(config[plugin_name][properties[i]]) !== "undefined") {
					o[properties[i]] = config[plugin_name][properties[i]];
					props++;
				}
			}

			return (props>0);
		}
	},

	init: function(config) {
		var i, k,
		    properties = ["beacon_url", "site_domain", "user_ip"],
		    that=this;
	
		if(!config) {
			config = {};
		}

		for(i=0; i<properties.length; i++) {
			if(typeof config[properties[i]] !== "undefined") {
				bmr[properties[i]] = config[properties[i]];
			}
		}

		if(typeof config.log  !== "undefined") {
			this.log = config.log;
		}
	
		for(k in this.plugins) {
			if(this.plugins.hasOwnProperty(k) && typeof this.plugins[k].init === "function") {
				this.plugins[k].init(config);
			}
		}
	
		// The developer can override onload by setting autorun to false
		if(typeof config.autorun === "undefined" || config.autorun !== false) {
			this.utils.addListener(w, "load", function() { that.fireEvent("page_ready"); that=null; });
		}
	
		return this;
	},

	// The page dev calls this method when they determine the page is usable.  Only call this if autorun is explicitly set to false
	page_ready: function() {
		this.fireEvent("page_ready");
		return this;
	},

	subscribe: function(e, fn, cb_data, cb_scope) {
		if(e === 'page_unload') {
			this.utils.addListener(w, "unload", function() { fn.call(cb_scope, null, cb_data); });
		}
		else if(bmr.events.hasOwnProperty(e)) {
			bmr.events[e].push([ fn, cb_data || {}, cb_scope || null ]);
		}

		return this;
	},

	fireEvent: function(e, data) {
		var i;
		if(bmr.events.hasOwnProperty(e)) {
			for(i=0; i<bmr.events[e].length; i++) {
				bmr.fireEvent(e, i, data);
			}
		}
	},

	addVar: function(name, value) {
		if(typeof name === "string") {
			bmr.vars[name] = value;
		}
		else if(typeof name === "object") {
			var o = name, k;
			for(k in o) {
				if(o.hasOwnProperty(k)) {
					bmr.vars[k] = o[k];
				}
			}
		}
		return this;
	},

	removeVar: function(name) {
		if(bmr.hasOwnProperty(name)) {
			delete bmr[name];
		}

		return this;
	},

	sendBeacon: function() {
		var k, url, img;
	
		// At this point someone is ready to send the beacon.  We send
		// the beacon only if all plugins have finished doing what they
		// wanted to do
		for(k in this.plugins) {
			if(this.plugins.hasOwnProperty(k)) {
				if(!this.plugins[k].is_complete()) {
					return;
				}
			}
		}
	
		// If we reach here, all plugins have completed
		this.fireEvent("before_beacon", bmr.vars);

		// Don't send a beacon if no beacon_url has been set
		if(!this.beacon_url) {
			return this;
		}

		url = this.beacon_url + '?v=' + encodeURIComponent(BOOMR.version);
		for(k in bmr.vars) {
			if(bmr.vars.hasOwnProperty(k)) {
				url += "&" + encodeURIComponent(k) + "=" + encodeURIComponent(bmr.vars[k]);
			}
		}
	
		img = new Image();
		img.src=url;

		return this;
	},

	log: function(m,l,s) {} // create a logger - we'll try to use the YUI logger if it exists or firebug if it exists, or just fall back to nothing.
};

if(typeof w.YAHOO !== "undefined" && typeof w.YAHOO.log !== "undefined") {
	O.log = w.YAHOO.log;
}
else if(typeof w.Y !== "undefined" && typeof w.Y.log !== "undefined") {
	O.log = w.Y.log;
}
else if(typeof console !== "undefined" && typeof console.log !== "undefined") {
	O.log = function(m,l,s) { console.log(s + ": [" + l + "] ", m); };
}


for(var k in O) {
	if(O.hasOwnProperty(k)) {
		BOOMR[k] = O[k];
	}
}

if(typeof BOOMR.plugins === "undefined" || !BOOMR.plugins) {
	BOOMR.plugins = {};
}

}(this, this.document));

// end of boomerang beaconing section
// Now we start built in plugins.  I might move them into separate source files at some point


// This is the RT plugin
// the two parameters are the window and document objects
(function(w, d) {

if(!BOOMR) {
	BOOMR = {};
}
if(!BOOMR.plugins) {
	BOOMR.plugins = {};
}

// private object
var rt = {
	complete: false,	//! Set when this plugin has completed

	timers: {},		//! Custom timers that the developer can use
				// Format for each timer is { start: XXX, end: YYY, delta: YYY-XXX }
	cookie: 'BRT',		//! Name of the cookie that stores the start time and referrer
	cookie_exp:600,		//! Cookie expiry in seconds
	strict_referrer: true	//! By default, don't beacon if referrers don't match. If set to false, beacon both referrer values and let the back end decide
};

BOOMR.plugins.RT = {
	// Methods

	init: function(config) {
		rt.complete = false;
		rt.timers = {};

		BOOMR.utils.pluginConfig(rt, config, "RT", ["cookie", "cookie_exp", "strict_referrer"]);

		return this;
	},

	// The start method is fired on page unload
	start: function() {
		var t_start = new Date().getTime();

		BOOMR.utils.setCookie(rt.cookie, { s: t_start, r: d.location }, rt.cookie_exp, "/", null);

		if(new Date().getTime() - t_start > 20) {
			// It took > 20ms to set the cookie
			// Most likely user has cookie prompting turned on so t_start won't be the actual unload time
			// We bail at this point since we can't reliably tell t_done
			BOOMR.utils.removeCookie(rt.cookie);

			// at some point we may want to log this info on the server side
		}

		return this;
	},

	startTimer: function(timer_name) {
		rt.timers[timer_name] = { start: new Date().getTime() };

		return this;
	},

	endTimer: function(timer_name, time_value) {
		if(typeof rt.timers[timer_name] === "undefined") {
			rt.timers[timer_name] = {};
		}
		rt.timers[timer_name].end = (typeof time_value === "number" ? time_value : new Date().getTime());

		return this;
	},

	setTimer: function(timer_name, time_delta) {
		rt.timers[timer_name] = { delta: time_delta };

		return this;
	},

	error: function(msg) {
		BOOMR.log(msg, "error", "boomerang.rt");
		return this;
	},

	// Called when the page has reached a "usable" state.  This may be when the onload event fires,
	// or it could be at some other moment during/after page load when the page is usable by the user
	done: function() {
		var t_start, u, r, r2, t_other=[],
		    subcookies,
		    basic_timers = { t_done: 1, t_rtpage: 1, t_resp: 1 },
		    ntimers = 0, timer;


		if(rt.complete) {
			return this;
		}

		this.endTimer("t_done");

		// A beacon may be fired automatically on page load or if the page dev fires it
		// manually with their own timers.  It may not always contain a referrer (eg: XHR calls)
		// We set default values for these cases

		u = d.location.href.replace(/#.*/, '');
		r = r2 = d.referrer.replace(/#.*/, '');

		subcookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(rt.cookie));
		BOOMR.utils.removeCookie(rt.cookie);

		if(subcookies !== null && typeof subcookies.s !== "undefined" && typeof subcookies.r !== "undefined") {
			r = subcookies.r;
			if(!rt.strict_referrer || r === r2) { 
				t_start = parseInt(subcookies.s, 10);
			}
		}

		for(timer in rt.timers) {
			if(!rt.timers.hasOwnProperty(timer)) {
				continue;
			}

			if(typeof rt.timers[timer].delta !== "number") {
				rt.timers[timer].delta = rt.timers[timer].end - ( typeof rt.timers[timer].start === "number" ? rt.timers[timer].start : t_start );
			}

			if(isNaN(rt.timers[timer].delta)) {
				continue;
			}

			if(basic_timers[timer]) {
				BOOMR.addVar(timer, rt.timers[timer].delta);
			}
			else {
				t_other.push(encodeURIComponent(timer) + "|" + encodeURIComponent(rt.timers[timer].delta));
			}
			ntimers++;
		}

		// make sure an old t_other doesn't stick around
		BOOMR.removeVar('t_other');

		// At this point we decide whether the beacon should be sent or not
		if(ntimers === 0) {
			return this.error("no timers");
		}
{'fraction_non_alphanumeric': 0.1593564123411357, 'fraction_numerical': 0.00410800633317643, 'mean_word_length': 3.123875066172578, 'pattern_counts': {'":': 10, '<': 13, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '47657', 'n_tokens_mistral': 9342, 'n_tokens_neox': 8193, 'n_words': 2284}