πŸ“˜ Step-by-Step Learning Guide

GTM & GA4 E-Commerce Tracking Tutorial

All 8 GA4 e-commerce events are already implemented in this store. Your job: read the real code, watch it fire in your browser, connect it to Google Tag Manager β€” then break and extend it with the exercises below.

Also available as markdown in TRACKING_TUTORIAL.md in the project root.

How to use this sandbox

  1. Watch (5 min). Open DevTools console (F12), click through the store, and watch [tracking] log lines appear.
  2. Read (20 min). Work through the 8 events below β€” each links to the exact file and element that produces it.
  3. Connect (20 min). Point the app at your GTM container (Β§5) and watch your clicks arrive in GTM Preview mode.
  4. Practice. Do the exercises at the bottom. They teach more than reading ever will.
1

The Big Picture

Your click (e.g. "Add to cart")
        ↓
Rails view or Stimulus controller builds a payload
        ↓
window.dataLayer.push({ event: "add_to_cart", ecommerce: { ... } })
        ↓
GTM trigger fires on the custom event name
        ↓
GTM tag forwards the payload to GA4 (or Ads, Meta Pixel, …)

dataLayer is a plain JS array β€” the only contract between your site and GTM. Your code pushes objects in; GTM reads them out.

Two mechanisms produce events here β€” learn to tell them apart:

  • Server-rendered pushes for page views (view_item_list, view_item, view_cart, begin_checkout, purchase). The view calls the ga4_data_layer helper (app/helpers/tracking_helper.rb), which renders a small <script> with JSON built via to_json β€” no hand-written string interpolation.
  • Browser-interaction pushes for clicks and submits (select_item, add_to_cart, remove_from_cart, plus virtual_page_view). One Stimulus controller (app/javascript/controllers/tracking_controller.js, attached to <body>) reads product facts from data-* attributes and pushes on click / submit.
2

The 8 Events β€” What Fires Them

Event Fires when… Code lives in
view_item_list Catalog loads (incl. filter / search) products/index.html.erb
select_item A product card or title is clicked tracking_controller.js #selectItem
view_item Product detail page loads products/show.html.erb
add_to_cart Any add-to-cart form is submitted tracking_controller.js #addToCart
view_cart Cart page loads with items carts/show.html.erb
remove_from_cart Trash button in the cart is clicked tracking_controller.js #removeFromCart
begin_checkout Checkout page loads checkouts/new.html.erb
purchase Order confirmation page loads orders/show.html.erb

Example A: server-rendered push (view_item)

From app/views/products/show.html.erb:

<%= ga4_data_layer("view_item",
      currency: TrackingHelper::CURRENCY,
      value: @product.price.to_f,
      items: [ ga4_product_item(@product) ]) %>

Example B: browser-interaction wiring (select_item)

From app/views/products/index.html.erb β€” product facts travel in plain data-* attributes on .product-card:

<%= link_to product_path(product), class: "product-image-link",
            data: { action: "click->tracking#selectItem" } do %>

Try it: open the catalog, right-click a product card β†’ Inspect. Find the data-product-* attributes, then click the card and watch the [tracking] select_item line in the console.

3

Two Rules That Prevent 90% of Bugs

Rule 1 β€” always clear ecommerce first.

GTM keeps the last ecommerce object in memory. Push a new event without clearing and fields bleed across events. Both mechanisms in this app push { ecommerce: null } first β€” find it in tracking_helper.rb and in tracking_controller.js#push.

Rule 2 β€” the item schema.

{
  item_id: "AUDIO-ANC-001",      // required β€” SKU
  item_name: "Aura Headphones",  // required β€” name
  item_category: "Audio",        // recommended
  price: 199.99,                 // Number, not a string!
  quantity: 1                    // Number, not a string!
}

Strings for price/quantity is the most common beginner mistake β€” GA4 silently drops the revenue. Notice the parseFloat/parseInt calls in the Stimulus controller: HTML attributes are always strings.

4

The Turbo Gotcha

Rails uses Turbo Drive: link clicks and form submits swap the <body> via AJAX without a full page reload. GTM's built-in Page View trigger fires only on the first load β€” every later navigation is invisible to it.

How this app handles it: a virtual pageview.

The tracking controller listens for Turbo navigations and pushes a synthetic virtual_page_view event with location, path and title. In GTM, trigger page-view tags on that custom event β€” or use GTM's History Change trigger (Turbo uses the History API, so it works too).

5

Connect Your GTM Container

  1. Create a container at tagmanager.google.com (platform Web) and copy the ID (GTM-XXXXXXX).
  2. Point the app at it β€” no code edits needed, the layout reads the environment:
    GTM_CONTAINER_ID=GTM-XXXXXXX bin/rails server
  3. In GTM click Preview, enter http://localhost:3000, and shop in the connected tab.
  4. Create one trigger (Custom Event, regex enabled):
    ^(view_item_list|select_item|view_item|add_to_cart|view_cart|remove_from_cart|begin_checkout|purchase)$
  5. Create one tag (GA4 Event, event name {{Event}}, Send Ecommerce data from Data Layer), attach your Measurement ID and the trigger. Save and Publish.
  6. Shop again in Preview: each event appears on the left, and the Data Layer tab shows currency, value and items.
6

Verify Like a Pro β€” Console Recipes

Most debugging needs no GTM at all. DevTools (F12) β†’ Console, paste:

dataLayer                                  // everything pushed so far
dataLayer.filter(e => e.event)             // only real events
dataLayer.filter(e => e.event === "purchase")
dataLayer.at(-1)                           // the most recent push

Plus the app's own debug log: every push prints a [tracking] <event> line (see tracking_controller.js#log; disable with data-tracking-debug-value="false" on <body>).

✎

Exercises β€” Do These, In Order

  1. Watch the funnel. Console open, go catalog β†’ product β†’ add to cart β†’ cart β†’ checkout β†’ place order. Check off all 8 events plus virtual_page_views. You learn: what fires when.
  2. Find the clear. Locate ecommerce: null in app/helpers/tracking_helper.rb and in tracking_controller.js#push. You learn: the one line that prevents data bleeding.
  3. Add a coupon. Hard-code coupon: "WELCOME10" into the purchase payload in app/views/orders/show.html.erb, place an order, and confirm it in dataLayer.at(-1). You learn: the server-rendered path.
  4. Track search. In app/views/products/index.html.erb, push a view_search_results event (with search_term: @search_query) only when @search_query is present. Search for "hoodie" and confirm it fires alongside view_item_list. You learn: adding a new event end to end.
  5. Break it on purpose. Comment out the { ecommerce: null } push in the helper, view a product, then open the cart. Inspect the view_cart payload β€” stale items bleeding in? Restore the line. You learn: why Rule 1 exists, by seeing the bug yourself.
≑

Quick Reference

Payload buildersapp/helpers/tracking_helper.rb
Clicks + virtual pageviewsapp/javascript/controllers/tracking_controller.js
GTM snippet wiringapp/views/layouts/application.html.erb
Cart math (free ship β‰₯ $75, 8% tax)app/models/cart.rb
Reset demo databin/rails db:seed (or footer button)
Inspect databin/rails console β†’ Product.count, Order.last