We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前端应用监控系统包含四部分
我们通过浏览器的 performance API 获取性能数据, 其中要用到以下数据
/** 性能数据字段 */ const fields = [ 'navigationStart', // 0 'unloadEventStart', 'unloadEventEnd', 'redirectStart', 'redirectEnd', 'fetchStart', // 5 'domainLookupStart', 'domainLookupEnd', 'connectStart', 'secureConnectionStart', 'connectEnd', // 10 'requestStart', 'responseStart', 'responseEnd', 'domLoading', 'domInteractive', // 15 'domContentLoadedEventStart', 'domContentLoadedEventEnd', 'domComplete', 'loadEventStart', 'loadEventEnd', // 20 ];
上面的数据是某事件触发的时刻, 下面我们定义要数据的性能指标, 这些指标都是由上述数据计算得出
/** * 性能数据指标 * key: 指标 * value: [字段1, 字段2] * result: key = fields[字段1] - fields[字段2] */ const targets: { [key: string]: [number, number] } = { firstbyte: [5, 12], // 首字节 domready: [5, 17], // DOM Ready load: [5, 19], // load 触发 dns: [6, 7], // DNS 查询 tcp: [8, 10], // TCP 连接 ssl: [9, 10], // HTTPS 连接 ttfb: [11, 12], // TTFB contentdownload: [12, 13], // HTML 下载 domparsing: [13, 15], // DOM 解析 total: [5, 20], // 总耗时 unload: [1, 2], // 上个页面 unload redirect: [3, 4], // 重定向时间 appcache: [5, 6], // 缓存查询 };
接下来开始计算每个指标的值, 其中白屏时间是单独写的计算方法(实际运行中发现很多时候收集到的数据是0), 还做了一些数据修正工作(抛弃负数、小数点保留三位)
/** * 获取性能信息 */ getPerformance() { /** timing api v1 */ const timing1 = window.performance.timing; /** timing api v2 */ let timing2: PerformanceEntry = {} as PerformanceEntry; // 优先使用 navigation v2 https://www.w3.org/TR/navigation-timing-2/ if (SupportNavigationV2) { try { var nt2Timing = performance.getEntriesByType('navigation')[0]; if (nt2Timing) { timing2 = nt2Timing; } } catch (err) {} } /** 合并 v1, v2 的数据 */ const timing: { [key: string]: number | string; } = {}; fields.forEach((field: string) => { // @ts-ignore timing[field] = timing2[field] || timing1[field]; }); /** 计算每个性能指标 */ const times: { [key: string]: number | string; } = {}; Object.keys(targets).forEach((key) => { const [fieldIndex1, fieldIndex2] = targets[key]; times[key] = <number>timing[fields[fieldIndex2]] - <number>timing[fields[fieldIndex1]]; // 修正负数指标 if (times[key] < 0) { times[key] = 0; } }); // 计算白屏时间 if (SupportNavigationV2) { const paintTimimg = performance.getEntriesByType('paint'); if (paintTimimg && paintTimimg.length > 0) { times.blank = paintTimimg[1] ? paintTimimg[1].startTime : paintTimimg[0].startTime; } } else if (window.chrome && window.chrome.loadTimes) { times.blank = window.chrome.loadTimes().firstPaintTime * 1000 - window.performance.timing.fetchStart; } // 修正负数时间 if (!times.blank || times.blank < 0) { times.blank = 0; } // 保留三位小数 Object.keys(times).forEach((key) => { if (typeof times[key] === 'number') { times[key] = ((times[key] as number).toFixed(3) as unknown) as number; } }); return times; }
网络类型经常会收集不到. id / userId / assertVer 由使用 SDK 的人在初始化 SDK 时传入, 还有用户可能刚进页面时没有登录态, 后面才登录的, 所以 userId 需要支持传入 function 来在上报信息时获取 userId (代码中未体现)
/** * 获取公共信息 */ getCommon() { const { innerWidth: width, innerHeight: height, location, document, navigator } = window; const { href: url } = location; const { title, referrer } = document; let net = ''; // @ts-ignore if (navigator.connection && navigator.connection.effectiveType) { // @ts-ignore net = navigator.connection.effectiveType; } // url, ua, ip 解析在服务端做 return { id: this.options.id, // 应用 id timestamp: Date.now(), // 上报时间 url, // 页面 url title, // 页面标题 referrer, // 页面 referrer ua: navigator.userAgent, // 用户 UA net, // 网络类型 width, // 屏幕宽度 height, // 屏幕高度 sdkVer: version, // SDK 版本 userId: '', // 用户 id assetsVer: '', // 页面版本(js 资源版本) }; }
上报数据有两种方式, sendBeacon 和 img.src
sendBeacon
img.src
sendBeacon 是 POST 请求, 数据通过 body 传输, 了解sendBeacon
img.src 是 GET 请求, 数据通过 url params 传输, string 数据需要编码
/** * 上报数据 * @param measurement influxDB Measurement * @param data 上报数据 */ log(measurement: Measurement, data: any) { // rollup 注入的值 // @ts-ignore const host = TRACKER_HOST; try { // throw Error('use img'); // 使用 sendBeacon 上报, 参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon data.sendMode = 'sendBeacon'; window.navigator.sendBeacon(`${host}/${measurement}`, JSON.stringify(data)); } catch { // 降级为 img.src 上报 data.sendMode = 'img'; const params = Object.keys(data) .map((field) => `${field}=${encodeURIComponent(data[field])}`) .join('&'); const img = document.createElement('img'); img.src = `${host}/${measurement}?${params}`; } }
代码中考虑了功能拓展, 第一个参数是上报数据类型(对应数据库表)
网关要做的事情是, 解析数据并存储到数据库中
async function parseCommonInfo(params: Object) { const result: { [key: string]: any } = Object.assign({}, params); const { protocol, host, pathname, query, hash } = url.parse(result.url); Object.assign(result, { protocol, page: host + pathname, query, hash, }); const parsedIpv4 = result.ip.match(/\d+.\d+.\d+.\d+/); result.ipv4 = parsedIpv4 ? parsedIpv4[0] : ''; // @ts-ignore const uaParseResult = uaParser(params.ua); const { browser, os, device } = uaParseResult; const mfwAppRegex = /mfwappver\/(\d+)\.\d+\.\d+/; // @ts-ignore const appParseResult = mfwAppRegex.exec(params.ua); if (appParseResult) { browser.name = 'Mfw APP'; browser.major = appParseResult[1]; } const osMajorRegex = /^\d+/; const osParseResult = osMajorRegex.exec(os.version); if (osParseResult) { os.version = osParseResult[0]; } Object.assign(result, { browser: browser.name + ' ' + browser.major, os: os.name + ' ' + os.version, device: device.vendor && device.model ? device.vendor + ' ' + device.model : '', }); let ipInfo = { country: '', region: '', county: '', city: '', isp: '', }; if (result.ipv4) { // 淘宝的服务是有限流的 const url = `https://ip.taobao.com/service/getIpInfo.php?ip=${result.ipv4}`; try { const { status, data } = await axios.get(url); if (status === 200 && data.code === 0 && data.data.isp !== '内网IP') { function getIpInfo(field: string) { return data.data[field] === 'XX' ? '' : data.data[field]; } ipInfo = { country: getIpInfo('country'), region: getIpInfo('region'), county: getIpInfo('county'), city: getIpInfo('city'), isp: getIpInfo('isp'), }; } } catch (err){ console.error('获取 ip 信息失败', url, err.message); } } Object.assign(result, ipInfo); return result; } router.get('/performance', async (ctx) => { const { query } = url.parse(ctx.request.url); const params = querystring.parse(query); params.ip = ctx.request.ip; const performance = await parseCommonInfo(params); await logPerformance(performance); ctx.status = 204; }); router.post('/performance', async (ctx) => { const params = ctx.request.body; params.ip = ctx.request.ip; const performance = await parseCommonInfo(params); await logPerformance(performance); ctx.status = 204; });
因为存的是日志数据, 适合使用时序数据库, 经过简单对比(star数量, 相关文章, 口碑)后选择了 influxDB
中文文档 node API
使用起来没什么难度, 和其它 SQL 数据库差不多
有了数据之后, 需要将其展示出来, 我没有自己去实现页面, 直接选择了使用 Grafana
Grafana 是一款开源的可视化仪表盘, 支持通过配置或者写 SQL 的方式直接生成展示图表, 并且配置报警项
文档 中文文档 配置文档 内置图表使用文档
启动 Grafana 后, 打开 web 端登录管理员账号, 先配置 influxDB 数据源, 然后创建图表配置数据查询就 ok 了
文章写得比较简单, 没有具体说一些细节, 如果大家很感兴趣的话, 可以直接看源码 https://github.com/yinxin630/fe-tracker, 还可以按照 README 中的步骤自己跑一下看看
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前端应用监控系统包含四部分
数据收集 SDK
收集性能数据
我们通过浏览器的 performance API 获取性能数据, 其中要用到以下数据
上面的数据是某事件触发的时刻, 下面我们定义要数据的性能指标, 这些指标都是由上述数据计算得出
接下来开始计算每个指标的值, 其中白屏时间是单独写的计算方法(实际运行中发现很多时候收集到的数据是0), 还做了一些数据修正工作(抛弃负数、小数点保留三位)
收集访问数据
网络类型经常会收集不到. id / userId / assertVer 由使用 SDK 的人在初始化 SDK 时传入, 还有用户可能刚进页面时没有登录态, 后面才登录的, 所以 userId 需要支持传入 function 来在上报信息时获取 userId (代码中未体现)
上报数据
上报数据有两种方式,
sendBeacon
和img.src
sendBeacon 是 POST 请求, 数据通过 body 传输, 了解sendBeacon
img.src 是 GET 请求, 数据通过 url params 传输, string 数据需要编码
代码中考虑了功能拓展, 第一个参数是上报数据类型(对应数据库表)
数据上报网关
网关要做的事情是, 解析数据并存储到数据库中
数据存储数据库
因为存的是日志数据, 适合使用时序数据库, 经过简单对比(star数量, 相关文章, 口碑)后选择了 influxDB
中文文档
node API
使用起来没什么难度, 和其它 SQL 数据库差不多
数据展示平台
有了数据之后, 需要将其展示出来, 我没有自己去实现页面, 直接选择了使用 Grafana
Grafana 是一款开源的可视化仪表盘, 支持通过配置或者写 SQL 的方式直接生成展示图表, 并且配置报警项
文档
中文文档
配置文档
内置图表使用文档
启动 Grafana 后, 打开 web 端登录管理员账号, 先配置 influxDB 数据源, 然后创建图表配置数据查询就 ok 了
结语
文章写得比较简单, 没有具体说一些细节, 如果大家很感兴趣的话, 可以直接看源码 https://github.com/yinxin630/fe-tracker, 还可以按照 README 中的步骤自己跑一下看看
The text was updated successfully, but these errors were encountered: