diff --git a/docs/backend/FRONTEND_COZSWEET_BEHAVIOR_ANALYTICS.md b/docs/backend/FRONTEND_COZSWEET_BEHAVIOR_ANALYTICS.md new file mode 100644 index 00000000..c3bd35aa --- /dev/null +++ b/docs/backend/FRONTEND_COZSWEET_BEHAVIOR_ANALYTICS.md @@ -0,0 +1,267 @@ +# 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 ( + <> + + + {/* NextAuth SessionProvider:为所有客户端组件提供 useSession() 上下文 */} diff --git a/src/app/private-room/components/__tests__/private-album-card.test.tsx b/src/app/private-room/components/__tests__/private-album-card.test.tsx index c012dcad..915ba8d6 100644 --- a/src/app/private-room/components/__tests__/private-album-card.test.tsx +++ b/src/app/private-room/components/__tests__/private-album-card.test.tsx @@ -61,6 +61,9 @@ describe("PrivateAlbumCard", () => { expect(html).toContain("8 photos"); expect(html).toContain("A quiet morning by the water."); expect(html).toContain('aria-label="Open 8 private album photos"'); + expect(html).toContain( + 'data-analytics-key="private_album.open_gallery"', + ); }); it("uses the backend first image as the locked cover", () => { @@ -80,6 +83,7 @@ describe("PrivateAlbumCard", () => { ); expect(html).toContain("Only for you."); expect(html).toContain("320 credits"); + expect(html).toContain('data-analytics-key="private_album.unlock"'); }); it("renders an empty cover when an unlocked album has no image", () => { diff --git a/src/app/private-room/components/private-album-card.tsx b/src/app/private-room/components/private-album-card.tsx index d4188447..257c7831 100644 --- a/src/app/private-room/components/private-album-card.tsx +++ b/src/app/private-room/components/private-album-card.tsx @@ -61,6 +61,8 @@ export function PrivateAlbumCard({ {isLocked ? ( ) : null} - diff --git a/src/app/sidebar/components/user-header.tsx b/src/app/sidebar/components/user-header.tsx index ac39a1d0..168137fe 100644 --- a/src/app/sidebar/components/user-header.tsx +++ b/src/app/sidebar/components/user-header.tsx @@ -80,6 +80,8 @@ export function UserHeader({ {state === "guest" && (