@alwaysmeticulous/backend-recorder-workerd 2.336.0 → 2.342.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 CHANGED
@@ -107,7 +107,9 @@ A browser cannot put a custom header on a top-level navigation, and on the navig
107
107
 
108
108
  The shim closes that gap from the other end: for such a request it **mints the session id itself**, records the whole request under it, and publishes it to the page, which adopts it instead of minting its own. The entire page load then sits under one session id.
109
109
 
110
- **When it mints.** Only for what is plausibly a browser navigating to a page: a `GET`/`HEAD` with `Sec-Fetch-Dest: document` (or, absent that header, an `Accept` containing `text/html`) and no inbound `x-meticulous-session-id`. An in-page `fetch`, an RSC navigation, an iframe, a health check and a crawler all decline. It only ever happens while recording, so a deployed worker with no sidecar configured is untouched.
110
+ **When it mints.** Only for what is plausibly a browser loading a page: a `GET`/`HEAD` with `Sec-Fetch-Dest: document`, `iframe` or `frame` (or, absent that header, an `Accept` containing `text/html`) and no inbound `x-meticulous-session-id`. An in-page `fetch`, an RSC navigation, a health check and a crawler all decline. It only ever happens while recording, so a deployed worker with no sidecar configured is untouched.
111
+
112
+ A subframe is included because it does not reliably fold into the top frame's session: the frontend recorder forwards a subframe's data to the top frame and suppresses its own session only when the top frame is recording under the same token, so a cross-origin frame whose parent has no recorder is a session in its own right — and its render needs the id as much as a top-level one does. Where the subframe _does_ defer, nothing adopts the id, and an id nothing adopts costs nothing: minted ids carry `meticulous.session_id_origin: "backend"`, and ingestion counts one that no session adopted as unstamped rather than as a stamp.
111
113
 
112
114
  **How the page learns it.** Two channels, either sufficient:
113
115
 
@@ -0,0 +1,42 @@
1
+ //#region src/captured-result-codec.d.ts
2
+ /**
3
+ * JSON codec for the results a database client hands back, used by the Prisma, pg and
4
+ * postgres.js capture paths on BOTH sides so a replayed result carries the same JS types
5
+ * the driver produced at record time.
6
+ *
7
+ * Plain `JSON.stringify` loses those types silently. Every one of these clients returns a
8
+ * `Date` for a timestamp column; it becomes an ISO string in the recording, and the app's
9
+ * `row.createdAt.toISOString()` then throws during replay — while the mock is reported as a
10
+ * clean hit, because the store did find the right recording. A `BigInt` is worse: stringify
11
+ * throws, the record side catches, and the operation is never captured at all.
12
+ *
13
+ * So a value JSON cannot represent is replaced by a tagged wrapper
14
+ * (`{ __meticulousType, value }`) and rebuilt on the way out, and the payload is wrapped once
15
+ * more in an envelope carrying the encoding version. That envelope is also what marks a
16
+ * recording as one this codec wrote: a recording made before it has none, and its timestamps
17
+ * are recovered by shape instead — see {@link reviveLegacyTimestamps}.
18
+ *
19
+ * Lives here rather than in `backend-recorder-js` because a deployed Cloudflare Worker records
20
+ * postgres.js and loses the same types, and a Worker cannot load that package — so the dependency
21
+ * runs this way round, as it already does for `serializeCapturedError` and `serializeKvCaptureFields`.
22
+ * Nothing loads this package at runtime on the Node path: it is bundled into the recorder the
23
+ * launcher fetches.
24
+ *
25
+ * Still unhandled: a `Prisma.Decimal` (`decimal.js`) replays as the string its `toJSON` produced,
26
+ * because rebuilding one needs the class and replay has no `@prisma/client` to reach it through.
27
+ */
28
+ /**
29
+ * Serializes a captured result to a JSON string, preserving the types JSON has no
30
+ * representation for. Throws on a cyclic value, exactly as `JSON.stringify` does — the record
31
+ * side catches that and captures nothing.
32
+ */
33
+ declare const serializeCapturedResult: (result: unknown) => string;
34
+ /**
35
+ * Reconstructs a captured result, rebuilding the tagged values as the types they were
36
+ * recorded from. A payload without the version envelope was written before this codec
37
+ * existed, and falls back to recovering timestamps by shape.
38
+ */
39
+ declare const deserializeCapturedResult: (json: string) => unknown;
40
+ //#endregion
41
+ export { deserializeCapturedResult, serializeCapturedResult };
42
+ //# sourceMappingURL=captured-result-codec.d.ts.map
@@ -0,0 +1,159 @@
1
+ //#region src/captured-result-codec.ts
2
+ /**
3
+ * JSON codec for the results a database client hands back, used by the Prisma, pg and
4
+ * postgres.js capture paths on BOTH sides so a replayed result carries the same JS types
5
+ * the driver produced at record time.
6
+ *
7
+ * Plain `JSON.stringify` loses those types silently. Every one of these clients returns a
8
+ * `Date` for a timestamp column; it becomes an ISO string in the recording, and the app's
9
+ * `row.createdAt.toISOString()` then throws during replay — while the mock is reported as a
10
+ * clean hit, because the store did find the right recording. A `BigInt` is worse: stringify
11
+ * throws, the record side catches, and the operation is never captured at all.
12
+ *
13
+ * So a value JSON cannot represent is replaced by a tagged wrapper
14
+ * (`{ __meticulousType, value }`) and rebuilt on the way out, and the payload is wrapped once
15
+ * more in an envelope carrying the encoding version. That envelope is also what marks a
16
+ * recording as one this codec wrote: a recording made before it has none, and its timestamps
17
+ * are recovered by shape instead — see {@link reviveLegacyTimestamps}.
18
+ *
19
+ * Lives here rather than in `backend-recorder-js` because a deployed Cloudflare Worker records
20
+ * postgres.js and loses the same types, and a Worker cannot load that package — so the dependency
21
+ * runs this way round, as it already does for `serializeCapturedError` and `serializeKvCaptureFields`.
22
+ * Nothing loads this package at runtime on the Node path: it is bundled into the recorder the
23
+ * launcher fetches.
24
+ *
25
+ * Still unhandled: a `Prisma.Decimal` (`decimal.js`) replays as the string its `toJSON` produced,
26
+ * because rebuilding one needs the class and replay has no `@prisma/client` to reach it through.
27
+ */
28
+ const ENCODING_VERSION = 1;
29
+ const ENVELOPE_VERSION_KEY = "__meticulousEncoding";
30
+ const TYPE_TAG_KEY = "__meticulousType";
31
+ const VALUE_KEY = "value";
32
+ /**
33
+ * Serializes a captured result to a JSON string, preserving the types JSON has no
34
+ * representation for. Throws on a cyclic value, exactly as `JSON.stringify` does — the record
35
+ * side catches that and captures nothing.
36
+ */
37
+ const serializeCapturedResult = (result) => JSON.stringify({
38
+ [ENVELOPE_VERSION_KEY]: ENCODING_VERSION,
39
+ [VALUE_KEY]: result
40
+ }, tagUnrepresentableValues);
41
+ /**
42
+ * Reconstructs a captured result, rebuilding the tagged values as the types they were
43
+ * recorded from. A payload without the version envelope was written before this codec
44
+ * existed, and falls back to recovering timestamps by shape.
45
+ */
46
+ const deserializeCapturedResult = (json) => {
47
+ const parsed = JSON.parse(json, rebuildTaggedValue);
48
+ if (isEncodedEnvelope(parsed)) return parsed[VALUE_KEY];
49
+ return reviveLegacyTimestamps(parsed);
50
+ };
51
+ /**
52
+ * `JSON.stringify` replacer. It has to be a `function` rather than an arrow: `value` has
53
+ * already been through the holder's `toJSON()` by the time a replacer sees it (a `Date`
54
+ * arrives as a string, a `Buffer` as `{ type, data }`), so the only way to see what the
55
+ * driver actually returned is `this[key]` on the bound holder.
56
+ */
57
+ function tagUnrepresentableValues(key, value) {
58
+ const raw = this[key];
59
+ if (typeof raw === "bigint") return tagged("BigInt", raw.toString());
60
+ if (typeof raw === "number" && !Number.isFinite(raw)) return tagged("NonFiniteNumber", String(raw));
61
+ if (raw instanceof Date) return tagged("Date", Number.isNaN(raw.getTime()) ? null : raw.toISOString());
62
+ if (ArrayBuffer.isView(raw) && !(raw instanceof DataView)) return tagged("Binary", toBase64(raw));
63
+ if (raw instanceof ArrayBuffer) return tagged("Binary", toBase64(new Uint8Array(raw)));
64
+ return value;
65
+ }
66
+ const tagged = (type, value) => ({
67
+ [TYPE_TAG_KEY]: type,
68
+ [VALUE_KEY]: value
69
+ });
70
+ /**
71
+ * `JSON.parse` reviver. A tag is only honoured when its payload is one this codec could have
72
+ * written, so an app value that happens to be shaped like a tag — and a tag type a later
73
+ * recorder introduced — is passed through untouched rather than decoded into something else.
74
+ * (The shim has no runtime dependencies, so `assertNever` is not available for the switch.)
75
+ */
76
+ const rebuildTaggedValue = (_key, value) => {
77
+ if (!isTaggedValue(value)) return value;
78
+ const encoded = value[VALUE_KEY];
79
+ switch (value[TYPE_TAG_KEY]) {
80
+ case "Date": return decodeDate(encoded) ?? value;
81
+ case "BigInt": return typeof encoded === "string" && INTEGER.test(encoded) ? BigInt(encoded) : value;
82
+ case "NonFiniteNumber": return typeof encoded === "string" && NON_FINITE_NUMBERS.has(encoded) ? Number(encoded) : value;
83
+ case "Binary": return typeof encoded === "string" && BASE64.test(encoded) ? fromBase64(encoded) : value;
84
+ }
85
+ };
86
+ const INTEGER = /^-?\d+$/;
87
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
88
+ const NON_FINITE_NUMBERS = new Set([
89
+ "NaN",
90
+ "Infinity",
91
+ "-Infinity"
92
+ ]);
93
+ const decodeDate = (encoded) => {
94
+ if (encoded == null) return /* @__PURE__ */ new Date(NaN);
95
+ const date = new Date(encoded);
96
+ if (Number.isNaN(date.getTime()) || date.toISOString() !== encoded) return;
97
+ return date;
98
+ };
99
+ const TYPE_TAGS = new Set([
100
+ "Date",
101
+ "BigInt",
102
+ "Binary",
103
+ "NonFiniteNumber"
104
+ ]);
105
+ const isTaggedValue = (value) => {
106
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
107
+ const keys = Object.keys(value);
108
+ if (keys.length !== 2 || !keys.includes(VALUE_KEY)) return false;
109
+ const tag = value[TYPE_TAG_KEY];
110
+ const encoded = value[VALUE_KEY];
111
+ return typeof tag === "string" && TYPE_TAGS.has(tag) && (typeof encoded === "string" || encoded === null);
112
+ };
113
+ const isEncodedEnvelope = (value) => {
114
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
115
+ return value[ENVELOPE_VERSION_KEY] === ENCODING_VERSION;
116
+ };
117
+ /**
118
+ * The compatibility path for every recording made before this codec: the driver's `Date`s
119
+ * are already flattened to strings there, and nothing in the payload says which strings they
120
+ * were. Recovering them by shape is a guess, but a tightly bounded one — only the exact
121
+ * 24-character form `Date.prototype.toJSON` emits, and only when it round-trips back to
122
+ * itself, so a string column can collide with it solely by holding a real UTC millisecond
123
+ * timestamp. Left as a guess rather than declined outright because the alternative is every
124
+ * date-formatting call in the app throwing.
125
+ *
126
+ * Mutates in place: the value has just come out of `JSON.parse` and is owned by the caller.
127
+ */
128
+ const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
129
+ const reviveLegacyTimestamps = (value) => {
130
+ if (typeof value === "string") return asLegacyTimestamp(value) ?? value;
131
+ if (value == null || typeof value !== "object") return value;
132
+ if (Array.isArray(value)) {
133
+ for (let i = 0; i < value.length; i++) value[i] = reviveLegacyTimestamps(value[i]);
134
+ return value;
135
+ }
136
+ const record = value;
137
+ for (const key of Object.keys(record)) record[key] = reviveLegacyTimestamps(record[key]);
138
+ return record;
139
+ };
140
+ const asLegacyTimestamp = (value) => ISO_TIMESTAMP.test(value) ? decodeDate(value) : void 0;
141
+ const BASE64_CHUNK_SIZE = 32768;
142
+ const toBase64 = (view) => {
143
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
144
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
145
+ let binary = "";
146
+ for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));
147
+ return btoa(binary);
148
+ };
149
+ const fromBase64 = (base64) => {
150
+ if (typeof Buffer !== "undefined") return Buffer.from(base64, "base64");
151
+ const binary = atob(base64);
152
+ const bytes = new Uint8Array(binary.length);
153
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
154
+ return bytes;
155
+ };
156
+ //#endregion
157
+ export { deserializeCapturedResult, serializeCapturedResult };
158
+
159
+ //# sourceMappingURL=captured-result-codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captured-result-codec.js","names":[],"sources":["../src/captured-result-codec.ts"],"sourcesContent":["/**\n * JSON codec for the results a database client hands back, used by the Prisma, pg and\n * postgres.js capture paths on BOTH sides so a replayed result carries the same JS types\n * the driver produced at record time.\n *\n * Plain `JSON.stringify` loses those types silently. Every one of these clients returns a\n * `Date` for a timestamp column; it becomes an ISO string in the recording, and the app's\n * `row.createdAt.toISOString()` then throws during replay — while the mock is reported as a\n * clean hit, because the store did find the right recording. A `BigInt` is worse: stringify\n * throws, the record side catches, and the operation is never captured at all.\n *\n * So a value JSON cannot represent is replaced by a tagged wrapper\n * (`{ __meticulousType, value }`) and rebuilt on the way out, and the payload is wrapped once\n * more in an envelope carrying the encoding version. That envelope is also what marks a\n * recording as one this codec wrote: a recording made before it has none, and its timestamps\n * are recovered by shape instead — see {@link reviveLegacyTimestamps}.\n *\n * Lives here rather than in `backend-recorder-js` because a deployed Cloudflare Worker records\n * postgres.js and loses the same types, and a Worker cannot load that package — so the dependency\n * runs this way round, as it already does for `serializeCapturedError` and `serializeKvCaptureFields`.\n * Nothing loads this package at runtime on the Node path: it is bundled into the recorder the\n * launcher fetches.\n *\n * Still unhandled: a `Prisma.Decimal` (`decimal.js`) replays as the string its `toJSON` produced,\n * because rebuilding one needs the class and replay has no `@prisma/client` to reach it through.\n */\n\nconst ENCODING_VERSION = 1;\nconst ENVELOPE_VERSION_KEY = \"__meticulousEncoding\";\nconst TYPE_TAG_KEY = \"__meticulousType\";\n// Both the envelope and a tag carry their payload under this key.\nconst VALUE_KEY = \"value\";\n\ntype TypeTag = \"Date\" | \"BigInt\" | \"Binary\" | \"NonFiniteNumber\";\n\ninterface TaggedValue {\n [TYPE_TAG_KEY]: TypeTag;\n [VALUE_KEY]: string | null;\n}\n\n/**\n * Serializes a captured result to a JSON string, preserving the types JSON has no\n * representation for. Throws on a cyclic value, exactly as `JSON.stringify` does — the record\n * side catches that and captures nothing.\n */\nexport const serializeCapturedResult = (result: unknown): string =>\n JSON.stringify(\n { [ENVELOPE_VERSION_KEY]: ENCODING_VERSION, [VALUE_KEY]: result },\n tagUnrepresentableValues,\n );\n\n/**\n * Reconstructs a captured result, rebuilding the tagged values as the types they were\n * recorded from. A payload without the version envelope was written before this codec\n * existed, and falls back to recovering timestamps by shape.\n */\nexport const deserializeCapturedResult = (json: string): unknown => {\n const parsed: unknown = JSON.parse(json, rebuildTaggedValue);\n if (isEncodedEnvelope(parsed)) {\n return parsed[VALUE_KEY];\n }\n return reviveLegacyTimestamps(parsed);\n};\n\n/**\n * `JSON.stringify` replacer. It has to be a `function` rather than an arrow: `value` has\n * already been through the holder's `toJSON()` by the time a replacer sees it (a `Date`\n * arrives as a string, a `Buffer` as `{ type, data }`), so the only way to see what the\n * driver actually returned is `this[key]` on the bound holder.\n */\nfunction tagUnrepresentableValues(this: unknown, key: string, value: unknown) {\n const raw = (this as Record<string, unknown>)[key];\n if (typeof raw === \"bigint\") {\n return tagged(\"BigInt\", raw.toString());\n }\n if (typeof raw === \"number\" && !Number.isFinite(raw)) {\n return tagged(\"NonFiniteNumber\", String(raw));\n }\n if (raw instanceof Date) {\n return tagged(\n \"Date\",\n Number.isNaN(raw.getTime()) ? null : raw.toISOString(),\n );\n }\n // A DataView is left alone: no driver returns one, and it would come back a Uint8Array.\n if (ArrayBuffer.isView(raw) && !(raw instanceof DataView)) {\n return tagged(\"Binary\", toBase64(raw));\n }\n if (raw instanceof ArrayBuffer) {\n return tagged(\"Binary\", toBase64(new Uint8Array(raw)));\n }\n return value;\n}\n\nconst tagged = (type: TypeTag, value: string | null): TaggedValue => ({\n [TYPE_TAG_KEY]: type,\n [VALUE_KEY]: value,\n});\n\n/**\n * `JSON.parse` reviver. A tag is only honoured when its payload is one this codec could have\n * written, so an app value that happens to be shaped like a tag — and a tag type a later\n * recorder introduced — is passed through untouched rather than decoded into something else.\n * (The shim has no runtime dependencies, so `assertNever` is not available for the switch.)\n */\nconst rebuildTaggedValue = (_key: string, value: unknown): unknown => {\n if (!isTaggedValue(value)) {\n return value;\n }\n const encoded = value[VALUE_KEY];\n switch (value[TYPE_TAG_KEY]) {\n case \"Date\":\n return decodeDate(encoded) ?? value;\n case \"BigInt\":\n return typeof encoded === \"string\" && INTEGER.test(encoded)\n ? BigInt(encoded)\n : value;\n case \"NonFiniteNumber\":\n return typeof encoded === \"string\" && NON_FINITE_NUMBERS.has(encoded)\n ? Number(encoded)\n : value;\n case \"Binary\":\n return typeof encoded === \"string\" && BASE64.test(encoded)\n ? fromBase64(encoded)\n : value;\n }\n};\n\nconst INTEGER = /^-?\\d+$/;\nconst BASE64 =\n /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst NON_FINITE_NUMBERS: ReadonlySet<string> = new Set([\n \"NaN\",\n \"Infinity\",\n \"-Infinity\",\n]);\n\n// `null` is how an invalid Date was recorded; anything else has to be the exact string\n// `toISOString` would have produced for it.\nconst decodeDate = (encoded: string | null): Date | undefined => {\n if (encoded == null) {\n return new Date(NaN);\n }\n const date = new Date(encoded);\n if (Number.isNaN(date.getTime()) || date.toISOString() !== encoded) {\n return undefined;\n }\n return date;\n};\n\nconst TYPE_TAGS: ReadonlySet<string> = new Set<TypeTag>([\n \"Date\",\n \"BigInt\",\n \"Binary\",\n \"NonFiniteNumber\",\n]);\n\nconst isTaggedValue = (value: unknown): value is TaggedValue => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const keys = Object.keys(value);\n if (keys.length !== 2 || !keys.includes(VALUE_KEY)) {\n return false;\n }\n const tag = (value as Record<string, unknown>)[TYPE_TAG_KEY];\n const encoded = (value as Record<string, unknown>)[VALUE_KEY];\n return (\n typeof tag === \"string\" &&\n TYPE_TAGS.has(tag) &&\n (typeof encoded === \"string\" || encoded === null)\n );\n};\n\nconst isEncodedEnvelope = (\n value: unknown,\n): value is Record<string, unknown> => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n return (\n (value as Record<string, unknown>)[ENVELOPE_VERSION_KEY] ===\n ENCODING_VERSION\n );\n};\n\n/**\n * The compatibility path for every recording made before this codec: the driver's `Date`s\n * are already flattened to strings there, and nothing in the payload says which strings they\n * were. Recovering them by shape is a guess, but a tightly bounded one — only the exact\n * 24-character form `Date.prototype.toJSON` emits, and only when it round-trips back to\n * itself, so a string column can collide with it solely by holding a real UTC millisecond\n * timestamp. Left as a guess rather than declined outright because the alternative is every\n * date-formatting call in the app throwing.\n *\n * Mutates in place: the value has just come out of `JSON.parse` and is owned by the caller.\n */\nconst ISO_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\nconst reviveLegacyTimestamps = (value: unknown): unknown => {\n if (typeof value === \"string\") {\n return asLegacyTimestamp(value) ?? value;\n }\n if (value == null || typeof value !== \"object\") {\n return value;\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n value[i] = reviveLegacyTimestamps(value[i]);\n }\n return value;\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n record[key] = reviveLegacyTimestamps(record[key]);\n }\n return record;\n};\n\nconst asLegacyTimestamp = (value: string): Date | undefined =>\n ISO_TIMESTAMP.test(value) ? decodeDate(value) : undefined;\n\nconst BASE64_CHUNK_SIZE = 0x8000;\n\nconst toBase64 = (view: ArrayBufferView): string => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) {\n binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));\n }\n return btoa(binary);\n};\n\n// Replay only ever runs on Node, so a `Buffer` is both what pg returned at record time and a\n// `Uint8Array` as far as a Prisma `Bytes` consumer is concerned.\nconst fromBase64 = (base64: string): Uint8Array => {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(base64, \"base64\");\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,eAAe;AAErB,MAAM,YAAY;;;;;;AAclB,MAAa,2BAA2B,WACtC,KAAK,UACH;EAAG,uBAAuB;EAAmB,YAAY;CAAQ,EACjE,yBACD;;;;;;AAOH,MAAa,6BAA6B,SAA0B;CAClE,MAAM,SAAkB,KAAK,MAAM,MAAM,mBAAmB;AAC5D,KAAI,kBAAkB,OAAO,CAC3B,QAAO,OAAO;AAEhB,QAAO,uBAAuB,OAAO;;;;;;;;AASvC,SAAS,yBAAwC,KAAa,OAAgB;CAC5E,MAAM,MAAO,KAAiC;AAC9C,KAAI,OAAO,QAAQ,SACjB,QAAO,OAAO,UAAU,IAAI,UAAU,CAAC;AAEzC,KAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,IAAI,CAClD,QAAO,OAAO,mBAAmB,OAAO,IAAI,CAAC;AAE/C,KAAI,eAAe,KACjB,QAAO,OACL,QACA,OAAO,MAAM,IAAI,SAAS,CAAC,GAAG,OAAO,IAAI,aAAa,CACvD;AAGH,KAAI,YAAY,OAAO,IAAI,IAAI,EAAE,eAAe,UAC9C,QAAO,OAAO,UAAU,SAAS,IAAI,CAAC;AAExC,KAAI,eAAe,YACjB,QAAO,OAAO,UAAU,SAAS,IAAI,WAAW,IAAI,CAAC,CAAC;AAExD,QAAO;;AAGT,MAAM,UAAU,MAAe,WAAuC;EACnE,eAAe;EACf,YAAY;CACd;;;;;;;AAQD,MAAM,sBAAsB,MAAc,UAA4B;AACpE,KAAI,CAAC,cAAc,MAAM,CACvB,QAAO;CAET,MAAM,UAAU,MAAM;AACtB,SAAQ,MAAM,eAAd;EACE,KAAK,OACH,QAAO,WAAW,QAAQ,IAAI;EAChC,KAAK,SACH,QAAO,OAAO,YAAY,YAAY,QAAQ,KAAK,QAAQ,GACvD,OAAO,QAAQ,GACf;EACN,KAAK,kBACH,QAAO,OAAO,YAAY,YAAY,mBAAmB,IAAI,QAAQ,GACjE,OAAO,QAAQ,GACf;EACN,KAAK,SACH,QAAO,OAAO,YAAY,YAAY,OAAO,KAAK,QAAQ,GACtD,WAAW,QAAQ,GACnB;;;AAIV,MAAM,UAAU;AAChB,MAAM,SACJ;AACF,MAAM,qBAA0C,IAAI,IAAI;CACtD;CACA;CACA;CACD,CAAC;AAIF,MAAM,cAAc,YAA6C;AAC/D,KAAI,WAAW,KACb,wBAAO,IAAI,KAAK,IAAI;CAEtB,MAAM,OAAO,IAAI,KAAK,QAAQ;AAC9B,KAAI,OAAO,MAAM,KAAK,SAAS,CAAC,IAAI,KAAK,aAAa,KAAK,QACzD;AAEF,QAAO;;AAGT,MAAM,YAAiC,IAAI,IAAa;CACtD;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,iBAAiB,UAAyC;AAC9D,KAAI,SAAS,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACpE,QAAO;CAET,MAAM,OAAO,OAAO,KAAK,MAAM;AAC/B,KAAI,KAAK,WAAW,KAAK,CAAC,KAAK,SAAS,UAAU,CAChD,QAAO;CAET,MAAM,MAAO,MAAkC;CAC/C,MAAM,UAAW,MAAkC;AACnD,QACE,OAAO,QAAQ,YACf,UAAU,IAAI,IAAI,KACjB,OAAO,YAAY,YAAY,YAAY;;AAIhD,MAAM,qBACJ,UACqC;AACrC,KAAI,SAAS,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACpE,QAAO;AAET,QACG,MAAkC,0BACnC;;;;;;;;;;;;;AAeJ,MAAM,gBAAgB;AAEtB,MAAM,0BAA0B,UAA4B;AAC1D,KAAI,OAAO,UAAU,SACnB,QAAO,kBAAkB,MAAM,IAAI;AAErC,KAAI,SAAS,QAAQ,OAAO,UAAU,SACpC,QAAO;AAET,KAAI,MAAM,QAAQ,MAAM,EAAE;AACxB,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,OAAM,KAAK,uBAAuB,MAAM,GAAG;AAE7C,SAAO;;CAET,MAAM,SAAS;AACf,MAAK,MAAM,OAAO,OAAO,KAAK,OAAO,CACnC,QAAO,OAAO,uBAAuB,OAAO,KAAK;AAEnD,QAAO;;AAGT,MAAM,qBAAqB,UACzB,cAAc,KAAK,MAAM,GAAG,WAAW,MAAM,GAAG,KAAA;AAElD,MAAM,oBAAoB;AAE1B,MAAM,YAAY,SAAkC;CAClD,MAAM,QAAQ,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,WAAW;AAC3E,KAAI,OAAO,WAAW,YACpB,QAAO,OAAO,KAAK,MAAM,CAAC,SAAS,SAAS;CAE9C,IAAI,SAAS;AACb,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,kBACrC,WAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC;AAE5E,QAAO,KAAK,OAAO;;AAKrB,MAAM,cAAc,WAA+B;AACjD,KAAI,OAAO,WAAW,YACpB,QAAO,OAAO,KAAK,QAAQ,SAAS;CAEtC,MAAM,SAAS,KAAK,OAAO;CAC3B,MAAM,QAAQ,IAAI,WAAW,OAAO,OAAO;AAC3C,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,IACjC,OAAM,KAAK,OAAO,WAAW,EAAE;AAEjC,QAAO"}
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap,
14
14
  import { MeticulousPagesFunction, MeticulousPagesFunctionContext, withMeticulousPagesFunction } from "./pages-function.js";
15
15
  import { KvCaptureFields, KvCaptureOutcome, serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
16
16
  import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
17
+ import { deserializeCapturedResult, serializeCapturedResult } from "./captured-result-codec.js";
17
18
  import { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, PostgresJsQueryLike, PostgresJsRowMode, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./postgres/capture.js";
18
19
  import { withMeticulousPostgres } from "./postgres/with-meticulous-postgres.js";
19
20
  import { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR } from "./spans/attributes.js";
@@ -88,5 +89,5 @@ declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler
88
89
  */
89
90
  declare const getMeticulousSessionId: () => string | undefined;
90
91
  //#endregion
91
- export { type BindingRequestEvent, CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, type CaptureEvent, type CaptureEventsPayload, type CapturedBody, type CoverageMap, type CoverageMapFile, type CoverageReportRequest, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, type HeaderReader, HealthProbeEventFilter, type HrTime, type InboundRequestEvent, type KvCaptureFields, type KvCaptureOutcome, type KvOmittedReason, type KvOperation, type KvOperationEvent, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, type MeticulousExecutionContext, type MeticulousPagesFunction, type MeticulousPagesFunctionContext, MeticulousWorkerHandler, type OutboundCallTarget, type OutboundFetchLookupRequest, type OutboundFetchLookupResponse, type OutboundRequestEvent, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, type PostgresJsQueryLike, type PostgresJsRowMode, type PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, type ReadableSpan, type ReplaySessionInfoResponse, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, type SerializedSpanBase, type SidecarFetcher, type SidecarTransport, SpanBuilder, type SpanStatus, VIRTUAL_TIME_HEADER, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, type WithMeticulousOptions, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isHealthProbeRequest, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
92
+ export { type BindingRequestEvent, CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, type CaptureEvent, type CaptureEventsPayload, type CapturedBody, type CoverageMap, type CoverageMapFile, type CoverageReportRequest, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, type HeaderReader, HealthProbeEventFilter, type HrTime, type InboundRequestEvent, type KvCaptureFields, type KvCaptureOutcome, type KvOmittedReason, type KvOperation, type KvOperationEvent, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, type MeticulousExecutionContext, type MeticulousPagesFunction, type MeticulousPagesFunctionContext, MeticulousWorkerHandler, type OutboundCallTarget, type OutboundFetchLookupRequest, type OutboundFetchLookupResponse, type OutboundRequestEvent, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, type PostgresJsQueryLike, type PostgresJsRowMode, type PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, type ReadableSpan, type ReplaySessionInfoResponse, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, type SerializedSpanBase, type SidecarFetcher, type SidecarTransport, SpanBuilder, type SpanStatus, VIRTUAL_TIME_HEADER, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, type WithMeticulousOptions, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, deserializeCapturedResult, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isHealthProbeRequest, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeCapturedResult, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
92
93
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { isHealthProbeRequest } from "./health-probe.js";
10
10
  import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
11
11
  import { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
12
12
  import { WORKERD_SHIM_VERSION } from "./version.js";
13
+ import { deserializeCapturedResult, serializeCapturedResult } from "./captured-result-codec.js";
13
14
  import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
14
15
  import { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./postgres/capture.js";
15
16
  import { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR } from "./spans/attributes.js";
@@ -94,6 +95,6 @@ const withMeticulous = (handler, options) => {
94
95
  */
95
96
  const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
96
97
  //#endregion
97
- export { CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, HealthProbeEventFilter, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, SpanBuilder, VIRTUAL_TIME_HEADER, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isHealthProbeRequest, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
98
+ export { CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, HealthProbeEventFilter, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, SpanBuilder, VIRTUAL_TIME_HEADER, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, deserializeCapturedResult, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isHealthProbeRequest, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeCapturedResult, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
98
99
 
99
100
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport {\n type MeticulousExecutionContext,\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\nexport type {\n BindingRequestEvent,\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n CoverageReportRequest,\n InboundRequestEvent,\n KvOmittedReason,\n KvOperation,\n KvOperationEvent,\n OutboundFetchLookupRequest,\n OutboundFetchLookupResponse,\n OutboundRequestEvent,\n PostgresQueryEvent,\n ReplaySessionInfoResponse,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n METICULOUS_PASSTHROUGH_HEADER,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n VIRTUAL_TIME_HEADER,\n WORKERD_SHIM_VERSION_HEADER,\n} from \"./protocol\";\nexport { WORKERD_SHIM_VERSION } from \"./version\";\nexport {\n MAX_BODY_CAPTURE_SIZE,\n readBodyWithCap,\n readRequestBodyWithCap,\n} from \"./body-capture\";\nexport { headersToRecord } from \"./outbound-capture\";\n/**\n * Exported for the Node recorder, whose `health-probe.ts` adapts this to\n * `http.IncomingMessage`. Which requests count as a health probe is a rule both surfaces\n * have to agree on, so — like the provisional-session-id rules below — it has exactly one\n * implementation.\n */\nexport { isHealthProbeRequest } from \"./health-probe\";\n/**\n * Exported for the Node recorder, whose `datadog-agent.ts` adapts these to the forked http and\n * undici instrumentations' ignore hooks. Which destinations count as a Datadog agent is a rule\n * both surfaces have to agree on, so — like the health-probe rule above — it has exactly one\n * implementation.\n */\nexport {\n isOrphanedDatadogAgentRequest,\n isOrphanedDatadogAgentUrl,\n type OutboundCallTarget,\n} from \"./datadog-agent\";\n/**\n * Exported for both sidecar implementations — the deployed Worker\n * (`@alwaysmeticulous/backend-recorder-sidecar-worker`) and the local Node one\n * (`backend-recorder-js`'s `sidecar/server.ts`) — which repeat the shim's health-probe\n * verdict on the events they receive, so the exclusion reaches a deployment whose bundled\n * shim predates it.\n */\nexport { HealthProbeEventFilter } from \"./health-probe-filter\";\nexport { redactRequestBody, STR_REDACTED } from \"./redact-body\";\nexport type { CoverageMap, CoverageMapFile } from \"./coverage/coverage.types\";\n// __mcEnter / __mcHit are the entry points the Vite plugin's injected imports\n// bind to, so they are part of the package's public surface even though no\n// human writes a call to them.\nexport {\n __mcEnter,\n __mcHit,\n collectHitIds,\n createCoverageSink,\n getCoverageMap,\n getUnreportedCoverageFiles,\n markCoverageFilesReported,\n registerCoverageFile,\n} from \"./coverage/runtime\";\nexport type { MeticulousExecutionContext, WithMeticulousOptions };\nexport {\n type MeticulousPagesFunction,\n type MeticulousPagesFunctionContext,\n withMeticulousPagesFunction,\n} from \"./pages-function\";\nexport {\n SIDECAR_BINDING_ENV_KEY,\n SIDECAR_BINDING_ORIGIN,\n SIDECAR_URL_ENV_KEY,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\n/**\n * Exported for the Node recorder's `withMeticulousCloudflareEnv`, which records the same KV\n * operations from a process whose bindings come from wrangler's `getPlatformProxy`. Both surfaces\n * must produce identical fields, so the contract has exactly one implementation.\n */\nexport {\n type KvCaptureFields,\n type KvCaptureOutcome,\n serializeKvArgs,\n serializeKvCaptureFields,\n} from \"./kv-capture\";\n/**\n * Exported for the same reason: the Node recorder re-exports these so a postgres.js query, a\n * captured error and a built span are byte-identical whichever surface produced them. A\n * Worker-hosted sidecar needs them too, and cannot load the Node bundle.\n */\nexport {\n deserializeCapturedError,\n serializeCapturedError,\n} from \"./error-capture\";\nexport {\n isSupportedPostgresJsQuery,\n MAX_POSTGRES_JS_RESULT_SIZE,\n POSTGRES_JS_ERROR_ATTR,\n POSTGRES_JS_QUERY_PARAMS_ATTR,\n POSTGRES_JS_QUERY_TEXT_ATTR,\n POSTGRES_JS_RESULT_ATTR,\n POSTGRES_JS_RESULT_TRUNCATED_ATTR,\n POSTGRES_JS_ROW_MODE_ATTR,\n postgresJsCommandOf,\n type PostgresJsQueryLike,\n type PostgresJsRowMode,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./postgres/capture\";\nexport { withMeticulousPostgres } from \"./postgres/with-meticulous-postgres\";\nexport * from \"./spans/attributes\";\nexport {\n hrTimeToMs,\n msToHrTime,\n type HrTime,\n type ReadableSpan,\n type SerializedSpanBase,\n SPAN_KIND_CLIENT,\n SPAN_KIND_INTERNAL,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_OK,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./spans/readable-span\";\nexport { serializeSpan } from \"./spans/serialize-span\";\nexport { randomHex, SpanBuilder } from \"./spans/span-builder\";\nexport { redactUrlCredentials } from \"./spans/url-redaction\";\n/**\n * Exported for the Node recorder, whose own `provisional-session-id.ts` adapts these to\n * `http.IncomingMessage` / `http.ServerResponse`. The mint format, the rule for which requests\n * get an id and the `Server-Timing` metric are all wire contracts with the page, so — as with\n * the KV capture fields above — they have exactly one implementation.\n */\nexport {\n type HeaderReader,\n SERVER_TIMING_HEADER,\n SERVER_TIMING_SESSION_METRIC,\n buildServerTimingSessionEntry,\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\n\n/** Structural subset of an ES-module Worker handler (`export default {...}`). */\nexport interface MeticulousWorkerHandler<Env = never> {\n fetch(\n request: Request,\n env: Env,\n ctx: MeticulousExecutionContext,\n ): Response | Promise<Response>;\n}\n\n/**\n * Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP\n * behaviour:\n *\n * export default withMeticulous({\n * async fetch(request, env, ctx) { ... },\n * });\n *\n * For a Cloudflare Pages project, whose worker exports `onRequest` instead, use\n * {@link withMeticulousPagesFunction} — same behaviour, different handler shape.\n *\n * **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**\n * Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)\n * or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),\n * or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through\n * `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js\n * queries are reported to the sidecar, batched into one report per request, without affecting the\n * app. A binding beats a var if both are configured: only the binding can have been added to\n * *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels\n * with an image.\n *\n * **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the\n * Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then\n * served from the recording instead of reaching the real service — and a call the recording does\n * not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`\n * header on a request that must stay real. The clock is frozen at the recorded session's end so\n * recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /\n * `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that\n * session. Workerd cannot read container environment variables, which is why per-replay config\n * arrives as a request header; the shim validates it and only honours a loopback /\n * docker-gateway / private-network `http:` origin.\n *\n * Replay takes precedence over recording when both are configured. A stale sidecar var baked into\n * an image is far more likely than a spurious replay header (nothing but the replay runner emits\n * one), and letting the env win would silently record a replay instead of mocking it.\n *\n * With neither configured the wrapper is a complete pass-through, so it is safe to keep in\n * deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture\n * failure or unreachable sidecar never affects the app.\n */\nexport const withMeticulous = <H extends MeticulousWorkerHandler<never>>(\n handler: H,\n options?: WithMeticulousOptions,\n): H => {\n const wrappedFetch = (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> =>\n runWithMeticulous(\n {\n request,\n env,\n ctx,\n invokeHandler: () => handler.fetch(request, env as never, ctx),\n },\n options,\n );\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript cannot verify a\n // spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\n/**\n * The Meticulous session id of the request currently being served, or `undefined` if this\n * call cannot be tied to one — including whenever the wrapper is a pass-through.\n *\n * Exposed so a server-rendering worker can put the id into the HTML it is about to return,\n * typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,\n * cache-proof alternative to the `Server-Timing` header the shim publishes automatically;\n * doing both is fine, since the recorder prefers the rendered value. It is also how the id\n * reaches another process — a document navigation has no inbound header for the app to\n * forward, so a worker that fans out to its own services has to pass this on as\n * `x-meticulous-session-id` itself.\n *\n * During a replay this returns the replayed session's id, which is the same value the render\n * being replayed was given when it was recorded (the page adopted the minted id, so that is\n * what the recording is stored under, and it is what the replay runner injects back). An app\n * that renders the id therefore renders the same bytes both times, rather than the id turning\n * every server-rendered page into a diff.\n */\nexport const getMeticulousSessionId = (): string | undefined =>\n requestCaptureContext.getStore()?.frontendSessionId;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6NA,MAAa,kBACX,SACA,YACM;CACN,MAAM,gBACJ,SACA,KACA,QAEA,kBACE;EACE;EACA;EACA;EACA,qBAAqB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAC/D,EACD,QACD;AAIH,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;;;;;;;;;;;;;;;;;;;AAqB5C,MAAa,+BACX,sBAAsB,UAAU,EAAE"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport {\n type MeticulousExecutionContext,\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\nexport type {\n BindingRequestEvent,\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n CoverageReportRequest,\n InboundRequestEvent,\n KvOmittedReason,\n KvOperation,\n KvOperationEvent,\n OutboundFetchLookupRequest,\n OutboundFetchLookupResponse,\n OutboundRequestEvent,\n PostgresQueryEvent,\n ReplaySessionInfoResponse,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n METICULOUS_PASSTHROUGH_HEADER,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n VIRTUAL_TIME_HEADER,\n WORKERD_SHIM_VERSION_HEADER,\n} from \"./protocol\";\nexport { WORKERD_SHIM_VERSION } from \"./version\";\nexport {\n MAX_BODY_CAPTURE_SIZE,\n readBodyWithCap,\n readRequestBodyWithCap,\n} from \"./body-capture\";\nexport { headersToRecord } from \"./outbound-capture\";\n/**\n * Exported for the Node recorder, whose `health-probe.ts` adapts this to\n * `http.IncomingMessage`. Which requests count as a health probe is a rule both surfaces\n * have to agree on, so — like the provisional-session-id rules below — it has exactly one\n * implementation.\n */\nexport { isHealthProbeRequest } from \"./health-probe\";\n/**\n * Exported for the Node recorder, whose `datadog-agent.ts` adapts these to the forked http and\n * undici instrumentations' ignore hooks. Which destinations count as a Datadog agent is a rule\n * both surfaces have to agree on, so — like the health-probe rule above — it has exactly one\n * implementation.\n */\nexport {\n isOrphanedDatadogAgentRequest,\n isOrphanedDatadogAgentUrl,\n type OutboundCallTarget,\n} from \"./datadog-agent\";\n/**\n * Exported for both sidecar implementations — the deployed Worker\n * (`@alwaysmeticulous/backend-recorder-sidecar-worker`) and the local Node one\n * (`backend-recorder-js`'s `sidecar/server.ts`) — which repeat the shim's health-probe\n * verdict on the events they receive, so the exclusion reaches a deployment whose bundled\n * shim predates it.\n */\nexport { HealthProbeEventFilter } from \"./health-probe-filter\";\nexport { redactRequestBody, STR_REDACTED } from \"./redact-body\";\nexport type { CoverageMap, CoverageMapFile } from \"./coverage/coverage.types\";\n// __mcEnter / __mcHit are the entry points the Vite plugin's injected imports\n// bind to, so they are part of the package's public surface even though no\n// human writes a call to them.\nexport {\n __mcEnter,\n __mcHit,\n collectHitIds,\n createCoverageSink,\n getCoverageMap,\n getUnreportedCoverageFiles,\n markCoverageFilesReported,\n registerCoverageFile,\n} from \"./coverage/runtime\";\nexport type { MeticulousExecutionContext, WithMeticulousOptions };\nexport {\n type MeticulousPagesFunction,\n type MeticulousPagesFunctionContext,\n withMeticulousPagesFunction,\n} from \"./pages-function\";\nexport {\n SIDECAR_BINDING_ENV_KEY,\n SIDECAR_BINDING_ORIGIN,\n SIDECAR_URL_ENV_KEY,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\n/**\n * Exported for the Node recorder's `withMeticulousCloudflareEnv`, which records the same KV\n * operations from a process whose bindings come from wrangler's `getPlatformProxy`. Both surfaces\n * must produce identical fields, so the contract has exactly one implementation.\n */\nexport {\n type KvCaptureFields,\n type KvCaptureOutcome,\n serializeKvArgs,\n serializeKvCaptureFields,\n} from \"./kv-capture\";\n/**\n * Exported for the same reason: the Node recorder re-exports these so a postgres.js query, a\n * captured error and a built span are byte-identical whichever surface produced them. A\n * Worker-hosted sidecar needs them too, and cannot load the Node bundle.\n */\nexport {\n deserializeCapturedError,\n serializeCapturedError,\n} from \"./error-capture\";\nexport {\n deserializeCapturedResult,\n serializeCapturedResult,\n} from \"./captured-result-codec\";\nexport {\n isSupportedPostgresJsQuery,\n MAX_POSTGRES_JS_RESULT_SIZE,\n POSTGRES_JS_ERROR_ATTR,\n POSTGRES_JS_QUERY_PARAMS_ATTR,\n POSTGRES_JS_QUERY_TEXT_ATTR,\n POSTGRES_JS_RESULT_ATTR,\n POSTGRES_JS_RESULT_TRUNCATED_ATTR,\n POSTGRES_JS_ROW_MODE_ATTR,\n postgresJsCommandOf,\n type PostgresJsQueryLike,\n type PostgresJsRowMode,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./postgres/capture\";\nexport { withMeticulousPostgres } from \"./postgres/with-meticulous-postgres\";\nexport * from \"./spans/attributes\";\nexport {\n hrTimeToMs,\n msToHrTime,\n type HrTime,\n type ReadableSpan,\n type SerializedSpanBase,\n SPAN_KIND_CLIENT,\n SPAN_KIND_INTERNAL,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_OK,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./spans/readable-span\";\nexport { serializeSpan } from \"./spans/serialize-span\";\nexport { randomHex, SpanBuilder } from \"./spans/span-builder\";\nexport { redactUrlCredentials } from \"./spans/url-redaction\";\n/**\n * Exported for the Node recorder, whose own `provisional-session-id.ts` adapts these to\n * `http.IncomingMessage` / `http.ServerResponse`. The mint format, the rule for which requests\n * get an id and the `Server-Timing` metric are all wire contracts with the page, so — as with\n * the KV capture fields above — they have exactly one implementation.\n */\nexport {\n type HeaderReader,\n SERVER_TIMING_HEADER,\n SERVER_TIMING_SESSION_METRIC,\n buildServerTimingSessionEntry,\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\n\n/** Structural subset of an ES-module Worker handler (`export default {...}`). */\nexport interface MeticulousWorkerHandler<Env = never> {\n fetch(\n request: Request,\n env: Env,\n ctx: MeticulousExecutionContext,\n ): Response | Promise<Response>;\n}\n\n/**\n * Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP\n * behaviour:\n *\n * export default withMeticulous({\n * async fetch(request, env, ctx) { ... },\n * });\n *\n * For a Cloudflare Pages project, whose worker exports `onRequest` instead, use\n * {@link withMeticulousPagesFunction} — same behaviour, different handler shape.\n *\n * **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**\n * Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)\n * or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),\n * or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through\n * `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js\n * queries are reported to the sidecar, batched into one report per request, without affecting the\n * app. A binding beats a var if both are configured: only the binding can have been added to\n * *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels\n * with an image.\n *\n * **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the\n * Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then\n * served from the recording instead of reaching the real service — and a call the recording does\n * not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`\n * header on a request that must stay real. The clock is frozen at the recorded session's end so\n * recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /\n * `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that\n * session. Workerd cannot read container environment variables, which is why per-replay config\n * arrives as a request header; the shim validates it and only honours a loopback /\n * docker-gateway / private-network `http:` origin.\n *\n * Replay takes precedence over recording when both are configured. A stale sidecar var baked into\n * an image is far more likely than a spurious replay header (nothing but the replay runner emits\n * one), and letting the env win would silently record a replay instead of mocking it.\n *\n * With neither configured the wrapper is a complete pass-through, so it is safe to keep in\n * deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture\n * failure or unreachable sidecar never affects the app.\n */\nexport const withMeticulous = <H extends MeticulousWorkerHandler<never>>(\n handler: H,\n options?: WithMeticulousOptions,\n): H => {\n const wrappedFetch = (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> =>\n runWithMeticulous(\n {\n request,\n env,\n ctx,\n invokeHandler: () => handler.fetch(request, env as never, ctx),\n },\n options,\n );\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript cannot verify a\n // spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\n/**\n * The Meticulous session id of the request currently being served, or `undefined` if this\n * call cannot be tied to one — including whenever the wrapper is a pass-through.\n *\n * Exposed so a server-rendering worker can put the id into the HTML it is about to return,\n * typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,\n * cache-proof alternative to the `Server-Timing` header the shim publishes automatically;\n * doing both is fine, since the recorder prefers the rendered value. It is also how the id\n * reaches another process — a document navigation has no inbound header for the app to\n * forward, so a worker that fans out to its own services has to pass this on as\n * `x-meticulous-session-id` itself.\n *\n * During a replay this returns the replayed session's id, which is the same value the render\n * being replayed was given when it was recorded (the page adopted the minted id, so that is\n * what the recording is stored under, and it is what the replay runner injects back). An app\n * that renders the id therefore renders the same bytes both times, rather than the id turning\n * every server-rendered page into a diff.\n */\nexport const getMeticulousSessionId = (): string | undefined =>\n requestCaptureContext.getStore()?.frontendSessionId;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiOA,MAAa,kBACX,SACA,YACM;CACN,MAAM,gBACJ,SACA,KACA,QAEA,kBACE;EACE;EACA;EACA;EACA,qBAAqB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAC/D,EACD,QACD;AAIH,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;;;;;;;;;;;;;;;;;;;AAqB5C,MAAa,+BACX,sBAAsB,UAAU,EAAE"}
@@ -54,7 +54,9 @@ declare const serializePostgresJsArgs: (args: unknown) => string;
54
54
  declare const postgresJsCommandOf: (text: string) => string;
55
55
  /**
56
56
  * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves
57
- * to) to a JSON string for storage as a span attribute.
57
+ * to) to a JSON string for storage as a span attribute. Row values go through the codec rather
58
+ * than plain JSON, because postgres.js returns a `Date` for a `timestamptz` column and a
59
+ * `Uint8Array` for `bytea`.
58
60
  */
59
61
  declare const serializePostgresJsResult: (result: unknown) => string;
60
62
  /**
@@ -1,3 +1,4 @@
1
+ import { serializeCapturedResult } from "../captured-result-codec.js";
1
2
  import { serializeCapturedError } from "../error-capture.js";
2
3
  //#region src/postgres/capture.ts
3
4
  /**
@@ -98,11 +99,12 @@ const serializeOneResult = (res) => {
98
99
  };
99
100
  /**
100
101
  * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves
101
- * to) to a JSON string for storage as a span attribute.
102
+ * to) to a JSON string for storage as a span attribute. Row values go through the codec rather
103
+ * than plain JSON, because postgres.js returns a `Date` for a `timestamptz` column and a
104
+ * `Uint8Array` for `bytea`.
102
105
  */
103
106
  const serializePostgresJsResult = (result) => {
104
- const serialized = Array.isArray(result) && result.length > 0 && result.every((r) => isResultLike(r)) ? result.map(serializeOneResult) : serializeOneResult(result);
105
- return JSON.stringify(serialized);
107
+ return serializeCapturedResult(Array.isArray(result) && result.length > 0 && result.every((r) => isResultLike(r)) ? result.map(serializeOneResult) : serializeOneResult(result));
106
108
  };
107
109
  const isResultLike = (value) => Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "command") && Object.prototype.hasOwnProperty.call(value, "count");
108
110
  const ERROR_DEBUG_PROPS = new Set([
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","names":[],"sources":["../../src/postgres/capture.ts"],"sourcesContent":["import { 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.\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 JSON.stringify(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":";;;;;;;;;;;;;;AAkBA,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;;;;;;AAOH,MAAa,6BAA6B,WAA4B;CASpE,MAAM,aAHJ,MAAM,QAAQ,OAAO,IACrB,OAAO,SAAS,KAChB,OAAO,OAAO,MAAM,aAAa,EAAE,CAAC,GAEjC,OAAqB,IAAI,mBAAmB,GAC7C,mBAAmB,OAAO;AAC9B,QAAO,KAAK,UAAU,WAAW;;AAInC,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 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"}
@@ -45,10 +45,11 @@ declare const buildServerTimingSessionEntry: (sessionId: string) => string;
45
45
  */
46
46
  declare const mintProvisionalSessionId: () => string;
47
47
  /**
48
- * Whether to mint for this inbound request. Deliberately narrow: an id nothing adopts is
49
- * worse than no id at all, because a span stamped with a session that never materialises is
50
- * excluded from the time-window fallback that would otherwise have caught it. So we mint only
51
- * for what is plausibly a browser navigating to a page.
48
+ * Whether to mint for this inbound request. Narrow on purpose — only what is plausibly a
49
+ * browser loading a page, so an API response never carries an id no document could adopt.
50
+ * Narrowness is a matter of not publishing noise rather than of avoiding harm: ingestion
51
+ * treats a backend-origin id that no session adopted as unstamped
52
+ * (`hasDisqualifyingSessionId`), so an unadopted mint is a no-op, not a regression.
52
53
  *
53
54
  * The caller decides separately whether minting is enabled at all.
54
55
  */
@@ -38,10 +38,11 @@ const mintProvisionalSessionId = () => {
38
38
  return `${(/* @__PURE__ */ new Date()).toISOString()}_${suffix}`;
39
39
  };
40
40
  /**
41
- * Whether to mint for this inbound request. Deliberately narrow: an id nothing adopts is
42
- * worse than no id at all, because a span stamped with a session that never materialises is
43
- * excluded from the time-window fallback that would otherwise have caught it. So we mint only
44
- * for what is plausibly a browser navigating to a page.
41
+ * Whether to mint for this inbound request. Narrow on purpose — only what is plausibly a
42
+ * browser loading a page, so an API response never carries an id no document could adopt.
43
+ * Narrowness is a matter of not publishing noise rather than of avoiding harm: ingestion
44
+ * treats a backend-origin id that no session adopted as unstamped
45
+ * (`hasDisqualifyingSessionId`), so an unadopted mint is a no-op, not a regression.
45
46
  *
46
47
  * The caller decides separately whether minting is enabled at all.
47
48
  */
@@ -53,16 +54,29 @@ const isProvisionalSessionIdCandidate = (method, getHeader) => {
53
54
  };
54
55
  /**
55
56
  * `Sec-Fetch-Dest` is the exact signal and every current browser sends it: `document` for a
56
- * navigation, `empty` for an in-page fetch (so a Next.js RSC navigation, which is a fetch, is
57
- * correctly excluded), `iframe` for a subframe — excluded too, since the top frame owns the
58
- * session and an iframe recorder reports into it.
57
+ * top-level navigation, `iframe`/`frame` for a subframe one, and `empty` for an in-page fetch
58
+ * (so a Next.js RSC navigation, which is a fetch, is correctly excluded).
59
+ *
60
+ * A subframe navigation qualifies because a subframe does not reliably fold into the top
61
+ * frame's session. The frontend recorder forwards a subframe's payload to the top frame and
62
+ * skips its own upload only when the top frame answers with a matching recording-token prefix
63
+ * (`tryForwardToTopFrame` in packages/recorder); where it does not — a cross-origin frame whose
64
+ * parent runs no recorder — that frame records a session of its own, and the SSR render behind
65
+ * it is precisely what minting exists to attribute. And even when the subframe does defer, the
66
+ * forwarding carries frontend data only: backend spans are attributed by the id stamped here,
67
+ * so without a mint that render is attributable to nobody either way.
68
+ *
69
+ * Nothing in the request distinguishes the two, and the server cannot know: the deciding
70
+ * handshake happens in the browser, after this response. Minting for both is what makes the
71
+ * owning case work and costs nothing in the deferring case, where the id is simply never
72
+ * adopted (see the note above).
59
73
  *
60
74
  * Absent, the caller is an older browser or not a browser at all (curl, a health check, a
61
75
  * crawler), and `Accept` is the only hint left.
62
76
  */
63
77
  const isDocumentNavigation = (getHeader) => {
64
78
  const dest = getHeader("sec-fetch-dest");
65
- if (typeof dest === "string") return dest === "document";
79
+ if (typeof dest === "string") return dest === "document" || dest === "iframe" || dest === "frame";
66
80
  const accept = getHeader("accept");
67
81
  return typeof accept === "string" && accept.includes("text/html");
68
82
  };
@@ -1 +1 @@
1
- {"version":3,"file":"provisional-session-id.js","names":[],"sources":["../src/provisional-session-id.ts"],"sourcesContent":["import { FRONTEND_SESSION_ID_HEADER } from \"./protocol\";\n\n/**\n * A session id the *backend* mints, for the one request that can never carry one from the\n * browser: a top-level document navigation. Browsers cannot add custom headers to a\n * navigation, and on the session-starting navigation the frontend has not minted an id yet\n * (it does so only once the HTML has arrived and the snippet has run) — so the server-side\n * render's spans are recorded against no session and nothing about that traffic can be\n * replayed exactly.\n *\n * Everything here is runtime-agnostic on purpose. Both backend recorders mint the same ids\n * by the same rule and publish them under the same `Server-Timing` metric, and all three are\n * wire contracts with the page, so there is exactly one implementation — the same reason\n * kv-capture.ts lives here rather than being written twice. The Node recorder imports these\n * from the package entry; see `provisional-session-id.ts` in\n * `@alwaysmeticulous/backend-recorder-js` for the `http.IncomingMessage` adapter, and\n * `publish-session-id.ts` next door for the workerd one.\n */\n\n/**\n * Reads one request header by its lower-cased name. Both `IncomingMessage.headers[name]`\n * (after the caller flattens Node's `string | string[]`) and `Headers.get(name)` satisfy it,\n * which is what lets the rules below be shared.\n */\nexport type HeaderReader = (name: string) => string | null | undefined;\n\nexport const SERVER_TIMING_HEADER = \"server-timing\";\n\n/**\n * The `Server-Timing` metric name carrying a backend-minted session id to the page. A wire\n * contract with the frontend recorder, which matches on it case-insensitively when reading\n * `PerformanceNavigationTiming.serverTiming` (see `resolve-session-id.ts` in\n * packages/recorder). Must be a valid HTTP token — no `.` or `:`, which is why it is not the\n * dotted span-attribute name.\n */\nexport const SERVER_TIMING_SESSION_METRIC = \"metsession\";\n\n/**\n * `Server-Timing` is the one response header a document's own JavaScript can read back, and\n * the navigation entry persists for the document's lifetime — so unlike a header-sniffing\n * approach this works no matter how late the snippet loads.\n */\nexport const buildServerTimingSessionEntry = (sessionId: string): string =>\n `${SERVER_TIMING_SESSION_METRIC};desc=\"${sessionId}\"`;\n\n/**\n * nanoid's default alphabet and length, reproduced rather than imported: this package carries\n * no runtime dependencies, and an id that did not look like every other session id would be a\n * gratuitous difference in S3 keys and URLs. 64 characters divides 256 exactly, so masking a\n * random byte with 63 is uniform — which is how nanoid itself generates them.\n */\nconst ID_ALPHABET =\n \"ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW\";\nconst ID_LENGTH = 21;\n\n/**\n * Mints an id in exactly the frontend recorder's format: `<ISO timestamp>_<nanoid>` (see\n * `createConfig` in packages/recorder). The format is a wire contract, not cosmetic —\n * ingestion parses the ISO prefix back out to anchor the inferred-attachment window\n * (`parseTimestampFromFrontendSessionId` in lambda-upload-sessions), and the page uses it to\n * reject an id stale enough to have come from a cache.\n */\nexport const mintProvisionalSessionId = (): string => {\n const bytes = new Uint8Array(ID_LENGTH);\n crypto.getRandomValues(bytes);\n let suffix = \"\";\n for (const byte of bytes) {\n suffix += ID_ALPHABET[byte & 63];\n }\n return `${new Date().toISOString()}_${suffix}`;\n};\n\n/**\n * Whether to mint for this inbound request. Deliberately narrow: an id nothing adopts is\n * worse than no id at all, because a span stamped with a session that never materialises is\n * excluded from the time-window fallback that would otherwise have caught it. So we mint only\n * for what is plausibly a browser navigating to a page.\n *\n * The caller decides separately whether minting is enabled at all.\n */\nexport const isProvisionalSessionIdCandidate = (\n method: string,\n getHeader: HeaderReader,\n): boolean => {\n // A request that already names its session needs nothing from us.\n if (getHeader(FRONTEND_SESSION_ID_HEADER) != null) {\n return false;\n }\n const normalizedMethod = method.toUpperCase();\n if (normalizedMethod !== \"GET\" && normalizedMethod !== \"HEAD\") {\n return false;\n }\n return isDocumentNavigation(getHeader);\n};\n\n/**\n * `Sec-Fetch-Dest` is the exact signal and every current browser sends it: `document` for a\n * navigation, `empty` for an in-page fetch (so a Next.js RSC navigation, which is a fetch, is\n * correctly excluded), `iframe` for a subframe — excluded too, since the top frame owns the\n * session and an iframe recorder reports into it.\n *\n * Absent, the caller is an older browser or not a browser at all (curl, a health check, a\n * crawler), and `Accept` is the only hint left.\n */\nconst isDocumentNavigation = (getHeader: HeaderReader): boolean => {\n const dest = getHeader(\"sec-fetch-dest\");\n if (typeof dest === \"string\") {\n return dest === \"document\";\n }\n const accept = getHeader(\"accept\");\n return typeof accept === \"string\" && accept.includes(\"text/html\");\n};\n"],"mappings":";;AA0BA,MAAa,uBAAuB;;;;;;;;AASpC,MAAa,+BAA+B;;;;;;AAO5C,MAAa,iCAAiC,cAC5C,GAAG,6BAA6B,SAAS,UAAU;;;;;;;AAQrD,MAAM,cACJ;AACF,MAAM,YAAY;;;;;;;;AASlB,MAAa,iCAAyC;CACpD,MAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,QAAO,gBAAgB,MAAM;CAC7B,IAAI,SAAS;AACb,MAAK,MAAM,QAAQ,MACjB,WAAU,YAAY,OAAO;AAE/B,QAAO,oBAAG,IAAI,MAAM,EAAC,aAAa,CAAC,GAAG;;;;;;;;;;AAWxC,MAAa,mCACX,QACA,cACY;AAEZ,KAAI,UAAA,0BAAqC,IAAI,KAC3C,QAAO;CAET,MAAM,mBAAmB,OAAO,aAAa;AAC7C,KAAI,qBAAqB,SAAS,qBAAqB,OACrD,QAAO;AAET,QAAO,qBAAqB,UAAU;;;;;;;;;;;AAYxC,MAAM,wBAAwB,cAAqC;CACjE,MAAM,OAAO,UAAU,iBAAiB;AACxC,KAAI,OAAO,SAAS,SAClB,QAAO,SAAS;CAElB,MAAM,SAAS,UAAU,SAAS;AAClC,QAAO,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY"}
1
+ {"version":3,"file":"provisional-session-id.js","names":[],"sources":["../src/provisional-session-id.ts"],"sourcesContent":["import { FRONTEND_SESSION_ID_HEADER } from \"./protocol\";\n\n/**\n * A session id the *backend* mints, for the one request that can never carry one from the\n * browser: a top-level document navigation. Browsers cannot add custom headers to a\n * navigation, and on the session-starting navigation the frontend has not minted an id yet\n * (it does so only once the HTML has arrived and the snippet has run) — so the server-side\n * render's spans are recorded against no session and nothing about that traffic can be\n * replayed exactly.\n *\n * Everything here is runtime-agnostic on purpose. Both backend recorders mint the same ids\n * by the same rule and publish them under the same `Server-Timing` metric, and all three are\n * wire contracts with the page, so there is exactly one implementation — the same reason\n * kv-capture.ts lives here rather than being written twice. The Node recorder imports these\n * from the package entry; see `provisional-session-id.ts` in\n * `@alwaysmeticulous/backend-recorder-js` for the `http.IncomingMessage` adapter, and\n * `publish-session-id.ts` next door for the workerd one.\n */\n\n/**\n * Reads one request header by its lower-cased name. Both `IncomingMessage.headers[name]`\n * (after the caller flattens Node's `string | string[]`) and `Headers.get(name)` satisfy it,\n * which is what lets the rules below be shared.\n */\nexport type HeaderReader = (name: string) => string | null | undefined;\n\nexport const SERVER_TIMING_HEADER = \"server-timing\";\n\n/**\n * The `Server-Timing` metric name carrying a backend-minted session id to the page. A wire\n * contract with the frontend recorder, which matches on it case-insensitively when reading\n * `PerformanceNavigationTiming.serverTiming` (see `resolve-session-id.ts` in\n * packages/recorder). Must be a valid HTTP token — no `.` or `:`, which is why it is not the\n * dotted span-attribute name.\n */\nexport const SERVER_TIMING_SESSION_METRIC = \"metsession\";\n\n/**\n * `Server-Timing` is the one response header a document's own JavaScript can read back, and\n * the navigation entry persists for the document's lifetime — so unlike a header-sniffing\n * approach this works no matter how late the snippet loads.\n */\nexport const buildServerTimingSessionEntry = (sessionId: string): string =>\n `${SERVER_TIMING_SESSION_METRIC};desc=\"${sessionId}\"`;\n\n/**\n * nanoid's default alphabet and length, reproduced rather than imported: this package carries\n * no runtime dependencies, and an id that did not look like every other session id would be a\n * gratuitous difference in S3 keys and URLs. 64 characters divides 256 exactly, so masking a\n * random byte with 63 is uniform — which is how nanoid itself generates them.\n */\nconst ID_ALPHABET =\n \"ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW\";\nconst ID_LENGTH = 21;\n\n/**\n * Mints an id in exactly the frontend recorder's format: `<ISO timestamp>_<nanoid>` (see\n * `createConfig` in packages/recorder). The format is a wire contract, not cosmetic —\n * ingestion parses the ISO prefix back out to anchor the inferred-attachment window\n * (`parseTimestampFromFrontendSessionId` in lambda-upload-sessions), and the page uses it to\n * reject an id stale enough to have come from a cache.\n */\nexport const mintProvisionalSessionId = (): string => {\n const bytes = new Uint8Array(ID_LENGTH);\n crypto.getRandomValues(bytes);\n let suffix = \"\";\n for (const byte of bytes) {\n suffix += ID_ALPHABET[byte & 63];\n }\n return `${new Date().toISOString()}_${suffix}`;\n};\n\n/**\n * Whether to mint for this inbound request. Narrow on purpose — only what is plausibly a\n * browser loading a page, so an API response never carries an id no document could adopt.\n * Narrowness is a matter of not publishing noise rather than of avoiding harm: ingestion\n * treats a backend-origin id that no session adopted as unstamped\n * (`hasDisqualifyingSessionId`), so an unadopted mint is a no-op, not a regression.\n *\n * The caller decides separately whether minting is enabled at all.\n */\nexport const isProvisionalSessionIdCandidate = (\n method: string,\n getHeader: HeaderReader,\n): boolean => {\n // A request that already names its session needs nothing from us.\n if (getHeader(FRONTEND_SESSION_ID_HEADER) != null) {\n return false;\n }\n const normalizedMethod = method.toUpperCase();\n if (normalizedMethod !== \"GET\" && normalizedMethod !== \"HEAD\") {\n return false;\n }\n return isDocumentNavigation(getHeader);\n};\n\n/**\n * `Sec-Fetch-Dest` is the exact signal and every current browser sends it: `document` for a\n * top-level navigation, `iframe`/`frame` for a subframe one, and `empty` for an in-page fetch\n * (so a Next.js RSC navigation, which is a fetch, is correctly excluded).\n *\n * A subframe navigation qualifies because a subframe does not reliably fold into the top\n * frame's session. The frontend recorder forwards a subframe's payload to the top frame and\n * skips its own upload only when the top frame answers with a matching recording-token prefix\n * (`tryForwardToTopFrame` in packages/recorder); where it does not — a cross-origin frame whose\n * parent runs no recorder — that frame records a session of its own, and the SSR render behind\n * it is precisely what minting exists to attribute. And even when the subframe does defer, the\n * forwarding carries frontend data only: backend spans are attributed by the id stamped here,\n * so without a mint that render is attributable to nobody either way.\n *\n * Nothing in the request distinguishes the two, and the server cannot know: the deciding\n * handshake happens in the browser, after this response. Minting for both is what makes the\n * owning case work and costs nothing in the deferring case, where the id is simply never\n * adopted (see the note above).\n *\n * Absent, the caller is an older browser or not a browser at all (curl, a health check, a\n * crawler), and `Accept` is the only hint left.\n */\nconst isDocumentNavigation = (getHeader: HeaderReader): boolean => {\n const dest = getHeader(\"sec-fetch-dest\");\n if (typeof dest === \"string\") {\n return dest === \"document\" || dest === \"iframe\" || dest === \"frame\";\n }\n const accept = getHeader(\"accept\");\n return typeof accept === \"string\" && accept.includes(\"text/html\");\n};\n"],"mappings":";;AA0BA,MAAa,uBAAuB;;;;;;;;AASpC,MAAa,+BAA+B;;;;;;AAO5C,MAAa,iCAAiC,cAC5C,GAAG,6BAA6B,SAAS,UAAU;;;;;;;AAQrD,MAAM,cACJ;AACF,MAAM,YAAY;;;;;;;;AASlB,MAAa,iCAAyC;CACpD,MAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,QAAO,gBAAgB,MAAM;CAC7B,IAAI,SAAS;AACb,MAAK,MAAM,QAAQ,MACjB,WAAU,YAAY,OAAO;AAE/B,QAAO,oBAAG,IAAI,MAAM,EAAC,aAAa,CAAC,GAAG;;;;;;;;;;;AAYxC,MAAa,mCACX,QACA,cACY;AAEZ,KAAI,UAAA,0BAAqC,IAAI,KAC3C,QAAO;CAET,MAAM,mBAAmB,OAAO,aAAa;AAC7C,KAAI,qBAAqB,SAAS,qBAAqB,OACrD,QAAO;AAET,QAAO,qBAAqB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;AAyBxC,MAAM,wBAAwB,cAAqC;CACjE,MAAM,OAAO,UAAU,iBAAiB;AACxC,KAAI,OAAO,SAAS,SAClB,QAAO,SAAS,cAAc,SAAS,YAAY,SAAS;CAE9D,MAAM,SAAS,UAAU,SAAS;AAClC,QAAO,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY"}
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.336.0";
11
+ const WORKERD_SHIM_VERSION = "2.342.0";
12
12
  //#endregion
13
13
  export { WORKERD_SHIM_VERSION };
14
14