Subscribe to RSS Feed

One of the new features introduced since Firebug 1.4a2 is possibility to extend Net panel with additional info. There are new APIs that allow to create a custom info-tab for network requests (like Headers, Params, etc.) from within a Firebug extensions.

This post is intended (a) to show how these APIs should be properly used by Firebug's extensions and also (b) to gather some feedback about the architecture.

So, let me know please, if there is something I haven't thought about when designing this new extension point.

Custom info tab for network request.

Custom Network Request Info

In order to demonstrate the new functionality I have created a simple example that appends a new info-tab with XML preview of a network response. The screen-shot above depicts where exactly (within the Firebug’s UI) the additional info-tab is displayed.

The XML Explorer tab is created only for text/html and text/xml responses. If the response is well-formed document, appropriate XML tree-like structure is displayed, otherwise there is an error message with highlighted line where parse errors occurred.

Custom info tab with an error message.

NetInfoBody Listener

The first step in creating additional tab for a network request is registering a listener into Firebug.NetMonitor.NetInfoBody object. This object represents entire info about a network request that is displayed when the user expands a request row (by clicking on it).

Notice that this info includes some built-in tabs like Headers (Request and Response headers info), Response (raw response data), HTML (HTML preview in case of text/html content type, introduced in Firebug 1.4.0a2) and other tabs.

The listener should be registered in initialize method and unregistered within shutdown method of an extension's model object.

Firebug.XmlInfoTab = extend(Firebug.Module,
{
    initialize: function() {
        Firebug.NetMonitor.NetInfoBody.addListener(this);
    },

    shutdown: function() {
        Firebug.NetMonitor.NetInfoBody.removeListener(this);
    },

    // Listener for NetInfoBody.
    initTabBody: function(infoBox, file) {},
    destroyTabBody: function(infoBox, file) {},
    updateTabBody: function(infoBox, file, context) {}
});

In our case, the model also implements all callbacks methods.

  • initTabBody(infoBox, file) - gets called when the user expands a network requests. An infoBox parameter represents the DOM element that wraps all the displayed info (including tabs). A file represents a data object associated with the request. This method is used to decide whether a custom tab should be created for the particular request or not.
  • destroyTabBody(infoBox, file) - the network request info has been collapsed. Use this method for clean up (if necessary).
  • updateTabBody(infoBox, file, context) - the tab has been selected. This method should be used to generate/update the content. A context parameter represents the context data object associated with the current page.

Create Tab

Since the example displays the tab only for text/html and text/xml content types, we can utilize a category, which is a member of the file object passed into initTabBody method. The proper category is initialized by the framework according to the response mime-type. If there is no mime-type, file extension is used to guess it. Some of these categories (undefined, html, css, js, xhr, image, flash, txt, bin) are also used to filter Net panel content.

Set of response categories used to filter Net panel content.

This is how the tab is created.

initTabBody: function(infoBox, file)
{
    if (file.category == "html")
        Firebug.NetMonitor.NetInfoBody.appendTab(infoBox, "XmlInfoTest",
            this.$STR("xmlinfo.tab.title"));
},
  • appendTab(netInfoBox, tabId, tabTitle) - Use this method to create a new tab. A tabId parameter specifies unique ID of the tab and a tabTitle represents it's label (should be localized).

Notice that our model object implements also a helper function for localization.

$STR: function(name)
{
    return document.getElementById("strings_xmlInfo").getString(name);
}

Generate tab content

The last step is to generate some content. This should be done within updateTabBody method. Putting the code into this method ensures that the content is generated only when the user really wants to see it (i.e. when the tab is selected) the first time and so, initial opening of the request-info is fast (at the beginning only one tab is visible anyway).

Here is how the updateTabBody is implemented.

updateTabBody: function(infoBox, file, context)
{
    // Get currently selected tab.
    var tab = infoBox.selectedTab;

    // Generate content only for the first time; and only if our tab
    // has been just activated.
    if (tab.dataPresented || !hasClass(tab, "netInfoXmlInfoTestTab"))
        return;

    // Make sure the content is generated just once.
    tab.dataPresented = true;

    // Get body element associated with the tab.
    var tabBody = getElementByClass(infoBox, "netInfoXmlInfoTestText");

    // Get response data from Firebug's cache.
    var responseText = context.sourceCache.loadText(file.href);

    // Parse response and create DOM.
    var parser = CCIN("@mozilla.org/xmlextras/domparser;1", "nsIDOMParser")
    var doc = parser.parseFromString(responseText, "text/xml");
    var root = doc.documentElement;

    // Error handling
    var nsURI = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
    if (root.namespaceURI == nsURI && root.nodeName == "parsererror")
    {
        Firebug.XmlInfoTab.ParseError.tag.replace({error: {
            message: root.firstChild.nodeValue,
            source: root.lastChild.textContent
        }}, tabBody);
        return;
    }

    // Generate UI using Domplate template (from HTML panel).
    Firebug.HTMLPanel.CompleteElement.tag.replace({object: root}, tabBody);
},

The code should be pretty straightforward, however there is a few things that should be explained. First, we are using some APIs from FBL (Firebug Library) for CSS and querying DOM.

  • hasClass(node, className [, className, ...]) - returns true if a given className(s) is present on specified node.
  • getElementByClass(node, className [, className, ...]) - returns a child element of a node with specified className(s)

Important thing is that the class name of our tab and body elements are automatically generated according to the tabId specified in appendTab.

Tab element class-name: "netInfo" + tabId + "Tab"
Body element class-name: "netInfo" + tabId + "Text"

Here is a snippet of HTML source that represents the underlying UI (it's the infoBox passed into all callbacks).

<div class="netInfoBody category-html">
    <div class="netInfoTabs">
        <!-- The other tabs are here -->
        <a class="netInfoXmlInfoTestTab netInfoTab"
            view="XmlInfoTest" selected="true">
XML Explorer</a>
    </div>
    <div class="netInfoBodies">
        <!-- The other bodies are here -->
        <div class="netInfoXmlInfoTestText netInfoText" selected="true">
        </div>
    </div>
</div>

You can also see that the XmlInfoTest tab was selected at the time when I did the snapshot (selected="true").

Error Handling

The response is parsed by @mozilla.org/xmlextras/domparser;1 component and its parseFromString method. In case of an error (the document isn't well formed), the method returns an error info (also DOM document).

In order to display the error properly within the UI (instead of an HTML tree) there is additional Domplate template.

Firebug.XmlInfoTab.ParseError = domplate(Firebug.Rep,
{
    tag:
        DIV({class: "xmlInfoError"},
            DIV({class: "xmlInfoErrorMsg"}, "$error.message"),
            PRE({class: "xmlInfoErrorSource"}, "$error|getSource")
        ),

    getSource: function(error)
    {
        var parts = error.source.split("\n");
        if (parts.length != 2)
            return error.source;

        var limit = 50;
        var column = parts[1].length;
        if (column >= limit) {
            parts[0] = "..." + parts[0].substr(column - limit);
            parts[1] = "..." + parts[1].substr(column - limit);
        }

        if (parts[0].length > 80)
            parts[0] = parts[0].substr(0, 80) + "...";

        return parts.join("\n");
    }
});

The example extension (xmlinfo-0.1.xpi) can be downloaded here. Not to forget you need Firebug 1.4a2 or higher.

Finally, the example contains some FBTrace logs so, if you have "X" version of Firebug you can see in real time when all the methods are called. You can also explore the real HTML structure of the netInfoBody UI. Just check the XMLINFOTAB option (or set extensions.firebug.DBG_XMLINFOTAB to true)

FBTrace console in action.


Rss Commenti

7 Comments

  1. I know it’s ‘just an example’, but… only text/html and text/xml? You should also add application/xml and application/xhtml+xml.

    Perhaps also some other XML MIME types such as SVG, RDF, etc.?

    #1 Laurens Holst
  2. Yeah agree. Also, if the example would turn out to be really useful I would consider to put this (with all proper xml mime types) directly into Firebug.

    #2 admin
  3. Seems that firebug has provided a nice & clear interface of monitoring HTTP requests, with which I can develop an AMF inspector easier.:)

    #3 Cong
  4. @Cong: yeah, I saw your comment in http://code.google.com/p/fbug/issues/detail?id=541
    I like the idea!
    Honza

    #4 admin
  5. Hello,
    Great feature and great tutorial!
    I have an issue: I try to parse a response text that use some \r\n and \n as line breaks (yes, I admit it is not consistent but I have to deal with it.
    When I use file.reponseText, \r\n has been replaced by \n. I could replace all \n by \r\n but it doesn't work since there are also some \n that have to be left like that.
    Is there a way to access to the original response content?
    Thanks in advance.

    #5 Laurent
  6. @Laurent: Hm, this sounds like a bug. Firebug shouldn't change the line endings. Do you have a test case that I could use to reproduce the problem?

    #6 Honza
  7. Hello Honza,

    Test case in now in the following case:
    http://code.google.com/p/fbug/issues/detail?id=1694

    BR, Laurent

    #7 Laurent

Sorry, the comment form is closed at this time.