Skip to content

feat: replace SMTP with Resend, switch Slack to bot token, make thresholds opt-in#9

Merged
ofershap merged 4 commits into
mainfrom
feat/alerts-resend-and-slack-bot
Feb 17, 2026
Merged

feat: replace SMTP with Resend, switch Slack to bot token, make thresholds opt-in#9
ofershap merged 4 commits into
mainfrom
feat/alerts-resend-and-slack-bot

Conversation

@ofershap
Copy link
Copy Markdown
Owner

Summary

  • Email alerts: Replaced nodemailer/SMTP (6 env vars) with Resend SDK (1 env var: RESEND_API_KEY). Same HTML template, same sendEmailAlert interface.
  • Slack alerts: Switched from webhook to bot token + chat.postMessage for richer control. Removed unused sendSlackResolution.
  • Thresholds opt-in: Static thresholds (spend, requests, tokens) now default to 0 (disabled). Z-score and trend detection remain active by default. Settings UI shows "disabled" hint when value is 0.
  • Cron: Passes DASHBOARD_URL to alert links so Slack/email alerts link back to the dashboard.
  • README: Updated section title, config defaults, alerting docs, removed stale CURSOR_ANALYTICS_API_KEY reference.

Test plan

  • Verify npm run typecheck passes
  • Verify detection engine skips threshold checks when values are 0
  • Verify Resend email sends when RESEND_API_KEY is set
  • Verify Slack bot token sends via chat.postMessage
  • Verify settings UI shows "disabled" for 0-value thresholds

Made with Cursor

…holds 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>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @ofershap, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the alerting infrastructure by upgrading email and Slack notification systems for improved efficiency and control. It also introduces more flexible threshold configurations, allowing static limits to be optionally enabled, thereby providing users with greater customization over their alert settings. These changes aim to simplify setup, enhance alert fidelity, and ensure relevant contextual links are always available.

Highlights

  • Email Alerting System Overhaul: The email alerting mechanism has been migrated from Nodemailer/SMTP to Resend SDK, significantly reducing the number of required environment variables for configuration and streamlining the email sending process.
  • Slack Alerting Enhancement: Slack alerts now utilize bot tokens and the chat.postMessage API, replacing the previous webhook-based system. This change offers richer control over messages and removes the unused sendSlackResolution function.
  • Thresholds Opt-in Mechanism: Static thresholds for spend, requests, and tokens are now opt-in, defaulting to 0 (disabled). The UI has been updated to reflect this 'disabled' state when a threshold value is zero, while Z-score and trend detection remain active by default.
  • Dashboard URL Integration: The cron job now passes the DASHBOARD_URL to alert links, ensuring that Slack and email notifications directly link back to the relevant dashboard pages.
  • Documentation Updates: The README.md and .env.example files have been updated to reflect the new configuration defaults, alerting documentation, and removal of stale references.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .cursor/rules/project-context.mdc
    • Updated the description for Slack alerts to reflect the switch to bot tokens and chat.postMessage.
  • .env.example
    • Removed SLACK_WEBHOOK_URL and SMTP-related environment variables (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM).
    • Added SLACK_BOT_TOKEN and SLACK_CHANNEL_ID for Slack bot integration.
    • Introduced DASHBOARD_URL for linking alerts to the dashboard.
    • Added RESEND_API_KEY and RESEND_FROM for the new email service.
  • README.md
    • Updated the main heading and section title to be more concise and relevant.
    • Modified the description of static thresholds to indicate they are optional and disabled by default.
    • Updated the 'Rich Alerting' section to reflect the use of Slack bot tokens and Resend for email.
    • Revised the .env configuration instructions to include new Slack and Resend variables and remove old SMTP and CURSOR_ANALYTICS_API_KEY references.
    • Changed default values for 'Max spend per cycle', 'Max requests per day', and 'Max tokens per day' to '0 (off)' in the settings table.
  • package-lock.json
    • Removed nodemailer and @types/nodemailer dependencies.
    • Added resend as a new dependency.
    • Introduced new transitive dependencies: @stablelib/base64, fast-sha256, postal-mime, standardwebhooks, svix, and uuid.
  • package.json
    • Removed nodemailer from direct dependencies.
    • Added resend as a new direct dependency.
  • src/app/api/cron/route.ts
    • Modified the sendAlerts function call to pass dashboardUrl from environment variables, enabling direct links in alerts.
  • src/app/settings/settings-client.tsx
    • Updated the description for 'Static Thresholds' to clarify they are hard limits per user and can be disabled by setting to 0.
    • Added a 'disabled' hint to the UI for 'Max spend / cycle', 'Max requests per day', and 'Max tokens per day' when their values are 0.
  • src/lib/alerts/email.ts
    • Replaced nodemailer import with Resend.
    • Refactored getTransporter to getClient to initialize the Resend SDK.
    • Updated sendEmailAlert to use the Resend API for sending emails, including error handling for Resend responses.
    • Changed the from email address to use RESEND_FROM environment variable.
  • src/lib/alerts/slack.ts
    • Added SLACK_API_URL constant.
    • Moved formatValue function definition.
    • Modified sendSlackAlert to use SLACK_BOT_TOKEN and SLACK_CHANNEL_ID for chat.postMessage API calls, including adding authorization headers.
    • Removed the sendSlackResolution function as it is no longer used.
    • Updated the sendSlackAlert response handling to check the ok property from the JSON response.
  • src/lib/anomaly/thresholds.ts
    • Modified detectThresholdAnomalies to only apply maxSpendCentsPerCycle threshold if its value is greater than 0.
    • Modified detectThresholdAnomalies to only apply maxRequestsPerDay threshold if its value is greater than 0.
  • src/lib/types.ts
    • Updated DEFAULT_CONFIG to set maxSpendCentsPerCycle, maxRequestsPerDay, and maxTokensPerDay to 0, effectively disabling them by default.
Activity
  • No specific human activity (comments, reviews, approvals) has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several significant improvements to the alerting and configuration systems. The migration from SMTP to Resend for emails and the switch to a bot token for Slack alerts are great changes that simplify configuration and enhance functionality. Making the static thresholds opt-in by default is also a sensible UX improvement.

My review includes a few suggestions to improve the robustness of the new alerting logic by adding more comprehensive error handling and logging. I've also identified a gap in the threshold detection feature where the maxTokensPerDay check, although configured, is not implemented in the detection engine. Addressing these points will make the system more reliable and complete.

Comment thread src/lib/alerts/slack.ts
Comment on lines 107 to 135
export async function sendSlackAlert(
anomaly: Anomaly,
incident: Incident,
options: { webhookUrl?: string; dashboardUrl?: string } = {},
options: { dashboardUrl?: string } = {},
): Promise<boolean> {
const webhookUrl = options.webhookUrl ?? process.env.SLACK_WEBHOOK_URL;
if (!webhookUrl) return false;
const token = process.env.SLACK_BOT_TOKEN;
const channel = process.env.SLACK_CHANNEL_ID;
if (!token || !channel) return false;

const blocks = buildAlertBlocks(anomaly, incident, options.dashboardUrl);

const response = await fetch(webhookUrl, {
const response = await fetch(SLACK_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json; charset=utf-8",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
channel,
text: `${severityEmoji(anomaly.severity)} ${anomaly.message} — ${anomaly.userEmail}`,
blocks,
}),
});

return response.ok;
}

export async function sendSlackResolution(
anomaly: Anomaly,
options: { webhookUrl?: string } = {},
): Promise<boolean> {
const webhookUrl = options.webhookUrl ?? process.env.SLACK_WEBHOOK_URL;
if (!webhookUrl) return false;

const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `:white_check_mark: Resolved: ${anomaly.message} — ${anomaly.userEmail}`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `:white_check_mark: *Resolved:* ${anomaly.message}\n*User:* ${anomaly.userEmail}`,
},
},
],
}),
});
if (!response.ok) return false;

return response.ok;
const data = (await response.json()) as { ok: boolean };
return data.ok;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

For better error handling and robustness, it's a good idea to wrap the fetch call in a try...catch block to handle network errors, similar to how sendEmailAlert is implemented. Additionally, when the response from Slack is not ok, the response body often contains a helpful error message. Logging this message would make debugging much easier.

export async function sendSlackAlert(
  anomaly: Anomaly,
  incident: Incident,
  options: { dashboardUrl?: string } = {},
): Promise<boolean> {
  const token = process.env.SLACK_BOT_TOKEN;
  const channel = process.env.SLACK_CHANNEL_ID;
  if (!token || !channel) return false;

  const blocks = buildAlertBlocks(anomaly, incident, options.dashboardUrl);

  try {
    const response = await fetch(SLACK_API_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        channel,
        text: `${severityEmoji(anomaly.severity)} ${anomaly.message}${anomaly.userEmail}`,
        blocks,
      }),
    });

    if (!response.ok) {
      const errorText = await response.text().catch(() => "Could not read error body");
      console.error(`[slack] Failed to send alert. Status: ${response.status}. Body: ${errorText}`);
      return false;
    }

    const data = (await response.json()) as { ok: boolean; error?: string };
    if (!data.ok) {
      console.error(`[slack] Slack API error: ${data.error}`);
      return false;
    }

    return true;
  } catch (error) {
    console.error("[slack] Exception while sending alert:", error);
    return false;
  }
}

) {
anomalies.push({
userEmail: r.email,
type: "threshold",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This file implements threshold checks for maxSpendCentsPerCycle and maxRequestsPerDay, but the check for maxTokensPerDay seems to be missing. The configuration for it exists and it's mentioned in the README, so it appears to be an incomplete feature. To complete the threshold detection, you should add logic to check for daily token usage against config.thresholds.maxTokensPerDay.

This would likely involve:

  1. Querying the usage_events table to sum total_tokens per user for the current day.
  2. Iterating through the results and creating an anomaly if a user's token count exceeds the configured threshold (and the threshold is > 0).

- 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>

const email = `${first.toLowerCase()}.${last.toLowerCase()}@acme-corp.com`;
const role = i < 3 ? "owner" : "member";
const userId = `usr_${Math.random().toString(36).slice(2, 14)}`;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 3 months ago

In general, to fix insecure randomness, replace uses of Math.random() (or other non‑cryptographic PRNGs) in security‑sensitive contexts with Node’s crypto module (randomBytes, randomUUID, or getRandomValues in browsers). Ensure the output length/format stays compatible with existing code (e.g., string length, allowed characters, prefixes), so you do not break consuming logic.

For this specific case, the best fix is to replace Math.random().toString(36).slice(2, 14) with a cryptographically secure random string generator while keeping a similar length and character set. In Node we can use crypto.randomBytes and map the resulting bytes into a base‑36 character set. Since this is a small utility script, we should avoid adding third‑party dependencies and instead rely on Node’s built‑in crypto. Concretely:

  • Add an import: import { randomBytes } from "node:crypto";
  • Define a helper function, e.g. generateSecureId(length: number): string, that uses randomBytes to produce a base‑36 string of the requested length. A simple approach is to generate bytes and for each byte take byte % 36 to index into "0123456789abcdefghijklmnopqrstuvwxyz", which avoids biases large enough to matter here and is clearly more secure than Math.random().
  • Replace the userId construction on line 253 with const userId = \usr_${generateSecureId(12)}`;` to keep the same length (12 chars) and format.

All changes are confined to scripts/generate-mock-db.ts: one new import, one new helper function (placed near the other utility functions, e.g., around dateStr/isoNow), and the replacement of the Math.random() expression.

Suggested changeset 1
scripts/generate-mock-db.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/generate-mock-db.ts b/scripts/generate-mock-db.ts
--- a/scripts/generate-mock-db.ts
+++ b/scripts/generate-mock-db.ts
@@ -2,6 +2,7 @@
 import { unlinkSync } from "node:fs";
 import path from "node:path";
 import { fileURLToPath } from "node:url";
+import { randomBytes } from "node:crypto";
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const DB_PATH = path.join(__dirname, "..", "data", "mock.db");
@@ -235,6 +236,17 @@
   return new Date().toISOString().replace("T", " ").slice(0, 19);
 }
 
+function generateSecureId(length: number): string {
+  const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
+  const bytes = randomBytes(length);
+  let result = "";
+  for (let i = 0; i < length; i++) {
+    const index = bytes[i] % chars.length;
+    result += chars[index];
+  }
+  return result;
+}
+
 function generateMembers(): Array<{ email: string; name: string; role: string; userId: string }> {
   const members: Array<{ email: string; name: string; role: string; userId: string }> = [];
   const usedNames = new Set<string>();
@@ -250,7 +262,7 @@
 
     const email = `${first.toLowerCase()}.${last.toLowerCase()}@acme-corp.com`;
     const role = i < 3 ? "owner" : "member";
-    const userId = `usr_${Math.random().toString(36).slice(2, 14)}`;
+    const userId = `usr_${generateSecureId(12)}`;
     members.push({ email, name: fullName, role, userId });
   }
   return members;
EOF
@@ -2,6 +2,7 @@
import { unlinkSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { randomBytes } from "node:crypto";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = path.join(__dirname, "..", "data", "mock.db");
@@ -235,6 +236,17 @@
return new Date().toISOString().replace("T", " ").slice(0, 19);
}

function generateSecureId(length: number): string {
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
const bytes = randomBytes(length);
let result = "";
for (let i = 0; i < length; i++) {
const index = bytes[i] % chars.length;
result += chars[index];
}
return result;
}

function generateMembers(): Array<{ email: string; name: string; role: string; userId: string }> {
const members: Array<{ email: string; name: string; role: string; userId: string }> = [];
const usedNames = new Set<string>();
@@ -250,7 +262,7 @@

const email = `${first.toLowerCase()}.${last.toLowerCase()}@acme-corp.com`;
const role = i < 3 ? "owner" : "member";
const userId = `usr_${Math.random().toString(36).slice(2, 14)}`;
const userId = `usr_${generateSecureId(12)}`;
members.push({ email, name: fullName, role, userId });
}
return members;
Copilot is powered by AI and may make mistakes. Always verify output.
ofershap and others added 2 commits February 17, 2026 23:31
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>
- 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>
@ofershap ofershap merged commit fe3a64f into main Feb 17, 2026
1 of 4 checks passed
@ofershap ofershap deleted the feat/alerts-resend-and-slack-bot branch February 17, 2026 21:44
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants