# HubSpot form-submit debug tracker
Drop this into Tampermonkey or paste into the browser DevTools Console on the page that's giving trouble (https://content.lyreco.com/create-my-lyreco-account or wherever the step-2 button lives). It captures everything around the button click + form submit and prefixes every line with [LYRECO-DEBUG] for easy filtering in the Console.
What it catches:
1. Clicks on any , or role=button โ even when the button only appears after the first page transition.
2. Form submit events (capture phase, so dynamic forms and frameworks are caught).
3. HubSpot hsFormCallback postMessages (the official callback channel).
4. History API + hashchange (so you see when step-2 swaps in).
5. fetch + XMLHttpRequest โ non-GET requests are logged with method+URL+body so you see the actual submission endpoint.
6. MutationObserver logs every new and the SPA adds to the DOM.
Option A โ Tampermonkey userscript
`javascript
// ==UserScript==
// @name Lyreco form submit tracker (debug)
// @namespace lyreco-debug
// @match https://content.lyreco.com/*
// @match https://www.lyreco.com/*
// @run-at document-start
// @grant none
// @version 1.0
// ==/UserScript==
(function () { 'use strict'; const TAG = '[LYRECO-DEBUG]'; const t0 = performance.now(); const log = (label, info) => { console.log( '%c' + TAG + ' +' + Math.round(performance.now() - t0) + 'ms ' + label, 'color:#88c4dd;font-weight:bold;', info || '' ); };
// 1) Capture-phase click listener โ catches every button/submit-input click document.addEventListener('click', function (e) { const el = e.target.closest('button, input[type="submit"], a[role="button"], [data-action]'); if (!el) return; log('CLICK', { tag: el.tagName, type: el.type || '', text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0, 100), id: el.id || '', className: (typeof el.className === 'string' ? el.className : '').slice(0, 100), formId: el.form ? el.form.id : (el.closest('form') || {}).id, url: location.pathname + location.search, }); }, true);
// 2) Capture-phase submit listener โ wins over preventDefault'd handlers document.addEventListener('submit', function (e) { const f = e.target; const data = {}; try { new FormData(f).forEach(function (v, k) { data[k] = typeof v === 'string' ? v.slice(0, 200) : '(file)'; }); } catch (err) { data._error = String(err); } log('SUBMIT', { formId: f.id || '', action: f.action || '', method: (f.method || 'GET').toUpperCase(), fields: f.elements ? f.elements.length : 0, data: data, defaultPrevented: e.defaultPrevented, }); }, true);
// 3) HubSpot embedded-form postMessage callbacks window.addEventListener('message', function (e) { if (e.data && e.data.type === 'hsFormCallback') { log('HS FORM CALLBACK ยท ' + e.data.eventName, { formId: e.data.id, data: e.data.data, }); } });
// 4) DOM additions (SPA-style transitions) const seen = new Set(); const obs = new MutationObserver(function (mutations) { for (const m of mutations) { for (const n of m.addedNodes) { if (n.nodeType !== 1) continue; const buttons = n.matches && n.matches('button, input[type="submit"]') ? [n] : Array.from((n.querySelectorAll && n.querySelectorAll('button, input[type="submit"]')) || []); buttons.forEach(function (b) { const key = (b.tagName || '') + '|' + (b.id || '') + '|' + ((b.innerText || b.value || '').trim().slice(0, 40)); if (seen.has(key)) return; seen.add(key); log('NEW BUTTON appeared', { tag: b.tagName, type: b.type || '', text: (b.innerText || b.value || '').trim().slice(0, 80), id: b.id || '', inFormId: (b.closest('form') || {}).id, }); }); const forms = n.matches && n.matches('form') ? [n] : Array.from((n.querySelectorAll && n.querySelectorAll('form')) || []); forms.forEach(function (f) { log('NEW FORM appeared', { id: f.id || '', action: f.action || '', fields: f.elements ? f.elements.length : 0, }); }); } } }); obs.observe(document.documentElement, { childList: true, subtree: true });
// 5) History API (SPA routing) ['pushState', 'replaceState'].forEach(function (method) { const orig = history[method]; history[method] = function () { log('HISTORY ยท ' + method, { url: arguments[2] || location.pathname }); return orig.apply(this, arguments); }; }); window.addEventListener('popstate', function () { log('POPSTATE', location.pathname); }); window.addEventListener('hashchange', function () { log('HASHCHANGE', location.hash); });
// 6) Fetch + XHR โ see the actual submit endpoint that fires const origFetch = window.fetch; window.fetch = function (resource, init) { const url = typeof resource === 'string' ? resource : (resource && resource.url) || ''; const method = ((init && init.method) || 'GET').toUpperCase(); if (method !== 'GET') { log('FETCH ยท ' + method, { url: url, body: (init && init.body && typeof init.body === 'string') ? init.body.slice(0, 400) : '(non-string body)', }); } return origFetch.apply(this, arguments); }; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__lyrecoMethod = method.toUpperCase(); this.__lyrecoUrl = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { if (this.__lyrecoMethod && this.__lyrecoMethod !== 'GET') { log('XHR ยท ' + this.__lyrecoMethod, { url: this.__lyrecoUrl, body: (body && typeof body === 'string') ? body.slice(0, 400) : '(non-string body)', }); } return origSend.apply(this, arguments); };
log('Lyreco form tracker armed', { url: location.href });
})();
`
Option B โ DevTools Console paste (no install)
Paste the same IIFE body (everything from (function () { to the closing })();) directly into the DevTools Console. It runs immediately and stays alive until you reload the page. Useful for a quick one-off check without installing Tampermonkey.
How to read the output
Open DevTools โ Console โ set the filter to [LYRECO-DEBUG]. Reproduce the broken flow (step 1 โ step 2 โ click the submit button). You'll see chronologically (each line timestamped from page load):
1. NEW BUTTON appeared / NEW FORM appeared when the second-page content is swapped in.
2. CLICK when the user clicks the submit button โ confirms the click is reaching the right element. If you see CLICK but no SUBMIT, the page is preventing the form submission (likely SPA wiring intercepting the click handler).
3. SUBMIT if a real submit event fires. defaultPrevented: true means a handler called e.preventDefault() โ common in SPA forms.
4. FETCH or XHR with method=POST + URL โ this is the actual submission going to HubSpot's API. If you see this but no SUBMIT, the form is being posted via JS (not a native form submit), which is why the submit event tracking misses it.
5. HS FORM CALLBACK ยท onFormSubmitted โ the official HubSpot callback we use in the production tagging (docs/hubspot_cms_tagging_content_lyreco.md). If this never fires, that's the bug โ the HubSpot form context isn't broadcasting the event.
If the form is in an iframe
HubSpot embedded forms often live in an iframe whose src is forms.hsforms.com. The userscript only sees events in the top page. To debug inside the iframe, open DevTools โ "Sources" โ change the execution context dropdown (top-right of Console) to the iframe's frame, then paste the IIFE. The MutationObserver and click listeners will run there.
Common patterns this script reveals
| Symptom in console | Likely cause | Fix |
|---|---|---|
| CLICK fires, no SUBMIT, no FETCH | Click handler did nothing โ button isn't wired | Inspect onclick / framework binding |
| CLICK + SUBMIT defaultPrevented=true, no FETCH | Submit blocked by validation | Check form validation state |
| CLICK + FETCH POST, no SUBMIT, no HS callback | Form posts via JS, bypasses native event โ HubSpot embed not configured | Use the HubSpot Forms API config to register an onFormSubmitted callback, OR hook the FETCH URL directly |
| CLICK + FETCH + HS FORM CALLBACK onFormSubmitted | Everything healthy โ the production tagging snippet's listener will fire correctly |
---
_Authored 2026-06-01 to debug step-2 submit on the WISE onboarding form. Production tagging snippet lives in hubspot_cms_tagging_content_lyreco.md._