@radishbot/sdk
v0.8.2
Published
Flow-based logging SDK. Track what your app is doing with nested actions, timing, and structured logs — then inspect everything in the dashboard.
Readme
Radish Logger
Flow-based logging SDK. Track what your app is doing with nested actions, timing, and structured logs — then inspect everything in the dashboard.
No account needed. Generate a key, start logging.
Install
npm install @radishbot/sdkQuick Start
import { RL, generateKey } from "@radishbot/sdk";
const key = generateKey(); // save this — it's your dashboard login
const root = await RL(key, { release: "v1.0.0", retention: "30d" });
await root.a("handle-request", async (req) => {
console.log("GET /api/users"); // automatically captured
const users = await req.a("db-query", async () => {
return await db.query("SELECT * FROM users");
});
console.log("Done", { count: users.length });
});
await root.finish();Open the dashboard, paste your key, see everything.
Console Capture
Inside .a() callbacks, console.log/warn/error/debug are automatically captured as action logs. Output still prints to the terminal — but it also gets sent to the dashboard with full context.
await root.a("migrate", async () => {
console.log("Starting migration"); // → info log
console.warn("Deprecated column found"); // → warn log
console.error("Failed to migrate users"); // → error log
console.debug("SQL: ALTER TABLE ..."); // → debug log
console.log("Done", { tables: 5 }); // → info log with data
});Objects are properly serialized — no more [object Object].
Actions
Actions are nested scopes. Every RL() call creates a root action at /. Sub-actions branch off from there.
const root = await RL(key);
await root.a("request", async (req) => {
console.log("handling request");
await req.a("database", async () => {
console.log("querying users");
}); // auto-finished
await req.a("response", async () => {
console.log("sending 200");
}); // auto-finished
}); // auto-finished
await root.finish();If your function throws, the action is marked as errored and the exception propagates:
await root.a("risky-op", async () => {
throw new Error("something broke");
}); // action → error, rethrowsReturn values pass through:
const users = await flow.a("db-query", async () => {
return await db.query("SELECT * FROM users");
});
// users is the query resultDuplicate names are allowed — the dashboard shows them as request:1, request:2, etc:
await root.a("batch", async (batch) => {
for (const item of items) {
await batch.a("request", async () => {
await processItem(item);
});
}
});For long-lived actions (websockets, streams), use the manual API:
const stream = root.action("websocket");
stream.info("connected");
// ... hours later ...
stream.info("disconnected");
await stream.finish();Explicit Logging
Outside of .a() callbacks, or when you want to log to a specific action:
action.info("request received", { method: "POST", path: "/api/users" });
action.warn("rate limit approaching", { remaining: 3 });
action.error("query failed", new Error("connection refused"));
action.debug("cache hit", { key: "user:123", ttl: 300 });Cross-Context Actions
Export an action's handle to continue logging from another process, worker, or service:
// Service A
const action = root.action("job");
const handle = await action.exportID();
// pass handle to service B via queue, HTTP, etc.
// Service B
import { restoreFlow } from "@radishbot/sdk";
const action = await restoreFlow(key, handle);
action.info("continuing from service B");
await action.finish();Run ID
Every RL() call auto-generates a run ID — a short random string that groups flows across processes. Pass it to subprocesses via env vars, CLI args, or message queues so all flows from one logical execution link together.
const root = await RL(key);
// Pass to subprocess
spawn("worker.ts", { env: { ...process.env, RADISH_RUN_ID: root.runId } });
// In the subprocess — same run ID groups them together
const worker = await RL(key, { runId: process.env.RADISH_RUN_ID });
worker.info("processing");
await worker.finishAndDisconnect();You can also generate a run ID upfront:
import { generateRunId } from "@radishbot/sdk";
const runId = generateRunId();
// pass to multiple services
const a = await RL(key, { runId });
const b = await RL(key, { runId });In the dashboard, click a run ID badge to filter all flows from that run.
Release Tracking
Tag every flow with a version or commit SHA. Errors are tracked per-release, and regressions (errors reappearing after being resolved) are detected automatically.
const root = await RL(key, { release: "v1.2.3" });
// or
const root = await RL(key, { release: process.env.GIT_SHA });Sub-flows inherit the release from the root automatically.
Retention
Each key has a retention period. Flows older than the retention window are automatically cleaned up.
const root = await RL(key, { retention: "30d" }); // default
const root = await RL(key, { retention: "7d" }); // keep 1 week
const root = await RL(key, { retention: "90d" }); // keep 3 monthsCalling RL() with a new retention value updates the stored retention for that key.
Error Grouping
Errors are automatically deduplicated by normalizing the error message (stripping numbers, UUIDs) and combining it with the flow path. This means:
"User 123 not found"and"User 456 not found"at/request/db-querygroup together- Each group tracks: count, first/last seen, latest flow, and release
- Resolving an error group and seeing it again marks it as regressed
- Groups can be marked as
resolvedorignoredfrom the dashboard
Configuration
const root = await RL(key, {
host: "wss://maincloud.spacetimedb.com", // default
dbName: "radish-log", // default
defaultTimeout: 100, // seconds, default 100
release: "v1.0.0", // version or commit SHA
retention: "30d", // data retention period
runId: "abc123", // optional, auto-generated if omitted
});Sub-actions can have their own timeout:
await root.a(
"quick-task",
async () => {
// ...
},
10,
); // 10 second timeout
const slow = root.action("batch-job", 3600); // 1 hourActions that exceed their timeout are automatically marked as timed out.
API Reference
RL(secretKey, options?) → Promise<Flow>
Connect and create a root action. Options: host, dbName, defaultTimeout, release, retention, runId.
generateKey() → string
Generate a random secret key (prefix rl_).
generateRunId() → string
Generate a random run ID. Useful when you want to create the ID before calling RL().
restoreFlow(secretKey, handle, options?) → Promise<Flow>
Restore an action from an exported handle string.
Flow (Action)
| Method | Description |
| -------------------------- | ------------------------------------------------------------------ |
| .a(name, fn, timeout?) | Run a sub-action. Auto-finish, console capture. Returns fn result. |
| .action(name, timeout?) | Create a sub-action manually. |
| .finish() | Finish the action. |
| .finishWithError(err?) | Finish the action as errored. |
| .info(msg, data?) | Log at info level. |
| .warn(msg, data?) | Log at warn level. |
| .error(msg, data?) | Log at error level. |
| .debug(msg, data?) | Log at debug level. |
| .log(msg, data?, level?) | Log at any level. |
| .exportID() | Export handle for cross-context restore. |
| .runId | The run ID (read-only). Pass to subprocesses. |
| .getId() | Get server-assigned action ID. |
