Video

Want to see the full-length video right now for free?

Sign In with GitHub for Free Access

Notes

Take a peek behind the scenes at some of features and workflows we've built up to help support Upcase as a production application and manage it across the varied environments of development, staging, and production.

Masquerading as a User

One of the most used support features on Upcase is the ability to "Masquerade" as a user. This functionality allows an admin to essentially sign in as any user, giving them a complete view of the site from the user's point of view. This functionality has proved to be invaluable when working through more nuanced customer support situations, and thankfully is relatively straightforward to add.

Configuring Masquerade Functionality

In essence, when masquerading we save off the admin's id in the session, then sign_in as the user. Beyond that, we provide a few helpers to make it easier to begin and end a "masquerading" session.

To start, we have a section in the Rails admin config that adds a "Masquerade" link to each user's listing, allowing the admin to easily begin masquerading:

# config/initializers/rails_admin.rb
config.model User do
  list do
    field :masquerade do
      pretty_value do
        bindings[:view].link_to(
          'Masquerade',
          bindings[:view].main_app.user_masquerade_path(bindings[:object]),
          method: :post
        )
      end
    end
  end

This link then hits a MasqueradesController, and creates a masquerade. Note, Masquerade is treated as a distinct concept or "resource" in the REST sense, rather than adding a custom action to the UsersController. This aligns with our desire to stick to RESTful conventions as much as possible. You can see more about this in our Weekly Iteration episode on REST.

# app/controllers/admin/masquerades_controller.rb
module Admin
  class MasqueradesController < ApplicationController
    before_filter :must_be_admin

    def create
      session[:admin_id] = current_user.id
      user = User.find(params[:user_id])
      sign_in user
      redirect_to root_path, notice: "Now masquerading as #{user.email}"
    end

    def destroy
      sign_in User.find(session[:admin_id])
      session.delete(:admin_id)
      redirect_to admin_path, notice: "Stopped masquerading"
    end
  end
end
# config/routes.rb
scope module: "admin" do
  resources :users, only: [] do
    resource :masquerade, only: :create
  end
  resource :masquerade, only: :destroy
end

Lastly, we introduce a helper method to determine if we're currently masquerading while rendering, and conditionally add a "Stop Masquerading" link to the header if so:

# app/controllers/application_controller.rb
def masquerading?
  session[:admin_id].present?
end
helper_method :masquerading?
# app/views/layouts/_header_links.html.erb
<%= render "shared/masquerade_link" if masquerading? %>

Be Careful When Masquerading

While masquerading is a surprisingly useful feature, it can be dangerous since you're acting as a user. For the most part, we're careful to not take any actions while masquerading as those actions would affect the user's state, but one additional consideration is analytics. In order to keep our analytics clean, we disable all client side analytics using the masquerading? helper:

# app/views/shared/_javascript.html.erb
<% if can_use_analytics? %>
  <%= render "analytics" %>
<% end %>
# app/helpers/analytics_helper.rb
def can_use_analytics?
  ENV["ANALYTICS"].present? && !masquerading?
end

Environment banner

One of the key features to our development workflow is the use of a staging environment to review features before pushing to production. While critical to keeping production safe and stable, this can lead to confusion as we push up changes and bounce between environments.

In order to help clarify which environment and code we're looking at, we've added an "environment banner". This is a bit of UI that sits at the top of the page on development and staging and lists the environment, branch, and commit currently active. In addition, the banner is colored to help quickly identify the environment. Here's a sample:

environment banner

Again, nothing terribly complex here, but this has proved to be very useful in avoiding the confusion that can come from refreshing staging over and over, wondering why the local changes aren't changing anything!

The implementation consists of a single view partial, included in the application layout, and a few helpers that help gather the data.

# app/views/shared/_environment_banner.html.erb
<% unless Rails.env.production? %>
  <div class="environment-banner <%= Rails.env %>">
    <%= Rails.env %> | <%= "#{current_branch} @ #{current_sha}" %>
  </div>
<% end %>
module EnvironmentBannerHelper
  def current_branch
    if git_available?
      `git rev-parse --abbrev-ref HEAD`.chomp
    else
      ENV.fetch("CURRENT_BRANCH", "--branch-not-found--")
    end
  end

  def current_sha
    if git_available?
      `git log --oneline -1`
    else
      ENV.fetch("CURRENT_SHA", "--sha-not-found--")
    end
  end

  def git_available?
    to_dev_null = "> /dev/null 2>&1"
    system("which git #{to_dev_null} && git rev-parse --git-dir #{to_dev_null}")
  end
end

The only complex bit is that on Heroku, we actually don't have access to the git repo, and thus we can't dynamically determine these values. Luckily, CircleCI has access to them, and we can have it set ENV vars and use those rather than the live value.

# bin/deploy
if printenv | grep CIRCLE_BRANCH > /dev/null; then
  branch=$CIRCLE_BRANCH
else
  branch="$(basename "$(git symbolic-ref HEAD)")"
fi

Console Tricks

Similar to wanting to differentiate between the various environments in the browser, it's perhaps even more important to have this context when working in the console. It would be a big problem if we accidentally deleted production data, thinking we were working in a local console.

Prompt label

In order to make this easier, we use the wonderful parity set of commands. Parity provides the production, staging, and development wrappers for heroku CLI, making it very easy to interact with the different environments. You can see a bit more on the usage of parity in this Weekly Iteration episode about Heorku.

In order to help clarify which environment our console is in, we've added coloring and a label to our prompt:

prompt labels

We configure this via the .pryrc file, as shown below:

# .pryrc
color_escape_codes = {
  black: "\033[0;30m",
  red: "\033[0;31m",
  green: "\033[0;32m",
  yellow: "\033[0;33m",
  blue: "\033[0;34m",
  purple: "\033[0;35m",
  cyan: "\033[0;36m",
  reset: "\033[0;0m"
}

env_colors = {
  "development" => color_escape_codes[:white],
  "test" => color_escape_codes[:purple],
  "staging" => color_escape_codes[:yellow],
  "production" => color_escape_codes[:red],
}

if defined? Rails
  Pry.config.prompt = proc do |obj, nest_level, _|
    color = env_colors.fetch(Rails.env, color_escape_codes[:reset])
    colored_environment_name = "#{color}#{Rails.env}#{color_escape_codes[:reset]}"
    "(#{colored_environment_name}) #{obj}:#{nest_level}> "
  end
end

eval(File.open(".irbrc").read)

Helper methods

Lastly, we configure custom console helper methods by defining them in our .irbrc (which we source from the .pryrc).

# .irbrc
def find_user(email)
  User.find_by_email(email)
end

This is a very single example, but you can add more complex and even environment specific methods that will only be available in the console.

Sending data to scripts

Lastly, we have a custom script which we've configured to run against the bin/rails runner command, allowing us to pipe data into the command on staging and production.

You can check out the script referenced in the video for the full details on it, but the key feature is the shebang line at the top:

#!./bin/rails runner

The shebang line allows us to use the local rails runner command, even on Heroku! (If you're note sure what a shebang line is, check out the Weekly Iteration episode on building a CLI).

This means that we can pipe local data up to the script running on Heroku, with the piped data ending up as the stdin of the command. This means we can even provide extra options and flags! Our pipes extend into the cloud!

$ cat local-file.md | staging run bin/create-data.rb --with-args