fix(sentry): 修复 Sentry SDK 加载问题,改用 CDN 方式

由于项目使用原生 ES 模块而非 webpack 打包,npm 导入方式无法工作。
改用 CDN 动态加载方案:

- 新增 src/js/sentry-loader.js:从 CDN 动态加载 Sentry SDK
- 重写 src/js/modules/sentry-init.js:使用 window.Sentry 而非 npm 导入
- 新增 scripts/inject-sentry-config.js:构建时注入环境变量配置
- 更新 build-static.js:集成 Sentry 配置注入步骤
- 更新 HTML 文件:添加 Sentry 配置脚本块和 CSP 策略

测试方法:部署后在控制台执行 testSentry() 或 window.testSentry()
This commit is contained in:
Abner
2025-12-13 22:16:51 +08:00
parent 0d99f1f600
commit d8b06136dc
7 changed files with 305 additions and 210 deletions

View File

@@ -4,7 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="eSIM 工具集,支持 Giffgaff 与 Simyo 的 eSIM 申请与管理、二维码生成及验证码处理。">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.googletagmanager.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data:; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com https://www.google.com;">
<!-- Sentry 错误监控配置(必须在其他脚本之前) -->
<script>
// Sentry 配置 - 在 Netlify 部署时会通过环境变量注入
window.SENTRY_DSN = ''; // 在 Netlify 环境变量中配置
window.SENTRY_ENVIRONMENT = 'production';
window.SENTRY_RELEASE = 'esim-tools@unknown';
</script>
<script src="/src/js/sentry-loader.js"></script>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://browser.sentry-cdn.com https://www.googletagmanager.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data:; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com https://*.ingest.sentry.io; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com https://www.google.com;">
<title>eSIM Tools - Giffgaff & Simyo</title>
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#6366f1">
@@ -12,6 +22,7 @@
<!-- 性能优化:预连接与预加载关键资源 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://browser.sentry-cdn.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
<link rel="preconnect" href="https://qrcode.show" crossorigin>
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>

View File

@@ -72,6 +72,10 @@ async function copyDirectory(source, destination) {
await copyEntry(entry);
}
// 注入 Sentry 配置
BuildLogger.log('🔧 注入 Sentry 配置...');
require('./inject-sentry-config.js');
BuildLogger.success(' 静态资源构建完成,输出目录 dist/');
})().catch(err => {
console.error('构建静态资源失败:', err);

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Sentry 配置注入脚本
*
* 在构建时将 Sentry 环境变量注入到 HTML 文件中
* 用法:在 npm run build 中调用此脚本
*/
const fs = require('fs');
const path = require('path');
const SENTRY_DSN = process.env.SENTRY_DSN || '';
const SENTRY_ENVIRONMENT = process.env.SENTRY_ENVIRONMENT || 'production';
const SENTRY_RELEASE = process.env.SENTRY_RELEASE ||
(process.env.COMMIT_REF ? `esim-tools@${process.env.COMMIT_REF.slice(0, 7)}` : 'esim-tools@unknown');
// 要处理的 HTML 文件列表
const htmlFiles = [
'dist/index.html',
'dist/src/giffgaff/giffgaff_modular.html',
'dist/src/giffgaff/giffgaff_complete_esim.html',
'dist/src/simyo/simyo_modular.html',
];
console.log('[Sentry Config] 开始注入配置...');
console.log(` DSN: ${SENTRY_DSN ? '已配置' : '未配置'}`);
console.log(` Environment: ${SENTRY_ENVIRONMENT}`);
console.log(` Release: ${SENTRY_RELEASE}`);
let processedCount = 0;
htmlFiles.forEach(filePath => {
const fullPath = path.join(process.cwd(), filePath);
if (!fs.existsSync(fullPath)) {
console.log(` 跳过: ${filePath} (文件不存在)`);
return;
}
let content = fs.readFileSync(fullPath, 'utf8');
// 替换配置占位符
content = content.replace(
/window\.SENTRY_DSN\s*=\s*['"][^'"]*['"]/,
`window.SENTRY_DSN = '${SENTRY_DSN}'`
);
content = content.replace(
/window\.SENTRY_ENVIRONMENT\s*=\s*['"][^'"]*['"]/,
`window.SENTRY_ENVIRONMENT = '${SENTRY_ENVIRONMENT}'`
);
content = content.replace(
/window\.SENTRY_RELEASE\s*=\s*['"][^'"]*['"]/,
`window.SENTRY_RELEASE = '${SENTRY_RELEASE}'`
);
fs.writeFileSync(fullPath, content, 'utf8');
console.log(` ✓ 已处理: ${filePath}`);
processedCount++;
});
console.log(`[Sentry Config] 完成,共处理 ${processedCount} 个文件`);

View File

@@ -5,17 +5,27 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<meta name="description" content="Giffgaff eSIM 工具,支持 OAuth 登录、MFA 验证、二维码生成与设备更换。">
<!-- Sentry 错误监控配置(必须在其他脚本之前) -->
<script>
// Sentry 配置 - 在 Netlify 部署时会通过环境变量注入
window.SENTRY_DSN = ''; // 在 Netlify 环境变量中配置
window.SENTRY_ENVIRONMENT = 'production';
window.SENTRY_RELEASE = 'esim-tools@unknown';
</script>
<script src="/src/js/sentry-loader.js"></script>
<!-- 预连接优化 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://browser.sentry-cdn.com" crossorigin>
<link rel="preconnect" href="https://qrcode.show" crossorigin>
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://id.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://publicapi.giffgaff.com" crossorigin>
<!-- CSP策略 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com https://www.google.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://browser.sentry-cdn.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com https://www.google.com https://www.gstatic.com https://*.ingest.sentry.io; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com https://www.google.com;">
<title>Giffgaff eSIM 申请工具</title>
<link rel="manifest" href="/manifest.webmanifest">

View File

@@ -1,5 +1,5 @@
/**
* Sentry 前端监控初始化模块
* Sentry 前端监控初始化模块 (CDN 版本)
*
* 职责:
* - 环境检测(仅生产环境启用)
@@ -9,173 +9,132 @@
*
* @module sentry-init
*
* 注意事项
* - 此模块通过 Webpack 打包 @sentry/browser npm 包
* - Webpack DefinePlugin 会替换 process.env.* 为实际环境变量值
* - 使用 SDK v8+ API (browserTracingIntegration)
* 使用方式
* 1. 在 HTML <head> 中加载 Sentry CDN (由 sentry-loader.js 自动处理)
* 2. 导入此模块进行初始化
*/
import * as Sentry from '@sentry/browser';
// ===================================
// 环境检测
// ===================================
/**
* 检测是否为开发环境
* 开发环境包括:
* 1. NODE_ENV === 'development' 或 'test'
* 2. localhost 访问
*/
const isDev = (() => {
// Webpack DefinePlugin 会替换 process.env.NODE_ENV
const nodeEnv = process.env.NODE_ENV || 'production';
if (nodeEnv === 'development' || nodeEnv === 'test') {
return true;
}
// 浏览器 localhost 访问也视为开发环境
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
return true;
}
return false;
if (typeof window === 'undefined') return true;
const hostname = window.location.hostname;
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.');
})();
// ===================================
// Sentry 初始化(仅生产环境)
// Sentry 配置
// ===================================
// 从 window 获取配置(由 HTML 中的内联脚本设置)
const SENTRY_DSN = window.SENTRY_DSN || '';
const SENTRY_ENVIRONMENT = window.SENTRY_ENVIRONMENT || 'production';
const SENTRY_RELEASE = window.SENTRY_RELEASE || 'esim-tools@unknown';
// ===================================
// Sentry 初始化
// ===================================
let sentryInitialized = false;
if (!isDev) {
const sentryDsn = process.env.SENTRY_DSN || '';
const sentryRelease = process.env.SENTRY_RELEASE || 'esim-tools@unknown';
const sentryEnvironment = process.env.SENTRY_ENVIRONMENT || 'production';
/**
* 初始化 Sentry在 Sentry SDK 加载后调用)
*/
function initSentry() {
if (isDev) {
console.log('[Sentry] 开发环境,使用 Mock 模式');
window.Sentry = SentryMock;
return;
}
if (!sentryDsn) {
console.warn('[Sentry] SENTRY_DSN 未配置,跳过初始化');
} else {
try {
Sentry.init({
dsn: sentryDsn,
environment: sentryEnvironment,
release: sentryRelease,
if (!window.Sentry) {
console.warn('[Sentry] SDK 未加载,跳过初始化');
window.Sentry = SentryMock;
return;
}
// ===================================
// 性能监控配置 (SDK v8+ API)
// ===================================
integrations: [
Sentry.browserTracingIntegration({
// 追踪页面导航
tracePropagationTargets: [
'localhost',
'esim.cosr.eu.org',
/^\//, // 相对路径
],
}),
],
if (!SENTRY_DSN) {
console.warn('[Sentry] DSN 未配置,跳过初始化');
window.Sentry = SentryMock;
return;
}
// 性能采样率10% = 节省配额)
tracesSampleRate: 0.1,
try {
window.Sentry.init({
dsn: SENTRY_DSN,
environment: SENTRY_ENVIRONMENT,
release: SENTRY_RELEASE,
// 错误采样率100% = 全部错误)
sampleRate: 1.0,
// 性能监控
integrations: [
window.Sentry.browserTracingIntegration(),
],
tracesSampleRate: 0.1,
// ===================================
// 错误过滤
// ===================================
ignoreErrors: [
// 浏览器内部错误
'ResizeObserver loop limit exceeded',
'ResizeObserver loop completed with undelivered notifications',
// 错误采样率
sampleRate: 1.0,
// 非标准 Promise 拒绝
'Non-Error promise rejection captured',
// 错误过滤
ignoreErrors: [
'ResizeObserver loop',
'Non-Error promise rejection',
/Loading chunk .* failed/,
/Network request failed/,
/Failed to fetch/,
/Load failed/,
/AbortError/,
/chrome-extension/,
/moz-extension/,
],
// 网络相关错误(不可控)
'Network request failed',
'Failed to fetch',
'NetworkError',
'Load failed',
denyUrls: [
/extensions\//i,
/^chrome:\/\//i,
/^moz-extension:\/\//i,
],
// 浏览器扩展引起的错误
'Extension context invalidated',
'chrome-extension://',
'moz-extension://',
],
// ===================================
// 面包屑过滤Breadcrumb
// ===================================
beforeBreadcrumb(breadcrumb) {
// 过滤 console 日志(避免日志泄露)
if (breadcrumb.category === 'console') {
return null;
// 敏感数据过滤
beforeSend(event) {
if (event.request) {
delete event.request.cookies;
if (event.request.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['x-esim-key'];
delete event.request.headers['cookie'];
}
}
// 过滤导航到外部链接
if (breadcrumb.category === 'navigation' && breadcrumb.data?.to) {
const url = breadcrumb.data.to;
if (url.startsWith('http') && !url.includes('esim.cosr.eu.org')) {
return null;
if (event.exception?.values) {
event.exception.values.forEach(exception => {
if (exception.value) {
exception.value = exception.value
.replace(/token[=:]\s*[^\s,]+/gi, 'token=***')
.replace(/key[=:]\s*[^\s,]+/gi, 'key=***')
.replace(/password[=:]\s*[^\s,]+/gi, 'password=***')
.replace(/cookie[=:]\s*[^\s,]+/gi, 'cookie=***');
}
}
});
}
return breadcrumb;
},
return event;
},
});
// ===================================
// 事件前处理(移除敏感数据)
// ===================================
beforeSend(event) {
// 移除请求中的敏感数据
if (event.request) {
delete event.request.cookies;
if (event.request.headers) {
delete event.request.headers['Authorization'];
delete event.request.headers['X-Esim-Key'];
delete event.request.headers['Cookie'];
}
// 脱敏查询参数中的 Token
if (event.request.query_string) {
event.request.query_string = event.request.query_string
.replace(/token=[^&]+/gi, 'token=***')
.replace(/key=[^&]+/gi, 'key=***')
.replace(/code=[^&]+/gi, 'code=***');
}
}
// 脱敏异常消息中的敏感信息
if (event.exception?.values) {
event.exception.values.forEach(exception => {
if (exception.value) {
exception.value = exception.value
.replace(/token[=:]\s*[^\s,]+/gi, 'token=***')
.replace(/key[=:]\s*[^\s,]+/gi, 'key=***')
.replace(/password[=:]\s*[^\s,]+/gi, 'password=***');
}
});
}
return event;
},
});
sentryInitialized = true;
console.log('[Sentry] 初始化成功');
} catch (error) {
console.error('[Sentry] 初始化失败:', error);
sentryInitialized = false;
}
sentryInitialized = true;
console.log('[Sentry] 前端初始化成功');
} catch (error) {
console.error('[Sentry] 初始化失败:', error);
window.Sentry = SentryMock;
}
}
// ===================================
// Mock 对象(开发环境或初始化失败时使用)
// Mock 对象(开发环境使用)
// ===================================
const SentryMock = {
@@ -190,114 +149,103 @@ const SentryMock = {
setUser: () => {},
setTag: () => {},
setContext: () => {},
withScope: (callback) => callback({ setContext: () => {} }),
withScope: (callback) => callback({ setContext: () => {}, setTag: () => {} }),
browserTracingIntegration: () => ({}),
};
// 导出 Sentry 实例
const SentryInstance = (isDev || !sentryInitialized) ? SentryMock : Sentry;
// ===================================
// 自动初始化
// ===================================
// 暴露到全局变量
// 如果 Sentry SDK 已加载,立即初始化
if (typeof window !== 'undefined') {
window.Sentry = SentryInstance;
if (window.Sentry && window.Sentry.init) {
initSentry();
} else {
// 等待 SDK 加载
window.addEventListener('sentry-sdk-loaded', initSentry);
// 设置超时回退
setTimeout(() => {
if (!sentryInitialized) {
console.warn('[Sentry] SDK 加载超时,使用 Mock 模式');
window.Sentry = SentryMock;
}
}, 5000);
}
}
export default SentryInstance;
// ===================================
// Helper 函数
// 导出 Helper 函数
// ===================================
/**
* 设置用户上下文(登录后调用)
* @param {string} userId - 用户 ID
* @param {string} [email] - 用户邮箱(可选)
* 获取 Sentry 实例
*/
export function getSentry() {
return window.Sentry || SentryMock;
}
/**
* 设置用户上下文
*/
export function setUser(userId, email) {
if (isDev) return;
SentryInstance.setUser({
id: userId,
email: email,
});
getSentry().setUser({ id: userId, email });
}
/**
* 清除用户上下文(登出时调用)
* 清除用户上下文
*/
export function clearUser() {
if (isDev) return;
SentryInstance.setUser(null);
}
/**
* 设置自定义标签(用于筛选和分组)
* @param {string} key - 标签名
* @param {string} value - 标签值
*/
export function setTag(key, value) {
if (isDev) return;
SentryInstance.setTag(key, value);
}
/**
* 设置自定义上下文(附加元数据)
* @param {string} name - 上下文名称
* @param {object} context - 上下文数据
*/
export function setContext(name, context) {
if (isDev) return;
SentryInstance.setContext(name, context);
getSentry().setUser(null);
}
/**
* 手动捕获异常
* @param {Error} error - 错误对象
* @param {object} [context] - 额外上下文
*/
export function captureException(error, context) {
if (isDev) {
console.error('[Sentry Dev] Exception:', error, context);
return;
}
const sentry = getSentry();
if (context) {
SentryInstance.withScope(scope => {
sentry.withScope(scope => {
Object.entries(context).forEach(([key, value]) => {
scope.setContext(key, value);
});
SentryInstance.captureException(error);
sentry.captureException(error);
});
} else {
SentryInstance.captureException(error);
sentry.captureException(error);
}
}
/**
* 手动捕获消息
* @param {string} message - 消息内容
* @param {string} [level] - 级别 (fatal|error|warning|info|debug)
*/
export function captureMessage(message, level = 'info') {
if (isDev) {
console.log(`[Sentry Dev] ${level.toUpperCase()}:`, message);
return;
}
SentryInstance.captureMessage(message, level);
getSentry().captureMessage(message, level);
}
/**
* 添加面包屑(用于错误追溯)
* @param {object} breadcrumb - 面包屑数据
* 测试 Sentry 是否正常工作
*/
export function addBreadcrumb(breadcrumb) {
if (isDev) return;
export function testSentry() {
const sentry = getSentry();
console.log('[Sentry Test] 开始测试...');
console.log('[Sentry Test] SDK 已加载:', !!window.Sentry);
console.log('[Sentry Test] 已初始化:', sentryInitialized);
console.log('[Sentry Test] 开发环境:', isDev);
console.log('[Sentry Test] DSN 已配置:', !!SENTRY_DSN);
SentryInstance.addBreadcrumb({
level: 'info',
...breadcrumb,
});
if (sentryInitialized && !isDev) {
sentry.captureMessage('Sentry 测试消息 - 前端集成正常', 'info');
console.log('[Sentry Test] ✅ 已发送测试消息到 Sentry');
} else {
console.log('[Sentry Test] ⚠️ Mock 模式,消息不会发送到 Sentry');
sentry.captureMessage('测试消息 (Mock)', 'info');
}
}
// 暴露测试函数到全局
if (typeof window !== 'undefined') {
window.testSentry = testSentry;
}
export default { getSentry, setUser, clearUser, captureException, captureMessage, testSentry };

51
src/js/sentry-loader.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* Sentry SDK CDN 加载器
*
* 在 HTML <head> 中尽早加载此脚本,它会:
* 1. 异步加载 Sentry SDK from CDN
* 2. 触发 'sentry-sdk-loaded' 事件通知其他模块
*
* 使用方法:
* <script src="/src/js/sentry-loader.js"></script>
*/
(function() {
'use strict';
// 开发环境检测
var hostname = window.location.hostname;
var isDev = hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.');
// 开发环境不加载 SDK
if (isDev) {
console.log('[Sentry Loader] 开发环境,跳过 SDK 加载');
return;
}
// Sentry SDK CDN URL (v8.x)
var SENTRY_SDK_URL = 'https://browser.sentry-cdn.com/8.40.0/bundle.tracing.min.js';
// 创建 script 标签
var script = document.createElement('script');
script.src = SENTRY_SDK_URL;
script.crossOrigin = 'anonymous';
script.async = true;
script.onload = function() {
console.log('[Sentry Loader] SDK 加载成功');
// 触发自定义事件通知其他模块
window.dispatchEvent(new CustomEvent('sentry-sdk-loaded'));
};
script.onerror = function() {
console.warn('[Sentry Loader] SDK 加载失败,错误监控将不可用');
};
// 插入到 head 最前面
var firstScript = document.getElementsByTagName('script')[0];
if (firstScript) {
firstScript.parentNode.insertBefore(script, firstScript);
} else {
document.head.appendChild(script);
}
})();

View File

@@ -5,15 +5,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<meta name="description" content="Simyo eSIM 工具,支持设备更换、短信验证码、二维码生成与安装确认,帮助您快速管理荷兰 Simyo eSIM。">
<!-- Sentry 错误监控配置(必须在其他脚本之前) -->
<script>
// Sentry 配置 - 在 Netlify 部署时会通过环境变量注入
window.SENTRY_DSN = ''; // 在 Netlify 环境变量中配置
window.SENTRY_ENVIRONMENT = 'production';
window.SENTRY_RELEASE = 'esim-tools@unknown';
</script>
<script src="/src/js/sentry-loader.js"></script>
<!-- 预连接优化 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://browser.sentry-cdn.com" crossorigin>
<link rel="preconnect" href="https://qrcode.show" crossorigin>
<link rel="preconnect" href="https://appapi.simyo.nl" crossorigin>
<!-- CSP策略 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://browser.sentry-cdn.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://challenges.cloudflare.com https://*.ingest.sentry.io; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<title>Simyo NL eSIM 申请工具</title>
<link rel="manifest" href="/manifest.webmanifest">