前端埋点上报错误本地存储

/**
 * 前端全局错误监控 & 崩溃监控 封装
 * 功能:捕获JS报错、Vue报错、异步报错、资源加载失败、白屏、页面崩溃
 * 存储:使用原生 IndexedDB 本地存储(不丢数据、支持大容量)
 * 上报:有网时自动上报,断网时本地保存,下次启动自动续报
 * 适用:Vue3 项目(任何封装框架都能用)
 */
class ErrorMonitor {
  constructor() {
    // 数据库配置
    this.dbName = "ErrorMonitorDB"; // 数据库名
    this.storeName = "errorLogs"; // 表名
    this.db = null;

    // 初始化
    this.initDB();
    this.bindErrorEvents();

    // 页面启动时,自动上报之前未发送的错误
    this.uploadOnStart();
  }

  // ==============================================
  // 1. 初始化 IndexedDB(原生,无依赖)
  // ==============================================
  initDB() {
    const request = indexedDB.open(this.dbName, 1);

    request.onupgradeneeded = (e) => {
      this.db = e.target.result;
      if (!this.db.objectStoreNames.contains(this.storeName)) {
        this.db.createObjectStore(this.storeName, { keyPath: "id", autoIncrement: true });
      }
    };

    request.onsuccess = (e) => {
      this.db = e.target.result;
    };
  }

  // ==============================================
  // 2. 保存错误到本地 IndexedDB
  // ==============================================
  saveToLocal(data) {
    return new Promise((resolve) => {
      if (!this.db) return resolve();
      const tx = this.db.transaction(this.storeName, "readwrite");
      const store = tx.objectStore(this.storeName);

      const log = {
        ...data,
        url: location.href,
        ua: navigator.userAgent,
        time: new Date().toLocaleString(),
      };

      store.add(log);
      tx.oncomplete = () => resolve();
    });
  }

  // ==============================================
  // 3. 获取所有本地错误
  // ==============================================
  getLocalLogs() {
    return new Promise((resolve) => {
      if (!this.db) return resolve([]);
      const tx = this.db.transaction(this.storeName, "readonly");
      const store = tx.objectStore(this.storeName);
      const logs = [];

      store.openCursor().onsuccess = (e) => {
        const cursor = e.target.result;
        if (cursor) {
          logs.push(cursor.value);
          cursor.continue();
        } else {
          resolve(logs);
        }
      };
    });
  }

  // ==============================================
  // 4. 清空本地错误
  // ==============================================
  clearLocalLogs() {
    return new Promise((resolve) => {
      if (!this.db) return resolve();
      const tx = this.db.transaction(this.storeName, "readwrite");
      tx.objectStore(this.storeName).clear();
      tx.oncomplete = () => resolve();
    });
  }

  // ==============================================
  // 5. 上报错误(后台接口)
  // ==============================================
  async upload(data) {
    try {
      // 可换成你们后台真实接口
      await fetch("/api/error/report", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
    } catch (err) {
      // 上报失败 → 已经存在本地,不处理
    }
  }

  // ==============================================
  // 6. 统一入口:先存本地 → 再上报
  // ==============================================
  async capture(data) {
    await this.saveToLocal(data);
    this.upload(data);
  }

  // ==============================================
  // 7. 页面启动时自动上报本地积压的错误
  // ==============================================
  async uploadOnStart() {
    const logs = await this.getLocalLogs();
    if (logs.length === 0) return;

    await this.upload({ type: "batch", list: logs });
    await this.clearLocalLogs();
  }

  // ==============================================
  // 8. 绑定所有浏览器错误监听
  // ==============================================
  bindErrorEvents() {
    const monitor = this;

    // ------------------------------
    // JS 同步错误
    // ------------------------------
    window.onerror = function (msg, url, line, col, err) {
      monitor.capture({
        type: "js_error",
        message: msg,
        stack: err?.stack,
      });
      return true;
    };

    // ------------------------------
    // Promise / async 错误
    // ------------------------------
    window.addEventListener("unhandledrejection", (e) => {
      monitor.capture({
        type: "promise_error",
        message: e.reason?.message,
        stack: e.reason?.stack,
      });
    });

    // ------------------------------
    // 资源加载失败(图片/CSS/JS)
    // ------------------------------
    window.addEventListener(
      "error",
      (e) => {
        if (e.target?.src || e.target?.href) {
          monitor.capture({
            type: "resource_error",
            tag: e.target.tagName,
            url: e.target.src || e.target.href,
          });
        }
      },
      true
    );

    // ------------------------------
    // 页面关闭/崩溃
    // ------------------------------
    window.addEventListener("beforeunload", () => {
      monitor.capture({ type: "page_crash" });
    });

    // ------------------------------
    // 白屏检测
    // ------------------------------
    setTimeout(() => {
      const app = document.getElementById("app");
      if (!app || app.innerHTML.length < 50) {
        monitor.capture({ type: "blank_screen" });
      }
    }, 3000);
  }

  // ==============================================
  // 给 Vue 绑定全局错误捕获
  // ==============================================
  bindVueErrorHandler(app) {
    app.config.errorHandler = (err, instance, info) => {
      this.capture({
        type: "vue_error",
        message: err.message,
        stack: err.stack,
        info,
      });
    };
  }
}

// 单例模式,全局只一个实例
export const errorMonitor = new ErrorMonitor();
=========================================================本地查看日志错误===================================================
<template>
  <div class="error-log-wrapper" style="padding:20px;max-width:1200px;margin:0 auto">
    <h2>前端本地错误日志(IndexedDB)</h2>
    <div style="margin:10px 0">
      <button @click="getLogs" style="padding:6px 12px;margin-right:8px">刷新日志</button>
      <button @click="clearLogs" style="padding:6px 12px;margin-right:8px">清空本地日志</button>
      <button @click="testThrowError" style="padding:6px 12px">测试抛错</button>
    </div>

    <div v-if="logList.length === 0" style="color:#999;margin:20px 0">暂无本地错误日志</div>

    <!-- 日志列表 -->
    <div v-for="(item,idx) in logList" :key="idx" 
         style="border:1px solid #eee;padding:12px;margin:8px 0;border-radius:6px;background:#fafafa">
      <div><b>类型:</b>{{ item.type }}</div>
      <div><b>时间:</b>{{ item.time }}</div>
      <div><b>页面:</b>{{ item.url }}</div>
      <div><b>信息:</b>{{ item.message || '无' }}</div>
      <div style="margin-top:6px">
        <b>堆栈:</b>
        <pre style="white-space:pre-wrap;font-size:12px;color:#666">{{ item.stack || '无' }}</pre>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
// 引入你之前封装的全局监控实例
import { errorMonitor } from '@/utils/errorMonitor'

const logList = ref([])

// 读取本地IndexedDB错误日志
const getLogs = async () => {
  logList.value = await errorMonitor.getLocalLogs()
}

// 清空本地日志
const clearLogs = async () => {
  await errorMonitor.clearLocalLogs()
  getLogs()
}

// 测试:主动抛一个JS错误,看是否能捕获+入库
const testThrowError = () => {
  throw new Error('测试主动抛出:前端监控demo错误')
}

onMounted(() => {
  getLogs()
})
</script>
posted @ 2026-03-31 17:55  Uncle_MrLee  阅读(0)  评论(0)    收藏  举报