Skip to content

Commit 183780e

Browse files
committed
fix: reduce alert noise and add actionable cost driver diagnosis
- Disable cycle outlier detection by default (cycleOutlierMultiplier=0) since it just flags active users against an inactive team median - Add cost driver breakdown to daily spike and cost/req alerts showing thinking % and max-mode % with dollar amounts - Remove useless "500 premium reqs" from alert messages (legacy field, same for everyone) - Separate cycle_spend metric from daily spend to prevent dedup collision - Add spend legend and blue color to user detail chart tooltip Made-with: Cursor
1 parent 3a5e8b8 commit 183780e

7 files changed

Lines changed: 86 additions & 11 deletions

File tree

src/components/charts/spend-trend-chart.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export function SpendTrendChart({ data, selectedDays, avgPerDay }: SpendTrendCha
9292
<div className="flex items-baseline gap-2 mb-2">
9393
<h3 className="text-xs font-medium text-zinc-400">Daily Spend</h3>
9494
<span className="text-[10px] text-zinc-600">{data.length} days of data</span>
95+
<span className="text-[10px] text-blue-400/70 flex items-center gap-1">
96+
<span className="inline-block w-3 border-t border-blue-400" />
97+
spend
98+
</span>
9599
{hasCostPerReq && (
96100
<span className="text-[10px] text-purple-400/70 flex items-center gap-1">
97101
<span className="inline-block w-3 border-t border-dashed border-purple-400" />
@@ -144,7 +148,9 @@ export function SpendTrendChart({ data, selectedDays, avgPerDay }: SpendTrendCha
144148
{!isInRange && <span className="text-zinc-600 ml-1">(context)</span>}
145149
{isProvisional && <span className="text-amber-500 ml-1">(partial)</span>}
146150
</div>
147-
<div className="font-mono font-bold text-sm">{fmtDollars(pt.spend)}</div>
151+
<div className="font-mono font-bold text-sm text-blue-400">
152+
{fmtDollars(pt.spend)}
153+
</div>
148154
{pt.change !== 0 && (
149155
<div style={{ color: changeColor }} className="font-mono text-[11px]">
150156
{changeSign}

src/lib/alerts/email.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function getClient(): Resend | null {
1010
function formatValue(metric: string, value: number): string {
1111
switch (metric) {
1212
case "spend":
13+
case "cycle_spend":
1314
return `$${(value / 100).toFixed(2)}`;
1415
case "cost_per_req":
1516
return `$${(value / 100).toFixed(2)}/req`;

src/lib/alerts/slack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function severityEmoji(severity: string): string {
1818
function formatValue(metric: string, value: number): string {
1919
switch (metric) {
2020
case "spend":
21+
case "cycle_spend":
2122
return `$${(value / 100).toFixed(2)}`;
2223
case "cost_per_req":
2324
return `$${(value / 100).toFixed(2)}/req`;

src/lib/anomaly/thresholds.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function detectThresholdAnomalies(config: DetectionConfig): Anomaly[] {
2727
metric: "spend",
2828
value: s.spend_cents,
2929
threshold: config.thresholds.maxSpendCentsPerCycle,
30-
message: `${s.name}: spend $${(s.spend_cents / 100).toFixed(2)} exceeds limit $${(config.thresholds.maxSpendCentsPerCycle / 100).toFixed(2)} (${s.fast_premium_requests} premium reqs)`,
30+
message: `${s.name}: spend $${(s.spend_cents / 100).toFixed(2)} exceeds limit $${(config.thresholds.maxSpendCentsPerCycle / 100).toFixed(2)}`,
3131
detectedAt: now,
3232
resolvedAt: null,
3333
alertedAt: null,

src/lib/anomaly/trends.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,30 @@ import {
55
getUserDailySpendHistory,
66
getCycleSpendWithModels,
77
getUserCostPerRequest,
8+
getUserCostDrivers,
89
} from "../data";
910

1011
const MIN_DAILY_SPEND_CENTS = 5000;
1112
const MIN_CYCLE_MEDIAN_CENTS = 1000;
1213

14+
function buildCostDriverNote(email: string, date?: string): string {
15+
const drivers = getUserCostDrivers(email, date);
16+
if (!drivers) return "";
17+
18+
const parts: string[] = [];
19+
if (drivers.thinking_pct > 0) {
20+
const thinkingDollars = (drivers.thinking_spend_cents / 100).toFixed(0);
21+
parts.push(`${drivers.thinking_pct}% thinking ($${thinkingDollars})`);
22+
}
23+
if (drivers.max_pct > 0) {
24+
const maxDollars = (drivers.max_spend_cents / 100).toFixed(0);
25+
parts.push(`${drivers.max_pct}% max-mode ($${maxDollars})`);
26+
}
27+
28+
if (parts.length === 0) return "";
29+
return ` · cost drivers: ${parts.join(", ")}`;
30+
}
31+
1332
export function detectTrendAnomalies(config: DetectionConfig): Anomaly[] {
1433
const anomalies: Anomaly[] = [];
1534
const now = new Date().toISOString();
@@ -23,7 +42,9 @@ export function detectTrendAnomalies(config: DetectionConfig): Anomaly[] {
2342
} = config.trends;
2443

2544
detectSpendSpikes(anomalies, now, spendSpikeMultiplier, spendSpikeLookbackDays);
26-
detectCycleOutliers(anomalies, now, cycleOutlierMultiplier);
45+
if (cycleOutlierMultiplier > 0) {
46+
detectCycleOutliers(anomalies, now, cycleOutlierMultiplier);
47+
}
2748
detectCostPerReqSpikes(
2849
anomalies,
2950
now,
@@ -58,14 +79,15 @@ function detectSpendSpikes(
5879
if (ratio > spikeMultiplier) {
5980
const todayDollars = (user.spend_cents / 100).toFixed(2);
6081
const avgDollars = (history.avg_spend / 100).toFixed(2);
82+
const driverNote = buildCostDriverNote(user.email, targetDate);
6183
anomalies.push({
6284
userEmail: user.email,
6385
type: "trend",
6486
severity: ratio > spikeMultiplier * 3 ? "critical" : "warning",
6587
metric: "spend",
6688
value: user.spend_cents,
6789
threshold: history.avg_spend * spikeMultiplier,
68-
message: `${user.name}: daily spend spiked to $${todayDollars} (${ratio.toFixed(1)}x their ${lookbackDays}-day avg of $${avgDollars}), model: ${user.most_used_model || "unknown"}`,
90+
message: `${user.name}: daily spend spiked to $${todayDollars} (${ratio.toFixed(1)}x their ${lookbackDays}-day avg of $${avgDollars}), model: ${user.most_used_model || "unknown"}${driverNote}`,
6991
detectedAt: now,
7092
resolvedAt: null,
7193
alertedAt: null,
@@ -97,10 +119,10 @@ function detectCycleOutliers(anomalies: Anomaly[], now: string, outlierMultiplie
97119
userEmail: user.email,
98120
type: "trend",
99121
severity: ratio > outlierMultiplier * 3 ? "critical" : "warning",
100-
metric: "spend",
122+
metric: "cycle_spend",
101123
value: user.spend_cents,
102124
threshold: median * outlierMultiplier,
103-
message: `${user.name}: cycle spend $${userDollars} is ${ratio.toFixed(1)}x the team median ($${medianDollars}), model: ${user.most_used_model || "unknown"}, ${user.fast_premium_requests} premium reqs`,
125+
message: `${user.name}: cycle spend $${userDollars} is ${ratio.toFixed(1)}x the team median ($${medianDollars}), model: ${user.most_used_model || "unknown"}`,
104126
detectedAt: now,
105127
resolvedAt: null,
106128
alertedAt: null,
@@ -136,9 +158,7 @@ function detectCostPerReqSpikes(
136158
const todayCpr = (user.today_cost_per_req / 100).toFixed(2);
137159
const histCpr = (user.hist_avg_cost_per_req / 100).toFixed(2);
138160
const todaySpend = (user.today_spend_cents / 100).toFixed(2);
139-
const maxModePct =
140-
user.today_reqs > 0 ? Math.round((user.today_max_mode_reqs / user.today_reqs) * 100) : 0;
141-
const maxModeNote = maxModePct > 0 ? ` [${maxModePct}% max mode]` : "";
161+
const driverNote = buildCostDriverNote(user.email);
142162

143163
anomalies.push({
144164
userEmail: user.email,
@@ -147,7 +167,7 @@ function detectCostPerReqSpikes(
147167
metric: "cost_per_req",
148168
value: Math.round(user.today_cost_per_req),
149169
threshold: Math.round(user.hist_avg_cost_per_req * spikeMultiplier),
150-
message: `${user.name}: cost/request spiked to $${todayCpr}/req (${ratio.toFixed(1)}x their avg of $${histCpr}/req), using ${user.today_top_model || "unknown"}${maxModeNote}, $${todaySpend} total today across ${user.today_reqs} reqs`,
170+
message: `${user.name}: cost/request spiked to $${todayCpr}/req (${ratio.toFixed(1)}x their avg of $${histCpr}/req), using ${user.today_top_model || "unknown"}, $${todaySpend} total today across ${user.today_reqs} reqs${driverNote}`,
151171
detectedAt: now,
152172
resolvedAt: null,
153173
alertedAt: null,

src/lib/data/sqlite.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,6 +3067,52 @@ export function getCycleSpendWithModels(): Array<{
30673067
}>;
30683068
}
30693069

3070+
export interface CostDriverSummary {
3071+
thinking_pct: number;
3072+
max_pct: number;
3073+
thinking_spend_cents: number;
3074+
max_spend_cents: number;
3075+
total_spend_cents: number;
3076+
top_model: string;
3077+
}
3078+
3079+
export function getUserCostDrivers(email: string, date?: string): CostDriverSummary | null {
3080+
const db = getDb();
3081+
const dateFilter = date ? `date(ue.timestamp/1000, 'unixepoch') = ?` : `date(ue.timestamp/1000, 'unixepoch') = date('now')`;
3082+
const params = date ? [email, date] : [email];
3083+
const row = db
3084+
.prepare(
3085+
`SELECT
3086+
ROUND(100.0 * SUM(CASE WHEN ue.model LIKE '%thinking%' THEN 1 ELSE 0 END) / COUNT(*)) as thinking_pct,
3087+
ROUND(100.0 * SUM(CASE WHEN ue.max_mode = 1 THEN 1 ELSE 0 END) / COUNT(*)) as max_pct,
3088+
ROUND(SUM(CASE WHEN ue.model LIKE '%thinking%' THEN ue.total_cents ELSE 0 END)) as thinking_spend_cents,
3089+
ROUND(SUM(CASE WHEN ue.max_mode = 1 THEN ue.total_cents ELSE 0 END)) as max_spend_cents,
3090+
ROUND(SUM(ue.total_cents)) as total_spend_cents
3091+
FROM usage_events ue
3092+
WHERE ue.user_email = ? AND ${dateFilter} AND ue.total_cents > 0`,
3093+
)
3094+
.get(...params) as { thinking_pct: number | null; max_pct: number | null; thinking_spend_cents: number; max_spend_cents: number; total_spend_cents: number } | undefined;
3095+
3096+
if (!row || !row.total_spend_cents) return null;
3097+
3098+
const topModel = db
3099+
.prepare(
3100+
`SELECT model, SUM(total_cents) as s FROM usage_events
3101+
WHERE user_email = ? AND ${dateFilter} AND total_cents > 0
3102+
GROUP BY model ORDER BY s DESC LIMIT 1`,
3103+
)
3104+
.get(...params) as { model: string } | undefined;
3105+
3106+
return {
3107+
thinking_pct: row.thinking_pct ?? 0,
3108+
max_pct: row.max_pct ?? 0,
3109+
thinking_spend_cents: row.thinking_spend_cents,
3110+
max_spend_cents: row.max_spend_cents,
3111+
total_spend_cents: row.total_spend_cents,
3112+
top_model: topModel?.model ?? "",
3113+
};
3114+
}
3115+
30703116
export function getActiveMembers(): Array<{ email: string; user_id: string | null; name: string }> {
30713117
const db = getDb();
30723118
return db

src/lib/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ export type AnomalySeverity = "info" | "warning" | "critical";
337337
export type AnomalyType = "threshold" | "zscore" | "trend";
338338
export type AnomalyMetric =
339339
| "spend"
340+
| "cycle_spend"
340341
| "requests"
341342
| "tokens"
342343
| "plan_exhausted"
@@ -412,7 +413,7 @@ export const DEFAULT_CONFIG: DetectionConfig = {
412413
trends: {
413414
spendSpikeMultiplier: 5,
414415
spendSpikeLookbackDays: 7,
415-
cycleOutlierMultiplier: 10,
416+
cycleOutlierMultiplier: 0,
416417
costPerReqSpikeMultiplier: 3,
417418
costPerReqMinSpendCents: 2000,
418419
costPerReqMinAbsoluteCents: 500,

0 commit comments

Comments
 (0)