# 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 (
    <>
      <Script id="cozsweet-analytics-config" strategy="beforeInteractive">
        {`window.COZSWEET_ANALYTICS_ENDPOINT=${JSON.stringify(
          `${apiBase}/api/behavior/events`,
        )}`}
      </Script>
      <Script
        src={`${apiBase}/js/cozsweet-payment-analytics.js`}
        strategy="afterInteractive"
      />
    </>
  )
}
```

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
<button data-analytics-key="chat.send_message">Send</button>

<button data-analytics-key="recharge.open" data-analytics-label="Open recharge">
  Recharge
</button>

<div
  role="button"
  data-analytics-key="private_album.unlock"
  data-analytics-label="Unlock private album"
  onClick={unlockAlbum}
/>
```

纯 `div onClick` 如果没有 `role="button"`，必须添加 `data-analytics-key`，否则浏览器无法可靠识别它是按钮。

不需要统计的区域：

```tsx
<div data-analytics-ignore>
  {/* 此节点及其子节点不自动记录点击 */}
</div>
```

程序触发、没有真实 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>  // 有登录 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 是符合当前数据事实的。
