普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月28日首页

Sentry 错误监控系统

2026年1月28日 14:30

Vue2 项目从零搭建免费 Sentry 错误监控系统完整指南

老 Vue2 项目搭建免费的 Sentry 错误监控系统,从注册账号到最终使用的全部步骤。


第一部分:注册 Sentry 账号

1. 访问 Sentry 官网

打开浏览器,访问 sentry.io/signup/

2. 注册账号

有以下几种注册方式:

  • GitHub 账号登录(推荐,最快捷)
  • Google 账号登录
  • 邮箱注册

💡 免费套餐说明:Sentry 免费版每月支持 5,000 个错误事件,对于个人项目或小型团队完全够用。

3. 完成注册流程

  1. 选择登录/注册方式
  2. 填写组织名称(Organization Name),例如:my-company
  3. 确认邮箱(如果是邮箱注册)

第二部分:创建 Sentry 项目并获取 DSN

1. 创建新项目

登录后,进入 Sentry 控制台:

  1. 点击左侧菜单 "Projects"
  2. 点击右上角 "Create Project" 按钮
  3. 在平台列表中选择 "Vue"
  4. 设置告警规则(Alert Settings):
    • 选择 "Alert me on every new issue"(推荐新手)
    • 或者选择自定义规则
  5. 输入项目名称,例如:my-vue2-app
  6. 选择团队(如果有多个团队)
  7. 点击 "Create Project"

2. 获取 DSN(数据源名称)

项目创建成功后,系统会自动跳转到配置页面,显示你的 DSN。

如果错过了这个页面,可以通过以下方式找到 DSN:

  1. 进入项目 → Settings(设置)
  2. 左侧菜单选择 Client Keys (DSN)
  3. 复制 DSN,格式类似:
    https://xxxxxxxxxxxxxxxx@o123456.ingest.sentry.io/1234567
    

⚠️ 重要:DSN 是你项目的唯一标识,请妥善保管,不要泄露到公开的代码仓库中。


第三部分:在 Vue2 项目中安装 Sentry SDK

1. 安装依赖包

在你的 Vue2 项目根目录下执行:

# 使用 npm
npm install @sentry/vue@5 @sentry/tracing@5 --save

# 或使用 yarn
yarn add @sentry/vue@5 @sentry/tracing@5

⚠️ Vue2 必须使用 @sentry/vue@5 版本,@sentry/vue@7+ 版本只支持 Vue3。

2. 确认安装成功

查看 package.json,确保依赖已添加:

{
  "dependencies": {
    "@sentry/vue": "^5.x.x",
    "@sentry/tracing": "^5.x.x"
  }
}

第四部分:配置 Sentry 初始化代码

1. 修改入口文件 src/main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router' // 如果你使用了 vue-router

// 引入 Sentry
import * as Sentry from '@sentry/vue'
import { Integrations } from '@sentry/tracing'

// 初始化 Sentry(必须在 new Vue() 之前)
Sentry.init({
  Vue,
  dsn: 'https://你的DSN@o123456.ingest.sentry.io/1234567', // 替换为你的 DSN
  integrations: [
    new Integrations.BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
      tracingOrigins: ['localhost', 'your-domain.com', /^\//]
    })
  ],
  // 性能监控采样率(1.0 = 100%,生产环境建议设置 0.1-0.2)
  tracesSampleRate: 1.0,
  // 环境标识
  environment: process.env.NODE_ENV || 'development',
  // 在控制台也输出错误(开发时有用)
  logErrors: true
})

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

2. 如果没有使用 vue-router 的简化配置

import Vue from 'vue'
import App from './App.vue'
import * as Sentry from '@sentry/vue'

Sentry.init({
  Vue,
  dsn: 'https://你的DSN@o123456.ingest.sentry.io/1234567',
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV || 'development',
  logErrors: true
})

new Vue({
  render: h => h(App)
}).$mount('#app')

第五部分:使用环境变量管理 DSN(推荐)

为了安全起见,建议使用环境变量管理 DSN。

1. 创建环境变量文件

在项目根目录创建 .env 文件:

# .env.development(开发环境)
VUE_APP_SENTRY_DSN=https://开发环境的DSN

# .env.production(生产环境)
VUE_APP_SENTRY_DSN=https://生产环境的DSN

2. 修改 main.js 使用环境变量

Sentry.init({
  Vue,
  dsn: process.env.VUE_APP_SENTRY_DSN,
  // ... 其他配置
})

3. 将 .env 文件添加到 .gitignore

# .gitignore
.env
.env.local
.env.*.local

第六部分:测试 Sentry 是否正常工作

1. 添加测试按钮

在任意组件中添加一个测试按钮,例如 App.vue

<template>
  <div id="app">
    <h1>Sentry 测试</h1>
    <button @click="throwTestError">触发测试错误</button>
    <button @click="throwTypeError">触发类型错误</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    throwTestError() {
      throw new Error('这是一个 Sentry 测试错误!')
    },
    throwTypeError() {
      // 故意访问未定义变量的属性
      const obj = undefined
      console.log(obj.property)
    }
  }
}
</script>

2. 运行项目并测试

npm run serve
  1. 打开浏览器访问项目
  2. 点击测试按钮触发错误
  3. 打开浏览器开发者工具(F12),查看 Network 标签
  4. 你应该能看到发送到 sentry.io 的请求

3. 在 Sentry 控制台查看错误

  1. 登录 sentry.io
  2. 进入你的项目
  3. 点击左侧 "Issues" 菜单
  4. 你应该能看到刚才触发的测试错误

💡 错误通常会在几秒钟内出现在 Sentry 控制台中。


第七部分:高级配置(可选但推荐)

1. 捕获 API 请求错误

如果你使用 axios,可以添加拦截器:

// src/utils/request.js
import axios from 'axios'
import * as Sentry from '@sentry/vue'

const service = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  timeout: 10000
})

// 响应拦截器
service.interceptors.response.use(
  response => response,
  error => {
    // 将 API 错误发送到 Sentry
    Sentry.captureException(error)
    
    // 添加额外上下文信息
    Sentry.setContext('api_error', {
      url: error.config?.url,
      method: error.config?.method,
      status: error.response?.status,
      data: error.response?.data
    })
    
    return Promise.reject(error)
  }
)

export default service

2. 添加用户信息(用于追踪特定用户的问题)

// 用户登录成功后调用
import * as Sentry from '@sentry/vue'

function onUserLogin(user) {
  Sentry.setUser({
    id: user.id,
    username: user.username,
    email: user.email // 可选,注意隐私保护
  })
}

// 用户登出时清除
function onUserLogout() {
  Sentry.setUser(null)
}

3. 使用 Vue 错误边界捕获组件错误

// 在 main.js 中添加全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  Sentry.captureException(err, {
    extra: {
      componentName: vm?.$options?.name,
      propsData: vm?.$options?.propsData,
      lifecycleHook: info
    }
  })
}

4. 手动捕获错误和消息

import * as Sentry from '@sentry/vue'

// 捕获异常
try {
  riskyOperation()
} catch (error) {
  Sentry.captureException(error)
}

// 发送自定义消息
Sentry.captureMessage('用户完成了重要操作', 'info')

// 添加面包屑(用于追踪用户操作路径)
Sentry.addBreadcrumb({
  category: 'user-action',
  message: '用户点击了购买按钮',
  level: 'info'
})

5. 配置不同环境的采样率

// src/config/sentry.js
export const sentryConfig = {
  development: {
    tracesSampleRate: 1.0,  // 开发环境 100% 采样
    debug: true
  },
  production: {
    tracesSampleRate: 0.2,  // 生产环境 20% 采样(节省配额)
    debug: false
  }
}

第八部分:Source Maps 配置(用于定位压缩代码)

生产环境的代码经过压缩后,错误堆栈很难阅读。上传 Source Maps 可以让 Sentry 显示原始代码位置。

1. 安装 Sentry CLI

npm install @sentry/cli --save-dev

2. 创建 .sentryclirc 配置文件

在项目根目录创建 .sentryclirc

[defaults]
url = https://sentry.io
org = 你的组织名
project = 你的项目名

[auth]
token = 你的AuthToken

3. 获取 Auth Token

  1. 登录 Sentry
  2. 进入 SettingsAccountAPIAuth Tokens
  3. 点击 Create New Token
  4. 勾选 project:releasesorg:read 权限
  5. 复制生成的 Token

4. 在构建时上传 Source Maps

修改 package.json

{
  "scripts": {
    "build": "vue-cli-service build",
    "postbuild": "sentry-cli releases files $npm_package_version upload-sourcemaps ./dist/js --url-prefix '~/js'"
  }
}

第九部分:Sentry 控制台功能介绍

Issues(问题列表)

  • 查看所有捕获的错误
  • 按频率、影响用户数排序
  • 标记问题状态(已解决/忽略)

Performance(性能监控)

  • 页面加载时间
  • API 请求耗时
  • 性能瓶颈分析

Alerts(告警设置)

  • 设置错误阈值告警
  • 配置邮件/Slack/钉钉通知

Releases(版本管理)

  • 关联代码版本
  • 追踪哪个版本引入了问题

第十部分:常见问题排查

问题 解决方案
控制台没有收到错误 检查 DSN 是否正确;确保网络能访问 sentry.io
错误信息不完整 确认 SDK 版本正确(Vue2 用 v5)
Source Map 不生效 检查 release 版本是否匹配;确认上传成功
免费额度用完 降低 tracesSampleRate 采样率;或升级套餐

完整配置示例

最终的 src/main.js 完整代码:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import * as Sentry from '@sentry/vue'
import { Integrations } from '@sentry/tracing'

// 仅在有 DSN 时初始化 Sentry
if (process.env.VUE_APP_SENTRY_DSN) {
  Sentry.init({
    Vue,
    dsn: process.env.VUE_APP_SENTRY_DSN,
    integrations: [
      new Integrations.BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        tracingOrigins: ['localhost', /^\//]
      })
    ],
    tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
    environment: process.env.NODE_ENV,
    release: process.env.VUE_APP_VERSION || '1.0.0',
    logErrors: process.env.NODE_ENV === 'development'
  })
}

// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue Error:', err)
  Sentry.captureException(err, {
    extra: { componentName: vm?.$options?.name, info }
  })
}

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

总结

完成以上步骤后,你的 Vue2 项目就已经成功接入了 Sentry 错误监控系统。当用户在使用你的应用时遇到错误,你可以:

  1. 实时收到错误通知
  2. 查看完整的错误堆栈和上下文
  3. 了解错误影响的用户数量
  4. 追踪用户操作路径(面包屑)
  5. 关联代码版本定位问题

完整的白屏检测 SDK

2026年1月28日 11:32

前端白屏检测完整方案

白屏检测是前端监控体系中非常重要的一环,用于检测页面是否正常渲染,及时发现并上报白屏异常。

一、白屏检测的核心原理

白屏检测主要有以下几种实现思路:

  1. 采样点检测法 - 在页面关键位置采样,判断是否有有效内容
  2. DOM 元素检测法 - 检测页面关键 DOM 元素是否存在
  3. MutationObserver 监听法 - 监听 DOM 变化判断页面渲染状态
  4. 骨架屏检测法 - 检测骨架屏是否被替换为实际内容
  5. 截图对比法 - 通过 Canvas 截图分析页面内容
  6. Performance API 检测法 - 利用浏览器性能 API 判断渲染状态

二、完整的白屏检测 SDK 实现

// ==================== 类型定义 ====================

/**
 * 白屏检测配置接口
 */
interface WhiteScreenConfig {
  // 采样点数量(水平和垂直方向)
  samplingPoints?: number;
  // 检测延迟时间(毫秒)
  delay?: number;
  // 检测超时时间(毫秒)
  timeout?: number;
  // 白屏阈值(0-1之间,超过该比例认为是白屏)
  threshold?: number;
  // 是否启用 DOM 检测
  enableDOMDetection?: boolean;
  // 是否启用采样点检测
  enableSamplingDetection?: boolean;
  // 是否启用 MutationObserver 检测
  enableMutationDetection?: boolean;
  // 是否启用截图检测
  enableScreenshotDetection?: boolean;
  // 是否启用骨架屏检测
  enableSkeletonDetection?: boolean;
  // 骨架屏容器选择器
  skeletonSelector?: string;
  // 关键元素选择器列表
  keyElementSelectors?: string[];
  // 需要忽略的元素选择器
  ignoreSelectors?: string[];
  // 容器元素(默认为 document.body)
  container?: HTMLElement | null;
  // 上报回调函数
  onReport?: (data: WhiteScreenReport) => void;
  // 检测完成回调
  onDetectionComplete?: (result: DetectionResult) => void;
  // 是否在开发环境启用
  enableInDev?: boolean;
  // 最大重试次数
  maxRetries?: number;
  // 重试间隔(毫秒)
  retryInterval?: number;
  // 自定义白屏判断函数
  customDetector?: () => boolean | Promise<boolean>;
}

/**
 * 白屏检测报告接口
 */
interface WhiteScreenReport {
  // 是否白屏
  isWhiteScreen: boolean;
  // 检测时间戳
  timestamp: number;
  // 页面 URL
  url: string;
  // 检测方法
  detectionMethod: DetectionMethod;
  // 采样点结果
  samplingResult?: SamplingResult;
  // DOM 检测结果
  domResult?: DOMDetectionResult;
  // 截图检测结果
  screenshotResult?: ScreenshotResult;
  // 骨架屏检测结果
  skeletonResult?: SkeletonResult;
  // 页面性能数据
  performanceData?: PerformanceData;
  // 用户代理信息
  userAgent: string;
  // 视口尺寸
  viewport: ViewportSize;
  // 设备像素比
  devicePixelRatio: number;
  // 网络信息
  networkInfo?: NetworkInfo;
  // 错误信息
  errorInfo?: ErrorInfo;
  // 自定义数据
  customData?: Record<string, unknown>;
}

/**
 * 检测方法枚举
 */
enum DetectionMethod {
  SAMPLING = 'sampling',
  DOM = 'dom',
  MUTATION = 'mutation',
  SCREENSHOT = 'screenshot',
  SKELETON = 'skeleton',
  PERFORMANCE = 'performance',
  CUSTOM = 'custom',
  COMBINED = 'combined'
}

/**
 * 采样点结果接口
 */
interface SamplingResult {
  // 总采样点数
  totalPoints: number;
  // 空白点数
  emptyPoints: number;
  // 空白比例
  emptyRatio: number;
  // 采样点详情
  pointDetails: SamplingPointDetail[];
}

/**
 * 采样点详情
 */
interface SamplingPointDetail {
  // X 坐标
  x: number;
  // Y 坐标
  y: number;
  // 元素标签名
  tagName: string | null;
  // 是否为空白点
  isEmpty: boolean;
  // 元素类名
  className?: string;
  // 元素 ID
  id?: string;
}

/**
 * DOM 检测结果接口
 */
interface DOMDetectionResult {
  // 是否通过检测
  passed: boolean;
  // 检测到的关键元素数量
  foundElements: number;
  // 期望的关键元素数量
  expectedElements: number;
  // 缺失的元素选择器
  missingSelectors: string[];
  // 元素详情
  elementDetails: ElementDetail[];
}

/**
 * 元素详情
 */
interface ElementDetail {
  // 选择器
  selector: string;
  // 是否存在
  exists: boolean;
  // 是否可见
  isVisible?: boolean;
  // 元素尺寸
  dimensions?: {
    width: number;
    height: number;
  };
}

/**
 * 截图检测结果
 */
interface ScreenshotResult {
  // 是否白屏
  isWhiteScreen: boolean;
  // 白色像素比例
  whitePixelRatio: number;
  // 总像素数
  totalPixels: number;
  // 白色像素数
  whitePixels: number;
  // 颜色分布
  colorDistribution?: ColorDistribution;
}

/**
 * 颜色分布
 */
interface ColorDistribution {
  white: number;
  black: number;
  gray: number;
  colored: number;
}

/**
 * 骨架屏检测结果
 */
interface SkeletonResult {
  // 骨架屏是否存在
  skeletonExists: boolean;
  // 骨架屏是否已移除
  skeletonRemoved: boolean;
  // 检测时间
  detectionTime: number;
}

/**
 * 性能数据
 */
interface PerformanceData {
  // DOM 加载完成时间
  domContentLoaded?: number;
  // 页面完全加载时间
  loadComplete?: number;
  // 首次内容绘制时间
  firstContentfulPaint?: number;
  // 最大内容绘制时间
  largestContentfulPaint?: number;
  // 首次输入延迟
  firstInputDelay?: number;
  // 累计布局偏移
  cumulativeLayoutShift?: number;
  // 可交互时间
  timeToInteractive?: number;
}

/**
 * 视口尺寸
 */
interface ViewportSize {
  width: number;
  height: number;
}

/**
 * 网络信息
 */
interface NetworkInfo {
  // 网络类型
  effectiveType?: string;
  // 下行带宽
  downlink?: number;
  // RTT
  rtt?: number;
  // 是否在线
  online: boolean;
}

/**
 * 错误信息
 */
interface ErrorInfo {
  // 错误消息
  message: string;
  // 错误堆栈
  stack?: string;
  // 错误类型
  type: string;
}

/**
 * 检测结果
 */
interface DetectionResult {
  // 是否白屏
  isWhiteScreen: boolean;
  // 置信度(0-1)
  confidence: number;
  // 检测方法
  methods: DetectionMethod[];
  // 各方法结果
  methodResults: Map<DetectionMethod, boolean>;
  // 最终判定依据
  basis: string;
}

// ==================== 工具函数 ====================

/**
 * 防抖函数
 */
function debounce<T extends (...args: unknown[]) => unknown>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  
  return function (this: unknown, ...args: Parameters<T>) {
    if (timeoutId !== null) {
      clearTimeout(timeoutId);
    }
    
    timeoutId = setTimeout(() => {
      func.apply(this, args);
      timeoutId = null;
    }, wait);
  };
}

/**
 * 节流函数
 */
function throttle<T extends (...args: unknown[]) => unknown>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  
  return function (this: unknown, ...args: Parameters<T>) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

/**
 * 延迟执行
 */
function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * 带超时的 Promise
 */
function withTimeout<T>(
  promise: Promise<T>,
  ms: number,
  errorMessage = 'Operation timed out'
): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error(errorMessage)), ms);
  });
  
  return Promise.race([promise, timeout]);
}

/**
 * 重试函数
 */
async function retry<T>(
  fn: () => Promise<T>,
  maxRetries: number,
  retryInterval: number
): Promise<T> {
  let lastError: Error | null = null;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      if (i < maxRetries - 1) {
        await delay(retryInterval);
      }
    }
  }
  
  throw lastError;
}

/**
 * 生成唯一 ID
 */
function generateUniqueId(): string {
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

/**
 * 深度合并对象
 */
function deepMerge<T extends Record<string, unknown>>(
  target: T,
  source: Partial<T>
): T {
  const result = { ...target };
  
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      const sourceValue = source[key];
      const targetValue = result[key];
      
      if (
        typeof sourceValue === 'object' &&
        sourceValue !== null &&
        !Array.isArray(sourceValue) &&
        typeof targetValue === 'object' &&
        targetValue !== null &&
        !Array.isArray(targetValue)
      ) {
        result[key] = deepMerge(
          targetValue as Record<string, unknown>,
          sourceValue as Record<string, unknown>
        ) as T[Extract<keyof T, string>];
      } else if (sourceValue !== undefined) {
        result[key] = sourceValue as T[Extract<keyof T, string>];
      }
    }
  }
  
  return result;
}

/**
 * 检查元素是否可见
 */
function isElementVisible(element: Element): boolean {
  if (!element) return false;
  
  const style = window.getComputedStyle(element);
  
  if (style.display === 'none') return false;
  if (style.visibility === 'hidden') return false;
  if (style.opacity === '0') return false;
  
  const rect = element.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) return false;
  
  return true;
}

/**
 * 获取元素在指定坐标处
 */
function getElementAtPoint(x: number, y: number): Element | null {
  try {
    return document.elementFromPoint(x, y);
  } catch {
    return null;
  }
}

/**
 * 判断是否为包装元素(通常用于布局的空元素)
 */
function isWrapperElement(element: Element | null): boolean {
  if (!element) return true;
  
  const wrapperTags = [
    'HTML', 'BODY', 'DIV', 'SECTION', 'ARTICLE', 'MAIN',
    'HEADER', 'FOOTER', 'NAV', 'ASIDE', 'SPAN'
  ];
  
  const tagName = element.tagName.toUpperCase();
  
  if (!wrapperTags.includes(tagName)) {
    return false;
  }
  
  // 检查是否有实际内容
  const hasText = element.textContent?.trim().length ?? 0 > 0;
  const hasChildren = element.children.length > 0;
  const hasBackground = hasBackgroundContent(element);
  
  // 如果只是空的包装元素,认为是包装元素
  if (!hasText && !hasBackground && element === document.body) {
    return true;
  }
  
  return false;
}

/**
 * 检查元素是否有背景内容
 */
function hasBackgroundContent(element: Element): boolean {
  const style = window.getComputedStyle(element);
  
  // 检查背景颜色(排除白色和透明)
  const bgColor = style.backgroundColor;
  if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
    // 解析颜色值判断是否为白色
    const isWhite = isWhiteColor(bgColor);
    if (!isWhite) return true;
  }
  
  // 检查背景图片
  const bgImage = style.backgroundImage;
  if (bgImage && bgImage !== 'none') {
    return true;
  }
  
  return false;
}

/**
 * 判断颜色是否为白色
 */
function isWhiteColor(color: string): boolean {
  // 处理 rgb/rgba 格式
  const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (rgbMatch) {
    const [, r, g, b] = rgbMatch.map(Number);
    // 接近白色的阈值
    return r > 250 && g > 250 && b > 250;
  }
  
  // 处理十六进制格式
  if (color.startsWith('#')) {
    const hex = color.slice(1);
    const r = parseInt(hex.slice(0, 2), 16);
    const g = parseInt(hex.slice(2, 4), 16);
    const b = parseInt(hex.slice(4, 6), 16);
    return r > 250 && g > 250 && b > 250;
  }
  
  return color === 'white' || color === '#fff' || color === '#ffffff';
}

/**
 * 获取网络信息
 */
function getNetworkInfo(): NetworkInfo {
  const connection = (navigator as Navigator & {
    connection?: {
      effectiveType?: string;
      downlink?: number;
      rtt?: number;
    };
  }).connection;
  
  return {
    effectiveType: connection?.effectiveType,
    downlink: connection?.downlink,
    rtt: connection?.rtt,
    online: navigator.onLine
  };
}

/**
 * 获取性能数据
 */
function getPerformanceData(): PerformanceData {
  const performanceData: PerformanceData = {};
  
  // 获取导航性能数据
  const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
  if (navigation) {
    performanceData.domContentLoaded = navigation.domContentLoadedEventEnd - navigation.startTime;
    performanceData.loadComplete = navigation.loadEventEnd - navigation.startTime;
  }
  
  // 获取绘制性能数据
  const paintEntries = performance.getEntriesByType('paint');
  paintEntries.forEach(entry => {
    if (entry.name === 'first-contentful-paint') {
      performanceData.firstContentfulPaint = entry.startTime;
    }
  });
  
  // 获取 LCP
  try {
    const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
    if (lcpEntries.length > 0) {
      const lastLcp = lcpEntries[lcpEntries.length - 1] as PerformanceEntry & { startTime: number };
      performanceData.largestContentfulPaint = lastLcp.startTime;
    }
  } catch {
    // LCP 可能不被支持
  }
  
  return performanceData;
}

// ==================== 采样点检测器 ====================

/**
 * 采样点检测器类
 */
class SamplingDetector {
  private config: Required<Pick<WhiteScreenConfig, 
    'samplingPoints' | 'threshold' | 'ignoreSelectors' | 'container'
  >>;
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      samplingPoints: config.samplingPoints ?? 17,
      threshold: config.threshold ?? 0.95,
      ignoreSelectors: config.ignoreSelectors ?? [],
      container: config.container ?? document.body
    };
  }
  
  /**
   * 执行采样检测
   */
  detect(): SamplingResult {
    const { samplingPoints, ignoreSelectors, container } = this.config;
    const points: SamplingPointDetail[] = [];
    
    // 获取视口尺寸
    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    
    // 计算采样间隔
    const horizontalStep = viewportWidth / (samplingPoints + 1);
    const verticalStep = viewportHeight / (samplingPoints + 1);
    
    let emptyPoints = 0;
    
    // 生成采样点矩阵
    for (let i = 1; i <= samplingPoints; i++) {
      for (let j = 1; j <= samplingPoints; j++) {
        const x = Math.floor(horizontalStep * i);
        const y = Math.floor(verticalStep * j);
        
        const pointResult = this.checkPoint(x, y, ignoreSelectors);
        points.push(pointResult);
        
        if (pointResult.isEmpty) {
          emptyPoints++;
        }
      }
    }
    
    // 添加中心点检测(权重更高)
    const centerX = Math.floor(viewportWidth / 2);
    const centerY = Math.floor(viewportHeight / 2);
    const centerPoints = this.checkCenterRegion(centerX, centerY, ignoreSelectors);
    points.push(...centerPoints);
    
    centerPoints.forEach(point => {
      if (point.isEmpty) emptyPoints++;
    });
    
    const totalPoints = points.length;
    const emptyRatio = totalPoints > 0 ? emptyPoints / totalPoints : 1;
    
    return {
      totalPoints,
      emptyPoints,
      emptyRatio,
      pointDetails: points
    };
  }
  
  /**
   * 检查单个采样点
   */
  private checkPoint(
    x: number,
    y: number,
    ignoreSelectors: string[]
  ): SamplingPointDetail {
    const element = getElementAtPoint(x, y);
    
    const detail: SamplingPointDetail = {
      x,
      y,
      tagName: element?.tagName ?? null,
      isEmpty: true,
      className: element?.className?.toString(),
      id: element?.id
    };
    
    if (!element) {
      return detail;
    }
    
    // 检查是否在忽略列表中
    if (this.shouldIgnoreElement(element, ignoreSelectors)) {
      return detail;
    }
    
    // 检查是否为有效内容元素
    detail.isEmpty = !this.isContentElement(element);
    
    return detail;
  }
  
  /**
   * 检查中心区域(九宫格)
   */
  private checkCenterRegion(
    centerX: number,
    centerY: number,
    ignoreSelectors: string[]
  ): SamplingPointDetail[] {
    const points: SamplingPointDetail[] = [];
    const offsets = [-50, 0, 50];
    
    for (const offsetX of offsets) {
      for (const offsetY of offsets) {
        if (offsetX === 0 && offsetY === 0) continue; // 跳过正中心,已在主循环中处理
        
        const x = centerX + offsetX;
        const y = centerY + offsetY;
        
        if (x > 0 && y > 0) {
          points.push(this.checkPoint(x, y, ignoreSelectors));
        }
      }
    }
    
    return points;
  }
  
  /**
   * 判断元素是否应被忽略
   */
  private shouldIgnoreElement(element: Element, ignoreSelectors: string[]): boolean {
    for (const selector of ignoreSelectors) {
      try {
        if (element.matches(selector) || element.closest(selector)) {
          return true;
        }
      } catch {
        // 选择器无效,忽略
      }
    }
    return false;
  }
  
  /**
   * 判断是否为有内容的元素
   */
  private isContentElement(element: Element): boolean {
    const tagName = element.tagName.toUpperCase();
    
    // 明确的内容元素
    const contentTags = [
      'IMG', 'VIDEO', 'AUDIO', 'CANVAS', 'SVG', 'IFRAME',
      'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON',
      'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
      'P', 'A', 'SPAN', 'LABEL', 'LI', 'TD', 'TH',
      'STRONG', 'EM', 'B', 'I', 'U', 'CODE', 'PRE'
    ];
    
    if (contentTags.includes(tagName)) {
      return isElementVisible(element);
    }
    
    // 检查是否有文本内容
    const textContent = element.textContent?.trim();
    if (textContent && textContent.length > 0) {
      return isElementVisible(element);
    }
    
    // 检查是否有背景内容
    if (hasBackgroundContent(element)) {
      return isElementVisible(element);
    }
    
    // 检查是否为包装元素
    if (isWrapperElement(element)) {
      return false;
    }
    
    return isElementVisible(element);
  }
  
  /**
   * 判断是否为白屏
   */
  isWhiteScreen(result: SamplingResult): boolean {
    return result.emptyRatio >= this.config.threshold;
  }
}

// ==================== DOM 检测器 ====================

/**
 * DOM 检测器类
 */
class DOMDetector {
  private config: Required<Pick<WhiteScreenConfig, 
    'keyElementSelectors' | 'container'
  >>;
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      keyElementSelectors: config.keyElementSelectors ?? [
        '#app', '#root', '.app', '.main-content',
        'main', '[data-page]', '.page-container'
      ],
      container: config.container ?? document.body
    };
  }
  
  /**
   * 执行 DOM 检测
   */
  detect(): DOMDetectionResult {
    const { keyElementSelectors, container } = this.config;
    const elementDetails: ElementDetail[] = [];
    const missingSelectors: string[] = [];
    let foundElements = 0;
    
    for (const selector of keyElementSelectors) {
      const result = this.checkElement(selector, container);
      elementDetails.push(result);
      
      if (result.exists && result.isVisible) {
        foundElements++;
      } else {
        missingSelectors.push(selector);
      }
    }
    
    const expectedElements = keyElementSelectors.length;
    const passed = foundElements > 0;
    
    return {
      passed,
      foundElements,
      expectedElements,
      missingSelectors,
      elementDetails
    };
  }
  
  /**
   * 检查单个元素
   */
  private checkElement(
    selector: string,
    container: HTMLElement | null
  ): ElementDetail {
    const searchRoot = container ?? document;
    
    try {
      const element = searchRoot.querySelector(selector);
      
      if (!element) {
        return {
          selector,
          exists: false,
          isVisible: false
        };
      }
      
      const isVisible = isElementVisible(element);
      const rect = element.getBoundingClientRect();
      
      return {
        selector,
        exists: true,
        isVisible,
        dimensions: {
          width: rect.width,
          height: rect.height
        }
      };
    } catch {
      return {
        selector,
        exists: false,
        isVisible: false
      };
    }
  }
  
  /**
   * 检测页面是否有有效内容
   */
  hasValidContent(): boolean {
    const body = document.body;
    if (!body) return false;
    
    // 检查 body 是否有子元素
    if (body.children.length === 0) return false;
    
    // 检查是否有可见的子元素
    const children = Array.from(body.children);
    const visibleChildren = children.filter(child => isElementVisible(child));
    
    if (visibleChildren.length === 0) return false;
    
    // 检查是否有实际内容(文本或媒体)
    const hasContent = visibleChildren.some(child => {
      // 检查文本内容
      const text = child.textContent?.trim();
      if (text && text.length > 0) return true;
      
      // 检查媒体元素
      const mediaElements = child.querySelectorAll('img, video, canvas, svg, iframe');
      if (mediaElements.length > 0) return true;
      
      // 检查背景
      if (hasBackgroundContent(child)) return true;
      
      return false;
    });
    
    return hasContent;
  }
  
  /**
   * 获取页面 DOM 统计信息
   */
  getDOMStats(): {
    totalElements: number;
    visibleElements: number;
    textNodes: number;
    mediaElements: number;
    interactiveElements: number;
  } {
    const allElements = document.querySelectorAll('*');
    let visibleElements = 0;
    let textNodes = 0;
    let mediaElements = 0;
    let interactiveElements = 0;
    
    allElements.forEach(element => {
      if (isElementVisible(element)) {
        visibleElements++;
      }
      
      const tagName = element.tagName.toUpperCase();
      
      if (['IMG', 'VIDEO', 'AUDIO', 'CANVAS', 'SVG', 'IFRAME'].includes(tagName)) {
        mediaElements++;
      }
      
      if (['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'A'].includes(tagName)) {
        interactiveElements++;
      }
    });
    
    // 统计文本节点
    const walker = document.createTreeWalker(
      document.body,
      NodeFilter.SHOW_TEXT,
      null
    );
    
    while (walker.nextNode()) {
      const node = walker.currentNode;
      if (node.textContent?.trim()) {
        textNodes++;
      }
    }
    
    return {
      totalElements: allElements.length,
      visibleElements,
      textNodes,
      mediaElements,
      interactiveElements
    };
  }
}

// ==================== MutationObserver 检测器 ====================

/**
 * MutationObserver 检测器类
 */
class MutationDetector {
  private observer: MutationObserver | null = null;
  private mutations: MutationRecord[] = [];
  private startTime: number = 0;
  private isObserving: boolean = false;
  private config: {
    timeout: number;
    minMutations: number;
    stableTime: number;
  };
  
  private resolvePromise: ((value: boolean) => void) | null = null;
  private timeoutId: ReturnType<typeof setTimeout> | null = null;
  private stableTimeoutId: ReturnType<typeof setTimeout> | null = null;
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      timeout: config.timeout ?? 10000,
      minMutations: 10,
      stableTime: 1000
    };
  }
  
  /**
   * 开始观察
   */
  observe(): Promise<boolean> {
    return new Promise((resolve) => {
      this.resolvePromise = resolve;
      this.startTime = Date.now();
      this.mutations = [];
      this.isObserving = true;
      
      // 创建 MutationObserver
      this.observer = new MutationObserver((mutations) => {
        this.handleMutations(mutations);
      });
      
      // 配置观察选项
      const observerConfig: MutationObserverInit = {
        childList: true,
        subtree: true,
        attributes: true,
        characterData: true,
        attributeOldValue: false,
        characterDataOldValue: false
      };
      
      // 开始观察
      this.observer.observe(document.body, observerConfig);
      
      // 设置超时
      this.timeoutId = setTimeout(() => {
        this.complete(false);
      }, this.config.timeout);
    });
  }
  
  /**
   * 处理 mutation 记录
   */
  private handleMutations(mutations: MutationRecord[]): void {
    if (!this.isObserving) return;
    
    this.mutations.push(...mutations);
    
    // 重置稳定计时器
    if (this.stableTimeoutId) {
      clearTimeout(this.stableTimeoutId);
    }
    
    // 设置新的稳定计时器
    this.stableTimeoutId = setTimeout(() => {
      this.checkStability();
    }, this.config.stableTime);
  }
  
  /**
   * 检查页面稳定性
   */
  private checkStability(): void {
    if (!this.isObserving) return;
    
    // 检查是否有足够的 mutations(表示页面有渲染活动)
    const hasSufficientMutations = this.mutations.length >= this.config.minMutations;
    
    // 检查是否有有意义的内容变化
    const hasContentChanges = this.hasContentMutations();
    
    if (hasSufficientMutations && hasContentChanges) {
      this.complete(true);
    } else if (Date.now() - this.startTime > this.config.timeout / 2) {
      // 如果已经过了一半的超时时间,且没有足够的活动,可能是白屏
      this.complete(false);
    }
  }
  
  /**
   * 检查是否有内容相关的 mutations
   */
  private hasContentMutations(): boolean {
    let contentMutations = 0;
    
    for (const mutation of this.mutations) {
      if (mutation.type === 'childList') {
        // 检查是否添加了有意义的节点
        const addedNodes = Array.from(mutation.addedNodes);
        for (const node of addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE) {
            const element = node as Element;
            if (isElementVisible(element)) {
              contentMutations++;
            }
          } else if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent?.trim()) {
              contentMutations++;
            }
          }
        }
      }
    }
    
    return contentMutations >= 5;
  }
  
  /**
   * 完成检测
   */
  private complete(hasContent: boolean): void {
    if (!this.isObserving) return;
    
    this.isObserving = false;
    
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
    
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
      this.timeoutId = null;
    }
    
    if (this.stableTimeoutId) {
      clearTimeout(this.stableTimeoutId);
      this.stableTimeoutId = null;
    }
    
    if (this.resolvePromise) {
      this.resolvePromise(hasContent);
      this.resolvePromise = null;
    }
  }
  
  /**
   * 获取 mutation 统计
   */
  getMutationStats(): {
    totalMutations: number;
    childListMutations: number;
    attributeMutations: number;
    characterDataMutations: number;
    duration: number;
  } {
    let childListMutations = 0;
    let attributeMutations = 0;
    let characterDataMutations = 0;
    
    for (const mutation of this.mutations) {
      switch (mutation.type) {
        case 'childList':
          childListMutations++;
          break;
        case 'attributes':
          attributeMutations++;
          break;
        case 'characterData':
          characterDataMutations++;
          break;
      }
    }
    
    return {
      totalMutations: this.mutations.length,
      childListMutations,
      attributeMutations,
      characterDataMutations,
      duration: Date.now() - this.startTime
    };
  }
  
  /**
   * 停止观察
   */
  stop(): void {
    this.complete(false);
  }
}

// ==================== 截图检测器 ====================

/**
 * 截图检测器类
 */
class ScreenshotDetector {
  private canvas: HTMLCanvasElement | null = null;
  private ctx: CanvasRenderingContext2D | null = null;
  private config: {
    sampleWidth: number;
    sampleHeight: number;
    whiteThreshold: number;
    brightnessThreshold: number;
  };
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      sampleWidth: 100,
      sampleHeight: 100,
      whiteThreshold: config.threshold ?? 0.95,
      brightnessThreshold: 250
    };
  }
  
  /**
   * 初始化 Canvas
   */
  private initCanvas(): boolean {
    try {
      this.canvas = document.createElement('canvas');
      this.canvas.width = this.config.sampleWidth;
      this.canvas.height = this.config.sampleHeight;
      this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
      return this.ctx !== null;
    } catch {
      return false;
    }
  }
  
  /**
   * 执行截图检测
   */
  async detect(): Promise<ScreenshotResult> {
    if (!this.initCanvas() || !this.canvas || !this.ctx) {
      return {
        isWhiteScreen: false,
        whitePixelRatio: 0,
        totalPixels: 0,
        whitePixels: 0
      };
    }
    
    try {
      // 使用 html2canvas 或原生方法截图
      await this.captureScreen();
      
      // 分析图像
      return this.analyzeImage();
    } catch (error) {
      console.error('Screenshot detection failed:', error);
      return {
        isWhiteScreen: false,
        whitePixelRatio: 0,
        totalPixels: 0,
        whitePixels: 0
      };
    }
  }
  
  /**
   * 捕获屏幕
   */
  private async captureScreen(): Promise<void> {
    if (!this.canvas || !this.ctx) return;
    
    const { sampleWidth, sampleHeight } = this.config;
    
    // 方案1: 使用 html2canvas(需要引入库)
    // 这里使用简化的方案:遍历可见元素并绘制
    
    // 先填充白色背景
    this.ctx.fillStyle = '#ffffff';
    this.ctx.fillRect(0, 0, sampleWidth, sampleHeight);
    
    // 获取 body 的背景色
    const bodyStyle = window.getComputedStyle(document.body);
    const bodyBgColor = bodyStyle.backgroundColor;
    if (bodyBgColor && bodyBgColor !== 'rgba(0, 0, 0, 0)') {
      this.ctx.fillStyle = bodyBgColor;
      this.ctx.fillRect(0, 0, sampleWidth, sampleHeight);
    }
    
    // 绘制可见元素的简化表示
    const elements = document.querySelectorAll('*');
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
    const scaleX = sampleWidth / viewportWidth;
    const scaleY = sampleHeight / viewportHeight;
    
    elements.forEach(element => {
      if (!isElementVisible(element)) return;
      
      const rect = element.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) return;
      
      // 检查是否在视口内
      if (rect.right < 0 || rect.bottom < 0) return;
      if (rect.left > viewportWidth || rect.top > viewportHeight) return;
      
      const style = window.getComputedStyle(element);
      const bgColor = style.backgroundColor;
      
      // 只绘制有背景色的元素
      if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
        const x = rect.left * scaleX;
        const y = rect.top * scaleY;
        const width = rect.width * scaleX;
        const height = rect.height * scaleY;
        
        this.ctx!.fillStyle = bgColor;
        this.ctx!.fillRect(x, y, width, height);
      }
      
      // 绘制文本区域
      const text = element.textContent?.trim();
      if (text && text.length > 0 && element.children.length === 0) {
        const x = rect.left * scaleX;
        const y = rect.top * scaleY;
        const width = rect.width * scaleX;
        const height = rect.height * scaleY;
        
        // 用深色表示文本区域
        this.ctx!.fillStyle = style.color || '#000000';
        this.ctx!.fillRect(x, y, Math.max(width, 2), Math.max(height, 2));
      }
    });
    
    // 绘制图片元素
    const images = document.querySelectorAll('img');
    for (const img of images) {
      if (!isElementVisible(img)) continue;
      
      const rect = img.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) continue;
      
      try {
        const x = rect.left * scaleX;
        const y = rect.top * scaleY;
        const width = rect.width * scaleX;
        const height = rect.height * scaleY;
        
        // 用灰色表示图片区域
        this.ctx!.fillStyle = '#808080';
        this.ctx!.fillRect(x, y, width, height);
      } catch {
        // 跨域图片无法绘制
      }
    }
  }
  
  /**
   * 分析图像
   */
  private analyzeImage(): ScreenshotResult {
    if (!this.canvas || !this.ctx) {
      return {
        isWhiteScreen: false,
        whitePixelRatio: 0,
        totalPixels: 0,
        whitePixels: 0
      };
    }
    
    const { sampleWidth, sampleHeight, brightnessThreshold } = this.config;
    const imageData = this.ctx.getImageData(0, 0, sampleWidth, sampleHeight);
    const data = imageData.data;
    
    let whitePixels = 0;
    let blackPixels = 0;
    let grayPixels = 0;
    let coloredPixels = 0;
    const totalPixels = (sampleWidth * sampleHeight);
    
    // 遍历像素
    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      
      // 计算亮度
      const brightness = (r + g + b) / 3;
      
      // 判断像素颜色类型
      if (brightness >= brightnessThreshold) {
        whitePixels++;
      } else if (brightness <= 10) {
        blackPixels++;
      } else if (Math.abs(r - g) <= 20 && Math.abs(g - b) <= 20 && Math.abs(r - b) <= 20) {
        grayPixels++;
      } else {
        coloredPixels++;
      }
    }
    
    const whitePixelRatio = whitePixels / totalPixels;
    const isWhiteScreen = whitePixelRatio >= this.config.whiteThreshold;
    
    return {
      isWhiteScreen,
      whitePixelRatio,
      totalPixels,
      whitePixels,
      colorDistribution: {
        white: whitePixels / totalPixels,
        black: blackPixels / totalPixels,
        gray: grayPixels / totalPixels,
        colored: coloredPixels / totalPixels
      }
    };
  }
  
  /**
   * 释放资源
   */
  dispose(): void {
    this.canvas = null;
    this.ctx = null;
  }
}

// ==================== 骨架屏检测器 ====================

/**
 * 骨架屏检测器类
 */
class SkeletonDetector {
  private config: {
    skeletonSelector: string;
    timeout: number;
    checkInterval: number;
  };
  
  private intervalId: ReturnType<typeof setInterval> | null = null;
  private startTime: number = 0;
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      skeletonSelector: config.skeletonSelector ?? '.skeleton, [data-skeleton], .loading-skeleton',
      timeout: config.timeout ?? 10000,
      checkInterval: 500
    };
  }
  
  /**
   * 检测骨架屏状态
   */
  detect(): Promise<SkeletonResult> {
    return new Promise((resolve) => {
      this.startTime = Date.now();
      
      // 首先检查骨架屏是否存在
      const skeletonExists = this.isSkeletonPresent();
      
      if (!skeletonExists) {
        // 骨架屏不存在,可能已经加载完成或根本没有骨架屏
        resolve({
          skeletonExists: false,
          skeletonRemoved: true,
          detectionTime: 0
        });
        return;
      }
      
      // 开始定期检查骨架屏是否消失
      this.intervalId = setInterval(() => {
        const elapsed = Date.now() - this.startTime;
        
        if (!this.isSkeletonPresent()) {
          // 骨架屏已消失
          this.stop();
          resolve({
            skeletonExists: true,
            skeletonRemoved: true,
            detectionTime: elapsed
          });
          return;
        }
        
        if (elapsed >= this.config.timeout) {
          // 超时,骨架屏仍然存在
          this.stop();
          resolve({
            skeletonExists: true,
            skeletonRemoved: false,
            detectionTime: elapsed
          });
        }
      }, this.config.checkInterval);
    });
  }
  
  /**
   * 检查骨架屏是否存在
   */
  private isSkeletonPresent(): boolean {
    const { skeletonSelector } = this.config;
    
    try {
      const selectors = skeletonSelector.split(',').map(s => s.trim());
      
      for (const selector of selectors) {
        const elements = document.querySelectorAll(selector);
        
        for (const element of elements) {
          if (isElementVisible(element)) {
            return true;
          }
        }
      }
      
      return false;
    } catch {
      return false;
    }
  }
  
  /**
   * 停止检测
   */
  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
  
  /**
   * 获取所有骨架屏元素
   */
  getSkeletonElements(): Element[] {
    const { skeletonSelector } = this.config;
    const elements: Element[] = [];
    
    try {
      const selectors = skeletonSelector.split(',').map(s => s.trim());
      
      for (const selector of selectors) {
        const found = document.querySelectorAll(selector);
        elements.push(...Array.from(found));
      }
    } catch {
      // 选择器无效
    }
    
    return elements;
  }
}

// ==================== Performance API 检测器 ====================

/**
 * Performance API 检测器类
 */
class PerformanceDetector {
  private config: {
    fcpThreshold: number;
    lcpThreshold: number;
  };
  
  private lcpObserver: PerformanceObserver | null = null;
  private lcpValue: number = 0;
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    this.config = {
      fcpThreshold: 3000,
      lcpThreshold: 4000
    };
  }
  
  /**
   * 初始化 LCP 观察器
   */
  initLCPObserver(): void {
    try {
      this.lcpObserver = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        if (entries.length > 0) {
          const lastEntry = entries[entries.length - 1] as PerformanceEntry & { startTime: number };
          this.lcpValue = lastEntry.startTime;
        }
      });
      
      this.lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
    } catch {
      // LCP 观察器可能不被支持
    }
  }
  
  /**
   * 获取性能指标
   */
  getMetrics(): PerformanceData {
    const data = getPerformanceData();
    
    // 添加 LCP
    if (this.lcpValue > 0) {
      data.largestContentfulPaint = this.lcpValue;
    }
    
    return data;
  }
  
  /**
   * 判断是否可能白屏(基于性能指标)
   */
  isPotentialWhiteScreen(): boolean {
    const metrics = this.getMetrics();
    
    // 如果 FCP 超过阈值,可能是白屏
    if (metrics.firstContentfulPaint && metrics.firstContentfulPaint > this.config.fcpThreshold) {
      return true;
    }
    
    // 如果 LCP 超过阈值,可能有问题
    if (metrics.largestContentfulPaint && metrics.largestContentfulPaint > this.config.lcpThreshold) {
      return true;
    }
    
    return false;
  }
  
  /**
   * 获取资源加载统计
   */
  getResourceStats(): {
    totalResources: number;
    failedResources: number;
    slowResources: number;
    resourceDetails: {
      name: string;
      type: string;
      duration: number;
      size: number;
      failed: boolean;
    }[];
  } {
    const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
    const resourceDetails: {
      name: string;
      type: string;
      duration: number;
      size: number;
      failed: boolean;
    }[] = [];
    
    let failedResources = 0;
    let slowResources = 0;
    
    for (const resource of resources) {
      const failed = resource.transferSize === 0 && resource.decodedBodySize === 0;
      const slow = resource.duration > 2000;
      
      if (failed) failedResources++;
      if (slow) slowResources++;
      
      resourceDetails.push({
        name: resource.name,
        type: resource.initiatorType,
        duration: resource.duration,
        size: resource.transferSize || 0,
        failed
      });
    }
    
    return {
      totalResources: resources.length,
      failedResources,
      slowResources,
      resourceDetails
    };
  }
  
  /**
   * 停止观察
   */
  stop(): void {
    if (this.lcpObserver) {
      this.lcpObserver.disconnect();
      this.lcpObserver = null;
    }
  }
}

// ==================== 错误监听器 ====================

/**
 * 错误监听器类
 */
class ErrorMonitor {
  private errors: ErrorInfo[] = [];
  private maxErrors: number = 50;
  private listeners: {
    error: (event: ErrorEvent) => void;
    unhandledrejection: (event: PromiseRejectionEvent) => void;
  };
  
  constructor() {
    this.listeners = {
      error: this.handleError.bind(this),
      unhandledrejection: this.handleUnhandledRejection.bind(this)
    };
  }
  
  /**
   * 开始监听
   */
  start(): void {
    window.addEventListener('error', this.listeners.error, true);
    window.addEventListener('unhandledrejection', this.listeners.unhandledrejection);
  }
  
  /**
   * 停止监听
   */
  stop(): void {
    window.removeEventListener('error', this.listeners.error, true);
    window.removeEventListener('unhandledrejection', this.listeners.unhandledrejection);
  }
  
  /**
   * 处理错误事件
   */
  private handleError(event: ErrorEvent): void {
    if (this.errors.length >= this.maxErrors) return;
    
    this.errors.push({
      message: event.message || 'Unknown error',
      stack: event.error?.stack,
      type: 'error'
    });
  }
  
  /**
   * 处理未处理的 Promise 拒绝
   */
  private handleUnhandledRejection(event: PromiseRejectionEvent): void {
    if (this.errors.length >= this.maxErrors) return;
    
    let message = 'Unhandled Promise rejection';
    let stack: string | undefined;
    
    if (event.reason instanceof Error) {
      message = event.reason.message;
      stack = event.reason.stack;
    } else if (typeof event.reason === 'string') {
      message = event.reason;
    }
    
    this.errors.push({
      message,
      stack,
      type: 'unhandledrejection'
    });
  }
  
  /**
   * 获取所有错误
   */
  getErrors(): ErrorInfo[] {
    return [...this.errors];
  }
  
  /**
   * 判断是否有关键错误
   */
  hasCriticalErrors(): boolean {
    // 检查是否有可能导致白屏的错误
    const criticalPatterns = [
      /chunk.*failed/i,
      /loading.*chunk/i,
      /script.*error/i,
      /syntaxerror/i,
      /referenceerror/i,
      /cannot read/i,
      /is not defined/i,
      /unexpected token/i
    ];
    
    for (const error of this.errors) {
      for (const pattern of criticalPatterns) {
        if (pattern.test(error.message)) {
          return true;
        }
      }
    }
    
    return false;
  }
  
  /**
   * 清除错误
   */
  clear(): void {
    this.errors = [];
  }
}

// ==================== 数据上报器 ====================

/**
 * 数据上报器类
 */
class Reporter {
  private config: {
    endpoint?: string;
    enableConsole: boolean;
    enableBeacon: boolean;
    sampleRate: number;
    customHeaders?: Record<string, string>;
  };
  
  private queue: WhiteScreenReport[] = [];
  private maxQueueSize: number = 10;
  private isFlushing: boolean = false;
  
  constructor(config: {
    endpoint?: string;
    enableConsole?: boolean;
    enableBeacon?: boolean;
    sampleRate?: number;
    customHeaders?: Record<string, string>;
  } = {}) {
    this.config = {
      endpoint: config.endpoint,
      enableConsole: config.enableConsole ?? true,
      enableBeacon: config.enableBeacon ?? true,
      sampleRate: config.sampleRate ?? 1,
      customHeaders: config.customHeaders
    };
    
    // 页面卸载时发送队列中的数据
    window.addEventListener('beforeunload', () => {
      this.flush();
    });
    
    window.addEventListener('pagehide', () => {
      this.flush();
    });
  }
  
  /**
   * 上报数据
   */
  report(data: WhiteScreenReport): void {
    // 采样
    if (Math.random() > this.config.sampleRate) {
      return;
    }
    
    // 控制台输出
    if (this.config.enableConsole) {
      this.logToConsole(data);
    }
    
    // 加入队列
    this.queue.push(data);
    
    // 队列满了就发送
    if (this.queue.length >= this.maxQueueSize) {
      this.flush();
    }
    
    // 也可以立即发送(针对白屏这种重要事件)
    if (data.isWhiteScreen) {
      this.flush();
    }
  }
  
  /**
   * 发送队列中的数据
   */
  flush(): void {
    if (this.isFlushing || this.queue.length === 0) return;
    
    this.isFlushing = true;
    const dataToSend = [...this.queue];
    this.queue = [];
    
    if (this.config.endpoint) {
      this.sendData(dataToSend);
    }
    
    this.isFlushing = false;
  }
  
  /**
   * 发送数据
   */
  private sendData(data: WhiteScreenReport[]): void {
    const payload = JSON.stringify(data);
    
    // 优先使用 Beacon API
    if (this.config.enableBeacon && navigator.sendBeacon) {
      try {
        const blob = new Blob([payload], { type: 'application/json' });
        const success = navigator.sendBeacon(this.config.endpoint!, blob);
        if (success) return;
      } catch {
        // Beacon 失败,降级到 fetch
      }
    }
    
    // 降级使用 fetch
    this.sendWithFetch(payload);
  }
  
  /**
   * 使用 fetch 发送
   */
  private sendWithFetch(payload: string): void {
    if (!this.config.endpoint) return;
    
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...this.config.customHeaders
    };
    
    fetch(this.config.endpoint, {
      method: 'POST',
      headers,
      body: payload,
      keepalive: true
    }).catch(() => {
      // 静默失败
    });
  }
  
  /**
   * 控制台输出
   */
  private logToConsole(data: WhiteScreenReport): void {
    const prefix = '[WhiteScreen]';
    
    if (data.isWhiteScreen) {
      console.warn(
        `${prefix} 检测到白屏!`,
        '\n方法:', data.detectionMethod,
        '\nURL:', data.url,
        '\n时间:', new Date(data.timestamp).toISOString(),
        '\n详情:', data
      );
    } else {
      console.log(
        `${prefix} 页面正常`,
        '\n方法:', data.detectionMethod,
        '\n耗时:', Date.now() - data.timestamp, 'ms'
      );
    }
  }
  
  /**
   * 设置上报端点
   */
  setEndpoint(endpoint: string): void {
    this.config.endpoint = endpoint;
  }
  
  /**
   * 设置采样率
   */
  setSampleRate(rate: number): void {
    this.config.sampleRate = Math.max(0, Math.min(1, rate));
  }
}

// ==================== 主 SDK 类 ====================

/**
 * 白屏检测 SDK 主类
 */
class WhiteScreenSDK {
  private config: Required<WhiteScreenConfig>;
  private samplingDetector: SamplingDetector;
  private domDetector: DOMDetector;
  private mutationDetector: MutationDetector;
  private screenshotDetector: ScreenshotDetector;
  private skeletonDetector: SkeletonDetector;
  private performanceDetector: PerformanceDetector;
  private errorMonitor: ErrorMonitor;
  private reporter: Reporter;
  
  private isInitialized: boolean = false;
  private isDetecting: boolean = false;
  private detectionCount: number = 0;
  private lastDetectionTime: number = 0;
  
  // 默认配置
  private static defaultConfig: Required<WhiteScreenConfig> = {
    samplingPoints: 17,
    delay: 1000,
    timeout: 10000,
    threshold: 0.95,
    enableDOMDetection: true,
    enableSamplingDetection: true,
    enableMutationDetection: true,
    enableScreenshotDetection: false,
    enableSkeletonDetection: true,
    skeletonSelector: '.skeleton, [data-skeleton], .loading-skeleton',
    keyElementSelectors: ['#app', '#root', '.app', 'main'],
    ignoreSelectors: ['script', 'style', 'link', 'meta'],
    container: null,
    onReport: () => {},
    onDetectionComplete: () => {},
    enableInDev: false,
    maxRetries: 3,
    retryInterval: 1000,
    customDetector: undefined as unknown as () => boolean
  };
  
  constructor(config: Partial<WhiteScreenConfig> = {}) {
    // 合并配置
    this.config = deepMerge(WhiteScreenSDK.defaultConfig, config);
    
    // 初始化各个检测器
    this.samplingDetector = new SamplingDetector(this.config);
    this.domDetector = new DOMDetector(this.config);
    this.mutationDetector = new MutationDetector(this.config);
    this.screenshotDetector = new ScreenshotDetector(this.config);
    this.skeletonDetector = new SkeletonDetector(this.config);
    this.performanceDetector = new PerformanceDetector(this.config);
    this.errorMonitor = new ErrorMonitor();
    
    // 初始化上报器
    this.reporter = new Reporter({
      enableConsole: true,
      enableBeacon: true
    });
  }
  
  /**
   * 初始化 SDK
   */
  init(): WhiteScreenSDK {
    if (this.isInitialized) {
      console.warn('[WhiteScreenSDK] Already initialized');
      return this;
    }
    
    // 检查是否在开发环境
    if (!this.config.enableInDev && this.isDevelopment()) {
      console.log('[WhiteScreenSDK] Disabled in development environment');
      return this;
    }
    
    // 开始错误监听
    this.errorMonitor.start();
    
    // 初始化性能检测器
    this.performanceDetector.initLCPObserver();
    
    // 在页面加载完成后开始检测
    if (document.readyState === 'complete') {
      this.scheduleDetection();
    } else {
      window.addEventListener('load', () => {
        this.scheduleDetection();
      });
    }
    
    // 监听页面可见性变化
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible' && !this.isDetecting) {
        this.scheduleDetection();
      }
    });
    
    this.isInitialized = true;
    console.log('[WhiteScreenSDK] Initialized');
    
    return this;
  }
  
  /**
   * 判断是否为开发环境
   */
  private isDevelopment(): boolean {
    return (
      window.location.hostname === 'localhost' ||
      window.location.hostname === '127.0.0.1' ||
      window.location.hostname.includes('.local') ||
      window.location.port !== ''
    );
  }
  
  /**
   * 调度检测
   */
  private scheduleDetection(): void {
    setTimeout(() => {
      this.detect();
    }, this.config.delay);
  }
  
  /**
   * 执行检测
   */
  async detect(): Promise<DetectionResult> {
    if (this.isDetecting) {
      return {
        isWhiteScreen: false,
        confidence: 0,
        methods: [],
        methodResults: new Map(),
        basis: 'Already detecting'
      };
    }
    
    this.isDetecting = true;
    this.detectionCount++;
    this.lastDetectionTime = Date.now();
    
    const methodResults = new Map<DetectionMethod, boolean>();
    const usedMethods: DetectionMethod[] = [];
    
    try {
      // 执行各种检测方法
      
      // 1. 采样点检测
      if (this.config.enableSamplingDetection) {
        const samplingResult = this.samplingDetector.detect();
        const isWhite = this.samplingDetector.isWhiteScreen(samplingResult);
        methodResults.set(DetectionMethod.SAMPLING, isWhite);
        usedMethods.push(DetectionMethod.SAMPLING);
      }
      
      // 2. DOM 检测
      if (this.config.enableDOMDetection) {
        const domResult = this.domDetector.detect();
        const isWhite = !domResult.passed || !this.domDetector.hasValidContent();
        methodResults.set(DetectionMethod.DOM, isWhite);
        usedMethods.push(DetectionMethod.DOM);
      }
      
      // 3. 骨架屏检测
      if (this.config.enableSkeletonDetection) {
        const skeletonResult = await this.skeletonDetector.detect();
        const isWhite = skeletonResult.skeletonExists && !skeletonResult.skeletonRemoved;
        methodResults.set(DetectionMethod.SKELETON, isWhite);
        usedMethods.push(DetectionMethod.SKELETON);
      }
      
      // 4. 截图检测(较慢,可选)
      if (this.config.enableScreenshotDetection) {
        const screenshotResult = await this.screenshotDetector.detect();
        methodResults.set(DetectionMethod.SCREENSHOT, screenshotResult.isWhiteScreen);
        usedMethods.push(DetectionMethod.SCREENSHOT);
      }
      
      // 5. 自定义检测器
      if (this.config.customDetector) {
        try {
          const customResult = await this.config.customDetector();
          methodResults.set(DetectionMethod.CUSTOM, customResult);
          usedMethods.push(DetectionMethod.CUSTOM);
        } catch {
          // 自定义检测器失败
        }
      }
      
      // 综合判断
      const result = this.combineResults(methodResults, usedMethods);
      
      // 生成报告
      const report = this.generateReport(result, methodResults);
      
      // 上报
      this.reporter.report(report);
      this.config.onReport(report);
      this.config.onDetectionComplete(result);
      
      // 如果检测到白屏,可能需要重试
      if (result.isWhiteScreen && this.detectionCount < this.config.maxRetries) {
        setTimeout(() => {
          this.isDetecting = false;
          this.detect();
        }, this.config.retryInterval);
      }
      
      return result;
    } catch (error) {
      console.error('[WhiteScreenSDK] Detection error:', error);
      return {
        isWhiteScreen: false,
        confidence: 0,
        methods: usedMethods,
        methodResults,
        basis: `Error: ${(error as Error).message}`
      };
    } finally {
      this.isDetecting = false;
    }
  }
  
  /**
   * 综合各方法结果
   */
  private combineResults(
    methodResults: Map<DetectionMethod, boolean>,
    usedMethods: DetectionMethod[]
  ): DetectionResult {
    if (usedMethods.length === 0) {
      return {
        isWhiteScreen: false,
        confidence: 0,
        methods: [],
        methodResults,
        basis: 'No detection methods enabled'
      };
    }
    
    // 计算白屏票数
    let whiteVotes = 0;
    let totalVotes = 0;
    
    // 不同方法的权重
    const weights: Record<DetectionMethod, number> = {
      [DetectionMethod.SAMPLING]: 3,
      [DetectionMethod.DOM]: 2,
      [DetectionMethod.MUTATION]: 2,
      [DetectionMethod.SCREENSHOT]: 2,
      [DetectionMethod.SKELETON]: 1,
      [DetectionMethod.PERFORMANCE]: 1,
      [DetectionMethod.CUSTOM]: 2,
      [DetectionMethod.COMBINED]: 1
    };
    
    for (const method of usedMethods) {
      const isWhite = methodResults.get(method);
      const weight = weights[method] || 1;
      
      totalVotes += weight;
      if (isWhite) {
        whiteVotes += weight;
      }
    }
    
    // 计算置信度
    const confidence = totalVotes > 0 ? Math.abs(whiteVotes - totalVotes / 2) / (totalVotes / 2) : 0;
    
    // 判断是否白屏(超过半数加权投票)
    const isWhiteScreen = whiteVotes > totalVotes / 2;
    
    // 确定判定依据
    let basis = isWhiteScreen ? 'Multiple methods indicate white screen' : 'Page appears normal';
    
    // 检查是否有关键错误
    if (this.errorMonitor.hasCriticalErrors()) {
      basis += ' (Critical JS errors detected)';
    }
    
    return {
      isWhiteScreen,
      confidence,
      methods: usedMethods,
      methodResults,
      basis
    };
  }
  
  /**
   * 生成报告
   */
  private generateReport(
    result: DetectionResult,
    methodResults: Map<DetectionMethod, boolean>
  ): WhiteScreenReport {
    const report: WhiteScreenReport = {
      isWhiteScreen: result.isWhiteScreen,
      timestamp: Date.now(),
      url: window.location.href,
      detectionMethod: result.methods.length > 1 ? DetectionMethod.COMBINED : result.methods[0],
      userAgent: navigator.userAgent,
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      },
      devicePixelRatio: window.devicePixelRatio,
      networkInfo: getNetworkInfo(),
      performanceData: this.performanceDetector.getMetrics()
    };
    
    // 添加采样结果
    if (methodResults.has(DetectionMethod.SAMPLING)) {
      report.samplingResult = this.samplingDetector.detect();
    }
    
    // 添加 DOM 检测结果
    if (methodResults.has(DetectionMethod.DOM)) {
      report.domResult = this.domDetector.detect();
    }
    
    // 添加错误信息
    const errors = this.errorMonitor.getErrors();
    if (errors.length > 0) {
      report.errorInfo = errors[0];
    }
    
    return report;
  }
  
  /**
   * 手动触发检测
   */
  manualDetect(): Promise<DetectionResult> {
    return this.detect();
  }
  
  /**
   * 设置上报回调
   */
  setReportCallback(callback: (data: WhiteScreenReport) => void): void {
    this.config.onReport = callback;
  }
  
  /**
   * 设置检测完成回调
   */
  setDetectionCompleteCallback(callback: (result: DetectionResult) => void): void {
    this.config.onDetectionComplete = callback;
  }
  
  /**
   * 获取检测统计
   */
  getStats(): {
    detectionCount: number;
    lastDetectionTime: number;
    errors: ErrorInfo[];
    performance: PerformanceData;
  } {
    return {
      detectionCount: this.detectionCount,
      lastDetectionTime: this.lastDetectionTime,
      errors: this.errorMonitor.getErrors(),
      performance: this.performanceDetector.getMetrics()
    };
  }
  
  /**
   * 销毁 SDK
   */
  destroy(): void {
    this.errorMonitor.stop();
    this.performanceDetector.stop();
    this.skeletonDetector.stop();
    this.screenshotDetector.dispose();
    this.isInitialized = false;
    console.log('[WhiteScreenSDK] Destroyed');
  }
}

// ==================== 导出 ====================

// 创建单例
let sdkInstance: WhiteScreenSDK | null = null;

/**
 * 获取 SDK 实例(单例模式)
 */
function getWhiteScreenSDK(config?: Partial<WhiteScreenConfig>): WhiteScreenSDK {
  if (!sdkInstance) {
    sdkInstance = new WhiteScreenSDK(config);
  }
  return sdkInstance;
}

/**
 * 快速初始化并返回实例
 */
function initWhiteScreenDetection(config?: Partial<WhiteScreenConfig>): WhiteScreenSDK {
  const sdk = getWhiteScreenSDK(config);
  sdk.init();
  return sdk;
}

// ES Module 导出
export {
  WhiteScreenSDK,
  WhiteScreenConfig,
  WhiteScreenReport,
  DetectionMethod,
  DetectionResult,
  SamplingDetector,
  DOMDetector,
  MutationDetector,
  ScreenshotDetector,
  SkeletonDetector,
  PerformanceDetector,
  ErrorMonitor,
  Reporter,
  getWhiteScreenSDK,
  initWhiteScreenDetection
};

// UMD 导出(兼容浏览器直接使用)
if (typeof window !== 'undefined') {
  (window as Window & { WhiteScreenSDK: typeof WhiteScreenSDK; initWhiteScreenDetection: typeof initWhiteScreenDetection }).WhiteScreenSDK = WhiteScreenSDK;
  (window as Window & { initWhiteScreenDetection: typeof initWhiteScreenDetection }).initWhiteScreenDetection = initWhiteScreenDetection;
}

三、使用示例

1. 基础使用

// 最简单的使用方式
import { initWhiteScreenDetection } from './white-screen-sdk';

// 初始化并开始检测
const sdk = initWhiteScreenDetection({
  threshold: 0.9,
  delay: 2000,
  onReport: (report) => {
    if (report.isWhiteScreen) {
      // 发送到监控平台
      sendToMonitor(report);
    }
  }
});

2. 完整配置示例

import { WhiteScreenSDK, WhiteScreenReport, DetectionResult } from './white-screen-sdk';

// 创建 SDK 实例,完整配置
const sdk = new WhiteScreenSDK({
  // 采样配置
  samplingPoints: 20,
  threshold: 0.9,
  
  // 时间配置
  delay: 1500,
  timeout: 15000,
  
  // 功能开关
  enableDOMDetection: true,
  enableSamplingDetection: true,
  enableMutationDetection: true,
  enableScreenshotDetection: false,
  enableSkeletonDetection: true,
  
  // 选择器配置
  keyElementSelectors: [
    '#app',
    '#root',
    '.main-content',
    '[data-page-ready]'
  ],
  skeletonSelector: '.skeleton, .loading-placeholder',
  ignoreSelectors: [
    'script',
    'style',
    '.loading-indicator',
    '.modal-backdrop'
  ],
  
  // 重试配置
  maxRetries: 3,
  retryInterval: 2000,
  
  // 环境配置
  enableInDev: false,
  
  // 回调函数
  onReport: (report: WhiteScreenReport) => {
    console.log('检测报告:', report);
    
    if (report.isWhiteScreen) {
      // 上报到监控系统
      reportToMonitoringSystem(report);
      
      // 可选:尝试恢复页面
      attemptPageRecovery();
    }
  },
  
  onDetectionComplete: (result: DetectionResult) => {
    console.log('检测完成:', result);
    console.log('置信度:', result.confidence);
    console.log('判定依据:', result.basis);
  },
  
  // 自定义检测逻辑
  customDetector: () => {
    // 自定义白屏判断逻辑
    const appElement = document.getElementById('app');
    if (!appElement) return true;
    
    // 检查是否有实际内容
    const hasContent = appElement.children.length > 0;
    const hasText = (appElement.textContent?.trim().length ?? 0) > 100;
    
    return !hasContent && !hasText;
  }
});

// 初始化
sdk.init();

// 手动触发检测
document.getElementById('checkBtn')?.addEventListener('click', async () => {
  const result = await sdk.manualDetect();
  console.log('手动检测结果:', result);
});

// 上报函数
function reportToMonitoringSystem(report: WhiteScreenReport): void {
  // 发送到监控后端
  fetch('/api/monitor/white-screen', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(report)
  }).catch(console.error);
}

// 页面恢复尝试
function attemptPageRecovery(): void {
  // 尝试重新加载关键资源
  const failedScripts = document.querySelectorAll('script[data-retry]');
  failedScripts.forEach(script => {
    const newScript = document.createElement('script');
    newScript.src = (script as HTMLScriptElement).src;
    document.body.appendChild(newScript);
  });
  
  // 或者刷新页面
  // window.location.reload();
}

3. Vue 3 集成示例

// white-screen-plugin.ts
import { App, Plugin } from 'vue';
import { WhiteScreenSDK, WhiteScreenConfig } from './white-screen-sdk';

export interface WhiteScreenPluginOptions extends Partial<WhiteScreenConfig> {
  reportEndpoint?: string;
}

export const WhiteScreenPlugin: Plugin = {
  install(app: App, options: WhiteScreenPluginOptions = {}) {
    // 创建 SDK 实例
    const sdk = new WhiteScreenSDK({
      // 默认配置
      keyElementSelectors: ['#app', '[data-v-app]'],
      enableInDev: false,
      
      // 合并用户配置
      ...options,
      
      // 设置上报回调
      onReport: (report) => {
        // 调用用户提供的回调
        options.onReport?.(report);
        
        // 如果提供了上报端点,自动上报
        if (options.reportEndpoint && report.isWhiteScreen) {
          fetch(options.reportEndpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              ...report,
              framework: 'vue',
              version: app.version
            })
          }).catch(console.error);
        }
      }
    });
    
    // 注册全局属性
    app.config.globalProperties.$whiteScreen = sdk;
    
    // 提供给 Composition API 使用
    app.provide('whiteScreenSDK', sdk);
    
    // 在应用挂载后初始化
    const originalMount = app.mount.bind(app);
    app.mount = (rootContainer) => {
      const vm = originalMount(rootContainer);
      
      // 延迟初始化,等待 Vue 渲染完成
      setTimeout(() => {
        sdk.init();
      }, 100);
      
      return vm;
    };
    
    // 在应用卸载时销毁
    const originalUnmount = app.unmount.bind(app);
    app.unmount = () => {
      sdk.destroy();
      originalUnmount();
    };
  }
};

// 类型声明
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $whiteScreen: WhiteScreenSDK;
  }
}

// 组合式 API Hook
import { inject } from 'vue';

export function useWhiteScreen(): WhiteScreenSDK | undefined {
  return inject<WhiteScreenSDK>('whiteScreenSDK');
}
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { WhiteScreenPlugin } from './plugins/white-screen-plugin';

const app = createApp(App);

app.use(WhiteScreenPlugin, {
  threshold: 0.9,
  delay: 2000,
  reportEndpoint: '/api/monitor/white-screen',
  enableInDev: false,
  keyElementSelectors: ['#app', '.main-layout', '[data-page-content]'],
  onDetectionComplete: (result) => {
    if (result.isWhiteScreen) {
      // 可以在这里触发重试逻辑或显示友好提示
      console.warn('页面可能出现白屏,正在尝试恢复...');
    }
  }
});

app.mount('#app');
<!-- 在组件中使用 -->
<template>
  <div class="page">
    <h1>示例页面</h1>
    <button @click="checkWhiteScreen">手动检测白屏</button>
  </div>
</template>

<script setup lang="ts">
import { useWhiteScreen } from '@/plugins/white-screen-plugin';

const whiteScreenSDK = useWhiteScreen();

async function checkWhiteScreen() {
  if (whiteScreenSDK) {
    const result = await whiteScreenSDK.manualDetect();
    console.log('检测结果:', result);
    
    if (result.isWhiteScreen) {
      alert('检测到白屏问题!');
    } else {
      alert('页面正常');
    }
  }
}
</script>

4. React 集成示例

// WhiteScreenContext.tsx
import React, { createContext, useContext, useEffect, useRef, ReactNode } from 'react';
import { WhiteScreenSDK, WhiteScreenConfig, DetectionResult } from './white-screen-sdk';

interface WhiteScreenContextValue {
  sdk: WhiteScreenSDK | null;
  manualDetect: () => Promise<DetectionResult | null>;
  getStats: () => ReturnType<WhiteScreenSDK['getStats']> | null;
}

const WhiteScreenContext = createContext<WhiteScreenContextValue>({
  sdk: null,
  manualDetect: async () => null,
  getStats: () => null
});

export interface WhiteScreenProviderProps {
  children: ReactNode;
  config?: Partial<WhiteScreenConfig>;
  onWhiteScreen?: (result: DetectionResult) => void;
}

export function WhiteScreenProvider({ 
  children, 
  config = {},
  onWhiteScreen 
}: WhiteScreenProviderProps) {
  const sdkRef = useRef<WhiteScreenSDK | null>(null);
  
  useEffect(() => {
    // 创建 SDK 实例
    const sdk = new WhiteScreenSDK({
      keyElementSelectors: ['#root', '[data-reactroot]', '.app-container'],
      enableInDev: false,
      ...config,
      onDetectionComplete: (result) => {
        config.onDetectionComplete?.(result);
        
        if (result.isWhiteScreen && onWhiteScreen) {
          onWhiteScreen(result);
        }
      }
    });
    
    sdkRef.current = sdk;
    
    // 初始化
    sdk.init();
    
    // 清理
    return () => {
      sdk.destroy();
      sdkRef.current = null;
    };
  }, []);
  
  const manualDetect = async () => {
    if (sdkRef.current) {
      return sdkRef.current.manualDetect();
    }
    return null;
  };
  
  const getStats = () => {
    if (sdkRef.current) {
      return sdkRef.current.getStats();
    }
    return null;
  };
  
  return (
    <WhiteScreenContext.Provider value={{ 
      sdk: sdkRef.current, 
      manualDetect, 
      getStats 
    }}>
      {children}
    </WhiteScreenContext.Provider>
  );
}

// Hook
export function useWhiteScreen() {
  return useContext(WhiteScreenContext);
}

// HOC
export function withWhiteScreenDetection<P extends object>(
  WrappedComponent: React.ComponentType<P>,
  config?: Partial<WhiteScreenConfig>
) {
  return function WithWhiteScreenComponent(props: P) {
    return (
      <WhiteScreenProvider config={config}>
        <WrappedComponent {...props} />
      </WhiteScreenProvider>
    );
  };
}
// App.tsx
import React from 'react';
import { WhiteScreenProvider } from './contexts/WhiteScreenContext';

function App() {
  const handleWhiteScreen = (result) => {
    // 白屏时的处理逻辑
    console.error('检测到白屏:', result);
    
    // 可以显示错误边界或重试按钮
    // setShowErrorBoundary(true);
  };

  return (
    <WhiteScreenProvider 
      config={{
        threshold: 0.85,
        delay: 2000,
        enableScreenshotDetection: false
      }}
      onWhiteScreen={handleWhiteScreen}
    >
      <div className="app">
        {/* 应用内容 */}
      </div>
    </WhiteScreenProvider>
  );
}

export default App;
// 在组件中使用
import React from 'react';
import { useWhiteScreen } from './contexts/WhiteScreenContext';

function DebugPanel() {
  const { manualDetect, getStats } = useWhiteScreen();
  
  const handleCheck = async () => {
    const result = await manualDetect();
    if (result) {
      console.log('检测结果:', result);
      alert(result.isWhiteScreen ? '检测到白屏' : '页面正常');
    }
  };
  
  const handleShowStats = () => {
    const stats = getStats();
    console.log('统计信息:', stats);
  };
  
  return (
    <div className="debug-panel">
      <button onClick={handleCheck}>手动检测白屏</button>
      <button onClick={handleShowStats}>查看统计</button>
    </div>
  );
}

四、服务端数据处理示例

// server/white-screen-handler.ts
import { Request, Response } from 'express';

interface WhiteScreenReport {
  isWhiteScreen: boolean;
  timestamp: number;
  url: string;
  detectionMethod: string;
  userAgent: string;
  viewport: { width: number; height: number };
  performanceData?: Record<string, number>;
  errorInfo?: { message: string; stack?: string; type: string };
  // ... 其他字段
}

interface WhiteScreenStats {
  totalReports: number;
  whiteScreenCount: number;
  whiteScreenRate: number;
  topUrls: { url: string; count: number }[];
  topErrors: { message: string; count: number }[];
  hourlyDistribution: { hour: number; count: number }[];
}

class WhiteScreenAnalyzer {
  private reports: WhiteScreenReport[] = [];
  private maxReports: number = 10000;
  
  /**
   * 添加报告
   */
  addReport(report: WhiteScreenReport): void {
    this.reports.push(report);
    
    // 保持报告数量在限制内
    if (this.reports.length > this.maxReports) {
      this.reports = this.reports.slice(-this.maxReports);
    }
  }
  
  /**
   * 获取统计数据
   */
  getStats(timeRange?: { start: number; end: number }): WhiteScreenStats {
    let filteredReports = this.reports;
    
    if (timeRange) {
      filteredReports = this.reports.filter(
        r => r.timestamp >= timeRange.start && r.timestamp <= timeRange.end
      );
    }
    
    const totalReports = filteredReports.length;
    const whiteScreenReports = filteredReports.filter(r => r.isWhiteScreen);
    const whiteScreenCount = whiteScreenReports.length;
    const whiteScreenRate = totalReports > 0 ? whiteScreenCount / totalReports : 0;
    
    // 统计 Top URLs
    const urlCounts = new Map<string, number>();
    whiteScreenReports.forEach(r => {
      const url = new URL(r.url).pathname;
      urlCounts.set(url, (urlCounts.get(url) || 0) + 1);
    });
    
    const topUrls = Array.from(urlCounts.entries())
      .map(([url, count]) => ({ url, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 10);
    
    // 统计 Top Errors
    const errorCounts = new Map<string, number>();
    whiteScreenReports.forEach(r => {
      if (r.errorInfo?.message) {
        const msg = r.errorInfo.message.slice(0, 100);
        errorCounts.set(msg, (errorCounts.get(msg) || 0) + 1);
      }
    });
    
    const topErrors = Array.from(errorCounts.entries())
      .map(([message, count]) => ({ message, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 10);
    
    // 小时分布
    const hourCounts = new Array(24).fill(0);
    whiteScreenReports.forEach(r => {
      const hour = new Date(r.timestamp).getHours();
      hourCounts[hour]++;
    });
    
    const hourlyDistribution = hourCounts.map((count, hour) => ({ hour, count }));
    
    return {
      totalReports,
      whiteScreenCount,
      whiteScreenRate,
      topUrls,
      topErrors,
      hourlyDistribution
    };
  }
  
  /**
   * 检查是否需要告警
   */
  shouldAlert(): { alert: boolean; reason: string } {
    const recentReports = this.reports.filter(
      r => Date.now() - r.timestamp < 5 * 60 * 1000 // 最近5分钟
    );
    
    if (recentReports.length === 0) {
      return { alert: false, reason: '' };
    }
    
    const whiteScreenRate = recentReports.filter(r => r.isWhiteScreen).length / recentReports.length;
    
    if (whiteScreenRate > 0.1) {
      return { 
        alert: true, 
        reason: `白屏率过高: ${(whiteScreenRate * 100).toFixed(1)}%` 
      };
    }
    
    return { alert: false, reason: '' };
  }
}

// Express 路由处理
const analyzer = new WhiteScreenAnalyzer();

export function handleWhiteScreenReport(req: Request, res: Response): void {
  try {
    const reports: WhiteScreenReport[] = Array.isArray(req.body) ? req.body : [req.body];
    
    reports.forEach(report => {
      analyzer.addReport(report);
    });
    
    // 检查是否需要告警
    const alertStatus = analyzer.shouldAlert();
    if (alertStatus.alert) {
      // 发送告警(例如:发送邮件、短信、钉钉通知等)
      sendAlert(alertStatus.reason);
    }
    
    res.status(200).json({ success: true, received: reports.length });
  } catch (error) {
    console.error('处理白屏报告失败:', error);
    res.status(500).json({ success: false, error: (error as Error).message });
  }
}

export function getWhiteScreenStats(req: Request, res: Response): void {
  try {
    const { start, end } = req.query;
    
    const timeRange = start && end 
      ? { start: Number(start), end: Number(end) }
      : undefined;
    
    const stats = analyzer.getStats(timeRange);
    res.json(stats);
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  }
}

function sendAlert(reason: string): void {
  // 实现告警逻辑
  console.warn('[白屏告警]', reason);
  // 可以集成各种告警渠道:邮件、短信、钉钉、企业微信等
}

五、总结

以上代码实现了一个完整的前端白屏检测 SDK,包含以下核心功能:

  1. 多种检测方法

    • 采样点检测 - 在页面关键位置采样判断
    • DOM 检测 - 检查关键 DOM 元素是否存在和可见
    • MutationObserver 检测 - 监听 DOM 变化
    • 截图检测 - 通过 Canvas 分析页面内容
    • 骨架屏检测 - 检测 loading 状态
  2. 完善的上报机制

    • 支持 Beacon API 和 fetch 降级
    • 队列批量上报
    • 页面卸载时发送
  3. 框架集成

    • Vue 3 插件和 Composition API Hook
    • React Context 和 Hook
  4. 服务端处理

    • 数据统计分析
    • 告警机制

实际使用时,可以根据项目需求选择合适的检测方法组合,并配置合理的阈值和延迟时间,以达到最佳的检测效果。

❌
❌