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,
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<string, { tokens: number; requests: number; cost: number }> = {};
const bySource: Record<string, { tokens: number; requests: number; cost: number }> = {};
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(),

View File

@ -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({