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.