Skip to content

Commit 68dc502

Browse files
authored
feat(clerk-js): Add debug logging for Turnstile captcha errors (#7768)
1 parent a726252 commit 68dc502

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/clerk-js": patch
3+
---
4+
5+
Improve captcha error diagnostics

packages/clerk-js/src/utils/captcha/turnstile.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { waitForElement } from '@clerk/shared/dom';
22
import { loadScript } from '@clerk/shared/loadScript';
33
import type { CaptchaAppearanceOptions, CaptchaWidgetType } from '@clerk/shared/types';
44

5+
import { debugLogger } from '@/utils/debug';
6+
57
import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from './constants';
68
import 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

Comments
 (0)