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).