@alwaysmeticulous/backend-recorder-workerd 2.331.3 → 2.333.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +15 -0
  2. package/dist/binding-patch.js +1 -4
  3. package/dist/binding-patch.js.map +1 -1
  4. package/dist/body-capture.js +1 -4
  5. package/dist/body-capture.js.map +1 -1
  6. package/dist/capture-buffer.js +1 -4
  7. package/dist/capture-buffer.js.map +1 -1
  8. package/dist/context.js +1 -4
  9. package/dist/context.js.map +1 -1
  10. package/dist/coverage/replay-coverage.js +1 -4
  11. package/dist/coverage/replay-coverage.js.map +1 -1
  12. package/dist/coverage/runtime.js +1 -4
  13. package/dist/coverage/runtime.js.map +1 -1
  14. package/dist/error-capture.js +1 -4
  15. package/dist/error-capture.js.map +1 -1
  16. package/dist/fetch-patch.js +1 -4
  17. package/dist/fetch-patch.js.map +1 -1
  18. package/dist/index.js +1 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/kv-capture.js +1 -4
  21. package/dist/kv-capture.js.map +1 -1
  22. package/dist/kv-patch.js +1 -4
  23. package/dist/kv-patch.js.map +1 -1
  24. package/dist/log.js +1 -4
  25. package/dist/log.js.map +1 -1
  26. package/dist/original-fetch.js +1 -4
  27. package/dist/original-fetch.js.map +1 -1
  28. package/dist/outbound-capture.js +1 -4
  29. package/dist/outbound-capture.js.map +1 -1
  30. package/dist/pages-function.js +1 -4
  31. package/dist/pages-function.js.map +1 -1
  32. package/dist/postgres/capture.js +1 -4
  33. package/dist/postgres/capture.js.map +1 -1
  34. package/dist/postgres/with-meticulous-postgres.js +1 -4
  35. package/dist/postgres/with-meticulous-postgres.js.map +1 -1
  36. package/dist/protocol.js +1 -4
  37. package/dist/protocol.js.map +1 -1
  38. package/dist/provisional-session-id.js +1 -4
  39. package/dist/provisional-session-id.js.map +1 -1
  40. package/dist/publish-session-id.js +1 -4
  41. package/dist/publish-session-id.js.map +1 -1
  42. package/dist/publish-shim-version.js +1 -4
  43. package/dist/publish-shim-version.js.map +1 -1
  44. package/dist/redact-body.js +1 -4
  45. package/dist/redact-body.js.map +1 -1
  46. package/dist/replay-fetch.js +1 -4
  47. package/dist/replay-fetch.js.map +1 -1
  48. package/dist/replay-log-tagging.js +1 -4
  49. package/dist/replay-log-tagging.js.map +1 -1
  50. package/dist/replay-sidecar-url.js +1 -4
  51. package/dist/replay-sidecar-url.js.map +1 -1
  52. package/dist/sidecar-client.js +1 -4
  53. package/dist/sidecar-client.js.map +1 -1
  54. package/dist/sidecar-transport.js +1 -4
  55. package/dist/sidecar-transport.js.map +1 -1
  56. package/dist/spans/attributes.js +1 -4
  57. package/dist/spans/attributes.js.map +1 -1
  58. package/dist/spans/readable-span.js +1 -4
  59. package/dist/spans/readable-span.js.map +1 -1
  60. package/dist/spans/serialize-span.js +1 -4
  61. package/dist/spans/serialize-span.js.map +1 -1
  62. package/dist/spans/span-builder.js +1 -4
  63. package/dist/spans/span-builder.js.map +1 -1
  64. package/dist/spans/url-redaction.js +1 -4
  65. package/dist/spans/url-redaction.js.map +1 -1
  66. package/dist/version.js +2 -5
  67. package/dist/version.js.map +1 -1
  68. package/dist/virtual-clock.js +1 -4
  69. package/dist/virtual-clock.js.map +1 -1
  70. package/dist/virtual-random.js +1 -4
  71. package/dist/virtual-random.js.map +1 -1
  72. package/dist/vite/index.js +15 -16
  73. package/dist/vite/index.js.map +1 -1
  74. package/dist/with-meticulous.js +0 -0
  75. package/dist/with-meticulous.js.map +1 -1
  76. package/package.json +2 -2
@@ -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":"88d9b432-3aec-5b5d-abea-8e34937d6d14"}
1
+ {"version":3,"file":"kv-patch.js","names":[],"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"],"mappings":"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"}
package/dist/log.js CHANGED
@@ -4,8 +4,6 @@
4
4
  * that cannot be served, which throws), where knowing _which_ call it was matters more than
5
5
  * keeping the log short — a deduped key would hide every miss after the first.
6
6
  */
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]="b4ba8614-26e6-5049-9ded-6ce5ca3e5465")}catch(e){}}();
9
7
  const warn = (message) => {
10
8
  console.warn(`[meticulous] ${message}`);
11
9
  };
@@ -20,5 +18,4 @@ const warnOnce = (key, message, error) => {
20
18
  //#endregion
21
19
  export { warn, warnOnce };
22
20
 
23
- //# sourceMappingURL=log.js.map
24
- //# debugId=b4ba8614-26e6-5049-9ded-6ce5ca3e5465
21
+ //# sourceMappingURL=log.js.map
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":"b4ba8614-26e6-5049-9ded-6ce5ca3e5465"}
1
+ {"version":3,"file":"log.js","names":[],"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"],"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"}
@@ -1,6 +1,4 @@
1
1
  //#region src/original-fetch.ts
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]="2a9aa350-427d-5701-a421-a41157543c20")}catch(e){}}();
4
2
  let originalFetch;
5
3
  /**
6
4
  * Holds the pre-patch `globalThis.fetch`. Its own module so that both the fetch patch (which
@@ -18,5 +16,4 @@ const getOriginalFetch = () => originalFetch ?? globalThis.fetch;
18
16
  //#endregion
19
17
  export { getOriginalFetch, setOriginalFetch };
20
18
 
21
- //# sourceMappingURL=original-fetch.js.map
22
- //# debugId=2a9aa350-427d-5701-a421-a41157543c20
19
+ //# sourceMappingURL=original-fetch.js.map
@@ -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":"2a9aa350-427d-5701-a421-a41157543c20"}
1
+ {"version":3,"file":"original-fetch.js","names":[],"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"],"mappings":";AAEA,IAAI;;;;;;AAOJ,MAAa,oBAAoB,YAA2B;AAC1D,iBAAgB;;;;;;AAOlB,MAAa,yBACX,iBAAiB,WAAW"}
@@ -1,5 +1,3 @@
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]="f0de66db-f0c8-57c2-8f95-fd7b22d44d79")}catch(e){}}();
3
1
  import { warnOnce } from "./log.js";
4
2
  import { readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
5
3
  import { CAPTURED_HEADERS } from "./protocol.js";
@@ -115,5 +113,4 @@ const hasWebSocket = (response) => response.webSocket != null;
115
113
  //#endregion
116
114
  export { captureOutboundCall, hasWebSocket, headersToRecord };
117
115
 
118
- //# sourceMappingURL=outbound-capture.js.map
119
- //# debugId=f0de66db-f0c8-57c2-8f95-fd7b22d44d79
116
+ //# sourceMappingURL=outbound-capture.js.map
@@ -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":"f0de66db-f0c8-57c2-8f95-fd7b22d44d79"}
1
+ {"version":3,"file":"outbound-capture.js","names":[],"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"],"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"}
@@ -1,5 +1,3 @@
1
-
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="78b550d0-f18f-5c3c-b576-7cef30ecadc9")}catch(e){}}();
3
1
  import { runWithMeticulous } from "./with-meticulous.js";
4
2
  //#region src/pages-function.ts
5
3
  /**
@@ -24,5 +22,4 @@ const withMeticulousPagesFunction = (handler, options) => (context) => runWithMe
24
22
  //#endregion
25
23
  export { withMeticulousPagesFunction };
26
24
 
27
- //# sourceMappingURL=pages-function.js.map
28
- //# debugId=78b550d0-f18f-5c3c-b576-7cef30ecadc9
25
+ //# sourceMappingURL=pages-function.js.map
@@ -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":"78b550d0-f18f-5c3c-b576-7cef30ecadc9"}
1
+ {"version":3,"file":"pages-function.js","names":[],"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"],"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"}
@@ -1,5 +1,3 @@
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]="b2626960-f48d-5500-a2b3-a8c9fbaf2002")}catch(e){}}();
3
1
  import { serializeCapturedError } from "../error-capture.js";
4
2
  //#region src/postgres/capture.ts
5
3
  /**
@@ -132,5 +130,4 @@ const isSupportedPostgresJsQuery = (query) => reconstructQueryText(query.strings
132
130
  //#endregion
133
131
  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
132
 
135
- //# sourceMappingURL=capture.js.map
136
- //# debugId=b2626960-f48d-5500-a2b3-a8c9fbaf2002
133
+ //# sourceMappingURL=capture.js.map
@@ -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":"b2626960-f48d-5500-a2b3-a8c9fbaf2002"}
1
+ {"version":3,"file":"capture.js","names":[],"sources":["../../src/postgres/capture.ts"],"sourcesContent":["import { serializeCapturedError } from \"../error-capture\";\n\n/**\n * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node\n * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.\n *\n * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a\n * deployed Worker has to produce the byte-identical span a Node-recorded one does, because\n * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the\n * serialization subtly wrong on one surface and every replay of a Worker recording misses.\n * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here\n * and keeps the replay half (key building, result/error reconstruction) to itself — that half\n * needs a synchronous SHA-256 and never runs inside a Worker.\n */\n\n// Custom Meticulous span attributes carrying the query input + result. Kept distinct from the\n// `pg` attributes (`meticulous.pg.*`) so the two stores can never match each other's spans,\n// even though both talk to Postgres.\nexport const POSTGRES_JS_QUERY_TEXT_ATTR = \"meticulous.postgresjs.query.text\";\nexport const POSTGRES_JS_QUERY_PARAMS_ATTR =\n \"meticulous.postgresjs.query.params\";\nexport const POSTGRES_JS_ROW_MODE_ATTR = \"meticulous.postgresjs.row_mode\";\nexport const POSTGRES_JS_RESULT_ATTR = \"meticulous.postgresjs.result\";\nexport const POSTGRES_JS_RESULT_TRUNCATED_ATTR =\n \"meticulous.postgresjs.result.truncated\";\n// Set instead of `result` when the query rejected, so a query that legitimately errors (a\n// constraint violation, a missing relation) replays as the same error rather than as a \"no\n// recorded result\" miss.\nexport const POSTGRES_JS_ERROR_ATTR = \"meticulous.postgresjs.error\";\n\n// Cap the captured result to bound span size, matching the HTTP/undici/pg 256 KB cap.\nexport const MAX_POSTGRES_JS_RESULT_SIZE = 256 * 1024;\n\n/**\n * How the rows of a result are shaped: objects keyed by column name by default, or arrays under\n * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |\n * `true` | `\"values\"`).\n *\n * This belongs in the match key because the SAME sql text returns different row shapes\n * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits\n * the query directly for `db.execute`, so both shapes occur for identical text.\n */\nexport type PostgresJsRowMode = \"\" | \"raw\" | \"values\";\n\nexport const resolveRowMode = (isRaw: unknown): PostgresJsRowMode => {\n if (isRaw === \"values\") {\n return \"values\";\n }\n return isRaw ? \"raw\" : \"\";\n};\n\n/**\n * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the\n * interpolated values went.\n *\n * The SQL text does not exist at interception time: postgres.js builds `query.string` only once\n * a query reaches a connection, and replay never executes, so both sides must derive the text\n * from `query.strings` instead.\n */\nexport const reconstructQueryText = (strings: unknown): string | undefined => {\n if (!Array.isArray(strings) || strings.length === 0) {\n return undefined;\n }\n let text = \"\";\n for (let i = 0; i < strings.length; i++) {\n const chunk = strings[i];\n if (typeof chunk !== \"string\") {\n return undefined;\n }\n text += i === 0 ? chunk : `$${i}${chunk}`;\n }\n return text;\n};\n\n/**\n * Canonical serialization of the query's interpolated values.\n *\n * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not\n * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place\n * into wire-format strings — so it differs between the two sides.\n *\n * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /\n * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across\n * calls, which is all the match key needs.\n */\nexport const serializePostgresJsArgs = (args: unknown): string => {\n try {\n return JSON.stringify(Array.isArray(args) ? args : []);\n } catch {\n return \"[]\";\n }\n};\n\n/** The leading SQL keyword, for the span name only. */\nexport const postgresJsCommandOf = (text: string): string =>\n /^\\s*([a-z]+)/i.exec(text)?.[1]?.toLowerCase() ?? \"query\";\n\ninterface SerializedPostgresJsResult {\n rows: unknown[];\n count: number | null;\n command: string | null;\n columns: { name: string; type: number; table: number; number: number }[];\n statement: { string: string; types: number[]; name: string } | null;\n}\n\n/**\n * The metadata postgres.js hangs off a `Result`. Declared with `unknown` values so the\n * serializer below narrows every field rather than trusting the shape.\n */\ninterface PostgresJsResultLike {\n count?: unknown;\n command?: unknown;\n columns?: unknown;\n statement?: unknown;\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value != null && typeof value === \"object\"\n ? (value as Record<string, unknown>)\n : {};\n\nconst numberOrNull = (value: unknown): number | null =>\n typeof value === \"number\" ? value : null;\n\n/**\n * postgres.js resolves a query with a `Result` — an `Array` subclass whose metadata lives in\n * five non-enumerable own properties, so `JSON.stringify` would capture only the rows. Pull them\n * out explicitly.\n *\n * Two fields are dropped on purpose: each column's `parser` (a function, so it can't survive\n * JSON, and consumers never call it on an already-parsed row) and `state` (a live\n * per-connection `{pid, secret}` handle used only for query cancellation, which is meaningless\n * without a connection).\n */\nconst serializeOneResult = (res: unknown): SerializedPostgresJsResult => {\n const meta = res as PostgresJsResultLike | null | undefined;\n const statement = asRecord(meta?.statement);\n return {\n rows: Array.isArray(res) ? [...(res as unknown[])] : [],\n count: numberOrNull(meta?.count),\n command: typeof meta?.command === \"string\" ? meta.command : null,\n columns: Array.isArray(meta?.columns)\n ? meta.columns.map((column) => {\n const c = asRecord(column);\n return {\n name: String(c.name ?? \"\"),\n type: numberOrNull(c.type) ?? 0,\n table: numberOrNull(c.table) ?? 0,\n number: numberOrNull(c.number) ?? 0,\n };\n })\n : [],\n statement:\n meta?.statement != null\n ? {\n string: String(statement.string ?? \"\"),\n types: Array.isArray(statement.types)\n ? (statement.types as number[])\n : [],\n name: String(statement.name ?? \"\"),\n }\n : null,\n };\n};\n\n/**\n * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves\n * to) to a JSON string for storage as a span attribute.\n */\nexport const serializePostgresJsResult = (result: unknown): string => {\n // A Result IS an Array, so an array-of-Results is only distinguishable by its elements. The\n // `length > 0` guard matters: `[].every(...)` is true, so without it every zero-row result (a\n // SELECT that matched nothing, an INSERT without RETURNING, BEGIN/COMMIT) would be mistaken\n // for an empty list of results and lose its `count`/`command`.\n const isMultiple =\n Array.isArray(result) &&\n result.length > 0 &&\n result.every((r) => isResultLike(r));\n const serialized = isMultiple\n ? (result as unknown[]).map(serializeOneResult)\n : serializeOneResult(result);\n return JSON.stringify(serialized);\n};\n\n// A `Result` carries `command`/`count` own properties; a plain row object or scalar does not.\nconst isResultLike = (value: unknown): boolean =>\n Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, \"command\") &&\n Object.prototype.hasOwnProperty.call(value, \"count\");\n\n// Own properties postgres.js attaches to a query error for debugging. Excluded from the\n// captured error: they are large, redundant with the query attributes we already record, and\n// only enumerable when the app enabled postgres.js' `debug` option.\nconst ERROR_DEBUG_PROPS = new Set([\n \"stack\",\n \"query\",\n \"parameters\",\n \"args\",\n \"types\",\n]);\n\n/**\n * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as\n * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields\n * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All\n * of those live in own enumerable properties, so copying those plus `message` (own but\n * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.\n */\nexport const serializePostgresJsError = (error: unknown): string =>\n serializeCapturedError(error, ERROR_DEBUG_PROPS);\n\n/**\n * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no\n * `postgres` dependency).\n */\nexport interface PostgresJsQueryLike {\n /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */\n strings: unknown;\n /** The interpolated values, still raw JS values at interception time. */\n args: unknown;\n /** `undefined` by default, `true` after `.raw()`, `\"values\"` after `.values()`. */\n isRaw?: unknown;\n /** Set once `handle()` has run; guards against double-instrumenting one query. */\n executed?: boolean;\n /** Set by `.describe()` — resolves with a Statement, not a Result. */\n onlyDescribe?: boolean;\n /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */\n cursorFn?: unknown;\n cursorRows?: unknown;\n forEachFn?: unknown;\n streaming?: unknown;\n options?: {\n /**\n * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it\n * called with the serving connection before the transaction body runs.\n */\n onexecute?: (connection: unknown) => void;\n [key: string]: unknown;\n };\n resolve: (result: unknown) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Whether a query resolves with a single `Result` we can capture and replay. Cursors,\n * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with\n * something else, and `sql.file()` is constructed with no template chunks at all (its text is\n * only read from disk later). All of those pass straight through, unrecorded and unmocked.\n */\nexport const isSupportedPostgresJsQuery = (\n query: PostgresJsQueryLike,\n): boolean =>\n reconstructQueryText(query.strings) != null &&\n !query.onlyDescribe &&\n !query.streaming &&\n query.cursorFn == null &&\n query.cursorRows == null &&\n query.forEachFn == null;\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,8BAA8B;AAC3C,MAAa,gCACX;AACF,MAAa,4BAA4B;AACzC,MAAa,0BAA0B;AACvC,MAAa,oCACX;AAIF,MAAa,yBAAyB;AAGtC,MAAa,8BAA8B,MAAM;AAajD,MAAa,kBAAkB,UAAsC;AACnE,KAAI,UAAU,SACZ,QAAO;AAET,QAAO,QAAQ,QAAQ;;;;;;;;;;AAWzB,MAAa,wBAAwB,YAAyC;AAC5E,KAAI,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,WAAW,EAChD;CAEF,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,QAAQ,QAAQ;AACtB,MAAI,OAAO,UAAU,SACnB;AAEF,UAAQ,MAAM,IAAI,QAAQ,IAAI,IAAI;;AAEpC,QAAO;;;;;;;;;;;;;AAcT,MAAa,2BAA2B,SAA0B;AAChE,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,EAAE,CAAC;SAChD;AACN,SAAO;;;;AAKX,MAAa,uBAAuB,SAClC,gBAAgB,KAAK,KAAK,GAAG,IAAI,aAAa,IAAI;AAqBpD,MAAM,YAAY,UAChB,SAAS,QAAQ,OAAO,UAAU,WAC7B,QACD,EAAE;AAER,MAAM,gBAAgB,UACpB,OAAO,UAAU,WAAW,QAAQ;;;;;;;;;;;AAYtC,MAAM,sBAAsB,QAA6C;CACvE,MAAM,OAAO;CACb,MAAM,YAAY,SAAS,MAAM,UAAU;AAC3C,QAAO;EACL,MAAM,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAI,IAAkB,GAAG,EAAE;EACvD,OAAO,aAAa,MAAM,MAAM;EAChC,SAAS,OAAO,MAAM,YAAY,WAAW,KAAK,UAAU;EAC5D,SAAS,MAAM,QAAQ,MAAM,QAAQ,GACjC,KAAK,QAAQ,KAAK,WAAW;GAC3B,MAAM,IAAI,SAAS,OAAO;AAC1B,UAAO;IACL,MAAM,OAAO,EAAE,QAAQ,GAAG;IAC1B,MAAM,aAAa,EAAE,KAAK,IAAI;IAC9B,OAAO,aAAa,EAAE,MAAM,IAAI;IAChC,QAAQ,aAAa,EAAE,OAAO,IAAI;IACnC;IACD,GACF,EAAE;EACN,WACE,MAAM,aAAa,OACf;GACE,QAAQ,OAAO,UAAU,UAAU,GAAG;GACtC,OAAO,MAAM,QAAQ,UAAU,MAAM,GAChC,UAAU,QACX,EAAE;GACN,MAAM,OAAO,UAAU,QAAQ,GAAG;GACnC,GACD;EACP;;;;;;AAOH,MAAa,6BAA6B,WAA4B;CASpE,MAAM,aAHJ,MAAM,QAAQ,OAAO,IACrB,OAAO,SAAS,KAChB,OAAO,OAAO,MAAM,aAAa,EAAE,CAAC,GAEjC,OAAqB,IAAI,mBAAmB,GAC7C,mBAAmB,OAAO;AAC9B,QAAO,KAAK,UAAU,WAAW;;AAInC,MAAM,gBAAgB,UACpB,MAAM,QAAQ,MAAM,IACpB,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IACtD,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ;AAKtD,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAa,4BAA4B,UACvC,uBAAuB,OAAO,kBAAkB;;;;;;;AAwClD,MAAa,8BACX,UAEA,qBAAqB,MAAM,QAAQ,IAAI,QACvC,CAAC,MAAM,gBACP,CAAC,MAAM,aACP,MAAM,YAAY,QAClB,MAAM,cAAc,QACpB,MAAM,aAAa"}
@@ -1,5 +1,3 @@
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]="bcb30e85-33e1-52b2-9e08-5dd84f66d61c")}catch(e){}}();
3
1
  import { requestCaptureContext } from "../context.js";
4
2
  import { warnOnce } from "../log.js";
5
3
  import { MAX_POSTGRES_JS_RESULT_SIZE, isSupportedPostgresJsQuery, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./capture.js";
@@ -148,5 +146,4 @@ const captureQuery = (ctx, query) => {
148
146
  //#endregion
149
147
  export { withMeticulousPostgres };
150
148
 
151
- //# sourceMappingURL=with-meticulous-postgres.js.map
152
- //# debugId=bcb30e85-33e1-52b2-9e08-5dd84f66d61c
149
+ //# sourceMappingURL=with-meticulous-postgres.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"with-meticulous-postgres.js","sources":["../../src/postgres/with-meticulous-postgres.ts"],"sourcesContent":["import { type RequestCaptureContext, requestCaptureContext } from \"../context\";\nimport { warnOnce } from \"../log\";\nimport type { PostgresQueryEvent } from \"../protocol\";\nimport {\n isSupportedPostgresJsQuery,\n MAX_POSTGRES_JS_RESULT_SIZE,\n type PostgresJsQueryLike,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./capture\";\n\n/**\n * Records postgres.js queries a deployed Worker makes — typically over Hyperdrive, which is the\n * only way a Worker reaches Postgres.\n *\n * There is no seam to patch automatically here. The Node recorder can hook Node's module loader\n * to patch `postgres/{src,cjs/src,cf/src}/query.js`, but a Worker is a single bundle: `postgres`\n * was inlined at build time and never passes through a loader. So the app hands us its client:\n *\n * const sql = withMeticulousPostgres(postgres(env.HYPERDRIVE.connectionString));\n *\n * Every postgres.js query funnels through one internal method, `Query.prototype.handle`, and this\n * reaches that shared prototype *through the instance* — via a throwaway query, which is free\n * because postgres.js queries are lazy and nothing executes until awaited. One call therefore also\n * covers read-replica clients and any other client in the isolate.\n *\n * Apply it **outermost** if something else also wraps the client (Sentry's\n * `instrumentPostgresJsSql`, a logging proxy): those wrappers return a proxy whose `handle` is\n * still the shared prototype's, so ordering does not change what is captured — but keeping ours\n * outermost matches the Node recorder's contract and stays correct if that ever changes.\n *\n * Record-only, deliberately. A Worker recording is replayed against the app running under Node,\n * where `@alwaysmeticulous/backend-recorder-js` serves these queries from its own mock store —\n * which is why the captured attributes here are exactly the ones that store reads. Outside a\n * recorded request this is a complete pass-through, so it is safe to leave in deployed code.\n *\n * Returns the client unchanged; the patch is on the prototype.\n */\nexport const withMeticulousPostgres = <Sql>(sql: Sql): Sql => {\n try {\n const prototype = findQueryPrototype(sql);\n if (prototype === undefined) {\n warnOnce(\n \"postgres-wrapper-unsupported\",\n \"withMeticulousPostgres could not reach postgres.js' Query prototype — queries will not be recorded.\",\n );\n return sql;\n }\n patchHandle(prototype);\n } catch (error) {\n warnOnce(\n \"postgres-wrapper-failed\",\n \"withMeticulousPostgres failed — queries will not be recorded.\",\n error,\n );\n }\n return sql;\n};\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst HANDLE_PATCHED = Symbol.for(\"meticulous.workerd.postgresJsPatched\");\n\ntype HandleFn = (this: PostgresJsQueryLike) => unknown;\n\n/**\n * The `Query` prototype, reached by building a query and reading its prototype.\n *\n * `sql.unsafe` is used rather than a tagged template because it takes the text as a plain\n * argument: no interpolation, and postgres.js does not touch a connection until the query is\n * awaited, which nothing here does. The query is discarded.\n */\nconst findQueryPrototype = (sql: unknown): object | undefined => {\n const unsafe = (sql as { unsafe?: unknown } | null)?.unsafe;\n if (typeof unsafe !== \"function\") {\n return undefined;\n }\n const probe: unknown = (unsafe as (text: string) => unknown).call(\n sql,\n \"select 1\",\n );\n if (probe === null || typeof probe !== \"object\") {\n return undefined;\n }\n const prototype = Object.getPrototypeOf(probe) as object | null;\n if (prototype === null) {\n return undefined;\n }\n const descriptor = Object.getOwnPropertyDescriptor(prototype, \"handle\");\n return descriptor !== undefined &&\n typeof descriptor.value === \"function\" &&\n descriptor.configurable === true\n ? prototype\n : undefined;\n};\n\nconst patchHandle = (prototype: object): void => {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, \"handle\");\n const original = descriptor?.value as HandleFn | undefined;\n if (\n original === undefined ||\n (original as unknown as Record<symbol, unknown>)[HANDLE_PATCHED] === true\n ) {\n return;\n }\n\n const patched = function (this: PostgresJsQueryLike): unknown {\n // `handle` is reachable from then/catch/finally/execute, so it can be called more than once\n // for one query; postgres.js itself no-ops on the repeats via `executed`. Bail before\n // instrumenting so a query is never captured twice.\n if (this.executed || !isSupportedPostgresJsQuery(this)) {\n return original.call(this);\n }\n const ctx = requestCaptureContext.getStore();\n if (!ctx || ctx.mode !== \"record\") {\n return original.call(this);\n }\n try {\n captureQuery(ctx, this);\n } catch (error) {\n warnOnce(\n \"postgres-capture\",\n \"Failed to capture a postgres.js query.\",\n error,\n );\n }\n return original.call(this);\n };\n Object.defineProperty(patched, HANDLE_PATCHED, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n Object.defineProperty(prototype, \"handle\", {\n value: patched,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n};\n\n/**\n * Wraps the query's own `resolve`/`reject` so the outcome is captured without the query being\n * awaited here — the same trick the Node instrumentation uses, and the only place a query's input\n * and its result are both visible.\n */\nconst captureQuery = (\n ctx: RequestCaptureContext,\n query: PostgresJsQueryLike,\n): void => {\n const queryText = reconstructQueryText(query.strings);\n if (queryText === undefined) {\n return;\n }\n const startTimeMs = Date.now();\n const base = {\n kind: \"postgres\" as const,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n traceId: ctx.traceId,\n serverSpanId: ctx.serverSpanId,\n queryText,\n params: serializePostgresJsArgs(query.args),\n rowMode: resolveRowMode(query.isRaw),\n startTimeMs,\n };\n\n const report = (outcome: Partial<PostgresQueryEvent>): void => {\n try {\n ctx.buffer.add({ ...base, ...outcome, endTimeMs: Date.now() });\n } catch (error) {\n warnOnce(\n \"postgres-report\",\n \"Failed to report a postgres.js query.\",\n error,\n );\n }\n };\n\n const originalResolve = query.resolve;\n const originalReject = query.reject;\n query.resolve = (result: unknown) => {\n // A result that cannot be serialized is simply not captured → it misses during replay and\n // errors hermetically, rather than serving something wrong.\n let json: string | undefined;\n try {\n json = serializePostgresJsResult(result);\n } catch {\n json = undefined;\n }\n report(\n json === undefined\n ? {}\n : {\n result: {\n body:\n json.length > MAX_POSTGRES_JS_RESULT_SIZE\n ? json.slice(0, MAX_POSTGRES_JS_RESULT_SIZE)\n : json,\n truncated: json.length > MAX_POSTGRES_JS_RESULT_SIZE,\n },\n },\n );\n return originalResolve.call(query, result);\n };\n query.reject = (error: unknown) => {\n let errorJson: string | undefined;\n try {\n errorJson = serializePostgresJsError(error);\n } catch {\n errorJson = undefined;\n }\n report(errorJson === undefined ? {} : { errorJson });\n return originalReject.call(query, error);\n };\n};\n"],"names":[],"mappings":"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","debugId":"bcb30e85-33e1-52b2-9e08-5dd84f66d61c"}
1
+ {"version":3,"file":"with-meticulous-postgres.js","names":[],"sources":["../../src/postgres/with-meticulous-postgres.ts"],"sourcesContent":["import { type RequestCaptureContext, requestCaptureContext } from \"../context\";\nimport { warnOnce } from \"../log\";\nimport type { PostgresQueryEvent } from \"../protocol\";\nimport {\n isSupportedPostgresJsQuery,\n MAX_POSTGRES_JS_RESULT_SIZE,\n type PostgresJsQueryLike,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./capture\";\n\n/**\n * Records postgres.js queries a deployed Worker makes — typically over Hyperdrive, which is the\n * only way a Worker reaches Postgres.\n *\n * There is no seam to patch automatically here. The Node recorder can hook Node's module loader\n * to patch `postgres/{src,cjs/src,cf/src}/query.js`, but a Worker is a single bundle: `postgres`\n * was inlined at build time and never passes through a loader. So the app hands us its client:\n *\n * const sql = withMeticulousPostgres(postgres(env.HYPERDRIVE.connectionString));\n *\n * Every postgres.js query funnels through one internal method, `Query.prototype.handle`, and this\n * reaches that shared prototype *through the instance* — via a throwaway query, which is free\n * because postgres.js queries are lazy and nothing executes until awaited. One call therefore also\n * covers read-replica clients and any other client in the isolate.\n *\n * Apply it **outermost** if something else also wraps the client (Sentry's\n * `instrumentPostgresJsSql`, a logging proxy): those wrappers return a proxy whose `handle` is\n * still the shared prototype's, so ordering does not change what is captured — but keeping ours\n * outermost matches the Node recorder's contract and stays correct if that ever changes.\n *\n * Record-only, deliberately. A Worker recording is replayed against the app running under Node,\n * where `@alwaysmeticulous/backend-recorder-js` serves these queries from its own mock store —\n * which is why the captured attributes here are exactly the ones that store reads. Outside a\n * recorded request this is a complete pass-through, so it is safe to leave in deployed code.\n *\n * Returns the client unchanged; the patch is on the prototype.\n */\nexport const withMeticulousPostgres = <Sql>(sql: Sql): Sql => {\n try {\n const prototype = findQueryPrototype(sql);\n if (prototype === undefined) {\n warnOnce(\n \"postgres-wrapper-unsupported\",\n \"withMeticulousPostgres could not reach postgres.js' Query prototype — queries will not be recorded.\",\n );\n return sql;\n }\n patchHandle(prototype);\n } catch (error) {\n warnOnce(\n \"postgres-wrapper-failed\",\n \"withMeticulousPostgres failed — queries will not be recorded.\",\n error,\n );\n }\n return sql;\n};\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst HANDLE_PATCHED = Symbol.for(\"meticulous.workerd.postgresJsPatched\");\n\ntype HandleFn = (this: PostgresJsQueryLike) => unknown;\n\n/**\n * The `Query` prototype, reached by building a query and reading its prototype.\n *\n * `sql.unsafe` is used rather than a tagged template because it takes the text as a plain\n * argument: no interpolation, and postgres.js does not touch a connection until the query is\n * awaited, which nothing here does. The query is discarded.\n */\nconst findQueryPrototype = (sql: unknown): object | undefined => {\n const unsafe = (sql as { unsafe?: unknown } | null)?.unsafe;\n if (typeof unsafe !== \"function\") {\n return undefined;\n }\n const probe: unknown = (unsafe as (text: string) => unknown).call(\n sql,\n \"select 1\",\n );\n if (probe === null || typeof probe !== \"object\") {\n return undefined;\n }\n const prototype = Object.getPrototypeOf(probe) as object | null;\n if (prototype === null) {\n return undefined;\n }\n const descriptor = Object.getOwnPropertyDescriptor(prototype, \"handle\");\n return descriptor !== undefined &&\n typeof descriptor.value === \"function\" &&\n descriptor.configurable === true\n ? prototype\n : undefined;\n};\n\nconst patchHandle = (prototype: object): void => {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, \"handle\");\n const original = descriptor?.value as HandleFn | undefined;\n if (\n original === undefined ||\n (original as unknown as Record<symbol, unknown>)[HANDLE_PATCHED] === true\n ) {\n return;\n }\n\n const patched = function (this: PostgresJsQueryLike): unknown {\n // `handle` is reachable from then/catch/finally/execute, so it can be called more than once\n // for one query; postgres.js itself no-ops on the repeats via `executed`. Bail before\n // instrumenting so a query is never captured twice.\n if (this.executed || !isSupportedPostgresJsQuery(this)) {\n return original.call(this);\n }\n const ctx = requestCaptureContext.getStore();\n if (!ctx || ctx.mode !== \"record\") {\n return original.call(this);\n }\n try {\n captureQuery(ctx, this);\n } catch (error) {\n warnOnce(\n \"postgres-capture\",\n \"Failed to capture a postgres.js query.\",\n error,\n );\n }\n return original.call(this);\n };\n Object.defineProperty(patched, HANDLE_PATCHED, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n Object.defineProperty(prototype, \"handle\", {\n value: patched,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n};\n\n/**\n * Wraps the query's own `resolve`/`reject` so the outcome is captured without the query being\n * awaited here — the same trick the Node instrumentation uses, and the only place a query's input\n * and its result are both visible.\n */\nconst captureQuery = (\n ctx: RequestCaptureContext,\n query: PostgresJsQueryLike,\n): void => {\n const queryText = reconstructQueryText(query.strings);\n if (queryText === undefined) {\n return;\n }\n const startTimeMs = Date.now();\n const base = {\n kind: \"postgres\" as const,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n traceId: ctx.traceId,\n serverSpanId: ctx.serverSpanId,\n queryText,\n params: serializePostgresJsArgs(query.args),\n rowMode: resolveRowMode(query.isRaw),\n startTimeMs,\n };\n\n const report = (outcome: Partial<PostgresQueryEvent>): void => {\n try {\n ctx.buffer.add({ ...base, ...outcome, endTimeMs: Date.now() });\n } catch (error) {\n warnOnce(\n \"postgres-report\",\n \"Failed to report a postgres.js query.\",\n error,\n );\n }\n };\n\n const originalResolve = query.resolve;\n const originalReject = query.reject;\n query.resolve = (result: unknown) => {\n // A result that cannot be serialized is simply not captured → it misses during replay and\n // errors hermetically, rather than serving something wrong.\n let json: string | undefined;\n try {\n json = serializePostgresJsResult(result);\n } catch {\n json = undefined;\n }\n report(\n json === undefined\n ? {}\n : {\n result: {\n body:\n json.length > MAX_POSTGRES_JS_RESULT_SIZE\n ? json.slice(0, MAX_POSTGRES_JS_RESULT_SIZE)\n : json,\n truncated: json.length > MAX_POSTGRES_JS_RESULT_SIZE,\n },\n },\n );\n return originalResolve.call(query, result);\n };\n query.reject = (error: unknown) => {\n let errorJson: string | undefined;\n try {\n errorJson = serializePostgresJsError(error);\n } catch {\n errorJson = undefined;\n }\n report(errorJson === undefined ? {} : { errorJson });\n return originalReject.call(query, error);\n };\n};\n"],"mappings":"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"}
package/dist/protocol.js CHANGED
@@ -4,8 +4,6 @@
4
4
  * recorder sidecar. The sidecar imports these types (type-only), so this file
5
5
  * is the single source of truth for the shape of capture events.
6
6
  */
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]="7851ef28-53e0-596f-80dc-a26f37790747")}catch(e){}}();
9
7
  const SIDECAR_PROTOCOL_VERSION = "2";
10
8
  /** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */
11
9
  const SIDECAR_PROTOCOL_VERSION_HEADER = "x-meticulous-sidecar-protocol-version";
@@ -90,5 +88,4 @@ const CAPTURED_HEADERS = ["content-type", FRONTEND_SESSION_ID_HEADER];
90
88
  //#endregion
91
89
  export { 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, VIRTUAL_TIME_HEADER, WORKERD_SHIM_VERSION_HEADER, parseVirtualTimeMs };
92
90
 
93
- //# sourceMappingURL=protocol.js.map
94
- //# debugId=7851ef28-53e0-596f-80dc-a26f37790747
91
+ //# sourceMappingURL=protocol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Wire protocol between the in-worker shim and the Meticulous backend\n * recorder sidecar. The sidecar imports these types (type-only), so this file\n * is the single source of truth for the shape of capture events.\n */\n\nexport const SIDECAR_PROTOCOL_VERSION = \"2\";\n\n/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */\nexport const SIDECAR_PROTOCOL_VERSION_HEADER =\n \"x-meticulous-sidecar-protocol-version\";\n\n/**\n * Header the Meticulous frontend recorder stamps on requests made by the app\n * under recording; its value ties backend spans to the frontend session.\n */\nexport const FRONTEND_SESSION_ID_HEADER = \"x-meticulous-session-id\";\n\n/** Header identifying one replay attempt of a recorded frontend session. */\nexport const REPLAY_ID_HEADER = \"x-meticulous-replay-id\";\n\n/**\n * Elapsed virtual time (ms from replay start) of the triggering frontend\n * request. Injected by the replay runner on app-url requests during backend\n * testing; the sidecar uses it to pick the closest recorded burst when several\n * candidates share a loosened match key.\n */\nexport const VIRTUAL_TIME_HEADER = \"x-meticulous-virtual-time\";\n\n/**\n * Parses {@link VIRTUAL_TIME_HEADER}. `0` is a valid start-of-session time;\n * anything non-finite or negative is ignored so a garbage header cannot skew matching.\n */\nexport const parseVirtualTimeMs = (\n value: string | null | undefined,\n): number | undefined => {\n if (value == null || value === \"\") {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return undefined;\n }\n return parsed;\n};\n\n/**\n * A request carrying this header set to `\"true\"` is never served from the recording and\n * never fails on a miss — it goes to the real network. Use it for a call that must be live\n * during replay (a health probe, telemetry, a resource the recording cannot cover).\n *\n * Same convention and value as the frontend network stubbing and the Node backend recorder\n * (`METICULOUS_PASSTHROUGH_HEADER` in `@alwaysmeticulous/backend-recorder-js`); the value\n * must stay in step with both.\n */\nexport const METICULOUS_PASSTHROUGH_HEADER = \"meticulous-passthrough\";\n\nexport const SIDECAR_EVENTS_PATH = \"/v1/events\";\n\n/**\n * Replay routes. A sidecar started in record mode answers 404 on both. Replay is hermetic,\n * so an unanswered outbound-fetch lookup fails the app's call rather than letting it reach\n * the real service — the same as any other failure to stub.\n */\nexport const SIDECAR_REPLAY_SESSION_PATH = \"/v1/replay/session\";\nexport const SIDECAR_REPLAY_OUTBOUND_FETCH_PATH = \"/v1/replay/outbound-fetch\";\n\n/**\n * Line-coverage reporting. Additive: an older sidecar answers 404, which the\n * shim treats as \"coverage not collected here\" and stops reporting for the life\n * of the isolate. Deliberately NOT guarded by a\n * {@link SIDECAR_PROTOCOL_VERSION} bump — that check is exact equality, so a\n * bump would stop every already-built customer image from mocking at all.\n */\nexport const SIDECAR_REPLAY_COVERAGE_PATH = \"/v1/replay/coverage\";\n\n/**\n * Header carrying the replay sidecar's origin, injected by the Meticulous replay runner\n * on requests to the app under test. Workerd cannot see container environment variables,\n * so an inbound header is the only way per-replay config can reach the shim.\n *\n * The shim validates the value before using it and only honours a loopback /\n * `host.docker.internal` / private-network `http:` origin — see `replay-sidecar-url.ts`.\n */\nexport const REPLAY_SIDECAR_URL_HEADER =\n \"x-meticulous-backend-replay-sidecar-url\";\n\n/**\n * Header the in-worker shim stamps on every app response (record and replay)\n * carrying the shim's npm package version. The replay runner reads this from\n * Puppeteer — workerd cannot see container env, and the execute pod cannot\n * reach the in-pod sidecar, so a response header is the only channel that\n * reports which bundled shim produced a replay's After screenshots.\n */\nexport const WORKERD_SHIM_VERSION_HEADER = \"x-meticulous-workerd-shim-version\";\n\n/**\n * The only headers persisted on capture events. Everything else — notably\n * authorization, cookie/set-cookie, and API-key headers — is dropped at\n * capture time, before it ever leaves the worker.\n *\n * The Node backend recorder persists the same two, plus the inbound request's\n * `Cookie` header on SERVER spans (`recorded-request-headers.ts`). This list\n * has not followed it there, so a workerd recording of an authenticated server\n * render still carries no session cookie.\n */\nexport const CAPTURED_HEADERS = [\n \"content-type\",\n FRONTEND_SESSION_ID_HEADER,\n] as const;\n\nexport interface CapturedBody {\n /** UTF-8 decoded body, truncated to the capture cap. */\n body: string;\n /** True when the body exceeded the capture cap or the read was cut short. */\n truncated: boolean;\n}\n\n/**\n * The fields every capture event carries to say which request, session and trace it belongs to.\n */\ninterface CorrelatedEvent {\n /** Correlates outbound events to the inbound request being handled. */\n requestId: string;\n /** Value of {@link FRONTEND_SESSION_ID_HEADER} on the inbound request, if any. */\n frontendSessionId?: string;\n /**\n * Trace the request's spans belong to (32 hex chars), minted by the shim.\n *\n * The shim mints it, rather than the sidecar deriving it from {@link requestId}, so span\n * assembly is stateless: a sidecar that had to remember `requestId → traceId` would give a\n * request's SERVER span and its CLIENT children different traces whenever it lost that memory\n * between two batches — which, for a Worker-hosted sidecar, an eviction can do at any moment.\n *\n * Optional so a sidecar keeps working against a shim that predates this field, in which case\n * it falls back to its own per-`requestId` map.\n */\n traceId?: string;\n /**\n * Span id of the request's SERVER span (16 hex chars), also minted by the shim: it is the\n * parent of every CLIENT span the request produced, and the inbound event's own span id.\n */\n serverSpanId?: string;\n}\n\ninterface BaseRequestEvent extends CorrelatedEvent {\n method: string;\n /** Full URL including query string. */\n url: string;\n /** Lower-cased header name → values. */\n requestHeaders: Record<string, string[]>;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the request failed before a response was produced. */\n error?: string;\n}\n\n/**\n * The worker's own inbound request (becomes a SERVER span). Bodies are not\n * captured, matching the Node backend recorder's SERVER-span contract.\n */\nexport interface InboundRequestEvent extends BaseRequestEvent {\n kind: \"inbound\";\n /**\n * Where {@link BaseRequestEvent.frontendSessionId} came from. Absent means the browser\n * minted it and sent it on the request, which is the normal case. `\"backend\"` means the\n * shim minted it for a document navigation the browser could not tag, and the page may or\n * may not have gone on to adopt it — a distinction ingestion needs, because an unadopted\n * backend-minted id names a session that will never exist and must be treated as unstamped.\n * The sidecar turns it into `meticulous.session_id_origin`.\n *\n * Optional, and therefore compatible with an older sidecar (which ignores it) — which is\n * why adding it does not bump {@link SIDECAR_PROTOCOL_VERSION}, a strict-equality check\n * whose bump would instead make this shim unusable against one.\n */\n sessionIdOrigin?: \"backend\";\n}\n\n/**\n * An outgoing `fetch` call made while handling an inbound request (becomes a\n * CLIENT span).\n */\nexport interface OutboundRequestEvent extends BaseRequestEvent {\n kind: \"outbound\";\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding, an assets binding, or a\n * Durable Object stub — made while handling an inbound request (becomes a CLIENT span).\n *\n * Request/Response-shaped like {@link OutboundRequestEvent}, but a distinct kind because the\n * call never leaves the isolate and its URL is whatever the caller invented. Keeping the two\n * apart means a binding call and real egress can never be confused for one another when\n * these spans are eventually mocked.\n */\nexport interface BindingRequestEvent extends BaseRequestEvent {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Absent when the instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` produces rather\n * than being a binding in its own right.\n */\n bindingName?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/** KV namespace methods the shim records. */\nexport type KvOperation = \"get\" | \"getWithMetadata\" | \"put\" | \"delete\" | \"list\";\n\n/**\n * Why a KV value is absent from an event: workerd handed it over in a form that cannot be\n * persisted as text. A `\"stream\"` value must not even be read — consuming it would take the\n * bytes away from the app — and binary values are skipped deliberately, since KV blobs are\n * large and would both bloat the recording and be mangled by UTF-8 capture.\n */\nexport type KvOmittedReason = \"binary\" | \"stream\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, made while handling an inbound request\n * (becomes a CLIENT span).\n *\n * KV is not HTTP: there is no method, URL or status code, so this event shares nothing with\n * {@link BaseRequestEvent} beyond correlation and timing. Values are JSON so that a single\n * `JSON.parse` reconstructs exactly what the app saw, whichever `type` the read asked for.\n */\nexport interface KvOperationEvent extends CorrelatedEvent {\n kind: \"kv\";\n /**\n * The `env` key the KV namespace was found under. Always present, unlike on a binding\n * event: a namespace has no factory equivalent to `DurableObjectNamespace.get()`, so one\n * that was never seen on `env` is not recorded at all.\n */\n bindingName: string;\n operation: KvOperation;\n /**\n * The key operated on, when it is a single string. Absent for `list` (which spans keys) and\n * for a bulk `get` (which takes an array) — both keep their arguments in {@link args}.\n */\n key?: string;\n /**\n * JSON of the call's arguments: the read `type`, the `put` options, the `list` selector.\n * A `put` value appears as `null` here — it is captured in {@link value} instead, so that\n * redaction applies to it.\n */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, with secret-looking fields redacted. */\n value?: CapturedBody;\n /**\n * JSON of what the operation returned: the value for `get`, `{value, metadata}` for\n * `getWithMetadata`, the key page for `list`. Absent for `put`/`delete`, which return\n * nothing, and for a value that could not be captured (see {@link omitted}).\n */\n result?: CapturedBody;\n /**\n * Why the operation's value is missing. Refers to the written value for `put` and to the\n * read value otherwise — a `put` has no result, so this is never ambiguous.\n */\n omitted?: KvOmittedReason;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the operation threw. */\n error?: string;\n}\n\n/**\n * One postgres.js query made while handling an inbound request (becomes a CLIENT span).\n *\n * Deliberately carries the same fields the Node recorder puts on a postgres.js span rather than\n * anything Worker-specific: `PostgresJsMockStore` builds its match key from those attributes at\n * load time, so a query recorded here replays through the existing Node mock path unchanged.\n * See `postgres/capture.ts` for the serialization, which both surfaces share.\n */\nexport interface PostgresQueryEvent extends CorrelatedEvent {\n kind: \"postgres\";\n /** SQL rebuilt from the tagged template's chunks, with `$1`, `$2`… placeholders. */\n queryText: string;\n /** JSON of the interpolated values, from `serializePostgresJsArgs`. */\n params: string;\n /** `\"\"`, `\"raw\"` or `\"values\"` — the row shape the query asked for. */\n rowMode: string;\n /** JSON of the resolved `Result`, capped at {@link MAX_POSTGRES_JS_RESULT_SIZE}. */\n result?: CapturedBody;\n /**\n * JSON of the rejection, from `serializePostgresJsError`. Set instead of {@link result}, and\n * distinct from the transport-level `error` string on the HTTP-shaped events: this one is a\n * recorded outcome the replay reproduces, not a note that capture failed.\n */\n errorJson?: string;\n startTimeMs: number;\n endTimeMs: number;\n}\n\nexport type CaptureEvent =\n | InboundRequestEvent\n | OutboundRequestEvent\n | BindingRequestEvent\n | KvOperationEvent\n | PostgresQueryEvent;\n\nexport interface CaptureEventsPayload {\n events: CaptureEvent[];\n}\n\n/**\n * Answer to `GET {SIDECAR_REPLAY_SESSION_PATH}?sessionId=<id>`: whether the sidecar holds\n * mocks for a session, and the virtual clock anchor to freeze `Date` at while serving it.\n */\nexport interface ReplaySessionInfoResponse {\n found: boolean;\n /**\n * Epoch ms of the session's last recorded activity. The worker freezes its clock here so\n * that credentials minted during the recording are already issued but not yet expired.\n */\n clockAnchorMs?: number;\n}\n\n/**\n * One request's line coverage, sent to {@link SIDECAR_REPLAY_COVERAGE_PATH}\n * after the response has been produced.\n *\n * `files` carries the build-time id→line map and is sent only on the first\n * report of an isolate: it is the same for every request the isolate serves and\n * is far larger than the hits themselves.\n */\nexport interface CoverageReportRequest {\n frontendSessionId: string;\n /** Global line ids marked during this request, ascending. */\n hitIds: number[];\n /** Present on an isolate's first report only. */\n files?: Array<{\n path: string;\n firstId: number;\n /** Inclusive 1-based line ranges, flattened as `[s1, e1, s2, e2, ...]`. */\n lineRanges: number[];\n }>;\n}\n\n/** Lookup for a single outbound `fetch`, sent to {@link SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}. */\nexport interface OutboundFetchLookupRequest {\n frontendSessionId: string;\n replayId: string;\n method: string;\n /** Full URL including query string. */\n url: string;\n /**\n * Request body as captured by `readBodyWithCap`. The sidecar hashes this itself, so the\n * hash it compares against a recording is derived from byte-identical input on both the\n * record and replay sides.\n */\n requestBody?: CapturedBody;\n /**\n * Elapsed virtual time (ms from replay start) of the inbound request that\n * dispatched this outbound call, forwarded from {@link VIRTUAL_TIME_HEADER}.\n * The sidecar clusters candidates by recorded timestamp against this value.\n *\n * Additive and non-version-bumping: an older shim omits it, and matching\n * then hashes across every candidate at the matched key.\n */\n virtualTimeMs?: number;\n}\n\n/**\n * A recorded response to serve, or a signal the caller must fail the request. Replay is\n * hermetic, so the shim never turns any of these into a real call — nor an outcome it does\n * not recognise. The only live path is the request-side `meticulous-passthrough` header,\n * handled before the lookup, not any response here.\n */\nexport type OutboundFetchLookupResponse =\n | {\n outcome: \"mock\";\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n }\n /**\n * The store holds nothing for this call — a genuine miss, or a call the sidecar could not\n * look up at all (an unparseable URL). Replay is hermetic, so the shim fails the request\n * rather than letting it reach the real service — matching the Node backend recorder's\n * http/undici mocks. There is deliberately no \"let the call through\" outcome: the only\n * live path is the request-side `meticulous-passthrough` header.\n */\n | { outcome: \"no-mock\" };\n"],"names":[],"mappings":";;;;;;;;AAMA,MAAa,2BAA2B;;AAGxC,MAAa,kCACX;;;;;AAMF,MAAa,6BAA6B;;AAG1C,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,sBAAsB;;;;;AAMnC,MAAa,sBACX,UACuB;AACvB,KAAI,SAAS,QAAQ,UAAU,GAC7B;CAEF,MAAM,SAAS,OAAO,MAAM;AAC5B,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC;AAEF,QAAO;;;;;;;;;;;AAYT,MAAa,gCAAgC;AAE7C,MAAa,sBAAsB;;;;;;AAOnC,MAAa,8BAA8B;AAC3C,MAAa,qCAAqC;;;;;;;;AASlD,MAAa,+BAA+B;;;;;;;;;AAU5C,MAAa,4BACX;;;;;;;;AASF,MAAa,8BAA8B;;;;;;;;;;;AAY3C,MAAa,mBAAmB,CAC9B,gBACA,2BACD","debugId":"7851ef28-53e0-596f-80dc-a26f37790747"}
1
+ {"version":3,"file":"protocol.js","names":[],"sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Wire protocol between the in-worker shim and the Meticulous backend\n * recorder sidecar. The sidecar imports these types (type-only), so this file\n * is the single source of truth for the shape of capture events.\n */\n\nexport const SIDECAR_PROTOCOL_VERSION = \"2\";\n\n/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */\nexport const SIDECAR_PROTOCOL_VERSION_HEADER =\n \"x-meticulous-sidecar-protocol-version\";\n\n/**\n * Header the Meticulous frontend recorder stamps on requests made by the app\n * under recording; its value ties backend spans to the frontend session.\n */\nexport const FRONTEND_SESSION_ID_HEADER = \"x-meticulous-session-id\";\n\n/** Header identifying one replay attempt of a recorded frontend session. */\nexport const REPLAY_ID_HEADER = \"x-meticulous-replay-id\";\n\n/**\n * Elapsed virtual time (ms from replay start) of the triggering frontend\n * request. Injected by the replay runner on app-url requests during backend\n * testing; the sidecar uses it to pick the closest recorded burst when several\n * candidates share a loosened match key.\n */\nexport const VIRTUAL_TIME_HEADER = \"x-meticulous-virtual-time\";\n\n/**\n * Parses {@link VIRTUAL_TIME_HEADER}. `0` is a valid start-of-session time;\n * anything non-finite or negative is ignored so a garbage header cannot skew matching.\n */\nexport const parseVirtualTimeMs = (\n value: string | null | undefined,\n): number | undefined => {\n if (value == null || value === \"\") {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return undefined;\n }\n return parsed;\n};\n\n/**\n * A request carrying this header set to `\"true\"` is never served from the recording and\n * never fails on a miss — it goes to the real network. Use it for a call that must be live\n * during replay (a health probe, telemetry, a resource the recording cannot cover).\n *\n * Same convention and value as the frontend network stubbing and the Node backend recorder\n * (`METICULOUS_PASSTHROUGH_HEADER` in `@alwaysmeticulous/backend-recorder-js`); the value\n * must stay in step with both.\n */\nexport const METICULOUS_PASSTHROUGH_HEADER = \"meticulous-passthrough\";\n\nexport const SIDECAR_EVENTS_PATH = \"/v1/events\";\n\n/**\n * Replay routes. A sidecar started in record mode answers 404 on both. Replay is hermetic,\n * so an unanswered outbound-fetch lookup fails the app's call rather than letting it reach\n * the real service — the same as any other failure to stub.\n */\nexport const SIDECAR_REPLAY_SESSION_PATH = \"/v1/replay/session\";\nexport const SIDECAR_REPLAY_OUTBOUND_FETCH_PATH = \"/v1/replay/outbound-fetch\";\n\n/**\n * Line-coverage reporting. Additive: an older sidecar answers 404, which the\n * shim treats as \"coverage not collected here\" and stops reporting for the life\n * of the isolate. Deliberately NOT guarded by a\n * {@link SIDECAR_PROTOCOL_VERSION} bump — that check is exact equality, so a\n * bump would stop every already-built customer image from mocking at all.\n */\nexport const SIDECAR_REPLAY_COVERAGE_PATH = \"/v1/replay/coverage\";\n\n/**\n * Header carrying the replay sidecar's origin, injected by the Meticulous replay runner\n * on requests to the app under test. Workerd cannot see container environment variables,\n * so an inbound header is the only way per-replay config can reach the shim.\n *\n * The shim validates the value before using it and only honours a loopback /\n * `host.docker.internal` / private-network `http:` origin — see `replay-sidecar-url.ts`.\n */\nexport const REPLAY_SIDECAR_URL_HEADER =\n \"x-meticulous-backend-replay-sidecar-url\";\n\n/**\n * Header the in-worker shim stamps on every app response (record and replay)\n * carrying the shim's npm package version. The replay runner reads this from\n * Puppeteer — workerd cannot see container env, and the execute pod cannot\n * reach the in-pod sidecar, so a response header is the only channel that\n * reports which bundled shim produced a replay's After screenshots.\n */\nexport const WORKERD_SHIM_VERSION_HEADER = \"x-meticulous-workerd-shim-version\";\n\n/**\n * The only headers persisted on capture events. Everything else — notably\n * authorization, cookie/set-cookie, and API-key headers — is dropped at\n * capture time, before it ever leaves the worker.\n *\n * The Node backend recorder persists the same two, plus the inbound request's\n * `Cookie` header on SERVER spans (`recorded-request-headers.ts`). This list\n * has not followed it there, so a workerd recording of an authenticated server\n * render still carries no session cookie.\n */\nexport const CAPTURED_HEADERS = [\n \"content-type\",\n FRONTEND_SESSION_ID_HEADER,\n] as const;\n\nexport interface CapturedBody {\n /** UTF-8 decoded body, truncated to the capture cap. */\n body: string;\n /** True when the body exceeded the capture cap or the read was cut short. */\n truncated: boolean;\n}\n\n/**\n * The fields every capture event carries to say which request, session and trace it belongs to.\n */\ninterface CorrelatedEvent {\n /** Correlates outbound events to the inbound request being handled. */\n requestId: string;\n /** Value of {@link FRONTEND_SESSION_ID_HEADER} on the inbound request, if any. */\n frontendSessionId?: string;\n /**\n * Trace the request's spans belong to (32 hex chars), minted by the shim.\n *\n * The shim mints it, rather than the sidecar deriving it from {@link requestId}, so span\n * assembly is stateless: a sidecar that had to remember `requestId → traceId` would give a\n * request's SERVER span and its CLIENT children different traces whenever it lost that memory\n * between two batches — which, for a Worker-hosted sidecar, an eviction can do at any moment.\n *\n * Optional so a sidecar keeps working against a shim that predates this field, in which case\n * it falls back to its own per-`requestId` map.\n */\n traceId?: string;\n /**\n * Span id of the request's SERVER span (16 hex chars), also minted by the shim: it is the\n * parent of every CLIENT span the request produced, and the inbound event's own span id.\n */\n serverSpanId?: string;\n}\n\ninterface BaseRequestEvent extends CorrelatedEvent {\n method: string;\n /** Full URL including query string. */\n url: string;\n /** Lower-cased header name → values. */\n requestHeaders: Record<string, string[]>;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the request failed before a response was produced. */\n error?: string;\n}\n\n/**\n * The worker's own inbound request (becomes a SERVER span). Bodies are not\n * captured, matching the Node backend recorder's SERVER-span contract.\n */\nexport interface InboundRequestEvent extends BaseRequestEvent {\n kind: \"inbound\";\n /**\n * Where {@link BaseRequestEvent.frontendSessionId} came from. Absent means the browser\n * minted it and sent it on the request, which is the normal case. `\"backend\"` means the\n * shim minted it for a document navigation the browser could not tag, and the page may or\n * may not have gone on to adopt it — a distinction ingestion needs, because an unadopted\n * backend-minted id names a session that will never exist and must be treated as unstamped.\n * The sidecar turns it into `meticulous.session_id_origin`.\n *\n * Optional, and therefore compatible with an older sidecar (which ignores it) — which is\n * why adding it does not bump {@link SIDECAR_PROTOCOL_VERSION}, a strict-equality check\n * whose bump would instead make this shim unusable against one.\n */\n sessionIdOrigin?: \"backend\";\n}\n\n/**\n * An outgoing `fetch` call made while handling an inbound request (becomes a\n * CLIENT span).\n */\nexport interface OutboundRequestEvent extends BaseRequestEvent {\n kind: \"outbound\";\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding, an assets binding, or a\n * Durable Object stub — made while handling an inbound request (becomes a CLIENT span).\n *\n * Request/Response-shaped like {@link OutboundRequestEvent}, but a distinct kind because the\n * call never leaves the isolate and its URL is whatever the caller invented. Keeping the two\n * apart means a binding call and real egress can never be confused for one another when\n * these spans are eventually mocked.\n */\nexport interface BindingRequestEvent extends BaseRequestEvent {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Absent when the instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` produces rather\n * than being a binding in its own right.\n */\n bindingName?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/** KV namespace methods the shim records. */\nexport type KvOperation = \"get\" | \"getWithMetadata\" | \"put\" | \"delete\" | \"list\";\n\n/**\n * Why a KV value is absent from an event: workerd handed it over in a form that cannot be\n * persisted as text. A `\"stream\"` value must not even be read — consuming it would take the\n * bytes away from the app — and binary values are skipped deliberately, since KV blobs are\n * large and would both bloat the recording and be mangled by UTF-8 capture.\n */\nexport type KvOmittedReason = \"binary\" | \"stream\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, made while handling an inbound request\n * (becomes a CLIENT span).\n *\n * KV is not HTTP: there is no method, URL or status code, so this event shares nothing with\n * {@link BaseRequestEvent} beyond correlation and timing. Values are JSON so that a single\n * `JSON.parse` reconstructs exactly what the app saw, whichever `type` the read asked for.\n */\nexport interface KvOperationEvent extends CorrelatedEvent {\n kind: \"kv\";\n /**\n * The `env` key the KV namespace was found under. Always present, unlike on a binding\n * event: a namespace has no factory equivalent to `DurableObjectNamespace.get()`, so one\n * that was never seen on `env` is not recorded at all.\n */\n bindingName: string;\n operation: KvOperation;\n /**\n * The key operated on, when it is a single string. Absent for `list` (which spans keys) and\n * for a bulk `get` (which takes an array) — both keep their arguments in {@link args}.\n */\n key?: string;\n /**\n * JSON of the call's arguments: the read `type`, the `put` options, the `list` selector.\n * A `put` value appears as `null` here — it is captured in {@link value} instead, so that\n * redaction applies to it.\n */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, with secret-looking fields redacted. */\n value?: CapturedBody;\n /**\n * JSON of what the operation returned: the value for `get`, `{value, metadata}` for\n * `getWithMetadata`, the key page for `list`. Absent for `put`/`delete`, which return\n * nothing, and for a value that could not be captured (see {@link omitted}).\n */\n result?: CapturedBody;\n /**\n * Why the operation's value is missing. Refers to the written value for `put` and to the\n * read value otherwise — a `put` has no result, so this is never ambiguous.\n */\n omitted?: KvOmittedReason;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the operation threw. */\n error?: string;\n}\n\n/**\n * One postgres.js query made while handling an inbound request (becomes a CLIENT span).\n *\n * Deliberately carries the same fields the Node recorder puts on a postgres.js span rather than\n * anything Worker-specific: `PostgresJsMockStore` builds its match key from those attributes at\n * load time, so a query recorded here replays through the existing Node mock path unchanged.\n * See `postgres/capture.ts` for the serialization, which both surfaces share.\n */\nexport interface PostgresQueryEvent extends CorrelatedEvent {\n kind: \"postgres\";\n /** SQL rebuilt from the tagged template's chunks, with `$1`, `$2`… placeholders. */\n queryText: string;\n /** JSON of the interpolated values, from `serializePostgresJsArgs`. */\n params: string;\n /** `\"\"`, `\"raw\"` or `\"values\"` — the row shape the query asked for. */\n rowMode: string;\n /** JSON of the resolved `Result`, capped at {@link MAX_POSTGRES_JS_RESULT_SIZE}. */\n result?: CapturedBody;\n /**\n * JSON of the rejection, from `serializePostgresJsError`. Set instead of {@link result}, and\n * distinct from the transport-level `error` string on the HTTP-shaped events: this one is a\n * recorded outcome the replay reproduces, not a note that capture failed.\n */\n errorJson?: string;\n startTimeMs: number;\n endTimeMs: number;\n}\n\nexport type CaptureEvent =\n | InboundRequestEvent\n | OutboundRequestEvent\n | BindingRequestEvent\n | KvOperationEvent\n | PostgresQueryEvent;\n\nexport interface CaptureEventsPayload {\n events: CaptureEvent[];\n}\n\n/**\n * Answer to `GET {SIDECAR_REPLAY_SESSION_PATH}?sessionId=<id>`: whether the sidecar holds\n * mocks for a session, and the virtual clock anchor to freeze `Date` at while serving it.\n */\nexport interface ReplaySessionInfoResponse {\n found: boolean;\n /**\n * Epoch ms of the session's last recorded activity. The worker freezes its clock here so\n * that credentials minted during the recording are already issued but not yet expired.\n */\n clockAnchorMs?: number;\n}\n\n/**\n * One request's line coverage, sent to {@link SIDECAR_REPLAY_COVERAGE_PATH}\n * after the response has been produced.\n *\n * `files` carries the build-time id→line map and is sent only on the first\n * report of an isolate: it is the same for every request the isolate serves and\n * is far larger than the hits themselves.\n */\nexport interface CoverageReportRequest {\n frontendSessionId: string;\n /** Global line ids marked during this request, ascending. */\n hitIds: number[];\n /** Present on an isolate's first report only. */\n files?: Array<{\n path: string;\n firstId: number;\n /** Inclusive 1-based line ranges, flattened as `[s1, e1, s2, e2, ...]`. */\n lineRanges: number[];\n }>;\n}\n\n/** Lookup for a single outbound `fetch`, sent to {@link SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}. */\nexport interface OutboundFetchLookupRequest {\n frontendSessionId: string;\n replayId: string;\n method: string;\n /** Full URL including query string. */\n url: string;\n /**\n * Request body as captured by `readBodyWithCap`. The sidecar hashes this itself, so the\n * hash it compares against a recording is derived from byte-identical input on both the\n * record and replay sides.\n */\n requestBody?: CapturedBody;\n /**\n * Elapsed virtual time (ms from replay start) of the inbound request that\n * dispatched this outbound call, forwarded from {@link VIRTUAL_TIME_HEADER}.\n * The sidecar clusters candidates by recorded timestamp against this value.\n *\n * Additive and non-version-bumping: an older shim omits it, and matching\n * then hashes across every candidate at the matched key.\n */\n virtualTimeMs?: number;\n}\n\n/**\n * A recorded response to serve, or a signal the caller must fail the request. Replay is\n * hermetic, so the shim never turns any of these into a real call — nor an outcome it does\n * not recognise. The only live path is the request-side `meticulous-passthrough` header,\n * handled before the lookup, not any response here.\n */\nexport type OutboundFetchLookupResponse =\n | {\n outcome: \"mock\";\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n }\n /**\n * The store holds nothing for this call — a genuine miss, or a call the sidecar could not\n * look up at all (an unparseable URL). Replay is hermetic, so the shim fails the request\n * rather than letting it reach the real service — matching the Node backend recorder's\n * http/undici mocks. There is deliberately no \"let the call through\" outcome: the only\n * live path is the request-side `meticulous-passthrough` header.\n */\n | { outcome: \"no-mock\" };\n"],"mappings":";;;;;;AAMA,MAAa,2BAA2B;;AAGxC,MAAa,kCACX;;;;;AAMF,MAAa,6BAA6B;;AAG1C,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,sBAAsB;;;;;AAMnC,MAAa,sBACX,UACuB;AACvB,KAAI,SAAS,QAAQ,UAAU,GAC7B;CAEF,MAAM,SAAS,OAAO,MAAM;AAC5B,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC;AAEF,QAAO;;;;;;;;;;;AAYT,MAAa,gCAAgC;AAE7C,MAAa,sBAAsB;;;;;;AAOnC,MAAa,8BAA8B;AAC3C,MAAa,qCAAqC;;;;;;;;AASlD,MAAa,+BAA+B;;;;;;;;;AAU5C,MAAa,4BACX;;;;;;;;AASF,MAAa,8BAA8B;;;;;;;;;;;AAY3C,MAAa,mBAAmB,CAC9B,gBACA,2BACD"}
@@ -1,5 +1,3 @@
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]="08993983-84c7-52a8-95ae-69986b1ed730")}catch(e){}}();
3
1
  import "./protocol.js";
4
2
  //#region src/provisional-session-id.ts
5
3
  const SERVER_TIMING_HEADER = "server-timing";
@@ -71,5 +69,4 @@ const isDocumentNavigation = (getHeader) => {
71
69
  //#endregion
72
70
  export { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId };
73
71
 
74
- //# sourceMappingURL=provisional-session-id.js.map
75
- //# debugId=08993983-84c7-52a8-95ae-69986b1ed730
72
+ //# sourceMappingURL=provisional-session-id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provisional-session-id.js","sources":["../src/provisional-session-id.ts"],"sourcesContent":["import { FRONTEND_SESSION_ID_HEADER } from \"./protocol\";\n\n/**\n * A session id the *backend* mints, for the one request that can never carry one from the\n * browser: a top-level document navigation. Browsers cannot add custom headers to a\n * navigation, and on the session-starting navigation the frontend has not minted an id yet\n * (it does so only once the HTML has arrived and the snippet has run) — so the server-side\n * render's spans are recorded against no session and nothing about that traffic can be\n * replayed exactly.\n *\n * Everything here is runtime-agnostic on purpose. Both backend recorders mint the same ids\n * by the same rule and publish them under the same `Server-Timing` metric, and all three are\n * wire contracts with the page, so there is exactly one implementation — the same reason\n * kv-capture.ts lives here rather than being written twice. The Node recorder imports these\n * from the package entry; see `provisional-session-id.ts` in\n * `@alwaysmeticulous/backend-recorder-js` for the `http.IncomingMessage` adapter, and\n * `publish-session-id.ts` next door for the workerd one.\n */\n\n/**\n * Reads one request header by its lower-cased name. Both `IncomingMessage.headers[name]`\n * (after the caller flattens Node's `string | string[]`) and `Headers.get(name)` satisfy it,\n * which is what lets the rules below be shared.\n */\nexport type HeaderReader = (name: string) => string | null | undefined;\n\nexport const SERVER_TIMING_HEADER = \"server-timing\";\n\n/**\n * The `Server-Timing` metric name carrying a backend-minted session id to the page. A wire\n * contract with the frontend recorder, which matches on it case-insensitively when reading\n * `PerformanceNavigationTiming.serverTiming` (see `resolve-session-id.ts` in\n * packages/recorder). Must be a valid HTTP token — no `.` or `:`, which is why it is not the\n * dotted span-attribute name.\n */\nexport const SERVER_TIMING_SESSION_METRIC = \"metsession\";\n\n/**\n * `Server-Timing` is the one response header a document's own JavaScript can read back, and\n * the navigation entry persists for the document's lifetime — so unlike a header-sniffing\n * approach this works no matter how late the snippet loads.\n */\nexport const buildServerTimingSessionEntry = (sessionId: string): string =>\n `${SERVER_TIMING_SESSION_METRIC};desc=\"${sessionId}\"`;\n\n/**\n * nanoid's default alphabet and length, reproduced rather than imported: this package carries\n * no runtime dependencies, and an id that did not look like every other session id would be a\n * gratuitous difference in S3 keys and URLs. 64 characters divides 256 exactly, so masking a\n * random byte with 63 is uniform — which is how nanoid itself generates them.\n */\nconst ID_ALPHABET =\n \"ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW\";\nconst ID_LENGTH = 21;\n\n/**\n * Mints an id in exactly the frontend recorder's format: `<ISO timestamp>_<nanoid>` (see\n * `createConfig` in packages/recorder). The format is a wire contract, not cosmetic —\n * ingestion parses the ISO prefix back out to anchor the inferred-attachment window\n * (`parseTimestampFromFrontendSessionId` in lambda-upload-sessions), and the page uses it to\n * reject an id stale enough to have come from a cache.\n */\nexport const mintProvisionalSessionId = (): string => {\n const bytes = new Uint8Array(ID_LENGTH);\n crypto.getRandomValues(bytes);\n let suffix = \"\";\n for (const byte of bytes) {\n suffix += ID_ALPHABET[byte & 63];\n }\n return `${new Date().toISOString()}_${suffix}`;\n};\n\n/**\n * Whether to mint for this inbound request. Deliberately narrow: an id nothing adopts is\n * worse than no id at all, because a span stamped with a session that never materialises is\n * excluded from the time-window fallback that would otherwise have caught it. So we mint only\n * for what is plausibly a browser navigating to a page.\n *\n * The caller decides separately whether minting is enabled at all.\n */\nexport const isProvisionalSessionIdCandidate = (\n method: string,\n getHeader: HeaderReader,\n): boolean => {\n // A request that already names its session needs nothing from us.\n if (getHeader(FRONTEND_SESSION_ID_HEADER) != null) {\n return false;\n }\n const normalizedMethod = method.toUpperCase();\n if (normalizedMethod !== \"GET\" && normalizedMethod !== \"HEAD\") {\n return false;\n }\n return isDocumentNavigation(getHeader);\n};\n\n/**\n * `Sec-Fetch-Dest` is the exact signal and every current browser sends it: `document` for a\n * navigation, `empty` for an in-page fetch (so a Next.js RSC navigation, which is a fetch, is\n * correctly excluded), `iframe` for a subframe — excluded too, since the top frame owns the\n * session and an iframe recorder reports into it.\n *\n * Absent, the caller is an older browser or not a browser at all (curl, a health check, a\n * crawler), and `Accept` is the only hint left.\n */\nconst isDocumentNavigation = (getHeader: HeaderReader): boolean => {\n const dest = getHeader(\"sec-fetch-dest\");\n if (typeof dest === \"string\") {\n return dest === \"document\";\n }\n const accept = getHeader(\"accept\");\n return typeof accept === \"string\" && accept.includes(\"text/html\");\n};\n"],"names":[],"mappings":";;;;AA0BA,MAAa,uBAAuB;;;;;;;;AASpC,MAAa,+BAA+B;;;;;;AAO5C,MAAa,iCAAiC,cAC5C,GAAG,6BAA6B,SAAS,UAAU;;;;;;;AAQrD,MAAM,cACJ;AACF,MAAM,YAAY;;;;;;;;AASlB,MAAa,iCAAyC;CACpD,MAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,QAAO,gBAAgB,MAAM;CAC7B,IAAI,SAAS;AACb,MAAK,MAAM,QAAQ,MACjB,WAAU,YAAY,OAAO;AAE/B,QAAO,oBAAG,IAAI,MAAM,EAAC,aAAa,CAAC,GAAG;;;;;;;;;;AAWxC,MAAa,mCACX,QACA,cACY;AAEZ,KAAI,UAAA,0BAAqC,IAAI,KAC3C,QAAO;CAET,MAAM,mBAAmB,OAAO,aAAa;AAC7C,KAAI,qBAAqB,SAAS,qBAAqB,OACrD,QAAO;AAET,QAAO,qBAAqB,UAAU;;;;;;;;;;;AAYxC,MAAM,wBAAwB,cAAqC;CACjE,MAAM,OAAO,UAAU,iBAAiB;AACxC,KAAI,OAAO,SAAS,SAClB,QAAO,SAAS;CAElB,MAAM,SAAS,UAAU,SAAS;AAClC,QAAO,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY","debugId":"08993983-84c7-52a8-95ae-69986b1ed730"}
1
+ {"version":3,"file":"provisional-session-id.js","names":[],"sources":["../src/provisional-session-id.ts"],"sourcesContent":["import { FRONTEND_SESSION_ID_HEADER } from \"./protocol\";\n\n/**\n * A session id the *backend* mints, for the one request that can never carry one from the\n * browser: a top-level document navigation. Browsers cannot add custom headers to a\n * navigation, and on the session-starting navigation the frontend has not minted an id yet\n * (it does so only once the HTML has arrived and the snippet has run) — so the server-side\n * render's spans are recorded against no session and nothing about that traffic can be\n * replayed exactly.\n *\n * Everything here is runtime-agnostic on purpose. Both backend recorders mint the same ids\n * by the same rule and publish them under the same `Server-Timing` metric, and all three are\n * wire contracts with the page, so there is exactly one implementation — the same reason\n * kv-capture.ts lives here rather than being written twice. The Node recorder imports these\n * from the package entry; see `provisional-session-id.ts` in\n * `@alwaysmeticulous/backend-recorder-js` for the `http.IncomingMessage` adapter, and\n * `publish-session-id.ts` next door for the workerd one.\n */\n\n/**\n * Reads one request header by its lower-cased name. Both `IncomingMessage.headers[name]`\n * (after the caller flattens Node's `string | string[]`) and `Headers.get(name)` satisfy it,\n * which is what lets the rules below be shared.\n */\nexport type HeaderReader = (name: string) => string | null | undefined;\n\nexport const SERVER_TIMING_HEADER = \"server-timing\";\n\n/**\n * The `Server-Timing` metric name carrying a backend-minted session id to the page. A wire\n * contract with the frontend recorder, which matches on it case-insensitively when reading\n * `PerformanceNavigationTiming.serverTiming` (see `resolve-session-id.ts` in\n * packages/recorder). Must be a valid HTTP token — no `.` or `:`, which is why it is not the\n * dotted span-attribute name.\n */\nexport const SERVER_TIMING_SESSION_METRIC = \"metsession\";\n\n/**\n * `Server-Timing` is the one response header a document's own JavaScript can read back, and\n * the navigation entry persists for the document's lifetime — so unlike a header-sniffing\n * approach this works no matter how late the snippet loads.\n */\nexport const buildServerTimingSessionEntry = (sessionId: string): string =>\n `${SERVER_TIMING_SESSION_METRIC};desc=\"${sessionId}\"`;\n\n/**\n * nanoid's default alphabet and length, reproduced rather than imported: this package carries\n * no runtime dependencies, and an id that did not look like every other session id would be a\n * gratuitous difference in S3 keys and URLs. 64 characters divides 256 exactly, so masking a\n * random byte with 63 is uniform — which is how nanoid itself generates them.\n */\nconst ID_ALPHABET =\n \"ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW\";\nconst ID_LENGTH = 21;\n\n/**\n * Mints an id in exactly the frontend recorder's format: `<ISO timestamp>_<nanoid>` (see\n * `createConfig` in packages/recorder). The format is a wire contract, not cosmetic —\n * ingestion parses the ISO prefix back out to anchor the inferred-attachment window\n * (`parseTimestampFromFrontendSessionId` in lambda-upload-sessions), and the page uses it to\n * reject an id stale enough to have come from a cache.\n */\nexport const mintProvisionalSessionId = (): string => {\n const bytes = new Uint8Array(ID_LENGTH);\n crypto.getRandomValues(bytes);\n let suffix = \"\";\n for (const byte of bytes) {\n suffix += ID_ALPHABET[byte & 63];\n }\n return `${new Date().toISOString()}_${suffix}`;\n};\n\n/**\n * Whether to mint for this inbound request. Deliberately narrow: an id nothing adopts is\n * worse than no id at all, because a span stamped with a session that never materialises is\n * excluded from the time-window fallback that would otherwise have caught it. So we mint only\n * for what is plausibly a browser navigating to a page.\n *\n * The caller decides separately whether minting is enabled at all.\n */\nexport const isProvisionalSessionIdCandidate = (\n method: string,\n getHeader: HeaderReader,\n): boolean => {\n // A request that already names its session needs nothing from us.\n if (getHeader(FRONTEND_SESSION_ID_HEADER) != null) {\n return false;\n }\n const normalizedMethod = method.toUpperCase();\n if (normalizedMethod !== \"GET\" && normalizedMethod !== \"HEAD\") {\n return false;\n }\n return isDocumentNavigation(getHeader);\n};\n\n/**\n * `Sec-Fetch-Dest` is the exact signal and every current browser sends it: `document` for a\n * navigation, `empty` for an in-page fetch (so a Next.js RSC navigation, which is a fetch, is\n * correctly excluded), `iframe` for a subframe — excluded too, since the top frame owns the\n * session and an iframe recorder reports into it.\n *\n * Absent, the caller is an older browser or not a browser at all (curl, a health check, a\n * crawler), and `Accept` is the only hint left.\n */\nconst isDocumentNavigation = (getHeader: HeaderReader): boolean => {\n const dest = getHeader(\"sec-fetch-dest\");\n if (typeof dest === \"string\") {\n return dest === \"document\";\n }\n const accept = getHeader(\"accept\");\n return typeof accept === \"string\" && accept.includes(\"text/html\");\n};\n"],"mappings":";;AA0BA,MAAa,uBAAuB;;;;;;;;AASpC,MAAa,+BAA+B;;;;;;AAO5C,MAAa,iCAAiC,cAC5C,GAAG,6BAA6B,SAAS,UAAU;;;;;;;AAQrD,MAAM,cACJ;AACF,MAAM,YAAY;;;;;;;;AASlB,MAAa,iCAAyC;CACpD,MAAM,QAAQ,IAAI,WAAW,UAAU;AACvC,QAAO,gBAAgB,MAAM;CAC7B,IAAI,SAAS;AACb,MAAK,MAAM,QAAQ,MACjB,WAAU,YAAY,OAAO;AAE/B,QAAO,oBAAG,IAAI,MAAM,EAAC,aAAa,CAAC,GAAG;;;;;;;;;;AAWxC,MAAa,mCACX,QACA,cACY;AAEZ,KAAI,UAAA,0BAAqC,IAAI,KAC3C,QAAO;CAET,MAAM,mBAAmB,OAAO,aAAa;AAC7C,KAAI,qBAAqB,SAAS,qBAAqB,OACrD,QAAO;AAET,QAAO,qBAAqB,UAAU;;;;;;;;;;;AAYxC,MAAM,wBAAwB,cAAqC;CACjE,MAAM,OAAO,UAAU,iBAAiB;AACxC,KAAI,OAAO,SAAS,SAClB,QAAO,SAAS;CAElB,MAAM,SAAS,UAAU,SAAS;AAClC,QAAO,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY"}
@@ -1,5 +1,3 @@
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]="c7b8eed0-1eb8-5c3b-9d7f-7dec5248e263")}catch(e){}}();
3
1
  import { warnOnce } from "./log.js";
4
2
  import { hasWebSocket } from "./outbound-capture.js";
5
3
  import { SERVER_TIMING_HEADER, buildServerTimingSessionEntry } from "./provisional-session-id.js";
@@ -37,5 +35,4 @@ const publishSessionIdOnResponse = (response, sessionId) => {
37
35
  //#endregion
38
36
  export { publishSessionIdOnResponse };
39
37
 
40
- //# sourceMappingURL=publish-session-id.js.map
41
- //# debugId=c7b8eed0-1eb8-5c3b-9d7f-7dec5248e263
38
+ //# sourceMappingURL=publish-session-id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-session-id.js","sources":["../src/publish-session-id.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { hasWebSocket } from \"./outbound-capture\";\nimport {\n SERVER_TIMING_HEADER,\n buildServerTimingSessionEntry,\n} from \"./provisional-session-id\";\n\n/**\n * Publishes a backend-minted session id to the page on the response it is about to receive,\n * so the frontend recorder adopts it instead of minting its own and the whole page load —\n * server-side render included — ends up under one session id.\n *\n * Returns the response to send, which is not always the one passed in: a `Response` handed\n * back by `fetch` or by an assets binding has immutable headers, and the only way to add one\n * is to rebuild around the same body. Best-effort throughout — a recorder must never break\n * the customer's app, so anything unexpected returns the original response unchanged.\n *\n * Unlike the Node recorder there is no `headersSent` race to lose: workerd flushes the\n * response only once the handler has returned it to us, so even a streaming SSR body still\n * has its headers in front of it at this point.\n */\nexport const publishSessionIdOnResponse = (\n response: Response,\n sessionId: string,\n): Response => {\n const entry = buildServerTimingSessionEntry(sessionId);\n\n try {\n // Appends rather than replaces: apps publish their own server timings, and clobbering\n // them would be a visible regression in their tooling.\n response.headers.append(SERVER_TIMING_HEADER, entry);\n return response;\n } catch {\n // Immutable headers. Fall through and rebuild.\n }\n\n try {\n // A WebSocket upgrade carries the socket rather than a body and cannot survive being\n // rebuilt; it is never a document navigation either, so there is nothing to publish to.\n if (hasWebSocket(response) || response.status === 101) {\n return response;\n }\n const rebuilt = new Response(response.body, response);\n rebuilt.headers.append(SERVER_TIMING_HEADER, entry);\n return rebuilt;\n } catch (error) {\n warnOnce(\n \"publish-session-id\",\n \"Failed to publish the backend-minted session id on the response.\",\n error,\n );\n return response;\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,8BACX,UACA,cACa;CACb,MAAM,QAAQ,8BAA8B,UAAU;AAEtD,KAAI;AAGF,WAAS,QAAQ,OAAO,sBAAsB,MAAM;AACpD,SAAO;SACD;AAIR,KAAI;AAGF,MAAI,aAAa,SAAS,IAAI,SAAS,WAAW,IAChD,QAAO;EAET,MAAM,UAAU,IAAI,SAAS,SAAS,MAAM,SAAS;AACrD,UAAQ,QAAQ,OAAO,sBAAsB,MAAM;AACnD,SAAO;UACA,OAAO;AACd,WACE,sBACA,oEACA,MACD;AACD,SAAO","debugId":"c7b8eed0-1eb8-5c3b-9d7f-7dec5248e263"}
1
+ {"version":3,"file":"publish-session-id.js","names":[],"sources":["../src/publish-session-id.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { hasWebSocket } from \"./outbound-capture\";\nimport {\n SERVER_TIMING_HEADER,\n buildServerTimingSessionEntry,\n} from \"./provisional-session-id\";\n\n/**\n * Publishes a backend-minted session id to the page on the response it is about to receive,\n * so the frontend recorder adopts it instead of minting its own and the whole page load —\n * server-side render included — ends up under one session id.\n *\n * Returns the response to send, which is not always the one passed in: a `Response` handed\n * back by `fetch` or by an assets binding has immutable headers, and the only way to add one\n * is to rebuild around the same body. Best-effort throughout — a recorder must never break\n * the customer's app, so anything unexpected returns the original response unchanged.\n *\n * Unlike the Node recorder there is no `headersSent` race to lose: workerd flushes the\n * response only once the handler has returned it to us, so even a streaming SSR body still\n * has its headers in front of it at this point.\n */\nexport const publishSessionIdOnResponse = (\n response: Response,\n sessionId: string,\n): Response => {\n const entry = buildServerTimingSessionEntry(sessionId);\n\n try {\n // Appends rather than replaces: apps publish their own server timings, and clobbering\n // them would be a visible regression in their tooling.\n response.headers.append(SERVER_TIMING_HEADER, entry);\n return response;\n } catch {\n // Immutable headers. Fall through and rebuild.\n }\n\n try {\n // A WebSocket upgrade carries the socket rather than a body and cannot survive being\n // rebuilt; it is never a document navigation either, so there is nothing to publish to.\n if (hasWebSocket(response) || response.status === 101) {\n return response;\n }\n const rebuilt = new Response(response.body, response);\n rebuilt.headers.append(SERVER_TIMING_HEADER, entry);\n return rebuilt;\n } catch (error) {\n warnOnce(\n \"publish-session-id\",\n \"Failed to publish the backend-minted session id on the response.\",\n error,\n );\n return response;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAa,8BACX,UACA,cACa;CACb,MAAM,QAAQ,8BAA8B,UAAU;AAEtD,KAAI;AAGF,WAAS,QAAQ,OAAO,sBAAsB,MAAM;AACpD,SAAO;SACD;AAIR,KAAI;AAGF,MAAI,aAAa,SAAS,IAAI,SAAS,WAAW,IAChD,QAAO;EAET,MAAM,UAAU,IAAI,SAAS,SAAS,MAAM,SAAS;AACrD,UAAQ,QAAQ,OAAO,sBAAsB,MAAM;AACnD,SAAO;UACA,OAAO;AACd,WACE,sBACA,oEACA,MACD;AACD,SAAO"}
@@ -1,5 +1,3 @@
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]="1cb92f56-68e8-5791-b6c7-c8bca161b130")}catch(e){}}();
3
1
  import { warnOnce } from "./log.js";
4
2
  import { WORKERD_SHIM_VERSION_HEADER } from "./protocol.js";
5
3
  import { hasWebSocket } from "./outbound-capture.js";
@@ -33,5 +31,4 @@ const publishWorkerdShimVersionOnResponse = (response) => {
33
31
  //#endregion
34
32
  export { publishWorkerdShimVersionOnResponse };
35
33
 
36
- //# sourceMappingURL=publish-shim-version.js.map
37
- //# debugId=1cb92f56-68e8-5791-b6c7-c8bca161b130
34
+ //# sourceMappingURL=publish-shim-version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-shim-version.js","sources":["../src/publish-shim-version.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { hasWebSocket } from \"./outbound-capture\";\nimport { WORKERD_SHIM_VERSION_HEADER } from \"./protocol\";\nimport { WORKERD_SHIM_VERSION } from \"./version\";\n\n/**\n * Stamps this shim's npm package version on the response the browser is about\n * to receive, so the replay runner can tell which bundled shim produced the\n * After screenshots.\n *\n * Returns the response to send, which is not always the one passed in: a\n * `Response` handed back by `fetch` or by an assets binding has immutable\n * headers, and the only way to add one is to rebuild around the same body.\n * Best-effort throughout — a recorder must never break the customer's app.\n */\nexport const publishWorkerdShimVersionOnResponse = (\n response: Response,\n): Response => {\n try {\n response.headers.set(WORKERD_SHIM_VERSION_HEADER, WORKERD_SHIM_VERSION);\n return response;\n } catch {\n // Immutable headers. Fall through and rebuild.\n }\n\n try {\n // A WebSocket upgrade carries the socket rather than a body and cannot\n // survive being rebuilt.\n if (hasWebSocket(response) || response.status === 101) {\n return response;\n }\n const rebuilt = new Response(response.body, response);\n rebuilt.headers.set(WORKERD_SHIM_VERSION_HEADER, WORKERD_SHIM_VERSION);\n return rebuilt;\n } catch (error) {\n warnOnce(\n \"publish-shim-version\",\n \"Failed to publish the workerd shim version on the response.\",\n error,\n );\n return response;\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAeA,MAAa,uCACX,aACa;AACb,KAAI;AACF,WAAS,QAAQ,IAAI,6BAA6B,qBAAqB;AACvE,SAAO;SACD;AAIR,KAAI;AAGF,MAAI,aAAa,SAAS,IAAI,SAAS,WAAW,IAChD,QAAO;EAET,MAAM,UAAU,IAAI,SAAS,SAAS,MAAM,SAAS;AACrD,UAAQ,QAAQ,IAAI,6BAA6B,qBAAqB;AACtE,SAAO;UACA,OAAO;AACd,WACE,wBACA,+DACA,MACD;AACD,SAAO","debugId":"1cb92f56-68e8-5791-b6c7-c8bca161b130"}
1
+ {"version":3,"file":"publish-shim-version.js","names":[],"sources":["../src/publish-shim-version.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { hasWebSocket } from \"./outbound-capture\";\nimport { WORKERD_SHIM_VERSION_HEADER } from \"./protocol\";\nimport { WORKERD_SHIM_VERSION } from \"./version\";\n\n/**\n * Stamps this shim's npm package version on the response the browser is about\n * to receive, so the replay runner can tell which bundled shim produced the\n * After screenshots.\n *\n * Returns the response to send, which is not always the one passed in: a\n * `Response` handed back by `fetch` or by an assets binding has immutable\n * headers, and the only way to add one is to rebuild around the same body.\n * Best-effort throughout — a recorder must never break the customer's app.\n */\nexport const publishWorkerdShimVersionOnResponse = (\n response: Response,\n): Response => {\n try {\n response.headers.set(WORKERD_SHIM_VERSION_HEADER, WORKERD_SHIM_VERSION);\n return response;\n } catch {\n // Immutable headers. Fall through and rebuild.\n }\n\n try {\n // A WebSocket upgrade carries the socket rather than a body and cannot\n // survive being rebuilt.\n if (hasWebSocket(response) || response.status === 101) {\n return response;\n }\n const rebuilt = new Response(response.body, response);\n rebuilt.headers.set(WORKERD_SHIM_VERSION_HEADER, WORKERD_SHIM_VERSION);\n return rebuilt;\n } catch (error) {\n warnOnce(\n \"publish-shim-version\",\n \"Failed to publish the workerd shim version on the response.\",\n error,\n );\n return response;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,uCACX,aACa;AACb,KAAI;AACF,WAAS,QAAQ,IAAI,6BAA6B,qBAAqB;AACvE,SAAO;SACD;AAIR,KAAI;AAGF,MAAI,aAAa,SAAS,IAAI,SAAS,WAAW,IAChD,QAAO;EAET,MAAM,UAAU,IAAI,SAAS,SAAS,MAAM,SAAS;AACrD,UAAQ,QAAQ,IAAI,6BAA6B,qBAAqB;AACtE,SAAO;UACA,OAAO;AACd,WACE,wBACA,+DACA,MACD;AACD,SAAO"}
@@ -1,7 +1,5 @@
1
1
  //#region src/redact-body.ts
2
2
  /** Replacement for a redacted value. Matches the Node recorder's redaction marker. */
3
-
4
- !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]="c9ea8914-5f33-5a57-a950-20eae083e1d7")}catch(e){}}();
5
3
  const STR_REDACTED = "REDACTED";
6
4
  const secretKeys = new Set([
7
5
  "accesstoken",
@@ -49,5 +47,4 @@ const redactValue = (value) => {
49
47
  //#endregion
50
48
  export { STR_REDACTED, redactRequestBody };
51
49
 
52
- //# sourceMappingURL=redact-body.js.map
53
- //# debugId=c9ea8914-5f33-5a57-a950-20eae083e1d7
50
+ //# sourceMappingURL=redact-body.js.map