feat(stats): add unique user charts
This commit is contained in:
parent
2d993cd0d5
commit
24c70ec974
13 changed files with 327 additions and 127 deletions
|
|
@ -145,6 +145,7 @@ export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Servic
|
|||
continent: inserted("continent"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
unique_users: inserted("unique_users"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export type TokenCostEntry = { model: string; total: number; input: number; outp
|
|||
export type CacheRatioEntry = { model: string; ratio: number; cached: number; uncached: number; total: number }
|
||||
export type SessionCostEntry = { model: string; cost: number; tokens: number }
|
||||
export type CountryEntry = { country: string; continent: string; tokens: number; share: number; rank: number }
|
||||
export type ModelUsagePoint = { date: string; tokens: number; sessions: number; cost: number }
|
||||
export type ModelUsagePoint = { date: string; tokens: number; users: number; sessions: number; cost: number }
|
||||
export type ModelMixEntry = { label: string; tokens: number; share: number }
|
||||
export type ModelPeerEntry = {
|
||||
model: string
|
||||
|
|
@ -82,6 +82,7 @@ export type StatsLabData = {
|
|||
export type StatsHomeData = {
|
||||
updatedAt: string | null
|
||||
usage: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
|
||||
users: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
|
||||
leaderboard: Record<UsageProduct, Record<UsageRange, LeaderboardEntry[]>>
|
||||
market: Record<UsageRange, MarketDay[]>
|
||||
tokenCost: Record<TokenProduct, TokenCostEntry[]>
|
||||
|
|
@ -118,6 +119,7 @@ type ModelAggregate = {
|
|||
model: string
|
||||
provider: string
|
||||
sessions: number
|
||||
uniqueUsers: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
|
|
@ -200,6 +202,18 @@ function buildStatsHomeData(
|
|||
),
|
||||
),
|
||||
),
|
||||
users: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) =>
|
||||
buildUsagePoints(
|
||||
normalized,
|
||||
product,
|
||||
range,
|
||||
getWindow(range, earliest, latest),
|
||||
getWindow("1W", earliest, latest),
|
||||
"users",
|
||||
),
|
||||
),
|
||||
),
|
||||
leaderboard: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) => buildLeaderboard(normalized, product, getWindow("1W", earliest, latest))),
|
||||
),
|
||||
|
|
@ -340,6 +354,7 @@ function emptyStatsHomeData(): StatsHomeData {
|
|||
return {
|
||||
updatedAt: null,
|
||||
usage: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
users: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
leaderboard: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
market: createRangeRecord(() => []),
|
||||
tokenCost: createTokenProductRecord(() => []),
|
||||
|
|
@ -355,28 +370,39 @@ function buildUsagePoints(
|
|||
range: UsageRange,
|
||||
window: DateWindow,
|
||||
rankWindow: DateWindow,
|
||||
metric: "tokens" | "users" = "tokens",
|
||||
) {
|
||||
const modelOrder = aggregateByModelName(rowsForProduct(rows, product, rankWindow.start, rankWindow.end))
|
||||
.toSorted((a, b) => b.totalTokens - a.totalTokens)
|
||||
.toSorted((a, b) => modelUsageValue(b, metric) - modelUsageValue(a, metric))
|
||||
.slice(0, TOP_MODEL_SEGMENT_LIMIT)
|
||||
.map((item) => item.model)
|
||||
|
||||
return createBuckets(window, range).map((bucket) => {
|
||||
const bucketRows = aggregateByModelName(rowsForProduct(rows, product, bucket.start, bucket.end))
|
||||
const byModel = new Map(bucketRows.map((item) => [item.model, item.totalTokens]))
|
||||
const segmentTokens = modelOrder.map((model) => ({ model, tokens: byModel.get(model) ?? 0 }))
|
||||
const knownTokens = segmentTokens.reduce((sum, item) => sum + item.tokens, 0)
|
||||
const totalTokens = bucketRows.reduce((sum, item) => sum + item.totalTokens, 0)
|
||||
const byModel = new Map(bucketRows.map((item) => [item.model, modelUsageValue(item, metric)]))
|
||||
const segments = modelOrder.map((model) => ({ model, value: byModel.get(model) ?? 0 }))
|
||||
const knownValue = segments.reduce((sum, item) => sum + item.value, 0)
|
||||
const totalValue = bucketRows.reduce((sum, item) => sum + modelUsageValue(item, metric), 0)
|
||||
return {
|
||||
date: bucket.label,
|
||||
segments: [
|
||||
...segmentTokens.map((item) => ({ model: item.model, value: round(item.tokens / 1_000_000_000_000, 4) })),
|
||||
{ model: "Other", value: round(Math.max(totalTokens - knownTokens, 0) / 1_000_000_000_000, 4) },
|
||||
...segments.map((item) => ({ model: item.model, value: usagePointValue(item.value, metric) })),
|
||||
{ model: "Other", value: usagePointValue(Math.max(totalValue - knownValue, 0), metric) },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function modelUsageValue(item: ModelAggregate, metric: "tokens" | "users") {
|
||||
if (metric === "users") return item.uniqueUsers
|
||||
return item.totalTokens
|
||||
}
|
||||
|
||||
function usagePointValue(value: number, metric: "tokens" | "users") {
|
||||
if (metric === "users") return value
|
||||
return round(value / 1_000_000_000_000, 4)
|
||||
}
|
||||
|
||||
function buildLeaderboard(rows: StatMetricRow[], product: UsageProduct, rankWindow: DateWindow) {
|
||||
const previous = new Map(
|
||||
aggregateByModelName(rowsForProduct(rows, product, rankWindow.previousStart, rankWindow.previousEnd)).map(
|
||||
|
|
@ -502,6 +528,7 @@ function buildModelUsage(rows: StatMetricRow[], window: DateWindow, range: Usage
|
|||
return {
|
||||
date: bucket.label,
|
||||
tokens: aggregate.totalTokens,
|
||||
users: aggregate.uniqueUsers,
|
||||
sessions: aggregate.sessions,
|
||||
cost: round(microcentsToDollars(aggregate.totalCostMicrocents), 2),
|
||||
}
|
||||
|
|
@ -601,6 +628,7 @@ function combineRowsForModel(model: string, rows: StatMetricRow[]): ModelAggrega
|
|||
model,
|
||||
provider: "unknown",
|
||||
sessions: 0,
|
||||
uniqueUsers: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
reasoningTokens: 0,
|
||||
|
|
@ -617,6 +645,7 @@ function combineModelAggregate(current: ModelAggregate | undefined, row: StatMet
|
|||
model: row.model,
|
||||
provider: row.provider,
|
||||
sessions: (current?.sessions ?? 0) + row.sessions,
|
||||
uniqueUsers: (current?.uniqueUsers ?? 0) + row.uniqueUsers,
|
||||
inputTokens: (current?.inputTokens ?? 0) + row.inputTokens,
|
||||
outputTokens: (current?.outputTokens ?? 0) + row.outputTokens,
|
||||
reasoningTokens: (current?.reasoningTokens ?? 0) + row.reasoningTokens,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export function buildStatsQuery(periodStart: Date, periodEnd: Date, dimension: S
|
|||
const aggregateColumns = `
|
||||
COUNT(DISTINCT session) AS sessions,
|
||||
COUNT(*) AS requests,
|
||||
COUNT(DISTINCT user_key) AS unique_users,
|
||||
COALESCE(SUM(tokens_input), 0) AS input_tokens,
|
||||
COALESCE(SUM(tokens_output), 0) AS output_tokens,
|
||||
COALESCE(SUM(tokens_reasoning), 0) AS reasoning_tokens,
|
||||
|
|
@ -70,6 +71,8 @@ WITH normalized AS (
|
|||
UPPER(COALESCE(NULLIF(cf_country, ''), 'ZZ')) AS country,
|
||||
COALESCE(NULLIF(cf_continent, ''), '') AS continent,
|
||||
session,
|
||||
COALESCE(NULLIF(workspace, ''), '') AS workspace,
|
||||
COALESCE(NULLIF(api_key, ''), '') AS api_key,
|
||||
status,
|
||||
duration AS duration_ms,
|
||||
time_to_first_byte AS ttfb_ms,
|
||||
|
|
@ -108,6 +111,7 @@ WITH normalized AS (
|
|||
country,
|
||||
continent,
|
||||
session,
|
||||
COALESCE(NULLIF(workspace, ''), NULLIF(api_key, '')) AS user_key,
|
||||
status,
|
||||
duration_ms,
|
||||
ttfb_ms,
|
||||
|
|
@ -197,6 +201,7 @@ function toStatBaseAggregate(data: AthenaData): StatBaseAggregate[] {
|
|||
tier: normalizeTier(data.tier || "unknown"),
|
||||
sessions: integer(data, "sessions"),
|
||||
requests: integer(data, "requests"),
|
||||
unique_users: integer(data, "unique_users"),
|
||||
input_tokens: integer(data, "input_tokens"),
|
||||
output_tokens: integer(data, "output_tokens"),
|
||||
reasoning_tokens: integer(data, "reasoning_tokens"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export type ModelStatMetric = {
|
|||
provider: string
|
||||
model: string
|
||||
sessions: number
|
||||
uniqueUsers: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
|
|
@ -64,6 +65,7 @@ export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.
|
|||
provider: modelStat.provider,
|
||||
model: modelStat.model,
|
||||
sessions: modelStat.sessions,
|
||||
uniqueUsers: modelStat.unique_users,
|
||||
inputTokens: modelStat.input_tokens,
|
||||
outputTokens: modelStat.output_tokens,
|
||||
reasoningTokens: modelStat.reasoning_tokens,
|
||||
|
|
@ -101,6 +103,7 @@ export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.
|
|||
provider_model: inserted("provider_model"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
unique_users: inserted("unique_users"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export class ProviderStatRepo extends Context.Service<ProviderStatRepo, Provider
|
|||
set: {
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
unique_users: inserted("unique_users"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type StatBaseAggregate = {
|
|||
tier: string
|
||||
sessions: number
|
||||
requests: number
|
||||
unique_users: number
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
reasoning_tokens: number
|
||||
|
|
@ -41,6 +42,7 @@ export type StatBaseRow = {
|
|||
source?: string
|
||||
sessions?: number
|
||||
requests?: number
|
||||
unique_users?: number
|
||||
input_tokens?: number
|
||||
output_tokens?: number
|
||||
reasoning_tokens?: number
|
||||
|
|
@ -71,6 +73,7 @@ export function toStatBaseRow(data: StatBaseAggregate) {
|
|||
source: "all",
|
||||
sessions: data.sessions,
|
||||
requests: data.requests,
|
||||
unique_users: data.unique_users,
|
||||
input_tokens: data.input_tokens,
|
||||
output_tokens: data.output_tokens,
|
||||
reasoning_tokens: data.reasoning_tokens,
|
||||
|
|
@ -122,6 +125,7 @@ export function combineRows<T extends StatBaseRow>(left: T, right: T): T {
|
|||
...left,
|
||||
sessions: (left.sessions ?? 0) + (right.sessions ?? 0),
|
||||
requests: (left.requests ?? 0) + (right.requests ?? 0),
|
||||
unique_users: (left.unique_users ?? 0) + (right.unique_users ?? 0),
|
||||
input_tokens: (left.input_tokens ?? 0) + (right.input_tokens ?? 0),
|
||||
output_tokens: (left.output_tokens ?? 0) + (right.output_tokens ?? 0),
|
||||
reasoning_tokens: (left.reasoning_tokens ?? 0) + (right.reasoning_tokens ?? 0),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue