Skip to content
Jun 2 13

Programmatically List Routes/Paths from inside your Rails App

by cory
This post explains how to write code that can introspect a Rails app at runtime and determine what routes have been defined. Similar to `rake routes`, but something that you can use from inside your rails app.

(If you would like to skip ahead and just see the code, here is the gist.)

Why would you want to do this? Say you are creating a new rails site. You start with a home page and as the site gets larger you add other top-level paths like "/about" and "/team". This app is a fancy new color-picker and so you add a whimsically-named page "/rainbow", where you educate your users about light and color. Some time passes, and you decide you want to add a top-level page for every user at "/:username". Things hum along and then you launch the site. One day a user signs up with her preferred username "rainbow".

Now your site has two "/rainbow" pages: one is your static educational page. The other is this new user's page. This is a problem. If you were able to introspect your app's routes at the time that you validate a new user, you could have noticed that the requested username conflicted with an existing path and prevented the creation of that user.

Let's figure out how to do this. We'll start by looking at the source code for the rake routes task. The important line is this one:

all_routes = Rails.application.routes.routes
Let's inspect this code on the rails console to get a better idea of what we are looking at here.
>> Rails.application.routes.class
ActionDispatch::Routing::RouteSet
>> Rails.application.routes.routes.class
Journey::Routes
So our variable `all_routes` is an instance of a `Journey::Routes` class. What's journey? Journey is the router that Rails uses (and unfortunately its README is a bit terse, so there's not much more I can say about that). If we look at the code for Journey::Routes we see some enumerator-type methods (`each`, `last`, `length`). This is meant to be iterated over, once for each route. (If you look carefully you'll see that it also includes an `attr_reader` named `:routes`, so we could get really crazy here and access the routes as an array using: `Rails.application.routes.routes.routes`. But that's just painful). When we inspect each element of the `Journey::Routes` object, we see that each one is, as expected, an instance of `Journey::Route`.

Now we know how to get the routes, using the same code that `rake routes` does. Next step is to figure out how to determine the actual path that a `Journey::Route` object represents.

At this point, looking at the code in Journey isn't all that helpful. There are a lot of avenues we might explore here. Each `Journey::Route` has a number of potentially-promising attributes and methods, such as `path`, `name`, `segments` and `parts`, but each one just takes us deeper down the rabbit hole and the path (no pun intended) from here to a string representing this path is still elusive. Let's look again at the `rake routes` task, since we know that this task does indeed turn the routes to strings to display on the console.

Looking at the rake task again, we see the following lines, culminating in one that `puts`-es the routes to the console:

  require 'action_dispatch/routing/inspector'
  inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
  puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER'])
So let's look at `ActionDispatch::Routing::RoutesInspector`. Hmm, this `path` method looks promising:
  def path
    super.spec.to_s
  end
It's not entirely clear here what `super` refers to since we are looking at the `RouteWrapper` class here and its parent is a SimpleDelegator, but it is reasonable to expect that its parent delegates to a `Journey::Route`. So let's go back to the rails console and try something out with that in mind. The call to `super` from `RouteWrapper#path` simply becomes a call to `path` on the `Journey::Route`:

>> routes_set = Rails.application.routes.routes
...
>> first_route = routes_set.first
...
>> puts first_route.path.spec.to_s
"/assets"
Bingo! You can try this in your own rails app:
>> Rails.application.routes.routes.each do |route|
>>    puts route.path.spec.to_s
>> end;nil
Great! Now all that's left to do is to iterate through those routes and extract the initial path segment from each one. Here is the code:



To use it within your app, do this:
top_level_paths = \
  RouteRecognizer.new.initial_path_segments
May 27 13

A short lesson in priorities, courtesy of the MTA

by cory
If you are one of the millions of of people who ride the MTA's Metro North trains every year, you've probably bought a ticket at one of its ticket machines. After choosing your station and other information, the screen prints your tickets and they drop into the metal tray at the bottom for you to collect.

While the tickets are printing, the screen displays this message - "Your ticket(s) are being printed."

This little hedge strikes me every time. An organization as large as the MTA, with an annual user base (ridership) in the millions (76.9 million in 2006), with a software/hardware project that, presumably, was contracted out for millions of dollars, and no one felt it important enough to get this part of the experience — a part that *every* customer experiences — perfect. No one put in a spec somewhere that the message must be "Your ticket is being printed" if it is one ticket and "Your tickets are being printed" otherwise.

Millions of dollars. Millions of users. And: "Your ticket(s) are being printed."

It's a great lesson in stopping at "good enough." I've worked with many startups that get hung up on small details like this. When perfection is achievable it's easy to feel that it should be a priority. But if an organization like the MTA with millions of users and millions of dollars can live with "Your ticket(s) are being printed," maybe your startup can live with "good enough," too.
Dec 31 12

Thoughts on New Year's Resolutions

by cory

Like most people, I have historically sucked at keeping my new year's resolutions. I used to be heavily goal-oriented around late December and (when I'm procrastinating -- bad start already!) early January, but recently my thinking on goals and goal-setting has, as a politician would say, evolved. I'm approaching this a little bit differently this year. Here are some of my thoughts on how to make good goals and, if you're still inclined after reading to the end, new year's resolutions as well.

For some context and an alternative viewpoint, skim Leo Babauta's "No Goals" post. Then, if you have time, watch him debate goals with Tim Ferris for some counter-points.

Until a few weeks ago, I wasn't planning on making any resolutions at all this year. I was down with the No Goals manifesto. But I've changed my thinking and instead I am going to work on introducing some small habits and align them with the goals that those habits reinforce.

Make SMART Goals

See Paul J. Meyer's SMART goals definitions. It's an acronym, and the letters stand for Specific, Measurable, Attainable (within your abilities to achieve), Relevant (this the least important component, imo) and Time-Bound.

For example, "Lose weight" is a bad goal. How much weight? That's not specific, and it's not time-bound.

"Lose 10 pounds": Better, but not good enough yet. There's no time-limit.

"Lose 10 pounds by the end of January." Ok, now we are talking. The best goal is one where a friend of yours can objectively say whether or not you succeeded. In this case they'd need to see you stand on a scale but you get the idea -- if it's not something that an external person can say that you've completed, the goal is too ambiguous or subjective. If your friend can see the scale and it shows 10 fewer pounds than when you started, and it's not February yet, they can give you a big high five and say that you are #winning.

Use short time frames. AKA ABR (Always Be Resolving)

read more...
Oct 30 12

How Target Sells More By Showing You Ads For Things It Knows You Won't Buy

by cory

Lots of great stuff in Charles Duhigg's NYT article about consumer data mining and habits, but one of the things that stuck out the most to me is how Target will deliberately insert ads into a weekly circular for items it knows the intended recipients have no interest in buying. And this allows them to sell more!

"Then we started mixing in all these ads for things we knew pregnant women would never buy, so the baby ads looked random. We’d put an ad for a lawn mower next to diapers. We’d put a coupon for wineglasses next to infant clothes. That way, it looked like all the products were chosen by chance. And we found out that as long as a pregnant woman thinks she hasn’t been spied on, she’ll use the coupons. She just assumes that everyone else on her block got the same mailer for diapers and cribs. As long as we don’t spook her, it works."

There's so much interesting stuff about the irrationality of humans that we're only starting to unpack now.

Jun 14 12

On-boarding New Users (Class Notes)

by cory
Saw a good class at Ben Chow about on-boarding new users. My unfiltered notes:

Understanding: It's not enough to just explain your product. You have to 'take their hands' and show them
Tips:
1 grab hands and guide them
-- big arrow pointing to what they should do
-- Progress bar
2 sell your value proposition
3 get personal -- give personalized info/results as fast as possible
4 invite friends and make it make sense (give users a reason to invite)
5 help set up
6 measure performance, optimize funnel

What do you want the user to do every time they use the product? The users want the same thing!

Tell ppl what you do, how you do it, and tell the how they will use it. Think of all the reasons someone would say no and not use it, and preemptively address those

the thing we want to do is build habit. Get ppl coming back frequently. And every time they come back, show them something awesome

Videos do a great job informing but they are passive. Users need to start clicking and doing to build those habits. Not a big believer in using videos in the on-boarding flow because they take a lot of time. The user should move very quickly. Keep them moving and doing.

5-second rule: you have the users attention for 5 seconds. Give them something interesting to do/click every 5 seconds while on-boarding...each time they click, they are giving you another 5 seconds of their attention span.

enemies:
Confusion, apathy, and time
If they're confused, if they're bored, if they are not getting something quick, they're gone
Most users who are trying your app want to love it. So give them something to love.

"how does this product fit in my life?" contextualize for your users. Explain how you fit, how best they can use, etc

Get contact info as soon as possible. Eg get their email before their CC. That way if you lose someone before they complete a purchase you can contact them and offer a free trial or otherwise get another shot at converting them

Email marketing: back off the frequency every time they don't click on your email, otherwise you risk them unsubscribing.