前端监控系统详细实践方案
目录
前言
随着前端应用的复杂度不断提高,前端监控系统已经成为保障应用质量和用户体验的关键基础设施。本文将详细介绍基于 Sentry 和 WebTracing 两种主流方案的前端监控系统实践,包括系统架构、部署方案、集成方法、配置优化以及最佳实践,帮助开发团队快速搭建高效的前端监控体系。
前端监控系统概述
为什么需要前端监控
前端监控系统的价值主要体现在以下几个方面:
- 问题快速发现:实时监控前端应用运行状态,第一时间发现异常
- 用户体验提升:通过性能指标监控,持续优化用户体验
- 业务数据分析:结合用户行为数据,为产品决策提供依据
- 开发效率提升:减少问题排查时间,提高开发团队效率
- 预防问题发生:通过趋势分析,预判可能出现的问题
监控指标分类
前端监控系统通常关注以下几类指标:
错误监控
- JavaScript 异常
- Promise 异常
- 资源加载异常
- API 请求异常
- 框架特定异常
性能监控
- 页面加载性能(FP、FCP、LCP 等)
- 交互响应性能(FID、TTI 等)
- 资源加载性能
- JavaScript 执行性能
- 内存使用情况
用户行为监控
- PV/UV 统计
- 用户点击行为
- 页面停留时间
- 页面跳转路径
- 用户操作轨迹
业务指标监控
- 转化率
- 功能使用频率
- 自定义业务指标
监控系统架构
一个完整的前端监控系统通常包含以下核心组件:
- 数据采集层:前端 SDK,负责采集各类监控数据
- 数据传输层:将采集的数据安全高效地传输到服务端
- 数据处理层:对原始数据进行清洗、聚合和分析
- 数据存储层:持久化存储处理后的监控数据
- 数据展示层:直观展示监控数据和分析结果
- 告警系统:根据预设规则触发告警通知
基于 Sentry 的前端监控实践
Sentry 简介
Sentry 是一个开源的实时错误跟踪系统,支持多种编程语言和框架,能够帮助开发者实时监控和修复线上问题。Sentry 提供了云服务和自建部署两种使用方式,具有以下特点:
- 实时错误捕获和报告
- 详细的错误上下文信息
- 源码映射支持(SourceMap)
- 性能监控功能
- 丰富的集成能力
- 完善的告警机制
Sentry 部署方案
方案一:使用 Sentry 云服务
优势:
- 快速上手,无需维护服务器
- 自动扩展,应对高并发场景
- 持续更新,获取最新功能
步骤:
- 注册 Sentry 账号:https://sentry.io
- 创建组织和项目
- 获取 DSN(Data Source Name)
- 前端项目集成 SDK
方案二:自建 Sentry 服务
优势:
- 数据私有化存储
- 可定制化程度高
- 长期使用成本较低
部署步骤:
准备环境
bash# 克隆官方部署仓库 git clone https://github.com/getsentry/onpremise.git cd onpremise # 安装 Docker 和 Docker Compose # Ubuntu apt-get update && apt-get install docker.io docker-compose -y # CentOS yum install docker docker-compose -y systemctl start docker && systemctl enable docker
配置环境变量
创建
.env
文件:COMPOSE_PROJECT_NAME=sentry SENTRY_EVENT_RETENTION_DAYS=90 # 生成密钥 SENTRY_SECRET_KEY=$(docker-compose run --rm web config generate-secret-key)
启动服务
bash# 初始化服务 ./install.sh # 创建管理员账号 docker-compose run --rm web createuser --superuser --email admin@example.com --password <your-password> # 启动所有服务 docker-compose up -d
配置 Nginx 反向代理(可选)
nginxserver { listen 80; server_name sentry.example.com; location / { proxy_pass http://localhost:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
配置 HTTPS(推荐)
使用 Let's Encrypt 配置 HTTPS:
bashapt-get install certbot python3-certbot-nginx -y certbot --nginx -d sentry.example.com
Sentry 前端集成
Vue 项目集成
Vue 2.x 集成
安装依赖
bashnpm install @sentry/vue @sentry/tracing
初始化配置
javascript// main.js import Vue from 'vue'; import Router from 'vue-router'; import * as Sentry from '@sentry/vue'; import { BrowserTracing } from '@sentry/tracing'; const router = new Router({ // 路由配置 }); Sentry.init({ Vue, dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', integrations: [ new BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation(router), tracingOrigins: ['localhost', 'my-site-url.com', /^\//], }), ], tracesSampleRate: 1.0, // 生产环境建议降低采样率,如 0.2 logErrors: true, release: process.env.VUE_APP_VERSION || '1.0.0', environment: process.env.NODE_ENV, }); new Vue({ router, render: h => h(App), }).$mount('#app');
Vue 3.x 集成
安装依赖
bashnpm install @sentry/vue @sentry/tracing
初始化配置
javascript// main.js import { createApp } from 'vue'; import { createRouter } from 'vue-router'; import * as Sentry from '@sentry/vue'; import { BrowserTracing } from '@sentry/tracing'; import App from './App.vue'; const app = createApp(App); const router = createRouter({ // 路由配置 }); app.use(router); Sentry.init({ app, dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', integrations: [ new BrowserTracing({ routingInstrumentation: Sentry.vueRouterInstrumentation(router), tracingOrigins: ['localhost', 'my-site-url.com', /^\//], }), ], tracesSampleRate: 1.0, release: process.env.VUE_APP_VERSION || '1.0.0', environment: process.env.NODE_ENV, }); app.mount('#app');
React 项目集成
安装依赖
bashnpm install @sentry/react @sentry/tracing
初始化配置
javascript// index.js import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import * as Sentry from '@sentry/react'; import { BrowserTracing } from '@sentry/tracing'; import App from './App'; Sentry.init({ dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', integrations: [ new BrowserTracing({ routingInstrumentation: Sentry.reactRouterV5Instrumentation( React.useHistory, React.useLocation, React.useRouteMatch, ), tracingOrigins: ['localhost', 'my-site-url.com', /^\//], }), ], tracesSampleRate: 1.0, release: process.env.REACT_APP_VERSION || '1.0.0', environment: process.env.NODE_ENV, }); ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') );
使用错误边界组件
javascript// App.js import React from 'react'; import * as Sentry from '@sentry/react'; const FallbackComponent = () => ( <div className="error-boundary"> <h2>出错了!</h2> <p>应用遇到了问题,请刷新页面或联系客服。</p> </div> ); const App = () => ( <Sentry.ErrorBoundary fallback={FallbackComponent}> {/* 应用组件 */} </Sentry.ErrorBoundary> ); export default App;
Sentry 高级配置
SourceMap 上传
为了在生产环境中准确定位错误位置,需要上传 SourceMap 文件:
安装 Sentry CLI
bashnpm install @sentry/cli --save-dev
配置 Sentry CLI
创建
.sentryclirc
文件:ini[auth] token=your-auth-token [defaults] url=https://sentry.io/ org=your-org-name project=your-project-name
配置 Webpack 插件
javascript// vue.config.js (Vue CLI) const SentryWebpackPlugin = require('@sentry/webpack-plugin'); module.exports = { configureWebpack: { plugins: [ new SentryWebpackPlugin({ include: './dist', ignore: ['node_modules', 'vue.config.js'], release: process.env.VUE_APP_VERSION || '1.0.0', }), ], }, };
自动化部署脚本
bash#!/bin/bash # 构建应用 npm run build # 创建 release npx sentry-cli releases new "$VERSION" # 上传 SourceMap npx sentry-cli releases files "$VERSION" upload-sourcemaps ./dist --url-prefix "~/" # 完成 release npx sentry-cli releases finalize "$VERSION"
性能监控配置
配置性能监控以跟踪前端性能指标:
Sentry.init({
// 基础配置...
// 启用性能监控
tracesSampleRate: 0.2, // 采样率,生产环境建议 0.1-0.3
// 自定义性能指标
tracingOptions: {
trackComponents: true, // 跟踪组件渲染性能
timeout: 30000, // 事务超时时间(毫秒)
idleTimeout: 5000, // 空闲超时时间(毫秒)
},
});
// 自定义性能事务
const transaction = Sentry.startTransaction({
name: 'DataLoading',
op: 'task',
});
// 设置当前事务为活动事务
Sentry.configureScope(scope => {
scope.setSpan(transaction);
});
// 创建子 span
const span = transaction.startChild({
op: 'http',
description: '加载用户数据',
});
// 异步操作
fetchUserData()
.then(data => {
// 处理数据...
span.setStatus('ok');
})
.catch(error => {
span.setStatus('unknown_error');
Sentry.captureException(error);
})
.finally(() => {
span.finish();
transaction.finish();
});
用户信息关联
关联用户信息,便于追踪特定用户遇到的问题:
// 用户登录后设置用户信息
Sentry.setUser({
id: '12345',
email: 'user@example.com',
username: 'username',
// 自定义属性
subscription: 'premium',
role: 'admin',
});
// 用户登出时清除用户信息
function logout() {
Sentry.setUser(null);
// 登出逻辑...
}
自定义上下文
添加自定义上下文信息,帮助更好地理解错误发生的环境:
// 添加标签(用于分类和筛选)
Sentry.setTag('feature_enabled', 'true');
Sentry.setTag('experiment_group', 'A');
// 添加额外上下文
Sentry.setContext('device', {
screen_resolution: `${window.screen.width}x${window.screen.height}`,
orientation: window.screen.orientation.type,
pixel_ratio: window.devicePixelRatio,
});
// 添加面包屑(用户操作轨迹)
Sentry.addBreadcrumb({
category: 'ui.click',
message: '用户点击了提交按钮',
level: 'info',
});
Sentry 告警配置
Sentry 提供了丰富的告警配置选项,可以根据不同的错误类型和严重程度设置不同的通知规则:
告警规则配置
在 Sentry 管理界面中:
- 进入项目设置 > 告警 > 规则
- 创建新规则,设置触发条件,如:
- 新问题出现
- 问题重新出现
- 问题频率超过阈值
- 受影响用户数超过阈值
通知渠道配置
支持多种通知渠道:
- 电子邮件
- Slack
- 钉钉
- 企业微信
- PagerDuty
- 自定义 Webhook
告警分级示例
# P0 级别(严重) 条件:影响用户数 > 100 或 错误率 > 5% 通知:PagerDuty + Slack + 短信 # P1 级别(高) 条件:影响用户数 > 50 或 错误率 > 2% 通知:Slack + 邮件 # P2 级别(中) 条件:新问题出现 通知:Slack
自定义 Webhook 集成
javascript// 接收 Sentry Webhook 的 Express 路由 app.post('/api/sentry-webhook', (req, res) => { const { action, data } = req.body; if (action === 'created' && data.event) { // 处理新问题 const { title, web_url, project, level } = data; // 发送到内部系统 notifyInternalSystem({ title, url: web_url, project: project.name, level, timestamp: new Date().toISOString(), }); } res.status(200).send('OK'); });
Sentry 最佳实践
错误过滤
避免上报无意义的错误:
javascriptSentry.init({ // 其他配置... beforeSend(event) { // 忽略网络错误 if (event.exception && event.exception.values) { const exceptionValue = event.exception.values[0]; if (exceptionValue.type === 'ChunkLoadError') { return null; // 不上报该错误 } } // 忽略特定来源的错误 if (event.request && event.request.url) { if (event.request.url.includes('third-party-script.js')) { return null; } } return event; }, });
采样率优化
根据环境和用户类型调整采样率:
javascriptSentry.init({ // 其他配置... tracesSampler: samplingContext => { // 根据环境调整 if (samplingContext.environment === 'development') { return 1.0; // 开发环境 100% 采样 } // 根据 URL 路径调整 if (samplingContext.transactionContext.name.includes('/checkout')) { return 0.5; // 结账流程 50% 采样 } // 根据用户类型调整 const user = samplingContext.transactionContext.tags?.user_type; if (user === 'premium') { return 0.8; // 高级用户 80% 采样 } return 0.1; // 默认 10% 采样 }, });
性能优化
减少 Sentry 对应用性能的影响:
javascriptSentry.init({ // 其他配置... // 限制每个事件的附加数据大小 maxBreadcrumbs: 50, // 使用更高效的传输方式 transport: Sentry.makeFetchTransport, // 批量处理事件 maxValueLength: 250, // 限制值长度 normalizeDepth: 5, // 限制对象嵌套深度 // 禁用不需要的集成 integrations: [ new Sentry.BrowserTracing({ // 只跟踪重要的路由 tracingOrigins: ['example.com'], }), // 禁用不需要的默认集成 Sentry.defaultIntegrations.filter(integration => integration.name !== 'Breadcrumbs' ), ], });
版本管理与发布跟踪
javascript// 在 CI/CD 流程中执行 const release = `${process.env.PROJECT_NAME}@${process.env.VERSION}`; // 创建发布版本 Sentry.init({ // 其他配置... release, environment: process.env.NODE_ENV, }); // 关联代码提交 exec(`sentry-cli releases set-commits --auto ${release}`); // 部署完成后标记发布状态 exec(`sentry-cli releases deploys ${release} new -e production`);
自定义错误处理
javascript// 全局错误处理 window.addEventListener('error', event => { // 自定义错误处理逻辑 const { message, filename, lineno, colno, error } = event; // 添加自定义上下文 Sentry.withScope(scope => { scope.setLevel('error'); scope.setExtra('filename', filename); scope.setExtra('line', lineno); scope.setExtra('column', colno); // 添加用户操作记录 scope.addBreadcrumb({ category: 'error', message: `Error occurred at ${filename}:${lineno}:${colno}`, level: 'error', }); Sentry.captureException(error || new Error(message)); }); }); // Promise 错误处理 window.addEventListener('unhandledrejection', event => { Sentry.captureException(event.reason); });
基于 WebTracing 的前端监控实践
WebTracing 简介
WebTracing 是基于 OpenTelemetry 标准的前端监控解决方案,提供了更灵活的自定义能力和更完整的链路追踪功能。与 Sentry 相比,WebTracing 更适合需要深度定制和与后端系统打通的场景。
核心特点:
- 基于 OpenTelemetry 标准,兼容性好
- 支持分布式追踪,可与后端链路打通
- 高度可定制,适应复杂业务场景
- 支持多种后端存储和可视化方案
- 开源免费,无商业限制
WebTracing 架构设计
WebTracing 的整体架构包括以下几个部分:
- 前端 SDK:基于 OpenTelemetry JS SDK 开发,负责数据采集
- 数据传输:通过 HTTP 或 gRPC 协议传输数据
- 后端收集器:OpenTelemetry Collector,负责数据接收、处理和转发
- 存储系统:支持 Jaeger、Zipkin、Prometheus、Elasticsearch 等
- 可视化平台:Grafana、Jaeger UI、Kibana 等
架构图:
+----------------+ HTTP/gRPC +----------------------+
| | -------------> | |
| WebTracing | | OpenTelemetry |
| Browser SDK | <------------- | Collector |
| | Response | |
+----------------+ +----------------------+
| ^
v |
+----------------+ +----------------+ | | +----------------+
| | | | | | | |
| Prometheus | | Elasticsearch | | | | Jaeger |
| (Metrics) | | (Logs) |<--+ +-->| (Traces) |
| | | | | |
+----------------+ +----------------+ +----------------+
^ ^ ^
| | |
v v v
+---------------------------------------------------------------+
| |
| Grafana |
| |
+---------------------------------------------------------------+
WebTracing 部署方案
后端服务部署
使用 Docker Compose 部署 OpenTelemetry Collector
创建
docker-compose.yml
文件:yamlversion: '3' services: # OpenTelemetry Collector otel-collector: image: otel/opentelemetry-collector:latest command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "4317:4317" # OTLP gRPC - "4318:4318" # OTLP HTTP - "8888:8888" # Metrics - "8889:8889" # Health check depends_on: - jaeger - prometheus # Jaeger jaeger: image: jaegertracing/all-in-one:latest ports: - "16686:16686" # UI - "14250:14250" # Model - "14268:14268" # Collector # Prometheus prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" # Grafana grafana: image: grafana/grafana:latest ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - ./grafana-provisioning:/etc/grafana/provisioning depends_on: - prometheus - elasticsearch # Elasticsearch elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0 environment: - discovery.type=single-node - ES_JAVA_OPTS=-Xms512m -Xmx512m ports: - "9200:9200" # Kibana kibana: image: docker.elastic.co/kibana/kibana:7.10.0 ports: - "5601:5601" depends_on: - elasticsearch
配置 OpenTelemetry Collector
创建
otel-collector-config.yaml
文件:yamlreceivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 1s send_batch_size: 1024 memory_limiter: check_interval: 1s limit_mib: 1000 resourcedetection: detectors: [env] filter: metrics: include: match_type: regexp metric_names: - "web_.*" exporters: jaeger: endpoint: jaeger:14250 tls: insecure: true prometheus: endpoint: 0.0.0.0:8889 elasticsearch: endpoints: ["http://elasticsearch:9200"] index: "web-traces" service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, batch, resourcedetection] exporters: [jaeger, elasticsearch] metrics: receivers: [otlp] processors: [memory_limiter, batch, filter, resourcedetection] exporters: [prometheus]
配置 Prometheus
创建
prometheus.yml
文件:yamlglobal: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'otel-collector' scrape_interval: 10s static_configs: - targets: ['otel-collector:8889']
启动服务
bashdocker-compose up -d
数据存储选型
WebTracing 支持多种数据存储方案,可以根据需求选择:
Traces 存储
存储系统 优势 适用场景 Jaeger 专为分布式追踪设计,查询性能好 中小规模应用,追踪数据量不大 Elasticsearch 搜索能力强,支持复杂查询,可扩展性好 大规模应用,需要长期存储和复杂查询 Tempo 高效存储,与 Grafana 集成好 Grafana 生态用户,注重成本效益 Metrics 存储
存储系统 优势 适用场景 Prometheus 时序数据库标准,查询语言强大 性能指标监控,告警系统集成 InfluxDB 写入性能好,支持高基数数据 高频指标采集,自定义指标多 Logs 存储
存储系统 优势 适用场景 Elasticsearch 全文搜索,聚合分析能力强 错误日志分析,需要复杂查询 Loki 资源占用低,标签索引 与 Grafana 集成,成本敏感
WebTracing SDK 集成
基础配置
安装依赖
bashnpm install @opentelemetry/sdk-trace-web @opentelemetry/exporter-trace-otlp-http @opentelemetry/context-zone @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/sdk-metrics
初始化 SDK
创建
tracing.js
文件:javascriptimport { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { ZoneContextManager } from '@opentelemetry/context-zone'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction'; import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; // 创建资源信息 const resource = new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'my-web-app', [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0', [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV, }); // 创建导出器 const exporter = new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces', }); // 创建处理器 const processor = new BatchSpanProcessor(exporter); // 创建提供器 const provider = new WebTracerProvider({ resource, }); // 添加处理器 provider.addSpanProcessor(processor); // 注册提供器 provider.register({ contextManager: new ZoneContextManager(), }); // 注册自动检测 registerInstrumentations({ instrumentations: [ // 页面加载性能 new DocumentLoadInstrumentation(), // 用户交互 new UserInteractionInstrumentation({ eventNames: ['click', 'submit'], }), // XHR 请求 new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls: [/.*/], }), // Fetch 请求 new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [/.*/], }), ], }); // 获取 tracer 实例 const tracer = provider.getTracer('web-tracer'); export { tracer };
在应用入口引入
javascript// main.js 或 index.js import './tracing'; // 其他应用代码...
性能指标采集
Web Vitals 指标采集
javascriptimport { tracer } from './tracing'; import { getMeasurement } from '@opentelemetry/sdk-trace-web'; import { onLCP, onFID, onCLS, onTTFB, onFCP } from 'web-vitals'; // 创建指标上报函数 function reportWebVital(name, value, attribution) { const span = tracer.startSpan(`web.${name.toLowerCase()}`); span.setAttribute('web.vital.name', name); span.setAttribute('web.vital.value', value); // 添加归因信息 if (attribution) { Object.entries(attribution).forEach(([key, value]) => { if (value && typeof value === 'object') { span.setAttribute(`web.vital.attribution.${key}`, JSON.stringify(value)); } else if (value !== undefined && value !== null) { span.setAttribute(`web.vital.attribution.${key}`, String(value)); } }); } span.end(); } // 注册 Web Vitals 监听 onLCP(metric => reportWebVital('LCP', metric.value, metric.attribution)); onFID(metric => reportWebVital('FID', metric.value, metric.attribution)); onCLS(metric => reportWebVital('CLS', metric.value, metric.attribution)); onTTFB(metric => reportWebVital('TTFB', metric.value, metric.attribution)); onFCP(metric => reportWebVital('FCP', metric.value, metric.attribution));
自定义性能指标
javascriptimport { tracer } from './tracing'; import { metrics, ValueType } from '@opentelemetry/api-metrics'; // 创建指标记录器 const meter = metrics.getMeter('web-metrics'); // 创建计数器 const pageViewCounter = meter.createCounter('web.page_views', { description: '页面浏览次数', valueType: ValueType.INT, }); // 创建直方图 const resourceLoadHistogram = meter.createHistogram('web.resource_load_time', { description: '资源加载时间分布', unit: 'ms', valueType: ValueType.DOUBLE, }); // 记录页面浏览 function recordPageView() { const url = window.location.pathname; pageViewCounter.add(1, { page: url }); } // 记录资源加载时间 function recordResourceTiming() { const resources = performance.getEntriesByType('resource'); resources.forEach(resource => { const { name, initiatorType, duration } = resource; resourceLoadHistogram.record(duration, { resource_type: initiatorType, resource_url: name, }); }); } // 页面加载完成后记录 window.addEventListener('load', () => { recordPageView(); recordResourceTiming(); });
错误监控
全局错误捕获
javascriptimport { tracer } from './tracing'; import { SpanStatusCode } from '@opentelemetry/api'; // 捕获 JS 错误 window.addEventListener('error', event => { const { message, filename, lineno, colno, error } = event; const span = tracer.startSpan('error.uncaught'); span.setStatus({ code: SpanStatusCode.ERROR, message: message, }); span.setAttribute('error.type', error?.name || 'Error'); span.setAttribute('error.message', message); span.setAttribute('error.stack', error?.stack || ''); span.setAttribute('error.source', filename); span.setAttribute('error.lineno', lineno); span.setAttribute('error.colno', colno); span.setAttribute('error.url', window.location.href); // 添加用户信息 const user = getUserInfo(); // 自定义函数获取用户信息 if (user) { span.setAttribute('user.id', user.id); span.setAttribute('user.type', user.type); } span.end(); }, true); // 捕获 Promise 错误 window.addEventListener('unhandledrejection', event => { const error = event.reason; const message = error?.message || 'Unhandled Promise Rejection'; const span = tracer.startSpan('error.unhandledrejection'); span.setStatus({ code: SpanStatusCode.ERROR, message: message, }); span.setAttribute('error.type', 'UnhandledRejection'); span.setAttribute('error.message', message); span.setAttribute('error.stack', error?.stack || ''); span.setAttribute('error.url', window.location.href); span.end(); });
Vue 错误处理
javascript// Vue 2.x import Vue from 'vue'; import { tracer } from './tracing'; import { SpanStatusCode } from '@opentelemetry/api'; Vue.config.errorHandler = (error, vm, info) => { const span = tracer.startSpan('error.vue'); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.setAttribute('error.type', error.name); span.setAttribute('error.message', error.message); span.setAttribute('error.stack', error.stack); span.setAttribute('error.component', vm.$options.name || 'AnonymousComponent'); span.setAttribute('error.info', info); span.setAttribute('error.url', window.location.href); span.end(); // 可以继续抛出错误以便控制台显示 console.error(error); };
React 错误处理
javascript// ErrorBoundary.jsx import React from 'react'; import { tracer } from './tracing'; import { SpanStatusCode } from '@opentelemetry/api'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { const span = tracer.startSpan('error.react'); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.setAttribute('error.type', error.name); span.setAttribute('error.message', error.message); span.setAttribute('error.stack', error.stack); span.setAttribute('error.component', this.constructor.name); span.setAttribute('error.componentStack', errorInfo.componentStack); span.setAttribute('error.url', window.location.href); span.end(); } render() { if (this.state.hasError) { return this.props.fallback || <h2>出错了!</h2>; } return this.props.children; } } export default ErrorBoundary;
用户行为追踪
页面导航追踪
javascriptimport { tracer } from './tracing'; // 单页应用路由变化追踪 let currentRoute = window.location.pathname; // 创建路由变化监听函数 function setupRouteTracing() { // 监听 history 变化 const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(this, arguments); handleRouteChange(); }; history.replaceState = function() { originalReplaceState.apply(this, arguments); handleRouteChange(); }; // 监听 popstate 事件 window.addEventListener('popstate', handleRouteChange); // 处理路由变化 function handleRouteChange() { const newRoute = window.location.pathname; if (newRoute !== currentRoute) { const span = tracer.startSpan('navigation.route_change'); span.setAttribute('navigation.from', currentRoute); span.setAttribute('navigation.to', newRoute); span.setAttribute('navigation.type', 'spa'); span.end(); currentRoute = newRoute; } } } setupRouteTracing();
用户点击行为追踪
javascriptimport { tracer } from './tracing'; import { context, trace } from '@opentelemetry/api'; // 创建点击事件监听 function setupClickTracking() { document.addEventListener('click', event => { // 获取点击元素 const target = event.target; // 获取元素信息 const tagName = target.tagName.toLowerCase(); const id = target.id; const classList = Array.from(target.classList).join(' '); const text = target.innerText?.substring(0, 50); const href = target.href || ''; // 创建点击事件 span const clickSpan = tracer.startSpan('user.click'); clickSpan.setAttribute('user.action', 'click'); clickSpan.setAttribute('target.element', tagName); clickSpan.setAttribute('target.id', id); clickSpan.setAttribute('target.class', classList); clickSpan.setAttribute('target.text', text); clickSpan.setAttribute('target.href', href); clickSpan.setAttribute('page.url', window.location.href); clickSpan.setAttribute('page.title', document.title); // 如果是链接点击,记录目标 URL if (tagName === 'a' && href) { clickSpan.setAttribute('navigation.target_url', href); } // 如果是按钮点击,记录按钮类型 if (tagName === 'button') { clickSpan.setAttribute('button.type', target.type || 'button'); } clickSpan.end(); }, true); } setupClickTracking();
表单提交追踪
javascriptimport { tracer } from './tracing'; // 创建表单提交监听 function setupFormTracking() { document.addEventListener('submit', event => { const form = event.target; // 获取表单信息 const formId = form.id; const formName = form.name; const formAction = form.action; const formMethod = form.method.toUpperCase(); // 创建表单提交 span const formSpan = tracer.startSpan('user.form_submit'); formSpan.setAttribute('form.id', formId); formSpan.setAttribute('form.name', formName); formSpan.setAttribute('form.action', formAction); formSpan.setAttribute('form.method', formMethod); formSpan.setAttribute('page.url', window.location.href); // 获取表单字段数量(不记录具体值以保护隐私) const inputCount = form.querySelectorAll('input').length; const selectCount = form.querySelectorAll('select').length; const textareaCount = form.querySelectorAll('textarea').length; formSpan.setAttribute('form.input_count', inputCount); formSpan.setAttribute('form.select_count', selectCount); formSpan.setAttribute('form.textarea_count', textareaCount); formSpan.end(); }, true); } setupFormTracking();
网络请求监控
Fetch API 监控
javascriptimport { tracer } from './tracing'; import { SpanStatusCode } from '@opentelemetry/api'; // 增强 Fetch API 监控 function enhanceFetchMonitoring() { const originalFetch = window.fetch; window.fetch = async function(input, init) { const url = typeof input === 'string' ? input : input.url; const method = init?.method || 'GET'; // 创建 span const span = tracer.startSpan('http.fetch'); span.setAttribute('http.url', url); span.setAttribute('http.method', method); // 添加请求头大小 if (init?.headers) { const headerSize = JSON.stringify(init.headers).length; span.setAttribute('http.request_header_size', headerSize); } // 添加请求体大小 if (init?.body) { let bodySize = 0; if (typeof init.body === 'string') { bodySize = init.body.length; } else if (init.body instanceof FormData || init.body instanceof URLSearchParams) { bodySize = String(init.body).length; } else if (init.body instanceof Blob) { bodySize = init.body.size; } span.setAttribute('http.request_body_size', bodySize); } try { const response = await originalFetch.apply(this, arguments); // 记录响应信息 span.setAttribute('http.status_code', response.status); span.setAttribute('http.status_text', response.statusText); span.setAttribute('http.response_content_type', response.headers.get('content-type') || ''); // 克隆响应以获取响应体大小 const clonedResponse = response.clone(); const buffer = await clonedResponse.arrayBuffer(); span.setAttribute('http.response_body_size', buffer.byteLength); // 设置状态 if (response.ok) { span.setStatus({ code: SpanStatusCode.OK }); } else { span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP Error ${response.status}: ${response.statusText}`, }); } span.end(); return response; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.setAttribute('error.type', error.name || 'FetchError'); span.setAttribute('error.message', error.message); span.setAttribute('error.stack', error.stack || ''); span.end(); throw error; } }; } enhanceFetchMonitoring();
Axios 拦截器监控
javascriptimport { tracer } from './tracing'; import { SpanStatusCode } from '@opentelemetry/api'; import axios from 'axios'; // 设置 Axios 拦截器 function setupAxiosMonitoring() { // 请求拦截器 axios.interceptors.request.use(config => { // 创建 span const span = tracer.startSpan('http.axios'); // 保存 span 到请求配置中 config._otelSpan = span; // 记录请求信息 span.setAttribute('http.method', config.method.toUpperCase()); span.setAttribute('http.url', config.url); span.setAttribute('http.request_timeout', config.timeout); // 记录请求头信息(排除敏感信息) const headers = { ...config.headers }; delete headers.Authorization; delete headers.Cookie; span.setAttribute('http.request_headers', JSON.stringify(headers)); // 记录请求体大小 if (config.data) { const bodySize = JSON.stringify(config.data).length; span.setAttribute('http.request_body_size', bodySize); } return config; }, error => { // 请求错误 const span = tracer.startSpan('http.axios.error'); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.setAttribute('error.type', 'AxiosRequestError'); span.setAttribute('error.message', error.message); span.end(); return Promise.reject(error); }); // 响应拦截器 axios.interceptors.response.use(response => { const span = response.config._otelSpan; if (span) { // 记录响应信息 span.setAttribute('http.status_code', response.status); span.setAttribute('http.response_size', JSON.stringify(response.data).length); span.setAttribute('http.response_time', Date.now() - response.config.metadata.startTime); // 设置状态 span.setStatus({ code: SpanStatusCode.OK }); span.end(); } return response; }, error => { const span = error.config?._otelSpan; if (span) { // 记录错误信息 span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); if (error.response) { // 服务器响应错误 span.setAttribute('http.status_code', error.response.status); span.setAttribute('http.status_text', error.response.statusText); span.setAttribute('error.type', 'AxiosResponseError'); } else if (error.request) { // 请求未收到响应 span.setAttribute('error.type', 'AxiosNoResponseError'); } else { // 请求配置错误 span.setAttribute('error.type', 'AxiosConfigError'); } span.setAttribute('error.message', error.message); span.end(); } return Promise.reject(error); }); } setupAxiosMonitoring();
WebTracing 数据分析与可视化
基于 WebTracing 收集的数据,可以构建丰富的分析和可视化面板:
Grafana 仪表盘配置
创建
grafana-provisioning/dashboards/web-monitoring.json
文件:json{ "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "id": 1, "links": [], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 2, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.0", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "web_page_views_total", "interval": "", "legendFormat": "{{page}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "页面浏览量", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.2.0", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "histogram_quantile(0.95, sum(rate(web_resource_load_time_bucket[5m])) by (le, resource_type))", "interval": "", "legendFormat": "{{resource_type}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "资源加载时间 (P95)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": "5s", "schemaVersion": 26, "style": "dark", "tags": [], "templating": { "list": [] }, "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Web 监控仪表盘", "uid": "web-monitoring", "version": 1 }
Jaeger UI 配置
Jaeger UI 提供了分布式追踪可视化,可以通过
http://localhost:16686
访问。主要功能包括:- 服务依赖图:展示服务间调用关系
- 追踪查询:按服务、操作、标签等筛选追踪数据
- 追踪详情:展示完整的调用链和时间分布
- 比较视图:对比不同追踪的差异
Kibana 仪表盘
创建 Kibana 索引模式和可视化:
- 创建索引模式:
web-traces*
- 创建错误分析仪表盘,包含:
- 错误类型分布饼图
- 错误趋势时间线图
- 按页面分组的错误表格
- 错误详情列表
- 创建索引模式:
WebTracing 最佳实践
性能优化
javascript// 优化采样策略 const provider = new WebTracerProvider({ resource, sampler: new ParentBasedSampler({ root: new TraceIdRatioBased(0.2), // 生产环境采样率 20% }), }); // 批处理优化 const processor = new BatchSpanProcessor(exporter, { maxQueueSize: 100, // 最大队列大小 maxExportBatchSize: 10, // 最大批处理大小 scheduledDelayMillis: 500, // 调度延迟 exportTimeoutMillis: 30000, // 导出超时 });
错误过滤
javascript// 过滤不需要的错误 window.addEventListener('error', event => { // 忽略第三方脚本错误 if (event.filename && isThirdPartyScript(event.filename)) { event.preventDefault(); return; } // 忽略 CORS 错误 if (event.message && event.message.includes('Script error.')) { event.preventDefault(); return; } // 处理其他错误... }, true); function isThirdPartyScript(url) { const thirdPartyDomains = [ 'cdn.example.com', 'analytics.example.com', ]; return thirdPartyDomains.some(domain => url.includes(domain)); }
用户隐私保护
javascript// 敏感信息过滤 function sanitizeUserData(userData) { const sanitized = { ...userData }; // 移除敏感信息 delete sanitized.password; delete sanitized.token; delete sanitized.creditCard; // 脱敏电话和邮箱 if (sanitized.phone) { sanitized.phone = sanitized.phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); } if (sanitized.email) { const [name, domain] = sanitized.email.split('@'); sanitized.email = `${name.charAt(0)}***@${domain}`; } return sanitized; } // 使用脱敏后的用户数据 const user = getUserInfo(); const sanitizedUser = sanitizeUserData(user); span.setAttribute('user.id', sanitizedUser.id); span.setAttribute('user.type', sanitizedUser.type); span.setAttribute('user.email', sanitizedUser.email);
自定义业务指标
javascript// 创建业务指标记录器 function createBusinessMetrics() { const meter = metrics.getMeter('business-metrics'); // 转化率计数器 const conversionCounter = meter.createCounter('business.conversion', { description: '转化次数', }); // 订单金额直方图 const orderValueHistogram = meter.createHistogram('business.order_value', { description: '订单金额分布', unit: 'CNY', }); // 用户停留时间直方图 const sessionDurationHistogram = meter.createHistogram('business.session_duration', { description: '用户会话时长', unit: 's', }); return { recordConversion: (step, success = true) => { conversionCounter.add(1, { step, success: String(success), }); }, recordOrderValue: (value, category) => { orderValueHistogram.record(value, { category, }); }, recordSessionDuration: (duration, pageType) => { sessionDurationHistogram.record(duration, { page_type: pageType, }); }, }; } // 使用业务指标 const businessMetrics = createBusinessMetrics(); // 记录转化 document.querySelector('#signup-button').addEventListener('click', () => { businessMetrics.recordConversion('signup_click'); }); // 记录订单 function handleOrderComplete(order) { businessMetrics.recordOrderValue(order.totalAmount, order.category); } // 记录会话时长 window.addEventListener('beforeunload', () => { const duration = (Date.now() - window.performance.timing.navigationStart) / 1000; businessMetrics.recordSessionDuration(duration, getPageType()); });
自定义前端监控系统实现
核心架构设计
自定义前端监控系统的核心架构主要包含以下几个组件:
- 数据采集层:负责收集各种类型的监控数据,包括错误日志、性能指标、用户行为等。
- 数据处理层:对采集到的原始数据进行清洗、格式化和初步分析。
- 数据传输层:将处理后的数据安全、高效地传输到后端存储系统。
- 数据存储层:持久化存储监控数据,支持快速查询和分析。
- 数据展示层:通过可视化界面展示监控数据,提供实时监控和历史数据分析功能。
- 告警通知层:当监控指标达到预设阈值时,及时发送告警通知给相关人员。
这种分层架构设计使得系统具有良好的可扩展性和可维护性,各层之间通过标准接口进行通信,降低了组件间的耦合度。
数据采集模块
数据采集是前端监控系统的第一步,也是至关重要的一步。一个完善的监控系统需要能够采集各种类型的前端数据,包括但不限于:
- JavaScript错误:通过
window.onerror
和window.addEventListener('error')
捕获未处理的JavaScript异常。 - Promise拒绝:通过
window.addEventListener('unhandledrejection')
捕获未处理的Promise拒绝。 - 资源加载错误:监控图片、CSS、JavaScript等静态资源的加载失败情况。
- 网络请求状态:监控Ajax请求的成功率、响应时间等指标。
- 页面性能指标:采集页面加载时间、首屏渲染时间等性能数据。
- 用户行为数据:记录用户的点击、滚动、页面跳转等行为。
为了实现这些数据采集功能,我们可以设计一个监控SDK,提供统一的API供业务代码调用,同时自动捕获各种异常和性能数据。SDK内部通过拦截原生API(如fetch
、XMLHttpRequest
)和监听DOM事件来实现数据采集。
// 监控SDK核心代码示例
class Monitor {
constructor(options) {
this.options = options || {};
this.init();
}
init() {
// 初始化各种监控项
this.initError();
this.initPerformance();
this.initXHR();
}
// 初始化错误监控
initError() {
// 捕获JavaScript运行时错误
window.addEventListener('error', (event) => {
this.reportError({
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
// 捕获Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise',
message: event.reason
});
});
}
// 初始化性能监控
initPerformance() {
// 页面加载完成后采集性能数据
window.addEventListener('load', () => {
setTimeout(() => {
const performance = window.performance;
if (performance) {
const data = performance.getEntriesByType('navigation')[0];
this.reportPerformance({
// 采集关键性能指标
dnsTime: data.domainLookupEnd - data.domainLookupStart,
tcpTime: data.connectEnd - data.connectStart,
requestTime: data.responseEnd - data.requestStart,
domParseTime: data.domContentLoadedEventEnd - data.domContentLoadedEventStart
});
}
}, 0);
});
}
// 初始化XHR监控
initXHR() {
const originalXHR = window.XMLHttpRequest;
const originalOpen = originalXHR.prototype.open;
const originalSend = originalXHR.prototype.send;
// 拦截XMLHttpRequest.open方法
originalXHR.prototype.open = function(method, url, async, user, password) {
this._url = url;
this._method = method;
return originalOpen.apply(this, arguments);
};
// 拦截XMLHttpRequest.send方法
originalXHR.prototype.send = function(body) {
const startTime = Date.now();
const xhr = this;
// 监听请求完成事件
const onReadyStateChange = () => {
if (xhr.readyState === 4) {
const duration = Date.now() - startTime;
// 上报请求性能数据
this.reportRequest({
method: xhr._method,
url: xhr._url,
status: xhr.status,
duration: duration
});
// 移除事件监听
xhr.removeEventListener('readystatechange', onReadyStateChange);
}
};
xhr.addEventListener('readystatechange', onReadyStateChange);
return originalSend.apply(this, arguments);
}.bind(this);
}
// 上报错误数据
reportError(errorData) {
this.sendData({
type: 'error',
data: errorData
});
}
// 上报性能数据
reportPerformance(perfData) {
this.sendData({
type: 'performance',
data: perfData
});
}
// 上报请求数据
reportRequest(requestData) {
this.sendData({
type: 'request',
data: requestData
});
}
// 发送数据到后端
sendData(data) {
// 添加时间戳和用户标识
const payload = {
...data,
timestamp: Date.now(),
userId: this.getUserId(),
userAgent: navigator.userAgent
};
// 发送到后端服务器
fetch(this.options.reportUrl, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
}).catch(err => {
console.error('数据上报失败:', err);
});
}
// 获取用户标识
getUserId() {
// 实现获取用户标识逻辑
return localStorage.getItem('userId') || 'anonymous';
}
}
// 使用示例
const monitor = new Monitor({
reportUrl: 'https://your-monitoring-server.com/report'
});
通过上述代码实现,我们构建了一个基础的前端监控SDK,能够自动捕获JavaScript错误、Promise拒绝、页面性能指标和网络请求状态。在实际应用中,还可以根据具体需求扩展更多监控功能。
高级功能扩展
为了构建更完善的监控系统,我们还需要实现以下高级功能:
1. 用户行为追踪
// 扩展Monitor类,添加用户行为追踪
class Monitor {
// ... 之前的代码 ...
init() {
this.initError();
this.initPerformance();
this.initXHR();
this.initFetch(); // 添加Fetch拦截
this.initUserBehavior(); // 添加用户行为追踪
this.initResourceMonitor(); // 添加资源监控
}
// 初始化用户行为追踪
initUserBehavior() {
// 页面访问追踪
this.trackPageView();
// 点击事件追踪
document.addEventListener('click', (event) => {
const target = event.target;
this.reportUserBehavior({
type: 'click',
element: target.tagName,
className: target.className,
id: target.id,
text: target.innerText?.substring(0, 100),
xpath: this.getXPath(target),
timestamp: Date.now()
});
});
// 页面停留时间追踪
this.startTime = Date.now();
window.addEventListener('beforeunload', () => {
const stayTime = Date.now() - this.startTime;
this.reportUserBehavior({
type: 'page_stay',
duration: stayTime,
url: window.location.href
});
});
// 滚动深度追踪
let maxScrollDepth = 0;
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollDepth = Math.round((scrollTop + windowHeight) / documentHeight * 100);
if (scrollDepth > maxScrollDepth) {
maxScrollDepth = scrollDepth;
this.reportUserBehavior({
type: 'scroll_depth',
depth: scrollDepth,
url: window.location.href
});
}
});
}
// 获取元素XPath
getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`;
}
const parts = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let nbOfPreviousSiblings = 0;
let hasNextSiblings = false;
let sibling = element.previousSibling;
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
nbOfPreviousSiblings++;
}
sibling = sibling.previousSibling;
}
sibling = element.nextSibling;
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
hasNextSiblings = true;
break;
}
sibling = sibling.nextSibling;
}
const prefix = element.nodeName.toLowerCase();
const nth = nbOfPreviousSiblings || hasNextSiblings ? `[${nbOfPreviousSiblings + 1}]` : '';
parts.push(prefix + nth);
element = element.parentNode;
}
return parts.length ? '/' + parts.reverse().join('/') : '';
}
// 页面访问追踪
trackPageView() {
this.reportUserBehavior({
type: 'page_view',
url: window.location.href,
referrer: document.referrer,
title: document.title,
timestamp: Date.now()
});
}
// 初始化Fetch拦截
initFetch() {
if (!window.fetch) return;
const originalFetch = window.fetch;
window.fetch = (...args) => {
const startTime = Date.now();
const url = args[0];
const options = args[1] || {};
return originalFetch.apply(this, args)
.then(response => {
const duration = Date.now() - startTime;
this.reportRequest({
method: options.method || 'GET',
url: url,
status: response.status,
duration: duration,
type: 'fetch'
});
return response;
})
.catch(error => {
const duration = Date.now() - startTime;
this.reportError({
type: 'fetch_error',
message: error.message,
url: url,
duration: duration
});
throw error;
});
};
}
// 初始化资源监控
initResourceMonitor() {
// 监控资源加载错误
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.reportError({
type: 'resource_error',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
message: '资源加载失败'
});
}
}, true);
// 监控资源加载性能
if (window.PerformanceObserver) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'resource') {
this.reportPerformance({
type: 'resource_timing',
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
initiatorType: entry.initiatorType
});
}
});
});
observer.observe({ entryTypes: ['resource'] });
}
}
// 上报用户行为数据
reportUserBehavior(behaviorData) {
this.sendData({
type: 'user_behavior',
data: behaviorData
});
}
}
2. 数据缓存与批量上报机制
class Monitor {
constructor(options) {
this.options = {
batchSize: 10, // 批量上报数量
batchTimeout: 5000, // 批量上报超时时间
maxRetries: 3, // 最大重试次数
...options
};
this.dataQueue = []; // 数据队列
this.timer = null; // 定时器
this.init();
}
// 改进的数据发送方法
sendData(data) {
const payload = {
...data,
timestamp: Date.now(),
userId: this.getUserId(),
userAgent: navigator.userAgent,
url: window.location.href,
sessionId: this.getSessionId()
};
// 添加到队列
this.dataQueue.push(payload);
// 检查是否需要立即发送
if (this.dataQueue.length >= this.options.batchSize) {
this.flushData();
} else {
// 设置定时发送
this.scheduleFlush();
}
}
// 定时发送数据
scheduleFlush() {
if (this.timer) return;
this.timer = setTimeout(() => {
this.flushData();
}, this.options.batchTimeout);
}
// 批量发送数据
async flushData() {
if (this.dataQueue.length === 0) return;
const data = [...this.dataQueue];
this.dataQueue = [];
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
try {
await this.sendBatch(data);
} catch (error) {
console.error('批量发送失败:', error);
// 重新加入队列,等待重试
this.dataQueue.unshift(...data);
}
}
// 发送批量数据
async sendBatch(data, retries = 0) {
try {
const response = await fetch(this.options.reportUrl, {
method: 'POST',
body: JSON.stringify({ events: data }),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response;
} catch (error) {
if (retries < this.options.maxRetries) {
// 指数退避重试
const delay = Math.pow(2, retries) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return this.sendBatch(data, retries + 1);
}
throw error;
}
}
// 获取会话ID
getSessionId() {
let sessionId = sessionStorage.getItem('monitor_session_id');
if (!sessionId) {
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('monitor_session_id', sessionId);
}
return sessionId;
}
// 页面卸载时发送剩余数据
init() {
// ... 之前的初始化代码 ...
// 页面卸载时发送剩余数据
window.addEventListener('beforeunload', () => {
if (this.dataQueue.length > 0) {
// 使用sendBeacon API确保数据能够发送
if (navigator.sendBeacon) {
navigator.sendBeacon(
this.options.reportUrl,
JSON.stringify({ events: this.dataQueue })
);
}
}
});
}
}
数据上报与存储
数据采集完成后,下一步是将数据安全、高效地传输到后端存储系统。这个过程需要考虑数据的实时性、可靠性和安全性。
数据上报策略
- 批量上报:为了避免频繁的网络请求影响页面性能,可以将采集到的数据先缓存在本地,然后定期批量上报。
- 失败重试:网络不稳定可能导致数据上报失败,需要实现重试机制确保数据不丢失。
- 数据压缩:对于大量数据,可以考虑使用gzip等压缩算法减小传输体积。
- 加密传输:敏感数据需要通过HTTPS等加密方式传输,防止数据泄露。
后端存储方案
后端存储系统需要具备高并发处理能力和大数据存储能力,常见的技术选型包括:
- 时序数据库:如InfluxDB,专门用于处理时间序列数据,适合存储性能指标等时序数据。
- 分布式数据库:如MongoDB或Elasticsearch,支持水平扩展,适合存储结构化和非结构化的监控数据。
- 消息队列:如Kafka或RabbitMQ,用于缓冲大量监控数据,削峰填谷,提高系统的稳定性。
- 对象存储:如AWS S3或阿里云OSS,用于存储大文件或长期归档数据。
后端服务实现示例
以下是一个基于Node.js和Express的监控数据接收服务示例:
// 监控数据接收服务 (Node.js + Express)
const express = require('express');
const cors = require('cors');
const { MongoClient } = require('mongodb');
const Redis = require('redis');
const app = express();
const port = 3000;
// 中间件配置
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// 数据库连接
let db;
let redisClient;
async function initDatabase() {
// MongoDB连接
const mongoClient = new MongoClient('mongodb://localhost:27017');
await mongoClient.connect();
db = mongoClient.db('monitoring');
// Redis连接
redisClient = Redis.createClient();
await redisClient.connect();
console.log('数据库连接成功');
}
// 数据验证中间件
function validateData(req, res, next) {
const { events } = req.body;
if (!Array.isArray(events)) {
return res.status(400).json({ error: '数据格式错误' });
}
// 验证每个事件的必要字段
for (const event of events) {
if (!event.type || !event.timestamp) {
return res.status(400).json({ error: '缺少必要字段' });
}
}
next();
}
// 数据处理函数
async function processEvents(events) {
const processedEvents = events.map(event => ({
...event,
receivedAt: new Date(),
processed: false
}));
// 按类型分类存储
const errorEvents = processedEvents.filter(e => e.type === 'error');
const performanceEvents = processedEvents.filter(e => e.type === 'performance');
const userBehaviorEvents = processedEvents.filter(e => e.type === 'user_behavior');
const requestEvents = processedEvents.filter(e => e.type === 'request');
// 批量插入数据库
const promises = [];
if (errorEvents.length > 0) {
promises.push(db.collection('errors').insertMany(errorEvents));
}
if (performanceEvents.length > 0) {
promises.push(db.collection('performance').insertMany(performanceEvents));
}
if (userBehaviorEvents.length > 0) {
promises.push(db.collection('user_behavior').insertMany(userBehaviorEvents));
}
if (requestEvents.length > 0) {
promises.push(db.collection('requests').insertMany(requestEvents));
}
await Promise.all(promises);
// 更新实时统计
await updateRealTimeStats(processedEvents);
}
// 更新实时统计
async function updateRealTimeStats(events) {
const now = new Date();
const hourKey = `stats:${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-${now.getHours()}`;
for (const event of events) {
// 更新错误统计
if (event.type === 'error') {
await redisClient.hIncrBy(hourKey, 'error_count', 1);
}
// 更新性能统计
if (event.type === 'performance' && event.data.duration) {
await redisClient.hIncrBy(hourKey, 'performance_count', 1);
await redisClient.hIncrBy(hourKey, 'performance_total', event.data.duration);
}
// 更新用户活跃度
if (event.type === 'user_behavior' && event.data.type === 'page_view') {
await redisClient.sAdd(`active_users:${hourKey}`, event.userId);
}
}
// 设置过期时间(7天)
await redisClient.expire(hourKey, 7 * 24 * 3600);
}
// 监控数据接收接口
app.post('/api/monitor/report', validateData, async (req, res) => {
try {
const { events } = req.body;
// 异步处理数据
processEvents(events).catch(error => {
console.error('数据处理失败:', error);
});
res.json({ success: true, received: events.length });
} catch (error) {
console.error('接收数据失败:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 获取实时统计数据
app.get('/api/monitor/stats', async (req, res) => {
try {
const now = new Date();
const hourKey = `stats:${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-${now.getHours()}`;
const stats = await redisClient.hGetAll(hourKey);
const activeUsers = await redisClient.sCard(`active_users:${hourKey}`);
res.json({
errorCount: parseInt(stats.error_count || 0),
performanceCount: parseInt(stats.performance_count || 0),
avgPerformance: stats.performance_count ?
parseInt(stats.performance_total) / parseInt(stats.performance_count) : 0,
activeUsers: activeUsers
});
} catch (error) {
console.error('获取统计数据失败:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 启动服务
initDatabase().then(() => {
app.listen(port, () => {
console.log(`监控服务运行在端口 ${port}`);
});
});
数据库设计
MongoDB集合设计:
// 错误事件集合 (errors)
{
_id: ObjectId,
type: "error",
data: {
type: "javascript|promise|resource_error|fetch_error",
message: "错误信息",
filename: "文件名",
lineno: 123,
colno: 45,
stack: "错误堆栈"
},
timestamp: 1640995200000,
userId: "user123",
sessionId: "session_abc",
url: "https://example.com/page",
userAgent: "Mozilla/5.0...",
receivedAt: ISODate("2021-12-31T16:00:00Z"),
processed: false
}
// 性能事件集合 (performance)
{
_id: ObjectId,
type: "performance",
data: {
type: "navigation|resource_timing",
dnsTime: 50,
tcpTime: 100,
requestTime: 200,
domParseTime: 300,
name: "资源名称",
duration: 500,
size: 1024
},
timestamp: 1640995200000,
userId: "user123",
sessionId: "session_abc",
url: "https://example.com/page",
receivedAt: ISODate("2021-12-31T16:00:00Z")
}
// 用户行为集合 (user_behavior)
{
_id: ObjectId,
type: "user_behavior",
data: {
type: "click|page_view|scroll_depth|page_stay",
element: "BUTTON",
className: "btn-primary",
xpath: "/html/body/div[1]/button[1]",
depth: 75,
duration: 30000
},
timestamp: 1640995200000,
userId: "user123",
sessionId: "session_abc",
url: "https://example.com/page",
receivedAt: ISODate("2021-12-31T16:00:00Z")
}
索引设计:
// 为查询性能优化创建索引
db.errors.createIndex({ timestamp: 1, userId: 1 });
db.errors.createIndex({ "data.type": 1, timestamp: 1 });
db.performance.createIndex({ timestamp: 1, "data.type": 1 });
db.user_behavior.createIndex({ userId: 1, timestamp: 1 });
db.user_behavior.createIndex({ "data.type": 1, timestamp: 1 });
// TTL索引,自动删除过期数据(保留30天)
db.errors.createIndex({ receivedAt: 1 }, { expireAfterSeconds: 2592000 });
db.performance.createIndex({ receivedAt: 1 }, { expireAfterSeconds: 2592000 });
db.user_behavior.createIndex({ receivedAt: 1 }, { expireAfterSeconds: 2592000 });
存储架构设计
一个典型的监控数据存储架构可能包含以下组件:
- 数据接收服务:提供HTTP接口接收前端上报的数据,进行初步验证和清洗。
- 消息队列:将接收到的数据写入消息队列,实现数据缓冲和异步处理。
- 数据处理服务:从消息队列消费数据,进行进一步的处理、分析和聚合。
- 主存储系统:存储处理后的结构化数据,支持快速查询和分析。
- 冷存储系统:将历史数据归档到成本更低的存储系统中,用于长期保存。
通过这样的架构设计,可以确保监控系统在高并发场景下的稳定性和可扩展性。
可视化与告警
监控数据的价值在于能够通过可视化的方式展示出来,并在异常情况发生时及时告警。
可视化方案
- 数据看板:构建实时监控看板,展示关键指标的趋势图、饼图、柱状图等,帮助运维人员快速了解系统状态。
- 多维度分析:支持按时间、地域、用户群体等维度对数据进行筛选和分析,深入挖掘问题根源。
- 自定义报表:允许用户根据业务需求自定义报表模板,定期生成和发送监控报告。
常见的可视化工具包括Grafana、Kibana等,它们提供了丰富的图表组件和灵活的配置选项。
前端Dashboard实现示例
以下是一个基于React和Chart.js的监控Dashboard实现:
// 监控Dashboard组件
import React, { useState, useEffect } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
ArcElement
} from 'chart.js';
import { Line, Bar, Doughnut } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
Title,
Tooltip,
Legend,
ArcElement
);
const MonitoringDashboard = () => {
const [stats, setStats] = useState({
errorCount: 0,
performanceCount: 0,
avgPerformance: 0,
activeUsers: 0
});
const [errorTrend, setErrorTrend] = useState([]);
const [performanceTrend, setPerformanceTrend] = useState([]);
const [errorTypes, setErrorTypes] = useState([]);
// 获取实时统计数据
useEffect(() => {
const fetchStats = async () => {
try {
const response = await fetch('/api/monitor/stats');
const data = await response.json();
setStats(data);
} catch (error) {
console.error('获取统计数据失败:', error);
}
};
// 获取错误趋势数据
const fetchErrorTrend = async () => {
try {
const response = await fetch('/api/monitor/error-trend?hours=24');
const data = await response.json();
setErrorTrend(data);
} catch (error) {
console.error('获取错误趋势失败:', error);
}
};
// 获取性能趋势数据
const fetchPerformanceTrend = async () => {
try {
const response = await fetch('/api/monitor/performance-trend?hours=24');
const data = await response.json();
setPerformanceTrend(data);
} catch (error) {
console.error('获取性能趋势失败:', error);
}
};
// 获取错误类型分布
const fetchErrorTypes = async () => {
try {
const response = await fetch('/api/monitor/error-types?hours=24');
const data = await response.json();
setErrorTypes(data);
} catch (error) {
console.error('获取错误类型失败:', error);
}
};
// 初始加载
fetchStats();
fetchErrorTrend();
fetchPerformanceTrend();
fetchErrorTypes();
// 定时刷新
const interval = setInterval(() => {
fetchStats();
fetchErrorTrend();
fetchPerformanceTrend();
fetchErrorTypes();
}, 30000); // 30秒刷新一次
return () => clearInterval(interval);
}, []);
// 错误趋势图配置
const errorTrendConfig = {
data: {
labels: errorTrend.map(item => item.hour),
datasets: [
{
label: '错误数量',
data: errorTrend.map(item => item.count),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.1
}
]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '24小时错误趋势'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
};
// 性能趋势图配置
const performanceTrendConfig = {
data: {
labels: performanceTrend.map(item => item.hour),
datasets: [
{
label: '平均响应时间(ms)',
data: performanceTrend.map(item => item.avgDuration),
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}
]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '24小时性能趋势'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
};
// 错误类型分布图配置
const errorTypesConfig = {
data: {
labels: errorTypes.map(item => item.type),
datasets: [
{
data: errorTypes.map(item => item.count),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF'
]
}
]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '错误类型分布'
}
}
}
};
return (
<div className="monitoring-dashboard">
<h1>前端监控Dashboard</h1>
{/* 实时统计卡片 */}
<div className="stats-cards">
<div className="stat-card">
<h3>当前小时错误数</h3>
<div className="stat-value">{stats.errorCount}</div>
</div>
<div className="stat-card">
<h3>性能事件数</h3>
<div className="stat-value">{stats.performanceCount}</div>
</div>
<div className="stat-card">
<h3>平均响应时间</h3>
<div className="stat-value">{Math.round(stats.avgPerformance)}ms</div>
</div>
<div className="stat-card">
<h3>活跃用户数</h3>
<div className="stat-value">{stats.activeUsers}</div>
</div>
</div>
{/* 图表区域 */}
<div className="charts-container">
<div className="chart-item">
<Line {...errorTrendConfig} />
</div>
<div className="chart-item">
<Bar {...performanceTrendConfig} />
</div>
<div className="chart-item">
<Doughnut {...errorTypesConfig} />
</div>
</div>
</div>
);
};
export default MonitoringDashboard;
告警系统实现
// 告警系统服务
class AlertSystem {
constructor(options) {
this.rules = options.rules || [];
this.channels = options.channels || {};
this.alertHistory = new Map();
}
// 添加告警规则
addRule(rule) {
this.rules.push({
id: rule.id,
name: rule.name,
condition: rule.condition, // 告警条件函数
threshold: rule.threshold,
duration: rule.duration || 300000, // 5分钟
channels: rule.channels || ['email'],
cooldown: rule.cooldown || 1800000, // 30分钟冷却期
enabled: rule.enabled !== false
});
}
// 检查告警条件
async checkAlerts(data) {
for (const rule of this.rules) {
if (!rule.enabled) continue;
try {
const shouldAlert = await rule.condition(data);
if (shouldAlert) {
await this.handleAlert(rule, data);
}
} catch (error) {
console.error(`告警规则 ${rule.name} 检查失败:`, error);
}
}
}
// 处理告警
async handleAlert(rule, data) {
const now = Date.now();
const lastAlert = this.alertHistory.get(rule.id);
// 检查冷却期
if (lastAlert && (now - lastAlert.timestamp) < rule.cooldown) {
return;
}
// 记录告警
this.alertHistory.set(rule.id, {
timestamp: now,
data: data
});
// 发送告警通知
await this.sendAlert(rule, data);
}
// 发送告警通知
async sendAlert(rule, data) {
const alertMessage = {
title: `告警: ${rule.name}`,
content: this.formatAlertMessage(rule, data),
timestamp: new Date().toISOString(),
severity: this.getSeverity(rule, data)
};
// 通过各个渠道发送告警
for (const channel of rule.channels) {
try {
await this.sendToChannel(channel, alertMessage);
} catch (error) {
console.error(`发送告警到 ${channel} 失败:`, error);
}
}
}
// 格式化告警消息
formatAlertMessage(rule, data) {
return `
告警规则: ${rule.name}
触发时间: ${new Date().toLocaleString()}
当前值: ${JSON.stringify(data, null, 2)}
阈值: ${rule.threshold}
`.trim();
}
// 获取告警严重程度
getSeverity(rule, data) {
// 根据规则和数据确定严重程度
if (rule.name.includes('错误率') && data.value > 0.1) {
return 'critical';
} else if (rule.name.includes('响应时间') && data.value > 5000) {
return 'high';
}
return 'medium';
}
// 发送到指定渠道
async sendToChannel(channel, message) {
const handler = this.channels[channel];
if (handler) {
await handler(message);
} else {
console.warn(`未找到告警渠道: ${channel}`);
}
}
}
// 告警渠道实现
const alertChannels = {
// 邮件通知
email: async (message) => {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransporter({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: {
user: 'alerts@example.com',
pass: 'password'
}
});
await transporter.sendMail({
from: 'alerts@example.com',
to: 'admin@example.com',
subject: message.title,
text: message.content
});
},
// 企业微信通知
wechat: async (message) => {
const axios = require('axios');
await axios.post('https://qyapi.weixin.qq.com/cgi-bin/webhook/send', {
msgtype: 'text',
text: {
content: `${message.title}\n${message.content}`
}
}, {
params: {
key: 'your-webhook-key'
}
});
},
// 短信通知
sms: async (message) => {
// 集成短信服务商API
console.log('发送短信告警:', message.title);
}
};
// 使用示例
const alertSystem = new AlertSystem({
channels: alertChannels
});
// 添加错误率告警规则
alertSystem.addRule({
id: 'error_rate_high',
name: '错误率过高',
condition: async (data) => {
// 计算最近5分钟的错误率
const errorRate = await calculateErrorRate(5);
return errorRate > 0.05; // 错误率超过5%
},
threshold: 0.05,
channels: ['email', 'wechat'],
cooldown: 1800000 // 30分钟
});
// 添加响应时间告警规则
alertSystem.addRule({
id: 'response_time_high',
name: '响应时间过长',
condition: async (data) => {
const avgResponseTime = await calculateAvgResponseTime(5);
return avgResponseTime > 3000; // 响应时间超过3秒
},
threshold: 3000,
channels: ['email'],
cooldown: 900000 // 15分钟
});
// 定期检查告警
setInterval(async () => {
await alertSystem.checkAlerts();
}, 60000); // 每分钟检查一次
部署指南
1. 环境准备
# 安装Node.js依赖
npm install express cors mongodb redis nodemailer axios
# 安装前端依赖
npm install react chart.js react-chartjs-2
# 启动MongoDB
mongod --dbpath /data/db
# 启动Redis
redis-server
2. 配置文件
// config/monitoring.js
module.exports = {
// 数据库配置
mongodb: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
dbName: 'monitoring'
},
// Redis配置
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
},
// 告警配置
alerts: {
email: {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
},
wechat: {
webhookKey: process.env.WECHAT_WEBHOOK_KEY
}
},
// 数据保留策略
retention: {
errors: 30, // 错误数据保留30天
performance: 7, // 性能数据保留7天
userBehavior: 14 // 用户行为数据保留14天
}
};
3. Docker部署
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
monitoring-api:
build: .
ports:
- "3000:3000"
environment:
- MONGODB_URL=mongodb://mongo:27017
- REDIS_HOST=redis
depends_on:
- mongo
- redis
mongo:
image: mongo:5.0
volumes:
- mongo_data:/data/db
ports:
- "27017:27017"
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
volumes:
mongo_data:
最佳实践
1. 性能优化
- 数据采样:对于高频事件(如滚动、鼠标移动),实施采样策略减少数据量
- 批量处理:使用批量上报和批量写入数据库,减少网络请求和数据库操作
- 索引优化:为常用查询字段创建合适的数据库索引
- 缓存策略:使用Redis缓存热点数据和统计结果
2. 数据安全
- 敏感信息过滤:在客户端和服务端都要过滤敏感信息
- 数据加密:传输过程中使用HTTPS,存储敏感数据时进行加密
- 访问控制:实施适当的身份验证和授权机制
- 数据脱敏:在非生产环境中使用脱敏数据
3. 监控系统监控
// 监控系统自身的健康检查
class SystemHealthCheck {
constructor() {
this.metrics = {
dataReceiveRate: 0,
errorRate: 0,
responseTime: 0,
storageUsage: 0
};
}
// 检查系统健康状态
async checkHealth() {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
checks: {}
};
// 检查数据库连接
try {
await db.admin().ping();
health.checks.database = 'healthy';
} catch (error) {
health.checks.database = 'unhealthy';
health.status = 'unhealthy';
}
// 检查Redis连接
try {
await redisClient.ping();
health.checks.redis = 'healthy';
} catch (error) {
health.checks.redis = 'unhealthy';
health.status = 'unhealthy';
}
// 检查磁盘空间
const diskUsage = await this.checkDiskUsage();
health.checks.disk = diskUsage < 0.9 ? 'healthy' : 'warning';
return health;
}
async checkDiskUsage() {
// 实现磁盘使用率检查
return 0.7; // 示例返回70%使用率
}
}
4. 扩展性考虑
- 微服务架构:将数据采集、处理、存储、展示分离为独立服务
- 负载均衡:使用负载均衡器分发请求到多个服务实例
- 数据分片:对大量数据进行分片存储,提高查询性能
- 消息队列:使用消息队列处理高并发数据写入
通过以上完整的自定义前端监控系统实现方案,我们可以构建一个功能完善、性能优良、可扩展的监控系统。这个系统不仅能够满足基本的错误监控和性能监控需求,还提供了用户行为分析、实时告警、可视化展示等高级功能,为前端应用的稳定运行提供了强有力的保障。
完整使用示例
1. 前端项目集成
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>我的应用</title>
<script src="./monitoring-sdk.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// 初始化监控系统
const monitor = new MonitoringSDK({
apiUrl: 'https://monitoring.example.com/api',
projectId: 'my-project-123',
userId: 'user-456',
environment: 'production',
enableUserBehavior: true,
enablePerformance: true,
sampleRate: 0.1 // 10%采样率
});
// 启动监控
monitor.init();
// 手动上报自定义事件
monitor.track('button_click', {
buttonId: 'submit-btn',
page: '/checkout',
timestamp: Date.now()
});
// 手动上报业务错误
try {
// 业务逻辑
processPayment();
} catch (error) {
monitor.reportError(error, {
context: 'payment_processing',
userId: getCurrentUserId(),
orderId: getCurrentOrderId()
});
}
</script>
</body>
</html>
2. React项目集成
// App.jsx
import React, { useEffect } from 'react';
import { MonitoringProvider, useMonitoring } from './monitoring/react-integration';
function App() {
return (
<MonitoringProvider config={{
apiUrl: process.env.REACT_APP_MONITORING_URL,
projectId: process.env.REACT_APP_PROJECT_ID,
environment: process.env.NODE_ENV
}}>
<MainApp />
</MonitoringProvider>
);
}
function MainApp() {
const { track, reportError } = useMonitoring();
const handleSubmit = async (formData) => {
try {
track('form_submit', { formType: 'contact' });
await submitForm(formData);
track('form_submit_success', { formType: 'contact' });
} catch (error) {
reportError(error, { context: 'form_submission' });
}
};
return (
<div>
<form onSubmit={handleSubmit}>
{/* 表单内容 */}
</form>
</div>
);
}
3. Vue项目集成
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { MonitoringPlugin } from './monitoring/vue-plugin';
const app = createApp(App);
app.use(MonitoringPlugin, {
apiUrl: process.env.VUE_APP_MONITORING_URL,
projectId: process.env.VUE_APP_PROJECT_ID,
environment: process.env.NODE_ENV
});
app.mount('#app');
<!-- Component.vue -->
<template>
<button @click="handleClick">点击我</button>
</template>
<script>
export default {
methods: {
handleClick() {
// 自动追踪点击事件
this.$monitoring.track('button_click', {
component: 'MyComponent',
action: 'primary_action'
});
}
},
async created() {
try {
await this.loadData();
} catch (error) {
this.$monitoring.reportError(error, {
context: 'component_initialization'
});
}
}
};
</script>
4. 监控Dashboard使用
// dashboard/main.js
import React from 'react';
import ReactDOM from 'react-dom';
import Dashboard from './Dashboard';
ReactDOM.render(<Dashboard />, document.getElementById('root'));
// Dashboard.jsx
import React, { useState, useEffect } from 'react';
import { ErrorChart, PerformanceChart, UserBehaviorChart } from './components';
function Dashboard() {
const [timeRange, setTimeRange] = useState('24h');
const [data, setData] = useState({});
useEffect(() => {
fetchDashboardData(timeRange).then(setData);
}, [timeRange]);
return (
<div className="dashboard">
<header>
<h1>前端监控Dashboard</h1>
<select value={timeRange} onChange={(e) => setTimeRange(e.target.value)}>
<option value="1h">最近1小时</option>
<option value="24h">最近24小时</option>
<option value="7d">最近7天</option>
<option value="30d">最近30天</option>
</select>
</header>
<div className="metrics-grid">
<div className="metric-card">
<h3>错误率</h3>
<div className="metric-value">{data.errorRate}%</div>
</div>
<div className="metric-card">
<h3>页面加载时间</h3>
<div className="metric-value">{data.avgLoadTime}ms</div>
</div>
<div className="metric-card">
<h3>活跃用户</h3>
<div className="metric-value">{data.activeUsers}</div>
</div>
</div>
<div className="charts-grid">
<ErrorChart data={data.errors} />
<PerformanceChart data={data.performance} />
<UserBehaviorChart data={data.userBehavior} />
</div>
</div>
);
}
5. 告警配置示例
// alerts/config.js
export const alertRules = [
{
name: '错误率过高',
condition: 'error_rate > 5%',
timeWindow: '5m',
channels: ['email', 'wechat'],
severity: 'high'
},
{
name: '页面加载缓慢',
condition: 'avg_load_time > 3000ms',
timeWindow: '10m',
channels: ['email'],
severity: 'medium'
},
{
name: '用户流失异常',
condition: 'bounce_rate > 80%',
timeWindow: '30m',
channels: ['email', 'sms'],
severity: 'high'
}
];
这个完整的自定义前端监控系统实现提供了从数据采集到可视化展示的全链路解决方案,具有以下特点:
- 轻量级SDK:客户端SDK体积小,对应用性能影响最小
- 实时监控:支持实时数据采集和告警
- 多维度分析:涵盖错误、性能、用户行为等多个维度
- 可扩展架构:支持水平扩展和功能扩展
- 完善的告警机制:多渠道告警,确保问题及时发现
- 易于集成:提供多种前端框架的集成方案
监控系统方案对比
功能对比
功能 | Sentry | WebTracing |
---|---|---|
错误监控 | ✅ 完善 | ✅ 可定制 |
性能监控 | ✅ 内置 | ✅ 可扩展 |
用户行为追踪 | ⚠️ 有限 | ✅ 完全支持 |
分布式追踪 | ⚠️ 有限 | ✅ 原生支持 |
SourceMap 支持 | ✅ 内置 | ⚠️ 需配置 |
告警系统 | ✅ 完善 | ⚠️ 需集成 |
多框架支持 | ✅ 内置 | ✅ 通过插件 |
自定义指标 | ⚠️ 有限 | ✅ 完全支持 |
数据导出 | ⚠️ 有限 | ✅ 多种选择 |
私有化部署 | ✅ 支持 | ✅ 支持 |
性能对比
性能指标 | Sentry | WebTracing |
---|---|---|
SDK 体积 | 中等 (~20KB gzip) | 较大 (~30KB gzip) |
浏览器性能影响 | 低 | 中等 |
数据传输量 | 中等 | 可配置 |
后端资源消耗 | 高 | 中等 |
可扩展性 | 中等 | 高 |
成本对比
成本因素 | Sentry | WebTracing |
---|---|---|
初始搭建成本 | 低 | 高 |
维护成本 | 低 | 高 |
云服务成本 | 按事件计费 | 按资源使用计费 |
私有化部署成本 | 中等 | 高 |
定制开发成本 | 高 | 中等 |
适用场景
Sentry 适用场景:
- 快速搭建监控系统,无需过多定制
- 小型团队,人力资源有限
- 主要关注错误监控和基本性能指标
- 需要完善的告警和通知机制
- 预算有限,希望使用成熟方案
WebTracing 适用场景:
- 需要深度定制监控系统
- 大型应用,需要全链路追踪能力
- 已有 OpenTelemetry 生态系统
- 对数据隐私和安全有严格要求
- 需要与后端监控系统无缝集成
监控系统落地案例
大型电商平台监控实践
背景:某电商平台日活用户 100 万+,前端应用包含商城、后台管理、供应商平台等多个子系统。
方案选择:采用 WebTracing + Sentry 混合方案
实施步骤:
基础设施搭建
- 部署 Sentry 私有化服务,用于错误监控
- 部署 OpenTelemetry Collector 集群
- 配置 Elasticsearch + Jaeger + Prometheus 存储
- 搭建 Grafana 可视化平台
监控接入
- 核心交易链路:使用 WebTracing 实现全链路追踪
- 非核心页面:使用 Sentry 实现基础错误监控
- 自定义业务指标:转化率、下单率、支付成功率等
告警配置
- P0 级别:核心交易链路错误率 > 1%,立即告警
- P1 级别:页面加载时间 P95 > 3s,或错误率 > 3%
- P2 级别:非核心功能错误率 > 5%
效果评估
- 问题发现时间:从平均 2 小时缩短至 5 分钟
- 用户体验:页面加载时间平均减少 30%
- 业务指标:转化率提升 15%,用户满意度提升 20%
中小型 SaaS 应用监控实践
背景:某 SaaS 企业协作平台,用户规模 5 万+,前端技术栈为 Vue + Element UI。
方案选择:采用 Sentry 云服务
实施步骤:
初始配置
- 注册 Sentry 云服务账号
- 集成 Sentry SDK 到 Vue 应用
- 配置 SourceMap 上传
监控范围
- 错误监控:JS 异常、API 请求异常
- 性能监控:页面加载性能、API 请求性能
- 用户信息关联:用户 ID、企业 ID、会员等级
告警配置
- 工作时间:错误率 > 2%,Slack 通知
- 非工作时间:错误率 > 5%,邮件通知
- 核心功能:任何错误,立即通知
效果评估
- 问题定位时间:从平均 1 天缩短至 1 小时
- 开发效率:修复 bug 时间减少 40%
- 用户反馈:负面反馈减少 30%
总结与展望
前端监控系统已经成为现代 Web 应用不可或缺的基础设施。通过本文介绍的 Sentry 和 WebTracing 两种方案,开发团队可以根据自身需求和资源情况选择合适的监控解决方案。
关键收益:
- 提升用户体验:通过实时监控和性能优化,提供更流畅的用户体验
- 降低运维成本:主动发现并解决问题,减少人工排查时间
- 辅助业务决策:基于用户行为数据,优化产品功能和流程
- 提高开发效率:快速定位问题,减少修复时间
未来趋势:
- AI 辅助分析:利用机器学习自动分析异常模式和根因
- 预测性监控:基于历史数据预测可能出现的问题
- 边缘计算:在用户浏览器端进行初步数据处理和分析
- 隐私保护增强:更智能的数据脱敏和合规性保障
- 跨平台监控:Web、移动端、小程序等多平台统一监控