11document . addEventListener ( 'alpine:init' , ( ) => {
22 Alpine . store ( 'fg' , { status : '' } ) ;
33
4+ function trackUmami ( eventName , payload = { } ) {
5+ if ( typeof window . apTrackUmami === 'function' ) {
6+ return window . apTrackUmami ( eventName , payload ) ;
7+ }
8+
9+ if ( ! window . umami || typeof window . umami . track !== 'function' ) {
10+ return false ;
11+ }
12+
13+ if ( Object . keys ( payload ) . length > 0 ) {
14+ window . umami . track ( eventName , payload ) ;
15+ } else {
16+ window . umami . track ( eventName ) ;
17+ }
18+
19+ return true ;
20+ }
21+
422 Alpine . data ( 'formGuard' , ( ) => ( {
523 start : 0 ,
624 lastElapsed : 0 ,
25+ requestPending : false ,
26+ requestOutcomeTracked : false ,
727
828 init ( ) {
929 this . start = Date . now ( ) ;
@@ -15,6 +35,9 @@ document.addEventListener('alpine:init', () => {
1535 this . lastElapsed = elapsed ;
1636 const hidden = this . $el . querySelector ( 'input[name=\"_elapsed_ms\"]' ) ;
1737 if ( hidden ) hidden . value = String ( elapsed ) ;
38+ this . requestPending = true ;
39+ this . requestOutcomeTracked = false ;
40+ trackUmami ( 'contact_form_submit' ) ;
1841 } , { capture : true } ) ;
1942
2043 // Fallback: copy hx-post into action if empty
@@ -55,20 +78,34 @@ document.addEventListener('alpine:init', () => {
5578 let data = null ;
5679 try { data = JSON . parse ( xhr . responseText || '' ) ; } catch { }
5780 if ( xhr . status >= 200 && xhr . status < 300 && data && data . ok ) {
81+ this . trackRequestOutcome ( 'contact_form_success' ) ;
5882 this . setState ( 'sent' ) ;
5983 this . $el . reset ( ) ;
6084 this . start = Date . now ( ) ;
6185 this . lastElapsed = 0 ;
6286 } else {
87+ this . trackRequestOutcome ( 'contact_form_error' , xhr . status ? { status_code : xhr . status } : { } ) ;
6388 this . setState ( 'error' ) ;
6489 }
6590 } ,
6691
6792 sendError ( e ) {
6893 if ( e . target !== this . $el ) return ;
94+ this . trackRequestOutcome ( 'contact_form_error' ) ;
6995 this . setState ( 'error' ) ;
7096 } ,
7197
98+ trackRequestOutcome ( eventName , payload = { } ) {
99+ // htmx can surface both sendError and afterRequest for the same attempt.
100+ if ( ! this . requestPending || this . requestOutcomeTracked ) {
101+ return ;
102+ }
103+
104+ this . requestOutcomeTracked = true ;
105+ this . requestPending = false ;
106+ trackUmami ( eventName , payload ) ;
107+ } ,
108+
72109 // visual state for CSS fallbacks
73110 setState ( state ) {
74111 this . $el . setAttribute ( 'data-status' , state || 'idle' ) ;
0 commit comments