spearkit
Guides

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:

LoggerUsage tracking
QuestionWhat is the bot doing? (diagnostics)Who used which feature? (audit)
ContentFree-form messages, levels, errors, internalsStructured UsageEvents for every completed use (with its outcome)
SinksConsole / your log pipelineA database store and/or a Discord channel
Configured bythe logger optionthe 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 everything

JsonFileUsageStore

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:

MemberDescription
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.
storeThe configured store, for querying.
enabledtrue 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

On this page