Skip to content

Commit fe3a64f

Browse files
feat: replace SMTP with Resend, switch Slack to bot token, make thresholds opt-in (#9)
* feat: replace SMTP with Resend, switch Slack to bot token, make thresholds opt-in - Replace nodemailer/SMTP with Resend SDK for email alerts (1 env var vs 6) - Switch Slack from webhook to bot token + chat.postMessage for richer control - Make static thresholds opt-in (default 0 = disabled), z-score and trends still active - Pass DASHBOARD_URL through cron to alert links - Update README: section title, config defaults, alerting docs - Remove unused sendSlackResolution function Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add mock database for safe demos and screenshots - Add scripts/generate-mock-db.ts: generates data/mock.db with 65 fake users, 30 days of activity, anomalies, billing groups, and analytics - Support DATABASE_PATH env var in db.ts for switching databases - Add npm run dev:mock and Dev Server (Mock Data) VS Code task - Commit mock.db (776KB) so cloners get demo data out of the box Co-authored-by: Cursor <cursoragent@cursor.com> * feat: rewrite anomaly detection to be spend-focused with smart batching Detection was too noisy — model shift and drift alerts flagged normal usage patterns, request-count alerts ignored cost, and 23+ individual Slack messages bombarded the channel. Detection changes: - Remove model shift detection (using an expensive model isn't inherently bad) - Remove drift/P75 detection (just identified power users, not actionable) - Remove request-based z-score (high request count on cheap models is fine) - Add daily spend spike: flags when today's spend > 5x personal average - Add cycle spend outlier: flags when cycle spend > 10x active team median - Z-score now spend-only, computed against active users (spend > $0) - Minimum $50/day floor to avoid alerts on trivial amounts - Critical threshold raised to 3x multiplier (was 2x) Alerting changes: - Slack uses bot token + chat.postMessage (replaces webhook) - Batch alerts: ≤3 anomalies send individual messages, >3 sends summary - Summary auto-chunks long lists to respect Slack's block character limit - Add logging to Slack and email modules (missing config, send success/failure) - Remove dead sendSlackResolution function - Pass DASHBOARD_URL to alert links Config changes: - New trend settings: spendSpikeMultiplier, spendSpikeLookbackDays, cycleOutlierMultiplier - getConfig() merges stored config with defaults for safe migration Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add npm publish support with npx setup command - Add bin/create.mjs: `npx cursor-usage-tracker my-tracker` clones the repo, installs deps, copies .env.example, and prints next steps - Remove "private": true to allow npm publishing - Add "files" field to ship only the bin/ directory (keeps package tiny) - Add @semantic-release/npm for auto-publish on release - Add NPM_TOKEN to release workflow - Update README quick start with npx option Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Ofer Shapira <ofershap@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9aea35c commit fe3a64f

21 files changed

Lines changed: 1338 additions & 391 deletions

File tree

.cursor/rules/project-context.mdc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ Single cron endpoint `POST /api/cron` does both: collect → detect → alert in
4343
| `src/lib/collector.ts` | Data collection pipeline (members, daily usage, spending, events, analytics, groups) |
4444
| `src/lib/anomaly/detector.ts` | Orchestrator — runs all 3 detection layers, deduplicates |
4545
| `src/lib/anomaly/thresholds.ts` | Layer 1: static limits (spend, requests, tokens) |
46-
| `src/lib/anomaly/zscore.ts` | Layer 2: statistical z-score vs team mean |
47-
| `src/lib/anomaly/trends.ts` | Layer 3: personal spikes, drift above P75, model shift |
46+
| `src/lib/anomaly/zscore.ts` | Layer 2: statistical z-score on daily spend vs active team mean |
47+
| `src/lib/anomaly/trends.ts` | Layer 3: daily spend spikes, cycle spend outliers |
4848
| `src/lib/incidents.ts` | Incident lifecycle: create, alert, acknowledge, resolve + MTTD/MTTI/MTTR |
49-
| `src/lib/alerts/slack.ts` | Slack webhook with block-kit messages |
50-
| `src/lib/alerts/email.ts` | SMTP email with HTML templates |
49+
| `src/lib/alerts/slack.ts` | Slack bot token + chat.postMessage with block-kit messages |
50+
| `src/lib/alerts/email.ts` | Email alerts via Resend with HTML templates |
5151
| `src/lib/date-utils.ts` | Date formatting helpers (formatDateShort, formatDateTick, formatDateLabel) |
5252
| `src/lib/format-utils.ts` | Model name shortening (MODEL_MAP regex → short labels) |
5353
| `src/app/api/cron/route.ts` | Main cron endpoint (collect + detect + alert) |
@@ -85,5 +85,5 @@ members, daily_usage, spending, usage_events, anomalies, incidents, config, coll
8585
## Important Caveats
8686

8787
- API response shapes may not exactly match the live Cursor API. If real responses differ, update `src/lib/types.ts` and `src/lib/cursor-client.ts`.
88-
- Trend detection can produce duplicate dedup keys (spike + drift both emit `trend:tokens` for same user) — first one wins, second is silently dropped.
88+
- Trend detection can produce duplicate dedup keys (spend spike + cycle outlier both emit `trend:spend` for same user) — first one wins, second is silently dropped.
8989
- CLI scripts (`npm run collect`, `npm run detect`) do NOT auto-load `.env` — use `source .env && export CURSOR_ADMIN_API_KEY` or run via the Next.js dev server cron endpoint instead.

.env.example

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
CURSOR_ADMIN_API_KEY=key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
22

3-
SLACK_WEBHOOK_URL=
3+
SLACK_BOT_TOKEN=xoxb-your-bot-token
4+
SLACK_CHANNEL_ID=C0123456789
45

5-
SMTP_HOST=smtp.gmail.com
6-
SMTP_PORT=587
7-
SMTP_USER=
8-
SMTP_PASS=
9-
SMTP_FROM=cursor-tracker@yourcompany.com
6+
DASHBOARD_URL=http://localhost:3000
7+
8+
RESEND_API_KEY=
9+
RESEND_FROM=Cursor Tracker <alerts@resend.dev>
1010
ALERT_EMAIL_TO=team-lead@yourcompany.com
1111

1212
CRON_SECRET=your-secret-for-cron-endpoint

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ jobs:
2626
- run: npx semantic-release
2727
env:
2828
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules/
22
.next/
33
dist/
44
data/*.db
5+
!data/mock.db
56
data/*.db-journal
67
data/*.db-shm
78
data/*.db-wal

.vscode/tasks.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
"color": "terminal.ansiCyan"
2121
}
2222
},
23+
{
24+
"label": "Dev Server (Mock Data)",
25+
"type": "shell",
26+
"command": "npm run dev:mock -- --turbopack -p 3456",
27+
"problemMatcher": [],
28+
"isBackground": true,
29+
"presentation": {
30+
"group": "cursor-tracker",
31+
"panel": "dedicated",
32+
"showReuseMessage": false,
33+
"clear": true,
34+
"reveal": "always",
35+
"focus": false
36+
},
37+
"icon": {
38+
"id": "globe",
39+
"color": "terminal.ansiYellow"
40+
}
41+
},
2342
{
2443
"label": "Collect Data",
2544
"type": "shell",

README.md

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
<h1 align="center">Cursor Usage Tracker</h1>
66

77
<p align="center">
8-
AI spend monitoring, anomaly detection, and alerting for teams on Cursor Enterprise.<br>
9-
Know who's burning through your budget before the invoice tells you.
8+
Know who's burning through your AI budget before the invoice tells you.
109
</p>
1110

1211
<p align="center">
@@ -21,7 +20,7 @@
2120

2221
---
2322

24-
## Why This Exists
23+
## AI Spend Is a Blind Spot
2524

2625
Engineering costs used to be two things: headcount and cloud infrastructure. You had tools for both. Then AI coding assistants showed up, and suddenly there's a third cost center that nobody has good tooling for.
2726

@@ -51,13 +50,12 @@ Developer uses Cursor → API collects data hourly → Engine detects anomaly
5150

5251
### How It Works
5352

54-
| What happens | Example |
55-
| -------------------------------------------- | ------------------------------------------------------- |
56-
| A developer exceeds the spend limit | `Bob spent $82 this cycle (limit: $50)` → Slack alert |
57-
| Someone's usage is 3x their personal average | `Token spike: 4.2x Alice's 7-day average` → Slack alert |
58-
| A user is statistically far from the team | `Bob: 2.8 std devs above team mean` → Slack alert |
59-
| Someone shifts to expensive models | `Opus usage jumped from 5% to 45%` → Slack alert |
60-
| Usage drifts above team P75 for days | `Above team P75 for 5 of last 6 days` → Slack alert |
53+
| What happens | Example |
54+
| ------------------------------------------ | ----------------------------------------------------------------------------- |
55+
| A developer exceeds the spend limit | `Bob spent $82 this cycle (limit: $50)` → Slack alert |
56+
| Someone's daily spend spikes | `Alice: daily spend spiked to $214 (4.2x her 7-day avg of $51)` → Slack alert |
57+
| A user's cycle spend is far above the team | `Bob: cycle spend $957 is 5.1x the team median ($188)` → Slack alert |
58+
| A user is statistically far from the team | `Bob: daily spend $214 is 3.2σ above team mean ($42)` → Slack alert |
6159

6260
Every alert includes who, what model, how much, and a link to their dashboard page so you can investigate immediately.
6361

@@ -67,11 +65,11 @@ Every alert includes who, what model, how much, and a link to their dashboard pa
6765

6866
### Three-Layer Anomaly Detection
6967

70-
| Layer | Method | What it catches |
71-
| -------------- | ------------- | --------------------------------------------------------------------------- |
72-
| **Thresholds** | Static limits | Spend > $50/cycle, > 500 requests/day, > 5M tokens/day |
73-
| **Z-Score** | Statistical | User 2+ standard deviations above team mean |
74-
| **Trends** | Behavioral | Personal spikes, sustained drift above P75, model shift to expensive models |
68+
| Layer | Method | What it catches |
69+
| -------------- | ------------- | ----------------------------------------------------------------------------- |
70+
| **Thresholds** | Static limits | Optional hard caps on spend, requests, or tokens (disabled by default) |
71+
| **Z-Score** | Statistical | User daily spend 2.5+ standard deviations above team mean (active users only) |
72+
| **Trends** | Spend-based | Daily spend spikes vs personal average, cycle spend outliers vs team median |
7573

7674
### Incident Lifecycle (MTTD / MTTI / MTTR)
7775

@@ -91,8 +89,8 @@ Anomaly Detected ──→ Alert Sent ──→ Acknowledged ──→ Resolved
9189

9290
### Rich Alerting
9391

94-
- **Slack**: Block Kit messages with severity, user, model, value vs threshold, and dashboard links
95-
- **Email**: HTML-formatted alerts with the same context
92+
- **Slack**: Block Kit messages via bot token (`chat.postMessage`) with severity, user, model, value vs threshold, and dashboard links. Batches alerts automatically (individual messages for 1-3 anomalies, single summary for 4+).
93+
- **Email**: HTML-formatted alerts via [Resend](https://resend.com) (one API key, no SMTP config)
9694

9795
### Web Dashboard
9896

@@ -116,7 +114,16 @@ Anomaly Detected ──→ Alert Sent ──→ Acknowledged ──→ Resolved
116114
| Admin API key | Cursor dashboard → Settings → Advanced → Admin API Keys |
117115
| Node.js 18+ | [nodejs.org](https://nodejs.org) |
118116

119-
### 1. Clone and install
117+
### 1. Set up
118+
119+
**Option A: One command**
120+
121+
```bash
122+
npx cursor-usage-tracker my-tracker
123+
cd my-tracker
124+
```
125+
126+
**Option B: Manual clone**
120127

121128
```bash
122129
git clone https://github.com/ofershap/cursor-usage-tracker.git
@@ -136,20 +143,19 @@ Edit `.env`:
136143
# Required
137144
CURSOR_ADMIN_API_KEY=your_admin_api_key
138145

139-
# Alerting (at least one recommended)
140-
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxx
146+
# Alerting — Slack (at least one alerting channel recommended)
147+
SLACK_BOT_TOKEN=xoxb-your-bot-token # bot token with chat:write scope
148+
SLACK_CHANNEL_ID=C0123456789 # channel to post alerts to
149+
150+
# Dashboard URL (used in alert links)
151+
DASHBOARD_URL=http://localhost:3000
141152

142153
# Optional
143-
CURSOR_ANALYTICS_API_KEY=your_analytics_key # for Insights page (DAU, model breakdowns, MCP)
144154
CRON_SECRET=your_secret_here # protects the cron endpoint
145155
DASHBOARD_PASSWORD=your_password # optional basic auth for the dashboard
146156

147-
# Email alerts (optional)
148-
SMTP_HOST=smtp.gmail.com
149-
SMTP_PORT=587
150-
SMTP_USER=you@gmail.com
151-
SMTP_PASS=app_password
152-
SMTP_FROM=cursor-tracker@yourcompany.com
157+
# Email alerts via Resend (optional)
158+
RESEND_API_KEY=re_xxxxxxxxxxxx
153159
ALERT_EMAIL_TO=team-lead@company.com
154160
```
155161

@@ -256,15 +262,16 @@ flowchart TB
256262

257263
All detection thresholds are configurable via the Settings page or the API:
258264

259-
| Setting | Default | What it does |
260-
| -------------------- | ------- | ------------------------------------------------- |
261-
| Max spend per cycle | $200 | Alert when a user exceeds this in a billing cycle |
262-
| Max requests per day | 200 | Alert on excessive daily request count |
263-
| Max tokens per day | 5M | Alert on excessive daily token consumption |
264-
| Z-score multiplier | 2.5 | How many standard deviations above mean to flag |
265-
| Z-score window | 7 days | Historical window for statistical comparison |
266-
| Spike multiplier | 3.0x | Alert when today > N× user's personal average |
267-
| Drift days above P75 | 3 | Consecutive days above team P75 to flag |
265+
| Setting | Default | What it does |
266+
| ------------------------ | ------- | -------------------------------------------------------------- |
267+
| Max spend per cycle | 0 (off) | Alert when a user exceeds this in a billing cycle |
268+
| Max requests per day | 0 (off) | Alert on excessive daily request count |
269+
| Max tokens per day | 0 (off) | Alert on excessive daily token consumption |
270+
| Z-score multiplier | 2.5 | How many standard deviations above mean to flag (spend + reqs) |
271+
| Z-score window | 7 days | Historical window for statistical comparison |
272+
| Spend spike multiplier | 5.0x | Alert when today's spend > N× user's personal daily average |
273+
| Spend spike lookback | 7 days | How many days of history to compare against |
274+
| Cycle outlier multiplier | 10.0x | Alert when cycle spend > N× team median (active users only) |
268275

269276
---
270277

bin/create.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
3+
import { execSync } from "node:child_process";
4+
import { existsSync, copyFileSync } from "node:fs";
5+
import { resolve, basename } from "node:path";
6+
7+
const REPO = "https://github.com/ofershap/cursor-usage-tracker.git";
8+
const dirName = process.argv[2] || "cursor-usage-tracker";
9+
const targetDir = resolve(process.cwd(), dirName);
10+
11+
function run(cmd, opts = {}) {
12+
execSync(cmd, { stdio: "inherit", ...opts });
13+
}
14+
15+
function log(msg) {
16+
console.log(`\n\x1b[36m${msg}\x1b[0m`);
17+
}
18+
19+
if (existsSync(targetDir)) {
20+
console.error(`\x1b[31mDirectory "${dirName}" already exists.\x1b[0m`);
21+
process.exit(1);
22+
}
23+
24+
log(`Cloning cursor-usage-tracker into ${dirName}...`);
25+
run(`git clone --depth 1 ${REPO} "${targetDir}"`);
26+
27+
log("Installing dependencies...");
28+
run("npm install", { cwd: targetDir });
29+
30+
const envExample = resolve(targetDir, ".env.example");
31+
const envFile = resolve(targetDir, ".env");
32+
if (existsSync(envExample) && !existsSync(envFile)) {
33+
copyFileSync(envExample, envFile);
34+
}
35+
36+
log("Done! Next steps:");
37+
console.log(`
38+
cd ${dirName}
39+
40+
1. Edit .env and add your CURSOR_ADMIN_API_KEY
41+
Get it from: Cursor dashboard → Settings → Advanced → Admin API Keys
42+
43+
2. Start the dashboard:
44+
npm run dev
45+
46+
3. Collect your first data:
47+
npm run collect
48+
49+
4. Open http://localhost:3000
50+
51+
Docs: https://github.com/ofershap/cursor-usage-tracker
52+
`);

data/mock.db

4 KB
Binary file not shown.

0 commit comments

Comments
 (0)