Home
Form Abandonment Tracker
📝

Form Abandonment Tracker

Conversion Rate Optimization

Comprehensive form interaction tracking that monitors field-level engagement, identifies abandonment patterns, and provides detailed analytics for conversion rate optimization and lead generation improvement.

CROIntermediateCRO & Analytics

Key Features

Field-level interaction tracking
Abandonment pattern analysis
Time-based abandonment detection
Multi-form support
Custom abandonment triggers
Conversion funnel analytics

Script Overview

📖

What It Does

This script monitors form interactions in real-time, tracking when users start filling forms, which fields they complete, and when they abandon the process. Essential for identifying friction points in your conversion funnel.

🎯

Use Case

Essential for identifying friction points in lead generation forms and checkout processes. Perfect for optimizing conversion rates and understanding where users drop off in your forms.

Key Benefits

  • Identify form abandonment patterns
  • Track field-level completion rates
  • Optimize form conversion rates
  • Understand user behavior friction points
  • Improve lead generation performance
  • Data-driven form optimization insights

JavaScript Code

<script>
// Form Abandonment Tracker Script
// Version: 1.0
// Last Updated: 2025-09-09

(function() {
  'use strict';
  
  var config = {
    // Time before considering form abandoned (milliseconds)
    abandonmentTime: 30000, // 30 seconds
    
    // Minimum interaction time before tracking (milliseconds)
    minInteractionTime: 2000, // 2 seconds
    
    // Form selectors to track
    formSelectors: 'form',
    
    // Field types to track
    trackedFields: ['input', 'textarea', 'select'],
    
    // Fields to exclude from tracking
    excludeFields: ['[type="hidden"]', '[type="submit"]', '[type="button"]'],
    
    // Events configuration
    events: {
      formStart: 'form_interaction_start',
      formAbandonment: 'form_abandoned',
      formSubmit: 'form_submitted',
      fieldInteraction: 'form_field_interaction'
    },
    
    debug: false
  };

  var trackedForms = {};
  var formTimers = {};

  function debugLog(message, data) {
    if (config.debug) {
      console.log('[Form Tracker] ' + message, data || '');
    }
  }

  function getFormIdentifier(form) {
    return form.id || form.name || form.className || 'form-' + Array.prototype.indexOf.call(document.forms, form);
  }

  function getFieldInfo(field) {
    return {
      name: field.name || field.id || 'unnamed',
      type: field.type || field.tagName.toLowerCase(),
      placeholder: field.placeholder || '',
      required: field.required || false
    };
  }

  function initFormTracking(form) {
    var formId = getFormIdentifier(form);
    
    if (trackedForms[formId]) return; // Already tracking
    
    trackedForms[formId] = {
      form: form,
      startTime: null,
      fields: {},
      interactions: 0,
      isAbandoned: false,
      isSubmitted: false
    };

    var fields = form.querySelectorAll(config.trackedFields.join(','));
    var excludeSelector = config.excludeFields.join(',');
    
    Array.prototype.forEach.call(fields, function(field) {
      if (excludeSelector && field.matches(excludeSelector)) return;
      
      var fieldInfo = getFieldInfo(field);
      trackedForms[formId].fields[fieldInfo.name] = {
        info: fieldInfo,
        interacted: false,
        completed: false,
        focusTime: null,
        totalFocusTime: 0
      };

      // Add event listeners
      field.addEventListener('focus', function() {
        handleFieldFocus(formId, fieldInfo.name);
      });

      field.addEventListener('blur', function() {
        handleFieldBlur(formId, fieldInfo.name, field);
      });

      field.addEventListener('input', function() {
        handleFieldInput(formId, fieldInfo.name, field);
      });
    });

    // Form submit tracking
    form.addEventListener('submit', function() {
      handleFormSubmit(formId);
    });

    debugLog('Initialized tracking for form:', formId);
  }

  function handleFieldFocus(formId, fieldName) {
    var formData = trackedForms[formId];
    if (!formData) return;

    var fieldData = formData.fields[fieldName];
    if (!fieldData) return;

    fieldData.focusTime = Date.now();
    
    // Track first interaction with form
    if (!formData.startTime) {
      formData.startTime = Date.now();
      trackFormEvent(config.events.formStart, formId, {
        fields_count: Object.keys(formData.fields).length
      });
      
      // Start abandonment timer
      startAbandonmentTimer(formId);
    }

    formData.interactions++;
    fieldData.interacted = true;

    debugLog('Field focused:', { form: formId, field: fieldName });
  }

  function handleFieldBlur(formId, fieldName, field) {
    var formData = trackedForms[formId];
    if (!formData) return;

    var fieldData = formData.fields[fieldName];
    if (!fieldData || !fieldData.focusTime) return;

    var focusTime = Date.now() - fieldData.focusTime;
    fieldData.totalFocusTime += focusTime;
    fieldData.focusTime = null;

    // Check if field is completed
    var isCompleted = field.value && field.value.trim().length > 0;
    if (isCompleted && !fieldData.completed) {
      fieldData.completed = true;
      
      trackFormEvent(config.events.fieldInteraction, formId, {
        field_name: fieldName,
        field_type: fieldData.info.type,
        focus_time: focusTime,
        total_focus_time: fieldData.totalFocusTime,
        completed: true
      });
    }

    debugLog('Field blurred:', { 
      form: formId, 
      field: fieldName, 
      focusTime: focusTime,
      completed: isCompleted 
    });
  }

  function handleFieldInput(formId, fieldName, field) {
    var formData = trackedForms[formId];
    if (!formData) return;

    // Reset abandonment timer on input
    resetAbandonmentTimer(formId);
  }

  function handleFormSubmit(formId) {
    var formData = trackedForms[formId];
    if (!formData) return;

    formData.isSubmitted = true;
    clearAbandonmentTimer(formId);

    var completedFields = 0;
    var totalFocusTime = 0;

    Object.keys(formData.fields).forEach(function(fieldName) {
      var fieldData = formData.fields[fieldName];
      if (fieldData.completed) completedFields++;
      totalFocusTime += fieldData.totalFocusTime;
    });

    var completionTime = Date.now() - formData.startTime;

    trackFormEvent(config.events.formSubmit, formId, {
      completion_time: completionTime,
      total_focus_time: totalFocusTime,
      completed_fields: completedFields,
      total_fields: Object.keys(formData.fields).length,
      completion_rate: Math.round((completedFields / Object.keys(formData.fields).length) * 100)
    });

    debugLog('Form submitted:', { 
      form: formId, 
      completionTime: completionTime,
      completedFields: completedFields 
    });
  }

  function startAbandonmentTimer(formId) {
    clearAbandonmentTimer(formId); // Clear existing timer
    
    formTimers[formId] = setTimeout(function() {
      handleFormAbandonment(formId);
    }, config.abandonmentTime);
  }

  function resetAbandonmentTimer(formId) {
    startAbandonmentTimer(formId); // Restart timer
  }

  function clearAbandonmentTimer(formId) {
    if (formTimers[formId]) {
      clearTimeout(formTimers[formId]);
      delete formTimers[formId];
    }
  }

  function handleFormAbandonment(formId) {
    var formData = trackedForms[formId];
    if (!formData || formData.isSubmitted || formData.isAbandoned) return;

    formData.isAbandoned = true;

    var completedFields = 0;
    var interactedFields = 0;
    var totalFocusTime = 0;

    Object.keys(formData.fields).forEach(function(fieldName) {
      var fieldData = formData.fields[fieldName];
      if (fieldData.completed) completedFields++;
      if (fieldData.interacted) interactedFields++;
      totalFocusTime += fieldData.totalFocusTime;
    });

    var abandonmentTime = Date.now() - formData.startTime;

    trackFormEvent(config.events.formAbandonment, formId, {
      abandonment_time: abandonmentTime,
      total_focus_time: totalFocusTime,
      completed_fields: completedFields,
      interacted_fields: interactedFields,
      total_fields: Object.keys(formData.fields).length,
      completion_rate: Math.round((completedFields / Object.keys(formData.fields).length) * 100),
      interaction_rate: Math.round((interactedFields / Object.keys(formData.fields).length) * 100)
    });

    debugLog('Form abandoned:', { 
      form: formId, 
      abandonmentTime: abandonmentTime,
      completedFields: completedFields 
    });
  }

  function trackFormEvent(eventName, formId, additionalData) {
    window.dataLayer = window.dataLayer || [];
    
    var eventData = {
      event: eventName,
      form_id: formId,
      form_url: window.location.href
    };

    if (additionalData) {
      Object.keys(additionalData).forEach(function(key) {
        eventData['form_' + key] = additionalData[key];
      });
    }

    window.dataLayer.push(eventData);
    debugLog('Form event tracked:', eventData);
  }

  function initAllForms() {
    var forms = document.querySelectorAll(config.formSelectors);
    
    Array.prototype.forEach.call(forms, function(form) {
      initFormTracking(form);
    });

    debugLog('Initialized tracking for ' + forms.length + ' forms');
  }

  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initAllForms);
  } else {
    initAllForms();
  }

  // Handle dynamically added forms
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.type === 'childList') {
        Array.prototype.forEach.call(mutation.addedNodes, function(node) {
          if (node.nodeType === 1) {
            if (node.tagName === 'FORM') {
              initFormTracking(node);
            } else if (node.querySelector) {
              var forms = node.querySelectorAll(config.formSelectors);
              Array.prototype.forEach.call(forms, initFormTracking);
            }
          }
        });
      }
    });
  });

  observer.observe(document.body, { childList: true, subtree: true });

  // Expose utility functions
  window.formAbandonmentTracker = {
    getTrackedForms: function() {
      return trackedForms;
    },
    reinitialize: function() {
      initAllForms();
    }
  };

})();
</script>

💡 Pro Tip: Copy the entire code block above and paste it directly into a GTM Custom HTML tag. The script is self-contained and ready to use immediately.