操作日志设计与实现:记录、追踪与审计的最佳实践

令狐光泽 安全 阅读 1,202
赞 39 收藏
二维码
手机扫码查看
反馈

操作日志这事,别再瞎搞了

最近项目里要加操作日志功能,产品经理一句话“记录用户关键操作”,结果我折腾了三天。不是技术多难,而是方案太多,选哪个都像踩雷。市面上常见的无非就三种:手动埋点、AOP 切面(前端模拟)、监听 DOM 事件自动上报。我挨个试了一遍,今天就说说我的真实体验——不讲理论,只讲谁省事、谁坑多。

操作日志设计与实现:记录、追踪与审计的最佳实践

手动埋点:最稳,但累死人

这是最原始的办法,哪里需要日志,就在代码里写一行 logAction('点击保存按钮')。优点?简单直接,可控性高,出问题一眼就能定位。缺点?改需求时你得满项目找埋点位置,漏一个就是事故。

比如这个场景:

function handleSave() {
  // 业务逻辑
  saveData();
  // 手动埋点
  logAction({
    action: 'save_form',
    page: 'user_profile',
    timestamp: Date.now()
  });
}

看着没问题,但如果你有 50 个页面,每个页面 3-5 个关键操作,光是维护这些埋点就够你喝一壶。更别说有时候开发赶时间,直接忘了加。我之前就因为漏了一个删除操作的日志,线上出了问题硬是查不到是谁删的,背了锅。

所以,除非项目特别小,或者对日志精度要求极高(比如金融类),否则我不推荐纯手动埋点。太容易出错,也太反人类。

监听 DOM 事件:看似聪明,实则坑多

有人想偷懒,说“我直接监听所有 click 事件,根据元素属性自动上报不就行了?”于是写了这么一段:

document.addEventListener('click', (e) => {
  const target = e.target;
  const actionName = target.dataset.action;
  if (actionName) {
    logAction({
      action: actionName,
      page: location.pathname,
      element: target.tagName + '.' + target.className
    });
  }
});

然后在 HTML 里加个 data-action="submit_order"。看起来挺美,不用改 JS 逻辑,只要在模板里加属性就行。

但实际用起来问题一堆:

  • 动态渲染的组件(比如 React/Vue)里,data-action 经常被忽略,尤其是第三方组件
  • 同一个按钮在不同状态下可能代表不同操作(比如“启用/禁用”切换),但 data-action 是静态的,没法传上下文
  • 误触上报:比如弹窗里的取消按钮,其实不算“关键操作”,但也被录了

我试过在项目里推这个方案,结果 QA 报了一堆无效日志,还得加白名单过滤。最后发现,为了处理边界情况,代码量快赶上手动埋点了,纯属自找麻烦。

前端 AOP:我的首选,灵活又可控

折腾一圈后,我回归了“伪 AOP”思路——用装饰器或高阶函数包装关键方法。虽然前端没有真正的 AOP,但用 JS 的特性可以模拟得很像。

比如我封装了一个 withLogging 高阶函数:

function withLogging(actionName, extraInfo = {}) {
  return function(target, propertyKey, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args) {
      // 上报前
      logAction({
        action: actionName,
        page: location.pathname,
        ...extraInfo,
        args: JSON.stringify(args.slice(0, 2)) // 只传前两个参数防泄露
      });
      // 执行原方法
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

然后在 Vue 方法上这么用:

export default {
methods: {
@withLogging('delete_user', { category: 'admin' })
async deleteUser(userId) {
await api.deleteUser(userId);
this.refreshList();
}
}
}
`>
&lt;p&gt;或者 React 里用 HOC:&lt;/p&gt;</code></pre>javascript
const withDeleteLogging = (WrappedComponent) => {
return (props) => {
const handleDelete = useCallback((id) => {
logAction({ action: 'delete_item', id });
props.onDelete(id);
}, []);
return <WrappedComponent {...props} onDelete={handleDelete} />;
};
};
<pre class="pure-highlightjs line-numbers language-none"><code class="no-highlight language-none">&lt;p&gt;这套方案我用了半年,真香。原因有三:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;和业务逻辑紧耦合&lt;/strong&gt;:日志逻辑就在方法定义处,不会漏,也不会误报&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可携带上下文&lt;/strong&gt;:比如删除用户时能带上
userId,而不是只知道“有人点了删除”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容易开关&lt;/strong&gt;:通过环境变量控制是否上报,本地开发完全静默&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然也有小缺点:装饰器语法需要 Babel 插件支持(Vue 3 原生支持),React 里 HOC 写法略啰嗦。但比起收益,这点成本完全可以接受。&lt;/p&gt;

&lt;h2&gt;性能对比:差距比我想象的大&lt;/h2&gt;
&lt;p&gt;很多人担心日志上报影响性能,其实只要注意两点,基本没影响:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用
navigator.sendBeacon() 而不是 fetch,避免阻塞页面卸载&lt;/li&gt;
&lt;li&gt;批量上报,比如每 10 秒发一次,而不是每次操作都发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我测过三种方案的性能损耗:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手动埋点:几乎为零(就一行函数调用)&lt;/li&gt;
&lt;li&gt;DOM 监听:高频点击时 CPU 占用略高(要遍历 dataset)&lt;/li&gt;
&lt;li&gt;AOP 方案:和手动埋点差不多,只是多了层函数包装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正拖慢速度的是日志内容序列化。比如有人直接
JSON.stringify(entireUserObject),那肯定卡。所以我在 withLogging 里特意限制只传前两个参数,且做了脱敏。&lt;/p&gt;

&lt;h2&gt;我的选型逻辑&lt;/h2&gt;
&lt;p&gt;总结一下我的选择标准:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目小、操作少 → 手动埋点,省事&lt;/li&gt;
&lt;li&gt;中大型项目、多人协作 → 用 AOP 风格的高阶函数/装饰器&lt;/li&gt;
&lt;li&gt;千万别用全局 DOM 监听,除非你愿意后期花双倍时间修 bug&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外,日志结构一定要统一。我见过团队里有人传
{ type: 'click' },有人传 { event: 'btn_save' },最后分析时根本没法聚合。所以我在 logAction 函数里强制校验字段:&lt;/p&gt;</code></pre>javascript
function logAction(payload) {
if (!payload.action || !payload.page) {
console.warn('Invalid log payload', payload);
return;
}
// 发送到 https://jztheme.com/api/logs
navigator.sendBeacon('https://jztheme.com/api/logs', JSON.stringify(payload));
}
``

这样至少保证基础字段齐全。

最后提醒:别把日志当监控

操作日志是给运营和产品看的,不是给开发者 debug 用的。别试图在里面塞堆栈信息、网络状态这些。真要排查问题,还是得靠专业的前端监控系统(比如 Sentry)。日志只需要回答“谁在什么页面干了什么事”就够了。

以上是我踩坑后的总结,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合 Redux action 自动日志),后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论