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:
@@ -4,7 +4,9 @@
|
||||
* includes reasoning), duplicate totals, a mid-session model switch, a fork's
|
||||
* copied history skipped, a sub-agent thread folded into its root session,
|
||||
* incremental reads, a deleted session not resurrected, and a Claude row never
|
||||
* touched. CODEX_HOME and the DB live in a temp dir.
|
||||
* touched; a remote source's mirror read with its own source tag, home and
|
||||
* activity window; a folder the user ignores never written. CODEX_HOME and the
|
||||
* DB live in a temp dir.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -427,4 +429,110 @@ describe("ingestCodexUsage", () => {
|
||||
const r = await ingestCodexUsage({ now: Date.now() });
|
||||
assert.deepEqual(r, { files: 0, sessions: 0 });
|
||||
});
|
||||
|
||||
describe("a remote source's mirror", () => {
|
||||
const MIRROR = path.join(ROOT, "remote-sources", "src_aaa", "codex");
|
||||
const remote = { home: MIRROR, source: "src_aaa", activeMs: 600_000 };
|
||||
function writeMirrored(id, lines, mtime) {
|
||||
const day = path.join(MIRROR, "sessions", "2026", "09", "29");
|
||||
fs.mkdirSync(day, { recursive: true });
|
||||
const file = path.join(day, `rollout-2026-09-29T01-00-00-${id}.jsonl`);
|
||||
fs.writeFileSync(file, jsonl(lines));
|
||||
fs.utimesSync(file, mtime, mtime);
|
||||
}
|
||||
beforeEach(() => fs.rmSync(MIRROR, { recursive: true, force: true }));
|
||||
|
||||
it("writes its sessions and tokens tagged with the source, named from its own index", async () => {
|
||||
writeMirrored(
|
||||
"r1",
|
||||
[meta("r1"), ctx("gpt-a"), tc([10, 4, 2], [10, 4, 2])],
|
||||
new Date("2026-09-29T02:00:00Z")
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(MIRROR, "session_index.jsonl"),
|
||||
JSON.stringify({ id: "r1", thread_name: "Remote work" }) + "\n"
|
||||
);
|
||||
const r = await ingestCodexUsage({ now: Date.parse("2026-09-29T03:00:00Z"), ...remote });
|
||||
assert.equal(r.sessions, 1);
|
||||
const s = stmts.getSession.get("r1");
|
||||
assert.equal(s.source, "src_aaa");
|
||||
assert.equal(s.tool, "codex");
|
||||
assert.equal(s.name, "Remote work");
|
||||
assert.deepEqual(usage("r1"), [{ model: "gpt-a", i: 6, r: 4, o: 2 }]);
|
||||
// The local pass never sees the mirror, and leaves the row alone.
|
||||
await ingestCodexUsage({ now: Date.parse("2026-09-29T03:00:00Z") });
|
||||
assert.equal(stmts.getSession.get("r1").source, "src_aaa");
|
||||
});
|
||||
|
||||
it("never writes over a session another source owns", async () => {
|
||||
writeRollout("dup", [meta("dup"), ctx("gpt-a"), tc([5, 0, 1], [5, 0, 1])]);
|
||||
await ingestCodexUsage({ now: Date.parse("2026-09-29T03:00:00Z") });
|
||||
writeMirrored(
|
||||
"dup",
|
||||
[meta("dup"), ctx("gpt-b"), tc([50, 0, 9], [50, 0, 9])],
|
||||
new Date("2026-09-29T02:30:00Z")
|
||||
);
|
||||
await ingestCodexUsage({ now: Date.parse("2026-09-29T03:00:00Z"), ...remote });
|
||||
assert.equal(stmts.getSession.get("dup").source, "local");
|
||||
assert.equal(stmts.getSession.get("dup").model, "gpt-a");
|
||||
});
|
||||
|
||||
it("keeps its own activity window, and only its own settle completes it", async () => {
|
||||
const t0 = Date.parse("2026-09-29T03:00:00Z");
|
||||
writeMirrored(
|
||||
"r2",
|
||||
[meta("r2"), ctx("gpt-a"), tc([1, 0, 1], [1, 0, 1])],
|
||||
new Date(t0 - 300_000)
|
||||
);
|
||||
await ingestCodexUsage({ now: t0, ...remote });
|
||||
assert.equal(stmts.getSession.get("r2").status, "active"); // 5 min < 10 min window
|
||||
settleActive(() => {}, t0 + 900_000); // the local settle: not its row
|
||||
assert.equal(stmts.getSession.get("r2").status, "active");
|
||||
settleActive(() => {}, t0 + 900_000, remote);
|
||||
assert.equal(stmts.getSession.get("r2").status, "completed");
|
||||
});
|
||||
});
|
||||
|
||||
it("never writes a session whose folder the user ignores", async () => {
|
||||
process.env.AGENTHUB_IGNORE_CWD = "/w";
|
||||
try {
|
||||
writeRollout("ig", [meta("ig"), ctx("gpt-a"), tc([5, 0, 1], [5, 0, 1])]);
|
||||
await ingestCodexUsage({ now: Date.parse("2026-09-29T03:00:00Z") });
|
||||
assert.equal(stmts.getSession.get("ig"), undefined);
|
||||
} finally {
|
||||
delete process.env.AGENTHUB_IGNORE_CWD;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("service tier", () => {
|
||||
const tier = (t) => ({
|
||||
timestamp: "2026-09-29T01:00:01.500Z",
|
||||
type: "event_msg",
|
||||
payload: { type: "thread_settings_applied", thread_settings: { service_tier: t } },
|
||||
});
|
||||
it("books fast/priority as speed fast and flex as service_tier flex", async () => {
|
||||
writeRollout("tier1", [
|
||||
meta("tier1"),
|
||||
ctx("gpt-a"),
|
||||
tc([10, 0, 1], [10, 0, 1]),
|
||||
tier("fast"),
|
||||
tc([30, 0, 3], [20, 0, 2]),
|
||||
tier("flex"),
|
||||
tc([60, 0, 6], [30, 0, 3]),
|
||||
tier("default"),
|
||||
tc([100, 0, 10], [40, 0, 4]),
|
||||
]);
|
||||
await ingestCodexUsage({ broadcast: () => {}, now: Date.parse("2026-09-29T03:00:00Z") });
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT speed, service_tier AS t, input_tokens AS i FROM token_usage WHERE session_id = 'tier1' ORDER BY i"
|
||||
)
|
||||
.all();
|
||||
assert.deepEqual(rows, [
|
||||
{ speed: "fast", t: "standard", i: 20 },
|
||||
{ speed: "standard", t: "flex", i: 30 },
|
||||
{ speed: "standard", t: "standard", i: 50 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user