Skip to content
  • There are no suggestions because the search field is empty.

Advanced Funnel Tracking in Convert.com: Mastering Multi-Step Forms (SPAs)

Robust JavaScript strategies to track every step of SPA and multi-step funnels with precise Convert.com goals.

Author: George F. Crew

IN THIS ARTICLE YOU WILL:

Tracking multi-step funnels, especially those built as Single Page Applications (SPAs) or with dynamic content loading, can be tricky. Traditional methods that rely solely on URL changes or full page reloads often fall short.1

This guide provides two powerful, robust JavaScript methods to ensure your Convert.com goals fire accurately at each step of your funnel, giving you precise insights into user behavior and drop-off points.

Method 1: The Robust "MutationObserver" Approach (Recommended)

This is the most reliable method for most modern funnels. It works by "observing" changes in your page's Document Object Model (DOM). Instead of waiting for a URL change, it triggers a goal when a unique element belonging to the next step dynamically appears on the page.

Best For:
  • Single Page Applications (SPAs) where URLs don't change between steps.
  • Forms that load new steps via AJAX without full page reloads.
  • Funnels where "Next" buttons might have the same selector across steps.
  • Ensuring goals only fire when a step's content is actually visible.
How it Works:
  1. You define a main "funnel container" that holds all your form steps.
  2. For each step, you identify a unique CSS selector (an ID or class) that only appears when that specific step is active.
  3. A MutationObserver watches the funnel container for changes.2
  4. When the unique element for a new step is detected, the corresponding Convert.com goal is fired.

Implementation Steps:

Step 1: Identify Your Funnel's Main Container

Find the main parent element (a <div> or <section>) that wraps all the steps of your funnel. This is the area where the content changes as users progress.

Example HTML:

<div id="my-funnel-wrapper">
    <div id="step-1-form">...</div> 
</div>
Step 2: Define Unique Selectors for Each Step

For each step (starting from Step 2 onwards, and including your final conversion/thank you page), find a unique CSS selector (ID or class) that only exists in the DOM when that specific step is active.

Example HTML for a multi-step form:

<div id="my-funnel-wrapper">
    <div id="step-2-details">
        <h2>Enter Your Vehicle Information</h2>
        </div>

    <div class="personal-info-section">
        <h3>Your Contact Details</h3>
        </div>

    <div id="thank-you-message">
        <h1>Thank You for Your Submission!</h1>
    </div>
</div>
Step 3: Insert the JavaScript into Convert.com

Place this script into your Convert.com experience's "Global JavaScript" section, or directly on the first page of your funnel within <script> tags.

<script>
/**
 * Convert.com Funnel Tracking: MutationObserver Method
 * (Recommended for SPAs and Dynamic Forms)
 * * This script fires a Convert.com goal whenever a new step's unique element
 * is rendered within the main funnel container.
 */

const FUNNEL_CONFIG = {
    // 1. The CSS selector for the main wrapper/container of your entire funnel form.
    //    *** REPLACE '#request-form-container' with your actual container selector ***
    FUNNEL_CONTAINER_SELECTOR: '#request-form-container', 

    // 2. Map unique step selectors to their corresponding Convert.com Goal IDs.
    //    The key is a CSS selector for an element that is *only* present/visible on that specific step.
    STEP_GOAL_MAP: {
        // Goal fires when the element for Step 2 is visible
        '#step-container-2': 'YOUR_STEP_2_GOAL_ID', // Replace with your selector and Goal ID
        
        // Goal fires when the element for Step 3 is visible
        '.vehicle-information-form': 'YOUR_STEP_3_GOAL_ID', // Replace with your selector and Goal ID
        
        // Final Conversion Goal (e.g., Thank You page element)
        '#confirmation-page': 'YOUR_CONVERSION_GOAL_ID'  // Replace with your selector and Goal ID
        // Add more entries for each step you want to track
    }
};

// Internal state to prevent goals from firing multiple times on the same page view
let goalsFired = {}; 

/**
 * Fires the Convert.com goal if it hasn't been fired already for this session.
 * @param {string} goalId - The Convert.com Goal ID.
 */
function fireConvertGoal(goalId) {
    if (goalsFired[goalId]) {
        return; // Goal already fired for this step in current session
    }

    if (window._conv_s && typeof window._conv_s.push === 'function') {
        window._conv_s.push(['goal', goalId]);
        goalsFired[goalId] = true; // Mark as fired
        console.log(`✅ Convert.com Goal Fired: ${goalId}`);
    } else {
        console.warn(`Convert.com tracking object (_conv_s) not found. Could not fire goal ${goalId}.`);
    }
}

/**
 * Checks the DOM for all defined step selectors and fires the corresponding goals.
 */
function checkAndFireGoals() {
    for (const [stepSelector, goalId] of Object.entries(FUNNEL_CONFIG.STEP_GOAL_MAP)) {
        // If the unique element for the step is currently present in the DOM, fire the goal.
        if (document.querySelector(stepSelector)) {
            fireConvertGoal(goalId);
        }
    }
}

/**
 * Initializes the funnel tracking using a MutationObserver.
 */
function trackFunnelSteps() {
    const container = document.querySelector(FUNNEL_CONFIG.FUNNEL_CONTAINER_SELECTOR);

    if (!container) {
        console.error(`🚨 Funnel container not found. Check the selector: ${FUNNEL_CONFIG.FUNNEL_CONTAINER_SELECTOR}`);
        // Retry after a short delay in case the container loads later dynamically
        setTimeout(trackFunnelSteps, 500); 
        return;
    }

    // 1. Initial check (in case the script loads after the first step is already visible)
    checkAndFireGoals();

    // 2. Set up a MutationObserver to watch for changes inside the container.
    // This fires whenever a step completes and the next step's content is inserted/modified.
    const observer = new MutationObserver((mutationsList, observer) => {
        // A change was detected, re-check for new step elements
        checkAndFireGoals();
    });

    // Configuration for the observer: watch for new children (childList) and changes within them (subtree)
    const config = { childList: true, subtree: true, attributes: true }; // 'attributes: true' can catch changes to 'display: none'
    observer.observe(container, config);

    // Optional: Also check on hash changes (e.g., for SPAs using /#step2 in URL)
    window.addEventListener('hashchange', checkAndFireGoals);
}

// Start the tracking script when the DOM is fully ready
document.addEventListener('DOMContentLoaded', trackFunnelSteps);

</script>
Key Advantages:
  • Highly Accurate: Fires goals based on actual content appearance, not just clicks.
  • Resilient: Works even if "Next" buttons are reused, or if validation prevents a step advance.

SPA Friendly: Ideal for dynamic content loading without full page reloads.

Method 2: Click-Based Tracking with Session Management

This method is useful when you have a consistent "Next" or "Continue" button selector across all your funnel steps, and you want to track progress based on button clicks. It uses sessionStorage to keep track of the user's current step within their browser session.

Best For:
  • Funnels where a single, shared button selector reliably advances the user to the next step.
  • Simpler funnels where client-side validation failures are less common or less critical to track precisely.
How it Works:
  1. You define a shared CSS selector for your "Next/Continue" button.
  2. An event listener detects clicks on this button.
  3. Upon each click, a "step counter" stored in sessionStorage is incremented.
  4. A Convert.com goal corresponding to the new step is fired.

Implementation Steps:

Step 1: Identify Your Shared "Next" Button Selector

Find the exact CSS selector (e.g., #next-button, .btn-continue) that is used for the "Next" or "Continue" button on all steps of your funnel.

Example HTML:

<button class="next-step-button">Continue</button>
<button class="next-step-button">Proceed to Payment</button>
Step 2: Order Your Convert.com Goal IDs

List your Convert.com Goal IDs in the exact sequence they should fire as the user advances through the funnel.

  • The first goal in your list should correspond to reaching Step 2 (after clicking the button on Step 1).
  • The last goal should be for your final conversion.

User Action

Corresponds to Reaching

Goal List Index

Goal ID Example

Clicks button on Step 1

Step 2

0

YOUR_STEP_2_GOAL_ID

Clicks button on Step 2

Step 3

1

YOUR_STEP_3_GOAL_ID

Clicks button on Step 3

Final Conversion

2

YOUR_CONVERSION_GOAL_ID

Step 3: Insert the JavaScript into Convert.com

Place this script into your Convert.com experience's "Global JavaScript" section, or directly on the first page of your funnel within<script> tags.

<script>
/**
 * Convert.com Funnel Tracking: Click-Based Method with Session Management
 * * This script tracks the funnel step by incrementing a counter upon each successful
 * click on a shared button selector and fires corresponding Convert.com goals.
 */

const FUNNEL_CONFIG = {
    // 1. The shared CSS selector for the button that advances the user to the next step.
    //    *** REPLACE '.next-button' with your actual shared button selector ***
    NEXT_BUTTON_SELECTOR: '.next-button', 
    
    // 2. An ORDERED list of Convert.com Goal IDs.
    //    The goal at index 0 fires when the user clicks the button on Step 1 (reaching Step 2).
    STEP_GOAL_IDS: [
        'YOUR_STEP_2_GOAL_ID',    // Replace with Goal ID for reaching Step 2
        'YOUR_STEP_3_GOAL_ID',    // Replace with Goal ID for reaching Step 3
        'YOUR_CONVERSION_GOAL_ID' // Replace with your final Conversion Goal ID
        // Add more goals as needed for each subsequent step
    ],
    
    // Internal key for storing the current step in session storage
    STORAGE_KEY: 'convertFunnelStep'
};

// --- Core Logic ---

/**
 * Gets the current step number from session storage.
 * Defaults to 0, representing the starting point (Step 1).
 */
function getCurrentStep() {
    const storedStep = sessionStorage.getItem(FUNNEL_CONFIG.STORAGE_KEY);
    return parseInt(storedStep, 10) || 0; // Returns 0 if not found or invalid
}

/**
 * Updates the step counter in session storage.
 * @param {number} newStep - The 0-based index of the current step in the user's journey.
 */
function updateStep(newStep) {
    sessionStorage.setItem(FUNNEL_CONFIG.STORAGE_KEY, newStep);
    console.log(`Funnel Step updated to: ${newStep + 1}`);
}

/**
 * Fires the Convert.com goal corresponding to the new step.
 * @param {number} newStepIndex - The 0-based index of the goal to fire from STEP_GOAL_IDS.
 */
function fireConvertGoal(newStepIndex) {
    if (newStepIndex >= 0 && newStepIndex < FUNNEL_CONFIG.STEP_GOAL_IDS.length) {
        const goalId = FUNNEL_CONFIG.STEP_GOAL_IDS[newStepIndex];
        
        if (window._conv_s && typeof window._conv_s.push === 'function') {
            window._conv_s.push(['goal', goalId]);
            console.log(`✅ Convert.com Goal Fired: ${goalId} (Reached Funnel Goal Index ${newStepIndex})`);
        } else {
            console.warn(`Convert.com tracking object (_conv_s) not found. Could not fire goal ${goalId}.`);
        }
    } else {
        console.warn(`Attempted to fire goal for unknown step index: ${newStepIndex}. Ensure STEP_GOAL_IDS is correctly configured.`);
    }
}

/**
 * Handles the click event on the shared button selector.
 * Includes a small delay to account for client-side processing/validation.
 */
function handleButtonClick(event) {
    // Use .closest to handle clicks on child elements within the button
    let button = event.target.closest(FUNNEL_CONFIG.NEXT_BUTTON_SELECTOR);
    
    if (button) {
        // Delay to allow form validation/rendering of the next step
        // IMPORTANT: This assumes the click **succeeds** and advances the user.
        setTimeout(() => {
            const currentStep = getCurrentStep();
            const nextStep = currentStep + 1; // Increment the step counter
            
            updateStep(nextStep);
            // Fire the goal corresponding to the *new* step index in our array
            fireConvertGoal(nextStep -1); // If currentStep was 0, nextStep is 1, fire goal at index 0.
                                         // This maps currentStep = 1 (reached step 2) to STEP_GOAL_IDS[0].
            
        }, 300); // 300ms delay
    }
}

/**
 * Initializes the script by setting up the click listener and session storage.
 */
function initFunnelTracker() {
    // Initialize step counter if it's the first visit or a new session
    if (!sessionStorage.getItem(FUNNEL_CONFIG.STORAGE_KEY)) {
        updateStep(0); // Set to 0, representing starting on Step 1.
    }

    // Attach a single click listener to the entire document using event delegation.
    // This allows tracking clicks on the shared button selector, even if it's dynamically loaded.
    document.addEventListener('click', handleButtonClick);
}

// Start the tracking script when the DOM is loaded
document.addEventListener('DOMContentLoaded', initFunnelTracker);

</script>
Key Advantages:
  • Simplicity: Easier to implement if your "Next" buttons are consistently selected.
  • Session Persistence: Keeps track of the user's progress even if they refresh the page within the same session.
Important Considerations:
  • Validation: This method assumes a button click successfully advances the user. If client-side validation prevents the step change (e.g., an error message appears but the page doesn't advance), the goal will still fire, leading to inaccurate data.

Navigation: If users can navigate directly to a later step without clicking previous buttons, the sessionStorage counter might be out of sync.

Choosing the Right Method

Feature

Method 1: MutationObserver (Recommended)

Method 2: Click-Based (Session Management)

Accuracy

High (tracks content appearance)

Moderate (tracks clicks, assumes success)

SPA Friendly

Excellent

Good (but consider validation issues)

Shared Buttons

Handles seamlessly

Handles with sessionStorage

Validation

Ignores failed validation clicks

May fire goals on failed validation

Complexity

Slightly more setup for step selectors

Simpler setup if button selector is clear

For the most robust and accurate funnel tracking, especially with modern web applications, the MutationObserver method is generally preferred. If your funnel is simpler and button clicks reliably indicate progression, the Click-Based method can be a quick and effective solution.

Remember to test your implementation thoroughly to ensure goals are firing as expected at each step of your funnel!