FastJSLogger

Intercepts console.log calls to display log messages in a div floating on the page. Tries to be a replacement for normal browser Error Consoles (which can be a little slow to open).

// ==UserScript==
// @name           FastJSLogger
// @namespace      FastJSLogger
// @description    Intercepts console.log calls to display log messages in a div floating on the page.  Tries to be a replacement for normal browser Error Consoles (which can be a little slow to open).
// @include        *
// But this script appears to break Facebook somewhat
// @exclude        *facebook.com/*
// @version        1.2.8
// @grant          none
// ==/UserScript==


(function(){

	// You may configure FastJSLogger before loading it, by putting options in
	// window.FJSL = { startHidden: false, displaySearchFilter: true };

	// var FJSL = {};
	if (!window.FJSL) {
		window.FJSL = {};
	}
	var FJSL = window.FJSL;

	// @concern Don't double-load
	// I haven't actually had any problems from double loading, but it seems silly to have two loggers.
	if (FJSL.loaded) {
		console.log("FJSL refusing to load a second time.");
		return;
	}
	FJSL.loaded = true;

	FJSL.defaults = {
		autoHide:    true,       // hides some time after displaying, even if you are focused on it!
		startHidden: true,
		logTimeouts: false,
		logEvents:   false,           // Log any activity over the wrapped listeners.
		logCommonMouseEvents: false,  // These can be triggered a lot!
		logChangesToGlobal:   true,   // Logs any new properties which are added to window

		watchWindowForErrors: true,   // Catches and reports DOM error events, including syntax errors from scripts loaded later, and missing images.  BUG: It can hide the line number from Chrome's console - sometimes worth disabling to get that back.
		interceptTimeouts:    true,   // Wraps calls to setTimeout, so any errors thrown may be reported.
		interceptEvents:      true,   // Wraps any listeners registered later, so errors can be caught and reported.

		displaySearchFilter:  false,  // This messes up the size of the textbox on Firefox and Konqueror but not Chrome.

		// Hack to avoid infinite loops.  Disabled since they have stopped!  :)
		// Mute console.log messages repeated to the browser console.
		// (Display them only in the fastlogger, keep the browser log clear for
		// warnings, errors and info messages.)
		// TODO: Similarly, we may want to mute log messages in the FJSL, and only
		// show info/warn/errors.
		muteLogRepeater: 0,

		logNodeInsertions:     false, // Watch for and report DOMNodeInserted events.

		bringBackTheStatusBar: true   // TODO: add fake statusbar div, watch window.status for change?
	};

	for (var k in FJSL.defaults) {
		if (FJSL[k] === undefined) {
			FJSL[k] = FJSL.defaults[k];
		}
	}

	if (this.localStorage) {
		// TODO: Recall and store FJSL preferences in localStorage.
		// UNWANTED: During testing, I am toggling this setting.
		// FJSL.interceptTimeouts = !Boolean(localStorage['fastjslogger_interceptTimeouts']);
		localStorage['fastjslogger_interceptTimeouts'] = FJSL.interceptTimeouts;
	}



	// This fails to intercept GM_log calls from other userscripts.
	// However it intercepts everything when we run bookmarklets.

	// Partly DONE: We may be able to capture errors by overriding setTimeout,
	// setInterval, XMLHttpRequest and any other functions which use callbacks,
	// with our own versions which attempt the callback within a try-catch which
	// logs and throws any intercepted exceptions.
	// Unfortunately wrapping a try-catch around the main scripts in the document
	// (which have alread run) is a bit harder from here.  ;)
	// Aha!  Turns out we can catch those errors with window.onerror!

	// TODO: No thanks to Wikipedia's pre styling(?) I can't set a good default, so we need font zoom buttons!
	// TODO: Add ability to minimize but popup again on new log message.
	// TODO: Report file/line-numbers where possible.  (Split output into table? :E)
	// TODO: Option to watch for new globals.
	// TODO: Options to toggle logging of any intercepted setTimeouts, XMLHttpRequest's etc, in case the reader is interested.

	// TESTING: all DOM events!  TODO: XHR.
	// TESTING: We could even put intercepts on generic functions, in case an error occurs inside them, in a context which we had otherwise failed to intercept.

	// TODO: If the FJSL is wanted for custom logging, not normal console.log
	// logging, we will need to expose a function (FJSL.log()?), and *not*
	// intercept console.log.

	// TODO: Perhaps instead of creating a new console, we should just replace
	// functions in the existing one (and leave anything we haven't altered
	// intact).

	// TODO: See https://github.com/h5bp/html5-boilerplate/blob/master/js/plugins.js
	// for more log actions.

	var loadFJSL = function(){



		// I did have two running happily in parallel (Chrome userscript and page
		// script) but it is rarely useful.  Perhaps we should close the older one?
		if (document.getElementById("fastJSLogger") != null) {
			return;
		}



		// GUI creation library functions

		function addStyle(css) {
			// Konqueror 3.5 does not act on textValue, it requires textContent.
			// innerHTML doesn't work for selectors containing '>'
			var st = newNode("style", { type: "text/css", innerHTML: css } );
			document.getElementsByTagName('head')[0].appendChild(st);
		}

		function newNode(tag,data) {
			var elem = document.createElement(tag);
			if (data) {
				for (var prop in data) {
					elem[prop] = data[prop];
				}
			}
			return elem;
		}

		function newSpan(text) {
			return newNode("span",{textContent:text});
		}

		function addCloseButtonTo(elem) {

			var b = document.createElement("button");
			b.textContent = "X";
			b.style.zIndex = 2000;

			b.onclick = function() {
				//GM_log("[",b,"] Destroying:",elem);
				elem.style.display = 'none';
				elem.parentNode.removeChild(elem);
			};

			b.style.float = 'right';   // BUG: not working in FF4!
			elem.appendChild(b);

		}



		// == Main Feature - Present floating popup for log messages ==

		// Cleaner to expose the div via id?  It may have been removed from DOM!
		// We could fetch logDiv from FJSL when needed?
		var logDiv = null;
		var logContainer = null;

		var autoHideTimer = null;

		var oldSetTimeout = window.setTimeout;

		function createGUI() {

			var css = "";
			css += " .fastJSLogger { position: fixed; right: 8px; top: 8px; width: 40%; /*max-height: 90%; height: 320px;*/ background-color: #333; color: white; border: 1px solid #666; border-radius: 5px; padding: 2px 4px; z-index: 10000; } ";
			css += " .fastJSLogger > span { max-height: 10%; padding: 0 0.3em; }";
			css += " .fastJSLogger > .fjsl-title { font-weight: bold; }";
			// css += " .fastJSLogger > pre  { max-height: 90%; overflow: auto; }";
			//// On the pre, max-height: 90% is not working, but specifying px does.
			var maxHeight = window.innerHeight * 0.8 | 0;
			css += " .fastJSLogger > pre  { max-height: "+maxHeight+"px; overflow: auto; word-wrap: break-word; }";
			css += " .fastJSLogger > pre  { padding: 0px; margin: 0.4em 0.2em; }";
			// Must set the colors again, in case the page defined its own bg or fg color for pres that conflicts ours.
			css += " .fastJSLogger > pre  { /* background-color: #ffffcc; */ background-color: #888; color: black; }";
			css += " .fastJSLogger > pre  { font-family: Sans; }";
			// css += " .fastJSLogger > pre > input { width: 100%, background-color: #888888; }";
			css += " .fastJSLogger > pre > div    { border-top: 1px solid #888; padding: 0px 2px; ";
			css += "                                white-space: normal; word-break: break-all; }";
			css += " .fastJSLogger > pre > .log   { background-color: #eee; color: #555; }";
			css += " .fastJSLogger > pre > .info  { background-color: #aaf; }";
			css += " .fastJSLogger > pre > .warn  { background-color: #ff4; }";
			css += " .fastJSLogger > pre > .error { background-color: #f99; }";
			css += " .fastJSLogger           { opacity: 0.1; transition: opacity 1s ease-out; } ";
			css += " .fastJSLogger:hover     { opacity: 1.0; transition: opacity 400ms ease-out; } ";
			css += " .fastJSLogger.notifying { opacity: 1.0; transition: opacity 200ms ease-out; } ";
			if (document.location.host.match(/wikipedia/))
				css += " .fastJSLogger > pre  { font-size: 60%; }";
			else
				css += " .fastJSLogger > pre { font-size: 70%; } ";
			addStyle(css);

			// Add var before logDiv to break hideLogger()!
			logDiv = newNode("div",{ id: 'fastJSLogger', className: 'fastJSLogger' });
			if (FJSL.startHidden) {
				hideLogger();
			}
			document.body.appendChild(logDiv);

			// logDiv.style.position = 'fixed';
			// logDiv.style.top = '20px';
			// logDiv.style.right = '20px';

			// @todo refactor
			// I/O: logDiv, logContainer

			var heading = newSpan("FastJSLogger");
			heading.className = "fjsl-title";
			logDiv.appendChild(heading);

			// addCloseButtonTo(logDiv);

			var closeButton = newSpan("[X]");
			closeButton.style.float = 'right';
			closeButton.style.cursor = 'pointer';
			closeButton.style.paddingLeft = '5px';
			closeButton.onclick = function() { logDiv.parentNode.removeChild(logDiv); };
			logDiv.appendChild(closeButton);

			var logContainer = newNode("pre");

			var rollupButton = newSpan("[--]");
			rollupButton.style.float = 'right';
			rollupButton.style.cursor = 'pointer';
			rollupButton.style.paddingLeft = '10px';
			rollupButton.onclick = function() {
				if (logContainer.style.display == 'none') {
					logContainer.style.display = '';
					rollupButton.textContent = "[--]";
				} else {
					logContainer.style.display = 'none';
					rollupButton.textContent = "[+]";
				}
			};
			logDiv.appendChild(rollupButton);

			if (FJSL.displaySearchFilter) {
				function createSearchFilter(logDiv,logContainer) {
					var searchFilter = document.createElement("input");
					searchFilter.type = 'text';
					searchFilter.style.float = 'right';
					searchFilter.style.paddingLeft = '5px';
					searchFilter.onchange = function(evt) {
						var searchText = this.value;
						// console.log("Searching for "+searchText);
						var logLines = logContainer.childNodes;
						for (var i=0;i<logLines.length;i++) {
							if (logLines[i].textContent.indexOf(searchText) >= 0) {
								logLines[i].style.display = '';
							} else {
								logLines[i].style.display = 'none';
							}
						}
					};
					return searchFilter;
				}
				var searchFilter = createSearchFilter(logDiv,logContainer);
				// logDiv.appendChild(document.createElement("br"));
				logDiv.appendChild(searchFilter);
			}

			logDiv.appendChild(logContainer);

			return [logDiv,logContainer];

		}

		// @parameter channel String
		// @parameter args Array<*>
		// Do not pass more than two parameters; addToFastJSLog will ignore any extra.
		function addToFastJSLog(channel, args) {

			// Make FJSL visible if hidden
			showLogger();

			if (logContainer) {

				var out = "";
				for (var i=0; i<args.length; i++) {
					var obj = args[i];
					var str = "" + obj;
					if (obj && obj.constructor === "Array") {
						str = "[" + obj.map(showObject).join(", ") + "]";
					}
					// Non-standard: inform type if toString() is dull.
					if (str === "[object Object]") {
						str = "";
						if (typeof obj !== 'object') {
							// Surely this is incredibly unlikely!  Apart from the obvious case of a String.
							str += "(" + (typeof obj) + ")";
						}
						if (obj.constructor && obj.constructor.name) {
							str = "[" + obj.constructor.name + "]";
							if (obj.constructor.name === "Object") {
								str = "";
								// Because showObject will give it {}s, this is implied.
								// Although Chrome's console does actually print: "Object { ... }"
							}
						}
						str += showObject(obj);
					} else {
					}
					str = shortenString(str);
					var gap = (i>0?' ':'');
					out += gap + str;
				}

				var d = document.createElement("div");
				d.className = channel;
				d.textContent = out;

				if (logContainer.childNodes.length >= 1000) {
					logContainer.removeChild(logContainer.firstChild);
				}

				logContainer.appendChild(d);

				// Scroll to bottom
				// TODO: This is undesirable if the scrollbar was not already at the bottom, i.e. the user has scrolled up manually and is trying to read earlier log entries!
				logContainer.scrollTop = logContainer.scrollHeight;

			}

			if (autoHideTimer !== null) {
				clearTimeout(autoHideTimer);
				autoHideTimer = null;
			}
			if (FJSL.autoHide) {
				// Never log this setTimeout!  That produces an infinite loop!
				autoHideTimer = oldSetTimeout(hideLogger,15 * 1000);
			}

			return d;

		}

		function debounce(duration, fn) {
			var timer = null;
			return function() {
				clearTimeout(timer);
				timer = setTimeout(fn, duration);
			};
		}

		var clearOpacity = debounce(3000, function(){
			//logDiv.style.opacity = '';
			logDiv.className = logDiv.className.replace(/(^|\s)notifying(\s|$)/, '');
		});

		function showLogger() {
			if (logDiv) {
				// logDiv.style.display = '';
				//// BUG: Transition is not working!  Perhaps it only works when switching between CSS classes.
				// logDiv.style._webkit_transition_property = 'opacity';
				// logDiv.style._webkit_transition_duration = '2s';
				//logDiv.style.opacity = 1.0;
				if (!logDiv.className.match(/(^|\s)notifying(\s|$)/)) {
					logDiv.className += " notifying";
				}
				clearOpacity();
			}
		}

		function hideLogger() {
			if (logDiv) {
				// logDiv.style.display = 'none';
				// logDiv.style._webkit_transition_property = 'opacity';
				// logDiv.style._webkit_transition_duration = '2s';
				// logDiv.style.opacity = 0.0;
				clearOpacity();
			}
		}

		var k = createGUI();
		logDiv = k[0];
		logContainer = k[1];

		// target.console.log("FastJSLogger loaded this="+this+" GM_log="+typeof this.GM_log);
		// console.log("interceptTimeouts is "+(FJSL.interceptTimeouts?"ENABLED":"DISABLED"));

		// Intercept messages for console.log if it exists.
		// Create console.log if it does not exist.

		var oldConsole = this.console; // When running as a userscript in Chrome, cannot see this.console!
		// TODO: We should probably remove all this GM_ stuff.
		// We aren't using it even if we do find it.
		var oldGM_log = this.GM_log;

		var target = ( this.unsafeWindow ? this.unsafeWindow : window );

		// Replace the old console
		target.console = {};

		var lastArgs = [];

		target.console.log = function(a,b,c) {
			// I tried disabling this and regretted it!
			// My Console bookmarklet can cause an infloop with FJSL if you want to test it.
			var args = Array.prototype.slice.call(arguments, 0);
			if (arraysEqual(args, lastArgs)) {
				return;
			}
			lastArgs = args;

			// Replicate to the old loggers we intercepted (overrode)

			if (oldConsole && oldConsole.log && !FJSL.muteLogRepeater) {
				// Some browsers dislike use of .call and .apply here, e.g. GM in FF4.
				// So to avoid "oldConsole.log is not a function":
				try {
					oldConsole.log.apply(oldConsole,arguments);
				} catch (e) {
					// Ugly chars to signify that sucky fallback is being used
					oldConsole.log("[noapply]",a,b,c);
				}
			}

			/*
			//// WARNING: *This* is the cause of infinite console.logging, if it has been implemented by FallbackGMAPI.
			//// Solution: FBGMAPI should take a reference to console when creating it's GM_log, not waiting until later to look for the console.
			if (oldGM_log) {
				oldGM_log(a,b,c);
			}
			*/

			addToFastJSLog("log", arguments);
		};

		//// NOT TESTED.  Intercept Greasemonkey log messages.  (Worth noting we have disabled calls *to* GM_log above.)
		// this.GM_log = target.console.log;

		// == MAIN SCRIPT ENDS ==
		// The rest is all optional so may be stripped for minification.



		/* Provide/intercept console.info/warn/error(). */

		target.console.error = function() {
			// We can get away with this in Chrome!

			//var args = Array.prototype.slice.call(arguments,0);
			//args.unshift("[ERROR]");

			oldConsole.error.apply(oldConsole, arguments);

			addToFastJSLog("error", arguments); // ,'\n'+getStack(2,20).join("\n"));

			// Report stacktrace if we were passed an error.
			if (arguments[0] instanceof Error) {
				//target.console.error("" + arguments[0].stack);
				addToFastJSLog("error", "" + arguments[0].stack);
			}
		};

		// Could generalise the two functions below:
		//interceptLogLevel("warn");
		//interceptLogLevel("info");

		target.console.warn = function() {
			//var args = Array.prototype.slice.call(arguments,0);
			//args.push(""+getCallerFromStack());

			oldConsole.warn.apply(oldConsole, arguments);
			addToFastJSLog("warn", arguments);

			//// This was quite useful on one occasion.  But not in the presence of lots of warnings!
			// logStack(getStackFromCaller());
		};

		target.console.info = function() {
			oldConsole.info.apply(oldConsole, arguments);
			addToFastJSLog("info", arguments);
		};

		/*
		target.console.error = function(e) {
			// target.console.log("[ERROR]",e,e.stack);
			var newArgs = [];
			newArgs.push("[ERROR]");
			for (var i=0;i<arguments.length;i++) {
				newArgs.push(arguments[i]);
			}
			try {
				newArgs.push("(reported from function "+arguments.callee.caller.name+")");
				// console.error("caller = "+arguments.callee.caller);
				// console.error("stack = "+e.stack);
			} catch (e) {
			}
			target.console.log.apply(target.console,newArgs);
			target.console.log.apply(target.console,["Stacktrace:\n",e.stack]);
		};
		*/



		// Some more library functions:

		function arraysEqual(a, b) {
			for (var i=0; i<a.length; i++) {
				if (a[i] !== b[i]) {
					return false;
				}
			}
			return true;
		}

		function tryToDo(fn, target, args) {
			try {

				return fn.apply(target, args);

			} catch (e) {
				// Actually hard to read!
				// var prettyFn = fn; // ("" + fn) .replace(/\n/g,'\\n');
				var fnName = fn && fn.name;
				if (!fnName) {
					fnName = "<anonymous>";
				}
				// console.log("[ERR]", e, prettyFn);
				// console.log("[Exception]", e, "from " + fnName + "()");
				if (e.stack) {
					console.error("!! " + e.stack);
				} else if (e.lineNumber) {
					console.error("!! " + e + " on " + e.lineNumber + ")");
				} else {
					console.error("!!", e);
				}
				// var prettyFn = ("" + fn).replace(/\n/,/ /, 'g');
				var prettyFn = shortenString(fn);
				console.error("occurred when calling: " + prettyFn);
				// If we rethrow the error, browser devtools will show the throw as coming from here!
				// But we should throw it anyway.  That is what the function's caller expects.
				// At one point we tried to reproduce the error by calling fn again, but not catching it.
				// However this is a dangerous tactic.  What if fn() creates a setTimeout before throwing its exception?  Then calling it again would produce a second setTimeout!
				//return fn.apply(target, args);
				throw e;
			}
		}

		// TODO: Unify showObject and shortenString with replay.js's toDetailedString and toSimpleString.
		function showObject(obj) {
			return "{ " + Object.keys(obj).map(function(prop) {
				return prop + ": " + shortenString(obj[prop]);
			}).join(", ") + " }";
		}

		function getXPath(node) {
			var parent = node.parentNode;
			if (!parent) {
				return '';
			}
			var siblings = parent.childNodes;
			var totalCount = 0;
			var thisCount = -1;
			for (var i=0;i<siblings.length;i++) {
				var sibling = siblings[i];
				if (sibling.nodeType == node.nodeType) {
					totalCount++;
				}
				if (sibling == node) {
					thisCount = totalCount;
					break;
				}
			}
			return getXPath(parent) + '/' + node.nodeName.toLowerCase() + (totalCount>1 ? '[' + thisCount + ']' : '' );
		}

		function getStack(drop,max) {
			var stack;
			try {
				throw new Error("Dummy for getStack");
			} catch (e) {
				stack = e.stack.split('\n').slice(drop).slice(0,max);
			}
			return stack;
		}

		// What frame/function called the function which called us?
		function getCallerFromStack() {
			return getStack(4,1);
		}

		// The frame/function which called our caller, and all above it.
		function getStackFromCaller() {
			return getStack(4,-1);
		}

		function logStack(stack) {
			for (var i=0;i<stack.length;i++) {
				console.log(""+stack[i]);
			}
		}



		// == Error Interceptors ==

		// Whenever a callback is placed, we should wrap it!
		// DONE:
		//   event listeners (added after we run)
		//   setTimeout
		// TODO:
		//   setInterval
		//   XMLHttpRequest

		// We could even wrap any standard functions which are common sources of
		// Errors, in case we fail to catch them any other way.

		if (FJSL.watchWindowForErrors) {
			// Registers a window.onerror event handler, which catches DOM Errors like
			// img elements which failed to load their src resource.
			function handleError(evt) {

				if (!FJSL.firstWindowErrorEvent) {
					FJSL.firstWindowErrorEvent = evt;
				}
				//// Expose this event for inspection
				FJSL.lastWindowErrorEvent = evt;
				// console.log("Error caught by FJSL:");
				// console.log(evt);
				// console.log(Object.keys(evt));
				// target.console.error(evt.filename+":"+evt.lineno+" "+evt.message+" ["+evt.srcElement+"]",evt);

				// In fact repeating these to the browser log is redundant, since the
				// browser is likely to report these errors anyway.
				// Therefore, we could use addToFastJSLog instead of console.log below.

				// Chrome sometimes gives us a message, sometimes doesn't.
				if (evt.message) {
					// This is what Chrome provides for an error thrown by a page script.
					// In Chrome this event object contains neither an Error nor a stack-trace.
					// Also the current stack-trace is uninformative (see nothing about the call to handleError.)
					// So we don't use the following: , evt, getStack(0,99).join('\n')
					console.error("[Caught Error] "+evt.message+" "+evt.filename+":"+evt.lineno);
				} else {
					var report = '[Caught Unknown Error] ';
					report += "type="+evt.type+" ";

					// For some errors neither Firefox nor Chrome give us a message.
					// But sometimes we can peek into the element that fired the error.
					// If it was an image, then its src attribute may be useful.
					var elem = evt.srcElement || evt.target;
					if (elem) {
						report += "From element "+elem+" ";
						try {
							report += "with src="+elem.src+" ";
						} catch (e) { }
					}

					/*
					if (elem == window) {
						FJSL.lastEE = evt;
					}
					*/

					/*
					if (!elem) {
						// Firefox 14 was providing only a constructor function in the event,
						// so I wonder if calling it will reproduce the error for us.  Either
						// that, or it will try to construct an event object.  :P
						// I will probably remove this if I see it again and confirm that it's useless.
						// I did get a HUGE string from Firefox which was pretty interesting.
						// But it's not what we were looking for.
						if (typeof evt.constructor == "function") {
							try {
								var result = evt.constructor();
								if (result) {
									console.error(report + "Constructor result: "+result);
								}
							} catch (e) {
								// I think in Firefox this gives us useful information.
								// TODO: But I have seen us reach here in Chrome, with an error "cannot call DOM Object constructor" presumably meaning out evt.constructor() call was forbidden, and we should look elsewhere for information.
								report += "(Constructor error: \""+e+"\") ";
								// Dirty cheat expose it for dev/debugger:
								evt.FJSLconstructorError = e;
								// I have come to the conclusion that although they are different, the errors I get in both Firefox and Chrome are errors about the way I have called the constructor, and not any useful error I was after :P
								// FF: Timestamp: 29/07/12 18:10:14
								// Error: NS_ERROR_XPC_NOT_ENOUGH_ARGS: Not enough arguments
								console.log("[Constructor Error]",e);
								throw e;
							}
						}
					}
					*/

					console.error(report,evt);
				}
			}
			window.addEventListener("error",handleError,true);
			// document.body.addEventListener("error",handleError,true);
		}

		// Split into shortenString, escapeString, compressString, and toSimpleString.
		function shortenString(s) {
			s = ""+s;
			s = s.replace(/\n[ \t]+/,'\\n ','g');
			s = s.replace(/\n/,'\\n','g');
			s = s.replace(/\t/,'  ','g');
			if (s.length > 202) {
				s = s.substring(0,202) + "...";
			}
			return s;
		}

		if (FJSL.interceptTimeouts) {

			window.setTimeout = function(fn, ms) {
				if (FJSL.logTimeouts) {
					// TODO: We used to pass ,fn to log here.  That was nice in Chrome, because we can fold it open or closed, but too spammy for plain FJSL.  Recommendation: Refactor out prettyShow() fn which will determine browser and provide raw object for chrome or shortened string for noob browsers?
					console.info("[EVENT] setTimeout() called: "+ms+", "+shortenString(fn));
				}
				var wrappedFn = function(){
					// setTimeout can be passed a string.  But we want a function so we can call it later.
					if (typeof fn === 'string') {
						var str = fn;
						fn = function(){ eval(str); };
					}
					// CONSIDER: Of dubious value
					if (typeof fn !== 'function') {
						console.error("[FJSL] setTimeout was not given a function!",fn);
						return;
					}

					// We don't really need to return here.  setTimeout won't do anything with the returned value.
					// Similarly it probably hasn't passed us any arguments.  The context object 'this' is probably 'window'.
					return tryToDo(fn, this, arguments);
				};

				return oldSetTimeout(wrappedFn, ms);
			};

		}



		// == General Info Logging Utilities ==

		if (FJSL.logNodeInsertions) {
			document.body.addEventListener('DOMNodeInserted', function(evt) {
				if (evt.target === logContainer || evt.target.parentNode === logContainer) {
					return;
				}
				// console.log("[DOMNodeInserted]: target=",evt.target," event=",evt);
				// console.log("[DOMNodeInserted]:",evt,evt.target,"path="+getXPath(evt.target),"html="+evt.target.outerHTML);
				console.log("[DOMNodeInserted]: path="+getXPath(evt.target)+" html="+evt.target.outerHTML,evt);
			}, true);
		}

		if (FJSL.interceptEvents) {

			// This store helps us to remove the wrapped event listeners which we add
			var wrapped_and_unwrapped = [];

			var realAddEventListener = HTMLElement.prototype.addEventListener;
			// HTMLElement.prototype.oldAddEventListener = realAddEventListener;
			HTMLElement.prototype.addEventListener = function(type,handler,capture,other){
				if (FJSL.logEvents) {
					// Note niceFunc is re-used later, so don't remove this!
					var niceFunc = handler.name || (""+handler).substring(0,80).replace(/\n/," ",'g').replace(/[ \t]*/," ",'g')+"...";
					/*
					console.info("[EVENTS] Listening for "+type+" events on "+getXPath(this)+" with handler "+niceFunc);
					console.info(getStack(3,3).join("\n"));
					*/
				}
				var newHandler = function(evt) {
					if (FJSL.logEvents) {
						var isSpammyEvent = ["mousemove","mouseover","mouseout","mousedown","mouseup","keydown","keyup"].indexOf(evt.type) >= 0;
						if (!isSpammyEvent || FJSL.logCommonMouseEvents) {
							if (evt.target.parentNode == logContainer) {
								// Do not log events in the console's log area, such as DOMNodeInserted!
							} else {
								// console.log("("+type+") on "+evt.target);
								// console.info("[EVENT] "+type+" on "+getXPath(evt.target));
								// console.log("[EVENT] "+type+" on "+getXPath(evt.target)+" evt="+showObject(evt));
								console.info("[EVENT] "+type+" on "+getXPath(evt.target)+" being handled by "+niceFunc);
							}
						}
					}
					return tryToDo(handler, this, arguments);
					// return handler.apply(this, arguments);
				};
				wrapped_and_unwrapped.push({
					unwrapped: handler,
					wrapped: newHandler,
				});
				return realAddEventListener.call(this,type,newHandler,capture,other);
			};

			// We cannot remove the original handler, because it was never added.  Instead we need to remove the wrapper that we did add.
			var realRemoveEventListener = HTMLElement.prototype.removeEventListener;
			// HTMLElement.prototype.oldRemoveEventListener = realAddEventListener;
			HTMLElement.prototype.removeEventListener = function(type,handler,capture,other){
				//console.log("removeEventListener was called with:",type,handler);
				//try { throw new Error("dummy for stacktrace"); } catch (e) { console.log("At:",e); }
				for (var i = wrapped_and_unwrapped.length; i--;) {
					var both = wrapped_and_unwrapped[i];
					if (handler === both.unwrapped) {
						handler = both.wrapped;
						wrapped_and_unwrapped.splice(i,1);
						break;
					}
				}
				return realRemoveEventListener.call(this,type,handler,capture,other);
			};

		}

		if (FJSL.logChangesToGlobal) {
			function newChangeWatcher() {
				var checkSpeed = 4000;
				var watcher = {};
				var global = window;
				var knownKeys = {};
				var firstCheck = true;
				function checkGlobal() {
					if (FJSL.logChangesToGlobal) {
						for (var key in global) {
							if (knownKeys[key] === undefined) {
								if (!firstCheck) {
									var obj = global[key];
									console.log("[New global detected] "+key+" =",obj);
								}
								knownKeys[key] = 1;
							}
						}
						firstCheck = false;
					}
					oldSetTimeout(checkGlobal,checkSpeed);
				}
				oldSetTimeout(checkGlobal,checkSpeed);
				return watcher;
			};
			var changeWatcher = newChangeWatcher();
		}



	};

	if (document.body) {
		loadFJSL();
	} else {
		// doc bod may not exist if we are loaded in the HEAD!  Better wait till later...
		setTimeout(loadFJSL,1000);
		// But we would quite like to start doing stuff now anyway!
		// TODO: Load what we can immediately, delay only things we must.
		// E.g. start capturing text, even if we can't display it yet.
	}

})();