npm package: @radishbot/sdk
Description: Flow-based logging SDK. Track what your app is doing with nested actions, timing, and structured logs — then inspect everything in the dashboard.
Latest version: 0.8.2

# 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

```bash
npm install @radishbot/sdk
```

## Quick Start

```ts
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.

```ts
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.

```ts
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:

```ts
await root.a("risky-op", async () => {
  throw new Error("something broke");
}); // action → error, rethrows
```

Return values pass through:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
// 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.

```ts
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:

```ts
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.

```ts
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.

```ts
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

```ts
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:

```ts
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.                                     |