@alwaysmeticulous/backend-recorder-workerd 2.326.0 → 2.329.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 (82) hide show
  1. package/README.md +36 -0
  2. package/dist/binding-patch.js +2 -2
  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 +2 -2
  7. package/dist/capture-buffer.js.map +1 -1
  8. package/dist/context.js +2 -2
  9. package/dist/context.js.map +1 -1
  10. package/dist/coverage/coverage.types.d.ts +32 -0
  11. package/dist/coverage/replay-coverage.js +84 -0
  12. package/dist/coverage/replay-coverage.js.map +1 -0
  13. package/dist/coverage/runtime.d.ts +70 -0
  14. package/dist/coverage/runtime.js +138 -0
  15. package/dist/coverage/runtime.js.map +1 -0
  16. package/dist/error-capture.js +2 -2
  17. package/dist/error-capture.js.map +1 -1
  18. package/dist/fetch-patch.js +2 -2
  19. package/dist/fetch-patch.js.map +1 -1
  20. package/dist/index.d.ts +5 -2
  21. package/dist/index.js +6 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/kv-capture.js +2 -2
  24. package/dist/kv-capture.js.map +1 -1
  25. package/dist/kv-patch.js +2 -2
  26. package/dist/kv-patch.js.map +1 -1
  27. package/dist/log.js +2 -2
  28. package/dist/log.js.map +1 -1
  29. package/dist/original-fetch.js +2 -2
  30. package/dist/original-fetch.js.map +1 -1
  31. package/dist/outbound-capture.js +2 -2
  32. package/dist/outbound-capture.js.map +1 -1
  33. package/dist/pages-function.js +2 -2
  34. package/dist/pages-function.js.map +1 -1
  35. package/dist/postgres/capture.js +2 -2
  36. package/dist/postgres/capture.js.map +1 -1
  37. package/dist/postgres/with-meticulous-postgres.js +2 -2
  38. package/dist/postgres/with-meticulous-postgres.js.map +1 -1
  39. package/dist/protocol.d.ts +48 -18
  40. package/dist/protocol.js +22 -6
  41. package/dist/protocol.js.map +1 -1
  42. package/dist/provisional-session-id.js +2 -2
  43. package/dist/provisional-session-id.js.map +1 -1
  44. package/dist/publish-session-id.js +2 -2
  45. package/dist/publish-session-id.js.map +1 -1
  46. package/dist/publish-shim-version.js +37 -0
  47. package/dist/publish-shim-version.js.map +1 -0
  48. package/dist/redact-body.js +2 -2
  49. package/dist/redact-body.js.map +1 -1
  50. package/dist/replay-fetch.js +15 -12
  51. package/dist/replay-fetch.js.map +1 -1
  52. package/dist/replay-log-tagging.js +57 -0
  53. package/dist/replay-log-tagging.js.map +1 -0
  54. package/dist/replay-sidecar-url.js +2 -2
  55. package/dist/replay-sidecar-url.js.map +1 -1
  56. package/dist/sidecar-client.js +47 -6
  57. package/dist/sidecar-client.js.map +1 -1
  58. package/dist/sidecar-transport.js +2 -2
  59. package/dist/sidecar-transport.js.map +1 -1
  60. package/dist/spans/attributes.js +2 -2
  61. package/dist/spans/attributes.js.map +1 -1
  62. package/dist/spans/readable-span.js +2 -2
  63. package/dist/spans/readable-span.js.map +1 -1
  64. package/dist/spans/serialize-span.js +2 -2
  65. package/dist/spans/serialize-span.js.map +1 -1
  66. package/dist/spans/span-builder.js +2 -2
  67. package/dist/spans/span-builder.js.map +1 -1
  68. package/dist/spans/url-redaction.js +2 -2
  69. package/dist/spans/url-redaction.js.map +1 -1
  70. package/dist/version.d.ts +14 -0
  71. package/dist/version.js +18 -0
  72. package/dist/version.js.map +1 -0
  73. package/dist/virtual-clock.js +2 -2
  74. package/dist/virtual-clock.js.map +1 -1
  75. package/dist/virtual-random.js +47 -20
  76. package/dist/virtual-random.js.map +1 -1
  77. package/dist/vite/index.d.ts +62 -0
  78. package/dist/vite/index.js +6764 -0
  79. package/dist/vite/index.js.map +1 -0
  80. package/dist/with-meticulous.js +0 -0
  81. package/dist/with-meticulous.js.map +1 -1
  82. package/package.json +5 -2
@@ -1 +1 @@
1
- {"version":3,"file":"error-capture.js","sources":["../src/error-capture.ts"],"sourcesContent":["/**\n * Capturing and reconstructing a thrown error, shared by every instrumentation that replays\n * failures rather than treating them as \"nothing was recorded\".\n *\n * Recording the error matters wherever the app's own behaviour depends on it: a call that\n * legitimately failed during recording (a constraint violation, a rejected payment) must replay\n * as the same failure, not as a miss. An uncaptured error replays as a hermetic \"no recorded\n * result\", which renders a different page and reads as a product change.\n *\n * What survives: `name`, `message` and every own enumerable property — which is where a JS\n * error carries the fields consumers actually branch on (`code`, `status`, `severity`). What\n * does not: the class. Reconstruction returns a plain `Error`, so `error.name ===\n * \"PostgresError\"` and `error.code` behave as recorded but `instanceof` does not match.\n *\n * It lives in this package, rather than in the Node recorder, because the workerd postgres.js\n * capture has to produce byte-identical JSON to the Node one — the same reason `kv-capture.ts`\n * is here. `packages/backend-recorder-js/src/error-capture-shared.ts` re-exports both halves.\n */\n\n/**\n * Serializes a thrown value to a JSON string.\n *\n * `omitProps` names own properties to leave out — large debug payloads, or values that are\n * redundant with attributes the span already carries. `stack` is almost always in it: it is\n * recorded-process-specific and would be misleading replayed into another process.\n *\n * A non-`Error` throw (a string, an object) is captured by its `String()` form, which is what a\n * consumer would have seen from a `catch` that assumed an `Error`.\n */\nexport const serializeCapturedError = (\n error: unknown,\n omitProps: ReadonlySet<string>,\n): string => {\n if (!(error instanceof Error)) {\n return JSON.stringify({ name: \"Error\", message: String(error) });\n }\n const fields: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(error)) {\n if (!omitProps.has(key)) {\n fields[key] = value;\n }\n }\n // `name`/`message` last so an own property of either name cannot shadow the real one.\n return JSON.stringify({\n ...fields,\n name: error.name,\n message: error.message,\n });\n};\n\n/**\n * Reconstructs a captured error. Both message fallbacks are the caller's, because they surface\n * in the app's own error handling and should name the technology that failed.\n */\nexport const deserializeCapturedError = (\n json: string,\n {\n unparseableMessage,\n fallbackMessage,\n }: { unparseableMessage: string; fallbackMessage: string },\n): Error => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(json) as Record<string, unknown>;\n } catch {\n return new Error(unparseableMessage);\n }\n const { message, name, ...fields } = parsed;\n const error = Object.assign(\n new Error(typeof message === \"string\" ? message : fallbackMessage),\n fields,\n );\n if (typeof name === \"string\") {\n error.name = name;\n }\n return error;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,0BACX,OACA,cACW;AACX,KAAI,EAAE,iBAAiB,OACrB,QAAO,KAAK,UAAU;EAAE,MAAM;EAAS,SAAS,OAAO,MAAM;EAAE,CAAC;CAElE,MAAM,SAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,CAC9C,KAAI,CAAC,UAAU,IAAI,IAAI,CACrB,QAAO,OAAO;AAIlB,QAAO,KAAK,UAAU;EACpB,GAAG;EACH,MAAM,MAAM;EACZ,SAAS,MAAM;EAChB,CAAC;;;;;;AAOJ,MAAa,4BACX,MACA,EACE,oBACA,sBAEQ;CACV,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,KAAK;SACnB;AACN,SAAO,IAAI,MAAM,mBAAmB;;CAEtC,MAAM,EAAE,SAAS,MAAM,GAAG,WAAW;CACrC,MAAM,QAAQ,OAAO,OACnB,IAAI,MAAM,OAAO,YAAY,WAAW,UAAU,gBAAgB,EAClE,OACD;AACD,KAAI,OAAO,SAAS,SAClB,OAAM,OAAO;AAEf,QAAO","debugId":"bf623e75-cd70-5ffa-bdf9-9e125c4dda04"}
1
+ {"version":3,"file":"error-capture.js","sources":["../src/error-capture.ts"],"sourcesContent":["/**\n * Capturing and reconstructing a thrown error, shared by every instrumentation that replays\n * failures rather than treating them as \"nothing was recorded\".\n *\n * Recording the error matters wherever the app's own behaviour depends on it: a call that\n * legitimately failed during recording (a constraint violation, a rejected payment) must replay\n * as the same failure, not as a miss. An uncaptured error replays as a hermetic \"no recorded\n * result\", which renders a different page and reads as a product change.\n *\n * What survives: `name`, `message` and every own enumerable property — which is where a JS\n * error carries the fields consumers actually branch on (`code`, `status`, `severity`). What\n * does not: the class. Reconstruction returns a plain `Error`, so `error.name ===\n * \"PostgresError\"` and `error.code` behave as recorded but `instanceof` does not match.\n *\n * It lives in this package, rather than in the Node recorder, because the workerd postgres.js\n * capture has to produce byte-identical JSON to the Node one — the same reason `kv-capture.ts`\n * is here. `packages/backend-recorder-js/src/error-capture-shared.ts` re-exports both halves.\n */\n\n/**\n * Serializes a thrown value to a JSON string.\n *\n * `omitProps` names own properties to leave out — large debug payloads, or values that are\n * redundant with attributes the span already carries. `stack` is almost always in it: it is\n * recorded-process-specific and would be misleading replayed into another process.\n *\n * A non-`Error` throw (a string, an object) is captured by its `String()` form, which is what a\n * consumer would have seen from a `catch` that assumed an `Error`.\n */\nexport const serializeCapturedError = (\n error: unknown,\n omitProps: ReadonlySet<string>,\n): string => {\n if (!(error instanceof Error)) {\n return JSON.stringify({ name: \"Error\", message: String(error) });\n }\n const fields: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(error)) {\n if (!omitProps.has(key)) {\n fields[key] = value;\n }\n }\n // `name`/`message` last so an own property of either name cannot shadow the real one.\n return JSON.stringify({\n ...fields,\n name: error.name,\n message: error.message,\n });\n};\n\n/**\n * Reconstructs a captured error. Both message fallbacks are the caller's, because they surface\n * in the app's own error handling and should name the technology that failed.\n */\nexport const deserializeCapturedError = (\n json: string,\n {\n unparseableMessage,\n fallbackMessage,\n }: { unparseableMessage: string; fallbackMessage: string },\n): Error => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(json) as Record<string, unknown>;\n } catch {\n return new Error(unparseableMessage);\n }\n const { message, name, ...fields } = parsed;\n const error = Object.assign(\n new Error(typeof message === \"string\" ? message : fallbackMessage),\n fields,\n );\n if (typeof name === \"string\") {\n error.name = name;\n }\n return error;\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,0BACX,OACA,cACW;AACX,KAAI,EAAE,iBAAiB,OACrB,QAAO,KAAK,UAAU;EAAE,MAAM;EAAS,SAAS,OAAO,MAAM;EAAE,CAAC;CAElE,MAAM,SAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,CAC9C,KAAI,CAAC,UAAU,IAAI,IAAI,CACrB,QAAO,OAAO;AAIlB,QAAO,KAAK,UAAU;EACpB,GAAG;EACH,MAAM,MAAM;EACZ,SAAS,MAAM;EAChB,CAAC;;;;;;AAOJ,MAAa,4BACX,MACA,EACE,oBACA,sBAEQ;CACV,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,KAAK;SACnB;AACN,SAAO,IAAI,MAAM,mBAAmB;;CAEtC,MAAM,EAAE,SAAS,MAAM,GAAG,WAAW;CACrC,MAAM,QAAQ,OAAO,OACnB,IAAI,MAAM,OAAO,YAAY,WAAW,UAAU,gBAAgB,EAClE,OACD;AACD,KAAI,OAAO,SAAS,SAClB,OAAM,OAAO;AAEf,QAAO","debugId":"543df2da-8d0b-579f-8a26-5392d803794b"}
@@ -1,5 +1,5 @@
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]="702f30a5-c657-58b7-90a4-8f78c5fcdb2b")}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]="006811cf-e9b2-5a53-b257-639036ac8912")}catch(e){}}();
3
3
  import { requestCaptureContext, sidecarOriginOf } from "./context.js";
4
4
  import { captureOutboundCall } from "./outbound-capture.js";
5
5
  import { getOriginalFetch, setOriginalFetch } from "./original-fetch.js";
@@ -42,4 +42,4 @@ const patchedFetch = async (input, init) => {
42
42
  export { installFetchPatch };
43
43
 
44
44
  //# sourceMappingURL=fetch-patch.js.map
45
- //# debugId=702f30a5-c657-58b7-90a4-8f78c5fcdb2b
45
+ //# debugId=006811cf-e9b2-5a53-b257-639036ac8912
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-patch.js","sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n return ctx.mode === \"replay\"\n ? replayOutboundCall(ctx, request, (req) => original(req))\n : captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"names":[],"mappings":";;;;;;;AAQA,MAAM,gBAAgB,OAAO,IAAI,kCAAkC;;;;;;;;;;;;;AAiBnE,MAAa,0BAAgC;CAC3C,MAAM,SAAS;AACf,KAAI,OAAO,eACT;AAEF,QAAO,iBAAiB;AACxB,kBAAiB,WAAW,MAAM,KAAK,WAAW,CAAC;AACnD,YAAW,QAAQ;;AAGrB,MAAM,eAAwB,OAAO,OAAO,SAAS;CACnD,MAAM,WAAW,kBAAkB;CACnC,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,CAAC,IACH,QAAO,SAAS,OAAO,KAAK;CAM9B,IAAI;AACJ,KAAI;AACF,YAAU,IAAI,QAAQ,OAAO,KAAK;SAC5B;AAEN,SAAO,SAAS,OAAO,KAAK;;AAK9B,KAAI,QAAQ,IAAI,WAAW,GAAG,gBAAgB,IAAI,CAAC,GAAG,CACpD,QAAO,SAAS,QAAQ;AAG1B,QAAO,IAAI,SAAS,WAChB,mBAAmB,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC,GACxD,oBAAoB,KAAK,EAAE,MAAM,YAAY,EAAE,UAAU,QACvD,SAAS,IAAI,CACd","debugId":"702f30a5-c657-58b7-90a4-8f78c5fcdb2b"}
1
+ {"version":3,"file":"fetch-patch.js","sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n return ctx.mode === \"replay\"\n ? replayOutboundCall(ctx, request, (req) => original(req))\n : captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"names":[],"mappings":";;;;;;;AAQA,MAAM,gBAAgB,OAAO,IAAI,kCAAkC;;;;;;;;;;;;;AAiBnE,MAAa,0BAAgC;CAC3C,MAAM,SAAS;AACf,KAAI,OAAO,eACT;AAEF,QAAO,iBAAiB;AACxB,kBAAiB,WAAW,MAAM,KAAK,WAAW,CAAC;AACnD,YAAW,QAAQ;;AAGrB,MAAM,eAAwB,OAAO,OAAO,SAAS;CACnD,MAAM,WAAW,kBAAkB;CACnC,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,CAAC,IACH,QAAO,SAAS,OAAO,KAAK;CAM9B,IAAI;AACJ,KAAI;AACF,YAAU,IAAI,QAAQ,OAAO,KAAK;SAC5B;AAEN,SAAO,SAAS,OAAO,KAAK;;AAK9B,KAAI,QAAQ,IAAI,WAAW,GAAG,gBAAgB,IAAI,CAAC,GAAG,CACpD,QAAO,SAAS,QAAQ;AAG1B,QAAO,IAAI,SAAS,WAChB,mBAAmB,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC,GACxD,oBAAoB,KAAK,EAAE,MAAM,YAAY,EAAE,UAAU,QACvD,SAAS,IAAI,CACd","debugId":"006811cf-e9b2-5a53-b257-639036ac8912"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, SidecarFetcher, SidecarTransport } from "./sidecar-transport.js";
2
2
  import { MeticulousExecutionContext, WithMeticulousOptions } from "./with-meticulous.js";
3
- import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
3
+ import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, CoverageReportRequest, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER } from "./protocol.js";
4
+ import { WORKERD_SHIM_VERSION } from "./version.js";
4
5
  import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
5
6
  import { headersToRecord } from "./outbound-capture.js";
6
7
  import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
8
+ import { CoverageMap, CoverageMapFile } from "./coverage/coverage.types.js";
9
+ import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap, getUnreportedCoverageFiles, markCoverageFilesReported, registerCoverageFile } from "./coverage/runtime.js";
7
10
  import { MeticulousPagesFunction, MeticulousPagesFunctionContext, withMeticulousPagesFunction } from "./pages-function.js";
8
11
  import { KvCaptureFields, KvCaptureOutcome, serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
9
12
  import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
@@ -82,5 +85,5 @@ declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler
82
85
  */
83
86
  declare const getMeticulousSessionId: () => string | undefined;
84
87
  //#endregion
85
- 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, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, type HeaderReader, 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 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_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, 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, type WithMeticulousOptions, buildServerTimingSessionEntry, deserializeCapturedError, getMeticulousSessionId, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
88
+ 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, 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 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, 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, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
86
89
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
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]="f420c8b0-634e-586e-b9bf-34d3afde7944")}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]="22e3bcf8-6298-5815-a54c-39716473b677")}catch(e){}}();
3
3
  import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY } from "./sidecar-transport.js";
4
4
  import { requestCaptureContext } from "./context.js";
5
5
  import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
6
6
  import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
7
- import { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
7
+ import { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER } from "./protocol.js";
8
8
  import { headersToRecord } from "./outbound-capture.js";
9
+ import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap, getUnreportedCoverageFiles, markCoverageFilesReported, registerCoverageFile } from "./coverage/runtime.js";
9
10
  import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
10
11
  import { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
12
+ import { WORKERD_SHIM_VERSION } from "./version.js";
11
13
  import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
12
14
  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";
13
15
  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";
@@ -91,7 +93,7 @@ const withMeticulous = (handler, options) => {
91
93
  */
92
94
  const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
93
95
  //#endregion
94
- 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, 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_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, 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, buildServerTimingSessionEntry, deserializeCapturedError, getMeticulousSessionId, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
96
+ 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, 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, 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, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
95
97
 
96
98
  //# sourceMappingURL=index.js.map
97
- //# debugId=f420c8b0-634e-586e-b9bf-34d3afde7944
99
+ //# debugId=22e3bcf8-6298-5815-a54c-39716473b677
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","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 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 REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\nexport {\n MAX_BODY_CAPTURE_SIZE,\n readBodyWithCap,\n readRequestBodyWithCap,\n} from \"./body-capture\";\nexport { headersToRecord } from \"./outbound-capture\";\nexport { redactRequestBody, STR_REDACTED } from \"./redact-body\";\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"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+KA,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","debugId":"f420c8b0-634e-586e-b9bf-34d3afde7944"}
1
+ {"version":3,"file":"index.js","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 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 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\";\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"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiMA,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","debugId":"22e3bcf8-6298-5815-a54c-39716473b677"}
@@ -1,5 +1,5 @@
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]="8c90b03e-9d0f-5403-a848-7872052017fa")}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]="9217cc0a-7c17-5ee8-9599-7faf9bd59bf8")}catch(e){}}();
3
3
  import { redactRequestBody } from "./redact-body.js";
4
4
  import { MAX_BODY_CAPTURE_SIZE } from "./body-capture.js";
5
5
  //#region src/kv-capture.ts
@@ -89,4 +89,4 @@ const capture = (json) => json.length > 262144 ? {
89
89
  export { serializeKvArgs, serializeKvCaptureFields };
90
90
 
91
91
  //# sourceMappingURL=kv-capture.js.map
92
- //# debugId=8c90b03e-9d0f-5403-a848-7872052017fa
92
+ //# debugId=9217cc0a-7c17-5ee8-9599-7faf9bd59bf8
@@ -1 +1 @@
1
- {"version":3,"file":"kv-capture.js","sources":["../src/kv-capture.ts"],"sourcesContent":["import { MAX_BODY_CAPTURE_SIZE } from \"./body-capture\";\nimport type { CapturedBody, KvOmittedReason, KvOperation } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * How a KV operation's key, arguments and value are turned into persistable fields.\n *\n * This lives apart from `kv-patch.ts` because it is not workerd-specific: the same operations\n * are recorded from a **Node** process whose bindings come from wrangler's `getPlatformProxy`\n * (`withMeticulousCloudflareEnv` in `@alwaysmeticulous/backend-recorder-js`), and the two\n * surfaces have to produce byte-identical fields or a session recorded through one cannot be\n * replayed through the other. So the contract is defined once, here, and both call it.\n */\n\ninterface SerializedKvValue {\n captured?: CapturedBody;\n omitted?: KvOmittedReason;\n}\n\n/** The outcome of a KV operation: what it returned, or how it failed. */\nexport type KvCaptureOutcome = { result: unknown } | { error: string };\n\n/** The persistable fields of one KV operation, as `KvOperationEvent` carries them. */\nexport interface KvCaptureFields {\n /** The key operated on, when it is a single string. Absent for `list` and a bulk `get`. */\n key?: string;\n /** JSON of the call's arguments, with a `put`'s value slot nulled out. */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, redacted. */\n value?: CapturedBody;\n /** JSON of what the operation returned. Absent for `put`/`delete` and for an error. */\n result?: CapturedBody;\n /** Why a value is missing. Refers to the written value for `put`, the read value otherwise. */\n omitted?: KvOmittedReason;\n}\n\n/**\n * Serializes one KV operation's fields. Never throws: a value that cannot be serialized is\n * simply absent, so the operation is still recorded and only its payload is missing.\n */\nexport const serializeKvCaptureFields = (\n operation: KvOperation,\n args: unknown[],\n outcome: KvCaptureOutcome,\n): KvCaptureFields => {\n const [key] = args;\n const written =\n operation === \"put\" ? serializeWrittenValue(args[1]) : undefined;\n const read =\n \"result\" in outcome ? serializeKvValue(outcome.result) : undefined;\n const omitted = written?.omitted ?? read?.omitted;\n const serializedArgs = serializeKvArgs(operation, args);\n\n return {\n ...(typeof key === \"string\" ? { key } : {}),\n ...(serializedArgs !== undefined ? { args: serializedArgs } : {}),\n ...(written?.captured !== undefined ? { value: written.captured } : {}),\n ...(read?.captured !== undefined ? { result: read.captured } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n};\n\n/**\n * JSON of a value the runtime returned, with anything unpersistable replaced by `null` and the\n * reason reported alongside. A `Map` (what a bulk `get` returns) becomes a plain object, which\n * `JSON.stringify` would otherwise flatten to `{}`.\n *\n * Read values are stored verbatim, deliberately: replay serves them straight back to the app,\n * so redacting one would corrupt what the app receives. Only the write path\n * ({@link serializeWrittenValue}) is redacted — nothing ever replays a `put`.\n */\nconst serializeKvValue = (value: unknown): SerializedKvValue => {\n let omitted: KvOmittedReason | undefined;\n try {\n const json = JSON.stringify(value, (_key: string, nested: unknown) => {\n const reason = omittedReason(nested);\n if (reason !== undefined) {\n omitted ??= reason;\n return null;\n }\n return nested instanceof Map\n ? Object.fromEntries(nested as Map<string, unknown>)\n : nested;\n });\n return {\n // `undefined` (what `put`/`delete` resolve to) has no JSON form — nothing to capture.\n ...(json !== undefined ? { captured: capture(json) } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n } catch {\n // Circular, or a getter that throws. The operation itself is still recorded.\n return {};\n }\n};\n\n/**\n * JSON of the value a `put` wrote, redacted like a request body. KV takes a string, an\n * ArrayBuffer(View) or a stream; only the string form can be captured, and it is redacted as\n * text before being JSON-encoded so that secret-looking fields *inside* the app's payload are\n * caught (redacting the JSON-encoded form would only ever see one opaque string).\n */\nconst serializeWrittenValue = (value: unknown): SerializedKvValue => {\n if (typeof value === \"string\") {\n try {\n return { captured: capture(JSON.stringify(redactRequestBody(value))) };\n } catch {\n return {};\n }\n }\n return serializeKvValue(value);\n};\n\n/**\n * JSON of the call's arguments. A `put` value is replaced by `null` rather than dropped, so the\n * remaining arguments keep their positions — and so the value is only ever persisted via the\n * redacted {@link serializeWrittenValue} path.\n *\n * Exported because it is a KV operation's whole identity: a replay derives its lookup key from the\n * live arguments and compares it against this, so the two must be produced by one function.\n */\nexport const serializeKvArgs = (\n operation: KvOperation,\n args: unknown[],\n): CapturedBody | undefined => {\n const withoutValue =\n operation === \"put\"\n ? args.map((arg, index) => (index === 1 ? null : arg))\n : args;\n return serializeKvValue(withoutValue).captured;\n};\n\nconst omittedReason = (value: unknown): KvOmittedReason | undefined => {\n // A stream is never read, only recognised: reading it would consume the app's bytes.\n if (\n typeof ReadableStream !== \"undefined\" &&\n value instanceof ReadableStream\n ) {\n return \"stream\";\n }\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {\n return \"binary\";\n }\n return undefined;\n};\n\nconst capture = (json: string): CapturedBody =>\n json.length > MAX_BODY_CAPTURE_SIZE\n ? { body: json.slice(0, MAX_BODY_CAPTURE_SIZE), truncated: true }\n : { body: json, truncated: false };\n"],"names":[],"mappings":";;;;;;;;;AAwCA,MAAa,4BACX,WACA,MACA,YACoB;CACpB,MAAM,CAAC,OAAO;CACd,MAAM,UACJ,cAAc,QAAQ,sBAAsB,KAAK,GAAG,GAAG,KAAA;CACzD,MAAM,OACJ,YAAY,UAAU,iBAAiB,QAAQ,OAAO,GAAG,KAAA;CAC3D,MAAM,UAAU,SAAS,WAAW,MAAM;CAC1C,MAAM,iBAAiB,gBAAgB,WAAW,KAAK;AAEvD,QAAO;EACL,GAAI,OAAO,QAAQ,WAAW,EAAE,KAAK,GAAG,EAAE;EAC1C,GAAI,mBAAmB,KAAA,IAAY,EAAE,MAAM,gBAAgB,GAAG,EAAE;EAChE,GAAI,SAAS,aAAa,KAAA,IAAY,EAAE,OAAO,QAAQ,UAAU,GAAG,EAAE;EACtE,GAAI,MAAM,aAAa,KAAA,IAAY,EAAE,QAAQ,KAAK,UAAU,GAAG,EAAE;EACjE,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;EAC7C;;;;;;;;;;;AAYH,MAAM,oBAAoB,UAAsC;CAC9D,IAAI;AACJ,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,QAAQ,MAAc,WAAoB;GACpE,MAAM,SAAS,cAAc,OAAO;AACpC,OAAI,WAAW,KAAA,GAAW;AACxB,gBAAY;AACZ,WAAO;;AAET,UAAO,kBAAkB,MACrB,OAAO,YAAY,OAA+B,GAClD;IACJ;AACF,SAAO;GAEL,GAAI,SAAS,KAAA,IAAY,EAAE,UAAU,QAAQ,KAAK,EAAE,GAAG,EAAE;GACzD,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;GAC7C;SACK;AAEN,SAAO,EAAE;;;;;;;;;AAUb,MAAM,yBAAyB,UAAsC;AACnE,KAAI,OAAO,UAAU,SACnB,KAAI;AACF,SAAO,EAAE,UAAU,QAAQ,KAAK,UAAU,kBAAkB,MAAM,CAAC,CAAC,EAAE;SAChE;AACN,SAAO,EAAE;;AAGb,QAAO,iBAAiB,MAAM;;;;;;;;;;AAWhC,MAAa,mBACX,WACA,SAC6B;AAK7B,QAAO,iBAHL,cAAc,QACV,KAAK,KAAK,KAAK,UAAW,UAAU,IAAI,OAAO,IAAK,GACpD,KAC+B,CAAC;;AAGxC,MAAM,iBAAiB,UAAgD;AAErE,KACE,OAAO,mBAAmB,eAC1B,iBAAiB,eAEjB,QAAO;AAET,KAAI,iBAAiB,eAAe,YAAY,OAAO,MAAM,CAC3D,QAAO;;AAKX,MAAM,WAAW,SACf,KAAK,SAAA,SACD;CAAE,MAAM,KAAK,MAAM,GAAG,sBAAsB;CAAE,WAAW;CAAM,GAC/D;CAAE,MAAM;CAAM,WAAW;CAAO","debugId":"8c90b03e-9d0f-5403-a848-7872052017fa"}
1
+ {"version":3,"file":"kv-capture.js","sources":["../src/kv-capture.ts"],"sourcesContent":["import { MAX_BODY_CAPTURE_SIZE } from \"./body-capture\";\nimport type { CapturedBody, KvOmittedReason, KvOperation } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * How a KV operation's key, arguments and value are turned into persistable fields.\n *\n * This lives apart from `kv-patch.ts` because it is not workerd-specific: the same operations\n * are recorded from a **Node** process whose bindings come from wrangler's `getPlatformProxy`\n * (`withMeticulousCloudflareEnv` in `@alwaysmeticulous/backend-recorder-js`), and the two\n * surfaces have to produce byte-identical fields or a session recorded through one cannot be\n * replayed through the other. So the contract is defined once, here, and both call it.\n */\n\ninterface SerializedKvValue {\n captured?: CapturedBody;\n omitted?: KvOmittedReason;\n}\n\n/** The outcome of a KV operation: what it returned, or how it failed. */\nexport type KvCaptureOutcome = { result: unknown } | { error: string };\n\n/** The persistable fields of one KV operation, as `KvOperationEvent` carries them. */\nexport interface KvCaptureFields {\n /** The key operated on, when it is a single string. Absent for `list` and a bulk `get`. */\n key?: string;\n /** JSON of the call's arguments, with a `put`'s value slot nulled out. */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, redacted. */\n value?: CapturedBody;\n /** JSON of what the operation returned. Absent for `put`/`delete` and for an error. */\n result?: CapturedBody;\n /** Why a value is missing. Refers to the written value for `put`, the read value otherwise. */\n omitted?: KvOmittedReason;\n}\n\n/**\n * Serializes one KV operation's fields. Never throws: a value that cannot be serialized is\n * simply absent, so the operation is still recorded and only its payload is missing.\n */\nexport const serializeKvCaptureFields = (\n operation: KvOperation,\n args: unknown[],\n outcome: KvCaptureOutcome,\n): KvCaptureFields => {\n const [key] = args;\n const written =\n operation === \"put\" ? serializeWrittenValue(args[1]) : undefined;\n const read =\n \"result\" in outcome ? serializeKvValue(outcome.result) : undefined;\n const omitted = written?.omitted ?? read?.omitted;\n const serializedArgs = serializeKvArgs(operation, args);\n\n return {\n ...(typeof key === \"string\" ? { key } : {}),\n ...(serializedArgs !== undefined ? { args: serializedArgs } : {}),\n ...(written?.captured !== undefined ? { value: written.captured } : {}),\n ...(read?.captured !== undefined ? { result: read.captured } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n};\n\n/**\n * JSON of a value the runtime returned, with anything unpersistable replaced by `null` and the\n * reason reported alongside. A `Map` (what a bulk `get` returns) becomes a plain object, which\n * `JSON.stringify` would otherwise flatten to `{}`.\n *\n * Read values are stored verbatim, deliberately: replay serves them straight back to the app,\n * so redacting one would corrupt what the app receives. Only the write path\n * ({@link serializeWrittenValue}) is redacted — nothing ever replays a `put`.\n */\nconst serializeKvValue = (value: unknown): SerializedKvValue => {\n let omitted: KvOmittedReason | undefined;\n try {\n const json = JSON.stringify(value, (_key: string, nested: unknown) => {\n const reason = omittedReason(nested);\n if (reason !== undefined) {\n omitted ??= reason;\n return null;\n }\n return nested instanceof Map\n ? Object.fromEntries(nested as Map<string, unknown>)\n : nested;\n });\n return {\n // `undefined` (what `put`/`delete` resolve to) has no JSON form — nothing to capture.\n ...(json !== undefined ? { captured: capture(json) } : {}),\n ...(omitted !== undefined ? { omitted } : {}),\n };\n } catch {\n // Circular, or a getter that throws. The operation itself is still recorded.\n return {};\n }\n};\n\n/**\n * JSON of the value a `put` wrote, redacted like a request body. KV takes a string, an\n * ArrayBuffer(View) or a stream; only the string form can be captured, and it is redacted as\n * text before being JSON-encoded so that secret-looking fields *inside* the app's payload are\n * caught (redacting the JSON-encoded form would only ever see one opaque string).\n */\nconst serializeWrittenValue = (value: unknown): SerializedKvValue => {\n if (typeof value === \"string\") {\n try {\n return { captured: capture(JSON.stringify(redactRequestBody(value))) };\n } catch {\n return {};\n }\n }\n return serializeKvValue(value);\n};\n\n/**\n * JSON of the call's arguments. A `put` value is replaced by `null` rather than dropped, so the\n * remaining arguments keep their positions — and so the value is only ever persisted via the\n * redacted {@link serializeWrittenValue} path.\n *\n * Exported because it is a KV operation's whole identity: a replay derives its lookup key from the\n * live arguments and compares it against this, so the two must be produced by one function.\n */\nexport const serializeKvArgs = (\n operation: KvOperation,\n args: unknown[],\n): CapturedBody | undefined => {\n const withoutValue =\n operation === \"put\"\n ? args.map((arg, index) => (index === 1 ? null : arg))\n : args;\n return serializeKvValue(withoutValue).captured;\n};\n\nconst omittedReason = (value: unknown): KvOmittedReason | undefined => {\n // A stream is never read, only recognised: reading it would consume the app's bytes.\n if (\n typeof ReadableStream !== \"undefined\" &&\n value instanceof ReadableStream\n ) {\n return \"stream\";\n }\n if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {\n return \"binary\";\n }\n return undefined;\n};\n\nconst capture = (json: string): CapturedBody =>\n json.length > MAX_BODY_CAPTURE_SIZE\n ? { body: json.slice(0, MAX_BODY_CAPTURE_SIZE), truncated: true }\n : { body: json, truncated: false };\n"],"names":[],"mappings":";;;;;;;;;AAwCA,MAAa,4BACX,WACA,MACA,YACoB;CACpB,MAAM,CAAC,OAAO;CACd,MAAM,UACJ,cAAc,QAAQ,sBAAsB,KAAK,GAAG,GAAG,KAAA;CACzD,MAAM,OACJ,YAAY,UAAU,iBAAiB,QAAQ,OAAO,GAAG,KAAA;CAC3D,MAAM,UAAU,SAAS,WAAW,MAAM;CAC1C,MAAM,iBAAiB,gBAAgB,WAAW,KAAK;AAEvD,QAAO;EACL,GAAI,OAAO,QAAQ,WAAW,EAAE,KAAK,GAAG,EAAE;EAC1C,GAAI,mBAAmB,KAAA,IAAY,EAAE,MAAM,gBAAgB,GAAG,EAAE;EAChE,GAAI,SAAS,aAAa,KAAA,IAAY,EAAE,OAAO,QAAQ,UAAU,GAAG,EAAE;EACtE,GAAI,MAAM,aAAa,KAAA,IAAY,EAAE,QAAQ,KAAK,UAAU,GAAG,EAAE;EACjE,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;EAC7C;;;;;;;;;;;AAYH,MAAM,oBAAoB,UAAsC;CAC9D,IAAI;AACJ,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,QAAQ,MAAc,WAAoB;GACpE,MAAM,SAAS,cAAc,OAAO;AACpC,OAAI,WAAW,KAAA,GAAW;AACxB,gBAAY;AACZ,WAAO;;AAET,UAAO,kBAAkB,MACrB,OAAO,YAAY,OAA+B,GAClD;IACJ;AACF,SAAO;GAEL,GAAI,SAAS,KAAA,IAAY,EAAE,UAAU,QAAQ,KAAK,EAAE,GAAG,EAAE;GACzD,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;GAC7C;SACK;AAEN,SAAO,EAAE;;;;;;;;;AAUb,MAAM,yBAAyB,UAAsC;AACnE,KAAI,OAAO,UAAU,SACnB,KAAI;AACF,SAAO,EAAE,UAAU,QAAQ,KAAK,UAAU,kBAAkB,MAAM,CAAC,CAAC,EAAE;SAChE;AACN,SAAO,EAAE;;AAGb,QAAO,iBAAiB,MAAM;;;;;;;;;;AAWhC,MAAa,mBACX,WACA,SAC6B;AAK7B,QAAO,iBAHL,cAAc,QACV,KAAK,KAAK,KAAK,UAAW,UAAU,IAAI,OAAO,IAAK,GACpD,KAC+B,CAAC;;AAGxC,MAAM,iBAAiB,UAAgD;AAErE,KACE,OAAO,mBAAmB,eAC1B,iBAAiB,eAEjB,QAAO;AAET,KAAI,iBAAiB,eAAe,YAAY,OAAO,MAAM,CAC3D,QAAO;;AAKX,MAAM,WAAW,SACf,KAAK,SAAA,SACD;CAAE,MAAM,KAAK,MAAM,GAAG,sBAAsB;CAAE,WAAW;CAAM,GAC/D;CAAE,MAAM;CAAM,WAAW;CAAO","debugId":"9217cc0a-7c17-5ee8-9599-7faf9bd59bf8"}
package/dist/kv-patch.js CHANGED
@@ -1,5 +1,5 @@
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]="303b51b3-97aa-5dde-9000-ea94bcd5a2de")}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
5
  import { serializeKvCaptureFields } from "./kv-capture.js";
@@ -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=303b51b3-97aa-5dde-9000-ea94bcd5a2de
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 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":";;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,aAAa,OAAO,IAAI,+BAA+B;;;;;;;;AAS7D,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,MAAM,iCAAiB,IAAI,SAAyB;;;;;;;;AAuBpD,MAAa,kBACX,KACA,YACS;AACT,KAAI;AACF,OAAK,MAAM,EAAE,MAAM,OAAO,UAAU,WAClC,KACA,SAAS,cACT,SAAS,cACV,EAAE;AAID,kBAAe,MAAM;AACrB,OAAI,CAAC,KACH,gBAAe,IAAI,OAAO,KAAK;;UAG5B,OAAO;AACd,WACE,oBACA,mFACA,MACD;;;AAUL,MAAM,cACJ,KACA,cACA,kBAC0B;AAC1B,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,QAAO,EAAE;CAEX,MAAM,UAAU,IAAI,IAAY,gBAAgB,EAAE,CAAC;CACnD,MAAM,mBAAmB,IAAI,IAAY,iBAAiB,EAAE,CAAC;CAC7D,MAAM,QAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,CAC7C,KAAI,kBAAkB,MAAM,CAC1B,OAAM,KAAK;EACT;EACA;EACA,MAAM,QAAQ,IAAI,KAAK,IAAI,iBAAiB,IAAI,MAAM;EACvD,CAAC;AAGN,QAAO;;AAGT,MAAM,qBAAqB,UACzB,UAAU,QACV,OAAO,UAAU,YACjB,oBAAoB,OACjB,WACC,OAAQ,MAAkC,YAAY,WACzD;;;;;;;;;;;AAYH,MAAM,kBAAkB,YAA0B;CAChD,MAAM,yBAAS,IAAI,KAA+B;AAClD,MAAK,MAAM,UAAU,qBAAqB;EACxC,MAAM,QAAQ,gBAAgB,SAAS,OAAO;AAC9C,MAAI,UAAU,KAAA,GAAW;AAGvB,YACE,wBACA,iDAAiD,OAAO,0CACzD;AACD;;AAEF,SAAO,IAAI,QAAQ,MAAM;;AAG3B,MAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,eAAe,QAAQ;AAClD,MAAK,SAAgD,gBAAgB,KACnE;AAEF,SAAO,eAAe,OAAO,QAAQ;GACnC,OAAO,gBAAgB,QAAQ,SAAS;GACxC,UAAU;GACV,YAAY;GACZ,cAAc;GACf,CAAC;;;;;;;;AAcN,MAAM,mBACJ,UACA,WAC4B;AAC5B,KAAI,OAAO,OAAO,UAAU,OAAO,CAEjC;CAEF,IAAI,UAAyB,OAAO,eAAe,SAAS;AAC5D,QAAO,YAAY,MAAM;EACvB,MAAM,aAAa,OAAO,yBAAyB,SAAS,OAAO;AACnE,MAAI,eAAe,KAAA,EACjB,QAAO,OAAO,WAAW,UAAU,cACjC,WAAW,iBAAiB,OAC1B;GAAE,OAAO;GAAS,UAAU,WAAW;GAAmB,GAC1D,KAAA;AAEN,YAAU,OAAO,eAAe,QAAQ;;;;;;;AAS5C,MAAM,mBACJ,WACA,aACa;CACb,MAAM,UAAU,SAEd,GAAG,MACe;EAClB,MAAM,oBAAsC,SAAS,MAAM,MAAM,KAAK;EAEtE,MAAM,MAAM,sBAAsB,UAAU;AAI5C,MAAI,CAAC,OAAO,IAAI,SAAS,SACvB,QAAO,aAAa;EAEtB,MAAM,WACJ,OAAO,SAAS,YAAY,SAAS,OAAO,OAAO,KAAA;EAErD,MAAM,cACJ,aAAa,KAAA,IAAY,KAAA,IAAY,eAAe,IAAI,SAAS;AACnE,MAAI,gBAAgB,KAAA,EAClB,QAAO,aAAa;AAGtB,SAAO,mBAAmB,KAAK,WAAW,aAAa,MAAM,YAAY;;AAE3E,QAAO,eAAe,SAAS,YAAY;EACzC,OAAO;EACP,YAAY;EACZ,cAAc;EACf,CAAC;AACF,QAAO;;;;;;;AAQT,MAAM,qBAAqB,OACzB,KACA,WACA,aACA,MACA,WACqB;CACrB,MAAM,cAAc,KAAK,KAAK;CAC9B,IAAI;AACJ,KAAI;AACF,WAAS,MAAM,QAAQ;UAChB,OAAO;AACd,SAAO,KAAK,WAAW,aAAa,MAAM,aAAa,EACrD,OAAO,OAAO,MAAM,EACrB,CAAC;AACF,QAAM;;AAER,QAAO,KAAK,WAAW,aAAa,MAAM,aAAa,EAAE,QAAQ,CAAC;AAClE,QAAO;;AAGT,MAAM,UACJ,KACA,WACA,aACA,MACA,aACA,YACS;AACT,KAAI;AACF,MAAI,OAAO,IACT,aAAa,KAAK,WAAW,aAAa,MAAM,aAAa,QAAQ,CACtE;UACM,OAAO;AACd,WAAS,aAAa,oCAAoC,MAAM;;;AAIpE,MAAM,gBACJ,KACA,WACA,aACA,MACA,aACA,aACsB;CACtB,MAAM;CACN,WAAW,IAAI;CACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;CACN,SAAS,IAAI;CACb,cAAc,IAAI;CAClB;CACA;CACA,GAAG,yBAAyB,WAAW,MAAM,QAAQ;CACrD;CACA,WAAW,KAAK,KAAK;CACrB,GAAI,WAAW,UAAU,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;CACvD","debugId":"303b51b3-97aa-5dde-9000-ea94bcd5a2de"}
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":";;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,aAAa,OAAO,IAAI,+BAA+B;;;;;;;;AAS7D,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,MAAM,iCAAiB,IAAI,SAAyB;;;;;;;;AAuBpD,MAAa,kBACX,KACA,YACS;AACT,KAAI;AACF,OAAK,MAAM,EAAE,MAAM,OAAO,UAAU,WAClC,KACA,SAAS,cACT,SAAS,cACV,EAAE;AAID,kBAAe,MAAM;AACrB,OAAI,CAAC,KACH,gBAAe,IAAI,OAAO,KAAK;;UAG5B,OAAO;AACd,WACE,oBACA,mFACA,MACD;;;AAUL,MAAM,cACJ,KACA,cACA,kBAC0B;AAC1B,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,QAAO,EAAE;CAEX,MAAM,UAAU,IAAI,IAAY,gBAAgB,EAAE,CAAC;CACnD,MAAM,mBAAmB,IAAI,IAAY,iBAAiB,EAAE,CAAC;CAC7D,MAAM,QAA+B,EAAE;AACvC,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,CAC7C,KAAI,kBAAkB,MAAM,CAC1B,OAAM,KAAK;EACT;EACA;EACA,MAAM,QAAQ,IAAI,KAAK,IAAI,iBAAiB,IAAI,MAAM;EACvD,CAAC;AAGN,QAAO;;AAGT,MAAM,qBAAqB,UACzB,UAAU,QACV,OAAO,UAAU,YACjB,oBAAoB,OACjB,WACC,OAAQ,MAAkC,YAAY,WACzD;;;;;;;;;;;AAYH,MAAM,kBAAkB,YAA0B;CAChD,MAAM,yBAAS,IAAI,KAA+B;AAClD,MAAK,MAAM,UAAU,qBAAqB;EACxC,MAAM,QAAQ,gBAAgB,SAAS,OAAO;AAC9C,MAAI,UAAU,KAAA,GAAW;AAGvB,YACE,wBACA,iDAAiD,OAAO,0CACzD;AACD;;AAEF,SAAO,IAAI,QAAQ,MAAM;;AAG3B,MAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,eAAe,QAAQ;AAClD,MAAK,SAAgD,gBAAgB,KACnE;AAEF,SAAO,eAAe,OAAO,QAAQ;GACnC,OAAO,gBAAgB,QAAQ,SAAS;GACxC,UAAU;GACV,YAAY;GACZ,cAAc;GACf,CAAC;;;;;;;;AAcN,MAAM,mBACJ,UACA,WAC4B;AAC5B,KAAI,OAAO,OAAO,UAAU,OAAO,CAEjC;CAEF,IAAI,UAAyB,OAAO,eAAe,SAAS;AAC5D,QAAO,YAAY,MAAM;EACvB,MAAM,aAAa,OAAO,yBAAyB,SAAS,OAAO;AACnE,MAAI,eAAe,KAAA,EACjB,QAAO,OAAO,WAAW,UAAU,cACjC,WAAW,iBAAiB,OAC1B;GAAE,OAAO;GAAS,UAAU,WAAW;GAAmB,GAC1D,KAAA;AAEN,YAAU,OAAO,eAAe,QAAQ;;;;;;;AAS5C,MAAM,mBACJ,WACA,aACa;CACb,MAAM,UAAU,SAEd,GAAG,MACe;EAClB,MAAM,oBAAsC,SAAS,MAAM,MAAM,KAAK;EAEtE,MAAM,MAAM,sBAAsB,UAAU;AAI5C,MAAI,CAAC,OAAO,IAAI,SAAS,SACvB,QAAO,aAAa;EAEtB,MAAM,WACJ,OAAO,SAAS,YAAY,SAAS,OAAO,OAAO,KAAA;EAErD,MAAM,cACJ,aAAa,KAAA,IAAY,KAAA,IAAY,eAAe,IAAI,SAAS;AACnE,MAAI,gBAAgB,KAAA,EAClB,QAAO,aAAa;AAGtB,SAAO,mBAAmB,KAAK,WAAW,aAAa,MAAM,YAAY;;AAE3E,QAAO,eAAe,SAAS,YAAY;EACzC,OAAO;EACP,YAAY;EACZ,cAAc;EACf,CAAC;AACF,QAAO;;;;;;;AAQT,MAAM,qBAAqB,OACzB,KACA,WACA,aACA,MACA,WACqB;CACrB,MAAM,cAAc,KAAK,KAAK;CAC9B,IAAI;AACJ,KAAI;AACF,WAAS,MAAM,QAAQ;UAChB,OAAO;AACd,SAAO,KAAK,WAAW,aAAa,MAAM,aAAa,EACrD,OAAO,OAAO,MAAM,EACrB,CAAC;AACF,QAAM;;AAER,QAAO,KAAK,WAAW,aAAa,MAAM,aAAa,EAAE,QAAQ,CAAC;AAClE,QAAO;;AAGT,MAAM,UACJ,KACA,WACA,aACA,MACA,aACA,YACS;AACT,KAAI;AACF,MAAI,OAAO,IACT,aAAa,KAAK,WAAW,aAAa,MAAM,aAAa,QAAQ,CACtE;UACM,OAAO;AACd,WAAS,aAAa,oCAAoC,MAAM;;;AAIpE,MAAM,gBACJ,KACA,WACA,aACA,MACA,aACA,aACsB;CACtB,MAAM;CACN,WAAW,IAAI;CACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;CACN,SAAS,IAAI;CACb,cAAc,IAAI;CAClB;CACA;CACA,GAAG,yBAAyB,WAAW,MAAM,QAAQ;CACrD;CACA,WAAW,KAAK,KAAK;CACrB,GAAI,WAAW,UAAU,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;CACvD","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,5 +1,5 @@
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]="9a4a84d0-427b-51fb-af59-6197c25b1671")}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
5
  import { CAPTURED_HEADERS } from "./protocol.js";
@@ -116,4 +116,4 @@ const hasWebSocket = (response) => response.webSocket != null;
116
116
  export { captureOutboundCall, hasWebSocket, headersToRecord };
117
117
 
118
118
  //# sourceMappingURL=outbound-capture.js.map
119
- //# debugId=9a4a84d0-427b-51fb-af59-6197c25b1671
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 {\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":"9a4a84d0-427b-51fb-af59-6197c25b1671"}
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"}
@@ -1,5 +1,5 @@
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]="592c70e5-3859-578f-bf7d-0dfc15e165ab")}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]="78b550d0-f18f-5c3c-b576-7cef30ecadc9")}catch(e){}}();
3
3
  import { runWithMeticulous } from "./with-meticulous.js";
4
4
  //#region src/pages-function.ts
5
5
  /**
@@ -25,4 +25,4 @@ const withMeticulousPagesFunction = (handler, options) => (context) => runWithMe
25
25
  export { withMeticulousPagesFunction };
26
26
 
27
27
  //# sourceMappingURL=pages-function.js.map
28
- //# debugId=592c70e5-3859-578f-bf7d-0dfc15e165ab
28
+ //# debugId=78b550d0-f18f-5c3c-b576-7cef30ecadc9
@@ -1 +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":"592c70e5-3859-578f-bf7d-0dfc15e165ab"}
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"}
@@ -1,5 +1,5 @@
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]="d01c8a65-211a-58f1-b35e-1ac030b75ece")}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]="b2626960-f48d-5500-a2b3-a8c9fbaf2002")}catch(e){}}();
3
3
  import { serializeCapturedError } from "../error-capture.js";
4
4
  //#region src/postgres/capture.ts
5
5
  /**
@@ -133,4 +133,4 @@ const isSupportedPostgresJsQuery = (query) => reconstructQueryText(query.strings
133
133
  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, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult };
134
134
 
135
135
  //# sourceMappingURL=capture.js.map
136
- //# debugId=d01c8a65-211a-58f1-b35e-1ac030b75ece
136
+ //# debugId=b2626960-f48d-5500-a2b3-a8c9fbaf2002
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","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"],"names":[],"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","debugId":"d01c8a65-211a-58f1-b35e-1ac030b75ece"}
1
+ {"version":3,"file":"capture.js","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"],"names":[],"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","debugId":"b2626960-f48d-5500-a2b3-a8c9fbaf2002"}
@@ -1,5 +1,5 @@
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]="7437e682-9879-55d9-aa51-34f2920d26d3")}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]="bcb30e85-33e1-52b2-9e08-5dd84f66d61c")}catch(e){}}();
3
3
  import { requestCaptureContext } from "../context.js";
4
4
  import { warnOnce } from "../log.js";
5
5
  import { MAX_POSTGRES_JS_RESULT_SIZE, isSupportedPostgresJsQuery, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./capture.js";
@@ -149,4 +149,4 @@ const captureQuery = (ctx, query) => {
149
149
  export { withMeticulousPostgres };
150
150
 
151
151
  //# sourceMappingURL=with-meticulous-postgres.js.map
152
- //# debugId=7437e682-9879-55d9-aa51-34f2920d26d3
152
+ //# debugId=bcb30e85-33e1-52b2-9e08-5dd84f66d61c