lopata 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { render } from "preact";
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
interface StackFrame {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
function: string;
|
|
9
|
+
source?: string[];
|
|
10
|
+
sourceLine?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TraceSpan {
|
|
14
|
+
spanId: string;
|
|
15
|
+
traceId: string;
|
|
16
|
+
parentSpanId: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
status: string;
|
|
19
|
+
startTime: number;
|
|
20
|
+
endTime: number | null;
|
|
21
|
+
durationMs: number | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ErrorPageData {
|
|
25
|
+
error: {
|
|
26
|
+
name: string;
|
|
27
|
+
message: string;
|
|
28
|
+
stack: string;
|
|
29
|
+
frames: StackFrame[];
|
|
30
|
+
};
|
|
31
|
+
request: {
|
|
32
|
+
method: string;
|
|
33
|
+
url: string;
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
};
|
|
36
|
+
env: Record<string, string>;
|
|
37
|
+
bindings: { name: string; type: string }[];
|
|
38
|
+
runtime: {
|
|
39
|
+
bunVersion: string;
|
|
40
|
+
platform: string;
|
|
41
|
+
arch: string;
|
|
42
|
+
workerName?: string;
|
|
43
|
+
configName?: string;
|
|
44
|
+
};
|
|
45
|
+
trace?: {
|
|
46
|
+
traceId: string;
|
|
47
|
+
spanId: string | null;
|
|
48
|
+
spans: TraceSpan[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare global {
|
|
53
|
+
interface Window {
|
|
54
|
+
__BUNFLARE_ERROR__: ErrorPageData;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Section({ title, open, children }: { title: string; open?: boolean; children: preact.ComponentChildren }) {
|
|
59
|
+
return (
|
|
60
|
+
<details open={open} class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
61
|
+
<summary class="px-5 py-3 cursor-pointer select-none text-sm font-semibold text-ink hover:bg-gray-50 transition-colors">
|
|
62
|
+
{title}
|
|
63
|
+
</summary>
|
|
64
|
+
<div class="border-t border-gray-100">
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</details>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const LIBRARY_PATH_RE = /\/node_modules\//;
|
|
72
|
+
|
|
73
|
+
function isLibraryFrame(frame: StackFrame): boolean {
|
|
74
|
+
return LIBRARY_PATH_RE.test(frame.file);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const HL_RE = /(\/\/.*$|\/\*.*?\*\/)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b)|(\b(?:const|let|var|function|return|if|else|for|while|do|class|new|import|export|from|default|async|await|throw|try|catch|finally|switch|case|break|continue|typeof|instanceof|in|of|yield|static|extends|super|void|delete|enum|interface|type|as|declare|readonly)\b)|(\b(?:true|false|null|undefined|this|NaN|Infinity)\b)/g;
|
|
78
|
+
|
|
79
|
+
function highlightLine(line: string) {
|
|
80
|
+
const parts: preact.ComponentChildren[] = [];
|
|
81
|
+
let last = 0;
|
|
82
|
+
for (const m of line.matchAll(HL_RE)) {
|
|
83
|
+
if (m.index! > last) parts.push(line.slice(last, m.index));
|
|
84
|
+
const t = m[0];
|
|
85
|
+
const c = m[1] ? "#6b7280" : m[2] ? "#16a34a" : m[3] ? "#d97706" : m[4] ? "#7c3aed" : m[5] ? "#2563eb" : undefined;
|
|
86
|
+
parts.push(c ? <span style={{ color: c }}>{t}</span> : t);
|
|
87
|
+
last = m.index! + t.length;
|
|
88
|
+
}
|
|
89
|
+
if (last < line.length) parts.push(line.slice(last));
|
|
90
|
+
return parts.length > 0 ? parts : line;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function FrameList({ frames }: { frames: StackFrame[] }) {
|
|
94
|
+
return (
|
|
95
|
+
<div class="divide-y divide-gray-100">
|
|
96
|
+
{frames.map((frame, i) => (
|
|
97
|
+
<CodeBlock key={i} frame={frame} defaultOpen={!isLibraryFrame(frame)} />
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function CodeBlock({ frame, defaultOpen }: { frame: StackFrame; defaultOpen: boolean }) {
|
|
104
|
+
if (!frame.source || frame.source.length === 0) return null;
|
|
105
|
+
const startLine = frame.line - (frame.sourceLine ?? 0);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<details open={defaultOpen}>
|
|
109
|
+
<summary class="px-4 py-2 bg-gray-50 text-xs font-medium text-ink-muted cursor-pointer select-none hover:bg-gray-100 transition-colors" style="font-family: 'JetBrains Mono', monospace;">
|
|
110
|
+
{frame.file}:{frame.line}:{frame.column}
|
|
111
|
+
{frame.function && <span class="ml-2 text-gray-400">in {frame.function}</span>}
|
|
112
|
+
</summary>
|
|
113
|
+
<div class="overflow-x-auto scrollbar-thin">
|
|
114
|
+
<pre class="text-xs leading-5 m-0" style="font-family: 'JetBrains Mono', monospace;">
|
|
115
|
+
{frame.source.map((line, i) => {
|
|
116
|
+
const lineNum = startLine + i;
|
|
117
|
+
const isError = i === frame.sourceLine;
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
key={i}
|
|
121
|
+
class={isError ? "bg-red-50 border-l-4 border-error-red" : "border-l-4 border-transparent hover:bg-gray-50"}
|
|
122
|
+
>
|
|
123
|
+
<span class={`inline-block w-12 text-right pr-3 select-none ${isError ? "text-error-red font-bold" : "text-gray-400"}`}>
|
|
124
|
+
{lineNum}
|
|
125
|
+
</span>
|
|
126
|
+
<span class={`text-ink${isError ? " font-medium" : ""}`}>{highlightLine(line)}</span>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</pre>
|
|
131
|
+
</div>
|
|
132
|
+
</details>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function KeyValueTable({ data, mask }: { data: Record<string, string>; mask?: boolean }) {
|
|
137
|
+
const entries = Object.entries(data);
|
|
138
|
+
if (entries.length === 0) {
|
|
139
|
+
return <div class="px-4 py-3 text-sm text-gray-400">No entries</div>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<table class="w-full text-sm">
|
|
144
|
+
<tbody>
|
|
145
|
+
{entries.map(([key, value]) => (
|
|
146
|
+
<tr key={key} class="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
|
|
147
|
+
<td class="px-4 py-2 font-medium text-ink-muted whitespace-nowrap align-top" style="font-family: 'JetBrains Mono', monospace; width: 1%;">
|
|
148
|
+
{key}
|
|
149
|
+
</td>
|
|
150
|
+
<td class="px-4 py-2 text-ink break-all" style="font-family: 'JetBrains Mono', monospace;">
|
|
151
|
+
{value}
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
))}
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const MAX_COLLAPSED_LINES = 10;
|
|
161
|
+
|
|
162
|
+
function ErrorMessage({ message }: { message: string }) {
|
|
163
|
+
const [expanded, setExpanded] = useState(false);
|
|
164
|
+
const nlIndex = message.indexOf("\n");
|
|
165
|
+
|
|
166
|
+
if (nlIndex === -1) {
|
|
167
|
+
return <h1 class="text-lg font-bold text-ink m-0 leading-snug break-words">{message}</h1>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const firstLine = message.slice(0, nlIndex);
|
|
171
|
+
const rest = message.slice(nlIndex + 1);
|
|
172
|
+
const restLines = rest.split("\n");
|
|
173
|
+
const needsCollapse = restLines.length > MAX_COLLAPSED_LINES;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<h1 class="text-lg font-bold text-ink m-0 leading-snug break-words">{firstLine}</h1>
|
|
178
|
+
<div class="relative mt-2">
|
|
179
|
+
<pre
|
|
180
|
+
class="text-xs text-ink-muted m-0 whitespace-pre-wrap break-words leading-5 overflow-hidden transition-all"
|
|
181
|
+
style={{
|
|
182
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
183
|
+
maxHeight: !expanded && needsCollapse ? `${MAX_COLLAPSED_LINES * 1.25}rem` : "none",
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{rest}
|
|
187
|
+
</pre>
|
|
188
|
+
{needsCollapse && !expanded && (
|
|
189
|
+
<div
|
|
190
|
+
class="absolute bottom-0 left-0 right-0 h-16 flex items-end justify-center pb-2 cursor-pointer"
|
|
191
|
+
style="background: linear-gradient(to bottom, transparent, white);"
|
|
192
|
+
onClick={() => setExpanded(true)}
|
|
193
|
+
>
|
|
194
|
+
<span class="text-xs font-medium text-gray-400 hover:text-ink transition-colors">
|
|
195
|
+
Show all ({restLines.length} lines)
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
{needsCollapse && expanded && (
|
|
200
|
+
<button
|
|
201
|
+
class="mt-1 text-xs font-medium text-gray-400 hover:text-ink transition-colors"
|
|
202
|
+
onClick={() => setExpanded(false)}
|
|
203
|
+
>
|
|
204
|
+
Collapse
|
|
205
|
+
</button>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function fmtDuration(ms: number): string {
|
|
213
|
+
if (ms < 1) return "<1ms";
|
|
214
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
215
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function SimpleTraceWaterfall({ trace }: { trace: NonNullable<ErrorPageData["trace"]> }) {
|
|
219
|
+
const { spans, spanId: errorSpanId } = trace;
|
|
220
|
+
if (spans.length === 0) return null;
|
|
221
|
+
|
|
222
|
+
const traceStart = Math.min(...spans.map(s => s.startTime));
|
|
223
|
+
const traceEnd = Math.max(...spans.map(s => s.endTime ?? Date.now()));
|
|
224
|
+
const traceDuration = traceEnd - traceStart || 1;
|
|
225
|
+
|
|
226
|
+
// Build tree and flatten
|
|
227
|
+
const childMap = new Map<string | null, TraceSpan[]>();
|
|
228
|
+
for (const s of spans) {
|
|
229
|
+
const key = s.parentSpanId;
|
|
230
|
+
if (!childMap.has(key)) childMap.set(key, []);
|
|
231
|
+
childMap.get(key)!.push(s);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function flatten(parentId: string | null, depth: number): Array<{ span: TraceSpan; depth: number }> {
|
|
235
|
+
const children = childMap.get(parentId) ?? [];
|
|
236
|
+
const result: Array<{ span: TraceSpan; depth: number }> = [];
|
|
237
|
+
for (const child of children) {
|
|
238
|
+
result.push({ span: child, depth });
|
|
239
|
+
result.push(...flatten(child.spanId, depth + 1));
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const flatSpans = flatten(null, 0);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div class="px-4 py-3">
|
|
248
|
+
<div class="flex items-center justify-between mb-2">
|
|
249
|
+
<span class="text-xs text-gray-400" style="font-family: 'JetBrains Mono', monospace;">0ms</span>
|
|
250
|
+
<span class="text-xs text-gray-400" style="font-family: 'JetBrains Mono', monospace;">{fmtDuration(traceDuration)}</span>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="space-y-0.5">
|
|
253
|
+
{flatSpans.map(({ span, depth }) => {
|
|
254
|
+
const offset = ((span.startTime - traceStart) / traceDuration) * 100;
|
|
255
|
+
const width = (((span.endTime ?? Date.now()) - span.startTime) / traceDuration) * 100;
|
|
256
|
+
const isError = errorSpanId === span.spanId;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
key={span.spanId}
|
|
261
|
+
class={`flex items-center py-1 px-1 rounded-md ${isError ? "bg-red-50 ring-1 ring-red-300" : ""}`}
|
|
262
|
+
>
|
|
263
|
+
<div
|
|
264
|
+
class="w-[180px] flex-shrink-0 truncate text-xs text-ink"
|
|
265
|
+
style={{ paddingLeft: `${depth * 14}px`, fontFamily: "'JetBrains Mono', monospace" }}
|
|
266
|
+
>
|
|
267
|
+
{span.name}
|
|
268
|
+
</div>
|
|
269
|
+
<div class="flex-1 h-5 relative bg-gray-50 rounded">
|
|
270
|
+
<div
|
|
271
|
+
class={`absolute top-0.5 bottom-0.5 rounded ${
|
|
272
|
+
span.status === "error" ? "bg-red-400" :
|
|
273
|
+
span.status === "ok" ? "bg-emerald-400" :
|
|
274
|
+
"bg-gray-300"
|
|
275
|
+
}`}
|
|
276
|
+
style={{ left: `${offset}%`, width: `${Math.max(width, 0.5)}%` }}
|
|
277
|
+
/>
|
|
278
|
+
<span
|
|
279
|
+
class="absolute top-0.5 text-[10px] text-gray-500 whitespace-nowrap"
|
|
280
|
+
style={{ left: `${offset + width + 1}%`, fontFamily: "'JetBrains Mono', monospace" }}
|
|
281
|
+
>
|
|
282
|
+
{span.durationMs != null ? fmtDuration(span.durationMs) : "..."}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function App() {
|
|
294
|
+
const data = window.__BUNFLARE_ERROR__;
|
|
295
|
+
|
|
296
|
+
if (!data) {
|
|
297
|
+
return <div class="p-8 text-gray-400">No error data available.</div>;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { error, request, env, bindings, runtime } = data;
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div class="min-h-full p-6 max-w-5xl mx-auto flex flex-col gap-4">
|
|
304
|
+
{/* Error header */}
|
|
305
|
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden border-l-4 border-l-error-red">
|
|
306
|
+
<div class="px-5 py-4">
|
|
307
|
+
<div class="flex items-center gap-2.5 mb-1.5">
|
|
308
|
+
<span class="w-6 h-6 rounded-md bg-red-50 flex items-center justify-center text-error-red text-xs font-bold">!</span>
|
|
309
|
+
<span class="text-xs font-semibold uppercase tracking-wider text-error-red">{error.name}</span>
|
|
310
|
+
</div>
|
|
311
|
+
<ErrorMessage message={error.message} />
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Source Code */}
|
|
316
|
+
{error.frames.length > 0 && (
|
|
317
|
+
<Section title="Source Code" open>
|
|
318
|
+
<FrameList frames={error.frames} />
|
|
319
|
+
</Section>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Stack Trace */}
|
|
323
|
+
<Section title="Stack Trace">
|
|
324
|
+
<div class="px-4 py-3 overflow-x-auto scrollbar-thin">
|
|
325
|
+
<pre class="text-xs text-ink-muted leading-5 m-0 whitespace-pre-wrap break-words" style="font-family: 'JetBrains Mono', monospace;">
|
|
326
|
+
{error.stack}
|
|
327
|
+
</pre>
|
|
328
|
+
</div>
|
|
329
|
+
</Section>
|
|
330
|
+
|
|
331
|
+
{/* Trace */}
|
|
332
|
+
{data.trace && data.trace.spans.length > 0 && (
|
|
333
|
+
<Section title="Trace" open>
|
|
334
|
+
<SimpleTraceWaterfall trace={data.trace} />
|
|
335
|
+
</Section>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* Request */}
|
|
339
|
+
<Section title="Request" open>
|
|
340
|
+
<div class="px-4 py-2.5 border-b border-gray-100">
|
|
341
|
+
<span class="inline-block px-2 py-0.5 rounded-md bg-gray-100 text-xs font-bold mr-2">{request.method}</span>
|
|
342
|
+
<span class="text-sm break-all" style="font-family: 'JetBrains Mono', monospace;">{request.url}</span>
|
|
343
|
+
</div>
|
|
344
|
+
<KeyValueTable data={request.headers} />
|
|
345
|
+
</Section>
|
|
346
|
+
|
|
347
|
+
{/* Environment */}
|
|
348
|
+
<Section title="Environment">
|
|
349
|
+
<KeyValueTable data={env} />
|
|
350
|
+
</Section>
|
|
351
|
+
|
|
352
|
+
{/* Bindings */}
|
|
353
|
+
<Section title="Bindings">
|
|
354
|
+
{bindings.length === 0 ? (
|
|
355
|
+
<div class="px-4 py-3 text-sm text-gray-400">No bindings configured</div>
|
|
356
|
+
) : (
|
|
357
|
+
<table class="w-full text-sm">
|
|
358
|
+
<tbody>
|
|
359
|
+
{bindings.map((b) => (
|
|
360
|
+
<tr key={b.name} class="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
|
|
361
|
+
<td class="px-4 py-2 font-medium text-ink-muted whitespace-nowrap" style="font-family: 'JetBrains Mono', monospace;">
|
|
362
|
+
{b.name}
|
|
363
|
+
</td>
|
|
364
|
+
<td class="px-4 py-2">
|
|
365
|
+
<span class="inline-block px-2 py-0.5 rounded-md bg-gray-100 text-xs font-medium text-gray-600">{b.type}</span>
|
|
366
|
+
</td>
|
|
367
|
+
</tr>
|
|
368
|
+
))}
|
|
369
|
+
</tbody>
|
|
370
|
+
</table>
|
|
371
|
+
)}
|
|
372
|
+
</Section>
|
|
373
|
+
|
|
374
|
+
{/* Runtime */}
|
|
375
|
+
<Section title="Runtime">
|
|
376
|
+
<KeyValueTable
|
|
377
|
+
data={{
|
|
378
|
+
"Bun": runtime.bunVersion,
|
|
379
|
+
"Platform": runtime.platform,
|
|
380
|
+
"Arch": runtime.arch,
|
|
381
|
+
...(runtime.workerName ? { "Worker": runtime.workerName } : {}),
|
|
382
|
+
...(runtime.configName ? { "Config": runtime.configName } : {}),
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
</Section>
|
|
386
|
+
|
|
387
|
+
<div class="text-center text-xs text-gray-400 py-4">
|
|
388
|
+
Bunflare Dev Server
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
render(<App />, document.getElementById("app")!);
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { WranglerConfig } from "../config";
|
|
3
|
+
import { getTraceStore } from "../tracing/store";
|
|
4
|
+
import { getActiveContext } from "../tracing/context";
|
|
5
|
+
import { type StackFrame, parseStackFrames, enrichFrameWithSourceAsync } from "../tracing/frames";
|
|
6
|
+
|
|
7
|
+
interface ErrorPageData {
|
|
8
|
+
error: {
|
|
9
|
+
name: string;
|
|
10
|
+
message: string;
|
|
11
|
+
stack: string;
|
|
12
|
+
frames: StackFrame[];
|
|
13
|
+
};
|
|
14
|
+
request: {
|
|
15
|
+
method: string;
|
|
16
|
+
url: string;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
bindings: { name: string; type: string }[];
|
|
21
|
+
runtime: {
|
|
22
|
+
bunVersion: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
arch: string;
|
|
25
|
+
workerName?: string;
|
|
26
|
+
configName?: string;
|
|
27
|
+
};
|
|
28
|
+
trace?: {
|
|
29
|
+
traceId: string;
|
|
30
|
+
spanId: string | null;
|
|
31
|
+
spans: Array<{
|
|
32
|
+
spanId: string;
|
|
33
|
+
traceId: string;
|
|
34
|
+
parentSpanId: string | null;
|
|
35
|
+
name: string;
|
|
36
|
+
status: string;
|
|
37
|
+
startTime: number;
|
|
38
|
+
endTime: number | null;
|
|
39
|
+
durationMs: number | null;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Pre-built error page HTML ────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
let errorPageHtml: string | null = null;
|
|
47
|
+
let errorPageAssets: Map<string, { content: Uint8Array; contentType: string }> | null = null;
|
|
48
|
+
|
|
49
|
+
async function buildErrorPage(): Promise<void> {
|
|
50
|
+
const tailwindPlugin = (await import("bun-plugin-tailwind")).default;
|
|
51
|
+
const htmlEntry = join(import.meta.dir, "index.html");
|
|
52
|
+
|
|
53
|
+
const result = await Bun.build({
|
|
54
|
+
entrypoints: [htmlEntry],
|
|
55
|
+
plugins: [tailwindPlugin],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
console.error("[bunflare] Error page build failed:", result.logs);
|
|
60
|
+
throw new Error("Error page build failed");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const assets = new Map<string, { content: Uint8Array; contentType: string }>();
|
|
64
|
+
let html = "";
|
|
65
|
+
|
|
66
|
+
for (const output of result.outputs) {
|
|
67
|
+
const name = output.path.split("/").pop()!;
|
|
68
|
+
const content = new Uint8Array(await output.arrayBuffer());
|
|
69
|
+
|
|
70
|
+
if (output.kind === "entry-point" && name.endsWith(".html")) {
|
|
71
|
+
html = new TextDecoder().decode(content);
|
|
72
|
+
} else {
|
|
73
|
+
const contentType = name.endsWith(".css")
|
|
74
|
+
? "text/css"
|
|
75
|
+
: name.endsWith(".js")
|
|
76
|
+
? "application/javascript"
|
|
77
|
+
: "application/octet-stream";
|
|
78
|
+
assets.set(name, { content, contentType });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Inline assets directly into the HTML to make it self-contained
|
|
83
|
+
for (const [name, asset] of assets) {
|
|
84
|
+
const assetText = new TextDecoder().decode(asset.content);
|
|
85
|
+
if (name.endsWith(".css")) {
|
|
86
|
+
html = html.replace(
|
|
87
|
+
new RegExp(`<link[^>]*href="\\./${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*/?>`),
|
|
88
|
+
`<style>${assetText}</style>`,
|
|
89
|
+
);
|
|
90
|
+
} else if (name.endsWith(".js")) {
|
|
91
|
+
html = html.replace(
|
|
92
|
+
new RegExp(`<script[^>]*src="\\./${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*>[^<]*</script>`),
|
|
93
|
+
`<script type="module">${assetText}</script>`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
errorPageHtml = html;
|
|
99
|
+
errorPageAssets = assets;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await buildErrorPage();
|
|
103
|
+
|
|
104
|
+
// ─── Env masking ──────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const SENSITIVE_KEYS = /SECRET|KEY|TOKEN|PASSWORD|API|PRIVATE/i;
|
|
107
|
+
|
|
108
|
+
function maskValue(value: string): string {
|
|
109
|
+
const show = 3;
|
|
110
|
+
if (value.length <= show * 2 + 3) return "***";
|
|
111
|
+
return value.slice(0, show) + "***" + value.slice(-show);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function maskEnv(env: Record<string, unknown>): Record<string, string> {
|
|
115
|
+
const masked: Record<string, string> = {};
|
|
116
|
+
for (const [key, value] of Object.entries(env)) {
|
|
117
|
+
if (typeof value === "object") continue; // skip bindings
|
|
118
|
+
const strVal = String(value);
|
|
119
|
+
masked[key] = SENSITIVE_KEYS.test(key) ? maskValue(strVal) : strVal;
|
|
120
|
+
}
|
|
121
|
+
return masked;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Binding extraction ──────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function extractBindings(config: WranglerConfig): { name: string; type: string }[] {
|
|
127
|
+
const bindings: { name: string; type: string }[] = [];
|
|
128
|
+
for (const kv of config.kv_namespaces ?? []) bindings.push({ name: kv.binding, type: "KV" });
|
|
129
|
+
for (const r2 of config.r2_buckets ?? []) bindings.push({ name: r2.binding, type: "R2" });
|
|
130
|
+
for (const d of config.durable_objects?.bindings ?? []) bindings.push({ name: d.name, type: "Durable Object" });
|
|
131
|
+
for (const wf of config.workflows ?? []) bindings.push({ name: wf.binding, type: "Workflow" });
|
|
132
|
+
for (const d1 of config.d1_databases ?? []) bindings.push({ name: d1.binding, type: "D1" });
|
|
133
|
+
for (const p of config.queues?.producers ?? []) bindings.push({ name: p.binding, type: "Queue" });
|
|
134
|
+
for (const svc of config.services ?? []) bindings.push({ name: svc.binding, type: "Service" });
|
|
135
|
+
if (config.images) bindings.push({ name: config.images.binding, type: "Images" });
|
|
136
|
+
if (config.assets?.binding) bindings.push({ name: config.assets.binding, type: "Assets" });
|
|
137
|
+
return bindings;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export async function renderErrorPage(
|
|
143
|
+
error: unknown,
|
|
144
|
+
request: Request,
|
|
145
|
+
env: Record<string, unknown>,
|
|
146
|
+
config: WranglerConfig,
|
|
147
|
+
workerName?: string,
|
|
148
|
+
): Promise<Response> {
|
|
149
|
+
if (!errorPageHtml) {
|
|
150
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
154
|
+
const frames = parseStackFrames(err.stack ?? "")
|
|
155
|
+
// Drop native/node internal frames — they have no readable source and waste enrichment slots
|
|
156
|
+
.filter(f => !f.file.startsWith("native:") && !f.file.startsWith("node:"));
|
|
157
|
+
|
|
158
|
+
// Enrich frames with source code (limit to 20 for performance)
|
|
159
|
+
const framesToEnrich = frames.slice(0, 20);
|
|
160
|
+
await Promise.all(framesToEnrich.map(enrichFrameWithSourceAsync));
|
|
161
|
+
|
|
162
|
+
// Strip cwd prefix from paths for display
|
|
163
|
+
const cwdPrefix = process.cwd() + "/";
|
|
164
|
+
const displayFrames = framesToEnrich.filter(f => f.source).map(f => ({
|
|
165
|
+
...f,
|
|
166
|
+
file: f.file.startsWith(cwdPrefix) ? f.file.slice(cwdPrefix.length) : f.file,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const headers: Record<string, string> = {};
|
|
170
|
+
request.headers.forEach((value, key) => {
|
|
171
|
+
headers[key] = value;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const data: ErrorPageData = {
|
|
175
|
+
error: {
|
|
176
|
+
name: err.name,
|
|
177
|
+
message: err.message,
|
|
178
|
+
stack: err.stack ?? String(error),
|
|
179
|
+
frames: displayFrames,
|
|
180
|
+
},
|
|
181
|
+
request: {
|
|
182
|
+
method: request.method,
|
|
183
|
+
url: request.url,
|
|
184
|
+
headers,
|
|
185
|
+
},
|
|
186
|
+
env: maskEnv(env),
|
|
187
|
+
bindings: extractBindings(config),
|
|
188
|
+
runtime: {
|
|
189
|
+
bunVersion: Bun.version,
|
|
190
|
+
platform: process.platform,
|
|
191
|
+
arch: process.arch,
|
|
192
|
+
workerName,
|
|
193
|
+
configName: config.name,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Attach trace data if available
|
|
198
|
+
try {
|
|
199
|
+
const ctx = getActiveContext();
|
|
200
|
+
if (ctx?.traceId) {
|
|
201
|
+
const traceDetail = getTraceStore().getTrace(ctx.traceId);
|
|
202
|
+
if (traceDetail.spans.length > 0) {
|
|
203
|
+
data.trace = {
|
|
204
|
+
traceId: ctx.traceId,
|
|
205
|
+
spanId: ctx.spanId ?? null,
|
|
206
|
+
spans: traceDetail.spans.map(s => ({
|
|
207
|
+
spanId: s.spanId,
|
|
208
|
+
traceId: s.traceId,
|
|
209
|
+
parentSpanId: s.parentSpanId,
|
|
210
|
+
name: s.name,
|
|
211
|
+
status: s.status,
|
|
212
|
+
startTime: s.startTime,
|
|
213
|
+
endTime: s.endTime,
|
|
214
|
+
durationMs: s.durationMs,
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// Don't break error page if trace fetch fails
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Persist error to tracing store
|
|
224
|
+
try {
|
|
225
|
+
const ctx = getActiveContext();
|
|
226
|
+
getTraceStore().insertError({
|
|
227
|
+
id: crypto.randomUUID(),
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
errorName: data.error.name,
|
|
230
|
+
errorMessage: data.error.message,
|
|
231
|
+
requestMethod: data.request.method,
|
|
232
|
+
requestUrl: data.request.url,
|
|
233
|
+
workerName: data.runtime.workerName ?? null,
|
|
234
|
+
traceId: ctx?.traceId ?? null,
|
|
235
|
+
spanId: ctx?.spanId ?? null,
|
|
236
|
+
source: "fetch",
|
|
237
|
+
data: JSON.stringify(data),
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
// Don't let persistence failure break the error response
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const wantsHtml = (request.headers.get("Accept") ?? "").includes("text/html");
|
|
244
|
+
|
|
245
|
+
if (wantsHtml && errorPageHtml) {
|
|
246
|
+
const script = `<script>window.__BUNFLARE_ERROR__ = ${JSON.stringify(data).replace(/</g, "\\u003c")};</script>`;
|
|
247
|
+
const html = errorPageHtml.replace("</head>", `${script}\n</head>`);
|
|
248
|
+
|
|
249
|
+
return new Response(html, {
|
|
250
|
+
status: 500,
|
|
251
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Text-only error response for non-HTML clients (curl, fetch, APIs, etc.)
|
|
256
|
+
let text = `${data.error.name}: ${data.error.message}\n`;
|
|
257
|
+
if (displayFrames.length > 0) {
|
|
258
|
+
text += "\nStack:\n";
|
|
259
|
+
for (const f of displayFrames) {
|
|
260
|
+
text += ` at ${f.function} (${f.file}:${f.line}:${f.column})\n`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
text += `\n${data.request.method} ${data.request.url}\n`;
|
|
264
|
+
|
|
265
|
+
return new Response(text, {
|
|
266
|
+
status: 500,
|
|
267
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Error — Bunflare</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
+
<link rel="stylesheet" href="./style.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body class="h-full bg-surface text-ink" style="font-family: 'Poppins', sans-serif;">
|
|
13
|
+
<div id="app" class="h-full"></div>
|
|
14
|
+
<script type="module" src="./app.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|