Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 108 additions & 30 deletions herdr-insights/mods/insights/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,6 @@ export function startOfWeek(day, weekStartsOn = 0) {
return addDays(day, -offset);
}

export function endOfWeek(day, weekStartsOn = 0) {
const start = startOfWeek(day, weekStartsOn);
return start ? addDays(start, 6) : null;
}

export function weekdayLabels(weekStartsOn = 0) {
const names = weekStartsOn === 1
? ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
Expand Down Expand Up @@ -286,18 +281,6 @@ export function normalizeConfig(config) {
};
}

export function isSnapshot(value) {
return Boolean(
value
&& typeof value === "object"
&& value.version === 1
&& Array.isArray(value.rows)
&& value.config
&& typeof value.config === "object"
&& !Array.isArray(value.config),
);
}

export function normalizeTokens(tokens) {
const value = tokens && typeof tokens === "object" ? tokens : {};
const hasParts = ["input", "output", "cacheRead", "cacheWrite"].some((key) => Object.prototype.hasOwnProperty.call(value, key));
Expand Down Expand Up @@ -397,10 +380,6 @@ function matchesCompiled(row, filters) {
return true;
}

export function matchesFilters(row, filters = {}) {
return matchesCompiled(row, compileFilters(filters));
}

export function filterRows(rows, filters = {}) {
const compiled = compileFilters(filters);
return (rows ?? []).filter((row) => matchesCompiled(row, compiled));
Expand Down Expand Up @@ -571,15 +550,6 @@ export function streakStats(rows, options = {}) {
return { current, longest, activeDays: days.length, lastActive: previous };
}

export function dailyTotals(rows, includeCache = true) {
const totals = new Map();
for (const row of rows ?? []) {
if (!isDay(row?.day)) continue;
totals.set(row.day, (totals.get(row.day) ?? 0) + rowTokenTotal(row, includeCache));
}
return totals;
}

export function peakDay(rows, includeCache = true) {
const totals = new Map();
const sessions = new Map();
Expand Down Expand Up @@ -1160,6 +1130,114 @@ export function compactDashboard(derived) {
};
}

/** Escapes one value for a Markdown table cell. */
function markdownCell(value) {
return String(value ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
}

function markdownChange(entry) {
const change = entry?.change;
if (!change || change.kind === "none") return "—";
if (change.kind === "new") return "New";
const previous = Number(entry?.previous);
return Number.isFinite(previous)
? `${change.label} (${formatInt(previous)} before)`
: String(change.label ?? "—");
}

/**
* A portable Markdown report of one prepared dashboard. Pure: it reads only the
* compact presentation already held by the view, so exporting never rescans,
* re-derives analytics, or runs the collector. Token counts stay exact; the
* dashboard's compact K/M/B labels are intentionally not reused.
*/
export function dashboardMarkdown(dashboard, options = {}) {
const now = options.now instanceof Date ? options.now : new Date(options.now ?? Date.now());
const generated = Number.isNaN(now.getTime()) ? "" : `${dayKey(now)} ${pad2(now.getHours())}:${pad2(now.getMinutes())}`;
const includeCache = dashboard.includeCache !== false;
const range = dashboard.range ?? {};
const lifetime = dashboard.lifetime ?? {};
const selectedTotals = dashboard.selectedTotals ?? {};
const selectedTop = dashboard.selectedTop ?? null;
const leaders = dashboard.leaders ?? {};
const heatmap = dashboard.heatmap ?? {};
const filters = dashboard.filters ?? {};
const rangeLabel = range.label ?? (dashboard.period === "all" ? "All time" : "Selected period");
const filterLabel = (value) => (!value || value === "all" ? "All" : markdownCell(value));
const dataThrough = dashboard.today ?? range.end ?? "";
const selectedDay = isDay(dashboard.selectedDay) ? dashboard.selectedDay : dataThrough;
const lines = [];

lines.push(`# Insights — ${rangeLabel}`, "");
lines.push(`- Generated: ${generated || "unknown"} (local)`);
lines.push(`- Data through: ${dayLabel(dataThrough) || "unknown"}`);
lines.push(`- Period: ${rangeLabel}${range.start && range.end ? ` (${range.start} to ${range.end})` : ""}`);
lines.push(`- Filters: Provider: ${filterLabel(filters.provider)} · Model: ${filterLabel(filters.model)}`);
lines.push(`- Tokens: input + output${includeCache ? " + cache reads + cache writes" : " (cache excluded)"}`);
lines.push("", "Lifetime and token history cover every session. Model usage follows the selected period and filters.", "");

lines.push("## Lifetime", "");
lines.push("| Metric | Value |", "| --- | ---: |");
lines.push(`| Tokens | ${formatInt(lifetime.total)} |`);
lines.push(`| Sessions | ${formatInt(lifetime.sessions)} |`);
lines.push(`| Active days | ${formatInt(lifetime.days)} |`);
lines.push(`| Messages | ${formatInt(lifetime.messages)} |`);
lines.push(`| Peak day | ${dashboard.peak?.day ? `${formatInt(dashboard.peak.tokens)} tokens on ${dayLabel(dashboard.peak.day)}` : "—"} |`);
lines.push(`| Longest task (observed) | ${formatDuration(lifetime.longestTaskMs)} |`);
lines.push(`| Longest streak | ${formatInt(dashboard.streaks?.longest)} days |`);
lines.push(`| Current streak | ${formatInt(dashboard.streaks?.current)} days |`, "");

lines.push(`## ${selectedDay === dashboard.today ? "Today" : "Selected day"} — ${dayLabel(selectedDay, { weekday: true })}`, "");
lines.push("| Metric | Value |", "| --- | ---: |");
lines.push(`| Tokens | ${formatInt(selectedTotals.total)} |`);
lines.push(`| Input | ${formatInt(selectedTotals.input)} |`);
lines.push(`| Output | ${formatInt(selectedTotals.output)} |`);
lines.push(`| Reasoning (inside output) | ${formatInt(selectedTotals.reasoning)} |`);
lines.push(`| Cache reads | ${formatInt(selectedTotals.cacheRead)} |`);
lines.push(`| Cache writes | ${formatInt(selectedTotals.cacheWrite)} |`);
lines.push(`| Cache hit rate | ${formatHitRate(selectedTotals.input, selectedTotals.cacheRead)} |`);
lines.push(`| Sessions | ${formatInt(selectedTotals.sessions)} |`);
lines.push(`| Messages | ${formatInt(selectedTotals.messages)} |`);
lines.push(`| Top model | ${selectedTop ? `${markdownCell(selectedTop.name)} (${formatInt(selectedTop.tokens)} tokens)` : "—"} |`, "");

lines.push(`## ${rangeLabel} model usage`, "");
if (!Array.isArray(leaders.entries) || leaders.entries.length === 0) {
lines.push("_No usage in this period._", "");
} else {
lines.push(`${formatInt(leaders.totalTokens)} tokens · ${formatInt(leaders.totalSessions)} sessions · ${formatInt(leaders.totalGroups)} models`, "");
lines.push(leaders.groupBy === "provider"
? "| # | Provider | Models | Tokens | Share | Sessions | vs previous |"
: "| # | Model | Provider(s) | Tokens | Share | Sessions | vs previous |");
lines.push("| ---: | --- | --- | ---: | ---: | ---: | --- |");
for (const entry of leaders.entries) {
lines.push(`| ${formatInt(entry.rank)} | ${markdownCell(entry.name)} | ${markdownCell(entry.sublabel)} | ${formatInt(entry.tokens)} | ${round1(Number(entry.sharePct) || 0)}% | ${formatInt(entry.sessions)} | ${markdownChange(entry)} |`);
}
if (leaders.totalGroups > leaders.entries.length) {
lines.push("", `Showing top ${leaders.entries.length} of ${leaders.totalGroups} by tokens.`);
}
lines.push("");
}

const weeks = Array.isArray(heatmap.weeks) ? heatmap.weeks : [];
const activeWeeks = weeks.filter((week) => {
const first = week?.days?.[0];
return Number(first?.weekTotal) > 0 || Number(first?.weekSessions) > 0;
});
if (activeWeeks.length > 0) {
lines.push(`## Token history — ${formatInt(heatmap.windowTotal)} tokens in window`, "");
lines.push("| Week of | Tokens | Sessions |");
lines.push("| --- | ---: | ---: |");
for (const week of activeWeeks) {
const first = week?.days?.[0];
lines.push(`| ${dayLabel(week.start)} | ${formatInt(first?.weekTotal)} | ${formatInt(first?.weekSessions)} |`);
}
if (activeWeeks.length < weeks.length) lines.push("", "Weeks without usage are omitted.");
lines.push("");
}

return `${lines.join("\n")}\n`;
}

function isDashboard(value) {
const object = (item) => item !== null && typeof item === "object" && !Array.isArray(item);
const arrayOf = (items, predicate) => Array.isArray(items) && items.every(predicate);
Expand Down
4 changes: 0 additions & 4 deletions herdr-insights/mods/insights/core/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,3 @@ export function sourcesSignature(config) {
export function defaultConfig() {
return normalizeConfig(DEFAULT_CONFIG);
}

export function adapterForSource(source) {
return source.adapter;
}
45 changes: 0 additions & 45 deletions herdr-insights/mods/insights/core/scan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -561,51 +561,6 @@ function createRecordConsumer(store, entry, source, sink) {
});
}

async function parseBuffers(
store,
entry,
source,
chunks,
startOffset,
endOffset,
sink,
options = {},
) {
const consumer = createRecordConsumer(store, entry, source, sink);
consumer.offset = startOffset;
consumer.lineNo = entry.lineNo ?? 0;
const now = nowFn(options);
const delay = pausableDelay(options.streamDelayMs);
let chunkStart = startOffset;
let chunksSeen = 0;
let bytes = 0;
for await (const chunk of chunks) {
if (chunksSeen > 0) {
if (stopRequested(options)) throw new ScanStopError("stop");
if (deadlineReached(options, now())) throw new ScanStopError("budget");
}
if (delay) await delay();
consumer.feedAt(chunk, chunkStart);
chunkStart += chunk.length;
bytes += chunk.length;
chunksSeen += 1;
if (chunksSeen % PROGRESS_CHUNK_INTERVAL === 0) {
options.onStreamProgress?.({
file: entry.path,
bytes,
phase: "decompress",
});
}
}
consumer.finish(endOffset ?? chunkStart);
if (consumer.skippedLargeLine) {
sink.warn(
`${source.adapter} ${entry.path}: a line larger than ${MAX_LINE_BYTES} bytes was skipped.`,
);
}
return { offset: consumer.offset, lineNo: consumer.lineNo, bytes };
}

async function scanPlainFile(
store,
entry,
Expand Down
39 changes: 1 addition & 38 deletions herdr-insights/mods/insights/core/store.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFile, stat, unlink } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import { storeRevisionFingerprint, writePresentationCache } from "./cache.mjs";
import { defaultConfig, migrateLoadedConfig, normalizeConfig } from "./config.mjs";
import {
Expand Down Expand Up @@ -683,28 +683,6 @@ export function removeLatestFallbackTaskForSession(store, session) {
return null;
}

export function removeFallbackTasksForSession(store, file, session) {
if (!Array.isArray(file.tasks) || file.tasks.length === 0) return;
store._tx?.trackFile(file);
const sHash = sessionHash(session);
const keep = [];
for (const key of file.tasks) {
const entry = store.tasks[key];
if (
Array.isArray(entry) &&
entry[0] === file.id &&
entry[3] === 0 &&
entry[4] === sHash
) {
store._tx?.touchTask(key);
delete store.tasks[key];
} else {
keep.push(key);
}
}
file.tasks = keep;
}

function emptyAggregate(meta) {
return {
day: meta.day,
Expand Down Expand Up @@ -989,21 +967,6 @@ export async function saveStore(
return savedAt;
}

export async function fileStatOrNull(file) {
try {
return await stat(file);
} catch (error) {
if (error?.code === "ENOENT") return null;
throw error;
}
}

export async function removeStoreFile(storePath) {
await unlink(storePath).catch((error) => {
if (error?.code !== "ENOENT") throw error;
});
}

export function dedupKeyFor(harness, ...parts) {
return hashKey("usage", harness, ...parts);
}
Expand Down
Loading
Loading