I had a client who’s app was getting feedback some of the data returned from an external API wasn’t what was expected. After pouring over logs and trying to pinpoint the problem, I decided that we needed to get screen captures of the offending results. Asking the end-users of the application to go through that process manually would likely end up not getting us what we needed though. Time to automate it!

The app’s backend in written in Rails. I imagine that any back end would do for this however; it could even be serverless. Our goals were:

capture a screenshot of the application without user interaction

display a form asking for a brief and long description

on submit, capture the information and email it off to the right people

Let’s dive into the backend of it!

First I needed an endpoint to post my form, so I made an entry in config/routes.rb:

post '/feedback' => 'feedback#create', constraints: { format: 'json' }

Then I created the FeedbackController :

class FeedbackController < ApplicationController



def create

img = feedback_params[:img]

subj = feedback_params[:subject]

desc = feedback_params[:description]

FeedbackMailer.email_admins(img, subj, desc).deliver_later

render json: nil, status: :ok

end



protected



def feedback_params

params.require(:feedback).permit(%i[img subject description])

end

end

Pretty self explanatory I imagine. Grab the params from the request, and send them to my FeedbackMailer class I created with rails g mailer Feedback .

class FeedbackMailer < ApplicationMailer



def email_admins(img, subj, desc)

@img = img

@subject = subj

@description = desc

mail(to: ENV['FEEDBACK_EMAIL_RECEIVER'], subject: 'Sorting Hat feedback')

end

end

This particular app had never sent emails before, so we needed to grab a responsive layout template for it. Since it is aimed at internal users, we wanted it to look good but it didn’t need to win design awards. A good starter template can be found here: https://github.com/leemunroe/responsive-html-email-template

For Rails, I simply removed the starter copy from this template, added a <%= yield %> erb tag, and we were ready to rock.

For the actual email guts we are basically spitting out the parameters that the FeedbackMailer received:

<h1><%= @subject %></h1>



<p><%= @description %></p>



<p>Here's a picture of what they had on screen:</p>



<small>(note this tool does not capture product images)</small>

<img src="<%= @img %>" alt="screenshot">

The final step for development is to make sure we can mock getting emails out, and to do that I used the excellent mailcatcher gem. Then in config/environments/development.rb I added the SMTP configuration for catching emails:

# gem install mailcatcher

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

config.action_mailer.delivery_method = :smtp

config.action_mailer.smtp_settings = { address: 'localhost', port: 1025 }

For production we want to do a couple of things. It’s important to process emails asynchronously, because the end-user would end up waiting on the server to process sending an email and who knows if that would even work! We chose to use sidekiq and Redis to handle background jobs, and that required a few entries in our config/environments/production.rb :

# Use a real queuing backend for Active Job (and separate queues per environment)

config.active_job.queue_adapter = :sidekiq

config.active_job.queue_name_prefix = "appname_#{Rails.env}"

config.action_mailer.perform_caching = false config.action_mailer.default_url_options = { host: 'myapp.com' }

config.action_mailer.delivery_method = :smtp

config.action_mailer.smtp_settings = {

address: ENV['SMTP_HOST'],

port: ENV['SMTP_PORT'],

username: ENV['SMTP_USER'],

password: ENV['SMTP_PASSWORD']

}

That’s our backend setup! Nothing to crazy or non-standard for a Rails application.

For the frontend we wanted to add a button, which would capture what was rendered in the <body> and present a modal. Time for some HTML!

<button class="btn btn-secondary" id="feedback">

<span id="feedbackSpinner" class="spinner"></span>

Feedback

</button>

And our modal:

<div id="feedbackModal" class="modal" tabindex="-1" role="dialog">

<div class="modal-dialog" role="document">

<div class="modal-content">

<div class="modal-header">

<h5 class="modal-title">Send Feedback</h5>

<button type="button" class="close" data-dismiss="modal" aria-label="Close">

<span aria-hidden="true">×</span>

</button>

</div>

<div class="modal-body">

<p>Let us know if something is incorrect, needs improvement, or any other general feedback about Sorting Hat.</p>

<div class="form-group">

<label for="subject">Summary</label>

<input id="subject" type="text" class="form-control" placeholder="Brief description, name, email, etc.">

</div>

<div class="form-group">

<label for="description">Description</label>

<textarea id="description" row="3" class="form-control" placeholder="What were you searching for? What was expected? What happened?"></textarea>

</div>

</div>

<div class="modal-footer">

<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>

<button id="sendFeedback" type="button" class="btn btn-primary">Send Feedback</button>

</div>

</div>

</div>

</div>

Alright pretty standard bootstrap’d modal and button components here. Now for the Javascript. We found a great library called html2canvas to help us capture the screen. It’s based on Promises, which made it super easy to delay showing the modal until the screen grab completed. Because that would not be ok to get a picture of a modal all the time. html2canvas can capture anything by selector name, making it very flexible for even image manipulation tools or any other crazy function you can imagine. In our case we are just posting the data uri of the screen grab, since there is no reason to store screen captures on our server for any long term period.

(function ($) {

$(document).on('turbolinks:load', function () {



let img = null;



function sendFeedback() {

let subject = $('input#subject').val()

let description = $('input#description').val()

$.ajax({

beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},

url: '/feedback',

method: 'post',

data: {

feedback: {

subject: subject,

description: description,

img: img

}

},

success(e) {

$feedbackModal.modal('hide')

tmpl = $("#alert").html();

$.results.html(tmpl({error: "Thanks! We received your feedback and will get back to you soon.", type: 'success'}));

}

})

}

$.itemSpinner = Handlebars.compile($('#item-spinner').html());

function startItemSpinner(el) {

$(el).html($.itemSpinner);

}



function stopItemSpinner(el) {

$(el).html('');

}



let $feedbackModal = $('#feedbackModal').modal({show: false});



$('button#feedback').click(function (evt) {

evt.preventDefault();

startItemSpinner('#feedbackSpinner');

html2canvas(document.body).then(function (canvas) {

img = canvas.toDataURL();

stopItemSpinner('#feedbackSpinner');

$feedbackModal.modal('show')

});

})



$('button#sendFeedback').click(sendFeedback)

})

})(jQuery);

Yes gasp it’s using jQuery, and the alert and spinner is this bit of handlebars template:

<script id="item-spinner" type="text/x-handlebars-template">

<i class="fa fa-spin fa-cog"></i>

</script> <script id="alert" type="text/x-handlebars-template" charset="utf-8">

<div class="alert alert-{{type}} alert-dismissible fade show" role="alert">

<button type="button" class="close" data-dismiss="alert" aria-label="Close">

<span aria-hidden="true">×</span>

</button>

{{error}}

</div>

</script>

Feel free to modify to your liking. This particular app we have these libs in place, and there isn’t a reason to build a huge isomorphic node.js ui around it. Fit to your needs!

The main bit of logic is this:

html2canvas(document.body).then(function (canvas) {

img = canvas.toDataURL();

stopItemSpinner('#feedbackSpinner');

$feedbackModal.modal('show')

});

I found it could take 1–2 seconds for the screen grab to process. Just long enough that we needed to provide some feedback that it was working. So we showed a spinner until the modal displayed. Then we have a handler for the ‘Send Feedback’ button to send an AJAX call to our controller, and kick off the process. No need for multipart for here, because we are using the canvas.toDataUR() method that gives us a URI representation of the image.

That’s all our code. You should start up mailcatcher, and see if you get feedback!

Want to give me some feedback? Have questions about implementing in other language or variations? Send me a message! Thanks for reading!