排查“客户端发送成功,报表没有数据”时,最容易临时加一句console.log(req.body)。它确实能显示请求,却可能把令牌、邮箱、完整 URL、Cookie 或业务属性一起写进排错日志。更稳妥的方法是先定义排错所需的最小证据,再按字段白名单生成结构化记录。
本文由 SensorFlow 项目维护方整理,AI 辅助起草;下方仅为离线合成示例,不是 SensorFlow 已内置的日志配置,也不是对真实用户数据的测试。
一、日志要回答什么问题?
一条埋点排错记录至少要能区分:请求是否进入接收层、是否被验证拒绝、是否进入后续处理、是否出现可查询的测试记录。received、rejected、persisted是本文合成示例中的阶段名称,不应未经核对就照搬为某个产品的实际状态码。
建议保留测试事件 ID、事件名、阶段、结果码、受控路由模板和发生时间。不要把“请求正文完整保存”当作诊断能力。OWASP 的日志指南明确提醒,会话标识、访问令牌、密码和敏感个人数据通常不应直接进入日志。OpenTelemetry 的敏感数据处理指南也强调最小化采集,并说明具体哪些数据敏感仍需由实施团队根据场景判断。
二、先选字段,再处理字符串
对调试数据做“先整包序列化,再用正则遮住敏感值”很脆弱:新字段、嵌套对象或不同编码都可能漏过。此处先构造一个全新的输出对象,只放约定字段。字段本身仍须验证;例如事件 ID 若由邮箱拼成,就不能因为字段名叫event_id而当作安全值。
完整 URL 常带查询参数。Node 的URL 文档说明search和searchParams对应查询部分。因此这里不记录url;只允许预先登记的路由模板。这个选择不等于所有路由模板都无敏感信息,生产字典仍需审核。
三、运行一个不联网的白名单示例
保存为safe-log.cjs,执行node safe-log.cjs。它只使用 Node 内置的严格断言,不会向任何网站上传数据。下面的 ID、邮箱和令牌都是合成占位符。
constassert=require('node:assert/strict');conststages=newSet(['received','rejected','persisted']);constnames=newSet(['page_view','signup_success']);constroutes=newSet(['/signup','/orders/:orderId']);functionsafeCode(value){if(typeofvalue!=='string'||!/^t-[A-Za-z0-9]{1,20}$/.test(value)){thrownewError('invalid synthetic test ID');}returnvalue;}functionpick(value,allowed,label){if(!allowed.has(value))thrownewError('invalid '+label);returnvalue;}functiondebugLine(input){constoutput={test_event_id:safeCode(input.test_event_id),event_name:pick(input.event_name,names,'event name'),stage:pick(input.stage,stages,'stage'),route_template:pick(input.route_template,routes,'route'),error_class:input.stage==='rejected'?pick(input.error_class,newSet(['schema','auth']),'error class'):null};returnJSON.stringify(output);}constraw={test_event_id:'t-A1',event_name:'signup_success',stage:'rejected',route_template:'/signup',error_class:'schema',email:'private@example.test',access_token:'synthetic-secret',url:'https://example.test/signup?token=synthetic-secret'};constline=debugLine(raw);assert.equal(JSON.parse(line).stage,'rejected');assert.equal(JSON.parse(line).error_class,'schema');assert.equal(line.includes('private@example.test'),false);assert.equal(line.includes('synthetic-secret'),false);assert.equal(line.includes('https://'),false);assert.throws(()=>debugLine({...raw,test_event_id:'private@example.test'}));assert.throws(()=>debugLine({...raw,stage:'received\nFAKE'}));assert.throws(()=>debugLine({...raw,route_template:'/orders/123'}));console.log(line);console.log('PASS: 8 synthetic log checks');通过时会输出一条只有五个受控字段的 JSON 日志行,并完成八条断言。这里使用 JSON 编码和枚举/长度约束,也是在减少换行注入和超长输入造成的排查困难;OWASP 的日志注入说明解释了不可信输入如何伪造日志行。
四、白名单之外还要验收什么?
这段代码没有实现真实 SDK 接收、身份关联、追踪系统或日志存储。上线前还应检查三件事:第一,日志采集器、反向代理和异常上报器是否另行记录了完整 URL 或请求体;第二,谁能读日志、保存多久、备份如何处理;第三,测试事件 ID 能否与接收日志、存储行和报表查询对应。只保护应用的一条console.log,不代表整条链路完成脱敏。
排错的目标是让差异可定位,而不是留下最多原始数据。对一个合成测试事件,写清“在哪个阶段消失、凭哪条受控证据判断”,通常比贴出整包真实请求更有用。