# Cozsweet 全站行为埋点前端接入
> 发布状态(2026-07-14):自动页面/按钮采集 SDK 已部署 `pro` 预发,生产仍是旧版支付专项 SDK。前端应先接 `pro` 完成联调;生产自动采集要等 unified 下一次生产镜像发布后再启用。
## 1. 目标
前端接入一次后,自动统计:
- 用户访问了哪个页面。
- Next.js 站内路由切换。
- 所有原生按钮、链接和带 `role="button"` 的控件点击。
- 带 `data-analytics-key` 的自定义可点击组件。
支付漏斗另外上报明确业务事件,统计付费墙原因、套餐曝光、套餐点击、创建订单、打开支付页和最终支付结果。
## 2. 环境地址
| 环境 | SDK | 事件接口 |
| --- | --- | --- |
| pro 预发 | `https://proapi.banlv-ai.com/js/cozsweet-payment-analytics.js` | `https://proapi.banlv-ai.com/api/behavior/events` |
| production 生产 | `https://api.banlv-ai.com/js/cozsweet-payment-analytics.js` | `https://api.banlv-ai.com/api/behavior/events` |
## 3. Next.js 根布局接入
只在根布局加载一次。预发必须先指定预发事件地址,否则 SDK 默认发送到生产。
```tsx
import Script from 'next/script'
export function BehaviorAnalyticsScripts() {
const isPro = process.env.NEXT_PUBLIC_APP_ENV === 'pro'
const apiBase = isPro
? 'https://proapi.banlv-ai.com'
: 'https://api.banlv-ai.com'
return (
<>
>
)
}
```
SDK 会自动生成并持久化:
- `anonymousId`:未登录用户标识。
- `sessionId`:30 分钟会话标识。
- `pagePath/pageTitle/referrer`。
- `siteHost/pageUrl/UTM`。
登录成功或恢复会话后设置 Token,让事件同时绑定 `userId`:
```ts
window.CozsweetPaymentAnalytics?.setToken(accessToken)
```
## 4. 所有按钮点击
以下元素会自动上报 `eventType=element_click`,前端不需要在每个 `onClick` 里手工请求接口:
- `button`
- `a[href]`
- `[role="button"]`
- `[role="link"]`
- `input[type="button|submit|reset"]`
- `[data-analytics-key]`
重要按钮必须提供稳定且不随语言变化的 `data-analytics-key`:
```tsx
```
纯 `div onClick` 如果没有 `role="button"`,必须添加 `data-analytics-key`,否则浏览器无法可靠识别它是按钮。
不需要统计的区域:
```tsx
{/* 此节点及其子节点不自动记录点击 */}
```
程序触发、没有真实 DOM 点击时可以手工记录:
```ts
window.CozsweetPaymentAnalytics?.elementClick(
'chat.retry_generation',
'Retry generation',
{ messageId },
)
```
不要把聊天正文、邮箱、手机号、Token 或支付参数放进 `data-analytics-key`、标签或 metadata。
## 5. 支付漏斗必须手工上报
通用按钮点击只能知道“点了某个按钮”,不能知道套餐价格、订单 ID 和进入充值页的原因。以下事件必须在支付业务代码对应步骤调用。
```ts
const analytics = window.CozsweetPaymentAnalytics
analytics?.paywallShown('daily_chat_limit', {
entryPoint: 'chat_input',
isVip: false,
})
analytics?.rechargeModalOpen('chat_input', 'daily_chat_limit')
plans.forEach((plan, index) => {
analytics?.planImpression(plan, index + 1)
})
analytics?.planClick(plan, {
entryPoint: 'paywall_modal',
triggerReason: 'daily_chat_limit',
})
analytics?.createOrderStart(plan, payChannel)
try {
const response = await createOrder({
planId: plan.planId,
payChannel,
autoRenew,
})
const data = response.data
const orderId = data.orderId
const checkoutUrl = analytics?.extractCheckoutUrl(data)
analytics?.createOrderSuccess(plan, orderId, payChannel)
if (checkoutUrl) {
analytics?.checkoutOpened(orderId, plan, payChannel, checkoutUrl)
window.location.href = checkoutUrl
} else {
analytics?.checkoutFailed(
orderId,
plan,
'missing_checkout_url',
payChannel,
)
}
} catch (error) {
analytics?.createOrderFailed(
plan,
error instanceof Error ? error.message : 'create_order_error',
payChannel,
)
}
```
轮询订单状态:
```ts
analytics?.statusPollStart(orderId, plan)
analytics?.statusPollUpdate(orderId, data.status, plan, {
expiresAt: data.expiresAt,
expiresInSeconds: data.expiresInSeconds,
})
// 前端主动放弃轮询且订单仍未进入终态时调用
analytics?.statusPollTimeout(orderId, plan)
```
## 6. 固定事件名
| 步骤 | 事件名 | 必要 metadata |
| --- | --- | --- |
| 触发付费墙 | `paywall_shown` | `triggerReason`, `entryPoint` |
| 打开充值页 | `recharge_modal_open` | `triggerReason`, `entryPoint` |
| 套餐曝光 | `payment_plan_impression` | `planId`, `amountCents`, `currency`, `position` |
| 点击套餐 | `payment_plan_click` | `planId`, `amountCents`, `currency` |
| 开始建单 | `payment_create_order_start` | `planId`, `payChannel` |
| 建单成功 | `payment_create_order_success` | `planId`, `orderId`, `payChannel` |
| 建单失败 | `payment_create_order_failed` | `planId`, `reason`, `payChannel` |
| 打开支付页 | `payment_checkout_opened` | `planId`, `orderId`, `payChannel` |
| 支付页缺失/打开失败 | `payment_checkout_failed` | `orderId`, `reason` |
| 支付状态 | `payment_status_paid/failed/expired` | `orderId`, `status` |
| 轮询超时 | `payment_status_poll_timeout` | `orderId` |
`triggerReason` 使用固定值:
- `daily_chat_limit`
- `private_topic_limit`
- `insufficient_credits`
- `sidebar_recharge`
- `vip_cta`
- `ad_landing`
- `manual_recharge`
- `unknown`
不要继续使用已经废弃的 `weekly_chat_limit` 作为新事件原因;当前免费聊天口径是每日额度。
## 7. 请求格式
SDK 最终批量发送:
```http
POST /api/behavior/events
Content-Type: application/json
Authorization: Bearer // 有登录 Token 时携带
```
```json
{
"events": [
{
"eventType": "element_click",
"eventName": "element_click",
"anonymousId": "browser-uuid",
"sessionId": "session-uuid",
"pagePath": "/chat",
"pageTitle": "Chat",
"elementKey": "recharge.open",
"elementText": "Recharge",
"elementTag": "button",
"metadata": {
"siteHost": "cozsweet.com",
"pageUrl": "https://cozsweet.com/chat"
}
}
]
}
```
成功响应:
```json
{"ok": true, "inserted": 1}
```
埋点失败不能阻塞聊天、解锁或支付流程。SDK 已吞掉网络异常,业务代码不要 `await` 埋点后再继续支付。
## 8. 联调验收
1. 打开浏览器 Network,筛选 `behavior/events`。
2. 首次打开页面应看到 `page_view`。
3. Next.js 路由切换后应再次看到新 `pagePath` 的 `page_view`。
4. 点击任意按钮应看到 `element_click` 和稳定 `elementKey`。
5. 从付费墙点击一个套餐,依次检查专用支付事件及 `planId`。
6. 建单成功后必须看到同一 `orderId` 出现在建单、支付页和状态事件中。
7. 接口响应必须为 `ok=true` 且 `inserted>0`。
8. Manager 选择同一日期范围,确认数据出现;统计不是实时推送时,刷新页面即可。
## 9. 当前线上为 0 的原因
截至 2026-07-14,线上 `cozsweet.com` 页面及加载的前端 JS 中没有找到本 SDK,也没有找到支付漏斗事件代码。数据库近期大部分 `element_click/page_view` 来自 Manager 后台自己的追踪,而不是 Cozsweet 用户端。后端只能自动记录建单开始/成功/失败,无法替前端知道付费墙是否展示、套餐是否曝光、用户点击了哪个套餐、支付页是否真正打开,因此这些步骤显示 0 是符合当前数据事实的。