Want to see the full-length video right now for free?
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.
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.
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 create
s 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][].
[Weekly Iteration episode on REST]: https://thoughtbot.com/upcase/videos/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? %>
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
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][]
[environment banner]: https://thoughtbot-images.s3.amazonaws.com/upcase/weekly-iteration/env-banner.png
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
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.
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][]
[prompt labels]: https://thoughtbot-images.s3.amazonaws.com/upcase/weekly-iteration/environment-specific-consoles.png
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)
[parity set of commands]: https://github.com/thoughtbot/parity [this Weekly Iteration episode about Heorku]: https://thoughtbot.com/upcase/videos/heroku-tips-and-tricks
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.
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
[the script referenced in the video]: https://github.com/thoughtbot/upcase/blob/2f41c34971cfc253d552f27c15c3ea0cbc1ae6b1/bin/create-notes-and-markers.rb [Weekly Iteration episode on building a CLI]: https://thoughtbot.com/upcase/videos/lets-build-a-cli