feat(analytics): add behavior and payment funnel tracking

This commit is contained in:
2026-07-14 16:54:13 +08:00
parent ca55723e48
commit 81d6489978
70 changed files with 1576 additions and 81 deletions
@@ -1,6 +1,9 @@
"use client";
import { useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth";
import { behaviorAnalytics } from "@/lib/analytics";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
import type { ChatUpgradeReason } from "@/stores/chat/chat-state";
@@ -33,6 +36,27 @@ export function useChatMessageLimitBanner({
const navigator = useAppNavigator();
const isVip = useUserSelector((state) => state.context.isVip);
const view = getInsufficientCreditsMessageLimitView(loginStatus);
const visible = shouldShowMessageLimitBanner({
upgradePromptVisible,
upgradeReason,
});
const trackedVisibleRef = useRef(false);
useEffect(() => {
if (!visible) {
trackedVisibleRef.current = false;
return;
}
if (trackedVisibleRef.current) return;
trackedVisibleRef.current = true;
behaviorAnalytics.paywallShown(
{
entryPoint: "chat_input",
triggerReason: "insufficient_credits",
},
{ isVip },
);
}, [isVip, visible]);
function unlock(): void {
if (view.action === "auth") {
@@ -43,15 +67,16 @@ export function useChatMessageLimitBanner({
navigator.openSubscription({
type: getInsufficientCreditsSubscriptionType(isVip),
returnTo: "chat",
analytics: {
entryPoint: "chat_input",
triggerReason: "insufficient_credits",
},
});
}
return {
...view,
visible: shouldShowMessageLimitBanner({
upgradePromptVisible,
upgradeReason,
}),
visible,
unlock,
};
}
@@ -1,9 +1,10 @@
"use client";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { shallowEqual } from "@xstate/react";
import type { ChatLockType } from "@/data/schemas/chat";
import { behaviorAnalytics } from "@/lib/analytics";
import {
consumePendingChatUnlock,
peekPendingChatUnlock,
@@ -60,6 +61,7 @@ export function useChatUnlockNavigationFlow({
}: UseChatUnlockNavigationFlowInput): UseChatUnlockNavigationFlowOutput {
const navigator = useAppNavigator();
const isVip = useUserSelector((state) => state.context.isVip);
const trackedPaywallRef = useRef<string | null>(null);
const chatState = useChatSelector(
(state) => ({
historyLoaded: state.context.historyLoaded,
@@ -77,6 +79,22 @@ export function useChatUnlockNavigationFlow({
enabled,
});
useEffect(() => {
if (!unlockPaywallRequest) return;
const requestKey = `${unlockPaywallRequest.displayMessageId}:${unlockPaywallRequest.clientLockId ?? ""}`;
if (trackedPaywallRef.current === requestKey) return;
trackedPaywallRef.current = requestKey;
behaviorAnalytics.paywallShown(
{
entryPoint: "chat_unlock",
triggerReason: unlockPaywallRequest.promotion
? "ad_landing"
: "insufficient_credits",
},
{ isVip },
);
}, [isVip, unlockPaywallRequest]);
useEffect(() => {
if (!enabled) return;
if (!chatState.historyLoaded || !navigator.isAuthenticatedUser) return;
@@ -172,6 +190,12 @@ export function useChatUnlockNavigationFlow({
promotion: unlockPaywallRequest.promotion,
returnUrl,
type: getInsufficientCreditsSubscriptionType(isVip),
analytics: {
entryPoint: "chat_unlock",
triggerReason: unlockPaywallRequest.promotion
? "ad_landing"
: "insufficient_credits",
},
});
}
@@ -139,7 +139,14 @@ export function useFirstRechargeOfferBanner({
}
function claim(): void {
navigator.openSubscription({ type: "vip", returnTo: "chat" });
navigator.openSubscription({
type: "vip",
returnTo: "chat",
analytics: {
entryPoint: "chat_offer_banner",
triggerReason: "vip_cta",
},
});
}
return {