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

2,490 阅读4分钟

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

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

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

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

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

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

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

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);
} 

那么onreadystatechange事件如何处理呢

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

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

 XMLHttpRequest.prototype.send = function (data) {
                // 添加 readystatechange 事件
                this.addEventListener('readystatechange', function () {
                   // 对请求结果做响应的处理
                });

                xhr_send.call(this, data);
            };

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

① 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);
            };

上面代码中,我们在当前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);
            };

上述response header信息,可以通过

xml.getAllResponseHeaders()

获取, 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
}

fetch

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

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

// 首先保存原先的fetch 引用
let origFetch = window.fetch
window.fetch =function(fn, t) {
                   
                    // 这边执行我们的数据收集工作

                    
                    return origFetch.apply(this, args)
                };

获取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;
                    });
                };

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

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

1. 请求数据反映了用户轨迹的一部分

2. 请求信息可以帮助我们定位问题

3. 通过分析请求的成功率,及时发现服务端问题

第三, 注意点

  1. 安全, 我们应该对涉及用户隐私的数据进行脱敏传输 或 不传输

  2. 对于response的数据, 我们应该有需要的截取, 而不是一股脑的传输,防止无效数据太多

以上内容只是对浏览器请求数据收集和使用做了个大概的分享,除了浏览器外, 还有一些,如小程序的请求获取,会在小程序信息收集中分享

阅读原文