日志打点规范

背景
日志上报可以记录用户行为,方便运营查看后期转化;除了日常开发中涉及到比较多的业务事件上报以外,异常上报、过程日志上报等信息能让开发更好的排查问题
通用打点
一般包含为用户行为上报(pv、uv、用户点击按钮)、异常上报(js异常、source异常、promise异常)等,异常处理涉及到source-map的使用,写一个通用的sdk,这里结合腾讯云cls的sdk实现
import {
Log,
LogGroup,
AsyncClient,
PutLogsRequest,
} from "tencentcloud-cls-sdk-js-web";
const baseConfig = {
autoTrack: true,
env: "development",
endpoint: "",
retry_times: 10,
address: "127.0.0.1",
topic_id: "",
};
/**
* @param {Object} config 日志追踪配置
* @param {Boolean} config.autoTrack 是否自动追踪
* @param {String} config.topic_id 日志主题ID
* @param {String} config.env 环境 release | development
*/
export default class LogTracker {
config: any;
logger: any;
pageStartTime: any;
currentPath: any;
constructor(config = {}) {
this.config = {
...baseConfig,
...config,
};
this.logger = new AsyncClient({
endpoint: this.config.endpoint,
retry_times: this.config.retry_times,
});
// 点击事件上报
this.initAutoTrack();
// 访问页面、离开页面上报
this.routerListen();
// 异常上报
this.initErrorHandler();
// 性能指标监控
this.initPerformanceTracking();
}
initPerformanceTracking() {
// 核心性能指标自动采集
this.trackCoreWebVitals();
this.trackResourceTiming();
}
// 核心Web指标监控
trackCoreWebVitals() {
const metrics = {};
const logMetric = (name: string, value: any) => {
this.trackPerformance(name, { value });
};
// FP监控
new PerformanceObserver(entryList => {
for (const entry of entryList.getEntriesByName('first-paint')) {
console.log('FP:', entry.startTime);
}
}).observe({ entryTypes: ['paint'] })
// FCP监控
// new PerformanceObserver(entryList => {
// for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
// console.log('FCP:', entry.startTime);
// }
// }).observe({ entryTypes: ['paint'] })
// LCP监控
new PerformanceObserver(entryList => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
logMetric('LCP', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// FID监控
new PerformanceObserver(entryList => {
const entries = entryList.getEntries();
entries.forEach((entry: any) => {
logMetric('FID', entry.processingStart - entry.startTime);
});
}).observe({ type: 'first-input', buffered: true });
// CLS监控
let clsValue = 0;
new PerformanceObserver(entryList => {
for (const entry of entryList.getEntries() as any) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
logMetric('CLS', clsValue);
}).observe({ type: 'layout-shift', buffered: true });
}
// 资源性能监控
trackResourceTiming() {
const resources = performance.getEntriesByType('resource');
resources.forEach((resource: any) => {
this.trackPerformance('resource', {
name: resource.name,
type: resource.initiatorType,
duration: resource.duration,
dns: resource.domainLookupEnd - resource.domainLookupStart,
tcp: resource.connectEnd - resource.connectStart,
ttfb: resource.responseStart - resource.requestStart
});
});
}
// 统一性能上报方法
trackPerformance(metricType: string, data: any) {
this.sendLog('performance', {
metricType,
...data
});
}
routerListen() {
// 监听浏览器前进/后退(原生事件)
window.addEventListener("popstate", this.handleRouteChange);
window.addEventListener("hashchange", this.handleRouteChange);
// 劫持 History API 方法
this.overrideHistoryMethods();
// 初始触发一次路由检测
this.handleRouteChange();
}
// 重写 pushState/replaceState
overrideHistoryMethods() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (state, title, url) => {
originalPushState.call(history, state, title, url);
this.handleRouteChange();
};
history.replaceState = (state, title, url) => {
originalReplaceState.call(history, state, title, url);
this.handleRouteChange();
};
}
// 统一处理路由变化
handleRouteChange = () => {
const newPath = this.getCurrentPath();
if (newPath === this.currentPath) return;
// 触发离开事件
if (this.currentPath) {
this.trackPageLeave({
from: this.currentPath,
to: newPath,
});
}
// 触发进入事件
this.trackPageView({
path: newPath,
referrer: this.currentPath,
});
this.currentPath = newPath;
};
// 获取当前路径(兼容 Hash 和 History 模式)
getCurrentPath() {
return window.location.hash.slice(1) || window.location.pathname;
}
// 销毁监听
destroy() {
window.removeEventListener("popstate", this.handleRouteChange);
window.removeEventListener("hashchange", this.handleRouteChange);
}
// 初始化自动追踪
initAutoTrack() {
if (!this.config.autoTrack) return;
// 点击事件上报
document.addEventListener(
"click",
(e) => {
const target: any = e.target;
this.trackEvent("click", {
tag: target.tagName,
id: target.id,
class: target.className,
text: target.innerText.slice(0, 50),
});
},
true
);
}
// 初始化错误监听
initErrorHandler() {
// 监听常规JS错误
window.addEventListener(
"error",
(event: any) => {
const target = event.target
if (
target &&
(target instanceof HTMLImageElement ||
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement ||
target instanceof HTMLVideoElement)
) {
const url = event.target?.src || event.target?.href
const data: Record<string, any> = {
stack: "",
assetUrl: url,
type: "SOURCE_ERROR",
}
this.trackError(data);
return
}
const error = event.error
const data: Record<string, any> = {
type: "JS_ERROR",
stack: error.stack,
}
this.trackError(data);
},
true,
)
// Promise错误
window.addEventListener("unhandledrejection", (e) => {
this.trackError({
type: "promise_error",
msg: e.reason?.message || e.reason,
stack: e.reason?.stack,
});
});
}
// 通用上报方法
async sendLog(logType: string, data: any) {
const logData = {
eventKey: logType,
eventData: {
timestamp: Date.now().toString(),
env: this.config.env,
page: window.location.href,
userAgent: navigator.userAgent,
...data,
},
};
try {
// 创建 LogGroup 实例
const logGroup = new LogGroup(this.config.address);
logGroup.setSource(this.config.address);
// 创建 Log 实例
const log = this.createLog(logData);
// 将 Log 添加到 LogGroup
logGroup.addLog(log);
// 创建 PutLogsRequest 实例
const request = new PutLogsRequest(this.config.topic_id, logGroup);
// 发送 PutLogsRequest
const data = await this.logger.PutLogs(request);
console.log("Logs uploaded successfully:", data);
} catch (err) {
console.error("日志上报失败:", err);
// 失败处理逻辑(可选存储到localStorage后续重发)
}
}
createLog(logData: any) {
// 创建一个 log
const log = new Log(Date.now());
Object.keys(logData).forEach((key) => {
const value =
typeof logData[key] !== "object"
? logData[key]
: logData[key] !== null
? JSON.stringify(logData[key])
: logData[key];
log.addContent(key, value);
});
return log;
}
// 页面访问追踪
trackPageView(routeInfo: any = {}) {
const now = Date.now();
// 更新当前路径
this.currentPath = routeInfo.to || window.location.pathname;
// 上报新页面访问
this.pageStartTime = now;
this.trackEvent("page_view", {
...routeInfo,
referrer: document.referrer,
title: document.title,
});
}
// 页面离开追踪
trackPageLeave(routeInfo: any = {}) {
const now = Date.now();
// 计算页面停留时间
const stayTime = now - this.pageStartTime;
this.trackEvent("page_leave", {
...routeInfo,
referrer: document.referrer,
title: document.title,
stayTime
});
}
// 行为事件上报
trackEvent(eventType: string, params = {}) {
this.sendLog("behavior", {
eventType,
...params,
});
}
// 错误上报
trackError(errorInfo: any) {
this.sendLog("error", errorInfo);
}
// 过程日志上报
log(level = "info", message: any, context = {}) {
this.sendLog("process", {
logLevel: level,
message,
...context,
});
}
// 业务事件上报
reportBusinessEvent(eventName: string, customData = {}) {
this.sendLog("business", {
eventName,
...customData,
});
}
}