hugh 的个人博客

vuePress-theme-reco hugh 的个人博客    2021
hugh 的个人博客

Choose mode

  • dark
  • auto
  • light
Home
分类
  • 前端
  • fe-robot
  • 前端监控
标签
专题
  • femonitor
  • jsby
  • fe-robot
TimeLine
工具
全版

hugh 的个人博客

154

Article

324

Tag

Home
分类
  • 前端
  • fe-robot
  • 前端监控
标签
专题
  • femonitor
  • jsby
  • fe-robot
TimeLine
工具
全版
  • 数据收集篇

    • 收集请求数据集详情-前端监控之数据采集篇
      • 第一,我们先要清楚,对于请求,我们通常需要哪些信息
      • 第二,对于这些数据,我们又用来做些什么
      • 第三, 注意点
    • 收集错误信息及堆栈-前端监控之数据收集篇
      • 1. 了解异常发生的情况和影响
      • 2. 了解 js 中异常抛出的内容
      • 3. 收集跨域异常信息
      • 4. 错误捕获上报
      • 5. 框架类异常收集
    • 收集资源加载错误
      • 资源类型
      • 如何监控
    • 前端日志打印收集-前端监控之数据收集篇
      • 问题主要分为以下几种
      • 什么是前端日志
      • 如何记录前端日志
    • 如何保证页面卸载时的上报数据完整性
      • 1. 使用异步 xhr
      • 2. 异步不好使,试试同步 xhr
      • 3. 杀手锏 sendBeacon
      • 4. 如果不追求时效性, 自然是使用缓存 + 延时发送的策略最优雅, 同时丢失数据的问题也可以大大改善
    • CPS报告收集-前端监控
      • csp
      • CSP 报告
      • 使用
  • 数据分析篇

  • 数据监控篇

  • 采坑篇

收集请求数据集详情-前端监控之数据采集篇

vuePress-theme-reco hugh 的个人博客    2021

收集请求数据集详情-前端监控之数据采集篇


hugh 的个人博客 2019-07-27 16:37:52 前端webfe_monitor监控

前端监控中的一项重要数据,就是请求数据

# 第一,我们先要清楚,对于请求,我们通常需要哪些信息

下图描述了我们对于请求数据通常有什么需要

对于浏览器而言, 主要有 xmlHttpRequest 和 fetch 两个请求 API(ws 暂未考虑),

那么我们如何才能做到对业务代码无侵入式的数据收集呢?

方法很明确, 就是重新原生的 API,在关键方法上进行做一层代理

下面, 我们就从这两个 API 的角度, 逐步分析如何实现

# a. xmlHttpRequest

上图可以发现, 主要方法就是 open 和 send, 主要事件为 onreadystatechange, 我们只需要对这三个属性进行包装,就可以轻松获取 xhr 相关的消息

一般封装的方法如下

// 首先将需要封装对象的原属性保存一个副本,用于代理之后调用
let xhr_open = XMLHttpRequest.prototype.open;
let xhr_send = XMLHttpRequest.prototype.send;
// 第二步,将原对象属性替换成代理对象 
XMLHttpRequest.prototype.open = function (...args) {
                // 在这里,我们加入自己的获取逻辑
                xhr_open.apply(this, args);
            };
 XMLHttpRequest.prototype.send = function (data) {
                // 在这里,我们加入自己的获取逻辑
                xhr_send.apply(this, args);
} 
1
2
3
4
5
6
7
8
9
10
11
12

那么 onreadystatechange 事件如何处理呢

注意点: 该事件的监听需要在 send 方法发送之前

我们可以在封装 send 方法时,加入该事件的监听

  
XMLHttpRequest.prototype.send = function (data) {
                // 添加 readystatechange 事件
                this.addEventListener('readystatechange', function () {
                   // 对请求结果做响应的处理
                });
            xhr_send.call(this, data);
        };
1
2
3
4
5
6
7
8

封装方法已经完成了, 那我们该如何获取到第一张图 描述的信息呢?

# ① request 信息

open 方法的参数列表是固定的, 依次是 method,url, async, username, password

在 open 代理过程中,获取即可

XMLHttpRequest.prototype.open = function (...args) {
this.__monitor_xhr = {
method: args[0],
url: args[1]
}
xhr_open.apply(this, args);
};
1
2
3
4
5
6
7

上面代码中,我们在当前 xhr 对象上写入了一个新的属性,用于保存我们获取到的信息。 请求 body 的数据,我们在下面的分析中获取

# ② reponse 和 timeline 信息

这时候,我们就需要对请求结果进行处理,获取我们想要的数据

XMLHttpRequest.prototype.send = function (data) {
// 记录请求开始时间,用于计算耗时
const startTime = new Date();

            // 添加 readystatechange 事件
            this.addEventListener('readystatechange', function () {
           
                    try {
                        if (this.readyState === XMLHttpRequest.DONE) {
                            // 请求结束时间
                            const endTime = new Date();
                            // 请求耗时
                            this.__monitor.duration = (endTime - startTime) ;
                           // 请求body
                           this.__monitor.req_body = data;
                           // 获取response header、body等信息

                        }
                    } catch (err) {
                     
                    }
                }
            });

           xhr_send.call(this, data);
        };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

上述 response header 信息,可以通过

xml.getAllResponseHeaders()
1

获取, body 信息可以通过以下方法获取

 function getBody (xhrObject) {
	var body = void 0;


	// IE 11 sometimes throws when trying to access a large responses:
	// https://connect.microsoft.com/IE/Feedback/Details/1053110
	// gte IE10 will support responseType
	try {
	    switch (xhrObject.responseType) {
	        case 'json':
	        case 'arraybuffer':
	        case 'blob':
	        {
	            body = xhrObject.response;
	            break;
	        }
	        case 'document':
	        {
	            body = xhrObject.responseXML;
	            break;
	        }
	        case 'text':
	        case '':
	        {
	            body = xhrObject.responseText;
	            break;
	        }
	        default:
	        {
	            body = '';
	        }
	    }
	    // When requesting binary data, IE6-9 will throw an exception
	    // on any attempt to access responseText (#11426)
	    if (!body && typeof xhrObject.responseText === "string" ) {
	        body = xhrObject.responseText;
	    }
	} catch (err) {
	    body = 'monitor: Error accessing response.';
	}
	return body
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# b. fetch

fetch 由于 API 和 xhr 有很大差异, fetch 返回了 promise 对象, 这种情况的封装

首先,我们还是需要对 fetch 加个代理, 方式类似 xhr

/ 首先保存原先的fetch 引用
let origFetch = window.fetch
window.fetch =function(fn, t) {
                // 这边执行我们的数据收集工作
                return origFetch.apply(this, args)
 };
1
2
3
4
5
6

# 获取 request 和 response 信息

window.fetch =function(fn, t) {
                var args = new Array(arguments.length);
                for (var i = 0; i < args.length; ++i) {
                    args[i] = arguments[i];
                }

                var p = null;
                 // 由于fetch的参数列表更灵活, 所以需要对应的处理
                if (typeof Request !== 'undefined' && args[0] instanceof Request) {
                    p = args[0].clone().text().then(function (body) {
                        return utils.extendsObjects({}, pluckFetchFields(args[0]), {
                            body: body
                        });
                    });
                } else {
                    p = Promise.resolve(utils.extendsObjects({}, pluckFetchFields(args[1]), {
                        url: '' + args[0],
                        body: (args[1] || {}).body
                    }));
                }

                var fetchData = {
                    method: '',
                    url: '',
                    status_code: null,
                    start_time: new Date().getTime(),
                    request:{
                        headers: {},
                        body: ''
                    },
                    response:{
                        headers: {},
                        body: ''
                    },
                    timeline:{
                        dns:0,
                        connect:0,
                        response:0,
                        request: 0,
                        duration: 0
                    }
                };

                // 此处默认加一个then,对结果进行收集处理
                return origFetch.apply(this, args).then(function(response) {
                    fetchData.status_code = response.status;
                    fetchData.duration = new Date().getTime() - fetchData.start_time
                    fetchData.timeline.duration = fetchData.duration
                    p.then(function(req) {
                        fetchData.method = req.method
                        fetchData.url = req.url
                        utils.objectMerge(fetchData.request, {mode: req.mode, referrer: req.referrer, credentials: req.credentials, headers: req.headers, body: req.body})
                        var clonedText = null;
                        try {
                         
                            clonedText = response.clone().text();
                        } catch (err) {
                            // safari has a bug where cloning can fail
                            clonedText = Promise.resolve('Monitor fetch error: ' + err.message);
                        }
                        clonedText.then(function(body) {
                            fetchData.response.body = body
                            fetchData.response.headers = makeObjectFromHeaders(response.headers)

                            // 将数据发送到服务器
                            _reportToServer(fetchData)
                        })
                    })
                  
                    return response;
                });
            };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

以上内容,我们还缺少了一步,那就是第一张图中的 timeline 数据, 该部分将和性能数据一起分析

# 第二,对于这些数据,我们又用来做些什么

  1. 请求数据反映了用户轨迹的一部分
  2. 请求信息可以帮助我们定位问题
  3. 通过分析请求的成功率,及时发现服务端问题

# 第三, 注意点

  1. 安全, 我们应该对涉及用户隐私的数据进行脱敏传输 或 不传输
  2. 对于 response 的数据, 我们应该有需要的截取, 而不是一股脑的传输,防止无效数据太多