♻️ refactor: QR 码加载多源重试与超时优化,降低生成失败与 Sentry 噪音

- 前端二维码生成改为两级加载:优先复用 window.QRCode,未加载则按多 CDN 源顺序重试加载,避免单一 CDN 故障导致失败(ESIM-TOOLS-15)
- 重构 CDN 动态加载逻辑:为每个 CDN 加入 5s 超时与单源错误处理,确保 Promise 不会卡死,并在窗口全局缺失时正确清理失效脚本重试
- Netlify 函数二维码生成超时从 10s 提升到 15s,兼容冷启动与生成耗时波动,减少超时失败(ESIM-TOOLS-16)
- Sentry 噪音过滤补充 Google Tag Manager 相关域名/脚本与 `Unexpected token '<'` 等异常规则,降低监控告警干扰(ESIM-TOOLS-18)
This commit is contained in:
Abner
2026-06-23 12:18:54 +08:00
parent 22db18f831
commit a19264cc01
4 changed files with 65 additions and 37 deletions

View File

@@ -6,7 +6,8 @@ const { withAuth, validateInput, AuthError } = require('./_shared/middleware');
const DEFAULT_QR_SIZE = 300;
const MIN_QR_SIZE = 200;
const MAX_QR_SIZE = 600;
const QR_TIMEOUT_MS = 10000;
// 增加超时到 15 秒,解决 Netlify Functions 冷启动 + QR 码生成时间问题 (ESIM-TOOLS-16)
const QR_TIMEOUT_MS = 15000;
const qrcodeSchema = {
data: {

View File

@@ -2,7 +2,11 @@
import { tl } from './i18n.js';
const QRCODE_CDN_URL = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js';
// CDN 多源备用列表(主源 + 备用源,任一成功即可,解决 ESIM-TOOLS-15 单一 CDN 故障问题)
const QRCODE_CDN_URLS = [
'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js'
];
const DEFAULT_QR_SIZE = 300;
const MIN_QR_SIZE = 200;
const MAX_QR_SIZE = 600;
@@ -100,44 +104,69 @@ function validateQRCodeData(data) {
}
/**
* 懒加载浏览器端 qrcode.js 库
* 首次调用会插入 CDN script后续调用复用同一个 Promise避免重复加载。
* 修复 Promise 挂起问题移除失效脚本、添加超时保护Issue #75 根因修复)
* @returns {Promise<Object>} qrcode.js 暴露的 QRCode 对象
* 获取 QRCode 库实例
* 两级加载策略window.QRCode (已加载) → 动态加载 CDN (多源重试)
* 解决 ESIM-TOOLS-15单 CDN 故障导致 116 个用户 QR 码生成失败
* @returns {Promise<Object>} qrcode 对象
*/
export async function loadQRCodeLibrary() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
throw new Error('QRCode local generation requires a browser environment');
}
// 1. 检查 window.QRCode可能已被 CDN 脚本加载)
if (window.QRCode) {
return window.QRCode;
}
// 2. 缓存中的 Promise防止并发重复加载
if (qrCodeLibraryPromise) {
return qrCodeLibraryPromise;
}
qrCodeLibraryPromise = new Promise((resolve, reject) => {
// 3. 动态加载 CDN带多源重试
qrCodeLibraryPromise = loadFromCDNWithRetry();
return qrCodeLibraryPromise;
}
/**
* 从 CDN 加载 QRCode 库,支持多源重试。
* @returns {Promise<Object>} QRCode 库对象
*/
async function loadFromCDNWithRetry() {
for (const cdnUrl of QRCODE_CDN_URLS) {
try {
const lib = await loadSingleCDN(cdnUrl);
return lib;
} catch (error) {
console.warn(`[QRCode] CDN ${cdnUrl} failed: ${error.message}`);
}
}
qrCodeLibraryPromise = null;
throw new Error('All QRCode CDN sources failed');
}
/**
* 从单个 CDN URL 加载 QRCode 库。
* @param {string} cdnUrl CDN 地址
* @returns {Promise<Object>} QRCode 库对象
*/
function loadSingleCDN(cdnUrl) {
return new Promise((resolve, reject) => {
if (typeof document === 'undefined') {
reject(new Error('QRCode local generation requires a browser environment'));
return;
}
const LOAD_TIMEOUT_MS = 5000;
let timeoutId = null;
const existingScript = document.querySelector(`script[src="${QRCODE_CDN_URL}"]`);
// 如果脚本已存在但 window.QRCode 未加载(说明脚本已失效),移除并重新加载
// 移除已失效的旧脚本
const existingScript = document.querySelector(`script[src="${cdnUrl}"]`);
if (existingScript) {
console.warn('[QRCode] Existing script found but QRCode global is missing, removing stale script');
existingScript.remove();
}
// 创建新的脚本元素
const script = document.createElement('script');
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
script.removeEventListener('load', handleLoad);
script.removeEventListener('error', handleError);
};
@@ -148,39 +177,29 @@ export async function loadQRCodeLibrary() {
resolve(window.QRCode);
return;
}
// 脚本加载成功但 window.QRCode 不存在CDN 内容异常)
qrCodeLibraryPromise = null;
script.remove();
reject(new Error('QRCode library loaded but QRCode global is missing'));
};
const handleError = () => {
cleanup();
qrCodeLibraryPromise = null;
script.remove();
reject(new Error('Failed to load QRCode library from CDN'));
};
const handleTimeout = () => {
cleanup();
qrCodeLibraryPromise = null;
script.remove();
reject(new Error(`QRCode library loading timed out after ${LOAD_TIMEOUT_MS}ms`));
reject(new Error(`Failed to load QRCode library from ${cdnUrl}`));
};
script.addEventListener('load', handleLoad, { once: true });
script.addEventListener('error', handleError, { once: true });
timeoutId = setTimeout(() => {
cleanup();
script.remove();
reject(new Error(`QRCode library loading timed out from ${cdnUrl}`));
}, LOAD_TIMEOUT_MS);
// 添加超时保护,防止 Promise 永久挂起
timeoutId = setTimeout(handleTimeout, LOAD_TIMEOUT_MS);
script.src = QRCODE_CDN_URL;
script.src = cdnUrl;
script.async = true;
script.crossOrigin = 'anonymous';
document.head.appendChild(script);
});
return qrCodeLibraryPromise;
}
/**

View File

@@ -409,6 +409,10 @@ function initSentry() {
/turnstile/i,
// 浏览器扩展注入的 SweetAlert 冲突t.swal=e() 模式)
/swal/i,
// Google Tag Manager 噪音 (ESIM-TOOLS-18)
/gtag\/js/,
/gtm\.js/,
/Unexpected token '<'/,
],
denyUrls: [

View File

@@ -443,6 +443,10 @@
/turnstile/i,
// 浏览器扩展注入的 SweetAlert 冲突t.swal=e() 模式)
/t\.swal=e\(\)/i,
// Google Tag Manager 噪音 (ESIM-TOOLS-18)
/gtag\/js/,
/gtm\.js/,
/Unexpected token '<'/,
],
denyUrls: [