O'Reilly Hacks
oreilly.comO'Reilly NetworkSafari BookshelfConferences Sign In/My Account | View Cart   
Book List Learning Lab PDFs O'Reilly Gear Newsletters Press Room Jobs  


 
Buy the book!
Greasemonkey Hacks
By Mark Pilgrim
November 2005
More Info

HACK
#46
Trace XMLHttpRequest Activity
Log XMLHttpRequest calls into JavaScript Console
The Code
[Discuss (1) | Link to this hack]

XMLHttpRequest is a JavaScript technique that enables a page to interact with the server without having to reload the entire page. This nonstandard API was first developed by Microsoft for Internet Explorer, but it was later picked up and implemented by most other browsers, including Firefox. Once used by only a few, it is now becoming more mainstream in the development of web applications.

The renewed interest for rich web applications such as Gmail, MSN Web-Messenger, and A9 Search, has crystallized a new nickname for the technique: AJAX.

TIP

Jesse James Garrett coined this term in early 2005 as a shorthand for "Asynchronous JavaScript And XML."

A large number of frameworks now make use of the XMLHttpRequest object, trying to abstract the API and make it easier to use for a larger portion of hackers. However, as with most abstractions, there are still many times when you need to look under the hood.

Traditional debugging tools allow you to do that. You can install HTTP sniffers such as the LiveHTTPHeaders extension; you can test code interactively with the Venkman JavaScript debugger or the JavaScript shell; you can just litter your code with JavaScript alert statements. But these tools often offer too much or too little of what you actually need. This user script approaches debugging from a different angle, by focusing on the XMLHttpRequest interactions themselves and providing lightweight and instant tracing.

Running the Hack

To demonstrate this hack, I'll use Backpack, an AJAX-based information management tool. Go to http://backpackit.com and sign up for a free account.

After logging in, you can try out the application by creating a page and editing it. You will notice that these interactions won't cause the page to reload.

Under the covers, the application uses XMLHttpRequest to send the data back to the central server. That's where our user script comes in.

When installing the user script (Tools → Install This User Script), modify the list of included domains. Change the default http://pick.some.domain to http://*.backpackit.com/*, as shown in .

Figure 1. Configuration for Backpack debugging

Select Tools → JavaScript Console, and then change the filter in the console window to display Messages only. Now, edit your BackpackIt page again— for example, by changing the title to "Greasemonkey Hacks." The script catches the XMLHttpRequest interaction and displays it in JavaScript Console.

What exactly gets logged? In this case, three events, as illustrated in . The browser calls the open and send methods on the XMLHttpRequest object, and the server responds with a confirmation page.

Backpack expects HTML content to be returned from the server. This script logs both the HTML confirmation page and the "200 OK" HTTP status code.

Figure 2. Backpack debugging output

In addition, each logged event includes the unique ID of the XMLHttpRequest instance; in this example, it was 445.

Julien Couvreur

The Code

A typical usage scenario of XMLHttpRequest starts with creating a new XMLHttpRequest instance, wiring some callbacks (such as onreadystatechange, onload, or onerror), and then calling open and send.

The basic approach of this script is to replace the open and send methods on any XMLHttpRequest instance that gets created. The replacement code mimics the behavior of the original methods, but it also traces the input parameters and adds some extra instrumentation on callback events.

Most common object-oriented languages differentiate the concept of class (the definition of an object) and object (an instance of a class).

Instead, JavaScript has classes only. When creating a new object, it uses another object as a template or prototype, rather than following an abstract blueprint (a class). It is called a prototype-based language.

This script takes advantage of this characteristic by modifying the prototype of the XMLHttpRequest constructor to replace the open and send methods on all XMLHttpRequest instances.

It overrides XMLHttpRequest.prototype.open and XMLHttpRequest.prototype.send with new implementations and keeps references to the original methods by backing them up into XMLHttpRequest.prototype.oldOpen and XMLHttpRequest.prototype.oldSend.

TIP

To keep the state of your UI, you often don't have to build your own structure in parallel with that of the document. The objects from the DOM can be extended with your own properties. In this case, the script uses the XMLHttpRequest object itself to store the unique ID for the object.

Because multiple calls to the server may occur simultaneously, using multiple XMLHttpRequest objects, it is useful to have an instance ID along with the traced information.

When first called on an XMLHttpRequest object, the uniqueID function will generate a random ID number and store it in the uniqueIDMemo property on the object. Subsequent calls will load that saved value and reuse it.

TIP

Greasemonkey lets you log events to JavaScript Console via the GM_log method. That feature was added in Greasemonkey starting with Version 0.3, but didn't exist in earlier versions. In cases like that, you should test whether the feature is present and degrade gracefully if it is missing.

Save the following user script as xmlhttprequest-tracing.user.js:

	// ==UserScript==
	// @name		XmlHttpRequest Tracing
	// @namespace	http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description Trace XmlHttpRequest calls into the Javascript Console
	// @include		http://pick.some.domain
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	XMLHttpRequest.prototype.uniqueID = function( ) {
		if (!this.uniqueIDMemo) {
			this.uniqueIDMemo = Math.floor(Math.random( ) * 1000);
		}
		return this.uniqueIDMemo;
	}

	XMLHttpRequest.prototype.oldOpen = XMLHttpRequest.prototype.open;

	var newOpen = function(method, url, async, user, password) {
		GM_log("[" + this.uniqueID( ) + "] intercepted open (" +
				method + " , " +
				url + " , " +
				async + " , " +
				user + " , " +
				password + ")");
		this.oldOpen(method, url, async, user, password);
	}

	XMLHttpRequest.prototype.open = newOpen;

	XMLHttpRequest.prototype.oldSend = XMLHttpRequest.prototype.send;

	var newSend = function(a) {
		var xhr = this;
		GM_log("[" + xhr.uniqueID( ) + "] intercepted send (" + a + ")");
		var onload = function( ) {
			GM_log("[" + xhr.uniqueID( ) + "] intercepted load: " +
				xhr.status +
				" " + xhr.responseText);
		};
		
		var onerror = function( ) {
			GM_log("[" + xhr.uniqueID( ) + "] intercepted error: " +				xhr.status);
		};
		
		xhr.addEventListener("load", onload, false);
		xhr.addEventListener("error", onerror, false);
		
		xhr.oldSend(a);
	}

	XMLHttpRequest.prototype.send = newSend;


O'Reilly Home | Privacy Policy

© 2007 O'Reilly Media, Inc.
Website: | Customer Service: | Book issues:

All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.