前端数据埋点(1):一次点击如何变成一条埋点

📅 2026/8/14 5:07:23
前端数据埋点(1):一次点击如何变成一条埋点
系列前端数据埋点· 第 1 篇下一篇无埋点——重写原生 API 之后业务代码不用自己打点读者会写 JavaScript。本篇把埋点当成一条数据管线来讲事件从页面里产生经过哪几层最后怎样离开浏览器。不预设你做过监控 SDK。先说结论步骤业务里自己打点埋点 SDK 的做法采集每个按钮、每个接口失败处手写fetch(/log)无埋点重写 XHR / fetch / error / click主动埋点调用track()携带上下文常常只有「出错了」这一句上报时附带行为栈用户刚才点了什么、走过哪条路由通道错误、点击、曝光挤在一个 URL错误走dsn业务埋点走trackDsn何时发出在当前调用栈里立刻请求推进微任务队列避免卡住页面、也避免上报请求被自己再次采集埋点是把「页面上发生过什么」变成结构化记录并送到服务端的过程。它不是console.log也不是随便 POST 一段字符串。一次完整走法只有四段产生点击 / 报错 / 业务调用 track → 采集截获或手动传入 → 整形 行为栈变成可分析的字段必要时先记下用户轨迹 → 上报选通道、装信封、排队发出1. 缺口为什么不能到处fetch(/log)假设结算按钮要统计点击。最直接的写法是payButton.addEventListener(click,(){fetch(/log,{method:POST,body:JSON.stringify({event:pay_click})})doPay()})这能跑但很快会卡在三件事上。漏。路由切换、脚本报错、图片加载失败、接口 500不可能每个地方都记得写一行。漏掉的那些恰恰是出故障时最需要的。脏。上报自己也是一次 HTTP 请求。如果你在监听所有 XHR却不过滤上报地址SDK 会采集到自己的上报再上报再采集——死循环。没法复现。服务端只收到{ message: Cannot read property x of undefined }。不知道用户上一秒点了哪个按钮、从哪页跳过来、刚才那次接口是不是已经 500 了。一条错误没有前文排查只能靠猜。所以埋点要解决的不是「把字发出去」而是采集要全、上报要干净、每条记录要带得上上下文。2. 概念词典下面这些词会贯穿整个系列。每个词只回答一件事它挡住了上面哪一种缺口。词是什么相对什么挡住哪类缺口无埋点重写浏览器原生 APIXHR、fetch、window.onerror、点击、history业务代码不用写采集相对「每个事件手写一行」漏主动埋点业务显式调用track(actionType, param)带上业务语义相对无埋点无埋点不知道「这是结算按钮」没有业务含义DSNData Source Name上报地址。错误一条埋点一条相对「所有日志打到同一个 URL」后端无法分流行为栈 / Breadcrumb固定长度的近期事件队列出错时整段附在上报里相对「只报当前这一条」没法复现整形把原生事件对象收成固定字段type、url、status、message…相对把ErrorEvent原样 JSON.stringify后端无法聚合信封真正 POST 出去的那一包身份 行为栈 本次数据 设备信息相对只发data本身无法区分用户、版本、机型actionType是主动埋点的类型枚举常见五种actionType含义典型场景PAGE页面曝光进入结算页EVENT事件点击「立即支付」VIEW区域曝光优惠券模块出现在视口DURATION时长停留在结果页 12sDURATION_VIEW区域停留时长某模块在视口内停留 3s无埋点截获的是技术事件一次 XHR、一次脚本错误。主动埋点上报的是业务事件一次支付点击。两条线最后进同一个send()用「有没有actionType」决定走哪条 DSN。3. 管线一条事件怎么走完初始化时要先告诉 SDK 两个地址以及「这是哪个应用」init({dsn:https://monitor.example.com/errors/upload,// 错误 / 异常trackDsn:https://monitor.example.com/track/upload,// 业务埋点apikey:app-web-001,trackKey:app-web-001-track,maxBreadcrumbs:10,backTrackerId:()currentUserId,// 用来统计「这个错误影响了多少用户」})之后发生的事按时间顺序是这样。3.1 采集事件从哪进来两条入口不要混。无埋点。SDK 在init时重写XMLHttpRequest.prototype.send、window.fetch、history.pushState并在捕获阶段监听click和error。业务照常写xhr.send()SDK 在原函数前后塞入自己的逻辑记下 method、url、开始时间等loadend再算出耗时和 status。主动埋点。业务在关键路径上调用track(EVENT,{trackId:pay_click,custom:{orderId:A1001,amount:99},})SDK 补上id一条记录的 uuid和trackTime然后交给send。点击「立即支付」时两条线可能同时动无埋点记下一次 DOM click进行为栈主动埋点再报一条EVENT真正给分析用。前者回答「出错前他点了什么」后者回答「有多少人点了支付」。3.2 订阅采集和处置要分开重写原生 API 的函数只负责截获不负责「这是错误还是点击、要不要上报」。截获之后触发一类事件名例如xhr、fetch、error、dom。监听方按类型注册回调subscribe(xhr,(httpData){// 这里才决定推进行为栈还是当成 HTTP 错误上报})同一类事件可以挂多个回调采集侧不用知道后面有几个人在听。这是整条管线能拆开演进的关键换一种上报方式不必再改 XHR 重写逻辑。3.3 整形原生对象不能直接发走ErrorEvent、XMLHttpRequest带一堆浏览器私有字段后端没法按「同一种错误」聚合。整形就是收成一张固定表。HTTP 失败大致变成{type:HTTP_ERROR,url:https://shop.example.com/checkout,// 当时的页面name:xhr--POST,message:internal_error /api/pay,elapsedTime:230,request:{method:POST,url:/api/pay,data:...},response:{status:500,data:...},}脚本错误变成JAVASCRIPT_ERROR message stack。资源加载失败变成RESOURCE_ERROR 标签名 src。主动埋点几乎不用再整形业务传入的trackId/custom本身就是分析字段SDK 只补id、actionType、trackTime。3.4 行为栈先记下不一定立刻上报点击、路由切换、成功的 XHR默认不上报只推进一个长度有上限的队列常见默认 10 条。新事件进来、队列满了丢掉最老的一条。breadcrumb.push({type:Click,category:user,// http | user | debug | exception | lifecycledata:button idpay立即支付/button,level:info,time:Date.now(),})只有「需要人来看」的时刻——脚本抛错、接口失败、主动track()——才调用send。send会把当前整段行为栈拷进信封。于是服务端拿到的不是孤零零一条 error而是从/cart进了/checkout点了「立即支付」POST /api/pay返回 500然后才是这次脚本错误行为栈解决的是复现不是统计。统计点击量仍然要靠主动埋点的EVENT。3.5 上报选通道、装信封、排队send只看一件事这份 data 有没有actionType。asyncfunctionsend(data){constdsndata.actionType?trackDsn:errorDsnif(!dsn)returnconstpacket{authInfo:{trackerId:String(backTrackerId()),apikey,trackKey,sdkName,sdkVersion,},breadcrumb:breadcrumb.getStack(),data,deviceInfo:{netType,clientWidth,clientHeight,ratio},}// beforeDataReport 可以改包、也可以返回空以取消本次上报queue.addFn(()xhrPost(packet,dsn))}双 DSN让错误监控和产品分析不必抢同一个接口、同一套存储。信封让后端先读authInfo知道是谁、哪个应用再读data知道发生了什么最后用breadcrumb复现过程。队列是微任务批量send并不在当前调用栈里立刻xhr.send。原因有二上报不应拖慢正在执行的业务上报本身是 HTTP必须标成「这是 SDK 自己的请求」采集层看到上报 URL 就跳过否则会死循环。浏览器里默认用 XHR POST JSON也可以改成 Image 打点new Image().src url ?data encodeURIComponent(...)优点是不吃跨域预检缺点是长度有限。小程序则走wx.request。发出去之前还有两道闸beforeDataReport改包或取消错误还有errorId 去重——同类错误同一个 type message apikey超过maxDuplicateCount就不再发避免一个死循环把上报打爆。4. 对照一次「支付点击」在管线里的位置把时间摊开同一秒里可能有三份数据职责不同。时刻谁采集进哪作用进入结算页track(PAGE, { trackId: checkout })trackDsn统计曝光点击按钮无埋点监听click只进行为栈给稍后的错误当前文点击按钮track(EVENT, { trackId: pay_click })trackDsn统计转化POST /api/pay结束无埋点重写了 XHR成功则只进行为栈失败则再send到dsn失败要告警随后脚本抛错window.onerrordsn信封里带上上面那些面包屑排查如果只做无埋点、不做track(EVENT)你能知道「有人点了某个 button」对不上产品说的「支付按钮点击量」。如果只做track()、没有行为栈错误上报仍然没法复现。两条线要一起看。5. 上报之后流程在浏览器这边结束浏览器或小程序把信封 POST 到 DSN 之后SDK 的工作就结束了。服务端通常会落库、按errorId聚合、按trackId做漏斗、必要时告警。那是另一截系统不改变采集侧这四段。接入时只要记住三件事没配dsn错误发不出去没配trackDsn主动埋点发不出去。backTrackerId不配后端就无法回答「影响了多少用户」。上报 URL 必须能被采集层识别并跳过否则无埋点会吃到自己。你可以从这里带走什么埋点是四段管线采集 → 整形 → 行为栈 → 上报。缺任何一段不是漏事件就是没法复现就是把自己打进死循环。无埋点解决「漏」主动埋点解决「没有业务语义」。点击统计靠EVENT出错复现靠行为栈不要用其中一条冒充另一条。send用有没有actionType选择trackDsn还是dsn。分析数据和稳定性数据从这一刻分开。真正发出去的是信封身份、行为栈、本次data、设备信息。只发一句 message后端聚合不了。上报必须排队且不能被自己的采集再次截获。下一篇前端数据埋点 · 2写无埋点本身怎样重写 XHR / fetch / error / click以及为什么必须跳过 SDK 自己的上报地址。