@@ -24,6 +24,44 @@ function track(eventName, eventData = {}) {
2424 }
2525}
2626
27+ // Global error handlers — capture uncaught JS + CSP violations.
28+ // Strip PII: only filename (basename), line/col, truncated message.
29+ // Throttle to avoid event-storms (max 1 per error signature per minute).
30+ const _errorThrottle = new Map ( ) ;
31+ function _shouldEmitError ( sig ) {
32+ const now = Date . now ( ) ;
33+ const last = _errorThrottle . get ( sig ) || 0 ;
34+ if ( now - last < 60_000 ) return false ;
35+ _errorThrottle . set ( sig , now ) ;
36+ return true ;
37+ }
38+ window . addEventListener ( "error" , ( e ) => {
39+ const file = ( e . filename || "" ) . split ( "/" ) . pop ( ) || "unknown" ;
40+ const msg = ( e . message || "" ) . slice ( 0 , 140 ) ;
41+ const sig = `${ file } :${ e . lineno } :${ msg . slice ( 0 , 40 ) } ` ;
42+ if ( _shouldEmitError ( sig ) ) {
43+ track ( "js_error" , { file, line : e . lineno , col : e . colno , message : msg } ) ;
44+ }
45+ } ) ;
46+ window . addEventListener ( "unhandledrejection" , ( e ) => {
47+ const reason = e . reason ;
48+ const msg = ( reason ?. message || String ( reason || "unknown" ) ) . slice ( 0 , 140 ) ;
49+ const sig = `promise:${ msg . slice ( 0 , 40 ) } ` ;
50+ if ( _shouldEmitError ( sig ) ) {
51+ track ( "js_unhandled_rejection" , { message : msg } ) ;
52+ }
53+ } ) ;
54+ document . addEventListener ( "securitypolicyviolation" , ( e ) => {
55+ const sig = `csp:${ e . violatedDirective } :${ e . blockedURI } ` ;
56+ if ( _shouldEmitError ( sig ) ) {
57+ track ( "csp_violation" , {
58+ directive : e . violatedDirective ,
59+ blocked_uri : ( e . blockedURI || "" ) . slice ( 0 , 200 ) ,
60+ source_file : ( e . sourceFile || "" ) . split ( "/" ) . pop ( ) || ""
61+ } ) ;
62+ }
63+ } ) ;
64+
2765// Simplified state - LessonEngine now manages lesson state and progress
2866const state = {
2967 userSettings : {
@@ -913,6 +951,18 @@ function runCode() {
913951 lesson : engineState . lessonIndex
914952 } ) ;
915953
954+ // Derive module_complete: validateCode() already marked progress, so refetch state
955+ const updatedState = lessonEngine . getCurrentState ( ) ;
956+ const moduleId = updatedState . module ?. id ;
957+ const completedCount = lessonEngine . userProgress ?. [ moduleId ] ?. completed ?. length ?? 0 ;
958+ const lessonCount = updatedState . module ?. lessons ?. length ?? 0 ;
959+ if ( moduleId && lessonCount > 0 && completedCount === lessonCount ) {
960+ track ( "module_complete" , {
961+ module : moduleId ,
962+ lessons : lessonCount
963+ } ) ;
964+ }
965+
916966 // Show success hint
917967 showSuccessHint ( validationResult . message || t ( "successMessage" ) ) ;
918968
@@ -970,6 +1020,15 @@ function runCode() {
9701020 const step = validationResult . validCases + 1 ;
9711021 const total = validationResult . totalCases ;
9721022
1023+ // Track partial / failed validation — funnel signal for which lessons trip users
1024+ track ( "lesson_fail" , {
1025+ module : engineState . module ?. id ,
1026+ lesson : engineState . lessonIndex ,
1027+ step,
1028+ total,
1029+ progress : total > 0 ? Math . round ( ( step - 1 ) / total * 100 ) : 0
1030+ } ) ;
1031+
9731032 // Only show hints if enabled
9741033 if ( ! state . userSettings . disableFeedbackErrors ) {
9751034 showHint ( validationResult . message || t ( "keepTrying" ) , step , total ) ;
0 commit comments