Skip to content

Instantly share code, notes, and snippets.

@ceaksan
Created April 17, 2026 20:39
Show Gist options
  • Select an option

  • Save ceaksan/65245a3b07a0a7707891867f578540d9 to your computer and use it in GitHub Desktop.

Select an option

Save ceaksan/65245a3b07a0a7707891867f578540d9 to your computer and use it in GitHub Desktop.
Cart Abandonment State Machine - funnel-aware client-side abandonment detector with grace period, BFCache support and cross-tab sync.
/**
* Cart Abandonment State Machine
*
* Funnel-aware client-side cart abandonment detector. Distinguishes
* real abandonment (cart -> homepage, cart -> blog, cart -> exit)
* from normal in-funnel behavior (cart -> product, cart -> checkout).
*
* Features:
* - Regex-based strict path matching per URL type
* - Grace period to swallow fast re-entries (e.g. cart -> product -> cart)
* - Whitelist of checkout steps (login, shipping, payment) as non-abandon
* - BFCache re-run via pageshow event
* - Cross-tab sync: purchase in tab B cancels pending abandonment in tab A
*
* Configure CONFIG.paths regex groups for your platform (Ticimax, IdeaSoft,
* T-Soft, Shopify, custom).
*
* Install via: GTM Custom HTML tag, trigger = All Pages / DOM Ready.
* On abandonment the code pushes { event: 'cart_abandonment', ... } to
* dataLayer; wire a GTM Custom Event trigger to your remarketing tags.
*
* @see https://ceaksan.com/tr/sepet-terk-takibi-javascript
* @license MIT
*/
(function () {
"use strict";
// Platform configuration: adjust regexes to match your store URLs
var CONFIG = {
paths: {
cart: /^\/(sepet|cart|basket)(\/|$)/i,
checkout: [
/^\/(odeme|checkout|payment)(\/|$)/i,
/^\/(teslimat|shipping|adres)(\/|$)/i,
/^\/(uyelik|login|uye-giris)(\/|$)/i,
],
orderSuccess:
/^\/(siparis-sonuc|siparis-onay|siparis-basarili|siparistamamlandi|thank[-_]?you)(\/|$)/i,
product: /^\/(urun|product|p)\//i,
},
storageKey: "cart_journey",
crossTabKey: "cart_sync",
graceDelay: 5000,
maxSessionAge: 30 * 60 * 1000,
};
var STATES = {
IN_CART: "in_cart",
BROWSING: "browsing_from_cart",
CHECKOUT: "checkout",
PENDING: "pending_abandonment",
};
function getState() {
try {
var raw = sessionStorage.getItem(CONFIG.storageKey);
if (!raw) return null;
var parsed = JSON.parse(raw);
if (Date.now() - parsed.lastUpdated > CONFIG.maxSessionAge) {
sessionStorage.removeItem(CONFIG.storageKey);
return null;
}
return parsed;
} catch (e) {
return null;
}
}
function setState(data) {
data.lastUpdated = Date.now();
sessionStorage.setItem(CONFIG.storageKey, JSON.stringify(data));
}
function clearState() {
sessionStorage.removeItem(CONFIG.storageKey);
// Notify other tabs so they cancel any pending abandonment
try {
localStorage.setItem(
CONFIG.crossTabKey,
JSON.stringify({ action: "purchase_complete", t: Date.now() }),
);
} catch (e) {}
}
function getPageType(path) {
if (CONFIG.paths.orderSuccess.test(path)) return "order";
if (CONFIG.paths.cart.test(path)) return "cart";
if (
CONFIG.paths.checkout.some(function (p) {
return p.test(path);
})
)
return "checkout";
if (CONFIG.paths.product.test(path)) return "product";
return "other";
}
function fireEvent(state) {
// Re-check: another tab may have cleared the state
var current = getState();
if (!current || current.state !== STATES.PENDING) return;
var timeInCart = Date.now() - (state.entryTime || Date.now());
function push() {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "cart_abandonment",
abandonmentContext: {
timeInCartMs: timeInCart,
fromCheckout: current.fromCheckout || false,
exitPage: window.location.pathname,
},
});
}
// Defer if GTM hasn't loaded yet
if (window.google_tag_manager) {
push();
} else {
setTimeout(push, 1000);
}
sessionStorage.removeItem(CONFIG.storageKey);
}
function handleTransition() {
var path = window.location.pathname;
var pageType = getPageType(path);
var state = getState();
var now = Date.now();
// Order success: clear everything
if (pageType === "order") {
clearState();
return;
}
// Cart page: start or refresh state
if (pageType === "cart") {
setState({
state: STATES.IN_CART,
entryTime: state ? state.entryTime : now,
lastCartTime: now,
});
return;
}
// Checkout step: in funnel, not abandonment
if (pageType === "checkout") {
if (
state &&
(state.state === STATES.IN_CART || state.state === STATES.BROWSING)
) {
setState({
state: STATES.CHECKOUT,
entryTime: state.entryTime,
checkoutEntryTime: now,
});
}
return;
}
// Outside cart/checkout
if (!state) return;
// Cart -> product: start grace period
if (state.state === STATES.IN_CART && pageType === "product") {
setState({
state: STATES.BROWSING,
entryTime: state.entryTime,
lastCartTime: state.lastCartTime,
browseStartTime: now,
});
return;
}
// Product -> product: still browsing
if (state.state === STATES.BROWSING && pageType === "product") {
setState(Object.assign({}, state, { lastUpdated: now }));
return;
}
// Real abandonment scenarios:
// 1. cart -> other (non-product)
// 2. browsing -> other (didn't return to cart)
// 3. checkout -> other (broke funnel)
var isAbandonment =
(state.state === STATES.IN_CART && pageType === "other") ||
(state.state === STATES.BROWSING && pageType === "other") ||
state.state === STATES.CHECKOUT;
if (isAbandonment) {
setState({
state: STATES.PENDING,
entryTime: state.entryTime,
fromCheckout: state.state === STATES.CHECKOUT,
});
// Grace period: swallows fast back-navigation
setTimeout(function () {
fireEvent(state);
}, CONFIG.graceDelay);
}
}
// Cross-tab sync: purchase in another tab cancels pending abandonment
window.addEventListener("storage", function (e) {
if (e.key === CONFIG.crossTabKey && e.newValue) {
try {
var data = JSON.parse(e.newValue);
if (data.action === "purchase_complete") {
sessionStorage.removeItem(CONFIG.storageKey);
}
} catch (err) {}
}
});
// BFCache: re-run when restored from back/forward cache
window.addEventListener("pageshow", function (e) {
if (e.persisted) {
setTimeout(handleTransition, 0);
}
});
// Kickoff
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", handleTransition);
} else {
handleTransition();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment