diff --git a/packages/stats/app/src/routes/api/health.ts b/packages/stats/app/src/routes/api/health.ts index 81e60f82e..eac4abb89 100644 --- a/packages/stats/app/src/routes/api/health.ts +++ b/packages/stats/app/src/routes/api/health.ts @@ -1,19 +1,3 @@ -import { AppConfig } from "@opencode-ai/stats-core/config" -import { Effect } from "effect" -import { runStatsEffect } from "../../stats-runtime" - export async function GET() { - return Response.json( - await runStatsEffect( - Effect.gen(function* () { - const config = yield* AppConfig - return { - ok: true, - app: "stats", - stage: config.stage, - publicUrl: config.publicUrl, - } - }), - ), - ) + return Response.json({ ok: true, app: "stats" }) } diff --git a/packages/stats/app/src/stats-runtime.ts b/packages/stats/app/src/stats-runtime.ts index 15beb678b..0fd2da5fd 100644 --- a/packages/stats/app/src/stats-runtime.ts +++ b/packages/stats/app/src/stats-runtime.ts @@ -1,16 +1,5 @@ -import { AppConfig } from "@opencode-ai/stats-core/config" -import { layer } from "@opencode-ai/stats-core/database" -import { GeoStatRepo } from "@opencode-ai/stats-core/domain/geo" -import { ModelStatRepo } from "@opencode-ai/stats-core/domain/model" -import { ProviderStatRepo } from "@opencode-ai/stats-core/domain/provider" -import { Effect, Layer } from "effect" -import type { Success } from "effect/Layer" +import { Effect } from "effect" -const repoLayer = Layer.mergeAll(ModelStatRepo.layer, ProviderStatRepo.layer, GeoStatRepo.layer).pipe( - Layer.provide(layer), -) -const statsLayer = Layer.mergeAll(AppConfig.layer, layer, repoLayer) - -export function runStatsEffect(effect: Effect.Effect>) { - return Effect.runPromise(Effect.provide(effect, statsLayer)) +export function runStatsEffect(effect: Effect.Effect) { + return Effect.runPromise(effect) } diff --git a/packages/stats/core/src/domain/home.ts b/packages/stats/core/src/domain/home.ts index 83a242d6b..c8c4d3e13 100644 --- a/packages/stats/core/src/domain/home.ts +++ b/packages/stats/core/src/domain/home.ts @@ -1,8 +1,9 @@ +import { Client } from "@planetscale/database" import { Effect } from "effect" -import { DatabaseError } from "../database" -import { GeoStatRepo, type GeoStatMetric } from "./geo" -import { ModelStatRepo, type ModelStatMetric } from "./model" -import { ProviderStatRepo, type ProviderStatMetric } from "./provider" +import { Resource } from "sst/resource" +import type { GeoStatMetric } from "./geo" +import type { ModelStatMetric } from "./model" +import type { ProviderStatMetric } from "./provider" export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise" export type TokenProduct = "Zen" | "Go" | "Enterprise" @@ -91,6 +92,14 @@ export type StatsHomeData = { country: Record } +export class StatsDataError extends Error { + override name = "StatsDataError" + + constructor(readonly cause: unknown) { + super("Failed to load stats data") + } +} + const DAY_MS = 86_400_000 const TOKEN_SCALE = 1_000_000 const DOLLARS_PER_MICROCENT = 1 / 100_000_000 @@ -130,53 +139,129 @@ type ModelAggregate = { totalCostMicrocents: number } -export function getStatsHomeData(): Effect.Effect< - StatsHomeData, - DatabaseError, - ModelStatRepo | ProviderStatRepo | GeoStatRepo -> { - return Effect.gen(function* () { - const modelStats = yield* ModelStatRepo - const providerStats = yield* ProviderStatRepo - const geoStats = yield* GeoStatRepo - const [modelRows, providerRows, geoRows] = yield* Effect.all( - [modelStats.listDaily(), providerStats.listDaily(), geoStats.listDaily()], - { concurrency: "unbounded" }, - ) - return buildStatsHomeData(modelRows, providerRows, geoRows) +type RawRow = Record + +export function getStatsHomeData(): Effect.Effect { + return Effect.tryPromise({ + try: async () => { + const [modelRows, providerRows, geoRows] = await Promise.all([listModelDaily(), listProviderDaily(), listGeoDaily()]) + return buildStatsHomeData(modelRows, providerRows, geoRows) + }, + catch: (cause) => new StatsDataError(cause), }) } export function getStatsModelData( model: string, provider?: string, -): Effect.Effect { - return Effect.gen(function* () { - const modelStats = yield* ModelStatRepo - const geoStats = yield* GeoStatRepo - const modelRows = yield* modelStats.listDaily() - const normalized = modelRows.flatMap(normalizeStatRow) - const resolvedModel = resolveModelName(model, normalized, provider) - if (!resolvedModel) return null - return buildStatsModelData( - resolvedModel, - modelRows, - yield* geoStats.listDaily({ - model: resolvedModel, - provider: resolveModelProvider(resolvedModel, normalized, provider), - }), - provider, - ) +): Effect.Effect { + return Effect.tryPromise({ + try: async () => { + const modelRows = await listModelDaily() + const normalized = modelRows.flatMap(normalizeStatRow) + const resolvedModel = resolveModelName(model, normalized, provider) + if (!resolvedModel) return null + return buildStatsModelData( + resolvedModel, + modelRows, + await listGeoDaily({ + model: resolvedModel, + provider: resolveModelProvider(resolvedModel, normalized, provider), + }), + provider, + ) + }, + catch: (cause) => new StatsDataError(cause), }) } -export function getStatsLabData(provider: string): Effect.Effect { - return Effect.gen(function* () { - const modelStats = yield* ModelStatRepo - return buildStatsLabData(provider, yield* modelStats.listDaily()) +export function getStatsLabData(provider: string): Effect.Effect { + return Effect.tryPromise({ + try: async () => buildStatsLabData(provider, await listModelDaily()), + catch: (cause) => new StatsDataError(cause), }) } +async function listModelDaily(): Promise { + return (await queryRows(`select period_key, updated_at, tier, provider, model, sessions, unique_users, input_tokens, + output_tokens, reasoning_tokens, cache_read_tokens, total_tokens, input_cost_microcents, output_cost_microcents, + total_cost_microcents from model_stat where grain = 'day' and client = 'all' and source = 'all' + and tier in ('Go', 'go') order by period_key`)).map((row) => ({ + periodKey: stringValue(row.period_key), + updatedAt: dateValue(row.updated_at), + tier: stringValue(row.tier), + provider: stringValue(row.provider), + model: stringValue(row.model), + sessions: numberValue(row.sessions), + uniqueUsers: numberValue(row.unique_users), + inputTokens: numberValue(row.input_tokens), + outputTokens: numberValue(row.output_tokens), + reasoningTokens: numberValue(row.reasoning_tokens), + cacheReadTokens: numberValue(row.cache_read_tokens), + totalTokens: numberValue(row.total_tokens), + inputCostMicrocents: numberValue(row.input_cost_microcents), + outputCostMicrocents: numberValue(row.output_cost_microcents), + totalCostMicrocents: numberValue(row.total_cost_microcents), + })) +} + +async function listProviderDaily(): Promise { + return (await queryRows(`select period_key, updated_at, tier, provider, total_tokens from provider_stat + where grain = 'day' and client = 'all' and source = 'all' and tier in ('Go', 'go') order by period_key`)).map( + (row) => ({ + periodKey: stringValue(row.period_key), + updatedAt: dateValue(row.updated_at), + tier: stringValue(row.tier), + provider: stringValue(row.provider), + totalTokens: numberValue(row.total_tokens), + }), + ) +} + +async function listGeoDaily(opts?: { provider?: string; model?: string }): Promise { + const scope = + opts?.model && opts.provider + ? "and provider = ? and model = ?" + : opts?.model + ? "and model = ?" + : "and provider = 'all' and model = 'all'" + const params = opts?.model && opts.provider ? [opts.provider, opts.model] : opts?.model ? [opts.model] : [] + return (await queryRows( + `select period_key, updated_at, tier, provider, model, country, continent, total_tokens from geo_stat + where grain = 'day' and client = 'all' and source = 'all' and tier in ('Go', 'go') ${scope} order by period_key`, + params, + )).map((row) => ({ + periodKey: stringValue(row.period_key), + updatedAt: dateValue(row.updated_at), + tier: stringValue(row.tier), + provider: stringValue(row.provider), + model: stringValue(row.model), + country: stringValue(row.country), + continent: stringValue(row.continent), + totalTokens: numberValue(row.total_tokens), + })) +} + +async function queryRows(query: string, params: string[] = []) { + return (await new Client({ url: databaseUrl() }).execute(query, params)).rows as RawRow[] +} + +function databaseUrl() { + return process.env.DATABASE_URL ?? Resource.StatsDatabase.url +} + +function stringValue(value: unknown) { + return value == null ? "" : String(value) +} + +function numberValue(value: unknown) { + return Number(value ?? 0) +} + +function dateValue(value: unknown) { + return value instanceof Date ? value : new Date(stringValue(value)) +} + function buildStatsHomeData( modelRows: ModelStatMetric[], providerRows: ProviderStatMetric[],