Boosting sales with browser cache!?!

Recently one of our customers informed us that they were getting weird numbers from Facebook and Google in regards to order purchase amounts on their Solidus based eCommerce.
Facebook and Google reported multiple order confirmations with the same order number, the actual order total amounts were from 2 to 6 times lower than what was reported by them.
These numbers looked very nice on paper 📈 and could boost some distracted egos (people are buying plenty of stuff, we’re doing much better than expected!!! 💵🤑💰) but unfortunately they were so inaccurate to be pretty useless. Houston, we have a problem 👨🚀.
The first step was to check how Google and Facebook get that data: some JavaScript code that is supposed to be executed only once sends all tracking information when the customer is redirected to the order page after completing the checkout.
How do we inject the JS code only once? By leveraging a feature in Solidus frontend
codebase: a convenient flash
message is set after the order is finalized, and it can
be used as a flag in order to trigger the tracking libraries.
What follows is the relevant code in Solidus codebase. Everything starts
in #update
, then #finalize_order
is called and eventually the message is set in
#set_successful_flash_notice
:
# solidus/frontend/controllers/spree/checkout_controller.rb
def update
if update_order
assign_temp_address
unless transition_forward
redirect_on_failure
return
end
if @order.completed?
finalize_order
else
send_to_next_state
end
else
render :edit
end
end
private
def finalize_order
@current_order = nil
set_successful_flash_notice
redirect_to completion_route
end
def set_successful_flash_notice
flash.notice = t('spree.order_processed_successfully')
flash['order_completed'] = true
end
And this is the code in our customer’s eCommerce that uses the flash
message:
<% if flash['order_completed'] %>
<%= render "facebook_tracking" %>
<%= render "google_remarketing" %>
<%= render "adwords" %>
<% end %>
The usual suspect for such issues is caching. If the page is cached (either on the server or the client) then the HTML (JS code included) is not fresh and will trigger the tracking events multiple times, one time for each page view.
Testing locally and on the staging environment (which happens to mirror production in regards to cache settings and such) confirmed that events are correctly triggered only once: the expected JS code is present on the first page load but is not on subsequent visits.
Soon we were able to confirm the issue with Safari on iOS. If you complete the order and close the browser/restart the device then, when you reopen Safari, the page is reloaded from the cache, not from the web.
The next step was to check the page HTTP headers. This can be done easily and
quickly from the command line using curl -I <page-url>
.
The response included these results:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/html
Let’s get some help from MDN website in regards to the Cache-Control: private
directive:
[private] Indicates that the response is intended for a single user and must not be stored by a shared cache. A private cache may store the response.
So, this directive does not completely exclude caching.
The fix was easy: customize the HTTP headers to completely avoid caching. This was
done with regular Ruby on Rails code by extending the controller
OrdersController
in Solidus frontend with a decorator:
module OrdersController
module EditActionCacheHeaders
def self.prepended(base)
base.before_action :set_no_cache_headers, only: :show
end
private
def set_no_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
Spree::OrdersController.prepend self
end
end
When changing Solidus default behavior, it’s better to do it in a decorator that extends the
default functionality with prepend
rather than reopening and patching its methods directly.
This solution allows for easier maintenance, more clarity of purpose and composition.
The order page is managed by OrdersController#show
action, which we don’t need to modify
at all as adding a new before_action
suffices to achieve the goal.
The first line of #set_no_cache_headers
method is the most relevant, as it instructs all
recent browsers (HTTP/1.1) to absolutely not cache and store the page for any reason.
The second line response.headers['Pragma'] = 'no-cache'
targets older browsers (HTTP/1.0)
that can accept only the Pragma
header, while the third line set the Expires
header well in
the past, as an extra safeguard just in case the previous headers failed their purpose.
before saying goodbye let me share a much cited quote from Phil Karlton:
There are only two hard things in Computer Science: cache invalidation and naming things.
Have a good one!