Correct product analytics cohorts and reporting
This commit is contained in:
@@ -187,6 +187,10 @@ export interface TrendPoint {
|
||||
}
|
||||
|
||||
export interface Overview {
|
||||
period: {
|
||||
from: string;
|
||||
until: string;
|
||||
};
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
newUsers: number;
|
||||
@@ -210,6 +214,10 @@ export interface ReferralRankingItem {
|
||||
}
|
||||
|
||||
export interface ReferralOverview {
|
||||
period: {
|
||||
from: string;
|
||||
until: string;
|
||||
};
|
||||
pendingBindings: number;
|
||||
ineligibleBindings: number;
|
||||
funnel: FunnelStep[];
|
||||
@@ -244,6 +252,12 @@ export interface AnalyticsFeatureUsage {
|
||||
successes: number;
|
||||
}
|
||||
|
||||
export interface AnalyticsLatencyBucket {
|
||||
bucket: string;
|
||||
successful: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export interface ProductAnalyticsOverview {
|
||||
period: {
|
||||
from: string;
|
||||
@@ -282,6 +296,8 @@ export interface ProductAnalyticsOverview {
|
||||
conversion7d: AnalyticsRate;
|
||||
conversion30d: AnalyticsRate;
|
||||
repeatPurchaseRate: AnalyticsRate;
|
||||
purchaseFunnel: FunnelStep[];
|
||||
cancelledUsers: number;
|
||||
};
|
||||
growthFunnel: FunnelStep[];
|
||||
retention: AnalyticsCohort[];
|
||||
@@ -305,11 +321,16 @@ export interface ProductAnalyticsOverview {
|
||||
mixedLanguageSessions: number;
|
||||
otherOnlySessions: number;
|
||||
};
|
||||
referralSignals: {
|
||||
shared: number;
|
||||
opened: number;
|
||||
};
|
||||
referralFunnel: FunnelStep[];
|
||||
guardrails: {
|
||||
clientAiSuccessRate: AnalyticsRate;
|
||||
managedSuccessRate: AnalyticsRate;
|
||||
creditBlockedUsers: number;
|
||||
latencyBuckets: AnalyticsLatencyBucket[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ type ChartTone = "primary" | "violet" | "success" | "warning";
|
||||
export interface ComparisonBarItem {
|
||||
id?: string;
|
||||
label: string;
|
||||
value: number;
|
||||
secondaryValue?: number;
|
||||
value: number | null;
|
||||
secondaryValue?: number | null;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ComparisonBarChart({
|
||||
}
|
||||
|
||||
const maximum = Math.max(
|
||||
...items.flatMap((item) => [item.value, item.secondaryValue ?? 0]),
|
||||
...items.flatMap((item) => [item.value ?? 0, item.secondaryValue ?? 0]),
|
||||
1,
|
||||
);
|
||||
const legend = [
|
||||
@@ -56,15 +56,17 @@ export function ComparisonBarChart({
|
||||
maximum={maximum}
|
||||
tone={primaryTone}
|
||||
value={item.value}
|
||||
valueLabel={valueFormatter(item.value)}
|
||||
valueLabel={item.value == null ? "—" : valueFormatter(item.value)}
|
||||
/>
|
||||
{secondaryLabel != null && item.secondaryValue != null ? (
|
||||
{secondaryLabel != null && item.secondaryValue !== undefined ? (
|
||||
<Bar
|
||||
label={`${item.label} ${secondaryLabel}`}
|
||||
maximum={maximum}
|
||||
tone={secondaryTone}
|
||||
value={item.secondaryValue}
|
||||
valueLabel={valueFormatter(item.secondaryValue)}
|
||||
valueLabel={
|
||||
item.secondaryValue == null ? "—" : valueFormatter(item.secondaryValue)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -84,7 +86,7 @@ function Bar({
|
||||
label: string;
|
||||
maximum: number;
|
||||
tone: ChartTone;
|
||||
value: number;
|
||||
value: number | null;
|
||||
valueLabel: string;
|
||||
}) {
|
||||
return (
|
||||
@@ -92,8 +94,8 @@ function Bar({
|
||||
<progress
|
||||
className={`bar-progress bar-progress--${tone} block h-2.5 min-w-0 flex-1 overflow-hidden rounded-full`}
|
||||
max={maximum}
|
||||
value={value}
|
||||
aria-label={`${label}:${valueLabel}`}
|
||||
value={value ?? 0}
|
||||
aria-label={value == null ? `${label}:暂无数据` : `${label}:${valueLabel}`}
|
||||
/>
|
||||
<strong className="w-16 shrink-0 text-right text-xs font-semibold tabular-nums text-foreground">
|
||||
{valueLabel}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function FunnelChart({
|
||||
return <p className="p-8 text-center text-sm text-muted">{emptyText}</p>;
|
||||
}
|
||||
|
||||
const maximum = Math.max(steps[0]?.count ?? 0, ...steps.map((step) => step.count), 1);
|
||||
const maximum = Math.max(steps[0]?.count ?? 0, 1);
|
||||
|
||||
return (
|
||||
<div className="space-y-5 p-5 sm:p-6">
|
||||
|
||||
@@ -7,11 +7,11 @@ export function RadialMetric({
|
||||
tone = "primary",
|
||||
}: {
|
||||
label: string;
|
||||
percent: number;
|
||||
percent: number | null;
|
||||
detail?: string;
|
||||
tone?: RadialTone;
|
||||
}) {
|
||||
const value = Math.min(Math.max(percent, 0), 100);
|
||||
const value = percent == null ? null : Math.min(Math.max(percent, 0), 100);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-5">
|
||||
@@ -19,7 +19,7 @@ export function RadialMetric({
|
||||
className="size-28 shrink-0"
|
||||
viewBox="0 0 120 120"
|
||||
role="img"
|
||||
aria-label={`${label}:${value.toFixed(1)}%`}
|
||||
aria-label={value == null ? `${label}:暂无数据` : `${label}:${value.toFixed(1)}%`}
|
||||
>
|
||||
<circle className="radial-track" cx="60" cy="60" r="48" pathLength="100" />
|
||||
<circle
|
||||
@@ -28,11 +28,11 @@ export function RadialMetric({
|
||||
cy="60"
|
||||
r="48"
|
||||
pathLength="100"
|
||||
strokeDasharray={`${value} ${100 - value}`}
|
||||
strokeDasharray={`${value ?? 0} ${100 - (value ?? 0)}`}
|
||||
transform="rotate(-90 60 60)"
|
||||
/>
|
||||
<text className="radial-label" x="60" y="65" textAnchor="middle">
|
||||
{Math.round(value)}%
|
||||
{value == null ? "—" : `${Math.round(value)}%`}
|
||||
</text>
|
||||
</svg>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export function PeriodCaption({
|
||||
from,
|
||||
until,
|
||||
}: {
|
||||
from: string;
|
||||
until: string;
|
||||
}) {
|
||||
return (
|
||||
<p className="text-xs text-muted" aria-label="实际统计周期">
|
||||
统计周期:{utcLabel(from)} 至 {utcLabel(until)}(UTC,包含今日未完整数据)
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function utcLabel(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "—";
|
||||
return date.toISOString().replace("T", " ").slice(0, 16);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ComparisonBarChart } from "../../components/charts/comparison-bar-chart
|
||||
import { FunnelChart } from "../../components/charts/funnel-chart";
|
||||
import { FilterControl, ToggleFilter } from "../../components/filter-control";
|
||||
import { Card, ErrorState, LoadingState, PageHeader, StatCard } from "../../components/primitives";
|
||||
import { PeriodCaption } from "../../components/period-caption";
|
||||
import { RangeControl } from "../../components/range-control";
|
||||
import { SortControl } from "../../components/sort-control";
|
||||
import { formatNumber } from "../../lib/format";
|
||||
@@ -110,7 +111,12 @@ export function AnalyticsPage() {
|
||||
eyebrow="CEO Dashboard"
|
||||
title="产品增长与留存"
|
||||
description="围绕成功使用 AI 的核心价值事件,观察增长质量、留存、消耗与付费。"
|
||||
actions={<RangeControl value={range} onChange={setRange} />}
|
||||
actions={
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<RangeControl value={range} onChange={setRange} />
|
||||
<PeriodCaption from={data.period.from} until={data.period.until} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4" aria-label="北极星指标">
|
||||
@@ -151,7 +157,7 @@ export function AnalyticsPage() {
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader
|
||||
title="增长激活漏斗"
|
||||
description="首次启动到首次 AI 成功,显示逐步及累计转化"
|
||||
description="同一批已完成 24 小时观察的新安装,所有步骤必须在首次启动后 24 小时内完成"
|
||||
icon={Target}
|
||||
/>
|
||||
<FunnelChart steps={data.growthFunnel} />
|
||||
@@ -241,7 +247,7 @@ export function AnalyticsPage() {
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader
|
||||
title="活跃与消耗"
|
||||
description="托管调用以服务端结算为准"
|
||||
description="DAU / WAU / MAU 为截至统计截止时刻的滚动 1 / 7 / 30 日 AI 价值活跃用户"
|
||||
icon={Activity}
|
||||
/>
|
||||
<ComparisonBarChart
|
||||
@@ -335,20 +341,24 @@ export function AnalyticsPage() {
|
||||
|
||||
<section className="grid gap-6 xl:grid-cols-3">
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader title="付费转化" description="StoreKit 已验证交易" icon={BadgeDollarSign} />
|
||||
<SectionHeader
|
||||
title="付费转化"
|
||||
description="所选周期内完成 7 / 30 天观察窗的注册 cohort;购买以 StoreKit 验证为准"
|
||||
icon={BadgeDollarSign}
|
||||
/>
|
||||
<ComparisonBarChart
|
||||
items={[
|
||||
{
|
||||
label: "7 天免费转付费",
|
||||
value: data.monetization.conversion7d.percent ?? 0,
|
||||
value: data.monetization.conversion7d.percent ?? null,
|
||||
},
|
||||
{
|
||||
label: "30 天免费转付费",
|
||||
value: data.monetization.conversion30d.percent ?? 0,
|
||||
value: data.monetization.conversion30d.percent ?? null,
|
||||
},
|
||||
{
|
||||
label: "复购率",
|
||||
value: data.monetization.repeatPurchaseRate.percent ?? 0,
|
||||
value: data.monetization.repeatPurchaseRate.percent ?? null,
|
||||
},
|
||||
]}
|
||||
primaryLabel="转化率"
|
||||
@@ -367,9 +377,15 @@ export function AnalyticsPage() {
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader
|
||||
title="推荐增长漏斗"
|
||||
description="分享、打开、绑定、激活与奖励"
|
||||
description="严格绑定 cohort;分享和打开仅作为独立方向信号"
|
||||
icon={Target}
|
||||
/>
|
||||
<MetricRows
|
||||
rows={[
|
||||
["客户端分享信号", formatNumber(data.referralSignals.shared)],
|
||||
["邀请打开信号", formatNumber(data.referralSignals.opened)],
|
||||
]}
|
||||
/>
|
||||
<FunnelChart steps={data.referralFunnel} />
|
||||
</Card>
|
||||
|
||||
@@ -379,11 +395,11 @@ export function AnalyticsPage() {
|
||||
items={[
|
||||
{
|
||||
label: "客户端 AI",
|
||||
value: data.guardrails.clientAiSuccessRate.percent ?? 0,
|
||||
value: data.guardrails.clientAiSuccessRate.percent ?? null,
|
||||
},
|
||||
{
|
||||
label: "托管请求",
|
||||
value: data.guardrails.managedSuccessRate.percent ?? 0,
|
||||
value: data.guardrails.managedSuccessRate.percent ?? null,
|
||||
},
|
||||
]}
|
||||
primaryLabel="成功率"
|
||||
@@ -399,6 +415,44 @@ export function AnalyticsPage() {
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-6 xl:grid-cols-2">
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader
|
||||
title="购买意向漏斗"
|
||||
description="同一安装依次浏览、发起购买,并由 StoreKit 服务端验证"
|
||||
icon={BadgeDollarSign}
|
||||
/>
|
||||
<FunnelChart
|
||||
steps={data.monetization.purchaseFunnel}
|
||||
emptyText="客户端购买事件暂无样本"
|
||||
/>
|
||||
<MetricRows
|
||||
rows={[["取消购买用户", formatNumber(data.monetization.cancelledUsers)]]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader
|
||||
title="AI 终态延迟分布"
|
||||
description="按客户端白名单耗时桶聚合;不推算虚假的精确分位数"
|
||||
icon={Gauge}
|
||||
/>
|
||||
<ComparisonBarChart
|
||||
items={data.guardrails.latencyBuckets.map((item) => ({
|
||||
id: item.bucket,
|
||||
label: latencyBucketLabel(item.bucket),
|
||||
value: item.successful,
|
||||
secondaryValue: item.failed,
|
||||
}))}
|
||||
primaryLabel="成功"
|
||||
secondaryLabel="失败"
|
||||
primaryTone="success"
|
||||
secondaryTone="warning"
|
||||
emptyText="客户端 AI 终态事件暂无样本"
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<SectionHeader title="渠道质量" description="新增不是终点,重点比较 24 小时激活" icon={Users} />
|
||||
<ChartToolbar label="渠道质量筛选与排序">
|
||||
@@ -406,7 +460,7 @@ export function AnalyticsPage() {
|
||||
value={channelSort}
|
||||
order={channelOrder}
|
||||
options={[
|
||||
{ value: "installs", label: "新增安装" },
|
||||
{ value: "installs", label: "已完成观察安装" },
|
||||
{ value: "activated", label: "激活人数" },
|
||||
{ value: "rate", label: "激活率" },
|
||||
]}
|
||||
@@ -425,7 +479,7 @@ export function AnalyticsPage() {
|
||||
secondaryValue: channel.activated,
|
||||
hint: `激活率 ${rateLabel(channel.activationRate)}`,
|
||||
}))}
|
||||
primaryLabel="新增安装"
|
||||
primaryLabel="已完成 24h 观察安装"
|
||||
secondaryLabel="24 小时激活"
|
||||
emptyText="当前筛选条件下暂无渠道归因数据"
|
||||
/>
|
||||
@@ -535,3 +589,13 @@ function executionModeLabel(mode: string): string {
|
||||
BYOK: "BYOK",
|
||||
}[mode] ?? mode;
|
||||
}
|
||||
|
||||
function latencyBucketLabel(bucket: string): string {
|
||||
return {
|
||||
LT_1S: "< 1 秒",
|
||||
S1_TO_3: "1–3 秒",
|
||||
S3_TO_10: "3–10 秒",
|
||||
S10_TO_30: "10–30 秒",
|
||||
GTE_30S: "≥ 30 秒",
|
||||
}[bucket] ?? bucket;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RadialMetric } from "../../components/charts/radial-metric";
|
||||
import { TrendChart } from "../../components/charts/trend-chart";
|
||||
import { ToggleFilter } from "../../components/filter-control";
|
||||
import { Card, ErrorState, LoadingState, PageHeader, StatCard } from "../../components/primitives";
|
||||
import { PeriodCaption } from "../../components/period-caption";
|
||||
import { RangeControl } from "../../components/range-control";
|
||||
import { SortControl } from "../../components/sort-control";
|
||||
import { formatNumber, usageTypeLabel } from "../../lib/format";
|
||||
@@ -58,7 +59,7 @@ export function OverviewPage() {
|
||||
if (!data) return <LoadingState label="加载运营总览" />;
|
||||
|
||||
const activeRate =
|
||||
data.totalUsers > 0 ? Math.round((data.activeUsers / data.totalUsers) * 100) : 0;
|
||||
data.totalUsers > 0 ? Math.round((data.activeUsers / data.totalUsers) * 100) : null;
|
||||
const usageRequests = data.usage.reduce((sum, item) => sum + item.requests, 0);
|
||||
|
||||
return (
|
||||
@@ -66,8 +67,13 @@ export function OverviewPage() {
|
||||
<PageHeader
|
||||
eyebrow="核心指标"
|
||||
title="运营总览"
|
||||
description="聚合用户增长、活跃度与积分流转,快速识别业务变化。"
|
||||
actions={<RangeControl value={range} onChange={setRange} />}
|
||||
description="活跃用户指周期内成功使用 AI 或产生手动键盘输入的注册用户。"
|
||||
actions={
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<RangeControl value={range} onChange={setRange} />
|
||||
<PeriodCaption from={data.period.from} until={data.period.until} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4" aria-label="关键指标">
|
||||
@@ -80,7 +86,7 @@ export function OverviewPage() {
|
||||
<StatCard
|
||||
label="活跃用户"
|
||||
value={formatNumber(data.activeUsers)}
|
||||
hint={`活跃率 ${activeRate}%`}
|
||||
hint={`活跃率 ${activeRate == null ? "—" : `${activeRate}%`}`}
|
||||
icon={Activity}
|
||||
tone="success"
|
||||
/>
|
||||
@@ -105,7 +111,9 @@ export function OverviewPage() {
|
||||
<div className="mb-7 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-foreground">增长与消耗趋势</h2>
|
||||
<p className="mt-1 text-xs text-muted">按 UTC 日期统计,双指标独立缩放</p>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
按 UTC 日期统计,恰好覆盖所选日期数;今日数据尚未完整
|
||||
</p>
|
||||
</div>
|
||||
<ChartLegend
|
||||
items={[
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
PageHeader,
|
||||
StatCard,
|
||||
} from "../../components/primitives";
|
||||
import { PeriodCaption } from "../../components/period-caption";
|
||||
import { RangeControl } from "../../components/range-control";
|
||||
import { SortControl } from "../../components/sort-control";
|
||||
import { formatNumber } from "../../lib/format";
|
||||
@@ -115,7 +116,7 @@ export function ReferralsPage() {
|
||||
|
||||
const first = data.funnel.at(0)?.count ?? 0;
|
||||
const last = data.funnel.at(-1)?.count ?? 0;
|
||||
const conversion = first > 0 ? Math.round((last / first) * 100) : 0;
|
||||
const conversion = first > 0 ? Math.round((last / first) * 100) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -123,7 +124,12 @@ export function ReferralsPage() {
|
||||
eyebrow="增长分析"
|
||||
title="裂变与排行"
|
||||
description="奖励以有效使用为前提,关注真实转化而不是单纯注册量。"
|
||||
actions={<RangeControl value={range} onChange={setRange} />}
|
||||
actions={
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<RangeControl value={range} onChange={setRange} />
|
||||
<PeriodCaption from={data.period.from} until={data.period.until} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 sm:grid-cols-3">
|
||||
@@ -143,7 +149,7 @@ export function ReferralsPage() {
|
||||
/>
|
||||
<StatCard
|
||||
label="漏斗转化率"
|
||||
value={`${conversion}%`}
|
||||
value={conversion == null ? "—" : `${conversion}%`}
|
||||
hint="首环节至最终有效使用"
|
||||
icon={TrendingUp}
|
||||
tone="success"
|
||||
@@ -184,7 +190,7 @@ export function ReferralsPage() {
|
||||
<div className="flex items-center justify-between border-b border-border p-5 sm:p-6">
|
||||
<div>
|
||||
<h2 className="text-base font-bold">裂变漏斗</h2>
|
||||
<p className="mt-1 text-xs text-muted">从分享触达到有效使用,逐层观察流失</p>
|
||||
<p className="mt-1 text-xs text-muted">同一批绑定用户从首次 AI 成功到完成奖励</p>
|
||||
</div>
|
||||
<span className="grid size-10 place-items-center rounded-2xl bg-primary-soft text-primary">
|
||||
<GitBranch className="size-4" aria-hidden />
|
||||
@@ -194,7 +200,7 @@ export function ReferralsPage() {
|
||||
<RadialMetric
|
||||
label="整体有效转化"
|
||||
percent={conversion}
|
||||
detail={`${formatNumber(first)} 个起点行为,最终形成 ${formatNumber(last)} 次有效使用`}
|
||||
detail={`${formatNumber(first)} 个绑定,最终形成 ${formatNumber(last)} 次奖励`}
|
||||
tone="success"
|
||||
/>
|
||||
</div>
|
||||
@@ -205,7 +211,9 @@ export function ReferralsPage() {
|
||||
<div className="flex items-center justify-between border-b border-border px-5 py-5 sm:px-6">
|
||||
<div>
|
||||
<h2 className="text-base font-bold">头部邀请贡献</h2>
|
||||
<p className="mt-1 text-xs text-muted">比较邀请总数与达到奖励条件的有效邀请</p>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
当前请求结果 Top 8;完整 {limit} 名见下方明细
|
||||
</p>
|
||||
</div>
|
||||
<span className="grid size-10 place-items-center rounded-2xl bg-warning-soft text-warning">
|
||||
<Award className="size-4" aria-hidden />
|
||||
|
||||
@@ -233,6 +233,10 @@ describe("React 管理页面", () => {
|
||||
expect(screen.getByText("中文活跃用户")).toBeTruthy();
|
||||
expect(screen.getByText("中英混合")).toBeTruthy();
|
||||
expect(screen.getByText("7 天免费转付费")).toBeTruthy();
|
||||
expect(screen.getByText("购买意向漏斗")).toBeTruthy();
|
||||
expect(screen.getByText("AI 终态延迟分布")).toBeTruthy();
|
||||
expect(screen.getByText("客户端分享信号")).toBeTruthy();
|
||||
expect(screen.getByLabelText("实际统计周期").textContent).toContain("包含今日未完整数据");
|
||||
});
|
||||
|
||||
it("产品图表可按执行模式筛选并按用户数稳定排序", async () => {
|
||||
@@ -318,6 +322,12 @@ function analyticsOverview(): ProductAnalyticsOverview {
|
||||
conversion7d: rate(8, 70, 11.4),
|
||||
conversion30d: rate(10, 50, 20),
|
||||
repeatPurchaseRate: rate(2, 10, 20),
|
||||
purchaseFunnel: [
|
||||
{ label: "浏览购买页", count: 30 },
|
||||
{ label: "发起购买", count: 15 },
|
||||
{ label: "StoreKit 验证完成", count: 10 },
|
||||
],
|
||||
cancelledUsers: 4,
|
||||
},
|
||||
growthFunnel: [
|
||||
{ label: "首次启动", count: 100 },
|
||||
@@ -353,14 +363,17 @@ function analyticsOverview(): ProductAnalyticsOverview {
|
||||
mixedLanguageSessions: 30,
|
||||
otherOnlySessions: 10,
|
||||
},
|
||||
referralSignals: { shared: 20, opened: 15 },
|
||||
referralFunnel: [
|
||||
{ label: "发起分享", count: 20 },
|
||||
{ label: "完成绑定", count: 10 },
|
||||
{ label: "绑定后首次 AI 成功", count: 8 },
|
||||
{ label: "完成奖励", count: 5 },
|
||||
],
|
||||
guardrails: {
|
||||
clientAiSuccessRate: rate(90, 100, 90),
|
||||
managedSuccessRate: rate(95, 100, 95),
|
||||
creditBlockedUsers: 3,
|
||||
latencyBuckets: [{ bucket: "S1_TO_3", successful: 80, failed: 5 }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ describe("高风险交互与 CSP", () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
<ComparisonBarChart
|
||||
items={[{ label: "自然量", value: 100, secondaryValue: 60 }]}
|
||||
items={[
|
||||
{ label: "自然量", value: 100, secondaryValue: 60 },
|
||||
{ label: "样本不足", value: null },
|
||||
]}
|
||||
primaryLabel="新增安装"
|
||||
secondaryLabel="24 小时激活"
|
||||
/>
|
||||
@@ -51,6 +54,7 @@ describe("高风险交互与 CSP", () => {
|
||||
]}
|
||||
/>
|
||||
<RadialMetric label="用户活跃率" percent={50} />
|
||||
<RadialMetric label="无样本活跃率" percent={null} />
|
||||
</>,
|
||||
);
|
||||
|
||||
@@ -58,6 +62,8 @@ describe("高风险交互与 CSP", () => {
|
||||
expect(screen.getByRole("progressbar", { name: /首次启动:100/ })).toBeTruthy();
|
||||
expect(screen.getByText("D1、D7、D30 价值留存 cohort 热力图")).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "用户活跃率:50.0%" })).toBeTruthy();
|
||||
expect(screen.getByRole("progressbar", { name: "样本不足 新增安装:暂无数据" })).toBeTruthy();
|
||||
expect(screen.getByRole("img", { name: "无样本活跃率:暂无数据" })).toBeTruthy();
|
||||
expect(container.querySelector("[style]")).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user