干货满满!如何做好前端日志和异常监控

在研发过程中,日志是非常重要的一环,它可以帮助我们快速定位问题,解决问题。在前端开发中,日志也是非常重要的一环,它可以帮助我们快速定位问题,解决问题。本文将介绍前端日志的规范和最佳实践。但是我们经常看到一些项目日志打得满天飞,但是到了真正定位问题的时候,发现日志并没有什么卵用。这是因为日志打得不规范,不规范的日志是没有意义的。所以我们需要规范日志的打印,才能让日志发挥最大的作用。

那么,我们首先就要思考一下,打印什么样的日志才是有助于定位前端问题的,我想,可以从我们真是定位用户反馈问题的场景来思考。

通常,前端用户反馈的问题大概有以下几种:

  1. 页面加载慢
  2. 页面渲染错乱
  3. 页面白屏等交互异常
  4. 页面崩溃
  5. 页面卡顿

下面,我们可以基于这些场景,来思考一下,我们应该打印什么样的日志,才能帮助我们快速定位问题。

业务异常日志

页面加载慢

对于页面加载慢,这个问题,我们可以通过performance对象来获取页面加载的性能数据,然后打印出来,比如:

javascript 复制代码
window.addEventListener('load', function() {
    setTimeout(function() {
        var timing = window.performance.timing;
        var loadTime = timing.loadEventEnd - timing.navigationStart;
        var pageUrl = window.location.href;
        console.log('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');
    }, 0);
});

这样,我们就可以在页面加载完成之后,打印出页面加载时间,这样我们就可以通过日志来定位页面加载慢的问题。

通常,日志里面可以穿插一些告警,比如,我们这里可以加上一个判断,如果页面加载时间超过了3秒,我们就打印一个告警,比如:

javascript 复制代码
window.addEventListener('load', function() {
    setTimeout(function() {
        var timing = window.performance.timing;
        var loadTime = timing.loadEventEnd - timing.navigationStart;
        var pageUrl = window.location.href;
        console.log('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');
        if (loadTime > 3000) {
            console.warn('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds, it is too slow.');
            //todo 上报到监控系统
            reportToMonitor('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds, it is too slow.');
        }
    }, 0);
});

页面渲染错乱

对于页面渲染错乱,造成这个问题的原因有很多,比如网络问题、代码问题、浏览器兼容问题等等,这个问题比较复杂,我们可以通过一些手段来定位这个问题,比如:

这个问题,我们可以通过window.onerror来做,从里面区出渲染错误的问题,比如:

javascript 复制代码
window.onerror = function(message, source, lineno, colno, error) {
    console.error('Error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);

    //分析渲染错误
    parseAndLogRenderError(message, source, lineno, colno, error);
};

// 举例:分析渲染错误
function parseAndLogRenderError(message, source, lineno, colno, error) {
    if (message.indexOf('render error') > -1) {
        console.error('Render error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);
        //todo 上报到监控系统
        reportToMonitor('Render error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);
    }
}

当然,window.onerror 只能捕获到一部分错误,我们还需要结合 window.addEventListener('error', function(event) {}) 来捕获一些资源加载错误,比如:

javascript 复制代码
window.addEventListener('error', function(event) {
    console.error('Error: ' + event.message + ' Script: ' + event.filename + ' Line: ' + event.lineno + ' Column: ' + event.colno + ' StackTrace: ' + event error.stack);

    parseAndLogRenderError(event.message, event.filename, event.lineno, event.colno, event.error);
});

页面交互异常

这里的页面交互异常,通常是指用户在页面上进行一些操作的时候,出现了一些异常,比如点击按钮无反应、输入框无法输入等等,这个问题,我们可以通过一些手段来定位。

先说一说点击按钮无反应的问题。

假如说,我们有一个这样的场景,用户点击一个按钮,理论上点击按钮会发送一个请求,成功失败可能都会有一个界面上的反馈,但是如何点击之后,界面没有任何的反馈,这个时候就,我们基本上可以判定,这种时候就是页面交互异常了。那么,我们该如何捕捉这种异常呢?

我们可以通过window.addEventListener('click', function(event) {})来捕捉用户的点击事件,然后在里面做一些判断,比如:

javascript 复制代码
// 设置一个标志位,当按钮被点击时,将标志位设置为 true。
var isButtonClicked = false;

window.addEventListener('click', function(event) {
    var target = event.target;
    if (target.tagName === 'BUTTON') {
        console.log('User clicked button: ' + target.innerText);
        // 设置标志位
        isButtonClicked = true;
    }
});

// 创建一个 observer 实例
var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        // 如果页面有变化并且按钮被点击过,重置标志位
        // 这里的mutations 可能需要根据实际情况来判断,具体要结合自己业务逻辑来判断
        if(mutation.type === 'childList' || mutation.type === 'attributes') {
            if (isButtonClicked) {
                console.log('Page change detected after button click.');
                isButtonClicked = false;
            }
        }
    });
});

// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true, subtree: true };

// 传入目标节点和观察选项
observer.observe(document.body, config);

// 启动定时器
setInterval(function() {
    if (isButtonClicked) {
        console.log('Button click exception detected.');
        // 上报到监控系统
        reportToMonitor('Button click exception detected');
        // 重置标志位
        isButtonClicked = false;
    }
}, 5000);  // 5秒后检查标志位

页面白屏的问题

页面白屏的问题,通常是指用户打开页面之后,页面长时间没有任何的反应,这个问题,我们可以通过一些手段来定位。

我们可以通过window.addEventListener('DOMContentLoaded', function() {})来捕捉页面的加载事件,然后在里面做一些判断,比如:

javascript 复制代码
window.addEventListener('DOMContentLoaded', function() {
    setTimeout(function() {
        if (document.body.innerHTML === '') {
            console.error('Page is blank.');
            // 上报到监控系统
            reportToMonitor('Page is blank.');
        }
    }, 3000);  // 3秒后检查页面是否为空
});

或者,我们可以通过window.addEventListener('load', function() {})来捕捉页面的加载事件,然后在里面做一些判断,比如:通过oberver监听页面变化,如果页面变化了,就说明页面没有白屏.

javascript 复制代码
window.addEventListener('load', function() {
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            console.log('Page change detected.');
            // 上报到监控系统
            reportToMonitor('Page change detected.');
        });
    });

    var config = { attributes: true, childList: true, characterData: true, subtree: true };
    observer.observe(document.body, config);
});

页面卡顿

页面卡顿的问题,通常是指用户在页面上进行一些操作的时候,页面出现了卡顿的现象,我们先来分析一下,页面卡顿的原因。

页面卡顿的原因,通常有以下几种:

  1. 页面渲染性能问题
  2. 页面交互性能问题
  3. 页面资源加载性能问题
  4. 页面网络性能问题 等等,不排除可能还有其他的原因,但是这里我们只列举了一些常见的原因。
  5. 内存泄漏,这通常是元凶

那么,我们该如何捕捉页面卡顿的问题呢?

我们可以通过window.requestAnimationFrame来捕捉页面的渲染性能问题,比如:

javascript 复制代码
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
    window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}

if (!window.requestAnimationFrame) {
    window.requestAnimationFrame = function(callback, element) {
        var currTime = new Date().getTime();
        var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
        var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
        lastTime = currTime + timeToCall;
        return id;
    };
}

if (!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function(id) {
        clearTimeout(id);
    };
}

var lastFrameTime = 0;
var frameCount = 0;
var frameRate = 0;
var frameRateThreshold = 60;

function onAnimationFrame() {
    var now = Date.now();
    var elapsed = now - lastFrameTime;
    lastFrameTime = now;
    frameCount++;
    if (elapsed > 1000) {
        frameRate = frameCount;
        frameCount = 0;
        if (frameRate < frameRateThreshold) {
            console.warn('Frame rate is ' + frameRate + 'fps, it is too low.');
            // 上报到监控系统
            reportToMonitor('Frame rate is ' + frameRate + 'fps, it is too low.');
        }
    }
    window.requestAnimationFrame(onAnimationFrame);
}

window.requestAnimationFrame(onAnimationFrame);

这样,我们就可以通过window.requestAnimationFrame来捕捉页面的渲染性能问题。

对于内存泄漏,我们可以监听页面卸载事件,然后检查所有的事件处理器和定时器是否都已经被清除,比如:

javascript 复制代码
// 存储所有的事件处理器和定时器
var eventHandlers = [];
var timers = [];

// 覆盖 addEventListener 和 removeEventListener 方法
var originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(eventName, eventHandler) {
    eventHandlers.push({target: this, eventName: eventName, eventHandler: eventHandler});
    originalAddEventListener.call(this, eventName, eventHandler);
};
var originalRemoveEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function(eventName, eventHandler) {
    eventHandlers = eventHandlers.filter(function(handler) {
        return handler.target !== this || handler.eventName !== eventName || handler.eventHandler !== eventHandler;
    });
    originalRemoveEventListener.call(this, eventName, eventHandler);
};

// 覆盖 setTimeout 和 clearTimeout 方法
var originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback, delay) {
    var id = originalSetTimeout(callback, delay);
    timers.push(id);
    return id;
};
var originalClearTimeout = window.clearTimeout;
window.clearTimeout = function(id) {
    timers = timers.filter(function(timer) {
        return timer !== id;
    });
    originalClearTimeout(id);
};

// 在页面卸载时检查所有的事件处理器和定时器是否都已经被清除
window.addEventListener('unload', function() {
    if (eventHandlers.length > 0) {
        console.error('Memory leak detected: Not all event handlers were removed.');
        // 上报到监控系统
        reportToMonitor('Memory leak detected: Not all event handlers were removed.');
    }
    if (timers.length > 0) {
        console.error('Memory leak detected: Not all timers were cleared.');
        // 上报到监控系统
        reportToMonitor('Memory leak detected: Not all timers were cleared.');
    }
});

用户行为日志

用户行为日志是指用户在页面上的一些操作,比如点击按钮、输入框输入等等,这些操作都是用户行为日志,这些日志是非常重要的,它可以帮助我们快速定位问题。比如,用户反馈说,我点击了一个按钮,但是没有反应,这个时候,我们就可以通过用户行为日志来定位问题。但是通常用户将的可能是问题出现的时间点,而不是问题出现的原因,所以我们需要在用户行为日志中加入一些额外的信息。用户经历过哪些路由页面,做过哪些交互,各交互步骤的数据是否正常,这些都是我们需要记录的。

对于页面的路由,我们可以通过window.addEventListener('hashchange', function() {})来捕捉页面的路由变化,然后在里面打印一些日志,比如:

javascript 复制代码
window.addEventListener('hashchange', function() {
    console.log('User navigated to ' + window.location.hash);
});

对于用户的点击事件,我们可以通过window.addEventListener('click', function(event) {})来捕捉用户的点击事件,然后在里面打印一些日志,比如:

javascript 复制代码
window.addEventListener('click', function(event) {
    var target = event.target;
    if (target.tagName === 'BUTTON') {
        console.log('User clicked button: ' + target.innerText);
    }
});

对于用户的输入事件,我们可以通过window.addEventListener('input', function(event) {})来捕捉用户的输入事件,然后在里面打印一些日志,比如:

javascript 复制代码
window.addEventListener('input', function(event) {
    var target = event.target;
    if (target.tagName === 'INPUT') {
        console.log('User input: ' + target.value);
    }
});

对于一些用户的交互事件,我们可以通过window.addEventListener('customEvent', function(event) {})来捕捉用户的交互事件,然后在里面打印一些日志,比如:

javascript 复制代码
// 触发自定义事件
window.dispatchEvent(new CustomEvent('customEvent', {detail: 'User interaction detected.'}));

// 监听自定义事件
window.addEventListener('customEvent', function(event) {
    console.log('User custom event: ' + event.detail);
});

// 触发自定义事件,我们可以做一个工具函数,比如:这阿姨给你方便在任何地方触发自定义事件
function triggerCustomEvent(eventName, detail) {
    window.dispatchEvent(new CustomEvent(eventName, {detail: detail}));
}

一些情况下,前端页面的交互是通过后端返回的数据来触发的,这个时候,数据的正确性也是非常重要的,我们可以通过window.addEventListener('ajaxSuccess', function(event) {})来捕捉用户的交互事件,然后在里面打印一些日志,比如:

javascript 复制代码
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
    // 请求成功
   // 请求成功
    if (response.data.code !== 0) {
        console.error('Request success, but response data is not as expected.');
        triggerCustomEvent('request abnormal', 'Request success, but response data is not as expected.');
    }
    triggerCustomEvent('request Success', response.data);
    return response;
}, function (error) {
    // 请求失败
    console.error(error);
    triggerCustomEvent('request Error', 'Ajax request error.');
    return Promise.reject(error);
});

日志规范

我们还差一些什么,我们怎么知道打一些日志,但是怎么评估这些日志是否非常容易帮助我们定位问题呢?换句话说,当用户反馈问题的时候,我们怎么知道我们的日志是否能帮助我们快速定位问题呢?

一个场景:用户A,通过反馈说,你们页面加载好慢啊。然后就没有其他信息了。这个时候,我们需要去从日志中发现一些问题。

我们会问用户一个问题,比如用户的uid,然后我们就可以通过uid去查找用户的日志,然后我们就可以通过用户的日志来定位问题。

那么,我们就需要在日志中加入一些用户的信息,比如用户的uid,用户的设备信息,用户的网络信息等等,这样我们就可以通过用户的日志来定位问题。因此,我们需要在日志中加入一些用户的信息,比如:

javascript 复制代码
// 获取用户的uid

var uid = getUid();

// 获取用户的设备信息

var deviceInfo = getDeviceInfo();

// 获取用户的网络信息

var networkInfo = getNetworkInfo();

// 打印环境信息

console.log('User: ' + uid + ' Device: ' + deviceInfo + ' Network: ' + networkInfo + ' Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');

日志上报

有了收集的日志之后,我们上报到日志系统,那么这个日志系统应该是怎么样的呢?现在,我们可以使用 mermaid 来绘制一下整个日志系统的架构图:

graph TD A[前端日志] -->|上报| B[日志系统] B -->|存储| C[日志存储] B -->|分析| D[日志分析] B -->|告警| E[日志告警]

这块后端可能有一些开源的日志系统,比如 ELK、Logstash、Kibana、Prometheus、Grafana 等等,这些都是比较常见的日志系统,我们可以根据自己的需求来选择。

比如,我们可以上报到ELK,然后通过Kibana来分析日志,通过Prometheus、Grafana来做一些监控。这些的搭建和使用,这里就不展开了。这里最终是需要提供前端日志的上报接口,然后后端来接收这些日志。

然后前端的日志上报,我们可以通过一些手段来做,比如:

javascript 复制代码
function reportToMonitor(log) {
    // 上报到监控系统
    var img = new Image();
    img.src = 'http://monitor.com/report?log=' + log;
}

这里为什么要用img标签来上报日志呢?因为img标签是不会阻塞页面的,而且可以跨域,这样我们就可以通过img标签来上报日志。

另外,我们还可以通过navigator.sendBeacon来上报日志,这个方法是异步的,不会阻塞页面,比如:

javascript 复制代码
function reportToMonitor(log) {
    // 上报到监控系统
    navigator.sendBeacon('http://monitor.com/report', log);
}

然后,为了保证日志的可靠性,我们还可以通过localStorage来存储日志,然后在下一次用户访问的时候,再上报日志,比如:

javascript 复制代码
function reportToMonitor(log) {
    // 上报到监控系统
    if (navigator.sendBeacon) {
        navigator.sendBeacon('http://monitor.com/report', log);
    } else {
        localStorage.setItem('log', log);
    }
}

有些人可能会说,我还需要做一些日志的压缩和加密,这个时候,我们可以通过pako来做日志的压缩,通过CryptoJS来做日志的加密,比如:

javascript 复制代码
function reportToMonitor(log) {
    // 上报到监控系统
    var compressedLog = pako.deflate(log, { to: 'string' });
    var encryptedLog = CryptoJS.AES.encrypt(compressedLog, 'secret key').toString();
    var img = new Image();
    img.src = 'http://monitor.com/report?log=' + encryptedLog;
}

有人可能会讲,频繁的发送请求,可能会导致一些性能问题,能不能积攒一批日志,然后再发送呢?这个时候,我们可以通过setTimeout来做,比如:

javascript 复制代码
var logs = []; // 存储日志
function reportToMonitor(log) {
    // 存储日志
    logs.push(log);
    // 延迟发送日志
    setTimeout(function() {
        var img = new Image();
        img.src = 'http://monitor.com/report?log=' + logs.join(',');
        logs = [];
    }, 5000);  // 5秒后发送日志
}

结构化日志

我们有了上述日志之后就,针对一个具体用户进行搜索,看到可能是这样的一些个日志:

  • User: 123456 Device: iPhone 6s Network: 4G enter page /home 2024-02-12 12:00:00
  • User: 123456 Device: iPhone 6s Network: 4G Page load time for /home is 3000 milliseconds 2024-02-12 12:00:03
  • User: 123456 Device: iPhone 6s Network: 4G User clicked button: submit 2024-02-12 12:00:05
  • User: 123456 Device: iPhone 6s Network: 4G User input[id=userName] : zhangsan 2024-02-12 12:00:06
  • User: 123456 Device: iPhone 6s Network: 4G User input[id=password] : 123456 2024-02-12 12:00:07
  • User: 123456 Device: iPhone 6s Network: 4G User clicked button: login 2024-02-12 12:00:08
  • User: 123456 Device: iPhone 6s Network: 4G Request success, url: /login. 2024-02-12 12:00:12
  • User: 123456 Device: iPhone 6s Network: 4G User navigated to /dashboard 2024-02-12 12:00:13
  • User: 123456 Device: iPhone 6s Network: 4G Page load time for /dashboard is 3000 milliseconds 2024-02-12 12:00:16
  • User: 123456 Device: iPhone 6s Network: 4G Request Failed, url: /dashboard. {errMessage: 'Internal Server Error'} 2024-02-12 12:00:20

这样,我们就可以通过用户的日志来定位问题。

日志图形化

比如,我们可以将用户的日志,使用 mermaid 来画一下这个流程图:

graph TD A[enter page /home] -->|3000ms| B[User clicked button: submit] B --> C[User input username: zhangsan] C --> D[User input password: 123456] D --> E[User clicked button: login] E --> F[Request success, url: /login] F --> G[User navigated to /dashboard] G -->|3000ms| H[Request Failed, url: /dashboard]

篇幅有限,顺着思路,本来想在写一些监控相关的内容,但是感觉篇幅有点长了,就到这里吧。后续在继续写一些监控相关的。

关注我的公众号,第一时间获取更新!

相关推荐
阿伟*rui2 分钟前
认识微服务,微服务的拆分,服务治理(nacos注册中心,远程调用)
微服务·架构·firefox
minDuck4 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!25 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。30 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
ZHOU西口34 分钟前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
花花鱼36 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093340 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架