npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/sdk

Quick 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, rethrows

Return values pass through:

const users = await flow.a("db-query", async () => {
  return await db.query("SELECT * FROM users");
});
// users is the query result

Duplicate 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 months

Calling 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-query group 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 resolved or ignored from 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 hour

Actions 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. |