Spree Commerce Analytics Implementation | OpsBlu Docs

Spree Commerce Analytics Implementation

Install tracking scripts using Deface overrides, build data layers with ERB templates, and configure e-commerce tracking on Spree Commerce.

Analytics Architecture on Spree Commerce

Spree Commerce is built on Ruby on Rails, which means analytics integration follows Rails conventions. The key injection points are:

  • content_for :head -- Rails content block for injecting scripts into the <head> section of the layout
  • Application layout (app/views/spree/layouts/spree_application.html.erb) -- the main layout template wrapping all storefront pages
  • Deface overrides -- Spree's recommended method for modifying views without editing core templates directly
  • Spree extensions (gems) -- packaged analytics integrations installable via Bundler
  • ERB templates -- standard Rails templates where Ruby variables are available for data layer construction
  • ActiveRecord callbacks -- server-side hooks on order state transitions for backend event tracking

Spree's modular architecture separates the frontend, backend, API, and core into individual gems. Analytics code targets the spree_frontend gem's views.


Installing Tracking Scripts

Using content_for :head

The simplest approach is adding scripts via the :head content block in a Deface override or custom view:

# app/overrides/add_analytics_to_head.rb
Deface::Override.new(
  virtual_path: 'spree/layouts/spree_application',
  name: 'add_gtm_to_head',
  insert_before: '</head>',
  text: <<-HTML
    <script>
    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXX');
    </script>
  HTML
)

Using a Spree Extension

Create a dedicated analytics extension:

spree extension analytics_tracking

In the extension's app/views/spree/shared/_analytics.html.erb:

<script async src="https://www.googletagmanager.com/gtag/js?id=<%= Spree::Config[:google_analytics_id] %>"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '<%= Spree::Config[:google_analytics_id] %>');
</script>

Then render it via a Deface override:

Deface::Override.new(
  virtual_path: 'spree/layouts/spree_application',
  name: 'add_analytics_partial',
  insert_before: '</head>',
  partial: 'spree/shared/analytics'
)

Direct Layout Editing

If not using Deface, edit the layout directly:

<%# app/views/spree/layouts/spree_application.html.erb %>
<head>
  <%= yield :head %>
  <!-- Analytics scripts -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
  ...
</head>

Data Layer Implementation

Rails ERB templates have full access to Spree's models and controllers, making data layer construction straightforward.

Product Page Data Layer

Create a Deface override targeting the product show page:

# app/overrides/product_data_layer.rb
Deface::Override.new(
  virtual_path: 'spree/products/show',
  name: 'product_data_layer',
  insert_bottom: '[data-hook="product_show"]',
  text: <<-HTML
    <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      'event': 'view_item',
      'ecommerce': {
        'items': [{
          'item_id': '<%= @product.sku || @product.id %>',
          'item_name': '<%= j @product.name %>',
          'price': <%= @product.price.to_f %>,
          'item_category': '<%= j @product.taxons.first&.name.to_s %>',
          'currency': '<%= current_currency %>'
        }]
      }
    });
    </script>
  HTML
)

Category/Taxon Page Data Layer

# app/overrides/taxon_data_layer.rb
Deface::Override.new(
  virtual_path: 'spree/taxons/show',
  name: 'taxon_data_layer',
  insert_bottom: '[data-hook="taxon_products"]',
  text: <<-HTML
    <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      'event': 'view_item_list',
      'ecommerce': {
        'item_list_name': '<%= j @taxon.name %>',
        'items': [
          <% @products.each_with_index do |product, index| %>
          {
            'item_id': '<%= product.sku || product.id %>',
            'item_name': '<%= j product.name %>',
            'price': <%= product.price.to_f %>,
            'index': <%= index %>
          }<%= ',' unless index == @products.length - 1 %>
          <% end %>
        ]
      }
    });
    </script>
  HTML
)

E-commerce Tracking

Order Confirmation Page

Target the spree/checkout/complete or spree/orders/show view:

# app/overrides/purchase_data_layer.rb
Deface::Override.new(
  virtual_path: 'spree/orders/show',
  name: 'purchase_tracking',
  insert_bottom: '[data-hook="order_details"]',
  text: <<-HTML
    <script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      'event': 'purchase',
      'ecommerce': {
        'transaction_id': '<%= @order.number %>',
        'value': <%= @order.total.to_f %>,
        'tax': <%= @order.tax_total.to_f %>,
        'shipping': <%= @order.ship_total.to_f %>,
        'currency': '<%= @order.currency %>',
        'items': [
          <% @order.line_items.each_with_index do |item, index| %>
          {
            'item_id': '<%= item.sku || item.variant.sku %>',
            'item_name': '<%= j item.name %>',
            'price': <%= item.price.to_f %>,
            'quantity': <%= item.quantity %>
          }<%= ',' unless index == @order.line_items.length - 1 %>
          <% end %>
        ]
      }
    });
    </script>
  HTML
)

Server-Side Event Tracking with ActiveRecord Callbacks

For server-side conversion tracking (Meta CAPI, Google Ads Enhanced Conversions), use Spree's state machine callbacks:

# app/models/spree/order_decorator.rb
module Spree
  module OrderDecorator
    def self.prepended(base)
      base.state_machine.after_transition to: :complete, do: :send_conversion_event
    end

    def send_conversion_event
      # Send to Meta Conversions API
      ConversionEventJob.perform_later(
        event_name: 'Purchase',
        order_id: number,
        value: total.to_f,
        currency: currency,
        email: email
      )
    end
  end
end

Spree::Order.prepend(Spree::OrderDecorator)

Add to Cart Tracking

Override the line items controller or use a Deface override on the cart form:

# app/overrides/add_to_cart_tracking.rb
Deface::Override.new(
  virtual_path: 'spree/products/show',
  name: 'add_to_cart_tracking',
  insert_bottom: '[data-hook="cart_form"]',
  text: <<-HTML
    <script>
    document.querySelector('form#add-to-cart-form')?.addEventListener('submit', function() {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        'event': 'add_to_cart',
        'ecommerce': {
          'items': [{
            'item_id': '<%= @product.sku || @product.id %>',
            'item_name': '<%= j @product.name %>',
            'price': <%= @product.price.to_f %>,
            'quantity': parseInt(document.querySelector('#quantity').value) || 1
          }]
        }
      });
    });
    </script>
  HTML
)

Common Issues

Deface overrides not applying -- Ensure the virtual_path matches the actual view path in your Spree version. Run rake deface:test_selector to verify selectors. Deface overrides are loaded from app/overrides/ and must end in .rb.

Turbolinks interfering with tracking -- Spree 3.x+ uses Turbolinks, which prevents full page reloads. Analytics scripts that fire on DOMContentLoaded will only fire once. Use turbolinks:load instead:

document.addEventListener('turbolinks:load', function() {
  // Push dataLayer events here
});

Currency mismatch -- @product.price returns the price in the store's default currency. If the store supports multiple currencies, use @product.price_in(current_currency) to get the correct value.

Order data not available on confirmation -- Spree's checkout flow uses a state machine. If the order view does not expose @order, check that the controller properly loads it. In some Spree versions, the confirmation page is at /orders/:number and requires authentication.

ERB injection security -- Always use j (alias for escape_javascript) when injecting Ruby strings into JavaScript to prevent XSS. For numeric values, use .to_f or .to_i to ensure clean output.


Platform-Specific Considerations

Spree version differences -- Spree 3.x, 4.x, and the Spark (hosted) version have significant architectural differences. View paths, Deface selector targets, and available model methods change between versions. Always check documentation for your specific version.

Headless Spree with Storefront API -- If using Spree as a headless backend with a decoupled frontend (React, Next.js, etc.), Deface overrides do not apply. Instead, implement analytics in your frontend framework and use the Spree Storefront API for order/product data.

Extension gems for analytics -- Community gems like spree_analytics_trackers provide Google Analytics and Facebook Pixel integration out of the box. Check compatibility with your Spree version before installing.

Spree::Config for settings -- Store analytics IDs in Spree::Config rather than hardcoding them. This allows admin-level configuration changes without code deployments:

# config/initializers/spree.rb
Spree.config do |config|
  config.google_analytics_id = ENV['GA_MEASUREMENT_ID']
end

Test with Spree sandbox -- Spree provides a sandbox generator (rails g spree:install --sample) that creates a store with sample data. Use this to validate your analytics implementation before deploying to production.