Boosting sales with browser cache!?!

Andrea Longhi

7 Jun 2019 Development, Solidus, eCommerce

Andrea Longhi

3 mins
How to fix a caching issue on your eCommerce

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!

You may also like

Let’s redefine
eCommerce together.