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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,28 @@
1
+ const DEFAULT_CF: Record<string, unknown> = {
2
+ country: "US",
3
+ city: "San Francisco",
4
+ continent: "NA",
5
+ latitude: "37.7749",
6
+ longitude: "-122.4194",
7
+ timezone: "America/Los_Angeles",
8
+ region: "California",
9
+ regionCode: "CA",
10
+ postalCode: "94102",
11
+ metroCode: "807",
12
+ asn: 13335,
13
+ asOrganization: "Cloudflare",
14
+ colo: "SFO",
15
+ httpProtocol: "HTTP/2",
16
+ tlsVersion: "TLSv1.3",
17
+ tlsCipher: "AEAD-AES128-GCM-SHA256",
18
+ };
19
+
20
+ export function addCfProperty(request: Request): Request {
21
+ Object.defineProperty(request, "cf", {
22
+ value: Object.freeze({ ...DEFAULT_CF }),
23
+ writable: false,
24
+ enumerable: false,
25
+ configurable: true,
26
+ });
27
+ return request;
28
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * RPC argument/return-value validation.
3
+ *
4
+ * Cloudflare Workers RPC serialises payloads via structured clone with
5
+ * extensions (function stubs, RpcTarget stubs). Bunflare runs everything
6
+ * in-process so values pass by reference — code that works locally may
7
+ * break in production. This module warns at dev time when a value
8
+ * contains types that CF cannot serialise.
9
+ */
10
+
11
+ const TYPED_ARRAY_TAGS = new Set([
12
+ "Int8Array", "Uint8Array", "Uint8ClampedArray",
13
+ "Int16Array", "Uint16Array",
14
+ "Int32Array", "Uint32Array",
15
+ "Float32Array", "Float64Array",
16
+ "BigInt64Array", "BigUint64Array",
17
+ ]);
18
+
19
+ function typeName(v: unknown): string {
20
+ if (v === null) return "null";
21
+ const tag = Object.prototype.toString.call(v); // "[object Foo]"
22
+ return tag.slice(8, -1); // "Foo"
23
+ }
24
+
25
+ /**
26
+ * Recursively validate a value for CF RPC serialisation compatibility.
27
+ * Returns an array of human-readable error strings (empty = OK).
28
+ */
29
+ export function validateRpcValue(value: unknown, path = "$"): string[] {
30
+ return _validate(value, path, new WeakSet());
31
+ }
32
+
33
+ function _validate(value: unknown, path: string, seen: WeakSet<object>): string[] {
34
+ // --- primitives ---
35
+ if (value === null || value === undefined) return [];
36
+ const t = typeof value;
37
+ if (t === "boolean" || t === "number" || t === "bigint" || t === "string") return [];
38
+
39
+ // --- symbol ---
40
+ if (t === "symbol") return [`${path}: Symbol values cannot be serialised over RPC`];
41
+
42
+ // --- function → CF converts to callback stub ---
43
+ if (t === "function") return [];
44
+
45
+ // --- object ---
46
+ if (t !== "object") return [`${path}: unsupported typeof "${t}"`];
47
+
48
+ const obj = value as object;
49
+
50
+ // cycle detection (structured clone handles cycles — just skip)
51
+ if (seen.has(obj)) return [];
52
+ seen.add(obj);
53
+
54
+ // --- explicitly disallowed ---
55
+ if (value instanceof Promise) return [`${path}: Promise values cannot be sent as RPC arguments`];
56
+ if (value instanceof WeakMap) return [`${path}: WeakMap cannot be serialised over RPC`];
57
+ if (value instanceof WeakSet) return [`${path}: WeakSet cannot be serialised over RPC`];
58
+ if (value instanceof SharedArrayBuffer) return [`${path}: SharedArrayBuffer cannot be serialised over RPC`];
59
+ if (typeof Blob !== "undefined" && value instanceof Blob && !(typeof File !== "undefined" && value instanceof File)) {
60
+ return [`${path}: Blob cannot be serialised over RPC`];
61
+ }
62
+ if (typeof File !== "undefined" && value instanceof File) {
63
+ return [`${path}: File cannot be serialised over RPC`];
64
+ }
65
+
66
+ // --- allowed leaf types (no recursion needed) ---
67
+ if (value instanceof Date) return [];
68
+ if (value instanceof RegExp) return [];
69
+ if (value instanceof Error) return [];
70
+ if (value instanceof ArrayBuffer) return [];
71
+ if (value instanceof DataView) return [];
72
+
73
+ // TypedArray: ArrayBuffer.isView but not DataView
74
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) return [];
75
+
76
+ // --- streams & web API types (pass-through on CF) ---
77
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) return [];
78
+ if (typeof WritableStream !== "undefined" && value instanceof WritableStream) return [];
79
+ if (typeof Request !== "undefined" && value instanceof Request) return [];
80
+ if (typeof Response !== "undefined" && value instanceof Response) return [];
81
+ if (typeof Headers !== "undefined" && value instanceof Headers) return [];
82
+
83
+ // --- Map ---
84
+ if (value instanceof Map) {
85
+ const errors: string[] = [];
86
+ let i = 0;
87
+ for (const [k, v] of value) {
88
+ errors.push(..._validate(k, `${path}.Map.key(${i})`, seen));
89
+ errors.push(..._validate(v, `${path}.Map.value(${i})`, seen));
90
+ i++;
91
+ }
92
+ return errors;
93
+ }
94
+
95
+ // --- Set ---
96
+ if (value instanceof Set) {
97
+ const errors: string[] = [];
98
+ let i = 0;
99
+ for (const v of value) {
100
+ errors.push(..._validate(v, `${path}.Set(${i})`, seen));
101
+ i++;
102
+ }
103
+ return errors;
104
+ }
105
+
106
+ // --- Array ---
107
+ if (Array.isArray(value)) {
108
+ const errors: string[] = [];
109
+ for (let i = 0; i < value.length; i++) {
110
+ errors.push(..._validate(value[i], `${path}[${i}]`, seen));
111
+ }
112
+ return errors;
113
+ }
114
+
115
+ // --- plain object ---
116
+ const proto = Object.getPrototypeOf(obj);
117
+ if (proto === Object.prototype || proto === null) {
118
+ const errors: string[] = [];
119
+ for (const key of Object.keys(obj)) {
120
+ errors.push(..._validate((obj as Record<string, unknown>)[key], `${path}.${key}`, seen));
121
+ }
122
+ return errors;
123
+ }
124
+
125
+ // --- RpcTarget instance: valid (becomes a stub on CF) ---
126
+ const brand = Symbol.for("bunflare.RpcTarget");
127
+ if ((obj as Record<symbol, unknown>)[brand] === true) return [];
128
+
129
+ // --- anything else: custom class instance ---
130
+ return [
131
+ `${path}: Custom class instance (${typeName(value)}) cannot be sent over RPC. ` +
132
+ `Extend RpcTarget or serialise manually.`,
133
+ ];
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Convenience wrappers called from binding proxies
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export function warnInvalidRpcArgs(args: unknown[], methodName: string): void {
141
+ for (let i = 0; i < args.length; i++) {
142
+ const errors = validateRpcValue(args[i], `arg${i}`);
143
+ for (const msg of errors) {
144
+ console.warn(`[bunflare] RPC ${methodName}() argument warning: ${msg}`);
145
+ }
146
+ }
147
+ }
148
+
149
+ export function warnInvalidRpcReturn(value: unknown, methodName: string): void {
150
+ const errors = validateRpcValue(value, "return");
151
+ for (const msg of errors) {
152
+ console.warn(`[bunflare] RPC ${methodName}() return value warning: ${msg}`);
153
+ }
154
+ }
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /** Mutable ref shared across all spans in the same trace. Allows fetch()
4
+ * call-site stacks captured in sub-spans to be visible in the root span's
5
+ * error handler. */
6
+ export interface FetchStackRef {
7
+ current: Error | null;
8
+ }
9
+
10
+ export interface SpanContext {
11
+ traceId: string;
12
+ spanId: string;
13
+ /** Shared ref to the last stack captured at an outbound fetch() call site.
14
+ * Used to reconstruct async stack traces broken by .then() in third-party
15
+ * libraries (e.g. GraphQL clients). The synchronous stack at the fetch()
16
+ * call still contains the user's code frames — we stitch it onto caught
17
+ * errors. */
18
+ fetchStack: FetchStackRef;
19
+ }
20
+
21
+ const storage = new AsyncLocalStorage<SpanContext>();
22
+
23
+ export function getActiveContext(): SpanContext | undefined {
24
+ return storage.getStore();
25
+ }
26
+
27
+ export function runWithContext<T>(ctx: SpanContext, fn: () => T): T {
28
+ return storage.run(ctx, fn);
29
+ }
30
+
31
+ export function generateId(byteCount = 8): string {
32
+ const bytes = new Uint8Array(byteCount);
33
+ crypto.getRandomValues(bytes);
34
+ return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
35
+ }
36
+
37
+ /** Generate a 128-bit trace ID (OTel standard: 16 bytes / 32 hex chars) */
38
+ export function generateTraceId(): string {
39
+ return generateId(16);
40
+ }
@@ -0,0 +1,73 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ const DATA_DIR = join(process.cwd(), ".bunflare");
6
+ const DB_PATH = join(DATA_DIR, "traces.sqlite");
7
+
8
+ let instance: Database | null = null;
9
+
10
+ export function getTracingDatabase(): Database {
11
+ if (instance) return instance;
12
+
13
+ mkdirSync(DATA_DIR, { recursive: true });
14
+
15
+ instance = new Database(DB_PATH, { create: true });
16
+ instance.run("PRAGMA journal_mode=WAL");
17
+ runTracingMigrations(instance);
18
+ return instance;
19
+ }
20
+
21
+ export function runTracingMigrations(db: Database): void {
22
+ db.run(`
23
+ CREATE TABLE IF NOT EXISTS spans (
24
+ span_id TEXT PRIMARY KEY,
25
+ trace_id TEXT NOT NULL,
26
+ parent_span_id TEXT,
27
+ name TEXT NOT NULL,
28
+ kind TEXT NOT NULL DEFAULT 'internal',
29
+ status TEXT NOT NULL DEFAULT 'unset',
30
+ status_message TEXT,
31
+ start_time INTEGER NOT NULL,
32
+ end_time INTEGER,
33
+ duration_ms REAL,
34
+ attributes TEXT,
35
+ worker_name TEXT
36
+ )
37
+ `);
38
+ db.run("CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id)");
39
+ db.run("CREATE INDEX IF NOT EXISTS idx_spans_start ON spans(start_time DESC)");
40
+
41
+ db.run(`
42
+ CREATE TABLE IF NOT EXISTS span_events (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ span_id TEXT NOT NULL,
45
+ trace_id TEXT NOT NULL,
46
+ timestamp INTEGER NOT NULL,
47
+ name TEXT NOT NULL,
48
+ level TEXT,
49
+ message TEXT,
50
+ attributes TEXT
51
+ )
52
+ `);
53
+ db.run("CREATE INDEX IF NOT EXISTS idx_events_span ON span_events(span_id)");
54
+ db.run("CREATE INDEX IF NOT EXISTS idx_events_trace ON span_events(trace_id)");
55
+
56
+ db.run(`
57
+ CREATE TABLE IF NOT EXISTS errors (
58
+ id TEXT PRIMARY KEY,
59
+ timestamp INTEGER NOT NULL,
60
+ error_name TEXT NOT NULL,
61
+ error_message TEXT NOT NULL,
62
+ request_method TEXT,
63
+ request_url TEXT,
64
+ worker_name TEXT,
65
+ trace_id TEXT,
66
+ span_id TEXT,
67
+ source TEXT,
68
+ data TEXT NOT NULL
69
+ )
70
+ `);
71
+ db.run("CREATE INDEX IF NOT EXISTS idx_errors_timestamp ON errors(timestamp DESC)");
72
+ db.run("CREATE INDEX IF NOT EXISTS idx_errors_trace ON errors(trace_id)");
73
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export interface StackFrame {
4
+ file: string;
5
+ line: number;
6
+ column: number;
7
+ function: string;
8
+ source?: string[];
9
+ sourceLine?: number;
10
+ }
11
+
12
+ const STACK_LINE_RE = /at\s+(?:(.+?)\s+\()?(.+):(\d+):(\d+)\)?/;
13
+
14
+ export function parseStackFrames(stack: string): StackFrame[] {
15
+ const frames: StackFrame[] = [];
16
+ for (const line of stack.split("\n")) {
17
+ const match = line.match(STACK_LINE_RE);
18
+ if (!match) continue;
19
+ frames.push({
20
+ file: match[2]!,
21
+ line: parseInt(match[3]!, 10),
22
+ column: parseInt(match[4]!, 10),
23
+ function: match[1] ?? "(anonymous)",
24
+ });
25
+ }
26
+ return frames;
27
+ }
28
+
29
+ /** Async version — reads source files asynchronously. */
30
+ export async function enrichFrameWithSourceAsync(frame: StackFrame): Promise<void> {
31
+ try {
32
+ const file = Bun.file(frame.file);
33
+ if (!await file.exists()) return;
34
+ const text = await file.text();
35
+ addSourceContext(frame, text);
36
+ } catch {
37
+ // File unreadable — skip source preview
38
+ }
39
+ }
40
+
41
+ /** Sync version — reads source files synchronously. Safe for use in catch blocks. */
42
+ export function enrichFrameWithSource(frame: StackFrame): void {
43
+ try {
44
+ const text = readFileSync(frame.file, "utf-8");
45
+ addSourceContext(frame, text);
46
+ } catch {
47
+ // File unreadable — skip source preview
48
+ }
49
+ }
50
+
51
+ function addSourceContext(frame: StackFrame, text: string): void {
52
+ const lines = text.split("\n");
53
+ const contextRadius = 7;
54
+ const start = Math.max(0, frame.line - 1 - contextRadius);
55
+ const end = Math.min(lines.length, frame.line + contextRadius);
56
+ frame.source = lines.slice(start, end);
57
+ frame.sourceLine = frame.line - 1 - start;
58
+ }
59
+
60
+ /** Parse, filter, enrich, and strip cwd from frames. Synchronous. */
61
+ export function buildErrorFrames(stack: string): StackFrame[] {
62
+ const frames = parseStackFrames(stack)
63
+ .filter(f => !f.file.startsWith("native:") && !f.file.startsWith("node:"));
64
+
65
+ const framesToEnrich = frames.slice(0, 20);
66
+ for (const frame of framesToEnrich) {
67
+ enrichFrameWithSource(frame);
68
+ }
69
+
70
+ const cwdPrefix = process.cwd() + "/";
71
+ return framesToEnrich.filter(f => f.source).map(f => ({
72
+ ...f,
73
+ file: f.file.startsWith(cwdPrefix) ? f.file.slice(cwdPrefix.length) : f.file,
74
+ }));
75
+ }
@@ -0,0 +1,186 @@
1
+ import { startSpan } from "./span";
2
+ import type { SpanData } from "./types";
3
+
4
+ /** Append caller frames to an error stack when Bun/JSC loses async context (ALS.run, .then). */
5
+ function stitchAsyncStack(err: Error, callerError: Error): void {
6
+ if (!err.stack || !callerError.stack) return;
7
+ if (err.stack.includes("--- async ---")) return;
8
+
9
+ const errFrames = err.stack.split("\n").filter(l => l.trim().startsWith("at "));
10
+ if (errFrames.length > 5 && !err.stack.includes("processTicksAndRejections")) return;
11
+
12
+ const callerLines = callerError.stack.split("\n").slice(1);
13
+ const filtered = callerLines.filter(l => !l.includes("/bunflare/runtime/"));
14
+ if (filtered.length === 0) return;
15
+
16
+ err.stack += "\n --- async ---\n" + filtered.join("\n");
17
+ }
18
+
19
+ interface InstrumentConfig {
20
+ type: string;
21
+ name: string;
22
+ methods: string[];
23
+ kind?: SpanData["kind"];
24
+ }
25
+
26
+ function wrapMethod(target: Function, type: string, bindingName: string, method: string, kind: SpanData["kind"]): Function {
27
+ return wrapMethodWithExtraAttrs(target, type, bindingName, method, kind, {});
28
+ }
29
+
30
+ function wrapMethodWithExtraAttrs(target: Function, type: string, bindingName: string, method: string, kind: SpanData["kind"], extraAttrs: Record<string, unknown>): Function {
31
+ return function (this: unknown, ...args: unknown[]) {
32
+ const attrs: Record<string, unknown> = {
33
+ "binding.type": type,
34
+ "binding.name": bindingName,
35
+ ...extraAttrs,
36
+ };
37
+ if (args.length > 0 && typeof args[0] === "string") {
38
+ attrs["binding.key"] = args[0];
39
+ }
40
+ return startSpan(
41
+ { name: `${type}.${method}`, kind, attributes: attrs },
42
+ () => target.apply(this, args),
43
+ );
44
+ };
45
+ }
46
+
47
+ export function instrumentBinding<T extends object>(binding: T, config: InstrumentConfig): T {
48
+ const kind = config.kind ?? "client";
49
+ const methodSet = new Set(config.methods);
50
+
51
+ return new Proxy(binding, {
52
+ get(obj, prop, receiver) {
53
+ const value = Reflect.get(obj, prop, receiver);
54
+ if (typeof prop === "string" && methodSet.has(prop) && typeof value === "function") {
55
+ return wrapMethod(value, config.type, config.name, prop, kind);
56
+ }
57
+ return value;
58
+ },
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Wrap D1 database: instrument statement execution methods (first, all, run, raw),
64
+ * not prepare() itself.
65
+ */
66
+ export function instrumentD1<T extends object>(binding: T, name: string): T {
67
+ return new Proxy(binding, {
68
+ get(obj, prop, receiver) {
69
+ const value = Reflect.get(obj, prop, receiver);
70
+ if (prop === "prepare" && typeof value === "function") {
71
+ return function (this: unknown, ...args: unknown[]) {
72
+ const stmt = value.apply(obj, args);
73
+ if (typeof stmt !== "object" || stmt === null) return stmt;
74
+ const sql = typeof args[0] === "string" ? args[0] : undefined;
75
+ return wrapD1Stmt(stmt, name, sql);
76
+ };
77
+ }
78
+ // Also instrument batch and exec directly
79
+ if ((prop === "batch" || prop === "exec" || prop === "dump") && typeof value === "function") {
80
+ return wrapMethod(value, "d1", name, prop as string, "client");
81
+ }
82
+ // withSession() returns a new database-like object — wrap it with the same instrumentation
83
+ if (prop === "withSession" && typeof value === "function") {
84
+ return function (this: unknown, ...args: unknown[]) {
85
+ const session = value.apply(obj, args);
86
+ if (typeof session === "object" && session !== null) {
87
+ return instrumentD1(session, name);
88
+ }
89
+ return session;
90
+ };
91
+ }
92
+ return value;
93
+ },
94
+ });
95
+ }
96
+
97
+ function wrapD1Stmt(stmt: object, name: string, sql: string | undefined): object {
98
+ const stmtMethods = ["first", "all", "run", "raw"];
99
+ return new Proxy(stmt, {
100
+ get(s, sProp, sReceiver) {
101
+ const sValue = Reflect.get(s, sProp, sReceiver);
102
+ if (typeof sProp === "string" && stmtMethods.includes(sProp) && typeof sValue === "function") {
103
+ return wrapMethodWithExtraAttrs(sValue.bind(s), "d1", name, sProp, "client", sql ? { "db.statement": sql } : {});
104
+ }
105
+ // bind() returns a new statement — re-wrap it so execution methods stay instrumented
106
+ if (sProp === "bind" && typeof sValue === "function") {
107
+ return function (this: unknown, ...args: unknown[]) {
108
+ const bound = sValue.apply(s, args);
109
+ if (typeof bound === "object" && bound !== null) {
110
+ return wrapD1Stmt(bound, name, sql);
111
+ }
112
+ return bound;
113
+ };
114
+ }
115
+ return sValue;
116
+ },
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Wrap DO namespace: get() returns a wrapped stub where fetch() and RPC methods are traced.
122
+ */
123
+ export function instrumentDONamespace<T extends object>(namespace: T, className: string): T {
124
+ return new Proxy(namespace, {
125
+ get(obj, prop, receiver) {
126
+ const value = Reflect.get(obj, prop, receiver);
127
+ if (prop === "get" && typeof value === "function") {
128
+ return function (this: unknown, ...args: unknown[]) {
129
+ const stub = value.apply(obj, args);
130
+ if (typeof stub !== "object" || stub === null) return stub;
131
+ return instrumentDOStub(stub, className);
132
+ };
133
+ }
134
+ return value;
135
+ },
136
+ });
137
+ }
138
+
139
+ function instrumentDOStub<T extends object>(stub: T, className: string): T {
140
+ return new Proxy(stub, {
141
+ get(obj, prop, receiver) {
142
+ const value = Reflect.get(obj, prop, receiver);
143
+ if (typeof prop === "symbol") return value;
144
+ // Skip internal/promise props
145
+ if (["then", "catch", "finally", "toJSON", "valueOf", "toString"].includes(prop)) return value;
146
+ if (typeof value === "function") {
147
+ // bind(obj) ensures correct `this` when the actual method is called;
148
+ // the wrapper's own `this` (from Proxy get trap) is intentionally ignored.
149
+ const wrapped = wrapMethod(value.bind(obj), "do", className, prop, "client");
150
+ // Capture caller stack at access site (before ALS.run destroys it)
151
+ // and stitch onto errors so the full call chain is visible.
152
+ return async function (this: unknown, ...args: unknown[]) {
153
+ const callerStack = new Error();
154
+ try {
155
+ return await (wrapped as Function).apply(this, args);
156
+ } catch (err) {
157
+ if (err instanceof Error) stitchAsyncStack(err, callerStack);
158
+ throw err;
159
+ }
160
+ };
161
+ }
162
+ return value;
163
+ },
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Wrap service binding: instrument fetch() and RPC methods.
169
+ */
170
+ export function instrumentServiceBinding<T extends object>(binding: T, serviceName: string): T {
171
+ const internalProps = new Set(["_wire", "isWired", "_subrequestCount"]);
172
+
173
+ return new Proxy(binding, {
174
+ get(obj, prop, receiver) {
175
+ const value = Reflect.get(obj, prop, receiver);
176
+ if (typeof prop === "symbol") return value;
177
+ if (internalProps.has(prop)) return value;
178
+ if (["then", "catch", "finally", "toJSON", "valueOf", "toString", "connect"].includes(prop)) return value;
179
+ if (typeof value === "function") {
180
+ const method = prop === "fetch" ? "fetch" : prop;
181
+ return wrapMethod(value, "service", serviceName, method, "client");
182
+ }
183
+ return value;
184
+ },
185
+ });
186
+ }
@@ -0,0 +1,138 @@
1
+ import { getActiveContext, runWithContext, generateId, generateTraceId } from "./context";
2
+ import { getTraceStore } from "./store";
3
+ import type { SpanData } from "./types";
4
+ import { buildErrorFrames } from "./frames";
5
+
6
+ export interface SpanOptions {
7
+ name: string;
8
+ kind?: SpanData["kind"];
9
+ attributes?: Record<string, unknown>;
10
+ workerName?: string;
11
+ /** Force a new root trace, ignoring any active parent context. */
12
+ newTrace?: boolean;
13
+ }
14
+
15
+ export async function startSpan<T>(opts: SpanOptions, fn: () => T | Promise<T>): Promise<T> {
16
+ const store = getTraceStore();
17
+ const parent = opts.newTrace ? undefined : getActiveContext();
18
+
19
+ const spanId = generateId();
20
+ const traceId = parent?.traceId ?? generateTraceId();
21
+ const parentSpanId = parent?.spanId ?? null;
22
+
23
+ const span: SpanData = {
24
+ spanId,
25
+ traceId,
26
+ parentSpanId,
27
+ name: opts.name,
28
+ kind: opts.kind ?? "internal",
29
+ status: "unset",
30
+ statusMessage: null,
31
+ startTime: Date.now(),
32
+ endTime: null,
33
+ durationMs: null,
34
+ attributes: opts.attributes ?? {},
35
+ workerName: opts.workerName ?? null,
36
+ };
37
+
38
+ store.insertSpan(span);
39
+
40
+ // Share fetchStack ref across all spans in the same trace so that
41
+ // fetch call-site stacks captured in sub-spans are visible in the root
42
+ // span's error handler.
43
+ const fetchStack = parent?.fetchStack ?? { current: null };
44
+
45
+ try {
46
+ const result = await runWithContext({ traceId, spanId, fetchStack }, () => fn());
47
+ if (result instanceof Response && result.status >= 500) {
48
+ store.setSpanStatus(spanId, "error", `HTTP ${result.status}`);
49
+ }
50
+ const currentStatus = store.getSpanStatus(spanId);
51
+ store.endSpan(spanId, Date.now(), currentStatus === "error" ? "error" : "ok");
52
+ return result;
53
+ } catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ store.endSpan(spanId, Date.now(), "error", message);
56
+ store.addEvent({
57
+ spanId,
58
+ traceId,
59
+ timestamp: Date.now(),
60
+ name: "exception",
61
+ level: "error",
62
+ message,
63
+ attributes: err instanceof Error ? { stack: err.stack } : {},
64
+ });
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ export function setSpanStatus(status: "ok" | "error", message?: string): void {
70
+ const ctx = getActiveContext();
71
+ if (!ctx) return;
72
+ const store = getTraceStore();
73
+ store.setSpanStatus(ctx.spanId, status, message ?? null);
74
+ }
75
+
76
+ export function setSpanAttribute(key: string, value: unknown): void {
77
+ const ctx = getActiveContext();
78
+ if (!ctx) return;
79
+ const store = getTraceStore();
80
+ store.updateAttributes(ctx.spanId, { [key]: value });
81
+ }
82
+
83
+ export function addSpanEvent(name: string, level: string, message: string, attrs?: Record<string, unknown>): void {
84
+ const ctx = getActiveContext();
85
+ if (!ctx) return;
86
+ const store = getTraceStore();
87
+ store.addEvent({
88
+ spanId: ctx.spanId,
89
+ traceId: ctx.traceId,
90
+ timestamp: Date.now(),
91
+ name,
92
+ level,
93
+ message,
94
+ attributes: attrs ?? {},
95
+ });
96
+ }
97
+
98
+ /** Persist an error to the errors table, linking it to the current trace/span context.
99
+ * Optional traceId/spanId override ALS context (needed when ALS scope is lost, e.g. after startSpan returns in Bun). */
100
+ export function persistError(error: unknown, source: string, workerName?: string, traceId?: string, spanId?: string): string | null {
101
+ try {
102
+ const err = error instanceof Error ? error : new Error(String(error));
103
+ const ctx = getActiveContext();
104
+ const store = getTraceStore();
105
+ const id = crypto.randomUUID();
106
+ store.insertError({
107
+ id,
108
+ timestamp: Date.now(),
109
+ errorName: err.name,
110
+ errorMessage: err.message,
111
+ workerName: workerName ?? null,
112
+ traceId: traceId ?? ctx?.traceId ?? null,
113
+ spanId: spanId ?? ctx?.spanId ?? null,
114
+ source,
115
+ data: JSON.stringify({
116
+ error: {
117
+ name: err.name,
118
+ message: err.message,
119
+ stack: err.stack ?? String(error),
120
+ frames: buildErrorFrames(err.stack ?? ""),
121
+ },
122
+ request: { method: "", url: "", headers: {} },
123
+ env: {},
124
+ bindings: [],
125
+ runtime: {
126
+ bunVersion: Bun.version,
127
+ platform: process.platform,
128
+ arch: process.arch,
129
+ workerName,
130
+ },
131
+ }),
132
+ });
133
+ return id;
134
+ } catch {
135
+ // Never let error persistence break the caller
136
+ return null;
137
+ }
138
+ }