feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files
Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file Regression test for the alert_rules rebuild that drops its 4-type
|
||||
* `rule_type` CHECK: an existing DB keeps every rule and every fired alert
|
||||
* (alert_events cascades on rule delete, so the rebuild must run with foreign
|
||||
* keys off), and the new types can be stored afterwards.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const { it } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "alert-mig-")));
|
||||
const DB_PATH = path.join(ROOT, "t.db");
|
||||
process.env.DASHBOARD_DB_PATH = DB_PATH;
|
||||
process.env.CLAUDE_HOME = path.join(ROOT, "claude");
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data");
|
||||
|
||||
// The schema an older build created.
|
||||
{
|
||||
const Database = require("better-sqlite3");
|
||||
const old = new Database(DB_PATH);
|
||||
old.pragma("foreign_keys = ON");
|
||||
old.exec(`
|
||||
CREATE TABLE alert_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
rule_type TEXT NOT NULL CHECK(rule_type IN ('event_pattern','inactivity','status_duration','token_threshold')),
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
cooldown_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
CREATE TABLE alert_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rule_id TEXT NOT NULL,
|
||||
rule_name TEXT NOT NULL,
|
||||
rule_type TEXT NOT NULL,
|
||||
session_id TEXT,
|
||||
agent_id TEXT,
|
||||
message TEXT NOT NULL,
|
||||
details TEXT,
|
||||
triggered_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
acknowledged_at TEXT,
|
||||
FOREIGN KEY (rule_id) REFERENCES alert_rules(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO alert_rules (id, name, rule_type, config) VALUES ('r1', 'Old', 'inactivity', '{"minutes":5}');
|
||||
INSERT INTO alert_events (rule_id, rule_name, rule_type, message) VALUES ('r1', 'Old', 'inactivity', 'fired');
|
||||
`);
|
||||
old.close();
|
||||
}
|
||||
|
||||
const { db, stmts } = require("../db");
|
||||
|
||||
it("keeps rules and fired alerts, and accepts the new types", () => {
|
||||
assert.equal(db.prepare("SELECT name FROM alert_rules WHERE id = 'r1'").get().name, "Old");
|
||||
assert.equal(db.prepare("SELECT COUNT(*) c FROM alert_events WHERE rule_id = 'r1'").get().c, 1);
|
||||
stmts.insertAlertRule.run("r2", "Budget", "budget", '{"period":"day","limit_usd":1}', 1, 0);
|
||||
assert.equal(stmts.getAlertRule.get("r2").rule_type, "budget");
|
||||
// The cascade still works after the rename.
|
||||
stmts.deleteAlertRule.run("r1");
|
||||
assert.equal(db.prepare("SELECT COUNT(*) c FROM alert_events").get().c, 0);
|
||||
assert.equal(db.pragma("foreign_keys", { simple: true }), 1);
|
||||
fs.rmSync(ROOT, { recursive: true, force: true });
|
||||
});
|
||||
Reference in New Issue
Block a user