# UTM Form Tracking Verification After Cleanup

**Last Updated:** 2026-01-29

## Overview

This document summarizes the verification and fixes implemented to ensure UTMs and tracking values (including Google Ads hsa_* parameters) are properly passed to HubSpot forms, especially the demo booking form, even after URL cleanup removes UTMs from the browser address bar.

## Problem Statement

After URL cleanup removes UTMs from the browser address bar, forms need to reliably access UTM data from:
1. Instance variables (persist after cleanup)
2. Cookies (90-day expiration)
3. localStorage (fallback)
4. Original URL (preserved in `page_url` field for backend fallback)

## Key Fixes Implemented

### 1. Fixed `refreshUTMData()` to Preserve Instance Variables

**File:** `v2/js/utm-tracking.js` (lines 1059-1118)

**Issue:** `refreshUTMData()` was calling `extractUTMParameters()` which reads from URL. After cleanup, URL has no UTMs, so it would overwrite instance variables with empty values.

**Fix:** Added check to only re-extract if URL has parameters. Otherwise, preserve instance variables and refresh hsa_* from URL (preserved after cleanup) and cookies.

```javascript
refreshUTMData() {
    const urlParams = new URLSearchParams(window.location.search);
    const hasURLParams = this.utmParams.some(param => urlParams.has(param)) || 
                        urlParams.has('gclid') || 
                        urlParams.has('leadSource') || 
                        urlParams.has('lead_source') ||
                        Object.keys(this.hsaParams || {}).some(param => urlParams.has(param));
    
    if (hasURLParams) {
        // URL has parameters - re-extract (e.g., user navigated to new page with UTMs)
        this.extractUTMParameters();
        this.correctGoogleAdsUTM();
    } else {
        // No URL parameters - preserve instance variables, refresh hsa_* and cookies
        // ... (preserve logic)
    }
}
```

### 2. Store Original URL for Backend Fallback

**File:** `v2/js/utm-tracking.js` (lines 27-40, 2826-2850)

**Issue:** `getUTMDataForAPI()` returns `page_url: window.location.href` which after cleanup won't have UTMs. Backend needs original URL as fallback.

**Fix:** Store original URL in constructor, use it in `getUTMDataForAPI()` if original had UTMs.

```javascript
constructor() {
    // ...
    this.originalUrl = window.location.href; // Store before cleanup
}

getUTMDataForAPI() {
    // ...
    let pageUrlToUse = window.location.href;
    if (this.originalUrl && this.originalUrl !== window.location.href) {
        const originalUrlParams = new URLSearchParams(new URL(this.originalUrl).search);
        const originalHadUTMs = this.utmParams.some(param => originalUrlParams.has(param));
        
        if (originalHadUTMs && (utmData.utm_source || utmData.utm_medium || utmData.utm_campaign)) {
            pageUrlToUse = this.originalUrl; // Use original URL for backend fallback
        }
    }
    return {
        // ...
        page_url: pageUrlToUse, // Preserves original URL with UTMs
        // ...
    };
}
```

### 3. Added hsa_* Parameters to `getUTMDataForAPI()` Return

**File:** `v2/js/utm-tracking.js` (lines 2830-2850)

**Issue:** `getUTMDataForAPI()` didn't include hsa_* parameters, so forms couldn't access them.

**Fix:** Include hsa_* parameters in return object.

```javascript
const hsaData = {};
if (this.hsaParams) {
    Object.keys(this.hsaParams).forEach(key => {
        if (this.hsaParams[key]) {
            hsaData[key] = this.hsaParams[key];
        }
    });
}

return {
    // ... UTM fields ...
    hsaParams: hsaData, // CRITICAL: Include hsa_* parameters
    // ...
};
```

### 4. Enhanced Form Field Population with Validation

**File:** `v2/base/include_form-hs.php` (lines 452-700)

**Issues:**
- No validation that form fields were populated
- No fallback if `getUTMDataForAPI()` returns empty data
- No error handling

**Fixes:**
- Added try-catch around `getUTMDataForAPI()` call
- Added fallback to cookies and localStorage if tracker returns empty data
- Added validation helper `setFieldValue()` with logging
- Added validation check after population to warn if no UTM data

```javascript
// Try to get UTM data from tracker
if (window.utmTracker && typeof window.utmTracker.getUTMDataForAPI === 'function') {
    try {
        utmData = window.utmTracker.getUTMDataForAPI();
        
        // Validate we got data
        if (!utmData || (!utmData.utm_source && !utmData.gclid && !utmData.leadSource)) {
            // Fallback to cookies/localStorage
            // ...
        }
    } catch (e) {
        // Error handling - fallback to cookies/localStorage
    }
}

// Validate form fields exist before populating
const setFieldValue = (fieldId, value, defaultValue = '') => {
    const field = document.getElementById(fieldId);
    if (field) {
        field.value = value || defaultValue;
        addLog(`Set ${fieldId} = ${field.value}`, 'info');
    } else {
        addLog(`WARN: Field ${fieldId} not found`, 'warn');
    }
};

// Validate that at least some UTM data was populated
const hasUTMData = !!(document.getElementById('utm_source_modal')?.value || 
                     document.getElementById('gclid_modal')?.value || 
                     document.getElementById('leadSource_modal')?.value);
if (!hasUTMData) {
    addLog('WARN: No UTM data populated - backend will rely on page_url and cookies', 'warn');
}
```

### 5. Added hsa_* Parameters as Hidden Form Fields

**File:** `v2/base/include_form-hs.php` (lines 737-757)

**Issue:** Backend reads hsa_* from `page_url` first, but form fields provide additional reliability.

**Fix:** Dynamically create hidden form fields for hsa_* parameters if present.

```javascript
if (utmData.hsaParams && typeof utmData.hsaParams === 'object') {
    const hsaFieldNames = ['hsa_acc', 'hsa_cam', 'hsa_grp', 'hsa_ad', 'hsa_src', 'hsa_tgt', 'hsa_kw', 'hsa_mt', 'hsa_net', 'hsa_ver'];
    hsaFieldNames.forEach(hsaParam => {
        if (utmData.hsaParams[hsaParam]) {
            let hsaField = document.getElementById(`${hsaParam}_modal`);
            if (!hsaField) {
                hsaField = document.createElement('input');
                hsaField.type = 'hidden';
                hsaField.id = `${hsaParam}_modal`;
                hsaField.name = `${hsaParam}_modal`;
                const form = document.getElementById('demo-form');
                if (form) {
                    form.appendChild(hsaField);
                }
            }
            hsaField.value = utmData.hsaParams[hsaParam];
        }
    });
}
```

### 6. Updated Backend to Read hsa_* from Form Fields First

**File:** `html/form-hs.php` (lines 354-430)

**Issue:** Backend only read hsa_* from `page_url` and cookies, not form fields.

**Fix:** Added priority: Form fields > page_url > cookies.

```php
// CRITICAL FIX: Extract hsa_* parameters from POST form fields first, then page_url, then cookies
// Priority: Form fields > page_url > cookies

// Check form fields first
$hsaFieldNames = [
    'hsa_acc' => 'hsa_acc',
    'hsa_cam' => 'hsa_cam',
    // ... etc
];
foreach ($hsaFieldNames as $hsaParam => $varName) {
    $formFieldValue = trim($_POST["{$hsaParam}_modal"] ?? '');
    if (!empty($formFieldValue)) {
        $$varName = $formFieldValue;
        error_log("form-hs.php - Extracted {$hsaParam} from form field: " . $$varName);
    }
}

// Parse from page_url if not found in form fields
if (!empty($pageUrl)) {
    // ... extract from page_url ...
}

// Fallback to cookies if not found in form fields or URL
foreach ($hsaFieldNames as $hsaParam => $varName) {
    if (empty($$varName)) {
        $$varName = trim($_COOKIE[$hsaParam] ?? '');
    }
}
```

### 7. Updated `addUTMToForm()` to Use Correct page_url

**File:** `v2/js/utm-tracking.js` (lines 2389-2420)

**Issue:** `addUTMToForm()` used `window.location.href` for page_url, which after cleanup won't have UTMs.

**Fix:** Get page_url from `getUTMDataForAPI()` which preserves original URL.

```javascript
const pageUrlForForm = (() => {
    if (typeof this.getUTMDataForAPI === 'function') {
        const apiData = this.getUTMDataForAPI();
        return apiData.page_url || window.location.href;
    }
    return window.location.href;
})();

const hubspotFields = {
    // ...
    '00N7Q000006rhep': pageUrlForForm, // Use preserved original URL
    // ...
};
```

## Data Flow After Cleanup

1. **Page Load:**
   - UTMs extracted from URL → stored in instance variables
   - Cookies set immediately
   - localStorage fallback set
   - Original URL stored
   - Cleanup scheduled (1.5s delay)

2. **After Cleanup (1.5s):**
   - UTMs removed from URL (via `history.replaceState()`)
   - hsa_* parameters PRESERVED in URL (intentional)
   - Instance variables persist (not affected by cleanup)
   - Cookies persist (90-day expiration)
   - Original URL stored in tracker

3. **Form Submission:**
   - Form reads from `window.utmTracker.getUTMDataForAPI()`
   - `getUTMDataForAPI()` uses `getAllUTMData()` which merges:
     - Instance variables (persist after cleanup) ✓
     - Cookies (fallback) ✓
     - localStorage (fallback) ✓
   - Returns `page_url` with original URL (has UTMs) ✓
   - Returns `hsaParams` object with hsa_* values ✓
   - Form fields populated with UTM data ✓
   - hsa_* fields created dynamically ✓
   - Backend reads from form fields (primary) ✓
   - Backend reads from page_url (fallback) ✓
   - Backend reads from cookies (last resort) ✓

## Testing

### Test Script

Created `v2/scripts/dev-helpers/test-utm-form-tracking.php` to verify:
- Instance variable persistence after cleanup
- Cookie persistence after cleanup
- `getUTMDataForAPI()` returns data after cleanup
- Form field population
- `page_url` field contains hsa_* after cleanup
- hsa_* parameters in form fields

### Usage

Visit: `http://localhost:8003/v2/scripts/dev-helpers/test-utm-form-tracking.php?utm_source=test&utm_campaign=test&hsa_src=g&hsa_cam=123&gclid=test123&utm_debug=true`

## Expected Behavior

After cleanup:

1. ✅ Instance variables persist (UTMs still available)
2. ✅ Cookies persist (90-day expiration)
3. ✅ Form fields populated from instance variables/cookies
4. ✅ `page_url` has original URL with UTMs (for backend fallback)
5. ✅ `page_url` has hsa_* parameters (preserved by cleanup)
6. ✅ hsa_* parameters in form fields (dynamically created)
7. ✅ Backend reads UTMs from form fields (primary)
8. ✅ Backend reads UTMs from page_url (fallback)
9. ✅ Backend reads hsa_* from form fields (primary)
10. ✅ Backend reads hsa_* from page_url (fallback)
11. ✅ Backend reads hsa_* from cookies (last resort)
12. ✅ HubSpot receives all tracking data

## Success Criteria

- ✅ UTMs are passed to HubSpot forms after cleanup
- ✅ hsa_* parameters are passed to HubSpot forms after cleanup
- ✅ Demo booking form works correctly
- ✅ All form types work correctly (use `addUTMToForm()`)
- ✅ Backend properly extracts all parameters
- ✅ HubSpot receives complete attribution data
- ✅ No data loss during cleanup process

## Related Files

- `v2/js/utm-tracking.js` - Main tracking script
- `v2/base/include_form-hs.php` - Demo booking form
- `html/form-hs.php` - Backend form handler
- `v2/scripts/dev-helpers/test-utm-form-tracking.php` - Test script

## Related Documentation

- `docs/development/UTM_CLEANUP_DISCREPANCY_FIX.md` - Cleanup discrepancy fix
- `docs/development/UTM_TRACKING_DEBUGGING.md` - Debugging guide
- `docs/development/UTM_TRACKING_VERIFICATION_CHECKLIST.md` - Verification checklist
