GA4 Add-to-Cart Tracking for Fonteva: A Practical Starter

by Garrett Nafzinger

Organizations that use Fonteva, such as associations, nonprofits, and membership groups, sell registrations, memberships, and products through a cart, even if the site doesn’t feel like a typical store.

GA4’s e-commerce events are the best way to measure this activity and send conversions to Google Ads, LinkedIn, or your ad platform of choice. The problem is that Fonteva doesn’t send GA4 events by default, and the software is a single-page app (SPA), which means “fire on /cart” pageview rules don’t work.

This guide shows how to implement GA4’s add_to_cart in Google Tag Manager (GTM) without changing Fonteva’s code. We watch page elements and SPA route changes, send GA4’s recommended parameters, and avoid duplicates. It’s a first step toward full-funnel tracking (view_cart, begin_checkout, purchase).

The ideal approach in the long term is a first-party data layer in Fonteva or Lightning, but this gets you moving now.

Glossary

  • SPA (Single-Page App): The site loads once and then swaps screens with JavaScript instead of loading new pages.
  • Route: The screen an SPA shows. In Fonteva, the route usually includes everything after the # in the URL, for example #/store/browse/detail/….
  • SPA Hop: A screen change inside the app without a full page load. You see the #… route change.
  • Cart Modal (Mini-Cart): A pop-up that appears on the product page immediately after an add. It confirms the add and offers View Cart or Checkout.
  • Stash: A temporary snapshot of the item a user is adding (id, name, price, quantity). We save it at click time to know exactly what the user selected.
  • Clear: This is the step where we immediately remove the stashed snapshot after sending GA4 so later actions do not double-count.
  • Session Storage (sessionStorage): Small, per-tab storage in the browser that disappears when the tab closes. It’s perfect for our short-lived, per-interaction stash.

Why This Works for Fonteva

  1. Fonteva often keeps users on the product route and opens a cart modal after an add. If you only watch for a cart URL, you miss the real add moment or count it late.
  2. We stash the selection on click to keep the correct item, price, and quantity even if the DOM changes.
  3. We fire add_to_cart when the cart modal is visible, which is the first reliable signal that the add succeeded.
  4. We clear the stash immediately to prevent duplicate fires when someone later clicks View Cart or navigates into the cart.

What We’ll Build

  • Variables that assemble GA4-ready items[], value, and a simple pending flag.
  • Two small HTML tags will be used to stash the item at click time and clear it after sending GA4.
  • A GA4 Event tag for add_to_cart.
  • Triggers that are SPA-safe: Element Visibility for the cart modal and History Change as a fallback.

Everything below is ordered for a clean implementation and QA path. Names are suggestions; consistency helps.

One-Time GTM Updates

Enable these in GTM → Variables → Configure:

  • Click Element, Click Text, Click Classes, Click Target
  • New History Fragment, Old History Fragment, New/Old History State, History Source
  • Page URL, Page Path, Page Hostname, Event

Note: The page URL does not include the hash. Use the New History Fragment to test routes like #/store/browse/detail/…

Step 1: Create Variables

1) Currency (Constant)

  • Name: Currency – Constant
  • Value: USD
  • Reason: GA4 expects a currency with monetary events.

2) Pending Flag (To Check if in The Middle Of An Add?)

Custom JavaScript variable JS – Fonteva Pending Add (bool):

function(){
  try { return sessionStorage.getItem('fonteva_pending_add') === '1'; }
  catch(e){ return false; }
}

Reason: Gates the modal and history triggers so they only fire when a product add just started.

3) Product Items Array (for 

add_to_cart

)

Custom JavaScript variable JS – Fonteva Items Array:

function () {
  try {
    var frag = (location.hash || '').replace(/^#/, '');
    if (/^\/store\/(cart|checkout)(\/|$)/i.test(frag)) { return []; }

    var s = sessionStorage.getItem('fonteva_item');
    if (s) { var o = JSON.parse(s); if (o && o.item_id) return [o]; }
  } catch (e) {}

  var nameEl = document.querySelector('.pfm-detail_discount_label');
  var rawName = nameEl ? nameEl.textContent.trim() : (document.title || '(unknown)');
  var parts = rawName.split(' - ');
  var variant = parts.length > 1 ? parts.pop().trim() : undefined;
  var baseName = parts.length ? parts.join(' - ').trim() : rawName;

  var priceEl = document.querySelector('.pfm-detail_discount_price .FrameworkCurrencyField') ||
                document.querySelector('.pfm-details_sub_price .FrameworkCurrencyField');
  var price;
  if (priceEl && priceEl.textContent) {
    var p = priceEl.textContent.replace(/[^0-9.]/g, '');
    price = p ? parseFloat(p) : undefined;
  }

  var qty = 1;
  var selects = Array.prototype.slice.call(
    document.querySelectorAll('.pfm-detail_quantity select.slds-select')
  );
  if (selects.length) {
    var sel = selects.find(function(s){ return s.offsetParent !== null; }) || selects[selects.length-1];
    var v = (sel.value || '').trim();
    if (/^other$/i.test(v)) {
      var wrap = sel.closest('.pfm-detail_quantity') || document;
      var inp = wrap.querySelector('input[type="number"], input[type="text"]');
      if (inp && inp.value) {
        var n = inp.value.replace(/[^0-9]/g, '');
        qty = n ? parseInt(n, 10) : 1;
      }
    } else {
      var n2 = v.replace(/[^0-9]/g, '');
      qty = n2 ? parseInt(n2, 10) : 1;
    }
  }

  var idMatch = location.href.match(/\/(?:detail|merch_new)\/([^/?#]+)/i);
  var itemId = idMatch && idMatch[1] ? decodeURIComponent(idMatch[1])
                                     : baseName.replace(/\s+/g, '_').toLowerCase();

  var item = { item_id: itemId || '(unknown)', item_name: baseName || '(unknown)', quantity: qty };
  if (typeof price === 'number') item.price = price;
  if (variant) item.item_variant = variant;
  return [item];
}

Reason: GA4’s add_to_cart expects items[]. We provide a single object for the item being added.

4) Product Value (Price × Quantity)

Custom JavaScript variable JS – Fonteva Value:

function () {
  try {
    var frag = (location.hash || '').replace(/^#/, '');
    if (/^\/store\/(cart|checkout)(\/|$)/i.test(frag)) { return undefined; }
  } catch(e) {}

  try {
    var s = sessionStorage.getItem('fonteva_item');
    if (s) {
      var o = JSON.parse(s);
      var q = parseInt(o && o.quantity, 10) || 1;
      var p = (o && typeof o.price === 'number') ? o.price : undefined;
      if (typeof p === 'number' && isFinite(p)) {
        var v = p * q;
        return Math.round(v * 100) / 100;
      }
    }
  } catch(e) {}

  try {
    var priceEl = document.querySelector('.pfm-detail_discount_price .FrameworkCurrencyField') ||
                  document.querySelector('.pfm-details_sub_price .FrameworkCurrencyField');
    var price;
    if (priceEl && priceEl.textContent) {
      var num = priceEl.textContent.replace(/[^0-9.]/g, '');
      price = num ? parseFloat(num) : undefined;
    }
    var qtyVar = {{JS - Fonteva Quantity}}; // insert via GTM picker if present
    var q2 = parseInt(qtyVar, 10) || 1;
    if (typeof price === 'number' && isFinite(price)) {
      return Math.round(price * q2 * 100) / 100;
    }
  } catch(e) {}

  return undefined;
}

Reason: GA4 monetization reports and Ads imports rely on a numeric value.

Step 2: Add Two Small HTML Tags

A) GA4 – Product Stash (Custom HTML)

Attach to the product button click trigger in the next step.

<script>
(function () {
  try {
    var now = Date.now();
    var lastTs = parseInt(sessionStorage.getItem('fonteva_item_ts') || '0', 10);
    var pending = sessionStorage.getItem('fonteva_pending_add') === '1';
    if (pending && (now - lastTs) < 800) { return; }

    var itemArr = {{JS - Fonteva Items Array}};
    if (!Array.isArray(itemArr) || !itemArr[0]) return;
    var o = itemArr[0];

    o.quantity = parseInt(o.quantity || '1', 10) || 1;
    if (typeof o.price === 'string') {
      var p = o.price.replace(/[^0-9.]/g, '');
      o.price = p ? parseFloat(p) : undefined;
    }

    sessionStorage.setItem('fonteva_item', JSON.stringify(o));
    sessionStorage.setItem('fonteva_pending_add', '1');
    sessionStorage.setItem('fonteva_item_ts', String(now));
  } catch (e) {}
})();
</script>

Reason: We snapshot the user’s selection at click time to send the correct data when the modal confirms the add.

B) GA4 – Product Clear (Custom HTML)

No trigger of its own; we will sequence it after the GA4 event.

<script>
try {
  sessionStorage.removeItem('fonteva_item');
  sessionStorage.removeItem('fonteva_pending_add');
  sessionStorage.removeItem('fonteva_item_ts');
} catch(e){}
</script>

Reason: Clearing immediately prevents double-counting when someone later opens the cart.

Step 3: Create Triggers

1) Product Add Button (For Stash Only)

Trigger name: Click – Add to Cart – General

Type: Click – All Elements → Some Clicks

Click Element matches CSS selector:

.pfm-details button[data-name="addToCart"],
.pfm-details button.FrameworkButton[aria-label="Add to Order"],
.pfm-details button[data-label="Add to Order"],
.pfm-details button[data-name="addToCart"] *,
.pfm-details button.FrameworkButton[aria-label="Add to Order"] *

Reason: We only stash here. No GA4 events fire on this click.

2) Cart Modal Visible (ensures the add to cart completes and is captured)

Trigger name: Add to Cart – Cart Modal Visible

Type: Element Visibility

  • Element selector: .addToCartModal .LTEShoppingCart, .slds-modal__content .LTEShoppingCart
  • Fire: Once per element
  • Minimum percent visible: 1
  • Minimum on-screen duration: 200 ms
  • Observe DOM changes: checked
  • Some visibility events (AND):
    • New History Fragment contains /store/browse/detail/
    • JS – Fonteva Pending Add (bool) equals true

Reason: Many Fonteva adds happen without a route change. The modal confirms the add succeeded.

3) History Fallback (safety net, de-dupe)

Trigger name: Add to Cart – History

Type: History Change → Some History Changes

  • History Source equals hashchange
  • New History Fragment matches RegEx ^/store/(cart|checkout)(/|$)
  • JS – Fonteva Pending Add (bool) equals true

Reason: It caters to edge cases where a route change confirms the addition. Restricting to hashchange prevents duplicate fires.

Step 4: Build The GA4 Event

Tag name: GA4 – add_to_cart

Type: GA4 Event

  • Event name: add_to_cart
  • Parameters:
    • items → {{JS – Fonteva Items Array}}
    • value → {{JS – Fonteva Value}}
    • currency → {{Currency – Constant}}
  • Triggers (OR):
    • Add to Cart – Cart Modal Visible
    • Add to Cart – No History
  • Tag Sequencing:
    • Fire a tag after → GA4 – Product Clear
    • Setup tag → none

In GA4 → Admin → Events, mark add_to_cart as a Key Event and import it into Ads platforms.

QA Checklist

  • On the product page, select a quantity that is not one and click Add to Order. Only GA4 – Product Stash should fire.
  • When the cart modal appears, GA4 – add_to_cart should fire on the Element Visibility event, then GA4 – Product Clear.
  • GA4 DebugView should show add_to_cart with a populated items[0] object, a numeric value, and currency set to USD.
  • Clicking View Cart should not fire another add_to_cart.

If the GA4 tag does not fire on the modal, open the Element Visibility event, click the tag, and use Why Not. Confirm the selector matches your modal, and the New History Fragment contains /store/browse/detail/. Ensure Observe DOM changes is checked.

What This Unlocks

  • Real add-to-cart data in GA4 Monetization reports, ready to mark as a Key Event and sync to Google Ads and LinkedIn.
  • A pattern you can extend to view_cart and begin_checkout using cart-level variables, and purchase on the confirmation page with a transaction id (ideally via a data layer push for accuracy).

Why This Matters

For membership-driven organizations, registrations and dues often fund the mission. GA4 is powerful, but only when standard events flow in with item-level detail. This GTM build gets you trustworthy add-to-cart data without a development sprint and is fully compatible with a future first-party data layer.

Recap — Everything We Added

  • Variables
    • Currency – Constant (USD)
    • JS – Fonteva Pending Add (bool)
    • JS – Fonteva Items Array
    • JS – Fonteva Value
  • Tags
    • GA4 – Product Stash (Custom HTML)
    • GA4 – add_to_cart (GA4 Event)
    • GA4 – Product Clear (Custom HTML; sequenced after GA4 event)
  • Triggers
    • Click – Add to Cart – General (stash only)
    • Add to Cart – Cart Modal Visible (Element Visibility)
    • Add to Cart – History

If you want this implemented, QA’d, or documented for your team, we can help.