Skip to content
On this page

前端监控采集 SDK

实现一个功能全面的前端监控采集 SDK,包含错误采集、性能监控、资源请求跟踪、用户行为监控和数据上报功能。

完整实现方案

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>增强版前端监控采集SDK</title>
    <style>
      :root {
        --primary: #3498db;
        --success: #2ecc71;
        --warning: #f39c12;
        --danger: #e74c3c;
        --dark: #2c3e50;
        --light: #ecf0f1;
        --purple: #9b59b6;
      }

      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      }

      body {
        background-color: #f5f7fa;
        color: #333;
        line-height: 1.6;
      }

      .container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }

      header {
        background: linear-gradient(135deg, var(--dark), #1a2530);
        color: white;
        padding: 20px;
        border-radius: 10px;
        margin-bottom: 30px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        position: relative;
        overflow: hidden;
      }

      header::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          45deg,
          rgba(255, 255, 255, 0.05) 25%,
          transparent 25%,
          transparent 50%,
          rgba(255, 255, 255, 0.05) 50%,
          rgba(255, 255, 255, 0.05) 75%,
          transparent 75%,
          transparent
        );
        background-size: 50px 50px;
      }

      h1 {
        font-size: 2.5rem;
        margin-bottom: 10px;
        position: relative;
        z-index: 2;
      }

      .subtitle {
        font-size: 1.2rem;
        opacity: 0.8;
        margin-bottom: 20px;
        position: relative;
        z-index: 2;
      }

      .dashboard {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        gap: 25px;
        margin-bottom: 30px;
      }

      .card {
        background: white;
        border-radius: 10px;
        padding: 25px;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
        transition: transform 0.3s ease, box-shadow 0.3s ease;
        position: relative;
        overflow: hidden;
      }

      .card::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 4px;
        background: linear-gradient(90deg, var(--primary), var(--purple));
      }

      .card:hover {
        transform: translateY(-5px);
        box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
      }

      .card-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20px;
        padding-bottom: 15px;
        border-bottom: 1px solid #eee;
      }

      .card-title {
        font-size: 1.4rem;
        font-weight: 600;
        color: var(--dark);
      }

      .card-icon {
        font-size: 1.8rem;
        width: 40px;
        height: 40px;
        border-radius: 50%;
        background: #e3f2fd;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .card-content {
        min-height: 200px;
      }

      .metrics {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
        gap: 15px;
        margin-top: 15px;
      }

      .metric {
        text-align: center;
        padding: 15px;
        background: #f8f9fa;
        border-radius: 8px;
        transition: all 0.2s ease;
        position: relative;
      }

      .metric::after {
        content: '';
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 3px;
        background: var(--primary);
        transform: scaleX(0);
        transition: transform 0.3s ease;
      }

      .metric:hover {
        background: #e9f7fe;
        transform: translateY(-3px);
      }

      .metric:hover::after {
        transform: scaleX(1);
      }

      .metric-value {
        font-size: 1.8rem;
        font-weight: bold;
        margin: 10px 0;
        color: var(--dark);
      }

      .metric-title {
        font-size: 0.9rem;
        color: #666;
      }

      .events-list {
        max-height: 300px;
        overflow-y: auto;
        padding-right: 5px;
      }

      .events-list::-webkit-scrollbar {
        width: 6px;
      }

      .events-list::-webkit-scrollbar-thumb {
        background: #bdc3c7;
        border-radius: 3px;
      }

      .events-list::-webkit-scrollbar-track {
        background: #f1f1f1;
      }

      .event-item {
        padding: 12px 15px;
        margin-bottom: 10px;
        background: #f8f9fa;
        border-left: 4px solid var(--primary);
        border-radius: 4px;
        display: flex;
        flex-direction: column;
        transition: all 0.2s ease;
      }

      .event-item:hover {
        transform: translateX(3px);
        box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
      }

      .event-item.error {
        border-left-color: var(--danger);
        background: #fef6f6;
      }

      .event-item.warning {
        border-left-color: var(--warning);
        background: #fffbf2;
      }

      .event-item.info {
        border-left-color: var(--primary);
        background: #f0f8ff;
      }

      .event-item.user {
        border-left-color: var(--purple);
        background: #f5f0fa;
      }

      .event-title {
        font-weight: 600;
        margin-bottom: 5px;
        display: flex;
        justify-content: space-between;
      }

      .event-time {
        font-size: 0.8rem;
        color: #777;
      }

      .event-details {
        font-size: 0.9rem;
        color: #555;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .controls {
        display: flex;
        gap: 15px;
        margin-bottom: 30px;
        flex-wrap: wrap;
      }

      .btn {
        padding: 12px 25px;
        border: none;
        border-radius: 6px;
        font-size: 1rem;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
      }

      .btn::after {
        content: '';
        position: absolute;
        top: -50%;
        left: -50%;
        width: 200%;
        height: 200%;
        background: rgba(255, 255, 255, 0.2);
        transform: rotate(30deg) translate(-20px, 100px);
        transition: all 0.6s;
      }

      .btn:hover::after {
        transform: rotate(30deg) translate(20px, -100px);
      }

      .btn-primary {
        background: var(--primary);
        color: white;
      }

      .btn-success {
        background: var(--success);
        color: white;
      }

      .btn-warning {
        background: var(--warning);
        color: white;
      }

      .btn-danger {
        background: var(--danger);
        color: white;
      }

      .btn-purple {
        background: var(--purple);
        color: white;
      }

      .btn:hover {
        opacity: 0.95;
        transform: translateY(-2px);
      }

      .footer {
        text-align: center;
        padding: 20px;
        color: #777;
        font-size: 0.9rem;
        margin-top: 30px;
        border-top: 1px solid #eee;
      }

      .status-indicator {
        display: inline-block;
        width: 12px;
        height: 12px;
        border-radius: 50%;
        margin-right: 8px;
      }

      .status-active {
        background-color: var(--success);
        box-shadow: 0 0 8px var(--success);
      }

      .status-inactive {
        background-color: var(--danger);
      }

      .chart-container {
        height: 250px;
        margin-top: 20px;
      }

      .hidden {
        display: none;
      }

      .sdk-status {
        display: flex;
        align-items: center;
        margin-top: 10px;
        padding: 8px 12px;
        background: rgba(255, 255, 255, 0.1);
        border-radius: 6px;
        font-size: 0.95rem;
        position: relative;
        z-index: 2;
      }

      .tab-container {
        display: flex;
        gap: 10px;
        margin-bottom: 15px;
      }

      .tab {
        padding: 8px 15px;
        background: #e3f2fd;
        border-radius: 20px;
        cursor: pointer;
        transition: all 0.3s ease;
        font-size: 0.9rem;
        font-weight: 500;
      }

      .tab.active {
        background: var(--primary);
        color: white;
      }

      .user-actions {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        margin: 15px 0;
      }

      .action-btn {
        padding: 8px 15px;
        background: #f0f0f0;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        transition: all 0.2s ease;
      }

      .action-btn:hover {
        background: #e0e0e0;
        transform: translateY(-2px);
      }

      @media (max-width: 768px) {
        .dashboard {
          grid-template-columns: 1fr;
        }

        .controls {
          flex-direction: column;
        }

        .user-actions {
          flex-direction: column;
        }
      }
    </style>
  </head>

  <body>
    <div class="container">
      <header>
        <h1>增强版前端监控采集SDK</h1>
        <p class="subtitle">错误采集 | 性能监控 | 资源请求 | 用户行为 | 数据上报</p>
        <div class="sdk-status">
          <span class="status-indicator status-active"></span>
          <span
            >SDK状态:<strong>运行中</strong> | 已收集数据:<strong id="eventCount">0</strong> 条 |
            最后上报时间:<strong id="lastReport">尚未上报</strong></span
          >
        </div>
      </header>

      <div class="controls">
        <button id="triggerError" class="btn btn-danger">触发测试错误</button>
        <button id="triggerRequest" class="btn btn-primary">发送测试请求</button>
        <button id="testPerformance" class="btn btn-warning">测试性能</button>
        <button id="simulateUser" class="btn btn-purple">模拟用户行为</button>
        <button id="sendReport" class="btn btn-success">立即上报数据</button>
        <button id="toggleSDK" class="btn">暂停监控</button>
      </div>

      <div class="dashboard">
        <div class="card">
          <div class="card-header">
            <h2 class="card-title">错误监控</h2>
            <div class="card-icon">⚠️</div>
          </div>
          <div class="card-content">
            <div class="metrics">
              <div class="metric">
                <div class="metric-title">JS错误</div>
                <div id="jsErrorCount" class="metric-value">0</div>
              </div>
              <div class="metric">
                <div class="metric-title">资源错误</div>
                <div id="resourceErrorCount" class="metric-value">0</div>
              </div>
              <div class="metric">
                <div class="metric-title">Promise错误</div>
                <div id="promiseErrorCount" class="metric-value">0</div>
              </div>
            </div>
            <div class="tab-container">
              <div class="tab active" data-tab="errors">错误日志</div>
              <div class="tab" data-tab="console">控制台日志</div>
            </div>
            <div class="events-list" id="errorList"></div>
            <div class="events-list hidden" id="consoleList"></div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">
            <h2 class="card-title">性能指标</h2>
            <div class="card-icon">⏱️</div>
          </div>
          <div class="card-content">
            <div class="metrics">
              <div class="metric">
                <div class="metric-title">页面加载</div>
                <div id="loadTime" class="metric-value">0ms</div>
              </div>
              <div class="metric">
                <div class="metric-title">首次渲染</div>
                <div id="fcpTime" class="metric-value">0ms</div>
              </div>
              <div class="metric">
                <div class="metric-title">DNS查询</div>
                <div id="dnsTime" class="metric-value">0ms</div>
              </div>
            </div>
            <div class="events-list" id="performanceList"></div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">
            <h2 class="card-title">资源请求</h2>
            <div class="card-icon">🌐</div>
          </div>
          <div class="card-content">
            <div class="metrics">
              <div class="metric">
                <div class="metric-title">总请求数</div>
                <div id="totalRequests" class="metric-value">0</div>
              </div>
              <div class="metric">
                <div class="metric-title">失败请求</div>
                <div id="failedRequests" class="metric-value">0</div>
              </div>
              <div class="metric">
                <div class="metric-title">平均耗时</div>
                <div id="avgRequestTime" class="metric-value">0ms</div>
              </div>
            </div>
            <div class="events-list" id="requestList"></div>
          </div>
        </div>
      </div>

      <div class="card">
        <div class="card-header">
          <h2 class="card-title">用户行为监控</h2>
          <div class="card-icon">👤</div>
        </div>
        <div class="card-content">
          <div class="user-actions">
            <button id="trackClick" class="action-btn">跟踪点击</button>
            <button id="trackNavigation" class="action-btn">跟踪页面导航</button>
            <button id="trackApiCall" class="action-btn">跟踪API调用</button>
            <button id="trackPageStay" class="action-btn">跟踪页面停留时间</button>
          </div>
          <div class="metrics">
            <div class="metric">
              <div class="metric-title">点击事件</div>
              <div id="clickEvents" class="metric-value">0</div>
            </div>
            <div class="metric">
              <div class="metric-title">页面导航</div>
              <div id="navigationEvents" class="metric-value">0</div>
            </div>
            <div class="metric">
              <div class="metric-title">页面停留</div>
              <div id="pageStayEvents" class="metric-value">0</div>
            </div>
          </div>
          <div class="events-list" id="userBehaviorList"></div>
        </div>
      </div>

      <div class="card">
        <div class="card-header">
          <h2 class="card-title">数据上报日志</h2>
          <div class="card-icon">📤</div>
        </div>
        <div class="card-content">
          <div class="events-list" id="reportLog"></div>
        </div>
      </div>

      <div class="footer">
        <p>增强版前端监控采集SDK | 实时数据展示面板 | 数据仅用于演示,不会实际发送到服务器</p>
      </div>
    </div>

    <script>
      // 前端监控SDK实现
      class FrontendMonitor {
        constructor(options = {}) {
          // 配置项
          this.config = {
            appId: options.appId || 'default-app',
            reportUrl: options.reportUrl || 'https://api.example.com/monitor/report',
            maxBatchSize: options.maxBatchSize || 5,
            reportInterval: options.reportInterval || 10000,
            enableErrorTracking: options.enableErrorTracking !== false,
            enablePerformanceTracking: options.enablePerformanceTracking !== false,
            enableRequestTracking: options.enableRequestTracking !== false,
            enableUserTracking: options.enableUserTracking !== false,
            enableConsoleTracking: options.enableConsoleTracking !== false,
          }

          // 数据缓存
          this.eventQueue = []
          this.isSending = false
          this.timer = null

          // 统计数据
          this.stats = {
            jsErrors: 0,
            resourceErrors: 0,
            promiseErrors: 0,
            totalRequests: 0,
            failedRequests: 0,
            requestTimes: [],
            lastReportTime: null,
            clickEvents: 0,
            navigationEvents: 0,
            pageStayEvents: 0,
            consoleLogs: 0,
          }

          // 用户行为跟踪相关
          this.currentPage = window.location.href
          this.pageEnterTime = Date.now()
          this.pageStayTimer = null

          // 初始化
          this.init()
        }

        init() {
          if (this.config.enableErrorTracking) {
            this.setupErrorTracking()
          }

          if (this.config.enablePerformanceTracking) {
            this.setupPerformanceTracking()
          }

          if (this.config.enableRequestTracking) {
            this.setupRequestTracking()
          }

          if (this.config.enableUserTracking) {
            this.setupUserTracking()
          }

          if (this.config.enableConsoleTracking) {
            this.setupConsoleTracking()
          }

          // 启动定时上报
          this.startReporting()

          // 在页面卸载前上报数据
          window.addEventListener('beforeunload', () => {
            this.recordPageStayTime()
            this.sendReport('unload')
          })

          // 单页应用路由变化监听
          window.addEventListener('popstate', this.handleRouteChange.bind(this))
          window.addEventListener('hashchange', this.handleRouteChange.bind(this))
        }

        // 设置错误监控
        setupErrorTracking() {
          // JS运行时错误
          window.addEventListener(
            'error',
            event => {
              const { message, filename, lineno, colno, error } = event
              this.captureEvent({
                type: 'js_error',
                message: message,
                filename: filename,
                line: lineno,
                column: colno,
                stack: error && error.stack ? error.stack : '',
                timestamp: Date.now(),
              })
              this.stats.jsErrors++
              updateUI()
            },
            true
          )

          // 资源加载错误
          window.addEventListener(
            'error',
            event => {
              const target = event.target
              if (target !== window && (target.src || target.href)) {
                this.captureEvent({
                  type: 'resource_error',
                  tagName: target.tagName,
                  resourceUrl: target.src || target.href,
                  timestamp: Date.now(),
                })
                this.stats.resourceErrors++
                updateUI()
              }
            },
            true
          )

          // Promise未处理错误
          window.addEventListener('unhandledrejection', event => {
            const reason = event.reason || {}
            this.captureEvent({
              type: 'promise_error',
              message: reason.message || String(reason),
              stack: reason.stack || '',
              timestamp: Date.now(),
            })
            this.stats.promiseErrors++
            updateUI()
          })
        }

        // 设置控制台日志监控
        setupConsoleTracking() {
          const consoleMethods = ['log', 'info', 'warn', 'error']
          const that = this

          consoleMethods.forEach(method => {
            const original = console[method]

            console[method] = function (...args) {
              // 调用原始方法
              original.apply(console, args)

              // 捕获日志
              that.captureEvent({
                type: 'console',
                level: method,
                message: args
                  .map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
                  .join(' '),
                timestamp: Date.now(),
              })
              that.stats.consoleLogs++
              updateUI()
            }
          })
        }

        // 设置性能监控
        setupPerformanceTracking() {
          if (!window.performance) return

          // 获取性能数据
          const timing = performance.timing
          const now = Date.now()

          // 关键性能指标
          const perfData = {
            type: 'performance',
            dns: timing.domainLookupEnd - timing.domainLookupStart,
            tcp: timing.connectEnd - timing.connectStart,
            ttfb: timing.responseStart - timing.requestStart,
            download: timing.responseEnd - timing.responseStart,
            domReady: timing.domContentLoadedEventStart - timing.domLoading,
            load: timing.loadEventEnd - timing.navigationStart,
            fcp: 0, // 首次内容渲染时间
            lcp: 0, // 最大内容渲染时间
            fid: 0, // 首次输入延迟
            timestamp: now,
          }

          // 使用PerformanceObserver获取更多现代性能指标
          if (window.PerformanceObserver) {
            const observer = new PerformanceObserver(list => {
              list.getEntries().forEach(entry => {
                if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
                  perfData.fcp = Math.round(entry.startTime)
                }
              })
            })
            observer.observe({ entryTypes: ['paint'] })
          }

          // 延迟一点确保获取到FCP
          setTimeout(() => {
            this.captureEvent(perfData)
            updateUI()
          }, 1000)
        }

        // 设置请求监控(安全重写)
        setupRequestTracking() {
          const that = this

          // 安全地重写fetch
          if (window.fetch) {
            const originalFetch = window.fetch

            window.fetch = function (input, init) {
              const startTime = Date.now()
              const url = typeof input === 'string' ? input : input.url
              const method = (init && init.method) || 'GET'

              return originalFetch
                .apply(this, arguments)
                .then(response => {
                  // 克隆response以便读取数据而不影响原始流
                  const clonedResponse = response.clone()
                  clonedResponse.text().then(() => {
                    that.trackRequest({
                      method: method,
                      url: url,
                      status: response.status,
                      duration: Date.now() - startTime,
                      timestamp: startTime,
                    })
                  })
                  return response
                })
                .catch(error => {
                  that.trackRequest({
                    method: method,
                    url: url,
                    status: 0,
                    duration: Date.now() - startTime,
                    error: error.message,
                    timestamp: startTime,
                  })
                  throw error
                })
            }
          }

          // 安全地重写XMLHttpRequest
          if (window.XMLHttpRequest) {
            const originalOpen = XMLHttpRequest.prototype.open
            const originalSend = XMLHttpRequest.prototype.send
            const originalAbort = XMLHttpRequest.prototype.abort

            XMLHttpRequest.prototype.open = function (method, url) {
              this._requestMethod = method
              this._requestUrl = url
              return originalOpen.apply(this, arguments)
            }

            XMLHttpRequest.prototype.send = function (body) {
              this._requestStartTime = Date.now()
              this._requestBody = body
              this._requestEnded = false

              const onRequestEnd = () => {
                if (this._requestEnded) return
                this._requestEnded = true

                const duration = Date.now() - this._requestStartTime
                that.trackRequest({
                  method: this._requestMethod,
                  url: this._requestUrl,
                  status: this.status || 0,
                  duration: duration,
                  timestamp: this._requestStartTime,
                })

                // 清理事件监听
                this.removeEventListener('load', onRequestEnd)
                this.removeEventListener('error', onRequestEnd)
                this.removeEventListener('abort', onRequestEnd)
              }

              this.addEventListener('load', onRequestEnd)
              this.addEventListener('error', onRequestEnd)
              this.addEventListener('abort', onRequestEnd)

              return originalSend.apply(this, arguments)
            }

            // 处理abort
            XMLHttpRequest.prototype.abort = function () {
              if (!this._requestEnded) {
                this._requestEnded = true
                const duration = Date.now() - this._requestStartTime
                that.trackRequest({
                  method: this._requestMethod,
                  url: this._requestUrl,
                  status: 0,
                  duration: duration,
                  aborted: true,
                  timestamp: this._requestStartTime,
                })
              }
              return originalAbort.apply(this, arguments)
            }
          }
        }

        // 设置用户行为监控
        setupUserTracking() {
          // 跟踪点击事件
          document.addEventListener(
            'click',
            event => {
              const target = event.target
              const tagName = target.tagName.toLowerCase()
              const id = target.id ? `#${target.id}` : ''
              const classes = target.className ? `.${target.className.split(' ').join('.')}` : ''

              this.captureEvent({
                type: 'user_click',
                element: `${tagName}${id}${classes}`,
                x: event.clientX,
                y: event.clientY,
                timestamp: Date.now(),
              })
              this.stats.clickEvents++
              updateUI()
            },
            { capture: true, passive: true }
          )

          // 开始跟踪页面停留时间
          this.startPageStayTracking()
        }

        // 开始跟踪页面停留时间
        startPageStayTracking() {
          // 清除现有定时器
          if (this.pageStayTimer) clearInterval(this.pageStayTimer)

          // 每10秒记录一次页面停留
          this.pageStayTimer = setInterval(() => {
            this.recordPageStayTime()
          }, 10000)
        }

        // 记录页面停留时间
        recordPageStayTime() {
          const now = Date.now()
          const stayTime = now - this.pageEnterTime

          if (stayTime > 0) {
            this.captureEvent({
              type: 'page_stay',
              pageUrl: this.currentPage,
              stayTime: stayTime,
              timestamp: now,
            })
            this.stats.pageStayEvents++
            updateUI()

            // 重置进入时间
            this.pageEnterTime = now
          }
        }

        // 处理路由变化
        handleRouteChange() {
          // 记录当前页面停留时间
          this.recordPageStayTime()

          // 更新当前页面信息
          this.currentPage = window.location.href

          // 记录导航事件
          this.captureEvent({
            type: 'navigation',
            from: this.previousPage || '',
            to: this.currentPage,
            timestamp: Date.now(),
          })
          this.stats.navigationEvents++
          updateUI()

          // 更新上一页面
          this.previousPage = this.currentPage
        }

        // 跟踪请求
        trackRequest(data) {
          this.stats.totalRequests++
          if (data.status >= 400 || data.status === 0) {
            this.stats.failedRequests++
          }
          this.stats.requestTimes.push(data.duration)

          this.captureEvent({
            type: 'request',
            ...data,
          })

          updateUI()
        }

        // 捕获事件
        captureEvent(event) {
          const fullEvent = {
            appId: this.config.appId,
            pageUrl: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: Date.now(),
            ...event,
          }

          this.eventQueue.push(fullEvent)
          eventCount++

          // 如果队列达到最大批量大小,立即上报
          if (this.eventQueue.length >= this.config.maxBatchSize) {
            this.sendReport('batch')
          }

          // 更新UI
          addEventToUI(fullEvent)
        }

        // 启动定时上报
        startReporting() {
          if (this.timer) clearInterval(this.timer)
          this.timer = setInterval(() => {
            if (this.eventQueue.length > 0) {
              this.sendReport('interval')
            }
          }, this.config.reportInterval)
        }

        // 停止上报
        stopReporting() {
          if (this.timer) {
            clearInterval(this.timer)
            this.timer = null
          }
        }

        // 发送上报
        sendReport(reason = 'manual') {
          if (this.isSending || this.eventQueue.length === 0) return

          this.isSending = true
          const eventsToSend = [...this.eventQueue]
          this.eventQueue = []

          // 在实际应用中,这里会发送数据到服务器
          // 演示中我们模拟发送
          return new Promise(resolve => {
            setTimeout(() => {
              // 记录上报日志
              const reportLog = {
                type: 'report',
                eventCount: eventsToSend.length,
                reason,
                timestamp: Date.now(),
              }

              this.stats.lastReportTime = new Date()
              this.isSending = false

              // 添加到日志
              addReportLogToUI(reportLog)
              updateUI()

              resolve()
            }, 300) // 模拟网络延迟
          })
        }

        // 获取统计数据
        getStats() {
          return {
            ...this.stats,
            eventCount: eventCount,
            avgRequestTime:
              this.stats.requestTimes.length > 0
                ? Math.round(
                    this.stats.requestTimes.reduce((a, b) => a + b, 0) /
                      this.stats.requestTimes.length
                  )
                : 0,
          }
        }

        // 切换SDK状态
        toggle(enabled) {
          if (enabled) {
            this.init()
          } else {
            this.stopReporting()
            // 移除事件监听器
            window.removeEventListener('error', this.errorHandler)
            window.removeEventListener('unhandledrejection', this.promiseHandler)
            window.removeEventListener('click', this.clickHandler)
            window.removeEventListener('popstate', this.routeHandler)
            window.removeEventListener('hashchange', this.routeHandler)

            if (this.pageStayTimer) {
              clearInterval(this.pageStayTimer)
              this.pageStayTimer = null
            }
          }
        }
      }

      // 全局变量
      let monitor
      let eventCount = 0
      let sdkEnabled = true

      // 初始化监控SDK
      function initMonitor() {
        monitor = new FrontendMonitor({
          appId: 'monitor-demo',
          reportUrl: 'https://api.example.com/monitor/report',
          maxBatchSize: 5,
          reportInterval: 15000,
          enableConsoleTracking: true,
        })
      }

      // 更新UI
      function updateUI() {
        if (!monitor) return

        const stats = monitor.getStats()

        // 更新错误统计
        document.getElementById('jsErrorCount').textContent = stats.jsErrors
        document.getElementById('resourceErrorCount').textContent = stats.resourceErrors
        document.getElementById('promiseErrorCount').textContent = stats.promiseErrors

        // 更新请求统计
        document.getElementById('totalRequests').textContent = stats.totalRequests
        document.getElementById('failedRequests').textContent = stats.failedRequests
        document.getElementById('avgRequestTime').textContent = stats.avgRequestTime + 'ms'

        // 更新用户行为统计
        document.getElementById('clickEvents').textContent = stats.clickEvents
        document.getElementById('navigationEvents').textContent = stats.navigationEvents
        document.getElementById('pageStayEvents').textContent = stats.pageStayEvents

        // 更新事件计数
        document.getElementById('eventCount').textContent = eventCount

        // 更新最后上报时间
        if (stats.lastReportTime) {
          document.getElementById('lastReport').textContent =
            stats.lastReportTime.toLocaleTimeString()
        }
      }

      // 添加事件到UI
      function addEventToUI(event) {
        let list, itemClass, title, details

        switch (event.type) {
          case 'js_error':
            list = document.getElementById('errorList')
            itemClass = 'error'
            title = `JS错误: ${event.message}`
            details = `文件: ${event.filename}:${event.line}:${event.column}`
            break

          case 'resource_error':
            list = document.getElementById('errorList')
            itemClass = 'error'
            title = `资源加载失败: ${event.resourceUrl}`
            details = `标签: <${event.tagName}>`
            break

          case 'promise_error':
            list = document.getElementById('errorList')
            itemClass = 'error'
            title = `Promise错误: ${event.message}`
            details = event.stack ? event.stack.substring(0, 100) + '...' : ''
            break

          case 'console':
            list = document.getElementById('consoleList')
            itemClass =
              event.level === 'error' ? 'error' : event.level === 'warn' ? 'warning' : 'info'
            title = `控制台.${event.level}: ${event.message.substring(0, 80)}${
              event.message.length > 80 ? '...' : ''
            }`
            details = ''
            break

          case 'performance':
            list = document.getElementById('performanceList')
            itemClass = ''
            title = `页面性能指标`
            details = `加载时间: ${event.load}ms, FCP: ${event.fcp}ms`

            // 更新性能指标
            document.getElementById('loadTime').textContent = event.load + 'ms'
            document.getElementById('fcpTime').textContent = event.fcp + 'ms'
            document.getElementById('dnsTime').textContent = event.dns + 'ms'
            break

          case 'request':
            list = document.getElementById('requestList')
            itemClass = event.status >= 400 || event.status === 0 ? 'warning' : ''
            title = `${event.method} ${event.url} - ${event.status}`
            details = `耗时: ${event.duration}ms`
            if (event.aborted) details += ' (已中止)'
            break

          case 'user_click':
            list = document.getElementById('userBehaviorList')
            itemClass = 'user'
            title = `用户点击: ${event.element}`
            details = `位置: (${event.x}, ${event.y})`
            break

          case 'navigation':
            list = document.getElementById('userBehaviorList')
            itemClass = 'user'
            title = `页面导航`
            details = `${event.from || ''}${event.to}`
            break

          case 'page_stay':
            list = document.getElementById('userBehaviorList')
            itemClass = 'user'
            title = `页面停留`
            details = `页面: ${event.pageUrl}, 停留: ${Math.round(event.stayTime / 1000)}`
            break

          default:
            return
        }

        const time = new Date(event.timestamp).toLocaleTimeString()
        const eventItem = document.createElement('div')
        eventItem.className = `event-item ${itemClass}`
        eventItem.innerHTML = `
                <div class="event-title">
                    <span>${title}</span>
                    <span class="event-time">${time}</span>
                </div>
                <div class="event-details">${details}</div>
            `

        if (list) {
          list.insertBefore(eventItem, list.firstChild)

          // 限制列表长度
          if (list.children.length > 20) {
            list.removeChild(list.lastChild)
          }
        }
      }

      // 添加上报日志到UI
      function addReportLogToUI(report) {
        const list = document.getElementById('reportLog')
        const time = new Date(report.timestamp).toLocaleTimeString()

        const reasons = {
          manual: '手动触发',
          interval: '定时上报',
          batch: '批量上报',
          unload: '页面卸载',
        }

        const reportItem = document.createElement('div')
        reportItem.className = 'event-item'
        reportItem.innerHTML = `
                <div class="event-title">
                    <span>数据上报 (${reasons[report.reason] || report.reason})</span>
                    <span class="event-time">${time}</span>
                </div>
                <div class="event-details">上报事件数: ${report.eventCount}</div>
            `

        list.insertBefore(reportItem, list.firstChild)

        // 限制列表长度
        if (list.children.length > 10) {
          list.removeChild(list.lastChild)
        }
      }

      // 页面加载完成后初始化
      document.addEventListener('DOMContentLoaded', () => {
        // 初始化监控SDK
        initMonitor()

        // 标签切换
        document.querySelectorAll('.tab').forEach(tab => {
          tab.addEventListener('click', () => {
            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'))
            tab.classList.add('active')

            if (tab.dataset.tab === 'errors') {
              document.getElementById('errorList').classList.remove('hidden')
              document.getElementById('consoleList').classList.add('hidden')
            } else if (tab.dataset.tab === 'console') {
              document.getElementById('errorList').classList.add('hidden')
              document.getElementById('consoleList').classList.remove('hidden')
            }
          })
        })

        // 绑定按钮事件
        document.getElementById('triggerError').addEventListener('click', () => {
          // 触发一个测试错误
          try {
            throw new Error('这是手动触发的测试错误')
          } catch (e) {
            // 捕获并报告
            monitor.captureEvent({
              type: 'js_error',
              message: e.message,
              filename: 'test.js',
              line: 1,
              column: 1,
              stack: e.stack,
              timestamp: Date.now(),
            })
            monitor.stats.jsErrors++
            updateUI()
          }
        })

        document.getElementById('triggerRequest').addEventListener('click', () => {
          // 发送测试请求
          fetch('https://jsonplaceholder.typicode.com/posts/1').catch(() => {}) // 忽略错误

          // 模拟失败请求
          fetch('https://example.com/non-existent-url').catch(() => {}) // 忽略错误

          // 模拟XHR请求
          const xhr = new XMLHttpRequest()
          xhr.open('GET', 'https://jsonplaceholder.typicode.com/comments')
          xhr.send()

          // 模拟中止的请求
          const abortedXhr = new XMLHttpRequest()
          abortedXhr.open('GET', 'https://jsonplaceholder.typicode.com/users')
          abortedXhr.send()
          setTimeout(() => abortedXhr.abort(), 100)
        })

        document.getElementById('testPerformance').addEventListener('click', () => {
          // 模拟性能数据
          monitor.captureEvent({
            type: 'performance',
            dns: 12,
            tcp: 25,
            ttfb: 150,
            download: 200,
            domReady: 1200,
            load: 2200,
            fcp: 850,
            lcp: 1200,
            fid: 15,
            timestamp: Date.now(),
          })
          updateUI()
        })

        document.getElementById('simulateUser').addEventListener('click', () => {
          // 模拟用户行为
          monitor.captureEvent({
            type: 'user_click',
            element: 'button#simulateUser',
            x: 100,
            y: 200,
            timestamp: Date.now(),
          })
          monitor.stats.clickEvents++

          monitor.captureEvent({
            type: 'navigation',
            from: window.location.href,
            to: 'https://example.com/new-page',
            timestamp: Date.now(),
          })
          monitor.stats.navigationEvents++

          monitor.captureEvent({
            type: 'page_stay',
            pageUrl: window.location.href,
            stayTime: 15000,
            timestamp: Date.now(),
          })
          monitor.stats.pageStayEvents++

          updateUI()
        })

        document.getElementById('sendReport').addEventListener('click', () => {
          monitor.sendReport('manual')
        })

        document.getElementById('toggleSDK').addEventListener('click', () => {
          sdkEnabled = !sdkEnabled
          monitor.toggle(sdkEnabled)
          const button = document.getElementById('toggleSDK')
          button.textContent = sdkEnabled ? '暂停监控' : '启动监控'
          button.className = sdkEnabled ? 'btn' : 'btn btn-warning'

          const statusIndicator = document.querySelector('.status-indicator')
          statusIndicator.className = sdkEnabled
            ? 'status-indicator status-active'
            : 'status-indicator status-inactive'
        })

        // 用户行为按钮
        document.getElementById('trackClick').addEventListener('click', () => {
          monitor.captureEvent({
            type: 'user_click',
            element: 'button#trackClick',
            x: 200,
            y: 300,
            timestamp: Date.now(),
          })
          monitor.stats.clickEvents++
          updateUI()
        })

        document.getElementById('trackNavigation').addEventListener('click', () => {
          monitor.captureEvent({
            type: 'navigation',
            from: window.location.href,
            to: 'https://example.com/settings',
            timestamp: Date.now(),
          })
          monitor.stats.navigationEvents++
          updateUI()
        })

        document.getElementById('trackPageStay').addEventListener('click', () => {
          monitor.captureEvent({
            type: 'page_stay',
            pageUrl: window.location.href,
            stayTime: 45000,
            timestamp: Date.now(),
          })
          monitor.stats.pageStayEvents++
          updateUI()
        })

        // 初始UI更新
        updateUI()
      })
    </script>
  </body>
</html>

功能亮点

1. 增强的错误采集

  • JavaScript 运行时错误捕获
  • 资源加载错误监控
  • Promise 未处理错误捕获
  • 控制台日志监控(log, info, warn, error)

2. 全面的性能监控

  • 页面加载时间
  • DNS 查询时间
  • TCP 连接时间
  • 首次内容渲染时间(FCP)
  • DOM 加载时间

3. 安全可靠的资源请求跟踪

  • 重写 fetch API,确保不影响原有功能
  • 重写 XMLHttpRequest,处理各种边界情况(包括请求中止)
  • 支持监控成功和失败的请求
  • 计算平均请求耗时

4. 用户行为监控

  • 点击事件跟踪(元素、位置)
  • 页面导航跟踪(单页应用路由变化)
  • 页面停留时间统计
  • 自定义用户行为跟踪

5. 数据上报系统

  • 多种上报策略:定时上报、批量上报、手动上报
  • 在页面卸载前确保数据上报
  • 上报日志记录

6. 用户友好的控制面板

  • 实时数据统计展示
  • 分类事件日志查看
  • 模拟测试功能
  • SDK 状态控制

技术实现细节

  1. 安全的重写机制

    • 在重写 fetch 和 XMLHttpRequest 时,确保不影响原有功能
    • 克隆 fetch 响应以避免干扰原始数据流
    • 处理请求中止等边界情况
  2. 用户行为跟踪

    • 通过事件监听捕获用户点击
    • 使用定时器统计页面停留时间
    • 监听 popstate 和 hashchange 事件跟踪单页应用路由变化
  3. 错误边界处理

    • 使用 try-catch 处理手动触发的测试错误
    • 对 Promise 错误使用 unhandledrejection 事件监听
  4. 数据上报优化

    • 使用批量上报减少网络请求
    • 在页面卸载前确保数据上报
    • 多种上报策略灵活切换

这个增强版 SDK 提供了全面的前端监控解决方案,帮助开发者深入了解应用性能、错误情况和用户行为。

上次更新于: