From 81d6489978027000eb2780d9053eeee9824612da Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 14 Jul 2026 16:54:13 +0800 Subject: [PATCH] feat(analytics): add behavior and payment funnel tracking --- .../FRONTEND_COZSWEET_BEHAVIOR_ANALYTICS.md | 267 +++++++++++++++++ env-example/.env.production.example | 4 +- .../__tests__/tailwind-components.test.tsx | 8 +- src/app/_components/back-button.tsx | 12 +- .../__tests__/tailwind-components.test.tsx | 4 + src/app/_components/core/app-bottom-nav.tsx | 4 + .../use-payment-plan-analytics.test.tsx | 82 ++++++ src/app/_hooks/use-payment-launch-flow.ts | 64 ++++- src/app/_hooks/use-payment-plan-analytics.ts | 29 ++ .../__tests__/tailwind-components.test.tsx | 3 + src/app/auth/components/auth-email-panel.tsx | 5 + .../auth/components/auth-facebook-panel.tsx | 4 + .../components/auth-other-options-dialog.tsx | 6 + src/app/auth/components/auth-panel.tsx | 2 +- src/app/auth/components/email-login-form.tsx | 7 +- .../auth/components/email-register-form.tsx | 7 +- .../__tests__/tailwind-components.test.tsx | 5 + .../chat/components/browser-hint-overlay.tsx | 2 + src/app/chat/components/chat-header.tsx | 5 + .../chat-insufficient-credits-banner.tsx | 2 + src/app/chat/components/chat-send-button.tsx | 2 + .../first-recharge-offer-banner.tsx | 2 + .../components/fullscreen-image-viewer.tsx | 2 + .../chat/components/history-unlock-dialog.tsx | 2 + .../insufficient-credits-dialog.tsx | 2 + .../components/locked-image-message-card.tsx | 2 + .../chat/components/private-message-card.tsx | 2 + src/app/chat/components/voice-bubble.tsx | 2 + .../hooks/use-chat-message-limit-banner.ts | 33 ++- .../hooks/use-chat-unlock-navigation-flow.ts | 26 +- .../hooks/use-first-recharge-offer-banner.ts | 9 +- src/app/layout.tsx | 9 + .../__tests__/private-album-card.test.tsx | 4 + .../components/private-album-card.tsx | 4 + .../components/unlock-confirm-dialog.tsx | 2 + src/app/private-room/private-room-screen.tsx | 10 +- src/app/private-room/use-private-room-flow.ts | 13 +- .../components/sidebar-wallet-card.tsx | 12 +- src/app/sidebar/components/user-header.tsx | 2 + src/app/sidebar/sidebar-screen.tsx | 27 +- .../__tests__/tailwind-components.test.tsx | 1 + src/app/splash/components/splash-button.tsx | 2 + .../components/splash-latest-message.tsx | 2 + src/app/splash/splash-screen.tsx | 5 - .../__tests__/tailwind-components.test.tsx | 3 + .../subscription-checkout-button.tsx | 4 + .../subscription-coins-offer-section.tsx | 2 + .../subscription-payment-method.tsx | 2 + .../subscription-vip-offer-section.tsx | 2 + .../subscription/subscription-page-client.tsx | 11 + src/app/subscription/subscription-screen.tsx | 43 ++- src/app/tip/tip-checkout-button.tsx | 4 + src/app/tip/tip-screen.tsx | 16 ++ .../__tests__/behavior_analytics.test.ts | 136 +++++++++ .../payment_analytics_context.test.ts | 39 +++ src/lib/analytics/behavior_analytics.ts | 268 ++++++++++++++++++ .../analytics/behavior_analytics_config.ts | 18 ++ src/lib/analytics/index.ts | 3 + .../analytics/payment_analytics_context.ts | 80 ++++++ .../payment/__tests__/payment_launch.test.ts | 21 +- src/lib/payment/payment_launch.ts | 9 +- src/providers/behavior-analytics-scripts.tsx | 15 + .../__tests__/navigation-resolver.test.ts | 21 ++ src/router/navigation-types.ts | 3 + src/router/routes.ts | 21 +- src/router/use-app-navigator.ts | 10 + .../payment/__tests__/payment-machine.test.ts | 67 +++++ src/stores/payment/payment-machine.helpers.ts | 1 + src/stores/payment/payment-machine.ts | 157 +++++++--- src/stores/payment/payment-state.ts | 2 + 70 files changed, 1576 insertions(+), 81 deletions(-) create mode 100644 docs/backend/FRONTEND_COZSWEET_BEHAVIOR_ANALYTICS.md create mode 100644 src/app/_hooks/__tests__/use-payment-plan-analytics.test.tsx create mode 100644 src/app/_hooks/use-payment-plan-analytics.ts create mode 100644 src/lib/analytics/__tests__/behavior_analytics.test.ts create mode 100644 src/lib/analytics/__tests__/payment_analytics_context.test.ts create mode 100644 src/lib/analytics/behavior_analytics.ts create mode 100644 src/lib/analytics/behavior_analytics_config.ts create mode 100644 src/lib/analytics/index.ts create mode 100644 src/lib/analytics/payment_analytics_context.ts create mode 100644 src/providers/behavior-analytics-scripts.tsx 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" && (