Your browser is no longer supported! Please upgrade your web browser now.

Harvest Upgrades to Rails 3

Harvest is on Rails 3! This is exciting news from the Harvest technical team. I’ll detail how the upgrade process went, but first a little history. The initial commit to Harvest was made on November 23rd, 2005 – five years ago. Back then, we ran Rails 0.14.1 and were still discussing how time should be entered into Harvest. That’s five years of Rails releases since we started, and each upgrade was a painful but worthwhile experience.

I cannot stress that last conviction enough. Besides the obvious technical improvements an upgrade brings, there is also a morale boost from using the very best tools. This comes at a cost: Upgrading a key dependency is challenging for an app of Harvest’s size.

Name Lines
Controllers 12K
Models 25K
Views 28K
Helpers 7K
Libraries 3K
Functional tests 18K
Unit tests 20K
Selenium tests 2K
Our Javascript 10K

Harvest also has 32 plugins, 82 gem dependencies, our widgets, mobile apps, and Co-op integration. These are all potential breaking points during an upgrade.

Upgrading to Rails 3

Our general process for rails upgrades is pretty consistent. First make the app load in the limited sense, avoiding occurrences of LoadError whilst starting rake. Focus on the minimum number of changes needed to get rake running, then unit, functional and integration tests.  The goal is to have tests pass, not to convert to the newest API or experiment with new Rails 3 stuff. You only add what must be added to reach the goal.

When encountering a failure or error in tests, check to see if a similar problem could occur in other untested sections of code. For example, an unexpected change in the content type of a generated email is likely to be something affecting all emails. It may have been unreasonable to repeat the same checks for every email. A useful tool here is git grep, it is lightning fast and only checks git tracked code.

During these first upgrade steps Harvest’s UI was not even visible. When we were ready to start working with UI logic, the best tool was our automated Selenium suite. Selenium drives a real browser, clicking, selecting and entering form data just as an actual user would do. The test suite is run with Webkit & Mozilla based browsers to bring out common errors, and lastly in a separate MSIE pass.

No matter how complete your automated testing, a good manual QA session is always an upgrade requirement. To bring some order to the manual QA process, we use a Google Spreadsheet where the app is split into major functionality areas like “Reports / Timesheets”. Each section is populated with generic tasks. Items are checked by two people with a third one responsible for fixing any issues found.

Once the team is sufficiently confident in the new version, we deploy it to production servers and a few select domains. After more QA on production, all accounts are switched over. In spite of careful testing, problems can still occur at this point. If it takes less than one or two hours to clean up any remaining issues, we have done a pretty good job with QA. Realistically, a zero issue Rails upgrade is not possible. Changing the framework of a codebase containing more than 100K lines has ramifications.

The last phase of upgrading is re-factoring to new APIs. New features are written with the new API, any rewrites or periodic UI redesigns will also involve improving the older code. Generally, we don’t have a goal of converting everything to the latest and greatest in one step. This would be an overly disruptive change with little real value. Metalworkers know that sometimes the material needs to cool down before you can do interesting things with it.

Security Improvements & Caveats

The first benefit of Rails 3 is the escape-by-default policy in all HTML templates. This improves security against XSS attacks considerably. The new method is not error proof in spite of numerous claims to the contrary. The biggest problem is rewriting and auditing your own helpers – small bits of ruby code that generate HTML snippets. By default all of these will be escaped so you need to mark each helper as html_safe:

def clear_both
   "<div style='clear:both;'></div>".html_safe
end

Or else they will be double-escaped when rendered. The opportunities for XSS holes in your app are likely to be in helpers after the Rails 3 upgrade. The developer must ensure that anything marked html_safe is indeed safe. For example:

button_to_function("Review and #{value.downcase}",
                   "timesheet.submit_for_full_review()",
                   :id => "submit_button")

May have an XSS vulnerability if value is not safe. Generally in helpers or flash messages the use of string interpolation via #{}, string concatanation (+) or join on an array should be avoided. Escaping HTML is an idempotent operation only in the view templates. Every time you use a native string operation you expose yourself to a potential XSS hole. This is still the case with Rails 3. You should still:

  • Run the app periodically through ratproxy for auditing.
  • Use the app in development periodically with a database where every string of every table was was systematically infected with an XSS test snippet. This stress test results in a messy UI, but missed escaping can be made very visible by a nice HTML alert.
  • Strip all angle brackets from the user input if you have the option.

Changes to Routes

Rails 3 has a new API for specifying routes. The old way was just as expressive, but the new format has been extended with extra features such as redirects, match on domain / subdomain and others. There are automated tools to upgrade to the new DSL, these are useful as a starting point. In the end a manual process yields nicer results.

Third Party Dependencies

Usually these form the hardest part of an upgrade process. For some of the gem/plugin dependencies you will have new Rails 3 compatible versions available. A few are in effect abandoned these should be replaced with other libraries or even removed. Adding a plugin/gem is easy but there are hidden costs, among them painful upgrades. Our love for the plugin/gem system is generally at the lowest point after an upgrade. Sure you cannot internalize everything to your app but in general the fewer dependencies you have the better.

Why You Need Manual QA, or `except(:order)`

With previous Rails releases any :order clause on an ActiveRecord association was overwritten by the custom :order used on invocation. For example:

class Company
  has_many :clients, :order => 'name asc'
end

Defines a company with clients that are returned alphabetically. If for whatever reason you need to order by another field you would:

 company.clients.find(:first, :order => 'id ASC')

Resulting in:

 SELECT * FROM clients ORDER id ASC limit 1;

But the same with Rails 3 ends up being:

 SELECT * FROM clients ORDER name ASC, id ASC limit 1;

To get the equivalent in Rails 3 you need:

 company.clients.except(:order).find(:first, :order => 'id ASC')

Note that I’ve used the old style find(:first) on purpose to highlight the difference. Before the upgrade our automated tests suite did not uncover this subtle difference, hence the need for systematized manual QA process. A human eye, though not as reliable as an automated test suite, is not limited to a pre-programmed series of checks.

Unobtrusive JavaScript

link_to_remote and its friends have been removed, the new API does not generate onclick JavaScript handlers. You just have to specify :remote => true in Rails and have separate JavaScript driver running on the client side to add in Javascript behavior instead of hard coding onclick handlers.

form_for(:expense_category,
         :remote => true,
         :html => {:id => 'add_expense_category_form'},
         :url => expense_categories_path)

Unfortunately some the AJAX-specific options representing callbacks can no longer be specified inline in Rails. There’s no :loaded, :loading:complete, :before in the new rails api but there are :confirm and others. You can still achieve feature parity by hooking into the events triggered by the JavaScript driver. For example if using prototype.js:

document.observe('dom:loaded',function() {
  $('add_expense_category_form').observe('ajax:before', function(){ })
                                .observe('ajax:complete', function(){ });
});

Would enable you to add the missing form specific behavior. It’s a bit more verbose to write in Rails templates, but the resulting HTML looks better. It is also far smaller as it avoids repeating the same verbiage for each AJAX form/link you may have. This method has a jQuery analogue. Rails is no longer prototype.js only.

Helpers with capture() blocks.

What used to be:

<% form_for() do |f| %>
<% end %>

is now:

<%= form_for() do |f| %>
<% end %>

Block helpers now use <%= %> instead of <% %>.

New Mailer API

Mailer methods now have the same feel as Controller actions, instance variables are automatically present in the template. This removes the need for redundant body hashes like:

 body :user => @user, :password => @password

Be sure all mailer views have the appropriate extension .text.erb for text messages and .html.erb for html content. Otherwise your old .erb templates may generate emails with the wrong content type.

There are of course countless small things needing mending during any upgrade. The best tip we can share is: Have a strong test suite and pair it with an intensive QA processes. Remember to keep in mind the awesome new features and abstractions you’ll gain after the upgrade (ARel, Prototype 1.7, more Rack, the list goes on).

Want to tackle Rails upgrades and other technical challenges with us? Harvest is looking for great hackers of all levels to join our team. Check out our Careers section and drop us a note at careers (at) getharvest.com to let us know why you’d like to work with us!

Thoughts or questions about this post? Need some help?
Get in touch →

This was posted in Behind-the-Scenes, Code.
  • Jim Kring on January 6, 2011

    Congrat’s on the upgrade and thanks for the behind-the-scenes look at the process. I’m happy to see that you’ve got lots of (lines of) automated tests :)

  • This a great writeup!
    I would be very interested in seeing a small sample of your QA spreadsheet.

  • Great work upgrading to Rails 3. Having done some rails work myself I can appreciate the process involved in upgrading a Rails application. Congrats on a successful upgrade. :)

  • Great post, didn’t know about the XSS test snippet or .except() for removing scopes on associations. Super helpful!

  • That was very helpful.
    Can do a Part 2 of the transition?

  • @BradM Happy to share a snippet of our informal QA spreadsheet: http://hrv.st/h57sma. In the full spreadsheet each sheet will represent a tab in our app (Timesheets, Invoices, Manage, etc.) We assign a few people to a tab, then rotate for the next round of QA. Overall it’s a fairly seamless process with google docs.

  • Thanks for the write-up, Dee. While you’re in the upgrading mood, you should also check out Selenium 2. :-)

  • Thanks for the tips. Just a quick thought on the new default behavior that treats helpers as unsafe. Perhaps one should refactor some of these html generating helpers into partials, rather than defeat Rails’ XSS protection. Especially with the shortcut syntax for including partials, this shouldn’t be any more difficult than calling a helper from the view.

  • Thanks for the article. Some very useful tips. It’s neat seeing how a well established company goes through these upgrades. It’s also neat to see how some painful aspects of the upgrade are the same no matter your size.

  • Great article! Just curious, how long did it take to do the upgrade? Did you freeze feature development during the upgrade process, or develop new features in concert with upgrading the core code?

  • Barry Hess on January 20, 2011

    @Andrew We started upgrading to Rails 3 in September. We landed the branch in the second week of November. We spent a solid month plus on code work, and nearly as much calendar time (and more person-hours) on testing and fixing.

    We did not pause feature development until the last couple weeks before launch. And even at that, new code was being written but the responsibility of merging to our Rails 3 branch became that of the original developer, not those working on our Rails 3 upgrade. Two big features I can think of launching during the Rails 3 upgrade were the updated web invoice and additional credit card and payment gateway options. Feature development slowed more for practical reasons as people were working on the upgrade rather than new features.

Comments have been closed for this post.
Still have questions? Contact our support team →