前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现一套前端应用监控系统

实现一套前端应用监控系统

作者头像
木子星兮
发布2020-07-16 19:59:47
7110
发布2020-07-16 19:59:47
举报

前端应用监控系统包含四部分

  1. 数据收集 SDK
  2. 数据上报网关
  3. 数据存储数据库
  4. 数据展示平台

数据收集 SDK

收集性能数据

我们通过浏览器的 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 资源版本)
    };
}

上报数据

上报数据有两种方式, sendBeaconimg.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 了

结语

文章写得比较简单, 没有具体说一些细节, 如果大家很感兴趣的话, 我可以提供一套可以直接跑的源码和 docker 镜像

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 牧码的星星 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数据收集 SDK
    • 收集性能数据
      • 收集访问数据
      • 数据上报网关
      • 数据存储数据库
      • 数据展示平台
      • 结语
      相关产品与服务
      数据库
      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档