Seeing Through Your Customers' Eyes with Customer Impersonation
Any website with customer support will eventually face an issue that is difficult for the customer to explain or difficult to understand without seeing what they are seeing. With the pretender gem we can log in to the users account and see exactly what they are seeing.

Trying to get support for an issue you're experiencing as a customer is often painful and slow, with the pain being compounded when the only way to contact support is via email. We can improve the experience by reducing the work the customer needs to do to fully articulate their issue. Without being able to see the issue a user is experiencing support agents must collect a lot of information and clarify every last detail to ensure they are understanding the customer correctly. We can reduce the amount of information required by giving the support agents the ability to log into the customers account. In this article I will be using pretender to allow support agents to log in as other users. Pretender works with any authentication system - I will be using devise
Entering impersonation mode
First let's make sure we have all the gems defined in our Gemfile:
gem 'devise'
gem 'pundit'
gem 'pretender'
We also need to add a snippet into any controllers that we would like the customer support agents to be able to use while in impersonation mode. Let's add it to the ApplicationController so we can impersonate all areas of our application:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
impersonates :user
end
We need a controller to allow support agents to log in and out of customer impersonation:
class CustomerImpersonationsController < ApplicationController
before_action :authenticate_customer_support_agent!
def create
user = User.find!(params[:id])
impersonate_user(user)
redirect_to root_path
end
def destroy
# current_user is the impersonated user before we call
# stop_impersonating_user so we should build the notice here or store the
# current_user in a variable so we can fetch values from it later
notice = "You are logged out as #{current_user.email}"
stop_impersonating_user
redirect_to root_path, notice: notice
end
private def authenticate_customer_support_agent!
# If we're already impersonating a user, true_user will be the customer
# support agent
#
# If we're not currently impersonating a user, current_user will be the
# customer support agent
return if (true_user || current_user)&.customer_support_agent?
raise ActionController::RoutingError, 'Not Found'
end
end
Don't forget to add the routes:
Rails.application.routes.draw do
resource :customer_impersonation, only: %i[create destroy]
end
Now we need to add a form for the admins to sign in, we could do this in a couple of different ways.
- Give the customer support agent a form where they can select and submit the user.
- Create a button which uses the POST method to submit the users ID.
Most applications I've worked with have an admin page where you can view all of the registered users in a paginated table so we'll use go with option 2. It's really simple to implement thanks to Ruby on Rails' built in link_to method. We can tell the link_to method to use the POST method and then pass the user's ID to the path.
<table>
<thead>
<tr>
<td>Name</td>
<td>Email</td>
<td></td>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td>
<%= link_to('Show', user) %>
<%= link_to(
'Impersonate',
customer_impersonation_path(id: user.id),
method: :post
) %>
</td>
</tr>
<% end %>
</tbody>
</table>
If you have multiple administrative roles you might want to consider creating a pundit policy to ensure only administrators with the correct roles are able to impersonate other users. Now that we are able to impersonate a user we also need to be able to stop impersonating the user. We already have the route to do this and the controller action. We need to create a link for the impersonators so they can actually log out. We also need to remove the normal logout link otherwise the admin will be logged out of their account. To create a seamless UI, we can simply replace the existing logout link with the new stop impersonation link.
<% if user_signed_in? %>
<%= link_to(
'Log out',
true_user ? customer_impersonation_path : destroy_user_session_path,
method: :delete
) %>
<% end %>
UI Considerations
There are a few things I highly recommend to protect your customers from accidental actions taken by your support agents. Your customers likely expect privacy so you may wish to enact a policy of requesting permission to log into their account before allowing agents to do so. You could do this via chat, email, or create a system to request permission that is recorded in the application. I would also highly recommend logging all activity that happens while impersonating. Creating an audit trail is a good way to protect yourself from the inevitable accident that will happen in the future. With an audit trail you will be able to validate the correct action to take when the customer raises the issue. In our applications we go even further and log every request that a customer support agent is taking. Let's add that to the ApplicationController
bin/rails generate model AuditLog data:jsonb
``` ```ruby
class AuditLog < ApplicationModel
belongs_to :user
end
``` ```ruby
class User
has_many :audit_logs
end
``` ```ruby
class ApplicationController < ActionController::Base
before_action :authenticate_user!
after_action :log_impersonated_activity, if: :true_user
impersonates :user
private def log_impersonated_activity
return unless true_user
true_user.audit_logs.create!(
data: {
controller: params[:controller],
action: params[:action],
method: request.method,
headers: headers,
params: params,
customer_id: current_user.id,
customer_email: current_user.email,
admin_id: true_user.id,
admin_email: true_user.email
}
)
end
end
I would also recommend restricting the actions the customer support agents can take. We typically use pundit for authorization, but unfortunately pundit isn't built in a way that expects two user objects. We can override the pundit_user to tell pundit where to find the user object. This means we can pass in a hash with the current_user and the true_user and update our ApplicationPolicy to reflect this change.
class ApplicationController
private def pundit_user
{ current_user: current_user, true_user: true_user }
end
end
``` ```ruby
class ApplicationPolicy
attr_reader :user, :true_user, :record
def initialize(current_and_true_user, record)
@user = current_and_true_user[:current_user]
@true_user = current_and_true_user[:true_user]
@record = record
end
protected def impersonating_customer?
user.present? && user != true_user
end
end
Now in your other policies you can give different permissions while in impersonation mode. e.g. prevent customer support agents from making payments
class PaymentPolicy < ApplicationPolicy
def create?
!impersonating_customer?
end
end
The last thing I think is important is making it very obvious that the support agent is in impersonation mode, and add some additional protections against accidental actions. First we can add a warning banner to the page when a customer support agent is in impersonation mode
<% if true_user.present? %>
<div class='banner danger warning'>
You are signed in as <%= current_user.name %>.
<%= link_to 'Stop impersonating', cutomer_impersonations_path(id: current_user.id), method: :delete %>
</div>
<% end %>
Then we can add confirmations to all the important actions:
<%= f.submit 'Save', data: { confirm: "Are you sure you want to save this item for #{current_user.name}'s account" } %>
I recommend disabling any actions that should be protected:
<% if true_user %>
<span class='disabled_impersonation'>Link account</span>
<% else %>
<%= link_to 'Link account', link_account_path %>
<% end %>
That about wraps up this guide. We've touched on everything we need to allow our customer support agents to log into our customers accounts.


