Correct product analytics cohorts and reporting
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled

This commit is contained in:
Rocky
2026-08-21 17:50:35 +08:00
parent b25f5ae6e9
commit edd0d9feca
31 changed files with 1134 additions and 262 deletions
+46 -15
View File
@@ -4,6 +4,10 @@ This document is the canonical definition of product metrics. All dates and
cohorts use UTC calendar boundaries. Counts are based on distinct accounts when
an installation is linked, otherwise on the pseudonymous installation.
Admin presets cover exactly 7, 30, or 90 UTC calendar dates, starting at 00:00
on the first date and ending at the current instant. The current UTC date is
therefore explicitly partial.
## North-star metric
### Weekly AI active users (WAIU)
@@ -39,9 +43,10 @@ Accounts whose `accounts.created_at` falls in the selected period.
### 24-hour AI activation rate
The percentage of new installations that successfully complete any AI feature
within 24 hours of their first open. The numerator uses the same value-event
rules as WAIU.
The percentage of new installations that have completed their full 24-hour
observation window and successfully complete any AI feature within 24 hours of
their first open. Unmatured installations are excluded from both numerator and
denominator. Managed usage before that installation's first open is ignored.
### Time to first value
@@ -49,6 +54,14 @@ Elapsed time from `FIRST_OPEN` to the first successful AI feature. The dashboard
reports the median in minutes. Users without a successful AI feature are not
included in the median and remain visible in the activation denominator.
### 24-hour growth funnel
A strict cohort of installations with a completed 24-hour observation window:
first open, account registration after first open, first AI value event after
registration, and first server-verified purchase after that value event. Every
downstream step must occur within 24 hours of first open. D7 belongs only to the
retention report and is not mixed into this funnel.
## Activity
### AI DAU, WAU and MAU
@@ -69,6 +82,13 @@ Managed client success events are excluded from this total.
`successful AI requests / distinct value-active users` for the selected period.
### Registered product-active users
The operations overview counts distinct registered accounts with either a
successful AI value event (managed, local, or BYOK) or a finalized manual
keyboard-input summary in the selected period. The displayed rate divides this
population by all registered accounts.
## Keyboard input usage
Keyboard input metrics use finalized UTC-day summaries produced on-device.
@@ -175,8 +195,11 @@ server credits and are excluded.
### 7-day and 30-day free-to-paid conversion
The percentage of newly registered accounts with a first credited StoreKit
purchase no later than 7 or 30 days after registration. Cohorts whose conversion
window has not elapsed are reported separately from mature cohorts.
purchase no later than 7 or 30 days after registration. The selected report
period filters when each observation window matures: a 7-day report cohort uses
registrations shifted exactly 7 days earlier, and the 30-day cohort is shifted
30 days earlier. This keeps every denominator fully observed and makes the rate
available even when the selected preset is no longer than the conversion window.
### Paying users
@@ -187,21 +210,29 @@ Distinct accounts with at least one credited StoreKit purchase in the period.
The percentage of paying accounts with at least two credited StoreKit purchases
across their lifetime.
### Purchase intent funnel
A strict installation cohort: `PURCHASE_VIEWED`, followed by
`PURCHASE_STARTED`, followed by a StoreKit purchase verified by the server for
the linked account. Each event must occur after the previous step and before the
report's `until`. `PURCHASE_CANCELLED` is a separate signal, not a funnel step.
StoreKit transaction count and granted credits are operational proxies. Net
revenue, App Store commission and refunds require App Store financial data and
are outside this service's first version.
## Referral funnel
The ordered growth funnel is:
The ordered cohort contains bindings created in the selected period:
1. `REFERRAL_SHARED` distinct sharing installations.
2. Invitation opens: accepted `INVITE_OPENED` client events plus anonymous
first-party invitation page views. Page views are aggregate requests rather
than distinct people and must be interpreted as a directional funnel signal.
3. Referral-bound accounts.
4. Referral-bound accounts that reach their first value event.
5. Rewarded referral bindings.
1. Referral binding created.
2. The same invitee reaches an AI value event after binding.
3. The same binding is rewarded before the report's `until`.
`REFERRAL_SHARED` distinct installations and invitation opens are independent
directional signals. Invitation opens combine accepted `INVITE_OPENED` events
with anonymous first-party page-view counters, so they are not people and must
never be placed in the ordered conversion funnel.
Pending and ineligible bindings are parallel status counts, not sequential
funnel steps.
@@ -212,8 +243,8 @@ funnel steps.
terminal success or failure event.
- Managed request failure rate: terminal non-settled `provider_requests` divided
by terminal managed requests.
- P50/P95 latency: client duration bucket distribution for all modes; exact
server duration percentiles may be added later.
- Client latency: successful and failed terminal events grouped by declared
duration bucket. Exact P50/P95 values are not inferred from buckets.
- Credit-blocked users: distinct installations reporting
`INSUFFICIENT_CREDITS` during the period.
+4 -1
View File
@@ -28,7 +28,10 @@ regardless of account linkage.
- Authentication is optional so first-open and pre-login events can be
measured. Invalid bearer credentials are rejected.
- `installationId` must be a client-generated UUID stored in the containing app
and shared with the keyboard extension through the App Group.
and shared with the keyboard extension through the App Group. New clients send
it once at the batch root. During the migration window, the server also accepts
released clients that repeat one identical `installationId` on every event;
missing, incomplete, or conflicting identities reject the entire batch.
- When a valid account session is present, the installation is linked to that
account. An installation cannot later be linked to a different account.
- Every `clientEventId` is a client-generated UUID. The pair
+93 -21
View File
@@ -85,6 +85,9 @@ paths:
session is supplied, the installation is linked to the account and is
deleted with that account. Audio, user text, prompts, transcripts,
model output, credentials and arbitrary properties are never accepted.
New clients must send installationId once at the batch level. During
migration, legacy clients that send the same installationId on every
event remain accepted; conflicting or incomplete identities are rejected.
requestBody:
required: true
content:
@@ -1314,6 +1317,7 @@ components:
AdminRange:
name: range
in: query
description: Covers exactly 7, 30, or 90 UTC calendar dates, from 00:00 on the first date through the current instant. The current UTC date is partial.
schema: { type: string, enum: [7d, 30d, 90d], default: 30d }
AdminFrom:
name: from
@@ -1438,12 +1442,20 @@ components:
minLength: 1
maxLength: 32
pattern: "^[A-Za-z0-9._+-]+$"
installationId:
type: string
format: uuid
deprecated: true
description: Transitional legacy field; new clients must use the batch-level installationId.
ProductAnalyticsBatchRequest:
type: object
additionalProperties: false
required: [installationId, events]
required: [events]
properties:
installationId: { type: string, format: uuid }
installationId:
type: string
format: uuid
description: Required for new clients; legacy batches may instead repeat one identical ID on every event.
events:
type: array
minItems: 1
@@ -1793,10 +1805,18 @@ components:
date: { type: string, format: date }
registrations: { type: integer, format: int64, minimum: 0 }
creditsUsed: { type: integer, format: int64, minimum: 0 }
AdminStatsPeriod:
type: object
additionalProperties: false
required: [from, until]
properties:
from: { type: string, format: date-time, description: Inclusive UTC lower bound. }
until: { type: string, format: date-time, description: Exclusive current-instant upper bound. }
AdminOverview:
type: object
additionalProperties: false
required:
- period
- totalUsers
- activeUsers
- newUsers
@@ -1806,8 +1826,13 @@ components:
- trend
- usage
properties:
period: { $ref: "#/components/schemas/AdminStatsPeriod" }
totalUsers: { type: integer, format: int64, minimum: 0 }
activeUsers: { type: integer, format: int64, minimum: 0 }
activeUsers:
type: integer
format: int64
minimum: 0
description: Registered accounts with successful AI use or manually committed keyboard input in the period.
newUsers: { type: integer, format: int64, minimum: 0 }
totalCreditBalance: { type: integer, format: int64, minimum: 0 }
creditsGranted: { type: integer, format: int64, minimum: 0 }
@@ -1825,7 +1850,7 @@ components:
properties:
label:
type: string
enum: [邀请码创建, 成功绑定, 有效使用并奖励]
enum: [成功绑定, 绑定后首次 AI 成功, 完成奖励]
count: { type: integer, format: int64, minimum: 0 }
AdminReferralRank:
type: object
@@ -1839,8 +1864,9 @@ components:
AdminReferralOverview:
type: object
additionalProperties: false
required: [pendingBindings, ineligibleBindings, funnel, ranking]
required: [period, pendingBindings, ineligibleBindings, funnel, ranking]
properties:
period: { $ref: "#/components/schemas/AdminStatsPeriod" }
pendingBindings: { type: integer, format: int64, minimum: 0 }
ineligibleBindings: { type: integer, format: int64, minimum: 0 }
funnel:
@@ -1856,7 +1882,11 @@ components:
properties:
numerator: { type: integer, format: int64, minimum: 0 }
denominator: { type: integer, format: int64, minimum: 0 }
percent: { type: ["number", "null"], minimum: 0, maximum: 100 }
percent:
type: ["number", "null"]
minimum: 0
maximum: 100
description: Null means unavailable, usually because the denominator is zero; clients must not render it as 0%.
AdminAnalyticsFunnelStep:
type: object
additionalProperties: false
@@ -1872,7 +1902,11 @@ components:
channel:
type: string
enum: [APP_STORE_ORGANIC, REFERRAL, SOCIAL_CONTENT, UNKNOWN]
installations: { type: integer, format: int64, minimum: 0 }
installations:
type: integer
format: int64
minimum: 0
description: Installations in this channel with a completed 24-hour observation window.
activated: { type: integer, format: int64, minimum: 0 }
activationRate: { $ref: "#/components/schemas/AdminAnalyticsRate" }
AdminAnalyticsCohort:
@@ -1905,6 +1939,16 @@ components:
executionMode: { type: string, enum: [MANAGED, LOCAL, BYOK] }
users: { type: integer, format: int64, minimum: 0 }
successes: { type: integer, format: int64, minimum: 0 }
AdminAnalyticsLatencyBucket:
type: object
additionalProperties: false
required: [bucket, successful, failed]
properties:
bucket:
type: string
enum: [LT_1S, S1_TO_3, S3_TO_10, S10_TO_30, GTE_30S]
successful: { type: integer, format: int64, minimum: 0 }
failed: { type: integer, format: int64, minimum: 0 }
AdminAnalyticsKeyboardUsage:
type: object
additionalProperties: false
@@ -1955,16 +1999,11 @@ components:
- retention
- aiFeatures
- keyboardUsage
- referralSignals
- referralFunnel
- guardrails
properties:
period:
type: object
additionalProperties: false
required: [from, until]
properties:
from: { type: string, format: date-time }
until: { type: string, format: date-time }
period: { $ref: "#/components/schemas/AdminStatsPeriod" }
northStar:
type: object
additionalProperties: false
@@ -1990,9 +2029,21 @@ components:
additionalProperties: false
required: [dau, wau, mau, successfulAiRequests]
properties:
dau: { type: integer, format: int64, minimum: 0 }
wau: { type: integer, format: int64, minimum: 0 }
mau: { type: integer, format: int64, minimum: 0 }
dau:
type: integer
format: int64
minimum: 0
description: Distinct AI value-active identities in the rolling 1-day window ending at period.until.
wau:
type: integer
format: int64
minimum: 0
description: Distinct AI value-active identities in the rolling 7-day window ending at period.until.
mau:
type: integer
format: int64
minimum: 0
description: Distinct AI value-active identities in the rolling 30-day window ending at period.until.
stickinessPercent: { type: ["number", "null"], minimum: 0, maximum: 100 }
successfulAiRequests: { type: integer, format: int64, minimum: 0 }
successfulRequestsPerActiveUser: { type: ["number", "null"], minimum: 0 }
@@ -2009,14 +2060,23 @@ components:
type: object
additionalProperties: false
required:
[payingUsers, purchases, creditsPurchased, conversion7d, conversion30d, repeatPurchaseRate]
[payingUsers, purchases, creditsPurchased, conversion7d, conversion30d, repeatPurchaseRate, purchaseFunnel, cancelledUsers]
properties:
payingUsers: { type: integer, format: int64, minimum: 0 }
purchases: { type: integer, format: int64, minimum: 0 }
creditsPurchased: { type: integer, format: int64, minimum: 0 }
conversion7d: { $ref: "#/components/schemas/AdminAnalyticsRate" }
conversion30d: { $ref: "#/components/schemas/AdminAnalyticsRate" }
conversion7d:
$ref: "#/components/schemas/AdminAnalyticsRate"
description: Conversion for account cohorts whose full 7-day observation window matures inside the selected report period.
conversion30d:
$ref: "#/components/schemas/AdminAnalyticsRate"
description: Conversion for account cohorts whose full 30-day observation window matures inside the selected report period.
repeatPurchaseRate: { $ref: "#/components/schemas/AdminAnalyticsRate" }
purchaseFunnel:
type: array
description: Strict installation cohort from purchase view through server-verified StoreKit purchase.
items: { $ref: "#/components/schemas/AdminAnalyticsFunnelStep" }
cancelledUsers: { type: integer, format: int64, minimum: 0 }
growthFunnel:
type: array
items: { $ref: "#/components/schemas/AdminAnalyticsFunnelStep" }
@@ -2028,17 +2088,29 @@ components:
items: { $ref: "#/components/schemas/AdminAnalyticsFeatureUsage" }
keyboardUsage:
$ref: "#/components/schemas/AdminAnalyticsKeyboardUsage"
referralSignals:
type: object
additionalProperties: false
required: [shared, opened]
description: Directional signals only; these counts are not funnel stages.
properties:
shared: { type: integer, format: int64, minimum: 0 }
opened: { type: integer, format: int64, minimum: 0 }
referralFunnel:
type: array
items: { $ref: "#/components/schemas/AdminAnalyticsFunnelStep" }
guardrails:
type: object
additionalProperties: false
required: [clientAiSuccessRate, managedSuccessRate, creditBlockedUsers]
required: [clientAiSuccessRate, managedSuccessRate, creditBlockedUsers, latencyBuckets]
properties:
clientAiSuccessRate: { $ref: "#/components/schemas/AdminAnalyticsRate" }
managedSuccessRate: { $ref: "#/components/schemas/AdminAnalyticsRate" }
creditBlockedUsers: { type: integer, format: int64, minimum: 0 }
latencyBuckets:
type: array
description: Client AI terminal events grouped into declared duration buckets.
items: { $ref: "#/components/schemas/AdminAnalyticsLatencyBucket" }
AdminUserSummary:
type: object
additionalProperties: false