@@ -2,6 +2,8 @@ import { waitForElement } from '@clerk/shared/dom';
22import { loadScript } from '@clerk/shared/loadScript' ;
33import type { CaptchaAppearanceOptions , CaptchaWidgetType } from '@clerk/shared/types' ;
44
5+ import { debugLogger } from '@/utils/debug' ;
6+
57import { CAPTCHA_ELEMENT_ID , CAPTCHA_INVISIBLE_CLASSNAME } from './constants' ;
68import type { CaptchaOptions } from './types' ;
79
@@ -74,8 +76,24 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
7476 const { siteKey, widgetType, invisibleSiteKey, nonce } = opts ;
7577 const { modalContainerQuerySelector, modalWrapperQuerySelector, closeModal, openModal } = opts ;
7678 const captcha : Turnstile . Turnstile = await loadCaptcha ( nonce ) ;
79+
80+ // Error codes array - used for actual error handling (unchanged from original behavior)
7781 const errorCodes : ( string | number ) [ ] = [ ] ;
7882
83+ // Diagnostic tracking - wrapped in try-catch to never affect production behavior
84+ let startTime = 0 ;
85+ const errorTimeline : Array < { code : string | number ; t : number } > = [ ] ;
86+ let captchaAttemptId = '' ;
87+ try {
88+ startTime = Date . now ( ) ;
89+ captchaAttemptId =
90+ typeof crypto !== 'undefined' && typeof crypto . randomUUID === 'function'
91+ ? crypto . randomUUID ( )
92+ : Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) ;
93+ } catch {
94+ // Silently ignore - diagnostics should never break captcha flow
95+ }
96+
7997 let captchaToken = '' ;
8098 let id = '' ;
8199 let turnstileSiteKey = siteKey ;
@@ -180,7 +198,14 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
180198 }
181199 } ,
182200 'error-callback' : function ( errorCode ) {
201+ // Track error for actual error handling (original behavior)
183202 errorCodes . push ( errorCode ) ;
203+ // Track timing for diagnostics only
204+ try {
205+ errorTimeline . push ( { code : errorCode , t : Date . now ( ) - startTime } ) ;
206+ } catch {
207+ // Silently ignore - diagnostics should never break captcha flow
208+ }
184209 /**
185210 * By setting retry to 'never' the responsibility for implementing retrying is ours
186211 * https://developers.cloudflare.com/turnstile/reference/client-side-errors/#retrying
@@ -219,6 +244,31 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
219244 // After a failed challenge remove it
220245 captcha . remove ( id ) ;
221246 }
247+
248+ // Log failure with full error history for debugging - wrapped to never affect production
249+ try {
250+ const containerExistsAtFailure = widgetContainerQuerySelector
251+ ? ! ! document . querySelector ( widgetContainerQuerySelector )
252+ : false ;
253+
254+ debugLogger . error (
255+ 'Turnstile captcha challenge failed' ,
256+ {
257+ captchaAttemptId,
258+ errorTimeline,
259+ lastErrorCode : errorTimeline . length > 0 ? errorTimeline [ errorTimeline . length - 1 ] . code : null ,
260+ finalError : String ( e ) ,
261+ retriesAttempted : retries ,
262+ widgetType : captchaTypeUsed ,
263+ containerExistsAtFailure,
264+ totalDurationMs : Date . now ( ) - startTime ,
265+ } ,
266+ 'captcha' ,
267+ ) ;
268+ } catch {
269+ // Silently ignore - diagnostics should never break captcha flow
270+ }
271+
222272 // eslint-disable-next-line @typescript-eslint/only-throw-error
223273 throw {
224274 captchaError : e ,
0 commit comments