Ubuntu 环境下追踪 JS 用户行为的落地方案
一 架构与采集要点
二 Ubuntu 端日志采集与监控
三 快速实现示例
// logger.js
const Logger = {
levels: { debug: 0, info: 1, warn: 2, error: 3 },
currentLevel: 1,
sessionId: sessionStorage.getItem('sessionId') || (crypto.randomUUID?.() ?? Date.now().toString()),
init() {
if (!sessionStorage.getItem('sessionId')) sessionStorage.setItem('sessionId', this.sessionId);
},
log(level, message, data = {}) {
if (this.levels[level] < this.levels[this.currentLevel]) return;
const entry = {
timestamp: new Date().toISOString(),
level,
sessionId: this.sessionId,
page: location.pathname,
url: location.href,
referrer: document.referrer,
ua: navigator.userAgent,
action: message,
...data
};
// 开发环境控制台输出
if (process.env.NODE_ENV !== 'production') console[level](entry);
// 生产环境可靠上报
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/log', JSON.stringify(entry));
} else {
fetch('/api/log', { method: 'POST', body: JSON.stringify(entry), keepalive: true }).catch(() => {});
}
},
info: (m, d) => this.log('info', m, d),
warn: (m, d) => this.log('warn', m, d),
error: (m, d) => this.log('error', m, d)
};
Logger.init();
// 自动采集示例
document.addEventListener('click', (e) => {
Logger.info('click', {
tagName: e.target.tagName, id: e.target.id, className: e.target.className,
text: (e.target.innerText || '').trim().slice(0, 100),
xpath: getXPath(e.target)
});
});
function getXPath(el) {
if (el.id) return `//*[@id="${el.id}"]`;
if (el === document.body) return '/html/body';
let ix = 0;
const sibs = el.parentNode?.childNodes || [];
for (let i = 0; i < sibs.length; i++) {
const s = sibs[i];
if (s === el) return `${getXPath(el.parentNode)}/${el.tagName}[${ix + 1}]`;
if (s.nodeType === 1 && s.tagName === el.tagName) ix++;
}
return '';
}
// server.js
const express = require('express');
const morgan = require('morgan');
const { createLogger, format, transports } = require('winston');
require('winston-daily-rotate-file');
const logger = createLogger({
level: 'info',
format: format.combine(format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.json()),
transports: [
new transports.File({ filename: 'error.log', level: 'error' }),
new DailyRotateFile({
filename: 'app-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true,
maxSize: '20m', maxFiles: '14d'
}),
new transports.Console({ format: format.simple() })
]
});
const app = express();
app.use(express.json({ limit: '10kb' }));
app.use(morgan('combined', { stream: { write: msg => logger.info(msg.trim()) } }));
app.post('/api/log', (req, res) => {
// 简单校验示例:{ sessionId, action, page, ... }
if (!req.body || !req.body.sessionId || !req.body.action) {
return res.status(400).send('bad request');
}
// 服务端补充字段
const entry = {
...req.body,
ip: req.ip || req.headers['x-forwarded-for'] || '-',
ua: req.headers['user-agent'] || '-',
receivedAt: new Date().toISOString()
};
logger.info('frontend_log', entry);
res.status(204).end();
});
app.listen(3000, () => logger.info('server listening on :3000'));
四 分析与可视化
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -20
五 合规与性能建议