Просмотр исходного кода

fix(stats): bypass worker runtime crash

Adam 2 месяцев назад
Родитель
Сommit
f96e6aa6ed

+ 1 - 17
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" })
 }

+ 3 - 14
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<A, E>(effect: Effect.Effect<A, E, Success<typeof statsLayer>>) {
-  return Effect.runPromise(Effect.provide(effect, statsLayer))
+export function runStatsEffect<A, E>(effect: Effect.Effect<A, E>) {
+  return Effect.runPromise(effect)
 }

+ 124 - 39
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<UsageRange, CountryEntry[]>
 }
 
+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<string, unknown>
+
+export function getStatsHomeData(): Effect.Effect<StatsHomeData, StatsDataError> {
+  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<StatsModelData | null, DatabaseError, ModelStatRepo | GeoStatRepo> {
-  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<StatsModelData | null, StatsDataError> {
+  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<StatsLabData | null, DatabaseError, ModelStatRepo> {
-  return Effect.gen(function* () {
-    const modelStats = yield* ModelStatRepo
-    return buildStatsLabData(provider, yield* modelStats.listDaily())
+export function getStatsLabData(provider: string): Effect.Effect<StatsLabData | null, StatsDataError> {
+  return Effect.tryPromise({
+    try: async () => buildStatsLabData(provider, await listModelDaily()),
+    catch: (cause) => new StatsDataError(cause),
   })
 }
 
+async function listModelDaily(): Promise<ModelStatMetric[]> {
+  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<ProviderStatMetric[]> {
+  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<GeoStatMetric[]> {
+  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[],