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
This commit is contained in:
saravanakumardb1 2026-02-15 18:57:16 -08:00
parent b977e85bc2
commit 1cf74d22fa
2 changed files with 25 additions and 1 deletions

View File

@ -16,6 +16,7 @@ import {
CheckLimitsSchema, CheckLimitsSchema,
type UsageDoc, type UsageDoc,
type ModelBreakdown, type ModelBreakdown,
type SourceBreakdown,
} from './types.js'; } from './types.js';
/** /**
@ -63,12 +64,19 @@ export async function usageRoutes(app: FastifyInstance) {
const records = await repo.list({ userId, days: Number(days), productId }); const records = await repo.list({ userId, days: Number(days), productId });
const byModel: Record<string, { tokens: number; requests: number; cost: number }> = {}; const byModel: Record<string, { tokens: number; requests: number; cost: number }> = {};
const bySource: Record<string, { tokens: number; requests: number; cost: number }> = {};
for (const r of records) { for (const r of records) {
const m = r.model || 'gpt-4o-mini'; const m = r.model || 'gpt-4o-mini';
if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 }; if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 };
byModel[m].tokens += r.tokensUsed; byModel[m].tokens += r.tokensUsed;
byModel[m].requests += r.dictations; byModel[m].requests += r.dictations;
byModel[m].cost += r.costUsd; 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]) => ({ const modelBreakdown: ModelBreakdown[] = Object.entries(byModel).map(([model, s]) => ({
@ -76,6 +84,11 @@ export async function usageRoutes(app: FastifyInstance) {
...s, ...s,
})); }));
const sourceBreakdown: SourceBreakdown[] = Object.entries(bySource).map(([source, s]) => ({
source,
...s,
}));
return { return {
totalWords: records.reduce((sum, r) => sum + r.words, 0), totalWords: records.reduce((sum, r) => sum + r.words, 0),
totalDictations: records.reduce((sum, r) => sum + r.dictations, 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), totalCost: records.reduce((sum, r) => sum + r.costUsd, 0),
records, records,
modelBreakdown, modelBreakdown,
sourceBreakdown,
}; };
}); });
@ -95,7 +109,7 @@ export async function usageRoutes(app: FastifyInstance) {
const input = parsed.data; const input = parsed.data;
const productId = getRequestProductId(req); const productId = getRequestProductId(req);
const doc: UsageDoc = { 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, productId,
...input, ...input,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),

View File

@ -15,9 +15,17 @@ export interface UsageDoc {
tokensUsed: number; tokensUsed: number;
costUsd: number; costUsd: number;
model?: string; model?: string;
source?: string;
createdAt: string; createdAt: string;
} }
export interface SourceBreakdown {
source: string;
tokens: number;
requests: number;
cost: number;
}
export interface UsageSummary { export interface UsageSummary {
totalWords: number; totalWords: number;
totalDictations: number; totalDictations: number;
@ -25,6 +33,7 @@ export interface UsageSummary {
totalCost: number; totalCost: number;
records: UsageDoc[]; records: UsageDoc[];
modelBreakdown?: ModelBreakdown[]; modelBreakdown?: ModelBreakdown[];
sourceBreakdown?: SourceBreakdown[];
} }
export interface ModelBreakdown { export interface ModelBreakdown {
@ -49,6 +58,7 @@ export const UpsertUsageSchema = z.object({
tokensUsed: z.number().int().min(0).default(0), tokensUsed: z.number().int().min(0).default(0),
costUsd: z.number().min(0).default(0), costUsd: z.number().min(0).default(0),
model: z.string().optional(), model: z.string().optional(),
source: z.string().optional(),
}); });
export const CheckLimitsSchema = z.object({ export const CheckLimitsSchema = z.object({