Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Use this page as a command reference. If you are setting up a project for the fi
| `void auth login` | Authenticate with Void |
| `void project link` | Link directory to a project |
| `void project logs` | Show runtime logs from deployed project |
| `void project requests` | Show request-level traffic (status, method, timing) |
| `void project rollback` | Roll back to a previous deployment |
| `void project cancel` | Cancel an active deployment |
| `void project purge-cache` | Purge all cached pages |
Expand Down Expand Up @@ -162,12 +163,12 @@ void project logs [--level <level>] [--filter <text>] [--range <duration>] [--de

Show runtime logs from the deployed project. Uses the linked project from `.void/project.json`.

| Flag | Purpose | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `--range <duration>` | How far back to look. Format: `<number><unit>` (m/h/d). Max 7d. | `1h` |
| `--level <level>` | Filter by log level. One of `error`, `warn`, `info`, `log`, `debug`, `all`. `error` also includes uncaught exceptions and non-`ok` requests. | `all` |
| `--filter <text>` | Case-insensitive **substring** match against log message text and exception name/message — not a level filter. Shows the full request entry on any hit. | none |
| `--deployment <id>` | Filter logs to a specific deployment ID. | none |
| Flag | Purpose | Default |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `--range <duration>` | How far back to look. Format: `<number><unit>` (m/h/d). Max 7d. | `1h` |
| `--level <level>` | Filter by log level. One of `error`, `warn`, `info`, `log`, `debug`, `all`. `error` also includes uncaught exceptions, non-`ok` outcomes, and any 5xx response — even when the worker neither threw nor logged. | `all` |
| `--filter <text>` | Case-insensitive **substring** match against log message text and exception name/message — not a level filter. Shows the full request entry on any hit. | none |
| `--deployment <id>` | Filter logs to a specific deployment ID. | none |

Output shows one line per request (`HH:MM:SS METHOD URL STATUS`) with indented console log and exception lines beneath. Errors and exceptions are colored red, warnings yellow.

Expand All @@ -178,7 +179,32 @@ void project logs --level error --range 12h
void project logs --level error --filter websocket
```

Tip: `void project logs` only sees what Cloudflare Tail captures — top-level `console.*` calls and uncaught throws. Application errors caught and persisted to your own DB are invisible to tail. Surface them via `console.error(...)` or `void/log`'s `logger.error(...)` so they show up under `--level error`.
Tip: `void project logs` only sees what Cloudflare Tail captures — top-level `console.*` calls and uncaught throws. Application errors caught and persisted to your own DB are invisible to tail. Surface them via `console.error(...)` or `void/log`'s `logger.error(...)` so they show up under `--level error`. For 5xx that never reach your worker at all (edge-router errors, static/SPA projects), use `void project requests --status 5xx`.
Comment thread
Brooooooklyn marked this conversation as resolved.

### `void project requests`

```
void project requests [--status <filter>] [--range <duration>]
```

Show request-level traffic recorded at the edge for the linked project: one line per request with time, method, HTTP status, request type, and server timing. Unlike `void project logs` (which only has rows for invoked user workers), this reads the edge request-metering data, so it also surfaces:

- **5xx the edge router generated itself** — missing deployment manifest, static-asset read timeouts, SSR dispatch timeouts — which never invoke your worker and so never appear in logs.
- **Requests to static/SPA projects**, which are served directly from storage and never run a worker.

| Flag | Purpose | Default |
| -------------------- | --------------------------------------------------------------------------------------------- | ------- |
| `--status <filter>` | Filter by status: a class (`2xx`, `3xx`, `4xx`, `5xx`) or an exact 3-digit code (e.g. `500`). | none |
| `--range <duration>` | How far back to look. Format: `<number><unit>` (m/h/d). Max 7d. | `1h` |

Output shows one line per request (`HH:MM:SS METHOD STATUS TYPE DURATION`). 5xx are colored red, 4xx yellow. Note: request paths are not recorded, so this view shows status and type rather than URLs — use `void project logs` for per-URL, per-log detail on requests that do reach your worker.

Examples:

```
void project requests --status 5xx
void project requests --range 24h
```

### `void project rollback [deployId]`

Expand Down
2 changes: 2 additions & 0 deletions packages/void/AGENT_PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Rewrites and redirects: declare static rules in `void.json` under `routing.redir

Logs: surface app-level errors that should show up under `void project logs --level error` via `import { logger } from "void/log"` and `logger.error(msg, fields?)` (also `.warn` / `.info`). Anything caught and only persisted to your own DB is invisible to Cloudflare Tail; route it through `logger.*` or `console.*` so the platform can see it.

Requests: `void project logs` only has rows when a worker actually ran. 5xx generated by the edge router, and every request to a static/SPA project, produce no log line at all. Use `void project requests --status 5xx --range 12h` to see those — it lists status, method, and duration for every request the edge served, whether or not a worker was invoked.

Self-host deploy: `void deploy` targets the Void platform; to deploy to the user's OWN Cloudflare account instead, use `void deploy --backend cloudflare` (add `--provision` on the first deploy to create the D1/KV/R2/Queues/Hyperdrive resources the source needs). It uses local wrangler auth, so pin an account (`account_id` in `wrangler.jsonc` or `CLOUDFLARE_ACCOUNT_ID`) and keep real secrets in `wrangler secret put` — every `.env*` file this backend loads (`.env`, `.env.local`, `.env.production`, `.env.production.local`) ships as plaintext worker vars, `.local` included. Provisioning a Hyperdrive config reads `DATABASE_URL` from the shell, not from `.env*`. v1 supports full Void apps on the Cloudflare Workers target only (D1/KV/R2/Queues/Hyperdrive; auth and ISR on D1/SQLite); framework SSR of every kind (TanStack Start, React Router, vinext, SvelteKit, Nuxt, Analog, Astro), static/SPA/SSG apps (including `output: "static"`), WebSocket/Durable Object apps (`*.ws.ts`), a custom `migrations_pattern`, node/bun/deno targets, and PostgreSQL apps with auth all fail closed with guidance. Auth apps must ship checked-in migrations that produce the Better Auth schema. `--provision` is a single-operator, dev-machine step and is disabled in CI.

Full docs are in `node_modules/void/docs/`. If you have the `void` skill available, use it for a complete API reference covering project structure, routing, pages mode, database, auth, typed fetch, KV, storage, queues, cron jobs, CLI, configuration, and deployment.
2 changes: 1 addition & 1 deletion packages/void/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default defineConfig({
- `void auth login` — platform auth
- `void deploy` — build and deploy
- `void db *` — local DB and migration commands
- `void project *` — link, status, logs, rollback, delete
- `void project *` — link, status, logs, requests, rollback, delete

Runtime helpers include:

Expand Down
2 changes: 1 addition & 1 deletion packages/void/getting-started-prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Database workflow: define Drizzle tables in `db/schema.ts`, import `db` from `vo

Routing extras: declare static redirects/rewrites/fallbacks in `void.json` under `routing.*`, or in a `public/_redirects` file. For dynamic rewrites, call `c.rewrite(path)` in a `defineMiddleware`.

Logs: surface app-level errors via `import { logger } from "void/log"` and `logger.error(msg, fields?)` (also `.warn` / `.info`) so they appear under `void project logs --level error`. Errors only persisted to your own DB are invisible to Cloudflare Tail.
Logs: surface app-level errors via `import { logger } from "void/log"` and `logger.error(msg, fields?)` (also `.warn` / `.info`) so they appear under `void project logs --level error`. Errors only persisted to your own DB are invisible to Cloudflare Tail. Logs only exist where a worker ran — for 5xx generated by the edge router or on static/SPA projects, use `void project requests --status 5xx` instead.

CI / fresh clones: run `void prepare` after install to generate `.void/routes.d.ts`, `.void/db.d.ts`, `.void/queues.d.ts`, `.void/env.d.ts`, and `.void/tsconfig.json` without booting Vite. `vite dev` / `vite build` populate these during normal app workflows.

Expand Down
2 changes: 1 addition & 1 deletion packages/void/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ if (helpPath) {
},
{
name: 'project',
hint: 'status | link | list | logs | rollback | cancel | delete | purge-cache',
hint: 'status | link | list | logs | requests | rollback | cancel | delete | purge-cache',
description: 'Manage projects and deployments',
},
{ name: 'secret', hint: 'list | put | delete | sync', description: 'Manage project secrets' },
Expand Down
37 changes: 37 additions & 0 deletions packages/void/src/cli/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ export type LogEntry = {
status: string;
};

export type RequestEntry = {
timestamp: string;
type: string;
method: string;
status: string;
bytes: number;
durationMs: number;
};

export type BuildSummary = {
id: string;
status: string;
Expand Down Expand Up @@ -907,6 +916,34 @@ export class PlatformClient {
return json.data;
}

async getRequests(
projectId: string,
since: string,
limit = 200,
status?: string,
): Promise<Array<RequestEntry>> {
const params = new URLSearchParams({
since,
limit: String(limit),
});
if (status && status !== 'all') {
params.set('status', status);
}

const res = await fetch(`${this.baseUrl}/projects/${projectId}/requests?${params}`, {
headers: {
...this.authHeaders,
},
});

if (!res.ok) {
await throwPlatformApiError('Failed to fetch requests', res);
}

const json = (await res.json()) as { data: Array<RequestEntry> };
return json.data;
}

async listBuilds(projectId: string, limit = 20): Promise<Array<BuildSummary>> {
const params = new URLSearchParams({ limit: String(limit) });
const res = await fetch(`${this.baseUrl}/projects/${projectId}/builds?${params}`, {
Expand Down
23 changes: 23 additions & 0 deletions packages/void/src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const HELP_TREE: HelpTree = {
{ command: 'link [name]', description: 'Link this directory to a project' },
{ command: 'list', description: 'List your projects' },
{ command: 'logs', description: 'Show runtime logs from the deployed project' },
{ command: 'requests', description: 'Show request-level traffic (status, method, timing)' },
{ command: 'rollback [deployId]', description: 'Roll back to a previous deployment' },
{ command: 'cancel [deployId]', description: 'Cancel an active deployment' },
{ command: 'delete [name]', description: 'Delete a project and all its resources' },
Expand Down Expand Up @@ -143,6 +144,28 @@ const HELP_TREE: HelpTree = {
},
],
},
requests: {
summary:
'Show request-level traffic (status, method, type, timing). Surfaces 5xx that the edge router generated itself and requests to static/SPA projects that never invoke a worker — neither of which appears in `project logs`.',
usage: ['void project requests [options]'],
options: [
{
command: '--status <filter>',
description: 'Filter by status: a class like 5xx or an exact code like 500',
},
{ command: '--range <range>', description: 'Time range to query (default: 1h)' },
],
examples: [
{
command: 'void project requests --status 5xx',
description: 'Show recent server-error responses, including edge-generated ones',
},
{
command: 'void project requests --range 24h',
description: 'Show all request traffic in the last day',
},
],
},
rollback: basicSubcommand(
'Roll back to a previous deployment.',
'void project rollback [deployId]',
Expand Down
5 changes: 5 additions & 0 deletions packages/void/src/cli/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export function filterLogEntries(
if (entry.exceptions.length > 0) {
return true;
}
// A 5xx response is an error even when the worker neither threw nor
// logged (mirrors the server-side `blob6 LIKE '5%'` predicate).
if (parseInt(entry.status, 10) >= 500) {
return true;
}
for (const l of entry.logs) {
if (l.level === 'error') {
return true;
Expand Down
5 changes: 5 additions & 0 deletions packages/void/src/cli/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ Available methods on the "void_" object:
void_.getLogs(projectId, since, limit?, deploymentId?) → { timestamp, outcome, logs, exceptions, url, method, status }[]
since: duration string (e.g. "1h"), limit: number (default 200)

Requests:
void_.getRequests(projectId, since, limit?, status?) → { timestamp, type, method, status, bytes, durationMs }[]
since: duration string (e.g. "1h"), limit: number (default 200), status: class like "5xx" or exact code like "500"
Edge request metering — includes edge-generated 5xx and static/SPA requests that never invoke a worker.

Secrets:
void_.listSecrets(projectId) → { name }[]
void_.setSecret(projectId, name, value) → { ok, name }
Expand Down
29 changes: 28 additions & 1 deletion packages/void/src/cli/project-args.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { SubcommandDef } from './subcommand-prompt.ts';

const LOG_LEVELS = new Set(['error', 'warn', 'info', 'log', 'debug', 'all']);
// A status class (`5xx`), an exact code in the 100-599 HTTP range (`500`), or
// `all`. Exact codes are range-bounded so a typo like `600` errors instead of
// silently returning an empty result.
const STATUS_FILTER = /^([1-5]xx|[1-5]\d{2}|all)$/;

export const projectSubcommands: Array<SubcommandDef> = [
{ name: 'status', arg: '[name]', description: 'Show deployments (omit name for linked project)' },
{ name: 'link', arg: '[name]', description: 'Link directory to a project' },
{ name: 'list', description: 'List your projects' },
{ name: 'logs', description: 'Show runtime logs from the deployed project' },
{ name: 'requests', description: 'Show request-level traffic (status, method, timing)' },
{ name: 'rollback', arg: '[deployId]', description: 'Roll back to a previous deployment' },
{ name: 'cancel', arg: '[deployId]', description: 'Cancel an active deployment' },
{
Expand All @@ -28,6 +33,7 @@ export type ProjectArgs =
deployment: string;
level: string;
}
| { subcommand: 'requests'; status: string; range: string }
| { subcommand: 'rollback'; deployId: string }
| { subcommand: 'cancel'; deployId: string }
| { subcommand: 'delete'; name: string; force: boolean }
Expand Down Expand Up @@ -92,6 +98,27 @@ export function parseProjectArgs(args: Array<string>): ProjectArgs {
return { subcommand, filter, range, deployment, level };
}

if (subcommand === 'requests') {
let status = '';
let range = '1h';
for (let i = 0; i < rest.length; i++) {
if (rest[i] === '--status' && rest[i + 1]) {
const value = rest[++i];
if (!STATUS_FILTER.test(value)) {
throw new Error(
`project: Invalid --status '${value}'. Use a class like 5xx or an exact code like 500.`,
);
}
status = value;
} else if (rest[i] === '--range' && rest[i + 1]) {
range = rest[++i];
} else {
throw new Error(`project: Unexpected argument '${rest[i]}'.`);
}
}
return { subcommand, status, range };
}

if (subcommand === 'rollback') {
const [deployId, extra] = rest;
if (deployId?.startsWith('-')) {
Expand Down Expand Up @@ -144,6 +171,6 @@ export function parseProjectArgs(args: Array<string>): ProjectArgs {
}

throw new Error(
'project: Usage: void project <status|link|list|logs|rollback|cancel|delete|purge-cache>.',
'project: Usage: void project <status|link|list|logs|requests|rollback|cancel|delete|purge-cache>.',
);
}
4 changes: 4 additions & 0 deletions packages/void/src/cli/project-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export async function runProjectCommand(root: string, args: ProjectArgs): Promis
args.level || undefined,
);
}
if (args.subcommand === 'requests') {
const { runRequestsCommand } = await import('./requests.ts');
return runRequestsCommand(root, args.range, args.status || undefined);
}
if (args.subcommand === 'rollback') {
const { runRollbackCommand } = await import('./rollback.ts');
return runRollbackCommand(root, args.deployId || undefined);
Expand Down
Loading