Usage tracking
Usage tracking records who used what: every command, component, context-menu and prefix-command invocation — successful or errored — becomes a UsageEvent that spearkit can persist…
Usage tracking vs the logger
These look similar but answer different questions, and they are completely independent sinks:
| Logger | Usage tracking | |
|---|---|---|
| Question | What is the bot doing? (diagnostics) | Who used which feature? (audit) |
| Content | Free-form messages, levels, errors, internals | Structured UsageEvents for every completed use (with its outcome) |
| Sinks | Console / your log pipeline | A database store and/or a Discord channel |
| Configured by | the logger option | the usage option |
Both successes and handler errors are recorded as usage events — an error carries
outcome: "error" and an errorMessage — so usage is a complete audit trail.
The logger is for debugging; usage tracking is for analytics,
audit trails, and "top commands" dashboards.
Enabling it
Set the usage option. Provide a store (a database), a channel (a Discord
channel id to mirror events into), or both:
import { MemoryUsageStore, SpearClient } from "spearkit";
const client = new SpearClient({
usage: {
store: new MemoryUsageStore(),
channel: "123456789012345678", // optional: also post each event here
},
});Once enabled, spearkit auto-tracks every command, component, context-menu and prefix-command invocation — successes and errors alike — with no tracking code in your handlers.
The usage event
Each tracked use is a UsageEvent:
interface UsageEvent {
type: UsageType; // "command" | "prefix" | "component" | "event"
name: string; // command / component / event name
userId?: string;
userTag?: string;
guildId?: string | null;
channelId?: string | null;
detail?: string; // free-form extra detail
outcome?: UsageOutcome; // "success" | "error"
durationMs?: number; // handler wall-clock time
options?: Readonly<Record<string, UsageMetaValue>>; // snapshot of typed options
errorMessage?: string; // set when outcome === "error"
timestamp: Date;
}
type UsageType = "command" | "prefix" | "component" | "event";
type UsageOutcome = "success" | "error";
type UsageMetaValue = string | number | boolean | null;Stores (the database)
A store is any object implementing UsageStore:
interface UsageStore {
record(event: UsageEvent): void | Promise<void>;
all(): UsageEvent[] | Promise<readonly UsageEvent[]>;
}spearkit ships two.
MemoryUsageStore
In-memory and synchronous — ideal for tests, prototypes, and live dashboards. Pass an optional cap to keep only the most recent N events:
import { MemoryUsageStore } from "spearkit";
const store = new MemoryUsageStore(1_000); // keep the last 1,000 events
store.all(); // readonly UsageEvent[]
store.size; // number of events held
store.byUser("123..."); // UsageEvent[] for one user id
store.clear(); // forget everythingJsonFileUsageStore
Durable and dependency-free: appends one event per line as newline-delimited
JSON (.jsonl). all() reads the file back and parses it, so it behaves like a
small file-backed database:
import { JsonFileUsageStore } from "spearkit";
const store = new JsonFileUsageStore("./usage.jsonl");
await store.record({ type: "command", name: "ping", timestamp: new Date() });
const events = await store.all(); // readonly UsageEvent[]The directory is created on demand. Because all() is async here (it reads from
disk), always await it.
BufferedUsageStore (high volume)
Wrap any store so bursts become bounded batches instead of one database/file call per interaction:
const usageStore = new BufferedUsageStore(databaseUsageStore, {
batchSize: 250,
flushIntervalMs: 1_000,
maxBuffered: 25_000,
onDrop: (total) => metrics.usageDropped.set(total),
});
const client = new SpearClient({ usage: { store: usageStore } });
client.enableGracefulShutdown({
onShutdown: () => usageStore.close(),
});Implement BatchUsageStore.recordMany(events) on a database adapter for one
bulk insert per batch. MemoryUsageStore and JsonFileUsageStore already do.
The buffer drops oldest telemetry when full rather than growing without bound;
observe store.dropped / onDrop.
Querying the store
client.usage.store is the store you configured — query it directly. Note that
all() may be synchronous (MemoryUsageStore) or asynchronous
(JsonFileUsageStore); awaiting works for both:
const store = client.usage.store;
if (store !== undefined) {
const events = await store.all();
const topCommand = events.filter((e) => e.type === "command").length;
console.log(`${topCommand} command uses recorded`);
}The Discord channel reporter
Besides (or instead of) a store, you can mirror each event into a Discord
channel. Pass channel (a channel id) in the usage option; spearkit posts one
line per event using formatUsage:
import { SpearClient } from "spearkit";
new SpearClient({ usage: { channel: "123456789012345678" } });formatUsage(event) is the default renderer (e.g. `command` **ping** by user#0001 in <#…>). Override it with format to control the line:
import { SpearClient, type UsageEvent } from "spearkit";
new SpearClient({
usage: {
channel: "123456789012345678",
format: (event: UsageEvent) => `${event.userTag ?? "someone"} used ${event.name}`,
},
});The usage tracker
client.usage is a UsageTracker. The client configures it from the usage
option, but you can drive it directly:
| Member | Description |
|---|---|
setStore(store) | Set (or swap) the persistence store. |
reportTo(channelId, format?) | Mirror events into a channel, optionally with a custom formatter. |
track(event) | Record a use. Returns immediately; storing/reporting run in the background. |
store | The configured store, for querying. |
enabled | true if a store or channel is configured. |
import { JsonFileUsageStore } from "spearkit";
client.usage.setStore(new JsonFileUsageStore("./usage.jsonl"));
client.usage.reportTo("123456789012345678");
// Record a custom event yourself (e.g. a non-command action).
client.usage.track({ type: "event", name: "signup", timestamp: new Date() });Tracking is fire-and-forget: a slow store or channel never blocks command handling, and any failure is logged rather than thrown.
See also
- Logging — diagnostics, the other sink.
- Commands / Prefix commands / Components — what gets tracked.
- Client — the
usageoption.
Messages & limits
Discord caps a message's content at 2000 characters. Long output — a log dump, a list, an AI response — silently fails or throws unless you split it. spearkit ships two helpers…
Key-value store & settings
Almost every community bot needs to remember something per guild — a custom prefix, a mod-log channel, a welcome message — and reaches for a database on day one. spearkit ships a…