@alwaysmeticulous/backend-recorder-workerd 2.339.0 → 2.343.0
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 +2 -2
- package/dist/body-capture.d.ts +7 -1
- package/dist/body-capture.js +8 -2
- package/dist/body-capture.js.map +1 -1
- package/dist/capture-buffer.js +3 -3
- package/dist/capture-buffer.js.map +1 -1
- package/dist/kv-capture.js +1 -1
- package/dist/kv-capture.js.map +1 -1
- package/dist/postgres/capture.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/vite/index.js +84 -3
- package/dist/vite/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ A request's capture events are batched into **one** report, sent under `ctx.wait
|
|
|
96
96
|
## What is captured
|
|
97
97
|
|
|
98
98
|
- **Inbound requests** (method, URL, headers, status; no bodies), correlated to the frontend session via the `x-meticulous-session-id` header stamped by the Meticulous frontend recorder — or, for the page's initial navigation, via an id the shim mints itself (see [Session ids for the first page render](#session-ids-for-the-first-page-render)).
|
|
99
|
-
- **Outgoing `fetch` calls** made while handling an inbound request: method, URL, headers, status, and request/response bodies (UTF-8, capped at
|
|
99
|
+
- **Outgoing `fetch` calls** made while handling an inbound request: method, URL, headers, status, and request/response bodies (UTF-8, capped at 1 MB; long-lived streams such as SSE are captured truncated).
|
|
100
100
|
- **Calls through `fetch`-shaped bindings** — service bindings and Durable Object stubs — recorded like outgoing fetches, plus the `env` key the call went through. No code change is needed: the binding's `fetch` is instrumented wherever the app reads it from, including via `cloudflare:workers` rather than the handler's `env` argument.
|
|
101
101
|
- **KV namespace operations** — `get`, `getWithMetadata`, `put`, `delete` and `list` on a namespace found on `env`, recorded with the binding name, the key, the call's arguments and the value as JSON (so one `JSON.parse` reconstructs exactly what the app saw, whichever `type` the read asked for). Also no code change needed. A value read as a stream is never read by the recorder — that would take the bytes from the app — and binary values are skipped; both cases are recorded with the reason instead of the value.
|
|
102
102
|
- **postgres.js queries**, when you apply `withMeticulousPostgres` (see above): the SQL with `$1`-style placeholders, the interpolated parameters, the row shape the query asked for, and the resolved rows — or the error, so a query that legitimately failed replays as the same failure rather than as a gap. Cursors, `.forEach()`, COPY streams and `.describe()` pass through unrecorded, since they do not resolve with a single result.
|
|
@@ -197,7 +197,7 @@ Because each function re-reads the current request's sink, a callback that outli
|
|
|
197
197
|
|
|
198
198
|
## Limitations
|
|
199
199
|
|
|
200
|
-
- Streamed responses are captured truncated: the inbound request is reported once the handler returns, which for a streamed response is before the stream ends, and outbound bodies cap at
|
|
200
|
+
- Streamed responses are captured truncated: the inbound request is reported once the handler returns, which for a streamed response is before the stream ends, and outbound bodies cap at 1 MB. An app whose main endpoints are SSE will not record them faithfully.
|
|
201
201
|
- Bindings other than `fetch`-shaped ones and KV namespaces are not captured: D1, R2, Queues, and RPC method calls on a named entrypoint (`env.SVC.someMethod()`) — an RPC method is not a patchable property, so it cannot be intercepted this way.
|
|
202
202
|
- A KV value that is not text is recorded without its value: reading a `stream` would take the bytes away from the app, and binary values are skipped because KV blobs are large and not UTF-8. The operation, key and arguments are still recorded, with `omitted` saying which case it was.
|
|
203
203
|
- Assets bindings are skipped by default (`ASSETS`, `__STATIC_CONTENT`): asset bytes are large and often binary, and asset serving is usually a worker's highest-volume call. Use `options.skipBindings` to skip others.
|
package/dist/body-capture.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { CapturedBody } from "./protocol.js";
|
|
2
2
|
|
|
3
3
|
//#region src/body-capture.d.ts
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Cap on the bytes of any one captured body, and so on the size of the span attribute it
|
|
6
|
+
* becomes. Defined here and re-exported by the Node recorder's `constants.ts` (same
|
|
7
|
+
* arrangement as the shared attribute names) so a Worker recording and a Node recording
|
|
8
|
+
* truncate at the same point, and so the replay side — which re-applies the cap to a live
|
|
9
|
+
* body before hashing it — agrees with what was stored.
|
|
10
|
+
*/
|
|
5
11
|
declare const MAX_BODY_CAPTURE_SIZE: number;
|
|
6
12
|
/**
|
|
7
13
|
* Reads a body stream up to {@link MAX_BODY_CAPTURE_SIZE} bytes, decoding as
|
package/dist/body-capture.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { redactRequestBody } from "./redact-body.js";
|
|
2
2
|
//#region src/body-capture.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Cap on the bytes of any one captured body, and so on the size of the span attribute it
|
|
5
|
+
* becomes. Defined here and re-exported by the Node recorder's `constants.ts` (same
|
|
6
|
+
* arrangement as the shared attribute names) so a Worker recording and a Node recording
|
|
7
|
+
* truncate at the same point, and so the replay side — which re-applies the cap to a live
|
|
8
|
+
* body before hashing it — agrees with what was stored.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_BODY_CAPTURE_SIZE = 1024 * 1024;
|
|
5
11
|
/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */
|
|
6
12
|
const BODY_READ_TIMEOUT_MS = 1e4;
|
|
7
13
|
const TIMED_OUT = Symbol("meticulous.bodyReadTimedOut");
|
package/dist/body-capture.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body-capture.js","names":[],"sources":["../src/body-capture.ts"],"sourcesContent":["import type { CapturedBody } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n
|
|
1
|
+
{"version":3,"file":"body-capture.js","names":[],"sources":["../src/body-capture.ts"],"sourcesContent":["import type { CapturedBody } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * Cap on the bytes of any one captured body, and so on the size of the span attribute it\n * becomes. Defined here and re-exported by the Node recorder's `constants.ts` (same\n * arrangement as the shared attribute names) so a Worker recording and a Node recording\n * truncate at the same point, and so the replay side — which re-applies the cap to a live\n * body before hashing it — agrees with what was stored.\n */\nexport const MAX_BODY_CAPTURE_SIZE = 1024 * 1024;\n\n/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */\nconst BODY_READ_TIMEOUT_MS = 10_000;\n\nconst TIMED_OUT = Symbol(\"meticulous.bodyReadTimedOut\");\n\n/**\n * Reads a body stream up to {@link MAX_BODY_CAPTURE_SIZE} bytes, decoding as\n * UTF-8. Stops (and marks the capture truncated) on the size cap or the time\n * guard, so a long-lived stream can never pin the capture work. Returns\n * undefined for absent bodies.\n */\nexport const readBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n if (!stream) {\n return undefined;\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), BODY_READ_TIMEOUT_MS);\n });\n\n try {\n while (true) {\n const result = await Promise.race([reader.read(), timedOut]);\n if (result === TIMED_OUT) {\n truncated = true;\n break;\n }\n const { done, value } = result;\n if (done) {\n break;\n }\n if (!value || value.byteLength === 0) {\n continue;\n }\n const remaining = MAX_BODY_CAPTURE_SIZE - bytes;\n if (value.byteLength >= remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n bytes += remaining;\n truncated = truncated || value.byteLength > remaining;\n // Even an exact-cap chunk means we stop reading, so a longer stream\n // counts as truncated.\n const next = await Promise.race([reader.read(), timedOut]);\n if (next === TIMED_OUT || !next.done) {\n truncated = true;\n }\n break;\n }\n text += decoder.decode(value, { stream: true });\n bytes += value.byteLength;\n }\n } finally {\n clearTimeout(timer);\n reader.cancel().catch(() => {});\n }\n\n text += decoder.decode();\n return { body: text, truncated };\n};\n\n/**\n * Reads a *request* body, redacting secret-looking fields before it leaves the worker.\n *\n * Every request-body capture path goes through here, so record and replay redact\n * identically and any hash derived from the body still agrees on both sides. Response\n * bodies deliberately use {@link readBodyWithCap} directly — they are served back to the\n * app during replay and must stay byte-exact.\n */\nexport const readRequestBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n const captured = await readBodyWithCap(stream);\n if (captured === undefined) {\n return undefined;\n }\n return { ...captured, body: redactRequestBody(captured.body) };\n};\n"],"mappings":";;;;;;;;;AAUA,MAAa,wBAAwB,OAAO;;AAG5C,MAAM,uBAAuB;AAE7B,MAAM,YAAY,OAAO,8BAA8B;;;;;;;AAQvD,MAAa,kBAAkB,OAC7B,WACsC;AACtC,KAAI,CAAC,OACH;CAGF,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,UAAU,IAAI,YAAY,QAAQ;CACxC,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,YAAY;CAEhB,IAAI;CACJ,MAAM,WAAW,IAAI,SAA2B,YAAY;AAC1D,UAAQ,iBAAiB,QAAQ,UAAU,EAAE,qBAAqB;GAClE;AAEF,KAAI;AACF,SAAO,MAAM;GACX,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC5D,OAAI,WAAW,WAAW;AACxB,gBAAY;AACZ;;GAEF,MAAM,EAAE,MAAM,UAAU;AACxB,OAAI,KACF;AAEF,OAAI,CAAC,SAAS,MAAM,eAAe,EACjC;GAEF,MAAM,YAAY,wBAAwB;AAC1C,OAAI,MAAM,cAAc,WAAW;AACjC,YAAQ,QAAQ,OAAO,MAAM,SAAS,GAAG,UAAU,EAAE,EAAE,QAAQ,MAAM,CAAC;AACtE,aAAS;AACT,gBAAY,aAAa,MAAM,aAAa;IAG5C,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC1D,QAAI,SAAS,aAAa,CAAC,KAAK,KAC9B,aAAY;AAEd;;AAEF,WAAQ,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;AAC/C,YAAS,MAAM;;WAET;AACR,eAAa,MAAM;AACnB,SAAO,QAAQ,CAAC,YAAY,GAAG;;AAGjC,SAAQ,QAAQ,QAAQ;AACxB,QAAO;EAAE,MAAM;EAAM;EAAW;;;;;;;;;;AAWlC,MAAa,yBAAyB,OACpC,WACsC;CACtC,MAAM,WAAW,MAAM,gBAAgB,OAAO;AAC9C,KAAI,aAAa,KAAA,EACf;AAEF,QAAO;EAAE,GAAG;EAAU,MAAM,kBAAkB,SAAS,KAAK;EAAE"}
|
package/dist/capture-buffer.js
CHANGED
|
@@ -81,9 +81,9 @@ var CaptureBuffer = class {
|
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
/**
|
|
84
|
-
* Buffered-payload ceiling, in JSON characters.
|
|
85
|
-
* body-carrying calls
|
|
86
|
-
* this bound
|
|
84
|
+
* Buffered-payload ceiling, in JSON characters. An event can carry two bodies of
|
|
85
|
+
* `MAX_BODY_CAPTURE_SIZE`, so one or two body-carrying calls can reach this; the sidecar's own
|
|
86
|
+
* payload limit is 8 MB, which leaves the batch this bound builds room for JSON escaping.
|
|
87
87
|
*/
|
|
88
88
|
const MAX_BUFFERED_CHARS = 2 * 1024 * 1024;
|
|
89
89
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-buffer.js","names":[],"sources":["../src/capture-buffer.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport type { CaptureEvent } from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\nimport type { SidecarTransport } from \"./sidecar-transport\";\n\n/**\n * Batches one request's capture events into a single report to the sidecar.\n *\n * A local `wrangler dev` recording could afford a POST per event over loopback. A deployed\n * Worker cannot: each one is a subrequest, and — with a service-binding transport — an extra\n * hop the runtime has to keep the request context alive for. An SSR request making twenty\n * outbound calls should cost one report, not twenty.\n *\n * Batching means the report has to wait for work that finishes *after* the handler returns: a\n * response body is only captured once its clone has been drained. So producers register here\n * with {@link track} instead of each holding its own `waitUntil`, and {@link close} — the single\n * promise the wrapper hands to `waitUntil` — drains them before sending.\n */\nexport class CaptureBuffer {\n private events: CaptureEvent[] = [];\n private bufferedChars = 0;\n private readonly pending = new Set<Promise<void>>();\n /** Chained so two flushes can never interleave and reorder a session's chunks. */\n private sending: Promise<void> = Promise.resolve();\n private closed = false;\n\n constructor(\n private readonly transport: SidecarTransport,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {}\n\n /**\n * Registers work that will add zero or more events. Never rejects: a producer that throws is\n * warned about once and dropped, exactly as an unbatched capture failure was.\n */\n track(\n producer: () => Promise<void>,\n warnKey: string,\n warnMessage: string,\n ): void {\n const promise = producer()\n .catch((error) => {\n warnOnce(warnKey, warnMessage, error);\n })\n .finally(() => {\n this.pending.delete(promise);\n });\n this.pending.add(promise);\n // A closed buffer has no `close()` left to await this, so it needs its own lifetime\n // extension — see the same reasoning in `add`.\n if (this.closed) {\n this.waitUntil(promise);\n }\n }\n\n /**\n * Buffers one event, sending early if the buffer has grown past {@link MAX_BUFFERED_CHARS}.\n *\n * The early send matters for exactly the requests batching is riskiest for: a long-lived SSE\n * response, or a handler making many body-carrying calls, would otherwise hold every captured\n * body in isolate memory until the request ended.\n */\n add(event: CaptureEvent): void {\n if (this.closed) {\n // Work the app itself queued in `waitUntil` can still make calls after our own `close()`\n // has run. Report those the old way rather than dropping them.\n this.waitUntil(this.send([event]));\n return;\n }\n this.events.push(event);\n this.bufferedChars += estimateChars(event);\n if (this.bufferedChars >= MAX_BUFFERED_CHARS) {\n this.waitUntil(this.flush());\n }\n }\n\n /**\n * Drains every producer and sends what they buffered, repeating while draining produces more\n * work (a body read that starts another capture). Idempotent; after it resolves the buffer is\n * closed and later events are sent individually.\n */\n async close(): Promise<void> {\n for (let round = 0; round < MAX_DRAIN_ROUNDS; round++) {\n if (this.pending.size === 0) {\n break;\n }\n await Promise.all([...this.pending]);\n }\n this.closed = true;\n await this.flush();\n }\n\n private flush(): Promise<void> {\n if (this.events.length === 0) {\n return this.sending;\n }\n const batch = this.events;\n this.events = [];\n this.bufferedChars = 0;\n this.sending = this.sending.then(() => this.send(batch));\n return this.sending;\n }\n\n /** `postCaptureEvents` never rejects — it warns once on an unreachable sidecar. */\n private send(events: CaptureEvent[]): Promise<void> {\n return postCaptureEvents(this.transport, events);\n }\n}\n\n/**\n * Buffered-payload ceiling, in JSON characters.
|
|
1
|
+
{"version":3,"file":"capture-buffer.js","names":[],"sources":["../src/capture-buffer.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport type { CaptureEvent } from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\nimport type { SidecarTransport } from \"./sidecar-transport\";\n\n/**\n * Batches one request's capture events into a single report to the sidecar.\n *\n * A local `wrangler dev` recording could afford a POST per event over loopback. A deployed\n * Worker cannot: each one is a subrequest, and — with a service-binding transport — an extra\n * hop the runtime has to keep the request context alive for. An SSR request making twenty\n * outbound calls should cost one report, not twenty.\n *\n * Batching means the report has to wait for work that finishes *after* the handler returns: a\n * response body is only captured once its clone has been drained. So producers register here\n * with {@link track} instead of each holding its own `waitUntil`, and {@link close} — the single\n * promise the wrapper hands to `waitUntil` — drains them before sending.\n */\nexport class CaptureBuffer {\n private events: CaptureEvent[] = [];\n private bufferedChars = 0;\n private readonly pending = new Set<Promise<void>>();\n /** Chained so two flushes can never interleave and reorder a session's chunks. */\n private sending: Promise<void> = Promise.resolve();\n private closed = false;\n\n constructor(\n private readonly transport: SidecarTransport,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {}\n\n /**\n * Registers work that will add zero or more events. Never rejects: a producer that throws is\n * warned about once and dropped, exactly as an unbatched capture failure was.\n */\n track(\n producer: () => Promise<void>,\n warnKey: string,\n warnMessage: string,\n ): void {\n const promise = producer()\n .catch((error) => {\n warnOnce(warnKey, warnMessage, error);\n })\n .finally(() => {\n this.pending.delete(promise);\n });\n this.pending.add(promise);\n // A closed buffer has no `close()` left to await this, so it needs its own lifetime\n // extension — see the same reasoning in `add`.\n if (this.closed) {\n this.waitUntil(promise);\n }\n }\n\n /**\n * Buffers one event, sending early if the buffer has grown past {@link MAX_BUFFERED_CHARS}.\n *\n * The early send matters for exactly the requests batching is riskiest for: a long-lived SSE\n * response, or a handler making many body-carrying calls, would otherwise hold every captured\n * body in isolate memory until the request ended.\n */\n add(event: CaptureEvent): void {\n if (this.closed) {\n // Work the app itself queued in `waitUntil` can still make calls after our own `close()`\n // has run. Report those the old way rather than dropping them.\n this.waitUntil(this.send([event]));\n return;\n }\n this.events.push(event);\n this.bufferedChars += estimateChars(event);\n if (this.bufferedChars >= MAX_BUFFERED_CHARS) {\n this.waitUntil(this.flush());\n }\n }\n\n /**\n * Drains every producer and sends what they buffered, repeating while draining produces more\n * work (a body read that starts another capture). Idempotent; after it resolves the buffer is\n * closed and later events are sent individually.\n */\n async close(): Promise<void> {\n for (let round = 0; round < MAX_DRAIN_ROUNDS; round++) {\n if (this.pending.size === 0) {\n break;\n }\n await Promise.all([...this.pending]);\n }\n this.closed = true;\n await this.flush();\n }\n\n private flush(): Promise<void> {\n if (this.events.length === 0) {\n return this.sending;\n }\n const batch = this.events;\n this.events = [];\n this.bufferedChars = 0;\n this.sending = this.sending.then(() => this.send(batch));\n return this.sending;\n }\n\n /** `postCaptureEvents` never rejects — it warns once on an unreachable sidecar. */\n private send(events: CaptureEvent[]): Promise<void> {\n return postCaptureEvents(this.transport, events);\n }\n}\n\n/**\n * Buffered-payload ceiling, in JSON characters. An event can carry two bodies of\n * `MAX_BODY_CAPTURE_SIZE`, so one or two body-carrying calls can reach this; the sidecar's own\n * payload limit is 8 MB, which leaves the batch this bound builds room for JSON escaping.\n */\nconst MAX_BUFFERED_CHARS = 2 * 1024 * 1024;\n\n/**\n * Bounds `close()` in the pathological case where every drained producer starts another. Capture\n * is a leaf operation, so one round is the norm and two the most a body read can cause.\n */\nconst MAX_DRAIN_ROUNDS = 4;\n\n/**\n * Rough size of an event, counting only the fields that can be large. Cheaper than\n * `JSON.stringify` on the whole event, and this only needs to be right to within a factor.\n */\nconst estimateChars = (event: CaptureEvent): number => {\n let chars = 256;\n if (\"requestBody\" in event && event.requestBody !== undefined) {\n chars += event.requestBody.body.length;\n }\n if (\"responseBody\" in event && event.responseBody !== undefined) {\n chars += event.responseBody.body.length;\n }\n if (\"result\" in event && event.result !== undefined) {\n chars += event.result.body.length;\n }\n if (\"value\" in event && event.value !== undefined) {\n chars += event.value.body.length;\n }\n if (\"args\" in event && event.args !== undefined) {\n chars += event.args.body.length;\n }\n if (\"queryText\" in event) {\n chars += event.queryText.length + event.params.length;\n }\n return chars;\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,IAAa,gBAAb,MAA2B;CACzB,SAAiC,EAAE;CACnC,gBAAwB;CACxB,0BAA2B,IAAI,KAAoB;;CAEnD,UAAiC,QAAQ,SAAS;CAClD,SAAiB;CAEjB,YACE,WACA,WACA;AAFiB,OAAA,YAAA;AACA,OAAA,YAAA;;;;;;CAOnB,MACE,UACA,SACA,aACM;EACN,MAAM,UAAU,UAAU,CACvB,OAAO,UAAU;AAChB,YAAS,SAAS,aAAa,MAAM;IACrC,CACD,cAAc;AACb,QAAK,QAAQ,OAAO,QAAQ;IAC5B;AACJ,OAAK,QAAQ,IAAI,QAAQ;AAGzB,MAAI,KAAK,OACP,MAAK,UAAU,QAAQ;;;;;;;;;CAW3B,IAAI,OAA2B;AAC7B,MAAI,KAAK,QAAQ;AAGf,QAAK,UAAU,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;AAClC;;AAEF,OAAK,OAAO,KAAK,MAAM;AACvB,OAAK,iBAAiB,cAAc,MAAM;AAC1C,MAAI,KAAK,iBAAiB,mBACxB,MAAK,UAAU,KAAK,OAAO,CAAC;;;;;;;CAShC,MAAM,QAAuB;AAC3B,OAAK,IAAI,QAAQ,GAAG,QAAQ,kBAAkB,SAAS;AACrD,OAAI,KAAK,QAAQ,SAAS,EACxB;AAEF,SAAM,QAAQ,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC;;AAEtC,OAAK,SAAS;AACd,QAAM,KAAK,OAAO;;CAGpB,QAA+B;AAC7B,MAAI,KAAK,OAAO,WAAW,EACzB,QAAO,KAAK;EAEd,MAAM,QAAQ,KAAK;AACnB,OAAK,SAAS,EAAE;AAChB,OAAK,gBAAgB;AACrB,OAAK,UAAU,KAAK,QAAQ,WAAW,KAAK,KAAK,MAAM,CAAC;AACxD,SAAO,KAAK;;;CAId,KAAa,QAAuC;AAClD,SAAO,kBAAkB,KAAK,WAAW,OAAO;;;;;;;;AASpD,MAAM,qBAAqB,IAAI,OAAO;;;;;AAMtC,MAAM,mBAAmB;;;;;AAMzB,MAAM,iBAAiB,UAAgC;CACrD,IAAI,QAAQ;AACZ,KAAI,iBAAiB,SAAS,MAAM,gBAAgB,KAAA,EAClD,UAAS,MAAM,YAAY,KAAK;AAElC,KAAI,kBAAkB,SAAS,MAAM,iBAAiB,KAAA,EACpD,UAAS,MAAM,aAAa,KAAK;AAEnC,KAAI,YAAY,SAAS,MAAM,WAAW,KAAA,EACxC,UAAS,MAAM,OAAO,KAAK;AAE7B,KAAI,WAAW,SAAS,MAAM,UAAU,KAAA,EACtC,UAAS,MAAM,MAAM,KAAK;AAE5B,KAAI,UAAU,SAAS,MAAM,SAAS,KAAA,EACpC,UAAS,MAAM,KAAK,KAAK;AAE3B,KAAI,eAAe,MACjB,UAAS,MAAM,UAAU,SAAS,MAAM,OAAO;AAEjD,QAAO"}
|
package/dist/kv-capture.js
CHANGED
|
@@ -76,7 +76,7 @@ const omittedReason = (value) => {
|
|
|
76
76
|
if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) return "stream";
|
|
77
77
|
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return "binary";
|
|
78
78
|
};
|
|
79
|
-
const capture = (json) => json.length >
|
|
79
|
+
const capture = (json) => json.length > 1048576 ? {
|
|
80
80
|
body: json.slice(0, MAX_BODY_CAPTURE_SIZE),
|
|
81
81
|
truncated: true
|
|
82
82
|
} : {
|
package/dist/kv-capture.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv-capture.js","names":[],"sources":["../src/kv-capture.ts"],"sourcesContent":["import { MAX_BODY_CAPTURE_SIZE } from \"./body-capture\";\nimport type { CapturedBody, KvOmittedReason, KvOperation } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * How a KV operation's key, arguments and value are turned into persistable fields.\n *\n * This lives apart from `kv-patch.ts` because it is not workerd-specific: the same operations\n * are recorded from a **Node** process whose bindings come from wrangler's `getPlatformProxy`\n * (`withMeticulousCloudflareEnv` in `@alwaysmeticulous/backend-recorder-js`), and the two\n * surfaces have to produce byte-identical fields or a session recorded through one cannot be\n * replayed through the other. So the contract is defined once, here, and both call it.\n */\n\ninterface SerializedKvValue {\n captured?: CapturedBody;\n omitted?: KvOmittedReason;\n}\n\n/** The outcome of a KV operation: what it returned, or how it failed. */\nexport type KvCaptureOutcome = { result: unknown } | { error: string };\n\n/** The persistable fields of one KV operation, as `KvOperationEvent` carries them. */\nexport interface KvCaptureFields {\n /** The key operated on, when it is a single string. Absent for `list` and a bulk `get`. */\n key?: string;\n /** JSON of the call's arguments, with a `put`'s value slot nulled out. */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, redacted. */\n value?: CapturedBody;\n /** JSON of what the operation returned. Absent for `put`/`delete` and for an error. */\n result?: CapturedBody;\n /** Why a value is missing. Refers to the written value for `put`, the read value otherwise. */\n omitted?: KvOmittedReason;\n}\n\n/**\n * Serializes one KV operation's fields. Never throws: a value that cannot be serialized is\n * simply absent, so the operation is still recorded and only its payload is missing.\n */\nexport const serializeKvCaptureFields = (\n operation: KvOperation,\n args: unknown[],\n outcome: KvCaptureOutcome,\n): KvCaptureFields => {\n const [key] = args;\n const written =\n operation === \"put\" ? serializeWrittenValue(args[1]) : undefined;\n const read =\n \"result\" in outcome ? serializeKvValue(outcome.result) : undefined;\n const omitted = written?.omitted ?? read?.omitted;\n const serializedArgs = serializeKvArgs(operation, args);\n\n return {\n ...(typeof key === \"string\" ? { key } : {}),\n ...(serializedArgs !== undefined ? { args: serializedArgs } : {}),\n ...(written?.captured !== undefined ? { value: written.captured } : {}),\n ...(read?.captured !== undefined ? { result: read.captured } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n};\n\n/**\n * JSON of a value the runtime returned, with anything unpersistable replaced by `null` and the\n * reason reported alongside. A `Map` (what a bulk `get` returns) becomes a plain object, which\n * `JSON.stringify` would otherwise flatten to `{}`.\n *\n * Read values are stored verbatim, deliberately: replay serves them straight back to the app,\n * so redacting one would corrupt what the app receives. Only the write path\n * ({@link serializeWrittenValue}) is redacted — nothing ever replays a `put`.\n */\nconst serializeKvValue = (value: unknown): SerializedKvValue => {\n let omitted: KvOmittedReason | undefined;\n try {\n const json = JSON.stringify(value, (_key: string, nested: unknown) => {\n const reason = omittedReason(nested);\n if (reason !== undefined) {\n omitted ??= reason;\n return null;\n }\n return nested instanceof Map\n ? Object.fromEntries(nested as Map<string, unknown>)\n : nested;\n });\n return {\n // `undefined` (what `put`/`delete` resolve to) has no JSON form — nothing to capture.\n ...(json !== undefined ? { captured: capture(json) } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n } catch {\n // Circular, or a getter that throws. The operation itself is still recorded.\n return {};\n }\n};\n\n/**\n * JSON of the value a `put` wrote, redacted like a request body. KV takes a string, an\n * ArrayBuffer(View) or a stream; only the string form can be captured, and it is redacted as\n * text before being JSON-encoded so that secret-looking fields *inside* the app's payload are\n * caught (redacting the JSON-encoded form would only ever see one opaque string).\n */\nconst serializeWrittenValue = (value: unknown): SerializedKvValue => {\n if (typeof value === \"string\") {\n try {\n return { captured: capture(JSON.stringify(redactRequestBody(value))) };\n } catch {\n return {};\n }\n }\n return serializeKvValue(value);\n};\n\n/**\n * JSON of the call's arguments. A `put` value is replaced by `null` rather than dropped, so the\n * remaining arguments keep their positions — and so the value is only ever persisted via the\n * redacted {@link serializeWrittenValue} path.\n *\n * Exported because it is a KV operation's whole identity: a replay derives its lookup key from the\n * live arguments and compares it against this, so the two must be produced by one function.\n */\nexport const serializeKvArgs = (\n operation: KvOperation,\n args: unknown[],\n): CapturedBody | undefined => {\n const withoutValue =\n operation === \"put\"\n ? args.map((arg, index) => (index === 1 ? null : arg))\n : args;\n return serializeKvValue(withoutValue).captured;\n};\n\nconst omittedReason = (value: unknown): KvOmittedReason | undefined => {\n // A stream is never read, only recognised: reading it would consume the app's bytes.\n if (\n typeof ReadableStream !== \"undefined\" &&\n value instanceof ReadableStream\n ) {\n return \"stream\";\n }\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {\n return \"binary\";\n }\n return undefined;\n};\n\nconst capture = (json: string): CapturedBody =>\n json.length > MAX_BODY_CAPTURE_SIZE\n ? { body: json.slice(0, MAX_BODY_CAPTURE_SIZE), truncated: true }\n : { body: json, truncated: false };\n"],"mappings":";;;;;;;AAwCA,MAAa,4BACX,WACA,MACA,YACoB;CACpB,MAAM,CAAC,OAAO;CACd,MAAM,UACJ,cAAc,QAAQ,sBAAsB,KAAK,GAAG,GAAG,KAAA;CACzD,MAAM,OACJ,YAAY,UAAU,iBAAiB,QAAQ,OAAO,GAAG,KAAA;CAC3D,MAAM,UAAU,SAAS,WAAW,MAAM;CAC1C,MAAM,iBAAiB,gBAAgB,WAAW,KAAK;AAEvD,QAAO;EACL,GAAI,OAAO,QAAQ,WAAW,EAAE,KAAK,GAAG,EAAE;EAC1C,GAAI,mBAAmB,KAAA,IAAY,EAAE,MAAM,gBAAgB,GAAG,EAAE;EAChE,GAAI,SAAS,aAAa,KAAA,IAAY,EAAE,OAAO,QAAQ,UAAU,GAAG,EAAE;EACtE,GAAI,MAAM,aAAa,KAAA,IAAY,EAAE,QAAQ,KAAK,UAAU,GAAG,EAAE;EACjE,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;EAC7C;;;;;;;;;;;AAYH,MAAM,oBAAoB,UAAsC;CAC9D,IAAI;AACJ,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,QAAQ,MAAc,WAAoB;GACpE,MAAM,SAAS,cAAc,OAAO;AACpC,OAAI,WAAW,KAAA,GAAW;AACxB,gBAAY;AACZ,WAAO;;AAET,UAAO,kBAAkB,MACrB,OAAO,YAAY,OAA+B,GAClD;IACJ;AACF,SAAO;GAEL,GAAI,SAAS,KAAA,IAAY,EAAE,UAAU,QAAQ,KAAK,EAAE,GAAG,EAAE;GACzD,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;GAC7C;SACK;AAEN,SAAO,EAAE;;;;;;;;;AAUb,MAAM,yBAAyB,UAAsC;AACnE,KAAI,OAAO,UAAU,SACnB,KAAI;AACF,SAAO,EAAE,UAAU,QAAQ,KAAK,UAAU,kBAAkB,MAAM,CAAC,CAAC,EAAE;SAChE;AACN,SAAO,EAAE;;AAGb,QAAO,iBAAiB,MAAM;;;;;;;;;;AAWhC,MAAa,mBACX,WACA,SAC6B;AAK7B,QAAO,iBAHL,cAAc,QACV,KAAK,KAAK,KAAK,UAAW,UAAU,IAAI,OAAO,IAAK,GACpD,KAC+B,CAAC;;AAGxC,MAAM,iBAAiB,UAAgD;AAErE,KACE,OAAO,mBAAmB,eAC1B,iBAAiB,eAEjB,QAAO;AAET,KAAI,iBAAiB,eAAe,YAAY,OAAO,MAAM,CAC3D,QAAO;;AAKX,MAAM,WAAW,SACf,KAAK,SAAA,
|
|
1
|
+
{"version":3,"file":"kv-capture.js","names":[],"sources":["../src/kv-capture.ts"],"sourcesContent":["import { MAX_BODY_CAPTURE_SIZE } from \"./body-capture\";\nimport type { CapturedBody, KvOmittedReason, KvOperation } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * How a KV operation's key, arguments and value are turned into persistable fields.\n *\n * This lives apart from `kv-patch.ts` because it is not workerd-specific: the same operations\n * are recorded from a **Node** process whose bindings come from wrangler's `getPlatformProxy`\n * (`withMeticulousCloudflareEnv` in `@alwaysmeticulous/backend-recorder-js`), and the two\n * surfaces have to produce byte-identical fields or a session recorded through one cannot be\n * replayed through the other. So the contract is defined once, here, and both call it.\n */\n\ninterface SerializedKvValue {\n captured?: CapturedBody;\n omitted?: KvOmittedReason;\n}\n\n/** The outcome of a KV operation: what it returned, or how it failed. */\nexport type KvCaptureOutcome = { result: unknown } | { error: string };\n\n/** The persistable fields of one KV operation, as `KvOperationEvent` carries them. */\nexport interface KvCaptureFields {\n /** The key operated on, when it is a single string. Absent for `list` and a bulk `get`. */\n key?: string;\n /** JSON of the call's arguments, with a `put`'s value slot nulled out. */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, redacted. */\n value?: CapturedBody;\n /** JSON of what the operation returned. Absent for `put`/`delete` and for an error. */\n result?: CapturedBody;\n /** Why a value is missing. Refers to the written value for `put`, the read value otherwise. */\n omitted?: KvOmittedReason;\n}\n\n/**\n * Serializes one KV operation's fields. Never throws: a value that cannot be serialized is\n * simply absent, so the operation is still recorded and only its payload is missing.\n */\nexport const serializeKvCaptureFields = (\n operation: KvOperation,\n args: unknown[],\n outcome: KvCaptureOutcome,\n): KvCaptureFields => {\n const [key] = args;\n const written =\n operation === \"put\" ? serializeWrittenValue(args[1]) : undefined;\n const read =\n \"result\" in outcome ? serializeKvValue(outcome.result) : undefined;\n const omitted = written?.omitted ?? read?.omitted;\n const serializedArgs = serializeKvArgs(operation, args);\n\n return {\n ...(typeof key === \"string\" ? { key } : {}),\n ...(serializedArgs !== undefined ? { args: serializedArgs } : {}),\n ...(written?.captured !== undefined ? { value: written.captured } : {}),\n ...(read?.captured !== undefined ? { result: read.captured } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n};\n\n/**\n * JSON of a value the runtime returned, with anything unpersistable replaced by `null` and the\n * reason reported alongside. A `Map` (what a bulk `get` returns) becomes a plain object, which\n * `JSON.stringify` would otherwise flatten to `{}`.\n *\n * Read values are stored verbatim, deliberately: replay serves them straight back to the app,\n * so redacting one would corrupt what the app receives. Only the write path\n * ({@link serializeWrittenValue}) is redacted — nothing ever replays a `put`.\n */\nconst serializeKvValue = (value: unknown): SerializedKvValue => {\n let omitted: KvOmittedReason | undefined;\n try {\n const json = JSON.stringify(value, (_key: string, nested: unknown) => {\n const reason = omittedReason(nested);\n if (reason !== undefined) {\n omitted ??= reason;\n return null;\n }\n return nested instanceof Map\n ? Object.fromEntries(nested as Map<string, unknown>)\n : nested;\n });\n return {\n // `undefined` (what `put`/`delete` resolve to) has no JSON form — nothing to capture.\n ...(json !== undefined ? { captured: capture(json) } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n } catch {\n // Circular, or a getter that throws. The operation itself is still recorded.\n return {};\n }\n};\n\n/**\n * JSON of the value a `put` wrote, redacted like a request body. KV takes a string, an\n * ArrayBuffer(View) or a stream; only the string form can be captured, and it is redacted as\n * text before being JSON-encoded so that secret-looking fields *inside* the app's payload are\n * caught (redacting the JSON-encoded form would only ever see one opaque string).\n */\nconst serializeWrittenValue = (value: unknown): SerializedKvValue => {\n if (typeof value === \"string\") {\n try {\n return { captured: capture(JSON.stringify(redactRequestBody(value))) };\n } catch {\n return {};\n }\n }\n return serializeKvValue(value);\n};\n\n/**\n * JSON of the call's arguments. A `put` value is replaced by `null` rather than dropped, so the\n * remaining arguments keep their positions — and so the value is only ever persisted via the\n * redacted {@link serializeWrittenValue} path.\n *\n * Exported because it is a KV operation's whole identity: a replay derives its lookup key from the\n * live arguments and compares it against this, so the two must be produced by one function.\n */\nexport const serializeKvArgs = (\n operation: KvOperation,\n args: unknown[],\n): CapturedBody | undefined => {\n const withoutValue =\n operation === \"put\"\n ? args.map((arg, index) => (index === 1 ? null : arg))\n : args;\n return serializeKvValue(withoutValue).captured;\n};\n\nconst omittedReason = (value: unknown): KvOmittedReason | undefined => {\n // A stream is never read, only recognised: reading it would consume the app's bytes.\n if (\n typeof ReadableStream !== \"undefined\" &&\n value instanceof ReadableStream\n ) {\n return \"stream\";\n }\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {\n return \"binary\";\n }\n return undefined;\n};\n\nconst capture = (json: string): CapturedBody =>\n json.length > MAX_BODY_CAPTURE_SIZE\n ? { body: json.slice(0, MAX_BODY_CAPTURE_SIZE), truncated: true }\n : { body: json, truncated: false };\n"],"mappings":";;;;;;;AAwCA,MAAa,4BACX,WACA,MACA,YACoB;CACpB,MAAM,CAAC,OAAO;CACd,MAAM,UACJ,cAAc,QAAQ,sBAAsB,KAAK,GAAG,GAAG,KAAA;CACzD,MAAM,OACJ,YAAY,UAAU,iBAAiB,QAAQ,OAAO,GAAG,KAAA;CAC3D,MAAM,UAAU,SAAS,WAAW,MAAM;CAC1C,MAAM,iBAAiB,gBAAgB,WAAW,KAAK;AAEvD,QAAO;EACL,GAAI,OAAO,QAAQ,WAAW,EAAE,KAAK,GAAG,EAAE;EAC1C,GAAI,mBAAmB,KAAA,IAAY,EAAE,MAAM,gBAAgB,GAAG,EAAE;EAChE,GAAI,SAAS,aAAa,KAAA,IAAY,EAAE,OAAO,QAAQ,UAAU,GAAG,EAAE;EACtE,GAAI,MAAM,aAAa,KAAA,IAAY,EAAE,QAAQ,KAAK,UAAU,GAAG,EAAE;EACjE,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;EAC7C;;;;;;;;;;;AAYH,MAAM,oBAAoB,UAAsC;CAC9D,IAAI;AACJ,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,QAAQ,MAAc,WAAoB;GACpE,MAAM,SAAS,cAAc,OAAO;AACpC,OAAI,WAAW,KAAA,GAAW;AACxB,gBAAY;AACZ,WAAO;;AAET,UAAO,kBAAkB,MACrB,OAAO,YAAY,OAA+B,GAClD;IACJ;AACF,SAAO;GAEL,GAAI,SAAS,KAAA,IAAY,EAAE,UAAU,QAAQ,KAAK,EAAE,GAAG,EAAE;GACzD,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;GAC7C;SACK;AAEN,SAAO,EAAE;;;;;;;;;AAUb,MAAM,yBAAyB,UAAsC;AACnE,KAAI,OAAO,UAAU,SACnB,KAAI;AACF,SAAO,EAAE,UAAU,QAAQ,KAAK,UAAU,kBAAkB,MAAM,CAAC,CAAC,EAAE;SAChE;AACN,SAAO,EAAE;;AAGb,QAAO,iBAAiB,MAAM;;;;;;;;;;AAWhC,MAAa,mBACX,WACA,SAC6B;AAK7B,QAAO,iBAHL,cAAc,QACV,KAAK,KAAK,KAAK,UAAW,UAAU,IAAI,OAAO,IAAK,GACpD,KAC+B,CAAC;;AAGxC,MAAM,iBAAiB,UAAgD;AAErE,KACE,OAAO,mBAAmB,eAC1B,iBAAiB,eAEjB,QAAO;AAET,KAAI,iBAAiB,eAAe,YAAY,OAAO,MAAM,CAC3D,QAAO;;AAKX,MAAM,WAAW,SACf,KAAK,SAAA,UACD;CAAE,MAAM,KAAK,MAAM,GAAG,sBAAsB;CAAE,WAAW;CAAM,GAC/D;CAAE,MAAM;CAAM,WAAW;CAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","names":[],"sources":["../../src/postgres/capture.ts"],"sourcesContent":["import { serializeCapturedResult } from \"../captured-result-codec\";\nimport { serializeCapturedError } from \"../error-capture\";\n\n/**\n * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node\n * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.\n *\n * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a\n * deployed Worker has to produce the byte-identical span a Node-recorded one does, because\n * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the\n * serialization subtly wrong on one surface and every replay of a Worker recording misses.\n * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here\n * and keeps the replay half (key building, result/error reconstruction) to itself — that half\n * needs a synchronous SHA-256 and never runs inside a Worker.\n */\n\n// Custom Meticulous span attributes carrying the query input + result. Kept distinct from the\n// `pg` attributes (`meticulous.pg.*`) so the two stores can never match each other's spans,\n// even though both talk to Postgres.\nexport const POSTGRES_JS_QUERY_TEXT_ATTR = \"meticulous.postgresjs.query.text\";\nexport const POSTGRES_JS_QUERY_PARAMS_ATTR =\n \"meticulous.postgresjs.query.params\";\nexport const POSTGRES_JS_ROW_MODE_ATTR = \"meticulous.postgresjs.row_mode\";\nexport const POSTGRES_JS_RESULT_ATTR = \"meticulous.postgresjs.result\";\nexport const POSTGRES_JS_RESULT_TRUNCATED_ATTR =\n \"meticulous.postgresjs.result.truncated\";\n// Set instead of `result` when the query rejected, so a query that legitimately errors (a\n// constraint violation, a missing relation) replays as the same error rather than as a \"no\n// recorded result\" miss.\nexport const POSTGRES_JS_ERROR_ATTR = \"meticulous.postgresjs.error\";\n\n// Cap the captured result to bound span size, matching the HTTP/undici/pg 256 KB cap.\nexport const MAX_POSTGRES_JS_RESULT_SIZE = 256 * 1024;\n\n/**\n * How the rows of a result are shaped: objects keyed by column name by default, or arrays under\n * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |\n * `true` | `\"values\"`).\n *\n * This belongs in the match key because the SAME sql text returns different row shapes\n * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits\n * the query directly for `db.execute`, so both shapes occur for identical text.\n */\nexport type PostgresJsRowMode = \"\" | \"raw\" | \"values\";\n\nexport const resolveRowMode = (isRaw: unknown): PostgresJsRowMode => {\n if (isRaw === \"values\") {\n return \"values\";\n }\n return isRaw ? \"raw\" : \"\";\n};\n\n/**\n * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the\n * interpolated values went.\n *\n * The SQL text does not exist at interception time: postgres.js builds `query.string` only once\n * a query reaches a connection, and replay never executes, so both sides must derive the text\n * from `query.strings` instead.\n */\nexport const reconstructQueryText = (strings: unknown): string | undefined => {\n if (!Array.isArray(strings) || strings.length === 0) {\n return undefined;\n }\n let text = \"\";\n for (let i = 0; i < strings.length; i++) {\n const chunk = strings[i];\n if (typeof chunk !== \"string\") {\n return undefined;\n }\n text += i === 0 ? chunk : `$${i}${chunk}`;\n }\n return text;\n};\n\n/**\n * Canonical serialization of the query's interpolated values.\n *\n * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not\n * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place\n * into wire-format strings — so it differs between the two sides.\n *\n * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /\n * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across\n * calls, which is all the match key needs.\n */\nexport const serializePostgresJsArgs = (args: unknown): string => {\n try {\n return JSON.stringify(Array.isArray(args) ? args : []);\n } catch {\n return \"[]\";\n }\n};\n\n/** The leading SQL keyword, for the span name only. */\nexport const postgresJsCommandOf = (text: string): string =>\n /^\\s*([a-z]+)/i.exec(text)?.[1]?.toLowerCase() ?? \"query\";\n\ninterface SerializedPostgresJsResult {\n rows: unknown[];\n count: number | null;\n command: string | null;\n columns: { name: string; type: number; table: number; number: number }[];\n statement: { string: string; types: number[]; name: string } | null;\n}\n\n/**\n * The metadata postgres.js hangs off a `Result`. Declared with `unknown` values so the\n * serializer below narrows every field rather than trusting the shape.\n */\ninterface PostgresJsResultLike {\n count?: unknown;\n command?: unknown;\n columns?: unknown;\n statement?: unknown;\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value != null && typeof value === \"object\"\n ? (value as Record<string, unknown>)\n : {};\n\nconst numberOrNull = (value: unknown): number | null =>\n typeof value === \"number\" ? value : null;\n\n/**\n * postgres.js resolves a query with a `Result` — an `Array` subclass whose metadata lives in\n * five non-enumerable own properties, so `JSON.stringify` would capture only the rows. Pull them\n * out explicitly.\n *\n * Two fields are dropped on purpose: each column's `parser` (a function, so it can't survive\n * JSON, and consumers never call it on an already-parsed row) and `state` (a live\n * per-connection `{pid, secret}` handle used only for query cancellation, which is meaningless\n * without a connection).\n */\nconst serializeOneResult = (res: unknown): SerializedPostgresJsResult => {\n const meta = res as PostgresJsResultLike | null | undefined;\n const statement = asRecord(meta?.statement);\n return {\n rows: Array.isArray(res) ? [...(res as unknown[])] : [],\n count: numberOrNull(meta?.count),\n command: typeof meta?.command === \"string\" ? meta.command : null,\n columns: Array.isArray(meta?.columns)\n ? meta.columns.map((column) => {\n const c = asRecord(column);\n return {\n name: String(c.name ?? \"\"),\n type: numberOrNull(c.type) ?? 0,\n table: numberOrNull(c.table) ?? 0,\n number: numberOrNull(c.number) ?? 0,\n };\n })\n : [],\n statement:\n meta?.statement != null\n ? {\n string: String(statement.string ?? \"\"),\n types: Array.isArray(statement.types)\n ? (statement.types as number[])\n : [],\n name: String(statement.name ?? \"\"),\n }\n : null,\n };\n};\n\n/**\n * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves\n * to) to a JSON string for storage as a span attribute. Row values go through the codec rather\n * than plain JSON, because postgres.js returns a `Date` for a `timestamptz` column and a\n * `Uint8Array` for `bytea`.\n */\nexport const serializePostgresJsResult = (result: unknown): string => {\n // A Result IS an Array, so an array-of-Results is only distinguishable by its elements. The\n // `length > 0` guard matters: `[].every(...)` is true, so without it every zero-row result (a\n // SELECT that matched nothing, an INSERT without RETURNING, BEGIN/COMMIT) would be mistaken\n // for an empty list of results and lose its `count`/`command`.\n const isMultiple =\n Array.isArray(result) &&\n result.length > 0 &&\n result.every((r) => isResultLike(r));\n const serialized = isMultiple\n ? (result as unknown[]).map(serializeOneResult)\n : serializeOneResult(result);\n return serializeCapturedResult(serialized);\n};\n\n// A `Result` carries `command`/`count` own properties; a plain row object or scalar does not.\nconst isResultLike = (value: unknown): boolean =>\n Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, \"command\") &&\n Object.prototype.hasOwnProperty.call(value, \"count\");\n\n// Own properties postgres.js attaches to a query error for debugging. Excluded from the\n// captured error: they are large, redundant with the query attributes we already record, and\n// only enumerable when the app enabled postgres.js' `debug` option.\nconst ERROR_DEBUG_PROPS = new Set([\n \"stack\",\n \"query\",\n \"parameters\",\n \"args\",\n \"types\",\n]);\n\n/**\n * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as\n * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields\n * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All\n * of those live in own enumerable properties, so copying those plus `message` (own but\n * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.\n */\nexport const serializePostgresJsError = (error: unknown): string =>\n serializeCapturedError(error, ERROR_DEBUG_PROPS);\n\n/**\n * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no\n * `postgres` dependency).\n */\nexport interface PostgresJsQueryLike {\n /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */\n strings: unknown;\n /** The interpolated values, still raw JS values at interception time. */\n args: unknown;\n /** `undefined` by default, `true` after `.raw()`, `\"values\"` after `.values()`. */\n isRaw?: unknown;\n /** Set once `handle()` has run; guards against double-instrumenting one query. */\n executed?: boolean;\n /** Set by `.describe()` — resolves with a Statement, not a Result. */\n onlyDescribe?: boolean;\n /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */\n cursorFn?: unknown;\n cursorRows?: unknown;\n forEachFn?: unknown;\n streaming?: unknown;\n options?: {\n /**\n * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it\n * called with the serving connection before the transaction body runs.\n */\n onexecute?: (connection: unknown) => void;\n [key: string]: unknown;\n };\n resolve: (result: unknown) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Whether a query resolves with a single `Result` we can capture and replay. Cursors,\n * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with\n * something else, and `sql.file()` is constructed with no template chunks at all (its text is\n * only read from disk later). All of those pass straight through, unrecorded and unmocked.\n */\nexport const isSupportedPostgresJsQuery = (\n query: PostgresJsQueryLike,\n): boolean =>\n reconstructQueryText(query.strings) != null &&\n !query.onlyDescribe &&\n !query.streaming &&\n query.cursorFn == null &&\n query.cursorRows == null &&\n query.forEachFn == null;\n"],"mappings":";;;;;;;;;;;;;;;AAmBA,MAAa,8BAA8B;AAC3C,MAAa,gCACX;AACF,MAAa,4BAA4B;AACzC,MAAa,0BAA0B;AACvC,MAAa,oCACX;AAIF,MAAa,yBAAyB;AAGtC,MAAa,8BAA8B,MAAM;AAajD,MAAa,kBAAkB,UAAsC;AACnE,KAAI,UAAU,SACZ,QAAO;AAET,QAAO,QAAQ,QAAQ;;;;;;;;;;AAWzB,MAAa,wBAAwB,YAAyC;AAC5E,KAAI,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,WAAW,EAChD;CAEF,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,QAAQ,QAAQ;AACtB,MAAI,OAAO,UAAU,SACnB;AAEF,UAAQ,MAAM,IAAI,QAAQ,IAAI,IAAI;;AAEpC,QAAO;;;;;;;;;;;;;AAcT,MAAa,2BAA2B,SAA0B;AAChE,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,EAAE,CAAC;SAChD;AACN,SAAO;;;;AAKX,MAAa,uBAAuB,SAClC,gBAAgB,KAAK,KAAK,GAAG,IAAI,aAAa,IAAI;AAqBpD,MAAM,YAAY,UAChB,SAAS,QAAQ,OAAO,UAAU,WAC7B,QACD,EAAE;AAER,MAAM,gBAAgB,UACpB,OAAO,UAAU,WAAW,QAAQ;;;;;;;;;;;AAYtC,MAAM,sBAAsB,QAA6C;CACvE,MAAM,OAAO;CACb,MAAM,YAAY,SAAS,MAAM,UAAU;AAC3C,QAAO;EACL,MAAM,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAI,IAAkB,GAAG,EAAE;EACvD,OAAO,aAAa,MAAM,MAAM;EAChC,SAAS,OAAO,MAAM,YAAY,WAAW,KAAK,UAAU;EAC5D,SAAS,MAAM,QAAQ,MAAM,QAAQ,GACjC,KAAK,QAAQ,KAAK,WAAW;GAC3B,MAAM,IAAI,SAAS,OAAO;AAC1B,UAAO;IACL,MAAM,OAAO,EAAE,QAAQ,GAAG;IAC1B,MAAM,aAAa,EAAE,KAAK,IAAI;IAC9B,OAAO,aAAa,EAAE,MAAM,IAAI;IAChC,QAAQ,aAAa,EAAE,OAAO,IAAI;IACnC;IACD,GACF,EAAE;EACN,WACE,MAAM,aAAa,OACf;GACE,QAAQ,OAAO,UAAU,UAAU,GAAG;GACtC,OAAO,MAAM,QAAQ,UAAU,MAAM,GAChC,UAAU,QACX,EAAE;GACN,MAAM,OAAO,UAAU,QAAQ,GAAG;GACnC,GACD;EACP;;;;;;;;AASH,MAAa,6BAA6B,WAA4B;AAYpE,QAAO,wBANL,MAAM,QAAQ,OAAO,IACrB,OAAO,SAAS,KAChB,OAAO,OAAO,MAAM,aAAa,EAAE,CAAC,GAEjC,OAAqB,IAAI,mBAAmB,GAC7C,mBAAmB,OAAO,CACY;;AAI5C,MAAM,gBAAgB,UACpB,MAAM,QAAQ,MAAM,IACpB,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IACtD,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ;AAKtD,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAa,4BAA4B,UACvC,uBAAuB,OAAO,kBAAkB;;;;;;;AAwClD,MAAa,8BACX,UAEA,qBAAqB,MAAM,QAAQ,IAAI,QACvC,CAAC,MAAM,gBACP,CAAC,MAAM,aACP,MAAM,YAAY,QAClB,MAAM,cAAc,QACpB,MAAM,aAAa"}
|
|
1
|
+
{"version":3,"file":"capture.js","names":[],"sources":["../../src/postgres/capture.ts"],"sourcesContent":["import { serializeCapturedResult } from \"../captured-result-codec\";\nimport { serializeCapturedError } from \"../error-capture\";\n\n/**\n * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node\n * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.\n *\n * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a\n * deployed Worker has to produce the byte-identical span a Node-recorded one does, because\n * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the\n * serialization subtly wrong on one surface and every replay of a Worker recording misses.\n * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here\n * and keeps the replay half (key building, result/error reconstruction) to itself — that half\n * needs a synchronous SHA-256 and never runs inside a Worker.\n */\n\n// Custom Meticulous span attributes carrying the query input + result. Kept distinct from the\n// `pg` attributes (`meticulous.pg.*`) so the two stores can never match each other's spans,\n// even though both talk to Postgres.\nexport const POSTGRES_JS_QUERY_TEXT_ATTR = \"meticulous.postgresjs.query.text\";\nexport const POSTGRES_JS_QUERY_PARAMS_ATTR =\n \"meticulous.postgresjs.query.params\";\nexport const POSTGRES_JS_ROW_MODE_ATTR = \"meticulous.postgresjs.row_mode\";\nexport const POSTGRES_JS_RESULT_ATTR = \"meticulous.postgresjs.result\";\nexport const POSTGRES_JS_RESULT_TRUNCATED_ATTR =\n \"meticulous.postgresjs.result.truncated\";\n// Set instead of `result` when the query rejected, so a query that legitimately errors (a\n// constraint violation, a missing relation) replays as the same error rather than as a \"no\n// recorded result\" miss.\nexport const POSTGRES_JS_ERROR_ATTR = \"meticulous.postgresjs.error\";\n\n// Cap the captured result to bound span size, matching the Node recorder's pg result cap.\n// Bodies are capped separately, at MAX_BODY_CAPTURE_SIZE.\nexport const MAX_POSTGRES_JS_RESULT_SIZE = 256 * 1024;\n\n/**\n * How the rows of a result are shaped: objects keyed by column name by default, or arrays under\n * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |\n * `true` | `\"values\"`).\n *\n * This belongs in the match key because the SAME sql text returns different row shapes\n * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits\n * the query directly for `db.execute`, so both shapes occur for identical text.\n */\nexport type PostgresJsRowMode = \"\" | \"raw\" | \"values\";\n\nexport const resolveRowMode = (isRaw: unknown): PostgresJsRowMode => {\n if (isRaw === \"values\") {\n return \"values\";\n }\n return isRaw ? \"raw\" : \"\";\n};\n\n/**\n * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the\n * interpolated values went.\n *\n * The SQL text does not exist at interception time: postgres.js builds `query.string` only once\n * a query reaches a connection, and replay never executes, so both sides must derive the text\n * from `query.strings` instead.\n */\nexport const reconstructQueryText = (strings: unknown): string | undefined => {\n if (!Array.isArray(strings) || strings.length === 0) {\n return undefined;\n }\n let text = \"\";\n for (let i = 0; i < strings.length; i++) {\n const chunk = strings[i];\n if (typeof chunk !== \"string\") {\n return undefined;\n }\n text += i === 0 ? chunk : `$${i}${chunk}`;\n }\n return text;\n};\n\n/**\n * Canonical serialization of the query's interpolated values.\n *\n * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not\n * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place\n * into wire-format strings — so it differs between the two sides.\n *\n * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /\n * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across\n * calls, which is all the match key needs.\n */\nexport const serializePostgresJsArgs = (args: unknown): string => {\n try {\n return JSON.stringify(Array.isArray(args) ? args : []);\n } catch {\n return \"[]\";\n }\n};\n\n/** The leading SQL keyword, for the span name only. */\nexport const postgresJsCommandOf = (text: string): string =>\n /^\\s*([a-z]+)/i.exec(text)?.[1]?.toLowerCase() ?? \"query\";\n\ninterface SerializedPostgresJsResult {\n rows: unknown[];\n count: number | null;\n command: string | null;\n columns: { name: string; type: number; table: number; number: number }[];\n statement: { string: string; types: number[]; name: string } | null;\n}\n\n/**\n * The metadata postgres.js hangs off a `Result`. Declared with `unknown` values so the\n * serializer below narrows every field rather than trusting the shape.\n */\ninterface PostgresJsResultLike {\n count?: unknown;\n command?: unknown;\n columns?: unknown;\n statement?: unknown;\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value != null && typeof value === \"object\"\n ? (value as Record<string, unknown>)\n : {};\n\nconst numberOrNull = (value: unknown): number | null =>\n typeof value === \"number\" ? value : null;\n\n/**\n * postgres.js resolves a query with a `Result` — an `Array` subclass whose metadata lives in\n * five non-enumerable own properties, so `JSON.stringify` would capture only the rows. Pull them\n * out explicitly.\n *\n * Two fields are dropped on purpose: each column's `parser` (a function, so it can't survive\n * JSON, and consumers never call it on an already-parsed row) and `state` (a live\n * per-connection `{pid, secret}` handle used only for query cancellation, which is meaningless\n * without a connection).\n */\nconst serializeOneResult = (res: unknown): SerializedPostgresJsResult => {\n const meta = res as PostgresJsResultLike | null | undefined;\n const statement = asRecord(meta?.statement);\n return {\n rows: Array.isArray(res) ? [...(res as unknown[])] : [],\n count: numberOrNull(meta?.count),\n command: typeof meta?.command === \"string\" ? meta.command : null,\n columns: Array.isArray(meta?.columns)\n ? meta.columns.map((column) => {\n const c = asRecord(column);\n return {\n name: String(c.name ?? \"\"),\n type: numberOrNull(c.type) ?? 0,\n table: numberOrNull(c.table) ?? 0,\n number: numberOrNull(c.number) ?? 0,\n };\n })\n : [],\n statement:\n meta?.statement != null\n ? {\n string: String(statement.string ?? \"\"),\n types: Array.isArray(statement.types)\n ? (statement.types as number[])\n : [],\n name: String(statement.name ?? \"\"),\n }\n : null,\n };\n};\n\n/**\n * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves\n * to) to a JSON string for storage as a span attribute. Row values go through the codec rather\n * than plain JSON, because postgres.js returns a `Date` for a `timestamptz` column and a\n * `Uint8Array` for `bytea`.\n */\nexport const serializePostgresJsResult = (result: unknown): string => {\n // A Result IS an Array, so an array-of-Results is only distinguishable by its elements. The\n // `length > 0` guard matters: `[].every(...)` is true, so without it every zero-row result (a\n // SELECT that matched nothing, an INSERT without RETURNING, BEGIN/COMMIT) would be mistaken\n // for an empty list of results and lose its `count`/`command`.\n const isMultiple =\n Array.isArray(result) &&\n result.length > 0 &&\n result.every((r) => isResultLike(r));\n const serialized = isMultiple\n ? (result as unknown[]).map(serializeOneResult)\n : serializeOneResult(result);\n return serializeCapturedResult(serialized);\n};\n\n// A `Result` carries `command`/`count` own properties; a plain row object or scalar does not.\nconst isResultLike = (value: unknown): boolean =>\n Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, \"command\") &&\n Object.prototype.hasOwnProperty.call(value, \"count\");\n\n// Own properties postgres.js attaches to a query error for debugging. Excluded from the\n// captured error: they are large, redundant with the query attributes we already record, and\n// only enumerable when the app enabled postgres.js' `debug` option.\nconst ERROR_DEBUG_PROPS = new Set([\n \"stack\",\n \"query\",\n \"parameters\",\n \"args\",\n \"types\",\n]);\n\n/**\n * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as\n * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields\n * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All\n * of those live in own enumerable properties, so copying those plus `message` (own but\n * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.\n */\nexport const serializePostgresJsError = (error: unknown): string =>\n serializeCapturedError(error, ERROR_DEBUG_PROPS);\n\n/**\n * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no\n * `postgres` dependency).\n */\nexport interface PostgresJsQueryLike {\n /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */\n strings: unknown;\n /** The interpolated values, still raw JS values at interception time. */\n args: unknown;\n /** `undefined` by default, `true` after `.raw()`, `\"values\"` after `.values()`. */\n isRaw?: unknown;\n /** Set once `handle()` has run; guards against double-instrumenting one query. */\n executed?: boolean;\n /** Set by `.describe()` — resolves with a Statement, not a Result. */\n onlyDescribe?: boolean;\n /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */\n cursorFn?: unknown;\n cursorRows?: unknown;\n forEachFn?: unknown;\n streaming?: unknown;\n options?: {\n /**\n * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it\n * called with the serving connection before the transaction body runs.\n */\n onexecute?: (connection: unknown) => void;\n [key: string]: unknown;\n };\n resolve: (result: unknown) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Whether a query resolves with a single `Result` we can capture and replay. Cursors,\n * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with\n * something else, and `sql.file()` is constructed with no template chunks at all (its text is\n * only read from disk later). All of those pass straight through, unrecorded and unmocked.\n */\nexport const isSupportedPostgresJsQuery = (\n query: PostgresJsQueryLike,\n): boolean =>\n reconstructQueryText(query.strings) != null &&\n !query.onlyDescribe &&\n !query.streaming &&\n query.cursorFn == null &&\n query.cursorRows == null &&\n query.forEachFn == null;\n"],"mappings":";;;;;;;;;;;;;;;AAmBA,MAAa,8BAA8B;AAC3C,MAAa,gCACX;AACF,MAAa,4BAA4B;AACzC,MAAa,0BAA0B;AACvC,MAAa,oCACX;AAIF,MAAa,yBAAyB;AAItC,MAAa,8BAA8B,MAAM;AAajD,MAAa,kBAAkB,UAAsC;AACnE,KAAI,UAAU,SACZ,QAAO;AAET,QAAO,QAAQ,QAAQ;;;;;;;;;;AAWzB,MAAa,wBAAwB,YAAyC;AAC5E,KAAI,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,WAAW,EAChD;CAEF,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,QAAQ,QAAQ;AACtB,MAAI,OAAO,UAAU,SACnB;AAEF,UAAQ,MAAM,IAAI,QAAQ,IAAI,IAAI;;AAEpC,QAAO;;;;;;;;;;;;;AAcT,MAAa,2BAA2B,SAA0B;AAChE,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,EAAE,CAAC;SAChD;AACN,SAAO;;;;AAKX,MAAa,uBAAuB,SAClC,gBAAgB,KAAK,KAAK,GAAG,IAAI,aAAa,IAAI;AAqBpD,MAAM,YAAY,UAChB,SAAS,QAAQ,OAAO,UAAU,WAC7B,QACD,EAAE;AAER,MAAM,gBAAgB,UACpB,OAAO,UAAU,WAAW,QAAQ;;;;;;;;;;;AAYtC,MAAM,sBAAsB,QAA6C;CACvE,MAAM,OAAO;CACb,MAAM,YAAY,SAAS,MAAM,UAAU;AAC3C,QAAO;EACL,MAAM,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAI,IAAkB,GAAG,EAAE;EACvD,OAAO,aAAa,MAAM,MAAM;EAChC,SAAS,OAAO,MAAM,YAAY,WAAW,KAAK,UAAU;EAC5D,SAAS,MAAM,QAAQ,MAAM,QAAQ,GACjC,KAAK,QAAQ,KAAK,WAAW;GAC3B,MAAM,IAAI,SAAS,OAAO;AAC1B,UAAO;IACL,MAAM,OAAO,EAAE,QAAQ,GAAG;IAC1B,MAAM,aAAa,EAAE,KAAK,IAAI;IAC9B,OAAO,aAAa,EAAE,MAAM,IAAI;IAChC,QAAQ,aAAa,EAAE,OAAO,IAAI;IACnC;IACD,GACF,EAAE;EACN,WACE,MAAM,aAAa,OACf;GACE,QAAQ,OAAO,UAAU,UAAU,GAAG;GACtC,OAAO,MAAM,QAAQ,UAAU,MAAM,GAChC,UAAU,QACX,EAAE;GACN,MAAM,OAAO,UAAU,QAAQ,GAAG;GACnC,GACD;EACP;;;;;;;;AASH,MAAa,6BAA6B,WAA4B;AAYpE,QAAO,wBANL,MAAM,QAAQ,OAAO,IACrB,OAAO,SAAS,KAChB,OAAO,OAAO,MAAM,aAAa,EAAE,CAAC,GAEjC,OAAqB,IAAI,mBAAmB,GAC7C,mBAAmB,OAAO,CACY;;AAI5C,MAAM,gBAAgB,UACpB,MAAM,QAAQ,MAAM,IACpB,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IACtD,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ;AAKtD,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAa,4BAA4B,UACvC,uBAAuB,OAAO,kBAAkB;;;;;;;AAwClD,MAAa,8BACX,UAEA,qBAAqB,MAAM,QAAQ,IAAI,QACvC,CAAC,MAAM,gBACP,CAAC,MAAM,aACP,MAAM,YAAY,QAClB,MAAM,cAAc,QACpB,MAAM,aAAa"}
|
package/dist/version.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Replaced by tsdown's `define`. The guard keeps a direct source import
|
|
9
9
|
* (vitest, typecheck) working, where no bundler has substituted the value.
|
|
10
10
|
*/
|
|
11
|
-
const WORKERD_SHIM_VERSION = "2.
|
|
11
|
+
const WORKERD_SHIM_VERSION = "2.343.0";
|
|
12
12
|
//#endregion
|
|
13
13
|
export { WORKERD_SHIM_VERSION };
|
|
14
14
|
|
package/dist/vite/index.js
CHANGED
|
@@ -6117,6 +6117,79 @@ var MagicString = class MagicString {
|
|
|
6117
6117
|
}
|
|
6118
6118
|
};
|
|
6119
6119
|
//#endregion
|
|
6120
|
+
//#region src/vite/annotations.ts
|
|
6121
|
+
/**
|
|
6122
|
+
* Keeps a build annotation attached to the node it was written for when the
|
|
6123
|
+
* instrumenter inserts code in front of that node.
|
|
6124
|
+
*
|
|
6125
|
+
* Bundlers read `/* @__PURE__ *\/` and its family off the comment immediately
|
|
6126
|
+
* preceding an expression. A marker inserted at the node's own start goes
|
|
6127
|
+
* *between* the two, leaving the annotation on the marker — a shape every
|
|
6128
|
+
* bundler reports as `INVALID_ANNOTATION` and then ignores, so a JSX-heavy
|
|
6129
|
+
* build both fills with warnings (esbuild's JSX transform annotates every
|
|
6130
|
+
* `jsx(...)` call, and the instrumenter runs after it) and silently loses the
|
|
6131
|
+
* hint. Opening the insertion before the annotation instead keeps it adjacent
|
|
6132
|
+
* to its own expression.
|
|
6133
|
+
*
|
|
6134
|
+
* Comment positions come from the parse rather than a backwards scan of the
|
|
6135
|
+
* text: `jsx("*\/", p)` puts a comment terminator inside a string literal, and
|
|
6136
|
+
* a scanner would read it as the end of a comment that never existed.
|
|
6137
|
+
*/
|
|
6138
|
+
/**
|
|
6139
|
+
* `@__PURE__`, `#__PURE__`, `@__NO_SIDE_EFFECTS__`, and whatever a bundler adds
|
|
6140
|
+
* next. Matching the family rather than a fixed list only risks moving an
|
|
6141
|
+
* insertion a few bytes earlier than it had to be, which is always valid.
|
|
6142
|
+
*/
|
|
6143
|
+
const ANNOTATION = /[@#]__[A-Z\d_]+__/;
|
|
6144
|
+
/** Whitespace between a comment and what it annotates. */
|
|
6145
|
+
const WHITESPACE = new Set([
|
|
6146
|
+
" ",
|
|
6147
|
+
" ",
|
|
6148
|
+
"\n",
|
|
6149
|
+
"\r",
|
|
6150
|
+
"\f",
|
|
6151
|
+
"\v"
|
|
6152
|
+
]);
|
|
6153
|
+
const createAnnotationIndex = (code) => {
|
|
6154
|
+
const commentsByEnd = /* @__PURE__ */ new Map();
|
|
6155
|
+
const annotationRunStart = (start, crossOpenParens) => {
|
|
6156
|
+
let index = start;
|
|
6157
|
+
let earliest = start;
|
|
6158
|
+
let sawAnnotation = false;
|
|
6159
|
+
for (;;) {
|
|
6160
|
+
index = skipBackwards(code, index, crossOpenParens);
|
|
6161
|
+
const comment = commentsByEnd.get(index);
|
|
6162
|
+
if (comment === void 0) break;
|
|
6163
|
+
sawAnnotation = sawAnnotation || comment.isAnnotation;
|
|
6164
|
+
index = comment.start;
|
|
6165
|
+
earliest = comment.start;
|
|
6166
|
+
}
|
|
6167
|
+
return sawAnnotation ? earliest : start;
|
|
6168
|
+
};
|
|
6169
|
+
return {
|
|
6170
|
+
record: (_isBlock, text, start, end) => {
|
|
6171
|
+
commentsByEnd.set(end, {
|
|
6172
|
+
start,
|
|
6173
|
+
isAnnotation: ANNOTATION.test(text)
|
|
6174
|
+
});
|
|
6175
|
+
},
|
|
6176
|
+
beforeStatement: (start) => annotationRunStart(start, false),
|
|
6177
|
+
beforeWrappedExpression: (start) => annotationRunStart(start, true)
|
|
6178
|
+
};
|
|
6179
|
+
};
|
|
6180
|
+
const skipBackwards = (code, index, crossOpenParens) => {
|
|
6181
|
+
let i = index;
|
|
6182
|
+
while (i > 0) {
|
|
6183
|
+
const char = code[i - 1];
|
|
6184
|
+
if (WHITESPACE.has(char) || crossOpenParens && char === "(") {
|
|
6185
|
+
i--;
|
|
6186
|
+
continue;
|
|
6187
|
+
}
|
|
6188
|
+
return i;
|
|
6189
|
+
}
|
|
6190
|
+
return i;
|
|
6191
|
+
};
|
|
6192
|
+
//#endregion
|
|
6120
6193
|
//#region src/vite/instrument.ts
|
|
6121
6194
|
/**
|
|
6122
6195
|
* Inserts per-line coverage markers into an ES module.
|
|
@@ -6184,6 +6257,7 @@ const SKIPPED_KEYS = new Set([
|
|
|
6184
6257
|
* worth breaking it for.
|
|
6185
6258
|
*/
|
|
6186
6259
|
const instrumentModule = ({ code, fileName, firstId, runtimeModuleId, resolveOriginalLine = (line) => line }) => {
|
|
6260
|
+
const annotations = createAnnotationIndex(code);
|
|
6187
6261
|
let program;
|
|
6188
6262
|
try {
|
|
6189
6263
|
program = parse(code, {
|
|
@@ -6192,7 +6266,8 @@ const instrumentModule = ({ code, fileName, firstId, runtimeModuleId, resolveOri
|
|
|
6192
6266
|
locations: true,
|
|
6193
6267
|
allowAwaitOutsideFunction: true,
|
|
6194
6268
|
allowReturnOutsideFunction: true,
|
|
6195
|
-
allowHashBang: true
|
|
6269
|
+
allowHashBang: true,
|
|
6270
|
+
onComment: annotations.record
|
|
6196
6271
|
});
|
|
6197
6272
|
} catch {
|
|
6198
6273
|
return null;
|
|
@@ -6213,7 +6288,13 @@ const instrumentModule = ({ code, fileName, firstId, runtimeModuleId, resolveOri
|
|
|
6213
6288
|
const markStatement = (node) => {
|
|
6214
6289
|
const id = nextId(node);
|
|
6215
6290
|
if (id === null) return;
|
|
6216
|
-
|
|
6291
|
+
const marker = `${SINK}&&(${SINK}[${id}]=1);`;
|
|
6292
|
+
const at = annotations.beforeStatement(node.start);
|
|
6293
|
+
if (at === node.start) {
|
|
6294
|
+
magic.appendLeft(at, marker);
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
magic.appendLeft(at, `;${marker}`);
|
|
6217
6298
|
};
|
|
6218
6299
|
const openFunctionBody = (body) => {
|
|
6219
6300
|
magic.appendLeft(body.start + 1, `const ${SINK}=typeof ${ENTER}==="function"?${ENTER}():undefined;`);
|
|
@@ -6221,7 +6302,7 @@ const instrumentModule = ({ code, fileName, firstId, runtimeModuleId, resolveOri
|
|
|
6221
6302
|
const markConciseArrowBody = (body) => {
|
|
6222
6303
|
const id = nextId(body);
|
|
6223
6304
|
if (id === null) return;
|
|
6224
|
-
magic.appendLeft(body.start, `(typeof ${HIT}==="function"&&${HIT}(${id}),`);
|
|
6305
|
+
magic.appendLeft(annotations.beforeWrappedExpression(body.start), `(typeof ${HIT}==="function"&&${HIT}(${id}),`);
|
|
6225
6306
|
magic.appendRight(body.end, ")");
|
|
6226
6307
|
};
|
|
6227
6308
|
visitProgram(program, {
|