From 1cf74d22fa2567d0e433b6756b3916bbd650d098 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 15 Feb 2026 18:57:16 -0800 Subject: [PATCH] feat(usage): add source/platform tracking + source breakdown in summary - Add optional 'source' field to UsageDoc (desktop/web/ios/android) - Add 'source' to UpsertUsageSchema validation - Include source in upsert document ID to avoid cross-platform overwrites - Add SourceBreakdown interface - Aggregate sourceBreakdown in GET /usage/summary alongside modelBreakdown - Clients can now pass source when reporting usage for per-app analytics --- .../platform-service/src/modules/usage/routes.ts | 16 +++++++++++++++- .../platform-service/src/modules/usage/types.ts | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/services/platform-service/src/modules/usage/routes.ts b/services/platform-service/src/modules/usage/routes.ts index d88033c4..d43d2c40 100644 --- a/services/platform-service/src/modules/usage/routes.ts +++ b/services/platform-service/src/modules/usage/routes.ts @@ -16,6 +16,7 @@ import { CheckLimitsSchema, type UsageDoc, type ModelBreakdown, + type SourceBreakdown, } from './types.js'; /** @@ -63,12 +64,19 @@ export async function usageRoutes(app: FastifyInstance) { const records = await repo.list({ userId, days: Number(days), productId }); const byModel: Record = {}; + const bySource: Record = {}; for (const r of records) { const m = r.model || 'gpt-4o-mini'; if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 }; byModel[m].tokens += r.tokensUsed; byModel[m].requests += r.dictations; byModel[m].cost += r.costUsd; + + const s = r.source || 'unknown'; + if (!bySource[s]) bySource[s] = { tokens: 0, requests: 0, cost: 0 }; + bySource[s].tokens += r.tokensUsed; + bySource[s].requests += r.dictations; + bySource[s].cost += r.costUsd; } const modelBreakdown: ModelBreakdown[] = Object.entries(byModel).map(([model, s]) => ({ @@ -76,6 +84,11 @@ export async function usageRoutes(app: FastifyInstance) { ...s, })); + const sourceBreakdown: SourceBreakdown[] = Object.entries(bySource).map(([source, s]) => ({ + source, + ...s, + })); + return { totalWords: records.reduce((sum, r) => sum + r.words, 0), totalDictations: records.reduce((sum, r) => sum + r.dictations, 0), @@ -83,6 +96,7 @@ export async function usageRoutes(app: FastifyInstance) { totalCost: records.reduce((sum, r) => sum + r.costUsd, 0), records, modelBreakdown, + sourceBreakdown, }; }); @@ -95,7 +109,7 @@ export async function usageRoutes(app: FastifyInstance) { const input = parsed.data; const productId = getRequestProductId(req); const doc: UsageDoc = { - id: `usg_${input.date}_${input.userId}${input.model ? `_${input.model}` : ''}`, + id: `usg_${input.date}_${input.userId}${input.model ? `_${input.model}` : ''}${input.source ? `_${input.source}` : ''}`, productId, ...input, createdAt: new Date().toISOString(), diff --git a/services/platform-service/src/modules/usage/types.ts b/services/platform-service/src/modules/usage/types.ts index 854d625d..75c70dd3 100644 --- a/services/platform-service/src/modules/usage/types.ts +++ b/services/platform-service/src/modules/usage/types.ts @@ -15,9 +15,17 @@ export interface UsageDoc { tokensUsed: number; costUsd: number; model?: string; + source?: string; createdAt: string; } +export interface SourceBreakdown { + source: string; + tokens: number; + requests: number; + cost: number; +} + export interface UsageSummary { totalWords: number; totalDictations: number; @@ -25,6 +33,7 @@ export interface UsageSummary { totalCost: number; records: UsageDoc[]; modelBreakdown?: ModelBreakdown[]; + sourceBreakdown?: SourceBreakdown[]; } export interface ModelBreakdown { @@ -49,6 +58,7 @@ export const UpsertUsageSchema = z.object({ tokensUsed: z.number().int().min(0).default(0), costUsd: z.number().min(0).default(0), model: z.string().optional(), + source: z.string().optional(), }); export const CheckLimitsSchema = z.object({