五月中旬,公司要给一个C端鸿蒙应用上一个新功能:付费转化漏斗分析。运营要看用户从"详情页→加入对比→点击分享→付费弹窗→付费成功"这五步的真实流失情况。
按理说,埋点嘛,我在行。Web 端埋过三四年,鸿蒙端也不是第一次。但那天我脑子一抽,想着"试试 AI 写埋点提效",于是打开了 Cursor。
我跟它说:
"帮我在鸿蒙 ArkTS 项目里给详情页加一套埋点,覆盖五个事件:详情页曝光、加入对比、点击分享、付费弹窗展示、付费成功。要求:解耦、不影响主流程、有完整注释。"
三分钟后,它给了我 200 行代码。
我看完第一反应是:牛逼。结构清晰、注释完整、错误处理也写了,比我手写还规范。
我直接合到了主干。
一个月后,运营找上门了
"斌哥,付费漏斗对不上------曝光数 1.2 万,但加入对比只有 800,分享 200,付费弹窗 1500,付费成功 3。"
我盯着这组数字,愣了十秒。
"等等,曝光 1.2 万,付费弹窗 1500?这说明很多用户没经过'加入对比'直接看到付费弹窗了?这漏斗逻辑根本不通。"
运营也懵了:"我也不知道为什么,反正你埋的数对不上。"
我打开埋点后台,逐条比对------五个核心事件,实际只有"详情页曝光"和"付费成功"两个有数据,"加入对比"、"点击分享"、"付费弹窗展示"全都没埋上。
那一刻我后背有点凉。AI 写的 200 行代码,看着那么漂亮,关键事件漏埋了 60%。
排查:AI 写的代码到底缺了什么
我重新看了一遍 AI 生成的代码。它给我写了一个 EventTracker 类,每个事件都封装了 try-catch,逻辑是这样的:
typescript
// AI 生成的埋点(简化版)
export class EventTracker {
static track(eventName: string, params?: Record<string, any>) {
try {
const payload = {
event: eventName,
timestamp: Date.now(),
params: params || {},
sessionId: AppStorage.get('sessionId')
}
// 上报到埋点服务
HttpClient.post('/track', payload).catch(e => {
console.warn('track failed', e)
})
} catch (e) {
console.error('tracker error', e)
}
}
}
// 详情页曝光埋点(AI 写的)
aboutToAppear() {
EventTracker.track('detail_view', { itemId: this.itemId })
}
// 加入对比埋点(AI 写的)
handleAddCompare() {
// ...业务逻辑
EventTracker.track('add_compare', { itemId: this.itemId })
}
单看每段代码都对------调用了 track 方法,参数传得也对。但为什么没数据?
我开始逐事件排查。第一个线索在"加入对比"事件。
我的"加入对比"按钮的完整链路是这样的:
typescript
// 真实链路(业务代码)
async handleAddCompare() {
// 1. 校验用户是否登录
const user = await this.checkLogin()
if (!user) {
// 2. 未登录跳登录页
router.pushUrl({ url: 'pages/Login' })
return // <-- 关键:这里 return 了
}
// 3. 已登录,加入对比
await this.compareService.add(this.itemId)
// 4. 弹个 toast
promptAction.showToast({ message: '已加入对比' })
// 5. 埋点
EventTracker.track('add_compare', { itemId: this.itemId })
}
看到了吗?AI 写埋点的时候,它只看到了"埋点在最后一行"这个理想路径。但真实业务里:
- 用户未登录 → 走分支 1-2,return 之前没埋点
compareService.add失败 → 走 catch 分支,没埋点- 组件被卸载(用户快速返回)→
add还没 await 完,组件就没了,没埋点
而 AI 写的代码,把 track('add_compare') 放在了"所有可能路径都成功"的假设下。
我顺藤摸瓜,把五个事件全排查了一遍。结果:
| 事件 | 实际触发场景数 | AI 埋点覆盖 | 漏埋场景 |
|---|---|---|---|
| 详情页曝光 | 1 | 1 | 0 |
| 加入对比 | 4(未登录/成功/失败/卸载) | 1 | 3 |
| 点击分享 | 5(弹窗打开/选择平台/分享成功/取消/失败) | 1 | 4 |
| 付费弹窗展示 | 3(正常/重试/被拦截) | 1 | 2 |
| 付费成功 | 1 | 1 | 0 |
总触发场景 14 个,AI 埋点覆盖 5 个,覆盖率 35.7%。
这就是为什么运营拿到的是 1.2 万 / 800 / 200 的离谱数据。
为什么 AI 会犯这个错
事后我想了很久,AI 写埋点为什么会系统性漏埋。
根本原因是:AI 写代码的逻辑是"从入口到出口"线性扫描,它默认所有代码路径都是"直走到底"的。但埋点的本质不是"写代码",是"覆盖所有可能路径"。
具体来说,AI 漏掉的场景有这么几类:
1. 异步路径中的提前 return
typescript
async handleXxx() {
const result = await this.check() // 可能 reject
if (!result.ok) return // 提前 return,AI 不会在这里埋点
// ...主流程
this.track() // AI 只会在这里埋
}
2. 错误分支的 catch
typescript
try {
await this.doSomething()
this.trackSuccess()
} catch (e) {
this.handleError()
// AI 不会在这里埋 trackFail
}
3. 组件生命周期中的提前卸载
typescript
aboutToDisappear() {
// 用户在异步操作完成前返回了
// AI 完全忽略这个时机
}
4. 用户主动取消的分支
typescript
handleShare() {
this.dialog.open()
this.dialog.onDismiss((action) => {
if (action === 'cancel') return // 用户取消,AI 不会埋 trackCancel
this.trackShare(action)
})
}
5. 重复触发时的去重逻辑
typescript
// 按钮防抖逻辑,AI 不知道这是为了去重,会重复埋
private lastClickTime = 0
handleClick() {
if (Date.now() - this.lastClickTime < 500) return
this.lastClickTime = Date.now()
this.track()
}
这五类场景,AI 一个都没考虑。它给你的是"理想路径的完美埋点",不是"真实路径的完整埋点"。
我的修复方案:把 AI 代码全删了,重写 35 行
最后我把 AI 生成的 200 行代码全删了。改回了我自己写的 35 行"丑但全"的埋点:
typescript
// 手写版埋点:覆盖率第一,可读性其次
export class EventTracker {
// 通用打点:所有可能路径都埋
static async trackWithGuard<T>(
eventName: string,
fn: () => Promise<T>,
errorEvent: string,
params?: Record<string, any>
): Promise<T | null> {
const startTime = Date.now()
try {
const result = await fn()
// 主路径成功埋点
HttpClient.post('/track', {
event: eventName,
status: 'success',
duration: Date.now() - startTime,
...params
}).catch(() => {}) // 埋点失败不影响主流程
return result
} catch (e) {
// 错误分支埋点
HttpClient.post('/track', {
event: errorEvent,
status: 'error',
error: String(e),
...params
}).catch(() => {})
return null
}
}
}
// 业务侧使用(以"加入对比"为例)
async handleAddCompare() {
const result = await EventTracker.trackWithGuard(
'add_compare', // 成功事件
'add_compare_fail', // 失败事件
async () => {
const user = await this.checkLogin()
if (!user) {
router.pushUrl({ url: 'pages/Login' })
return null // 未登录不算"加入对比"成功
}
await this.compareService.add(this.itemId)
promptAction.showToast({ message: '已加入对比' })
return true
},
{ itemId: this.itemId } // 公共参数
)
if (result === null) {
// 这里不用再埋点,trackWithGuard 内部已经处理
return
}
}
这套写法看着不如 AI 给的"漂亮",但它的优势是:
- 强制把成功/失败/取消三类路径都埋上 ,通过
try-catch和return null强制业务方走完整流程 - 埋点失败不影响主流程 ,因为
.catch(() => {})吞掉了网络错误 - 统一参数注入 ,避免漏传
itemId之类的上下文
我把这套改完,五个事件全部重新埋了一遍。两周后运营拉数据,14 个触发场景全部覆盖,覆盖率 100%。
反思:AI 适合写什么,不适合写什么
这次踩坑之后,我重新整理了一下 AI 在前端代码里的适用边界:
AI 适合写的:
- 工具函数(纯函数、输入输出明确)
- 组件样板(CRUD 列表、表单弹窗)
- 类型定义(TypeScript interface)
- 单测代码(AI 写测试用例反而很准)
AI 不适合写的:
- 埋点(覆盖率敏感) ← 本次的教训
- 状态管理(异步分支多)
- 错误处理(容易漏分支)
- 安全相关代码(鉴权、加密)
- 性能关键路径(容易写出"正确但慢"的代码)
判断标准其实就一句话:如果这段代码的"完整覆盖"比"优雅"更重要,就别让 AI 写。
埋点就是典型的"覆盖率即生命"的代码------漏埋一个事件,运营拉数据就少一块,可能影响一个产品决策。
而 AI 给你的是"看起来完整"的代码,反而比"明显不完整"的代码更危险,因为你会本能地信任它。
写在最后
我后来跟同事复盘这次经历,他说"那以后埋点就全手写呗"。我倒觉得不是这么绝对------AI 不是不能写埋点,是要换一种用法。
我现在的工作流是:
- AI 先帮我生成"理想路径"的埋点代码(主流程)
- 我自己手动补全所有边缘场景(提前 return、catch、卸载、取消)
- 跑一个两周的影子期,用真实数据比对覆盖率
- 漏埋的就补上
AI 在第一步提效明显(以前写 200 行埋点要半天,现在 5 分钟),但第二步第三步必须人来。
这次复盘之后,我把该应用所有埋点代码都过了一遍------果然,AI 写的另外三个模块也漏埋了若干个边缘场景。改了之后,运营那边的数据终于对得上了。
如果你也在用 AI 写埋点,建议第一周就拉真实数据比对覆盖率,别等一个月才发现问题。
关于作者:斌哥,10+ 年软件开发老兵,软件设计师、注册人工智能工程师、agent 工程师,日常折腾鸿蒙 ArkTS 北向开发和 Web 前端,最近在用 AI 写一些自己的小项目。偶尔在 CSDN 分享鸿蒙和 AI 方向的文章。雷达鸭(华为应用市场可下载的鸿蒙版)就是用这套 AI 协作工作流做的真实产品。
本文遵循 MIT 协议,转载请注明出处。