@alwaysmeticulous/backend-recorder-workerd 2.325.0 → 2.327.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.
Files changed (78) hide show
  1. package/README.md +94 -4
  2. package/dist/binding-patch.js +17 -8
  3. package/dist/binding-patch.js.map +1 -1
  4. package/dist/body-capture.js +2 -2
  5. package/dist/body-capture.js.map +1 -1
  6. package/dist/capture-buffer.js +114 -0
  7. package/dist/capture-buffer.js.map +1 -0
  8. package/dist/context.js +6 -3
  9. package/dist/context.js.map +1 -1
  10. package/dist/error-capture.d.ts +44 -0
  11. package/dist/error-capture.js +65 -0
  12. package/dist/error-capture.js.map +1 -0
  13. package/dist/fetch-patch.js +5 -5
  14. package/dist/fetch-patch.js.map +1 -1
  15. package/dist/index.d.ts +62 -39
  16. package/dist/index.js +65 -179
  17. package/dist/index.js.map +1 -1
  18. package/dist/kv-capture.js +2 -2
  19. package/dist/kv-capture.js.map +1 -1
  20. package/dist/kv-patch.js +9 -9
  21. package/dist/kv-patch.js.map +1 -1
  22. package/dist/log.js +2 -2
  23. package/dist/log.js.map +1 -1
  24. package/dist/original-fetch.js +2 -2
  25. package/dist/original-fetch.js.map +1 -1
  26. package/dist/outbound-capture.js +20 -28
  27. package/dist/outbound-capture.js.map +1 -1
  28. package/dist/pages-function.d.ts +33 -0
  29. package/dist/pages-function.js +28 -0
  30. package/dist/pages-function.js.map +1 -0
  31. package/dist/postgres/capture.d.ts +108 -0
  32. package/dist/postgres/capture.js +136 -0
  33. package/dist/postgres/capture.js.map +1 -0
  34. package/dist/postgres/with-meticulous-postgres.d.ts +32 -0
  35. package/dist/postgres/with-meticulous-postgres.js +152 -0
  36. package/dist/postgres/with-meticulous-postgres.js.map +1 -0
  37. package/dist/protocol.d.ts +88 -28
  38. package/dist/protocol.js +15 -10
  39. package/dist/protocol.js.map +1 -1
  40. package/dist/provisional-session-id.d.ts +58 -0
  41. package/dist/provisional-session-id.js +75 -0
  42. package/dist/provisional-session-id.js.map +1 -0
  43. package/dist/publish-session-id.js +41 -0
  44. package/dist/publish-session-id.js.map +1 -0
  45. package/dist/redact-body.js +2 -2
  46. package/dist/redact-body.js.map +1 -1
  47. package/dist/replay-fetch.js +17 -13
  48. package/dist/replay-fetch.js.map +1 -1
  49. package/dist/replay-sidecar-url.js +2 -2
  50. package/dist/replay-sidecar-url.js.map +1 -1
  51. package/dist/sidecar-client.js +31 -20
  52. package/dist/sidecar-client.js.map +1 -1
  53. package/dist/sidecar-transport.d.ts +38 -0
  54. package/dist/sidecar-transport.js +52 -0
  55. package/dist/sidecar-transport.js.map +1 -0
  56. package/dist/spans/attributes.d.ts +84 -0
  57. package/dist/spans/attributes.js +88 -0
  58. package/dist/spans/attributes.js.map +1 -0
  59. package/dist/spans/readable-span.d.ts +78 -0
  60. package/dist/spans/readable-span.js +35 -0
  61. package/dist/spans/readable-span.js.map +1 -0
  62. package/dist/spans/serialize-span.d.ts +14 -0
  63. package/dist/spans/serialize-span.js +38 -0
  64. package/dist/spans/serialize-span.js.map +1 -0
  65. package/dist/spans/span-builder.d.ts +61 -0
  66. package/dist/spans/span-builder.js +250 -0
  67. package/dist/spans/span-builder.js.map +1 -0
  68. package/dist/spans/url-redaction.d.ts +14 -0
  69. package/dist/spans/url-redaction.js +40 -0
  70. package/dist/spans/url-redaction.js.map +1 -0
  71. package/dist/virtual-clock.js +2 -2
  72. package/dist/virtual-clock.js.map +1 -1
  73. package/dist/virtual-random.js +47 -20
  74. package/dist/virtual-random.js.map +1 -1
  75. package/dist/with-meticulous.d.ts +32 -0
  76. package/dist/with-meticulous.js +0 -0
  77. package/dist/with-meticulous.js.map +1 -0
  78. package/package.json +2 -1
package/dist/kv-patch.js CHANGED
@@ -1,9 +1,7 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b871e5bd-a858-5783-9618-2ec0bd2a52bf")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="88d9b432-3aec-5b5d-abea-8e34937d6d14")}catch(e){}}();
3
3
  import { requestCaptureContext } from "./context.js";
4
4
  import { warnOnce } from "./log.js";
5
- import { getOriginalFetch } from "./original-fetch.js";
6
- import { postCaptureEvents } from "./sidecar-client.js";
7
5
  import { serializeKvCaptureFields } from "./kv-capture.js";
8
6
  //#region src/kv-patch.ts
9
7
  /**
@@ -57,7 +55,7 @@ const kvBindingNames = /* @__PURE__ */ new WeakMap();
57
55
  */
58
56
  const installKvPatch = (env, options) => {
59
57
  try {
60
- for (const { name, value, skip } of kvBindings(env, options?.skipBindings)) {
58
+ for (const { name, value, skip } of kvBindings(env, options?.skipBindings, options?.skipInstances)) {
61
59
  patchKvMethods(value);
62
60
  if (!skip) kvBindingNames.set(value, name);
63
61
  }
@@ -65,14 +63,15 @@ const installKvPatch = (env, options) => {
65
63
  warnOnce("kv-patch-install", "Failed to install the Meticulous KV patch — KV operations will not be recorded.", error);
66
64
  }
67
65
  };
68
- const kvBindings = (env, skipBindings) => {
66
+ const kvBindings = (env, skipBindings, skipInstances) => {
69
67
  if (env === null || typeof env !== "object") return [];
70
68
  const skipped = new Set(skipBindings ?? []);
69
+ const skippedInstances = new Set(skipInstances ?? []);
71
70
  const found = [];
72
71
  for (const [name, value] of Object.entries(env)) if (isKvNamespaceLike(value)) found.push({
73
72
  name,
74
73
  value,
75
- skip: skipped.has(name)
74
+ skip: skipped.has(name) || skippedInstances.has(value)
76
75
  });
77
76
  return found;
78
77
  };
@@ -164,8 +163,7 @@ const captureKvOperation = async (ctx, operation, bindingName, args, invoke) =>
164
163
  };
165
164
  const report = (ctx, operation, bindingName, args, startTimeMs, outcome) => {
166
165
  try {
167
- const event = buildKvEvent(ctx, operation, bindingName, args, startTimeMs, outcome);
168
- ctx.waitUntil(postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]));
166
+ ctx.buffer.add(buildKvEvent(ctx, operation, bindingName, args, startTimeMs, outcome));
169
167
  } catch (error) {
170
168
  warnOnce("kv-report", "Failed to report a KV operation.", error);
171
169
  }
@@ -174,6 +172,8 @@ const buildKvEvent = (ctx, operation, bindingName, args, startTimeMs, outcome) =
174
172
  kind: "kv",
175
173
  requestId: ctx.requestId,
176
174
  ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
175
+ traceId: ctx.traceId,
176
+ serverSpanId: ctx.serverSpanId,
177
177
  bindingName,
178
178
  operation,
179
179
  ...serializeKvCaptureFields(operation, args, outcome),
@@ -185,4 +185,4 @@ const buildKvEvent = (ctx, operation, bindingName, args, startTimeMs, outcome) =
185
185
  export { installKvPatch };
186
186
 
187
187
  //# sourceMappingURL=kv-patch.js.map
188
- //# debugId=b871e5bd-a858-5783-9618-2ec0bd2a52bf
188
+ //# debugId=88d9b432-3aec-5b5d-abea-8e34937d6d14
@@ -1 +1 @@
1
- {"version":3,"file":"kv-patch.js","sources":["../src/kv-patch.ts"],"sourcesContent":["import { type RequestCaptureContext, requestCaptureContext } from \"./context\";\nimport { type KvCaptureOutcome, serializeKvCaptureFields } from \"./kv-capture\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport type { KvOperation, KvOperationEvent } from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\n\n/**\n * Records operations on Cloudflare KV namespace bindings (`env.MY_KV.get(key)`).\n *\n * KV is invisible to both the `globalThis.fetch` patch and the binding patch: a namespace is\n * a host object with no `fetch`, and workerd services its methods internally rather than as\n * a subrequest. So the namespace's *prototype* methods are patched instead — the same move\n * `binding-patch.ts` makes on a Fetcher, and the Node recorder on `Redis.prototype.sendCommand`.\n *\n * The prototype is reached from an instance found on `env`: as with bindings there is no\n * global to name, and going through an instance is what makes this work for an app that reads\n * its bindings from `cloudflare:workers` rather than the handler argument — that module hands\n * out a different `env` object but the same namespace instances, so the prototype is shared\n * either way.\n *\n * Verified against real workerd in `packages/backend-recorder-js/src/__tests__/`:\n * `workerd-binding-patch.spec.ts` pins the property shape this relies on (every recorded\n * method inherited and configurable, one prototype across namespaces), and\n * `workerd-kv-sidecar.spec.ts` drives real KV through the patch end to end.\n */\n\n// Marks a patched method, rather than the prototype holding it: methods can live at different\n// depths of one chain, and a base prototype can be shared with another binding type, so\n// \"already ours\" has to be answerable per function. Symbol.for so a second copy of the shim\n// (e.g. bundled twice) still detects the patch.\nconst KV_PATCHED = Symbol.for(\"meticulous.workerd.kvPatched\");\n\n/**\n * The methods recorded, and the shape a KV namespace is recognised by.\n *\n * `getWithMetadata` is what separates KV from R2: an R2 bucket also has `get`, `put`,\n * `delete` and `list`, so without it a bucket would be duck-typed as a namespace and its\n * (large, binary) objects pushed through value capture.\n */\nconst RECORDED_KV_METHODS = [\n \"get\",\n \"getWithMetadata\",\n \"put\",\n \"delete\",\n \"list\",\n] as const;\n\n/**\n * The namespaces being recorded, and the `env` key each was found under.\n *\n * Doubles as the allow-list: only an instance in here is recorded. A skipped namespace is\n * simply never registered, and — since the patch can land on a prototype shared with another\n * binding type — nothing that is not a discovered KV namespace can be recorded by accident.\n */\nconst kvBindingNames = new WeakMap<object, string>();\n\ntype KvMethod = (...args: unknown[]) => Promise<unknown>;\n\ntype KvNamespaceLike = Record<KvOperation, KvMethod>;\n\nexport interface InstallKvPatchOptions {\n /** Binding names to leave unrecorded. */\n skipBindings?: readonly string[];\n}\n\n/**\n * Discovers KV namespace bindings on `env` and patches their shared prototype so their\n * operations are recorded. Safe to call on every request — discovery is cheap and the patch\n * itself is applied once per prototype per isolate.\n *\n * Never throws: a runtime that does not permit the patch simply goes unrecorded.\n */\nexport const installKvPatch = (\n env: unknown,\n options?: InstallKvPatchOptions,\n): void => {\n try {\n for (const { name, value, skip } of kvBindings(\n env,\n options?.skipBindings,\n )) {\n // Patch even a skipped namespace's methods: they are shared with every other namespace,\n // so seeding them is what gives those coverage. Skipping is enforced by leaving the\n // instance off the allow-list below, not by declining to patch.\n patchKvMethods(value);\n if (!skip) {\n kvBindingNames.set(value, name);\n }\n }\n } catch (error) {\n warnOnce(\n \"kv-patch-install\",\n \"Failed to install the Meticulous KV patch — KV operations will not be recorded.\",\n error,\n );\n }\n};\n\ninterface DiscoveredKvBinding {\n name: string;\n value: object;\n skip: boolean;\n}\n\nconst kvBindings = (\n env: unknown,\n skipBindings: readonly string[] | undefined,\n): DiscoveredKvBinding[] => {\n if (env === null || typeof env !== \"object\") {\n return [];\n }\n const skipped = new Set<string>(skipBindings ?? []);\n const found: DiscoveredKvBinding[] = [];\n for (const [name, value] of Object.entries(env)) {\n if (isKvNamespaceLike(value)) {\n found.push({ name, value, skip: skipped.has(name) });\n }\n }\n return found;\n};\n\nconst isKvNamespaceLike = (value: unknown): value is object & KvNamespaceLike =>\n value !== null &&\n typeof value === \"object\" &&\n RECORDED_KV_METHODS.every(\n (method) =>\n typeof (value as Record<string, unknown>)[method] === \"function\",\n );\n\n/**\n * Patches every recorded method, or none of them: a half-patched namespace would record a\n * partial view of the app's KV usage, which is worse than recording none of it, because\n * nothing in the recording would say which half is missing. So the whole chain is resolved\n * first, and only then patched.\n *\n * Each method is patched where it actually lives rather than on the instance's immediate\n * prototype — workerd already puts an inherited method further up the chain for Durable\n * Object stubs, so assuming one flat prototype is not safe.\n */\nconst patchKvMethods = (binding: object): void => {\n const owners = new Map<KvOperation, MethodOwner>();\n for (const method of RECORDED_KV_METHODS) {\n const owner = findMethodOwner(binding, method);\n if (owner === undefined) {\n // Either the method is an own per-instance property or the runtime has locked the\n // prototype. Recording KV is not possible; everything else keeps working.\n warnOnce(\n \"kv-patch-unsupported\",\n `This runtime does not expose a patchable KV \\`${method}\\` — KV operations will not be recorded.`,\n );\n return;\n }\n owners.set(method, owner);\n }\n\n for (const [method, { owner, original }] of owners) {\n if ((original as unknown as Record<symbol, unknown>)[KV_PATCHED] === true) {\n continue;\n }\n Object.defineProperty(owner, method, {\n value: patchedKvMethod(method, original),\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n};\n\ninterface MethodOwner {\n owner: object;\n original: KvMethod;\n}\n\n/**\n * The prototype whose `method` a call on `instance` would reach, or undefined if there is none\n * or it cannot be replaced. The search stops at the first prototype that *has* the property,\n * patchable or not: that is the one a call resolves to, so anything further up is shadowed.\n */\nconst findMethodOwner = (\n instance: object,\n method: KvOperation,\n): MethodOwner | undefined => {\n if (Object.hasOwn(instance, method)) {\n // An own per-instance method would shadow anything we patched on the prototype.\n return undefined;\n }\n let current: object | null = Object.getPrototypeOf(instance) as object | null;\n while (current !== null) {\n const descriptor = Object.getOwnPropertyDescriptor(current, method);\n if (descriptor !== undefined) {\n return typeof descriptor.value === \"function\" &&\n descriptor.configurable === true\n ? { owner: current, original: descriptor.value as KvMethod }\n : undefined;\n }\n current = Object.getPrototypeOf(current) as object | null;\n }\n return undefined;\n};\n\n/**\n * A `function`, not an arrow: `this` is the namespace the call was made on, and it must be\n * forwarded to the original or workerd throws \"Illegal invocation\".\n */\nconst patchedKvMethod = (\n operation: KvOperation,\n original: KvMethod,\n): KvMethod => {\n const patched = function (\n this: unknown,\n ...args: unknown[]\n ): Promise<unknown> {\n const passThrough = (): Promise<unknown> => original.apply(this, args);\n\n const ctx = requestCaptureContext.getStore();\n // Record mode only. `installKvPatch` is not called for a replay request, but the patch is\n // per-isolate: a record request could have installed it earlier, and no mock store serves\n // KV, so a replay request must reach the real namespace.\n if (!ctx || ctx.mode !== \"record\") {\n return passThrough();\n }\n const receiver =\n typeof this === \"object\" && this !== null ? this : undefined;\n // Only namespaces discovered on `env` are recorded — see `kvBindingNames`.\n const bindingName =\n receiver === undefined ? undefined : kvBindingNames.get(receiver);\n if (bindingName === undefined) {\n return passThrough();\n }\n\n return captureKvOperation(ctx, operation, bindingName, args, passThrough);\n };\n Object.defineProperty(patched, KV_PATCHED, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n return patched;\n};\n\n/**\n * Records an operation without affecting it: the real method runs untouched and its result is\n * returned as-is, with the event reported in the background via `ctx.waitUntil`. Any capture\n * failure is warned about once and swallowed — recording must never break the app.\n */\nconst captureKvOperation = async (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n invoke: () => Promise<unknown>,\n): Promise<unknown> => {\n const startTimeMs = Date.now();\n let result: unknown;\n try {\n result = await invoke();\n } catch (error) {\n report(ctx, operation, bindingName, args, startTimeMs, {\n error: String(error),\n });\n throw error;\n }\n report(ctx, operation, bindingName, args, startTimeMs, { result });\n return result;\n};\n\nconst report = (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n startTimeMs: number,\n outcome: KvCaptureOutcome,\n): void => {\n try {\n const event = buildKvEvent(\n ctx,\n operation,\n bindingName,\n args,\n startTimeMs,\n outcome,\n );\n // postCaptureEvents never rejects — it warns once on an unreachable sidecar.\n ctx.waitUntil(\n postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]),\n );\n } catch (error) {\n warnOnce(\"kv-report\", \"Failed to report a KV operation.\", error);\n }\n};\n\nconst buildKvEvent = (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n startTimeMs: number,\n outcome: KvCaptureOutcome,\n): KvOperationEvent => ({\n kind: \"kv\",\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n bindingName,\n operation,\n ...serializeKvCaptureFields(operation, args, outcome),\n startTimeMs,\n endTimeMs: Date.now(),\n ...(\"error\" in outcome ? { error: outcome.error } : {}),\n});\n"],"names":[],"mappings":"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","debugId":"b871e5bd-a858-5783-9618-2ec0bd2a52bf"}
1
+ {"version":3,"file":"kv-patch.js","sources":["../src/kv-patch.ts"],"sourcesContent":["import { type RequestCaptureContext, requestCaptureContext } from \"./context\";\nimport { type KvCaptureOutcome, serializeKvCaptureFields } from \"./kv-capture\";\nimport { warnOnce } from \"./log\";\nimport type { KvOperation, KvOperationEvent } from \"./protocol\";\n\n/**\n * Records operations on Cloudflare KV namespace bindings (`env.MY_KV.get(key)`).\n *\n * KV is invisible to both the `globalThis.fetch` patch and the binding patch: a namespace is\n * a host object with no `fetch`, and workerd services its methods internally rather than as\n * a subrequest. So the namespace's *prototype* methods are patched instead — the same move\n * `binding-patch.ts` makes on a Fetcher, and the Node recorder on `Redis.prototype.sendCommand`.\n *\n * The prototype is reached from an instance found on `env`: as with bindings there is no\n * global to name, and going through an instance is what makes this work for an app that reads\n * its bindings from `cloudflare:workers` rather than the handler argument — that module hands\n * out a different `env` object but the same namespace instances, so the prototype is shared\n * either way.\n *\n * Verified against real workerd in `packages/backend-recorder-js/src/__tests__/`:\n * `workerd-binding-patch.spec.ts` pins the property shape this relies on (every recorded\n * method inherited and configurable, one prototype across namespaces), and\n * `workerd-kv-sidecar.spec.ts` drives real KV through the patch end to end.\n */\n\n// Marks a patched method, rather than the prototype holding it: methods can live at different\n// depths of one chain, and a base prototype can be shared with another binding type, so\n// \"already ours\" has to be answerable per function. Symbol.for so a second copy of the shim\n// (e.g. bundled twice) still detects the patch.\nconst KV_PATCHED = Symbol.for(\"meticulous.workerd.kvPatched\");\n\n/**\n * The methods recorded, and the shape a KV namespace is recognised by.\n *\n * `getWithMetadata` is what separates KV from R2: an R2 bucket also has `get`, `put`,\n * `delete` and `list`, so without it a bucket would be duck-typed as a namespace and its\n * (large, binary) objects pushed through value capture.\n */\nconst RECORDED_KV_METHODS = [\n \"get\",\n \"getWithMetadata\",\n \"put\",\n \"delete\",\n \"list\",\n] as const;\n\n/**\n * The namespaces being recorded, and the `env` key each was found under.\n *\n * Doubles as the allow-list: only an instance in here is recorded. A skipped namespace is\n * simply never registered, and — since the patch can land on a prototype shared with another\n * binding type — nothing that is not a discovered KV namespace can be recorded by accident.\n */\nconst kvBindingNames = new WeakMap<object, string>();\n\ntype KvMethod = (...args: unknown[]) => Promise<unknown>;\n\ntype KvNamespaceLike = Record<KvOperation, KvMethod>;\n\nexport interface InstallKvPatchOptions {\n /** Binding names to leave unrecorded. */\n skipBindings?: readonly string[];\n /**\n * Binding instances to leave unrecorded, whatever they are called. Accepted for symmetry with\n * the binding patch — a KV namespace is never the sidecar, so nothing uses it today.\n */\n skipInstances?: readonly object[];\n}\n\n/**\n * Discovers KV namespace bindings on `env` and patches their shared prototype so their\n * operations are recorded. Safe to call on every request — discovery is cheap and the patch\n * itself is applied once per prototype per isolate.\n *\n * Never throws: a runtime that does not permit the patch simply goes unrecorded.\n */\nexport const installKvPatch = (\n env: unknown,\n options?: InstallKvPatchOptions,\n): void => {\n try {\n for (const { name, value, skip } of kvBindings(\n env,\n options?.skipBindings,\n options?.skipInstances,\n )) {\n // Patch even a skipped namespace's methods: they are shared with every other namespace,\n // so seeding them is what gives those coverage. Skipping is enforced by leaving the\n // instance off the allow-list below, not by declining to patch.\n patchKvMethods(value);\n if (!skip) {\n kvBindingNames.set(value, name);\n }\n }\n } catch (error) {\n warnOnce(\n \"kv-patch-install\",\n \"Failed to install the Meticulous KV patch — KV operations will not be recorded.\",\n error,\n );\n }\n};\n\ninterface DiscoveredKvBinding {\n name: string;\n value: object;\n skip: boolean;\n}\n\nconst kvBindings = (\n env: unknown,\n skipBindings: readonly string[] | undefined,\n skipInstances: readonly object[] | undefined,\n): DiscoveredKvBinding[] => {\n if (env === null || typeof env !== \"object\") {\n return [];\n }\n const skipped = new Set<string>(skipBindings ?? []);\n const skippedInstances = new Set<object>(skipInstances ?? []);\n const found: DiscoveredKvBinding[] = [];\n for (const [name, value] of Object.entries(env)) {\n if (isKvNamespaceLike(value)) {\n found.push({\n name,\n value,\n skip: skipped.has(name) || skippedInstances.has(value),\n });\n }\n }\n return found;\n};\n\nconst isKvNamespaceLike = (value: unknown): value is object & KvNamespaceLike =>\n value !== null &&\n typeof value === \"object\" &&\n RECORDED_KV_METHODS.every(\n (method) =>\n typeof (value as Record<string, unknown>)[method] === \"function\",\n );\n\n/**\n * Patches every recorded method, or none of them: a half-patched namespace would record a\n * partial view of the app's KV usage, which is worse than recording none of it, because\n * nothing in the recording would say which half is missing. So the whole chain is resolved\n * first, and only then patched.\n *\n * Each method is patched where it actually lives rather than on the instance's immediate\n * prototype — workerd already puts an inherited method further up the chain for Durable\n * Object stubs, so assuming one flat prototype is not safe.\n */\nconst patchKvMethods = (binding: object): void => {\n const owners = new Map<KvOperation, MethodOwner>();\n for (const method of RECORDED_KV_METHODS) {\n const owner = findMethodOwner(binding, method);\n if (owner === undefined) {\n // Either the method is an own per-instance property or the runtime has locked the\n // prototype. Recording KV is not possible; everything else keeps working.\n warnOnce(\n \"kv-patch-unsupported\",\n `This runtime does not expose a patchable KV \\`${method}\\` — KV operations will not be recorded.`,\n );\n return;\n }\n owners.set(method, owner);\n }\n\n for (const [method, { owner, original }] of owners) {\n if ((original as unknown as Record<symbol, unknown>)[KV_PATCHED] === true) {\n continue;\n }\n Object.defineProperty(owner, method, {\n value: patchedKvMethod(method, original),\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n};\n\ninterface MethodOwner {\n owner: object;\n original: KvMethod;\n}\n\n/**\n * The prototype whose `method` a call on `instance` would reach, or undefined if there is none\n * or it cannot be replaced. The search stops at the first prototype that *has* the property,\n * patchable or not: that is the one a call resolves to, so anything further up is shadowed.\n */\nconst findMethodOwner = (\n instance: object,\n method: KvOperation,\n): MethodOwner | undefined => {\n if (Object.hasOwn(instance, method)) {\n // An own per-instance method would shadow anything we patched on the prototype.\n return undefined;\n }\n let current: object | null = Object.getPrototypeOf(instance) as object | null;\n while (current !== null) {\n const descriptor = Object.getOwnPropertyDescriptor(current, method);\n if (descriptor !== undefined) {\n return typeof descriptor.value === \"function\" &&\n descriptor.configurable === true\n ? { owner: current, original: descriptor.value as KvMethod }\n : undefined;\n }\n current = Object.getPrototypeOf(current) as object | null;\n }\n return undefined;\n};\n\n/**\n * A `function`, not an arrow: `this` is the namespace the call was made on, and it must be\n * forwarded to the original or workerd throws \"Illegal invocation\".\n */\nconst patchedKvMethod = (\n operation: KvOperation,\n original: KvMethod,\n): KvMethod => {\n const patched = function (\n this: unknown,\n ...args: unknown[]\n ): Promise<unknown> {\n const passThrough = (): Promise<unknown> => original.apply(this, args);\n\n const ctx = requestCaptureContext.getStore();\n // Record mode only. `installKvPatch` is not called for a replay request, but the patch is\n // per-isolate: a record request could have installed it earlier, and no mock store serves\n // KV, so a replay request must reach the real namespace.\n if (!ctx || ctx.mode !== \"record\") {\n return passThrough();\n }\n const receiver =\n typeof this === \"object\" && this !== null ? this : undefined;\n // Only namespaces discovered on `env` are recorded — see `kvBindingNames`.\n const bindingName =\n receiver === undefined ? undefined : kvBindingNames.get(receiver);\n if (bindingName === undefined) {\n return passThrough();\n }\n\n return captureKvOperation(ctx, operation, bindingName, args, passThrough);\n };\n Object.defineProperty(patched, KV_PATCHED, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n return patched;\n};\n\n/**\n * Records an operation without affecting it: the real method runs untouched and its result is\n * returned as-is, with the event reported in the background via `ctx.waitUntil`. Any capture\n * failure is warned about once and swallowed — recording must never break the app.\n */\nconst captureKvOperation = async (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n invoke: () => Promise<unknown>,\n): Promise<unknown> => {\n const startTimeMs = Date.now();\n let result: unknown;\n try {\n result = await invoke();\n } catch (error) {\n report(ctx, operation, bindingName, args, startTimeMs, {\n error: String(error),\n });\n throw error;\n }\n report(ctx, operation, bindingName, args, startTimeMs, { result });\n return result;\n};\n\nconst report = (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n startTimeMs: number,\n outcome: KvCaptureOutcome,\n): void => {\n try {\n ctx.buffer.add(\n buildKvEvent(ctx, operation, bindingName, args, startTimeMs, outcome),\n );\n } catch (error) {\n warnOnce(\"kv-report\", \"Failed to report a KV operation.\", error);\n }\n};\n\nconst buildKvEvent = (\n ctx: RequestCaptureContext,\n operation: KvOperation,\n bindingName: string,\n args: unknown[],\n startTimeMs: number,\n outcome: KvCaptureOutcome,\n): KvOperationEvent => ({\n kind: \"kv\",\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n traceId: ctx.traceId,\n serverSpanId: ctx.serverSpanId,\n bindingName,\n operation,\n ...serializeKvCaptureFields(operation, args, outcome),\n startTimeMs,\n endTimeMs: Date.now(),\n ...(\"error\" in outcome ? { error: outcome.error } : {}),\n});\n"],"names":[],"mappings":"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","debugId":"88d9b432-3aec-5b5d-abea-8e34937d6d14"}
package/dist/log.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * keeping the log short — a deduped key would hide every miss after the first.
6
6
  */
7
7
 
8
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="af311c85-9c8c-5901-81b9-33d469149fa7")}catch(e){}}();
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b4ba8614-26e6-5049-9ded-6ce5ca3e5465")}catch(e){}}();
9
9
  const warn = (message) => {
10
10
  console.warn(`[meticulous] ${message}`);
11
11
  };
@@ -21,4 +21,4 @@ const warnOnce = (key, message, error) => {
21
21
  export { warn, warnOnce };
22
22
 
23
23
  //# sourceMappingURL=log.js.map
24
- //# debugId=af311c85-9c8c-5901-81b9-33d469149fa7
24
+ //# debugId=b4ba8614-26e6-5049-9ded-6ce5ca3e5465
package/dist/log.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sources":["../src/log.ts"],"sourcesContent":["/**\n * console.warn every time. Only for something the app is about to see anyway (a replay call\n * that cannot be served, which throws), where knowing _which_ call it was matters more than\n * keeping the log short — a deduped key would hide every miss after the first.\n */\nexport const warn = (message: string): void => {\n console.warn(`[meticulous] ${message}`);\n};\n\nconst warned = new Set<string>();\n\n/** console.warn once per unique key — capture problems must never spam or break the app. */\nexport const warnOnce = (\n key: string,\n message: string,\n error?: unknown,\n): void => {\n if (warned.has(key)) {\n return;\n }\n warned.add(key);\n if (error === undefined) {\n console.warn(`[meticulous] ${message}`);\n } else {\n console.warn(`[meticulous] ${message}`, error);\n }\n};\n"],"names":[],"mappings":";;;;;;;;AAKA,MAAa,QAAQ,YAA0B;AAC7C,SAAQ,KAAK,gBAAgB,UAAU;;AAGzC,MAAM,yBAAS,IAAI,KAAa;;AAGhC,MAAa,YACX,KACA,SACA,UACS;AACT,KAAI,OAAO,IAAI,IAAI,CACjB;AAEF,QAAO,IAAI,IAAI;AACf,KAAI,UAAU,KAAA,EACZ,SAAQ,KAAK,gBAAgB,UAAU;KAEvC,SAAQ,KAAK,gBAAgB,WAAW,MAAM","debugId":"af311c85-9c8c-5901-81b9-33d469149fa7"}
1
+ {"version":3,"file":"log.js","sources":["../src/log.ts"],"sourcesContent":["/**\n * console.warn every time. Only for something the app is about to see anyway (a replay call\n * that cannot be served, which throws), where knowing _which_ call it was matters more than\n * keeping the log short — a deduped key would hide every miss after the first.\n */\nexport const warn = (message: string): void => {\n console.warn(`[meticulous] ${message}`);\n};\n\nconst warned = new Set<string>();\n\n/** console.warn once per unique key — capture problems must never spam or break the app. */\nexport const warnOnce = (\n key: string,\n message: string,\n error?: unknown,\n): void => {\n if (warned.has(key)) {\n return;\n }\n warned.add(key);\n if (error === undefined) {\n console.warn(`[meticulous] ${message}`);\n } else {\n console.warn(`[meticulous] ${message}`, error);\n }\n};\n"],"names":[],"mappings":";;;;;;;;AAKA,MAAa,QAAQ,YAA0B;AAC7C,SAAQ,KAAK,gBAAgB,UAAU;;AAGzC,MAAM,yBAAS,IAAI,KAAa;;AAGhC,MAAa,YACX,KACA,SACA,UACS;AACT,KAAI,OAAO,IAAI,IAAI,CACjB;AAEF,QAAO,IAAI,IAAI;AACf,KAAI,UAAU,KAAA,EACZ,SAAQ,KAAK,gBAAgB,UAAU;KAEvC,SAAQ,KAAK,gBAAgB,WAAW,MAAM","debugId":"b4ba8614-26e6-5049-9ded-6ce5ca3e5465"}
@@ -1,6 +1,6 @@
1
1
  //#region src/original-fetch.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="57102687-d989-5686-9632-a7d913e39891")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2a9aa350-427d-5701-a421-a41157543c20")}catch(e){}}();
4
4
  let originalFetch;
5
5
  /**
6
6
  * Holds the pre-patch `globalThis.fetch`. Its own module so that both the fetch patch (which
@@ -19,4 +19,4 @@ const getOriginalFetch = () => originalFetch ?? globalThis.fetch;
19
19
  export { getOriginalFetch, setOriginalFetch };
20
20
 
21
21
  //# sourceMappingURL=original-fetch.js.map
22
- //# debugId=57102687-d989-5686-9632-a7d913e39891
22
+ //# debugId=2a9aa350-427d-5701-a421-a41157543c20
@@ -1 +1 @@
1
- {"version":3,"file":"original-fetch.js","sources":["../src/original-fetch.ts"],"sourcesContent":["type FetchFn = typeof globalThis.fetch;\n\nlet originalFetch: FetchFn | undefined;\n\n/**\n * Holds the pre-patch `globalThis.fetch`. Its own module so that both the fetch patch (which\n * captures it) and the capture tee (which uses it to reach the sidecar) can reach it without\n * importing each other.\n */\nexport const setOriginalFetch = (fetchFn: FetchFn): void => {\n originalFetch = fetchFn;\n};\n\n/**\n * The unpatched fetch — used for the shim's own sidecar requests, so they are never\n * themselves captured. Falls back to the current global before the patch is installed.\n */\nexport const getOriginalFetch = (): FetchFn =>\n originalFetch ?? globalThis.fetch;\n"],"names":[],"mappings":";;;AAEA,IAAI;;;;;;AAOJ,MAAa,oBAAoB,YAA2B;AAC1D,iBAAgB;;;;;;AAOlB,MAAa,yBACX,iBAAiB,WAAW","debugId":"57102687-d989-5686-9632-a7d913e39891"}
1
+ {"version":3,"file":"original-fetch.js","sources":["../src/original-fetch.ts"],"sourcesContent":["type FetchFn = typeof globalThis.fetch;\n\nlet originalFetch: FetchFn | undefined;\n\n/**\n * Holds the pre-patch `globalThis.fetch`. Its own module so that both the fetch patch (which\n * captures it) and the capture tee (which uses it to reach the sidecar) can reach it without\n * importing each other.\n */\nexport const setOriginalFetch = (fetchFn: FetchFn): void => {\n originalFetch = fetchFn;\n};\n\n/**\n * The unpatched fetch — used for the shim's own sidecar requests, so they are never\n * themselves captured. Falls back to the current global before the patch is installed.\n */\nexport const getOriginalFetch = (): FetchFn =>\n originalFetch ?? globalThis.fetch;\n"],"names":[],"mappings":";;;AAEA,IAAI;;;;;;AAOJ,MAAa,oBAAoB,YAA2B;AAC1D,iBAAgB;;;;;;AAOlB,MAAa,yBACX,iBAAiB,WAAW","debugId":"2a9aa350-427d-5701-a421-a41157543c20"}
@@ -1,10 +1,8 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="19b144de-8ce4-5457-bc5e-81d4e735a7bf")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f0de66db-f0c8-57c2-8f95-fd7b22d44d79")}catch(e){}}();
3
3
  import { warnOnce } from "./log.js";
4
4
  import { readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
5
- import { getOriginalFetch } from "./original-fetch.js";
6
5
  import { CAPTURED_HEADERS } from "./protocol.js";
7
- import { postCaptureEvents } from "./sidecar-client.js";
8
6
  //#region src/outbound-capture.ts
9
7
  const capturedHeaders = new Set(CAPTURED_HEADERS);
10
8
  const headersToRecord = (headers) => {
@@ -60,18 +58,17 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
60
58
  const frozenMeta = meta;
61
59
  const endTimeMs = Date.now();
62
60
  const errorMessage = String(error);
63
- reportInBackground(ctx, async () => {
61
+ ctx.buffer.track(async () => {
64
62
  const requestBody = await requestBodyPromise;
65
- await report(ctx, transport, {
63
+ report(ctx, transport, {
66
64
  ...frozenMeta,
67
- requestId: ctx.requestId,
68
- ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
65
+ ...correlation(ctx),
69
66
  ...requestBody !== void 0 ? { requestBody } : {},
70
67
  startTimeMs,
71
68
  endTimeMs,
72
69
  error: errorMessage
73
70
  });
74
- });
71
+ }, "outbound-error-report", "Failed to report a failed outbound request.");
75
72
  }
76
73
  throw error;
77
74
  }
@@ -83,12 +80,11 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
83
80
  const responseHeaders = headersToRecord(response.headers);
84
81
  const statusCode = response.status;
85
82
  const endTimeMs = Date.now();
86
- ctx.waitUntil((async () => {
83
+ ctx.buffer.track(async () => {
87
84
  const [requestBody, responseBody] = await Promise.all([requestBodyPromise, readBodyWithCap(responseClone.body).catch(() => void 0)]);
88
- await report(ctx, transport, {
85
+ report(ctx, transport, {
89
86
  ...frozenMeta,
90
- requestId: ctx.requestId,
91
- ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
87
+ ...correlation(ctx),
92
88
  responseHeaders,
93
89
  statusCode,
94
90
  ...requestBody !== void 0 ? { requestBody } : {},
@@ -96,32 +92,28 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
96
92
  startTimeMs,
97
93
  endTimeMs
98
94
  });
99
- })().catch((error) => {
100
- warnOnce("outbound-response-capture", "Failed to capture an outbound response.", error);
101
- }));
95
+ }, "outbound-response-capture", "Failed to capture an outbound response.");
102
96
  } catch (error) {
103
97
  warnOnce("outbound-response-capture", "Failed to capture an outbound response.", error);
104
98
  }
105
99
  }
106
100
  return response;
107
101
  };
108
- const report = async (ctx, transport, fields) => {
102
+ /** The fields tying an event to its request, session and trace. */
103
+ const correlation = (ctx) => ({
104
+ requestId: ctx.requestId,
105
+ ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
106
+ traceId: ctx.traceId,
107
+ serverSpanId: ctx.serverSpanId
108
+ });
109
+ const report = (ctx, transport, fields) => {
109
110
  const event = buildEvent(transport, fields);
110
- await postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]);
111
+ ctx.buffer.add(event);
111
112
  };
112
113
  /** Detects a WebSocket upgrade response without assuming the property exists. */
113
114
  const hasWebSocket = (response) => response.webSocket != null;
114
- const reportInBackground = (ctx, reportFn) => {
115
- try {
116
- ctx.waitUntil(reportFn().catch((error) => {
117
- warnOnce("outbound-error-report", "Failed to report a failed outbound request.", error);
118
- }));
119
- } catch (error) {
120
- warnOnce("outbound-error-report", "Failed to report a failed outbound request.", error);
121
- }
122
- };
123
115
  //#endregion
124
- export { captureOutboundCall, headersToRecord };
116
+ export { captureOutboundCall, hasWebSocket, headersToRecord };
125
117
 
126
118
  //# sourceMappingURL=outbound-capture.js.map
127
- //# debugId=19b144de-8ce4-5457-bc5e-81d4e735a7bf
119
+ //# debugId=f0de66db-f0c8-57c2-8f95-fd7b22d44d79
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-capture.js","sources":["../src/outbound-capture.ts"],"sourcesContent":["import { readBodyWithCap, readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n CAPTURED_HEADERS,\n type BindingRequestEvent,\n type CaptureEvent,\n type CapturedBody,\n type OutboundRequestEvent,\n} from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\n\n/**\n * Which transport an intercepted call left the isolate through. Both are\n * Request/Response-shaped, so they share the capture tee below and differ only in the\n * event they emit.\n */\nexport type OutboundTransport =\n | { kind: \"outbound\" }\n | {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Undefined when the instance was never\n * seen on `env` — e.g. a Durable Object stub, which is produced by `namespace.get()`\n * rather than being a binding itself.\n */\n bindingName: string | undefined;\n };\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\nexport const headersToRecord = (headers: Headers): Record<string, string[]> => {\n const record: Record<string, string[]> = {};\n headers.forEach((value, name) => {\n const lowerName = name.toLowerCase();\n if (capturedHeaders.has(lowerName)) {\n (record[lowerName] ??= []).push(value);\n }\n });\n return record;\n};\n\ninterface OutboundMeta {\n method: string;\n url: string;\n requestHeaders: Record<string, string[]>;\n}\n\ninterface EventFields extends OutboundMeta {\n requestId: string;\n frontendSessionId?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n error?: string;\n}\n\n/**\n * Builds the transport's event. An explicit two-arm branch rather than a spread of the\n * union, so the discriminant and the extra binding field stay obvious. (The shim has no\n * runtime dependencies, so `assertNever` is not available here.)\n */\nconst buildEvent = (\n transport: OutboundTransport,\n fields: EventFields,\n): OutboundRequestEvent | BindingRequestEvent =>\n transport.kind === \"binding\"\n ? {\n kind: \"binding\",\n ...(transport.bindingName !== undefined\n ? { bindingName: transport.bindingName }\n : {}),\n ...fields,\n }\n : { kind: \"outbound\", ...fields };\n\n/**\n * Records an outbound call without affecting it: the real transport is invoked directly\n * (the sidecar is never in the request path), and request/response clones are reported in\n * the background via `ctx.waitUntil`. Any capture failure is warned about once and\n * swallowed — recording must never break the app.\n *\n * `invoke` is what actually performs the call, so the caller keeps ownership of the\n * receiver (a binding's `fetch` must be applied to the binding, not to `globalThis`).\n */\nexport const captureOutboundCall = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n const startTimeMs = Date.now();\n let meta: OutboundMeta | undefined;\n let requestBodyPromise: Promise<CapturedBody | undefined> =\n Promise.resolve(undefined);\n try {\n meta = {\n method: request.method,\n url: request.url,\n requestHeaders: headersToRecord(request.headers),\n };\n if (request.body) {\n const bodyClone = request.clone();\n requestBodyPromise = readRequestBodyWithCap(bodyClone.body).catch(\n () => undefined,\n );\n }\n } catch (error) {\n warnOnce(\n \"outbound-request-capture\",\n \"Failed to capture an outbound request.\",\n error,\n );\n meta = undefined;\n }\n\n let response: Response;\n try {\n response = await invoke(request);\n } catch (error) {\n if (meta) {\n const frozenMeta = meta;\n const endTimeMs = Date.now();\n const errorMessage = String(error);\n reportInBackground(ctx, async () => {\n const requestBody = await requestBodyPromise;\n await report(ctx, transport, {\n ...frozenMeta,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n ...(requestBody !== undefined ? { requestBody } : {}),\n startTimeMs,\n endTimeMs,\n error: errorMessage,\n });\n });\n }\n throw error;\n }\n\n if (meta) {\n const frozenMeta = meta;\n try {\n // A WebSocket upgrade has no readable body and cannot be represented as a recorded\n // response, so it is left entirely alone.\n if (hasWebSocket(response)) {\n return response;\n }\n const responseClone = response.clone();\n const responseHeaders = headersToRecord(response.headers);\n const statusCode = response.status;\n const endTimeMs = Date.now();\n ctx.waitUntil(\n (async () => {\n const [requestBody, responseBody] = await Promise.all([\n requestBodyPromise,\n readBodyWithCap(responseClone.body).catch(() => undefined),\n ]);\n await report(ctx, transport, {\n ...frozenMeta,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n responseHeaders,\n statusCode,\n ...(requestBody !== undefined ? { requestBody } : {}),\n ...(responseBody !== undefined ? { responseBody } : {}),\n startTimeMs,\n endTimeMs,\n });\n })().catch((error) => {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }\n }\n\n return response;\n};\n\nconst report = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n fields: EventFields,\n): Promise<void> => {\n const event: CaptureEvent = buildEvent(transport, fields);\n await postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]);\n};\n\n/** Detects a WebSocket upgrade response without assuming the property exists. */\nconst hasWebSocket = (response: Response): boolean =>\n (response as Response & { webSocket?: unknown }).webSocket != null;\n\nconst reportInBackground = (\n ctx: { waitUntil: (p: Promise<unknown>) => void },\n reportFn: () => Promise<void>,\n): void => {\n try {\n ctx.waitUntil(\n reportFn().catch((error) => {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":"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","debugId":"19b144de-8ce4-5457-bc5e-81d4e735a7bf"}
1
+ {"version":3,"file":"outbound-capture.js","sources":["../src/outbound-capture.ts"],"sourcesContent":["import { readBodyWithCap, readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\nimport {\n CAPTURED_HEADERS,\n type BindingRequestEvent,\n type CaptureEvent,\n type CapturedBody,\n type OutboundRequestEvent,\n} from \"./protocol\";\n\n/**\n * Which transport an intercepted call left the isolate through. Both are\n * Request/Response-shaped, so they share the capture tee below and differ only in the\n * event they emit.\n */\nexport type OutboundTransport =\n | { kind: \"outbound\" }\n | {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Undefined when the instance was never\n * seen on `env` — e.g. a Durable Object stub, which is produced by `namespace.get()`\n * rather than being a binding itself.\n */\n bindingName: string | undefined;\n };\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\nexport const headersToRecord = (headers: Headers): Record<string, string[]> => {\n const record: Record<string, string[]> = {};\n headers.forEach((value, name) => {\n const lowerName = name.toLowerCase();\n if (capturedHeaders.has(lowerName)) {\n (record[lowerName] ??= []).push(value);\n }\n });\n return record;\n};\n\ninterface OutboundMeta {\n method: string;\n url: string;\n requestHeaders: Record<string, string[]>;\n}\n\ninterface EventFields extends OutboundMeta {\n requestId: string;\n frontendSessionId?: string;\n traceId: string;\n serverSpanId: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n error?: string;\n}\n\n/**\n * Builds the transport's event. An explicit two-arm branch rather than a spread of the\n * union, so the discriminant and the extra binding field stay obvious. (The shim has no\n * runtime dependencies, so `assertNever` is not available here.)\n */\nconst buildEvent = (\n transport: OutboundTransport,\n fields: EventFields,\n): OutboundRequestEvent | BindingRequestEvent =>\n transport.kind === \"binding\"\n ? {\n kind: \"binding\",\n ...(transport.bindingName !== undefined\n ? { bindingName: transport.bindingName }\n : {}),\n ...fields,\n }\n : { kind: \"outbound\", ...fields };\n\n/**\n * Records an outbound call without affecting it: the real transport is invoked directly\n * (the sidecar is never in the request path), and request/response clones are reported in\n * the background via `ctx.waitUntil`. Any capture failure is warned about once and\n * swallowed — recording must never break the app.\n *\n * `invoke` is what actually performs the call, so the caller keeps ownership of the\n * receiver (a binding's `fetch` must be applied to the binding, not to `globalThis`).\n */\nexport const captureOutboundCall = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n const startTimeMs = Date.now();\n let meta: OutboundMeta | undefined;\n let requestBodyPromise: Promise<CapturedBody | undefined> =\n Promise.resolve(undefined);\n try {\n meta = {\n method: request.method,\n url: request.url,\n requestHeaders: headersToRecord(request.headers),\n };\n if (request.body) {\n const bodyClone = request.clone();\n requestBodyPromise = readRequestBodyWithCap(bodyClone.body).catch(\n () => undefined,\n );\n }\n } catch (error) {\n warnOnce(\n \"outbound-request-capture\",\n \"Failed to capture an outbound request.\",\n error,\n );\n meta = undefined;\n }\n\n let response: Response;\n try {\n response = await invoke(request);\n } catch (error) {\n if (meta) {\n const frozenMeta = meta;\n const endTimeMs = Date.now();\n const errorMessage = String(error);\n // Tracked rather than reported directly: the request body is still being read, and the\n // buffer's `close()` is what waits for it before sending the batch.\n ctx.buffer.track(\n async () => {\n const requestBody = await requestBodyPromise;\n report(ctx, transport, {\n ...frozenMeta,\n ...correlation(ctx),\n ...(requestBody !== undefined ? { requestBody } : {}),\n startTimeMs,\n endTimeMs,\n error: errorMessage,\n });\n },\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n );\n }\n throw error;\n }\n\n if (meta) {\n const frozenMeta = meta;\n try {\n // A WebSocket upgrade has no readable body and cannot be represented as a recorded\n // response, so it is left entirely alone.\n if (hasWebSocket(response)) {\n return response;\n }\n const responseClone = response.clone();\n const responseHeaders = headersToRecord(response.headers);\n const statusCode = response.status;\n const endTimeMs = Date.now();\n ctx.buffer.track(\n async () => {\n const [requestBody, responseBody] = await Promise.all([\n requestBodyPromise,\n readBodyWithCap(responseClone.body).catch(() => undefined),\n ]);\n report(ctx, transport, {\n ...frozenMeta,\n ...correlation(ctx),\n responseHeaders,\n statusCode,\n ...(requestBody !== undefined ? { requestBody } : {}),\n ...(responseBody !== undefined ? { responseBody } : {}),\n startTimeMs,\n endTimeMs,\n });\n },\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n );\n } catch (error) {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }\n }\n\n return response;\n};\n\n/** The fields tying an event to its request, session and trace. */\nconst correlation = (\n ctx: RequestCaptureContext,\n): Pick<\n EventFields,\n \"requestId\" | \"frontendSessionId\" | \"traceId\" | \"serverSpanId\"\n> => ({\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n traceId: ctx.traceId,\n serverSpanId: ctx.serverSpanId,\n});\n\nconst report = (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n fields: EventFields,\n): void => {\n const event: CaptureEvent = buildEvent(transport, fields);\n ctx.buffer.add(event);\n};\n\n/** Detects a WebSocket upgrade response without assuming the property exists. */\nexport const hasWebSocket = (response: Response): boolean =>\n (response as Response & { webSocket?: unknown }).webSocket != null;\n"],"names":[],"mappings":";;;;;;AA4BA,MAAM,kBAAkB,IAAI,IAAY,iBAAiB;AAEzD,MAAa,mBAAmB,YAA+C;CAC7E,MAAM,SAAmC,EAAE;AAC3C,SAAQ,SAAS,OAAO,SAAS;EAC/B,MAAM,YAAY,KAAK,aAAa;AACpC,MAAI,gBAAgB,IAAI,UAAU,CAChC,EAAC,OAAO,eAAe,EAAE,EAAE,KAAK,MAAM;GAExC;AACF,QAAO;;;;;;;AA4BT,MAAM,cACJ,WACA,WAEA,UAAU,SAAS,YACf;CACE,MAAM;CACN,GAAI,UAAU,gBAAgB,KAAA,IAC1B,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;CACN,GAAG;CACJ,GACD;CAAE,MAAM;CAAY,GAAG;CAAQ;;;;;;;;;;AAWrC,MAAa,sBAAsB,OACjC,KACA,WACA,SACA,WACsB;CACtB,MAAM,cAAc,KAAK,KAAK;CAC9B,IAAI;CACJ,IAAI,qBACF,QAAQ,QAAQ,KAAA,EAAU;AAC5B,KAAI;AACF,SAAO;GACL,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,gBAAgB,gBAAgB,QAAQ,QAAQ;GACjD;AACD,MAAI,QAAQ,KAEV,sBAAqB,uBADH,QAAQ,OAC2B,CAAC,KAAK,CAAC,YACpD,KAAA,EACP;UAEI,OAAO;AACd,WACE,4BACA,0CACA,MACD;AACD,SAAO,KAAA;;CAGT,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,OAAO,QAAQ;UACzB,OAAO;AACd,MAAI,MAAM;GACR,MAAM,aAAa;GACnB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,eAAe,OAAO,MAAM;AAGlC,OAAI,OAAO,MACT,YAAY;IACV,MAAM,cAAc,MAAM;AAC1B,WAAO,KAAK,WAAW;KACrB,GAAG;KACH,GAAG,YAAY,IAAI;KACnB,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD;KACA;KACA,OAAO;KACR,CAAC;MAEJ,yBACA,8CACD;;AAEH,QAAM;;AAGR,KAAI,MAAM;EACR,MAAM,aAAa;AACnB,MAAI;AAGF,OAAI,aAAa,SAAS,CACxB,QAAO;GAET,MAAM,gBAAgB,SAAS,OAAO;GACtC,MAAM,kBAAkB,gBAAgB,SAAS,QAAQ;GACzD,MAAM,aAAa,SAAS;GAC5B,MAAM,YAAY,KAAK,KAAK;AAC5B,OAAI,OAAO,MACT,YAAY;IACV,MAAM,CAAC,aAAa,gBAAgB,MAAM,QAAQ,IAAI,CACpD,oBACA,gBAAgB,cAAc,KAAK,CAAC,YAAY,KAAA,EAAU,CAC3D,CAAC;AACF,WAAO,KAAK,WAAW;KACrB,GAAG;KACH,GAAG,YAAY,IAAI;KACnB;KACA;KACA,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD,GAAI,iBAAiB,KAAA,IAAY,EAAE,cAAc,GAAG,EAAE;KACtD;KACA;KACD,CAAC;MAEJ,6BACA,0CACD;WACM,OAAO;AACd,YACE,6BACA,2CACA,MACD;;;AAIL,QAAO;;;AAIT,MAAM,eACJ,SAII;CACJ,WAAW,IAAI;CACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;CACN,SAAS,IAAI;CACb,cAAc,IAAI;CACnB;AAED,MAAM,UACJ,KACA,WACA,WACS;CACT,MAAM,QAAsB,WAAW,WAAW,OAAO;AACzD,KAAI,OAAO,IAAI,MAAM;;;AAIvB,MAAa,gBAAgB,aAC1B,SAAgD,aAAa","debugId":"f0de66db-f0c8-57c2-8f95-fd7b22d44d79"}
@@ -0,0 +1,33 @@
1
+ import { WithMeticulousOptions } from "./with-meticulous.js";
2
+
3
+ //#region src/pages-function.d.ts
4
+ /**
5
+ * Structural subset of a Cloudflare Pages Functions `EventContext` — the argument an `onRequest`
6
+ * handler receives. Only the four fields the recorder reads are declared; everything else on the
7
+ * real context (`params`, `data`, `next`, `functionPath`, `passThroughOnException`) is preserved
8
+ * by the wrapper without being named here, so this stays free of a
9
+ * `@cloudflare/workers-types` dependency.
10
+ */
11
+ interface MeticulousPagesFunctionContext<Env = unknown> {
12
+ request: Request;
13
+ env: Env;
14
+ waitUntil(promise: Promise<unknown>): void;
15
+ }
16
+ type MeticulousPagesFunction<Context extends MeticulousPagesFunctionContext = MeticulousPagesFunctionContext> = (context: Context) => Response | Promise<Response>;
17
+ /**
18
+ * The Pages Functions equivalent of {@link withMeticulous}:
19
+ *
20
+ * export const onRequest = withMeticulousPagesFunction(handler);
21
+ *
22
+ * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,
23
+ * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and
24
+ * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.
25
+ *
26
+ * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the
27
+ * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own
28
+ * façades first would hide the real binding instances from it.
29
+ */
30
+ declare const withMeticulousPagesFunction: <Context extends MeticulousPagesFunctionContext>(handler: MeticulousPagesFunction<Context>, options?: WithMeticulousOptions) => (context: Context) => Promise<Response>;
31
+ //#endregion
32
+ export { MeticulousPagesFunction, MeticulousPagesFunctionContext, withMeticulousPagesFunction };
33
+ //# sourceMappingURL=pages-function.d.ts.map
@@ -0,0 +1,28 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="78b550d0-f18f-5c3c-b576-7cef30ecadc9")}catch(e){}}();
3
+ import { runWithMeticulous } from "./with-meticulous.js";
4
+ //#region src/pages-function.ts
5
+ /**
6
+ * The Pages Functions equivalent of {@link withMeticulous}:
7
+ *
8
+ * export const onRequest = withMeticulousPagesFunction(handler);
9
+ *
10
+ * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,
11
+ * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and
12
+ * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.
13
+ *
14
+ * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the
15
+ * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own
16
+ * façades first would hide the real binding instances from it.
17
+ */
18
+ const withMeticulousPagesFunction = (handler, options) => (context) => runWithMeticulous({
19
+ request: context.request,
20
+ env: context.env,
21
+ ctx: { waitUntil: (promise) => context.waitUntil(promise) },
22
+ invokeHandler: () => handler(context)
23
+ }, options);
24
+ //#endregion
25
+ export { withMeticulousPagesFunction };
26
+
27
+ //# sourceMappingURL=pages-function.js.map
28
+ //# debugId=78b550d0-f18f-5c3c-b576-7cef30ecadc9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages-function.js","sources":["../src/pages-function.ts"],"sourcesContent":["import {\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\n/**\n * Structural subset of a Cloudflare Pages Functions `EventContext` — the argument an `onRequest`\n * handler receives. Only the four fields the recorder reads are declared; everything else on the\n * real context (`params`, `data`, `next`, `functionPath`, `passThroughOnException`) is preserved\n * by the wrapper without being named here, so this stays free of a\n * `@cloudflare/workers-types` dependency.\n */\nexport interface MeticulousPagesFunctionContext<Env = unknown> {\n request: Request;\n env: Env;\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport type MeticulousPagesFunction<\n Context extends MeticulousPagesFunctionContext =\n MeticulousPagesFunctionContext,\n> = (context: Context) => Response | Promise<Response>;\n\n/**\n * The Pages Functions equivalent of {@link withMeticulous}:\n *\n * export const onRequest = withMeticulousPagesFunction(handler);\n *\n * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,\n * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and\n * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.\n *\n * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the\n * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own\n * façades first would hide the real binding instances from it.\n */\nexport const withMeticulousPagesFunction =\n <Context extends MeticulousPagesFunctionContext>(\n handler: MeticulousPagesFunction<Context>,\n options?: WithMeticulousOptions,\n ) =>\n (context: Context): Promise<Response> =>\n runWithMeticulous(\n {\n request: context.request,\n env: context.env,\n ctx: { waitUntil: (promise) => context.waitUntil(promise) },\n invokeHandler: () => handler(context),\n },\n options,\n );\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAoCA,MAAa,+BAET,SACA,aAED,YACC,kBACE;CACE,SAAS,QAAQ;CACjB,KAAK,QAAQ;CACb,KAAK,EAAE,YAAY,YAAY,QAAQ,UAAU,QAAQ,EAAE;CAC3D,qBAAqB,QAAQ,QAAQ;CACtC,EACD,QACD","debugId":"78b550d0-f18f-5c3c-b576-7cef30ecadc9"}
@@ -0,0 +1,108 @@
1
+ //#region src/postgres/capture.d.ts
2
+ /**
3
+ * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node
4
+ * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.
5
+ *
6
+ * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a
7
+ * deployed Worker has to produce the byte-identical span a Node-recorded one does, because
8
+ * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the
9
+ * serialization subtly wrong on one surface and every replay of a Worker recording misses.
10
+ * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here
11
+ * and keeps the replay half (key building, result/error reconstruction) to itself — that half
12
+ * needs a synchronous SHA-256 and never runs inside a Worker.
13
+ */
14
+ declare const POSTGRES_JS_QUERY_TEXT_ATTR = "meticulous.postgresjs.query.text";
15
+ declare const POSTGRES_JS_QUERY_PARAMS_ATTR = "meticulous.postgresjs.query.params";
16
+ declare const POSTGRES_JS_ROW_MODE_ATTR = "meticulous.postgresjs.row_mode";
17
+ declare const POSTGRES_JS_RESULT_ATTR = "meticulous.postgresjs.result";
18
+ declare const POSTGRES_JS_RESULT_TRUNCATED_ATTR = "meticulous.postgresjs.result.truncated";
19
+ declare const POSTGRES_JS_ERROR_ATTR = "meticulous.postgresjs.error";
20
+ declare const MAX_POSTGRES_JS_RESULT_SIZE: number;
21
+ /**
22
+ * How the rows of a result are shaped: objects keyed by column name by default, or arrays under
23
+ * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |
24
+ * `true` | `"values"`).
25
+ *
26
+ * This belongs in the match key because the SAME sql text returns different row shapes
27
+ * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits
28
+ * the query directly for `db.execute`, so both shapes occur for identical text.
29
+ */
30
+ type PostgresJsRowMode = "" | "raw" | "values";
31
+ declare const resolveRowMode: (isRaw: unknown) => PostgresJsRowMode;
32
+ /**
33
+ * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the
34
+ * interpolated values went.
35
+ *
36
+ * The SQL text does not exist at interception time: postgres.js builds `query.string` only once
37
+ * a query reaches a connection, and replay never executes, so both sides must derive the text
38
+ * from `query.strings` instead.
39
+ */
40
+ declare const reconstructQueryText: (strings: unknown) => string | undefined;
41
+ /**
42
+ * Canonical serialization of the query's interpolated values.
43
+ *
44
+ * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not
45
+ * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place
46
+ * into wire-format strings — so it differs between the two sides.
47
+ *
48
+ * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /
49
+ * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across
50
+ * calls, which is all the match key needs.
51
+ */
52
+ declare const serializePostgresJsArgs: (args: unknown) => string;
53
+ /** The leading SQL keyword, for the span name only. */
54
+ declare const postgresJsCommandOf: (text: string) => string;
55
+ /**
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.
58
+ */
59
+ declare const serializePostgresJsResult: (result: unknown) => string;
60
+ /**
61
+ * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as
62
+ * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields
63
+ * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All
64
+ * of those live in own enumerable properties, so copying those plus `message` (own but
65
+ * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.
66
+ */
67
+ declare const serializePostgresJsError: (error: unknown) => string;
68
+ /**
69
+ * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no
70
+ * `postgres` dependency).
71
+ */
72
+ interface PostgresJsQueryLike {
73
+ /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */
74
+ strings: unknown;
75
+ /** The interpolated values, still raw JS values at interception time. */
76
+ args: unknown;
77
+ /** `undefined` by default, `true` after `.raw()`, `"values"` after `.values()`. */
78
+ isRaw?: unknown;
79
+ /** Set once `handle()` has run; guards against double-instrumenting one query. */
80
+ executed?: boolean;
81
+ /** Set by `.describe()` — resolves with a Statement, not a Result. */
82
+ onlyDescribe?: boolean;
83
+ /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */
84
+ cursorFn?: unknown;
85
+ cursorRows?: unknown;
86
+ forEachFn?: unknown;
87
+ streaming?: unknown;
88
+ options?: {
89
+ /**
90
+ * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it
91
+ * called with the serving connection before the transaction body runs.
92
+ */
93
+ onexecute?: (connection: unknown) => void;
94
+ [key: string]: unknown;
95
+ };
96
+ resolve: (result: unknown) => void;
97
+ reject: (error: unknown) => void;
98
+ }
99
+ /**
100
+ * Whether a query resolves with a single `Result` we can capture and replay. Cursors,
101
+ * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with
102
+ * something else, and `sql.file()` is constructed with no template chunks at all (its text is
103
+ * only read from disk later). All of those pass straight through, unrecorded and unmocked.
104
+ */
105
+ declare const isSupportedPostgresJsQuery: (query: PostgresJsQueryLike) => boolean;
106
+ //#endregion
107
+ export { 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 };
108
+ //# sourceMappingURL=capture.d.ts.map