I’ve used Mint.com to categorize and measure my spending for years. It is an amazing tool, but the user interface for tagging transactions has always had a frustrating limitation. After adding a tag to a transaction, you don’t see that tag anywhere on the page. I wrote a user script to display them:

Mint.com transactions listing with tags visible

How do I install it?

Follow the (very short) installation instructions on GitHub.

Why is this necessary?

On Mint, a transaction must belong to one category (for example, Vacation or Restaurants). At the same time, a transaction may belong to any number of tags (for example, I can add the Taiwan trip tag to an expense in the Vacation category, or add Work lunch and Client X tags to an expense in the Restaurants category). The way I use Mint, categories and tags are both important to gaining a complete understanding of where my money goes. Unfortunately, in the transactions list (above), only categories are displayed.

The only time you ever see what tags you have applied to a transaction is when you are editing it:

The only part of Mint's interface where tags are visible.
The only part of Mint's interface where tags are visible.

I’m baffled that the tags feature has such a glaring omission in functionality… so I wrote a user script to fix it.

How does it work?

A user script is a piece of JavaScript code which your browser includes on certain web pages. In this case, I’ve written code which alters the way that the Mint.com transactions page works. I want to see what data my browser is requesting from Mint’s servers, find the transaction tags within that data, store a temporary copy of them, and insert the tags into the page.

Find XHR responses containing transaction data

The first step is to use my browser’s developer tools to watch the traffic on the transactions page. When the page loads, the Mint app makes many requests: images, JavaScript code, and data. I’m interested only in what data is being sent, so I filter the requests to show only XHR:

XHR requests made by the Mint app on page load.
XHR requests made by the Mint app on page load.

I notice that the Mint app makes a request to getJsonData.xevent on every page load. To investigate, I try loading different transactions into the page by clicking “Next” at the bottom of the transactions list, or by performing a transaction search. Each time, another request is made to that URL. Filtering the requests to show only those for this URL, and inspecting the content of the responses, confirms that the list of transactions is included in this data. Digging deeper into the structure of the data, I can find how the “labels” for each transaction are represented:

The structure of the data sent back for a request to /getJsonData.xevent.

Overwrite the browser’s XHR code to intercept the data

The above AJAX requests are made every time the transactions page loads. JavaScript allows a developer to overwrite the base AJAX request function which is used by all other code on a page (including Mint’s own code) to obtain data from the server. I can replace that function with my own code, then look for completed requests made to the getJsonData.xevent endpoint. Then my code can store its own copy of the data before the Mint app even sees it.

I overwrite the XMLHttpRequest.prototype.open method to run my code first, and then call the browser’s original version of the method. This allows me to inspect every request:

(function(open) {
  XMLHttpRequest.prototype.open = function() {
    // Add a listener for state changes on any request.
    this.addEventListener('readystatechange', function() {

      // Any time a request changes to the completed state, and the request URL
      // indicates it was for transaction data, store the data.
      if(this.readyState === 4 && this.responseURL.match('getJsonData.xevent')) {
        maybeInterceptTransactionsList(this.responseText);
      }
    }, false);

    // Call the original XMLHttpRequest.prototype.open.
    open.apply(this, arguments);
  };
})(XMLHttpRequest.prototype.open);

React to changes in the content of the transactions table

Next, I need to use the data stored by the code above to add the tags to the transactions table. I can’t simply add them as soon as the data arrives, because the Mint app needs a chance to process that data and populate the table with it. A hacky way to do this would be to use setTimeout or setInterval to check the transactions table every few seconds to see if it’s changed, and add/remove tags as necessary. But there’s a better way: the MutationObserver API. This is a new-ish browser API, almost tailor-made for the use case of third-party code watching for changes to the DOM.

new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // When the text in an element of the table changes, find the row it belongs to,
    // and add/remove tags for that transaction. It helps that the table rows we are
    // interested in have ids like 'transaction-743382659'.
    var $tr = jQuery(mutation.target).parents('tr').first();
    if($tr.length && $tr.attr('id') && $tr.attr('id').indexOf('transaction-') === 0) {
      updateRowWithTags($tr);
    }
  });
}).observe(
  document.querySelector('#transaction-list-body'),
  // We're interested in mutations to the child nodes of the transactions table,
  // but only characterData mutations, i.e. "some text has changed".
  {subtree: true, childList: true, characterData: true}
);

React to the user editing a transaction

It turns out that I can’t respond only to changes to the transactions table (triggered by navigating between pages of transactions or a search). I also need to update the table after the user edits a transaction. For this, the code must intercept XHR requests, so that it can spy on POSTs to the edit transaction endpoint (just as it spied on XHR responses for the transaction list above).

(function(open) {
  XMLHttpRequest.prototype.open = function() {
    // If the Mint app makes a POST request to the "update transaction" endpoint...
    if(arguments[0].match(/post/i) && arguments[1].match('updateTransaction.xevent')) {

      // Overwrite the `send` method of this request to store a copy of the data
      // before it gets sent.
      var self = this, send = this.send;
      this.send = function() {
        interceptTransactionEdit(arguments[0]);

        // Call the original `send` method.
        send.apply(self, arguments);
      };
    }

    open.apply(this, arguments);
  };
})(XMLHttpRequest.prototype.open);

It doesn’t take much code

The final code is a bit more nuanced than just the above. For example, Mint makes a separate request for a mapping from tag ID to tag name, which also must be cached. When Mint updates the transactions table, it actually triggers multiple DOM mutation events for each row (one each for date, merchant, price, etc.), so the code must be smart enough to update each row only once. Also, observing the DOM must be paused while the user script itself mutates the DOM by adding tags.

Nonetheless, the full code to add this functionality is fewer than 200 lines of readable, whitespace-heavy JavaScript. Adding a browser extension like this requires trust, so read through the code yourself on GitHub!