@alwaysmeticulous/backend-recorder-workerd 2.324.0 → 2.326.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +94 -4
  2. package/dist/binding-patch.js +17 -8
  3. package/dist/binding-patch.js.map +1 -1
  4. package/dist/capture-buffer.js +114 -0
  5. package/dist/capture-buffer.js.map +1 -0
  6. package/dist/context.js +6 -3
  7. package/dist/context.js.map +1 -1
  8. package/dist/error-capture.d.ts +44 -0
  9. package/dist/error-capture.js +65 -0
  10. package/dist/error-capture.js.map +1 -0
  11. package/dist/fetch-patch.js +5 -5
  12. package/dist/fetch-patch.js.map +1 -1
  13. package/dist/index.d.ts +63 -39
  14. package/dist/index.js +0 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/kv-capture.d.ts +39 -0
  17. package/dist/kv-capture.js +92 -0
  18. package/dist/kv-capture.js.map +1 -0
  19. package/dist/kv-patch.js +21 -94
  20. package/dist/kv-patch.js.map +1 -1
  21. package/dist/outbound-capture.js +20 -28
  22. package/dist/outbound-capture.js.map +1 -1
  23. package/dist/pages-function.d.ts +33 -0
  24. package/dist/pages-function.js +28 -0
  25. package/dist/pages-function.js.map +1 -0
  26. package/dist/postgres/capture.d.ts +108 -0
  27. package/dist/postgres/capture.js +136 -0
  28. package/dist/postgres/capture.js.map +1 -0
  29. package/dist/postgres/with-meticulous-postgres.d.ts +32 -0
  30. package/dist/postgres/with-meticulous-postgres.js +152 -0
  31. package/dist/postgres/with-meticulous-postgres.js.map +1 -0
  32. package/dist/protocol.d.ts +76 -11
  33. package/dist/protocol.js +12 -7
  34. package/dist/protocol.js.map +1 -1
  35. package/dist/provisional-session-id.d.ts +58 -0
  36. package/dist/provisional-session-id.js +75 -0
  37. package/dist/provisional-session-id.js.map +1 -0
  38. package/dist/publish-session-id.js +41 -0
  39. package/dist/publish-session-id.js.map +1 -0
  40. package/dist/replay-fetch.js +4 -3
  41. package/dist/replay-fetch.js.map +1 -1
  42. package/dist/sidecar-client.js +29 -18
  43. package/dist/sidecar-client.js.map +1 -1
  44. package/dist/sidecar-transport.d.ts +38 -0
  45. package/dist/sidecar-transport.js +52 -0
  46. package/dist/sidecar-transport.js.map +1 -0
  47. package/dist/spans/attributes.d.ts +84 -0
  48. package/dist/spans/attributes.js +88 -0
  49. package/dist/spans/attributes.js.map +1 -0
  50. package/dist/spans/readable-span.d.ts +78 -0
  51. package/dist/spans/readable-span.js +35 -0
  52. package/dist/spans/readable-span.js.map +1 -0
  53. package/dist/spans/serialize-span.d.ts +14 -0
  54. package/dist/spans/serialize-span.js +38 -0
  55. package/dist/spans/serialize-span.js.map +1 -0
  56. package/dist/spans/span-builder.d.ts +61 -0
  57. package/dist/spans/span-builder.js +250 -0
  58. package/dist/spans/span-builder.js.map +1 -0
  59. package/dist/spans/url-redaction.d.ts +14 -0
  60. package/dist/spans/url-redaction.js +40 -0
  61. package/dist/spans/url-redaction.js.map +1 -0
  62. package/dist/with-meticulous.d.ts +32 -0
  63. package/dist/with-meticulous.js +0 -0
  64. package/dist/with-meticulous.js.map +1 -0
  65. package/package.json +2 -1
@@ -1,10 +1,8 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="19b144de-8ce4-5457-bc5e-81d4e735a7bf")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9a4a84d0-427b-51fb-af59-6197c25b1671")}catch(e){}}();
3
3
  import { warnOnce } from "./log.js";
4
4
  import { readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
5
- import { getOriginalFetch } from "./original-fetch.js";
6
5
  import { CAPTURED_HEADERS } from "./protocol.js";
7
- import { postCaptureEvents } from "./sidecar-client.js";
8
6
  //#region src/outbound-capture.ts
9
7
  const capturedHeaders = new Set(CAPTURED_HEADERS);
10
8
  const headersToRecord = (headers) => {
@@ -60,18 +58,17 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
60
58
  const frozenMeta = meta;
61
59
  const endTimeMs = Date.now();
62
60
  const errorMessage = String(error);
63
- reportInBackground(ctx, async () => {
61
+ ctx.buffer.track(async () => {
64
62
  const requestBody = await requestBodyPromise;
65
- await report(ctx, transport, {
63
+ report(ctx, transport, {
66
64
  ...frozenMeta,
67
- requestId: ctx.requestId,
68
- ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
65
+ ...correlation(ctx),
69
66
  ...requestBody !== void 0 ? { requestBody } : {},
70
67
  startTimeMs,
71
68
  endTimeMs,
72
69
  error: errorMessage
73
70
  });
74
- });
71
+ }, "outbound-error-report", "Failed to report a failed outbound request.");
75
72
  }
76
73
  throw error;
77
74
  }
@@ -83,12 +80,11 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
83
80
  const responseHeaders = headersToRecord(response.headers);
84
81
  const statusCode = response.status;
85
82
  const endTimeMs = Date.now();
86
- ctx.waitUntil((async () => {
83
+ ctx.buffer.track(async () => {
87
84
  const [requestBody, responseBody] = await Promise.all([requestBodyPromise, readBodyWithCap(responseClone.body).catch(() => void 0)]);
88
- await report(ctx, transport, {
85
+ report(ctx, transport, {
89
86
  ...frozenMeta,
90
- requestId: ctx.requestId,
91
- ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
87
+ ...correlation(ctx),
92
88
  responseHeaders,
93
89
  statusCode,
94
90
  ...requestBody !== void 0 ? { requestBody } : {},
@@ -96,32 +92,28 @@ const captureOutboundCall = async (ctx, transport, request, invoke) => {
96
92
  startTimeMs,
97
93
  endTimeMs
98
94
  });
99
- })().catch((error) => {
100
- warnOnce("outbound-response-capture", "Failed to capture an outbound response.", error);
101
- }));
95
+ }, "outbound-response-capture", "Failed to capture an outbound response.");
102
96
  } catch (error) {
103
97
  warnOnce("outbound-response-capture", "Failed to capture an outbound response.", error);
104
98
  }
105
99
  }
106
100
  return response;
107
101
  };
108
- const report = async (ctx, transport, fields) => {
102
+ /** The fields tying an event to its request, session and trace. */
103
+ const correlation = (ctx) => ({
104
+ requestId: ctx.requestId,
105
+ ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
106
+ traceId: ctx.traceId,
107
+ serverSpanId: ctx.serverSpanId
108
+ });
109
+ const report = (ctx, transport, fields) => {
109
110
  const event = buildEvent(transport, fields);
110
- await postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]);
111
+ ctx.buffer.add(event);
111
112
  };
112
113
  /** Detects a WebSocket upgrade response without assuming the property exists. */
113
114
  const hasWebSocket = (response) => response.webSocket != null;
114
- const reportInBackground = (ctx, reportFn) => {
115
- try {
116
- ctx.waitUntil(reportFn().catch((error) => {
117
- warnOnce("outbound-error-report", "Failed to report a failed outbound request.", error);
118
- }));
119
- } catch (error) {
120
- warnOnce("outbound-error-report", "Failed to report a failed outbound request.", error);
121
- }
122
- };
123
115
  //#endregion
124
- export { captureOutboundCall, headersToRecord };
116
+ export { captureOutboundCall, hasWebSocket, headersToRecord };
125
117
 
126
118
  //# sourceMappingURL=outbound-capture.js.map
127
- //# debugId=19b144de-8ce4-5457-bc5e-81d4e735a7bf
119
+ //# debugId=9a4a84d0-427b-51fb-af59-6197c25b1671
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-capture.js","sources":["../src/outbound-capture.ts"],"sourcesContent":["import { readBodyWithCap, readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n CAPTURED_HEADERS,\n type BindingRequestEvent,\n type CaptureEvent,\n type CapturedBody,\n type OutboundRequestEvent,\n} from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\n\n/**\n * Which transport an intercepted call left the isolate through. Both are\n * Request/Response-shaped, so they share the capture tee below and differ only in the\n * event they emit.\n */\nexport type OutboundTransport =\n | { kind: \"outbound\" }\n | {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Undefined when the instance was never\n * seen on `env` — e.g. a Durable Object stub, which is produced by `namespace.get()`\n * rather than being a binding itself.\n */\n bindingName: string | undefined;\n };\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\nexport const headersToRecord = (headers: Headers): Record<string, string[]> => {\n const record: Record<string, string[]> = {};\n headers.forEach((value, name) => {\n const lowerName = name.toLowerCase();\n if (capturedHeaders.has(lowerName)) {\n (record[lowerName] ??= []).push(value);\n }\n });\n return record;\n};\n\ninterface OutboundMeta {\n method: string;\n url: string;\n requestHeaders: Record<string, string[]>;\n}\n\ninterface EventFields extends OutboundMeta {\n requestId: string;\n frontendSessionId?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n error?: string;\n}\n\n/**\n * Builds the transport's event. An explicit two-arm branch rather than a spread of the\n * union, so the discriminant and the extra binding field stay obvious. (The shim has no\n * runtime dependencies, so `assertNever` is not available here.)\n */\nconst buildEvent = (\n transport: OutboundTransport,\n fields: EventFields,\n): OutboundRequestEvent | BindingRequestEvent =>\n transport.kind === \"binding\"\n ? {\n kind: \"binding\",\n ...(transport.bindingName !== undefined\n ? { bindingName: transport.bindingName }\n : {}),\n ...fields,\n }\n : { kind: \"outbound\", ...fields };\n\n/**\n * Records an outbound call without affecting it: the real transport is invoked directly\n * (the sidecar is never in the request path), and request/response clones are reported in\n * the background via `ctx.waitUntil`. Any capture failure is warned about once and\n * swallowed — recording must never break the app.\n *\n * `invoke` is what actually performs the call, so the caller keeps ownership of the\n * receiver (a binding's `fetch` must be applied to the binding, not to `globalThis`).\n */\nexport const captureOutboundCall = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n const startTimeMs = Date.now();\n let meta: OutboundMeta | undefined;\n let requestBodyPromise: Promise<CapturedBody | undefined> =\n Promise.resolve(undefined);\n try {\n meta = {\n method: request.method,\n url: request.url,\n requestHeaders: headersToRecord(request.headers),\n };\n if (request.body) {\n const bodyClone = request.clone();\n requestBodyPromise = readRequestBodyWithCap(bodyClone.body).catch(\n () => undefined,\n );\n }\n } catch (error) {\n warnOnce(\n \"outbound-request-capture\",\n \"Failed to capture an outbound request.\",\n error,\n );\n meta = undefined;\n }\n\n let response: Response;\n try {\n response = await invoke(request);\n } catch (error) {\n if (meta) {\n const frozenMeta = meta;\n const endTimeMs = Date.now();\n const errorMessage = String(error);\n reportInBackground(ctx, async () => {\n const requestBody = await requestBodyPromise;\n await report(ctx, transport, {\n ...frozenMeta,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n ...(requestBody !== undefined ? { requestBody } : {}),\n startTimeMs,\n endTimeMs,\n error: errorMessage,\n });\n });\n }\n throw error;\n }\n\n if (meta) {\n const frozenMeta = meta;\n try {\n // A WebSocket upgrade has no readable body and cannot be represented as a recorded\n // response, so it is left entirely alone.\n if (hasWebSocket(response)) {\n return response;\n }\n const responseClone = response.clone();\n const responseHeaders = headersToRecord(response.headers);\n const statusCode = response.status;\n const endTimeMs = Date.now();\n ctx.waitUntil(\n (async () => {\n const [requestBody, responseBody] = await Promise.all([\n requestBodyPromise,\n readBodyWithCap(responseClone.body).catch(() => undefined),\n ]);\n await report(ctx, transport, {\n ...frozenMeta,\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n responseHeaders,\n statusCode,\n ...(requestBody !== undefined ? { requestBody } : {}),\n ...(responseBody !== undefined ? { responseBody } : {}),\n startTimeMs,\n endTimeMs,\n });\n })().catch((error) => {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }\n }\n\n return response;\n};\n\nconst report = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n fields: EventFields,\n): Promise<void> => {\n const event: CaptureEvent = buildEvent(transport, fields);\n await postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]);\n};\n\n/** Detects a WebSocket upgrade response without assuming the property exists. */\nconst hasWebSocket = (response: Response): boolean =>\n (response as Response & { webSocket?: unknown }).webSocket != null;\n\nconst reportInBackground = (\n ctx: { waitUntil: (p: Promise<unknown>) => void },\n reportFn: () => Promise<void>,\n): void => {\n try {\n ctx.waitUntil(\n reportFn().catch((error) => {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":";;;;;;;;AA8BA,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;;;;;;;AA0BT,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;AAClC,sBAAmB,KAAK,YAAY;IAClC,MAAM,cAAc,MAAM;AAC1B,UAAM,OAAO,KAAK,WAAW;KAC3B,GAAG;KACH,WAAW,IAAI;KACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;KACN,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD;KACA;KACA,OAAO;KACR,CAAC;KACF;;AAEJ,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,WACD,YAAY;IACX,MAAM,CAAC,aAAa,gBAAgB,MAAM,QAAQ,IAAI,CACpD,oBACA,gBAAgB,cAAc,KAAK,CAAC,YAAY,KAAA,EAAU,CAC3D,CAAC;AACF,UAAM,OAAO,KAAK,WAAW;KAC3B,GAAG;KACH,WAAW,IAAI;KACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;KACN;KACA;KACA,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD,GAAI,iBAAiB,KAAA,IAAY,EAAE,cAAc,GAAG,EAAE;KACtD;KACA;KACD,CAAC;OACA,CAAC,OAAO,UAAU;AACpB,aACE,6BACA,2CACA,MACD;KACD,CACH;WACM,OAAO;AACd,YACE,6BACA,2CACA,MACD;;;AAIL,QAAO;;AAGT,MAAM,SAAS,OACb,KACA,WACA,WACkB;CAClB,MAAM,QAAsB,WAAW,WAAW,OAAO;AACzD,OAAM,kBAAkB,kBAAkB,EAAE,IAAI,YAAY,CAAC,MAAM,CAAC;;;AAItE,MAAM,gBAAgB,aACnB,SAAgD,aAAa;AAEhE,MAAM,sBACJ,KACA,aACS;AACT,KAAI;AACF,MAAI,UACF,UAAU,CAAC,OAAO,UAAU;AAC1B,YACE,yBACA,+CACA,MACD;IACD,CACH;UACM,OAAO;AACd,WACE,yBACA,+CACA,MACD","debugId":"19b144de-8ce4-5457-bc5e-81d4e735a7bf"}
1
+ {"version":3,"file":"outbound-capture.js","sources":["../src/outbound-capture.ts"],"sourcesContent":["import { readBodyWithCap, readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\nimport {\n CAPTURED_HEADERS,\n type BindingRequestEvent,\n type CaptureEvent,\n type CapturedBody,\n type OutboundRequestEvent,\n} from \"./protocol\";\n\n/**\n * Which transport an intercepted call left the isolate through. Both are\n * Request/Response-shaped, so they share the capture tee below and differ only in the\n * event they emit.\n */\nexport type OutboundTransport =\n | { kind: \"outbound\" }\n | {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Undefined when the instance was never\n * seen on `env` — e.g. a Durable Object stub, which is produced by `namespace.get()`\n * rather than being a binding itself.\n */\n bindingName: string | undefined;\n };\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\nexport const headersToRecord = (headers: Headers): Record<string, string[]> => {\n const record: Record<string, string[]> = {};\n headers.forEach((value, name) => {\n const lowerName = name.toLowerCase();\n if (capturedHeaders.has(lowerName)) {\n (record[lowerName] ??= []).push(value);\n }\n });\n return record;\n};\n\ninterface OutboundMeta {\n method: string;\n url: string;\n requestHeaders: Record<string, string[]>;\n}\n\ninterface EventFields extends OutboundMeta {\n requestId: string;\n frontendSessionId?: string;\n traceId: string;\n serverSpanId: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n error?: string;\n}\n\n/**\n * Builds the transport's event. An explicit two-arm branch rather than a spread of the\n * union, so the discriminant and the extra binding field stay obvious. (The shim has no\n * runtime dependencies, so `assertNever` is not available here.)\n */\nconst buildEvent = (\n transport: OutboundTransport,\n fields: EventFields,\n): OutboundRequestEvent | BindingRequestEvent =>\n transport.kind === \"binding\"\n ? {\n kind: \"binding\",\n ...(transport.bindingName !== undefined\n ? { bindingName: transport.bindingName }\n : {}),\n ...fields,\n }\n : { kind: \"outbound\", ...fields };\n\n/**\n * Records an outbound call without affecting it: the real transport is invoked directly\n * (the sidecar is never in the request path), and request/response clones are reported in\n * the background via `ctx.waitUntil`. Any capture failure is warned about once and\n * swallowed — recording must never break the app.\n *\n * `invoke` is what actually performs the call, so the caller keeps ownership of the\n * receiver (a binding's `fetch` must be applied to the binding, not to `globalThis`).\n */\nexport const captureOutboundCall = async (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n const startTimeMs = Date.now();\n let meta: OutboundMeta | undefined;\n let requestBodyPromise: Promise<CapturedBody | undefined> =\n Promise.resolve(undefined);\n try {\n meta = {\n method: request.method,\n url: request.url,\n requestHeaders: headersToRecord(request.headers),\n };\n if (request.body) {\n const bodyClone = request.clone();\n requestBodyPromise = readRequestBodyWithCap(bodyClone.body).catch(\n () => undefined,\n );\n }\n } catch (error) {\n warnOnce(\n \"outbound-request-capture\",\n \"Failed to capture an outbound request.\",\n error,\n );\n meta = undefined;\n }\n\n let response: Response;\n try {\n response = await invoke(request);\n } catch (error) {\n if (meta) {\n const frozenMeta = meta;\n const endTimeMs = Date.now();\n const errorMessage = String(error);\n // Tracked rather than reported directly: the request body is still being read, and the\n // buffer's `close()` is what waits for it before sending the batch.\n ctx.buffer.track(\n async () => {\n const requestBody = await requestBodyPromise;\n report(ctx, transport, {\n ...frozenMeta,\n ...correlation(ctx),\n ...(requestBody !== undefined ? { requestBody } : {}),\n startTimeMs,\n endTimeMs,\n error: errorMessage,\n });\n },\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n );\n }\n throw error;\n }\n\n if (meta) {\n const frozenMeta = meta;\n try {\n // A WebSocket upgrade has no readable body and cannot be represented as a recorded\n // response, so it is left entirely alone.\n if (hasWebSocket(response)) {\n return response;\n }\n const responseClone = response.clone();\n const responseHeaders = headersToRecord(response.headers);\n const statusCode = response.status;\n const endTimeMs = Date.now();\n ctx.buffer.track(\n async () => {\n const [requestBody, responseBody] = await Promise.all([\n requestBodyPromise,\n readBodyWithCap(responseClone.body).catch(() => undefined),\n ]);\n report(ctx, transport, {\n ...frozenMeta,\n ...correlation(ctx),\n responseHeaders,\n statusCode,\n ...(requestBody !== undefined ? { requestBody } : {}),\n ...(responseBody !== undefined ? { responseBody } : {}),\n startTimeMs,\n endTimeMs,\n });\n },\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n );\n } catch (error) {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }\n }\n\n return response;\n};\n\n/** The fields tying an event to its request, session and trace. */\nconst correlation = (\n ctx: RequestCaptureContext,\n): Pick<\n EventFields,\n \"requestId\" | \"frontendSessionId\" | \"traceId\" | \"serverSpanId\"\n> => ({\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n traceId: ctx.traceId,\n serverSpanId: ctx.serverSpanId,\n});\n\nconst report = (\n ctx: RequestCaptureContext,\n transport: OutboundTransport,\n fields: EventFields,\n): void => {\n const event: CaptureEvent = buildEvent(transport, fields);\n ctx.buffer.add(event);\n};\n\n/** Detects a WebSocket upgrade response without assuming the property exists. */\nexport const hasWebSocket = (response: Response): boolean =>\n (response as Response & { webSocket?: unknown }).webSocket != null;\n"],"names":[],"mappings":";;;;;;AA4BA,MAAM,kBAAkB,IAAI,IAAY,iBAAiB;AAEzD,MAAa,mBAAmB,YAA+C;CAC7E,MAAM,SAAmC,EAAE;AAC3C,SAAQ,SAAS,OAAO,SAAS;EAC/B,MAAM,YAAY,KAAK,aAAa;AACpC,MAAI,gBAAgB,IAAI,UAAU,CAChC,EAAC,OAAO,eAAe,EAAE,EAAE,KAAK,MAAM;GAExC;AACF,QAAO;;;;;;;AA4BT,MAAM,cACJ,WACA,WAEA,UAAU,SAAS,YACf;CACE,MAAM;CACN,GAAI,UAAU,gBAAgB,KAAA,IAC1B,EAAE,aAAa,UAAU,aAAa,GACtC,EAAE;CACN,GAAG;CACJ,GACD;CAAE,MAAM;CAAY,GAAG;CAAQ;;;;;;;;;;AAWrC,MAAa,sBAAsB,OACjC,KACA,WACA,SACA,WACsB;CACtB,MAAM,cAAc,KAAK,KAAK;CAC9B,IAAI;CACJ,IAAI,qBACF,QAAQ,QAAQ,KAAA,EAAU;AAC5B,KAAI;AACF,SAAO;GACL,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,gBAAgB,gBAAgB,QAAQ,QAAQ;GACjD;AACD,MAAI,QAAQ,KAEV,sBAAqB,uBADH,QAAQ,OAC2B,CAAC,KAAK,CAAC,YACpD,KAAA,EACP;UAEI,OAAO;AACd,WACE,4BACA,0CACA,MACD;AACD,SAAO,KAAA;;CAGT,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,OAAO,QAAQ;UACzB,OAAO;AACd,MAAI,MAAM;GACR,MAAM,aAAa;GACnB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,eAAe,OAAO,MAAM;AAGlC,OAAI,OAAO,MACT,YAAY;IACV,MAAM,cAAc,MAAM;AAC1B,WAAO,KAAK,WAAW;KACrB,GAAG;KACH,GAAG,YAAY,IAAI;KACnB,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD;KACA;KACA,OAAO;KACR,CAAC;MAEJ,yBACA,8CACD;;AAEH,QAAM;;AAGR,KAAI,MAAM;EACR,MAAM,aAAa;AACnB,MAAI;AAGF,OAAI,aAAa,SAAS,CACxB,QAAO;GAET,MAAM,gBAAgB,SAAS,OAAO;GACtC,MAAM,kBAAkB,gBAAgB,SAAS,QAAQ;GACzD,MAAM,aAAa,SAAS;GAC5B,MAAM,YAAY,KAAK,KAAK;AAC5B,OAAI,OAAO,MACT,YAAY;IACV,MAAM,CAAC,aAAa,gBAAgB,MAAM,QAAQ,IAAI,CACpD,oBACA,gBAAgB,cAAc,KAAK,CAAC,YAAY,KAAA,EAAU,CAC3D,CAAC;AACF,WAAO,KAAK,WAAW;KACrB,GAAG;KACH,GAAG,YAAY,IAAI;KACnB;KACA;KACA,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;KACpD,GAAI,iBAAiB,KAAA,IAAY,EAAE,cAAc,GAAG,EAAE;KACtD;KACA;KACD,CAAC;MAEJ,6BACA,0CACD;WACM,OAAO;AACd,YACE,6BACA,2CACA,MACD;;;AAIL,QAAO;;;AAIT,MAAM,eACJ,SAII;CACJ,WAAW,IAAI;CACf,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;CACN,SAAS,IAAI;CACb,cAAc,IAAI;CACnB;AAED,MAAM,UACJ,KACA,WACA,WACS;CACT,MAAM,QAAsB,WAAW,WAAW,OAAO;AACzD,KAAI,OAAO,IAAI,MAAM;;;AAIvB,MAAa,gBAAgB,aAC1B,SAAgD,aAAa","debugId":"9a4a84d0-427b-51fb-af59-6197c25b1671"}
@@ -0,0 +1,33 @@
1
+ import { WithMeticulousOptions } from "./with-meticulous.js";
2
+
3
+ //#region src/pages-function.d.ts
4
+ /**
5
+ * Structural subset of a Cloudflare Pages Functions `EventContext` — the argument an `onRequest`
6
+ * handler receives. Only the four fields the recorder reads are declared; everything else on the
7
+ * real context (`params`, `data`, `next`, `functionPath`, `passThroughOnException`) is preserved
8
+ * by the wrapper without being named here, so this stays free of a
9
+ * `@cloudflare/workers-types` dependency.
10
+ */
11
+ interface MeticulousPagesFunctionContext<Env = unknown> {
12
+ request: Request;
13
+ env: Env;
14
+ waitUntil(promise: Promise<unknown>): void;
15
+ }
16
+ type MeticulousPagesFunction<Context extends MeticulousPagesFunctionContext = MeticulousPagesFunctionContext> = (context: Context) => Response | Promise<Response>;
17
+ /**
18
+ * The Pages Functions equivalent of {@link withMeticulous}:
19
+ *
20
+ * export const onRequest = withMeticulousPagesFunction(handler);
21
+ *
22
+ * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,
23
+ * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and
24
+ * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.
25
+ *
26
+ * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the
27
+ * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own
28
+ * façades first would hide the real binding instances from it.
29
+ */
30
+ declare const withMeticulousPagesFunction: <Context extends MeticulousPagesFunctionContext>(handler: MeticulousPagesFunction<Context>, options?: WithMeticulousOptions) => (context: Context) => Promise<Response>;
31
+ //#endregion
32
+ export { MeticulousPagesFunction, MeticulousPagesFunctionContext, withMeticulousPagesFunction };
33
+ //# sourceMappingURL=pages-function.d.ts.map
@@ -0,0 +1,28 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="592c70e5-3859-578f-bf7d-0dfc15e165ab")}catch(e){}}();
3
+ import { runWithMeticulous } from "./with-meticulous.js";
4
+ //#region src/pages-function.ts
5
+ /**
6
+ * The Pages Functions equivalent of {@link withMeticulous}:
7
+ *
8
+ * export const onRequest = withMeticulousPagesFunction(handler);
9
+ *
10
+ * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,
11
+ * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and
12
+ * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.
13
+ *
14
+ * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the
15
+ * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own
16
+ * façades first would hide the real binding instances from it.
17
+ */
18
+ const withMeticulousPagesFunction = (handler, options) => (context) => runWithMeticulous({
19
+ request: context.request,
20
+ env: context.env,
21
+ ctx: { waitUntil: (promise) => context.waitUntil(promise) },
22
+ invokeHandler: () => handler(context)
23
+ }, options);
24
+ //#endregion
25
+ export { withMeticulousPagesFunction };
26
+
27
+ //# sourceMappingURL=pages-function.js.map
28
+ //# debugId=592c70e5-3859-578f-bf7d-0dfc15e165ab
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages-function.js","sources":["../src/pages-function.ts"],"sourcesContent":["import {\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\n/**\n * Structural subset of a Cloudflare Pages Functions `EventContext` — the argument an `onRequest`\n * handler receives. Only the four fields the recorder reads are declared; everything else on the\n * real context (`params`, `data`, `next`, `functionPath`, `passThroughOnException`) is preserved\n * by the wrapper without being named here, so this stays free of a\n * `@cloudflare/workers-types` dependency.\n */\nexport interface MeticulousPagesFunctionContext<Env = unknown> {\n request: Request;\n env: Env;\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport type MeticulousPagesFunction<\n Context extends MeticulousPagesFunctionContext =\n MeticulousPagesFunctionContext,\n> = (context: Context) => Response | Promise<Response>;\n\n/**\n * The Pages Functions equivalent of {@link withMeticulous}:\n *\n * export const onRequest = withMeticulousPagesFunction(handler);\n *\n * A Pages project's worker exports `onRequest(context)` rather than `{ fetch(request, env, ctx) }`,\n * so the module-Worker wrapper cannot be applied to it. Recording, replay, configuration and\n * failure behaviour are otherwise identical — see {@link withMeticulous} for all of it.\n *\n * Apply it **outermost**, before any wrapper of your own that re-writes `context.env`: the\n * recorder discovers bindings on the `env` it is handed, so a wrapper that substitutes its own\n * façades first would hide the real binding instances from it.\n */\nexport const withMeticulousPagesFunction =\n <Context extends MeticulousPagesFunctionContext>(\n handler: MeticulousPagesFunction<Context>,\n options?: WithMeticulousOptions,\n ) =>\n (context: Context): Promise<Response> =>\n runWithMeticulous(\n {\n request: context.request,\n env: context.env,\n ctx: { waitUntil: (promise) => context.waitUntil(promise) },\n invokeHandler: () => handler(context),\n },\n options,\n );\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAoCA,MAAa,+BAET,SACA,aAED,YACC,kBACE;CACE,SAAS,QAAQ;CACjB,KAAK,QAAQ;CACb,KAAK,EAAE,YAAY,YAAY,QAAQ,UAAU,QAAQ,EAAE;CAC3D,qBAAqB,QAAQ,QAAQ;CACtC,EACD,QACD","debugId":"592c70e5-3859-578f-bf7d-0dfc15e165ab"}
@@ -0,0 +1,108 @@
1
+ //#region src/postgres/capture.d.ts
2
+ /**
3
+ * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node
4
+ * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.
5
+ *
6
+ * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a
7
+ * deployed Worker has to produce the byte-identical span a Node-recorded one does, because
8
+ * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the
9
+ * serialization subtly wrong on one surface and every replay of a Worker recording misses.
10
+ * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here
11
+ * and keeps the replay half (key building, result/error reconstruction) to itself — that half
12
+ * needs a synchronous SHA-256 and never runs inside a Worker.
13
+ */
14
+ declare const POSTGRES_JS_QUERY_TEXT_ATTR = "meticulous.postgresjs.query.text";
15
+ declare const POSTGRES_JS_QUERY_PARAMS_ATTR = "meticulous.postgresjs.query.params";
16
+ declare const POSTGRES_JS_ROW_MODE_ATTR = "meticulous.postgresjs.row_mode";
17
+ declare const POSTGRES_JS_RESULT_ATTR = "meticulous.postgresjs.result";
18
+ declare const POSTGRES_JS_RESULT_TRUNCATED_ATTR = "meticulous.postgresjs.result.truncated";
19
+ declare const POSTGRES_JS_ERROR_ATTR = "meticulous.postgresjs.error";
20
+ declare const MAX_POSTGRES_JS_RESULT_SIZE: number;
21
+ /**
22
+ * How the rows of a result are shaped: objects keyed by column name by default, or arrays under
23
+ * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |
24
+ * `true` | `"values"`).
25
+ *
26
+ * This belongs in the match key because the SAME sql text returns different row shapes
27
+ * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits
28
+ * the query directly for `db.execute`, so both shapes occur for identical text.
29
+ */
30
+ type PostgresJsRowMode = "" | "raw" | "values";
31
+ declare const resolveRowMode: (isRaw: unknown) => PostgresJsRowMode;
32
+ /**
33
+ * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the
34
+ * interpolated values went.
35
+ *
36
+ * The SQL text does not exist at interception time: postgres.js builds `query.string` only once
37
+ * a query reaches a connection, and replay never executes, so both sides must derive the text
38
+ * from `query.strings` instead.
39
+ */
40
+ declare const reconstructQueryText: (strings: unknown) => string | undefined;
41
+ /**
42
+ * Canonical serialization of the query's interpolated values.
43
+ *
44
+ * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not
45
+ * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place
46
+ * into wire-format strings — so it differs between the two sides.
47
+ *
48
+ * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /
49
+ * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across
50
+ * calls, which is all the match key needs.
51
+ */
52
+ declare const serializePostgresJsArgs: (args: unknown) => string;
53
+ /** The leading SQL keyword, for the span name only. */
54
+ declare const postgresJsCommandOf: (text: string) => string;
55
+ /**
56
+ * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves
57
+ * to) to a JSON string for storage as a span attribute.
58
+ */
59
+ declare const serializePostgresJsResult: (result: unknown) => string;
60
+ /**
61
+ * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as
62
+ * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields
63
+ * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All
64
+ * of those live in own enumerable properties, so copying those plus `message` (own but
65
+ * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.
66
+ */
67
+ declare const serializePostgresJsError: (error: unknown) => string;
68
+ /**
69
+ * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no
70
+ * `postgres` dependency).
71
+ */
72
+ interface PostgresJsQueryLike {
73
+ /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */
74
+ strings: unknown;
75
+ /** The interpolated values, still raw JS values at interception time. */
76
+ args: unknown;
77
+ /** `undefined` by default, `true` after `.raw()`, `"values"` after `.values()`. */
78
+ isRaw?: unknown;
79
+ /** Set once `handle()` has run; guards against double-instrumenting one query. */
80
+ executed?: boolean;
81
+ /** Set by `.describe()` — resolves with a Statement, not a Result. */
82
+ onlyDescribe?: boolean;
83
+ /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */
84
+ cursorFn?: unknown;
85
+ cursorRows?: unknown;
86
+ forEachFn?: unknown;
87
+ streaming?: unknown;
88
+ options?: {
89
+ /**
90
+ * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it
91
+ * called with the serving connection before the transaction body runs.
92
+ */
93
+ onexecute?: (connection: unknown) => void;
94
+ [key: string]: unknown;
95
+ };
96
+ resolve: (result: unknown) => void;
97
+ reject: (error: unknown) => void;
98
+ }
99
+ /**
100
+ * Whether a query resolves with a single `Result` we can capture and replay. Cursors,
101
+ * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with
102
+ * something else, and `sql.file()` is constructed with no template chunks at all (its text is
103
+ * only read from disk later). All of those pass straight through, unrecorded and unmocked.
104
+ */
105
+ declare const isSupportedPostgresJsQuery: (query: PostgresJsQueryLike) => boolean;
106
+ //#endregion
107
+ export { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, PostgresJsQueryLike, PostgresJsRowMode, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult };
108
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1,136 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d01c8a65-211a-58f1-b35e-1ac030b75ece")}catch(e){}}();
3
+ import { serializeCapturedError } from "../error-capture.js";
4
+ //#region src/postgres/capture.ts
5
+ /**
6
+ * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node
7
+ * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.
8
+ *
9
+ * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a
10
+ * deployed Worker has to produce the byte-identical span a Node-recorded one does, because
11
+ * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the
12
+ * serialization subtly wrong on one surface and every replay of a Worker recording misses.
13
+ * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here
14
+ * and keeps the replay half (key building, result/error reconstruction) to itself — that half
15
+ * needs a synchronous SHA-256 and never runs inside a Worker.
16
+ */
17
+ const POSTGRES_JS_QUERY_TEXT_ATTR = "meticulous.postgresjs.query.text";
18
+ const POSTGRES_JS_QUERY_PARAMS_ATTR = "meticulous.postgresjs.query.params";
19
+ const POSTGRES_JS_ROW_MODE_ATTR = "meticulous.postgresjs.row_mode";
20
+ const POSTGRES_JS_RESULT_ATTR = "meticulous.postgresjs.result";
21
+ const POSTGRES_JS_RESULT_TRUNCATED_ATTR = "meticulous.postgresjs.result.truncated";
22
+ const POSTGRES_JS_ERROR_ATTR = "meticulous.postgresjs.error";
23
+ const MAX_POSTGRES_JS_RESULT_SIZE = 256 * 1024;
24
+ const resolveRowMode = (isRaw) => {
25
+ if (isRaw === "values") return "values";
26
+ return isRaw ? "raw" : "";
27
+ };
28
+ /**
29
+ * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the
30
+ * interpolated values went.
31
+ *
32
+ * The SQL text does not exist at interception time: postgres.js builds `query.string` only once
33
+ * a query reaches a connection, and replay never executes, so both sides must derive the text
34
+ * from `query.strings` instead.
35
+ */
36
+ const reconstructQueryText = (strings) => {
37
+ if (!Array.isArray(strings) || strings.length === 0) return;
38
+ let text = "";
39
+ for (let i = 0; i < strings.length; i++) {
40
+ const chunk = strings[i];
41
+ if (typeof chunk !== "string") return;
42
+ text += i === 0 ? chunk : `$${i}${chunk}`;
43
+ }
44
+ return text;
45
+ };
46
+ /**
47
+ * Canonical serialization of the query's interpolated values.
48
+ *
49
+ * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not
50
+ * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place
51
+ * into wire-format strings — so it differs between the two sides.
52
+ *
53
+ * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /
54
+ * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across
55
+ * calls, which is all the match key needs.
56
+ */
57
+ const serializePostgresJsArgs = (args) => {
58
+ try {
59
+ return JSON.stringify(Array.isArray(args) ? args : []);
60
+ } catch {
61
+ return "[]";
62
+ }
63
+ };
64
+ /** The leading SQL keyword, for the span name only. */
65
+ const postgresJsCommandOf = (text) => /^\s*([a-z]+)/i.exec(text)?.[1]?.toLowerCase() ?? "query";
66
+ const asRecord = (value) => value != null && typeof value === "object" ? value : {};
67
+ const numberOrNull = (value) => typeof value === "number" ? value : null;
68
+ /**
69
+ * postgres.js resolves a query with a `Result` — an `Array` subclass whose metadata lives in
70
+ * five non-enumerable own properties, so `JSON.stringify` would capture only the rows. Pull them
71
+ * out explicitly.
72
+ *
73
+ * Two fields are dropped on purpose: each column's `parser` (a function, so it can't survive
74
+ * JSON, and consumers never call it on an already-parsed row) and `state` (a live
75
+ * per-connection `{pid, secret}` handle used only for query cancellation, which is meaningless
76
+ * without a connection).
77
+ */
78
+ const serializeOneResult = (res) => {
79
+ const meta = res;
80
+ const statement = asRecord(meta?.statement);
81
+ return {
82
+ rows: Array.isArray(res) ? [...res] : [],
83
+ count: numberOrNull(meta?.count),
84
+ command: typeof meta?.command === "string" ? meta.command : null,
85
+ columns: Array.isArray(meta?.columns) ? meta.columns.map((column) => {
86
+ const c = asRecord(column);
87
+ return {
88
+ name: String(c.name ?? ""),
89
+ type: numberOrNull(c.type) ?? 0,
90
+ table: numberOrNull(c.table) ?? 0,
91
+ number: numberOrNull(c.number) ?? 0
92
+ };
93
+ }) : [],
94
+ statement: meta?.statement != null ? {
95
+ string: String(statement.string ?? ""),
96
+ types: Array.isArray(statement.types) ? statement.types : [],
97
+ name: String(statement.name ?? "")
98
+ } : null
99
+ };
100
+ };
101
+ /**
102
+ * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves
103
+ * to) to a JSON string for storage as a span attribute.
104
+ */
105
+ const serializePostgresJsResult = (result) => {
106
+ const serialized = Array.isArray(result) && result.length > 0 && result.every((r) => isResultLike(r)) ? result.map(serializeOneResult) : serializeOneResult(result);
107
+ return JSON.stringify(serialized);
108
+ };
109
+ const isResultLike = (value) => Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "command") && Object.prototype.hasOwnProperty.call(value, "count");
110
+ const ERROR_DEBUG_PROPS = new Set([
111
+ "stack",
112
+ "query",
113
+ "parameters",
114
+ "args",
115
+ "types"
116
+ ]);
117
+ /**
118
+ * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as
119
+ * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields
120
+ * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All
121
+ * of those live in own enumerable properties, so copying those plus `message` (own but
122
+ * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.
123
+ */
124
+ const serializePostgresJsError = (error) => serializeCapturedError(error, ERROR_DEBUG_PROPS);
125
+ /**
126
+ * Whether a query resolves with a single `Result` we can capture and replay. Cursors,
127
+ * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with
128
+ * something else, and `sql.file()` is constructed with no template chunks at all (its text is
129
+ * only read from disk later). All of those pass straight through, unrecorded and unmocked.
130
+ */
131
+ const isSupportedPostgresJsQuery = (query) => reconstructQueryText(query.strings) != null && !query.onlyDescribe && !query.streaming && query.cursorFn == null && query.cursorRows == null && query.forEachFn == null;
132
+ //#endregion
133
+ export { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult };
134
+
135
+ //# sourceMappingURL=capture.js.map
136
+ //# debugId=d01c8a65-211a-58f1-b35e-1ac030b75ece
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sources":["../../src/postgres/capture.ts"],"sourcesContent":["import { serializeCapturedError } from \"../error-capture\";\n\n/**\n * The record-side contract for postgres.js (the `postgres` npm package), shared by the Node\n * recorder's `met-postgres-js-instrumentation/` and the in-worker `withMeticulousPostgres`.\n *\n * It lives in this package for the same reason `kv-capture.ts` does: a query recorded from a\n * deployed Worker has to produce the byte-identical span a Node-recorded one does, because\n * `PostgresJsMockStore` derives its match key from those attributes at load time. Get the\n * serialization subtly wrong on one surface and every replay of a Worker recording misses.\n * `packages/backend-recorder-js/src/postgres-js-capture-shared.ts` re-exports everything here\n * and keeps the replay half (key building, result/error reconstruction) to itself — that half\n * needs a synchronous SHA-256 and never runs inside a Worker.\n */\n\n// Custom Meticulous span attributes carrying the query input + result. Kept distinct from the\n// `pg` attributes (`meticulous.pg.*`) so the two stores can never match each other's spans,\n// even though both talk to Postgres.\nexport const POSTGRES_JS_QUERY_TEXT_ATTR = \"meticulous.postgresjs.query.text\";\nexport const POSTGRES_JS_QUERY_PARAMS_ATTR =\n \"meticulous.postgresjs.query.params\";\nexport const POSTGRES_JS_ROW_MODE_ATTR = \"meticulous.postgresjs.row_mode\";\nexport const POSTGRES_JS_RESULT_ATTR = \"meticulous.postgresjs.result\";\nexport const POSTGRES_JS_RESULT_TRUNCATED_ATTR =\n \"meticulous.postgresjs.result.truncated\";\n// Set instead of `result` when the query rejected, so a query that legitimately errors (a\n// constraint violation, a missing relation) replays as the same error rather than as a \"no\n// recorded result\" miss.\nexport const POSTGRES_JS_ERROR_ATTR = \"meticulous.postgresjs.error\";\n\n// Cap the captured result to bound span size, matching the HTTP/undici/pg 256 KB cap.\nexport const MAX_POSTGRES_JS_RESULT_SIZE = 256 * 1024;\n\n/**\n * How the rows of a result are shaped: objects keyed by column name by default, or arrays under\n * `.values()` / `.raw()`. postgres.js records this on the query as `isRaw` (`undefined` |\n * `true` | `\"values\"`).\n *\n * This belongs in the match key because the SAME sql text returns different row shapes\n * depending on it — Drizzle's postgres-js driver calls `.values()` for typed selects but awaits\n * the query directly for `db.execute`, so both shapes occur for identical text.\n */\nexport type PostgresJsRowMode = \"\" | \"raw\" | \"values\";\n\nexport const resolveRowMode = (isRaw: unknown): PostgresJsRowMode => {\n if (isRaw === \"values\") {\n return \"values\";\n }\n return isRaw ? \"raw\" : \"\";\n};\n\n/**\n * Rebuilds the SQL text from a tagged template's literal chunks, with `$1`, `$2`… where the\n * interpolated values went.\n *\n * The SQL text does not exist at interception time: postgres.js builds `query.string` only once\n * a query reaches a connection, and replay never executes, so both sides must derive the text\n * from `query.strings` instead.\n */\nexport const reconstructQueryText = (strings: unknown): string | undefined => {\n if (!Array.isArray(strings) || strings.length === 0) {\n return undefined;\n }\n let text = \"\";\n for (let i = 0; i < strings.length; i++) {\n const chunk = strings[i];\n if (typeof chunk !== \"string\") {\n return undefined;\n }\n text += i === 0 ? chunk : `$${i}${chunk}`;\n }\n return text;\n};\n\n/**\n * Canonical serialization of the query's interpolated values.\n *\n * Read from `query.args` (the raw JS values), never `query.parameters`: the latter does not\n * exist until postgres.js builds the statement, and its `Bind` step then mutates it in place\n * into wire-format strings — so it differs between the two sides.\n *\n * `args` is heterogeneous: postgres.js' dynamic helpers put `Builder` / `Identifier` /\n * `Parameter` instances in there alongside plain values. JSON-encoding them is stable across\n * calls, which is all the match key needs.\n */\nexport const serializePostgresJsArgs = (args: unknown): string => {\n try {\n return JSON.stringify(Array.isArray(args) ? args : []);\n } catch {\n return \"[]\";\n }\n};\n\n/** The leading SQL keyword, for the span name only. */\nexport const postgresJsCommandOf = (text: string): string =>\n /^\\s*([a-z]+)/i.exec(text)?.[1]?.toLowerCase() ?? \"query\";\n\ninterface SerializedPostgresJsResult {\n rows: unknown[];\n count: number | null;\n command: string | null;\n columns: { name: string; type: number; table: number; number: number }[];\n statement: { string: string; types: number[]; name: string } | null;\n}\n\n/**\n * The metadata postgres.js hangs off a `Result`. Declared with `unknown` values so the\n * serializer below narrows every field rather than trusting the shape.\n */\ninterface PostgresJsResultLike {\n count?: unknown;\n command?: unknown;\n columns?: unknown;\n statement?: unknown;\n}\n\nconst asRecord = (value: unknown): Record<string, unknown> =>\n value != null && typeof value === \"object\"\n ? (value as Record<string, unknown>)\n : {};\n\nconst numberOrNull = (value: unknown): number | null =>\n typeof value === \"number\" ? value : null;\n\n/**\n * postgres.js resolves a query with a `Result` — an `Array` subclass whose metadata lives in\n * five non-enumerable own properties, so `JSON.stringify` would capture only the rows. Pull them\n * out explicitly.\n *\n * Two fields are dropped on purpose: each column's `parser` (a function, so it can't survive\n * JSON, and consumers never call it on an already-parsed row) and `state` (a live\n * per-connection `{pid, secret}` handle used only for query cancellation, which is meaningless\n * without a connection).\n */\nconst serializeOneResult = (res: unknown): SerializedPostgresJsResult => {\n const meta = res as PostgresJsResultLike | null | undefined;\n const statement = asRecord(meta?.statement);\n return {\n rows: Array.isArray(res) ? [...(res as unknown[])] : [],\n count: numberOrNull(meta?.count),\n command: typeof meta?.command === \"string\" ? meta.command : null,\n columns: Array.isArray(meta?.columns)\n ? meta.columns.map((column) => {\n const c = asRecord(column);\n return {\n name: String(c.name ?? \"\"),\n type: numberOrNull(c.type) ?? 0,\n table: numberOrNull(c.table) ?? 0,\n number: numberOrNull(c.number) ?? 0,\n };\n })\n : [],\n statement:\n meta?.statement != null\n ? {\n string: String(statement.string ?? \"\"),\n types: Array.isArray(statement.types)\n ? (statement.types as number[])\n : [],\n name: String(statement.name ?? \"\"),\n }\n : null,\n };\n};\n\n/**\n * Serializes a postgres.js result (or the array form a multi-statement / `simple` query resolves\n * to) to a JSON string for storage as a span attribute.\n */\nexport const serializePostgresJsResult = (result: unknown): string => {\n // A Result IS an Array, so an array-of-Results is only distinguishable by its elements. The\n // `length > 0` guard matters: `[].every(...)` is true, so without it every zero-row result (a\n // SELECT that matched nothing, an INSERT without RETURNING, BEGIN/COMMIT) would be mistaken\n // for an empty list of results and lose its `count`/`command`.\n const isMultiple =\n Array.isArray(result) &&\n result.length > 0 &&\n result.every((r) => isResultLike(r));\n const serialized = isMultiple\n ? (result as unknown[]).map(serializeOneResult)\n : serializeOneResult(result);\n return JSON.stringify(serialized);\n};\n\n// A `Result` carries `command`/`count` own properties; a plain row object or scalar does not.\nconst isResultLike = (value: unknown): boolean =>\n Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, \"command\") &&\n Object.prototype.hasOwnProperty.call(value, \"count\");\n\n// Own properties postgres.js attaches to a query error for debugging. Excluded from the\n// captured error: they are large, redundant with the query attributes we already record, and\n// only enumerable when the app enabled postgres.js' `debug` option.\nconst ERROR_DEBUG_PROPS = new Set([\n \"stack\",\n \"query\",\n \"parameters\",\n \"args\",\n \"types\",\n]);\n\n/**\n * Captures a rejected query's error as a JSON string. postgres.js surfaces server errors as\n * `PostgresError` (`extends Error`, with the server's `code`/`severity`/`detail`/… fields\n * `Object.assign`ed on) and transport/validation failures as plain `Error`s carrying `code`. All\n * of those live in own enumerable properties, so copying those plus `message` (own but\n * non-enumerable, from the `Error` constructor) captures everything a consumer inspects.\n */\nexport const serializePostgresJsError = (error: unknown): string =>\n serializeCapturedError(error, ERROR_DEBUG_PROPS);\n\n/**\n * The shape of postgres.js' internal `Query` that both sides rely on. Declared structurally (no\n * `postgres` dependency).\n */\nexport interface PostgresJsQueryLike {\n /** Template chunks: the literal parts of a tagged template, or `[text]` for `unsafe`. */\n strings: unknown;\n /** The interpolated values, still raw JS values at interception time. */\n args: unknown;\n /** `undefined` by default, `true` after `.raw()`, `\"values\"` after `.values()`. */\n isRaw?: unknown;\n /** Set once `handle()` has run; guards against double-instrumenting one query. */\n executed?: boolean;\n /** Set by `.describe()` — resolves with a Statement, not a Result. */\n onlyDescribe?: boolean;\n /** Set by `.cursor()` / `.forEach()` / `.readable()` — streaming, not a single Result. */\n cursorFn?: unknown;\n cursorRows?: unknown;\n forEachFn?: unknown;\n streaming?: unknown;\n options?: {\n /**\n * Present only on the implicit `begin` query `sql.begin()` issues. postgres.js needs it\n * called with the serving connection before the transaction body runs.\n */\n onexecute?: (connection: unknown) => void;\n [key: string]: unknown;\n };\n resolve: (result: unknown) => void;\n reject: (error: unknown) => void;\n}\n\n/**\n * Whether a query resolves with a single `Result` we can capture and replay. Cursors,\n * `.forEach()`, `.readable()`/`.writable()` (COPY streams) and `.describe()` resolve with\n * something else, and `sql.file()` is constructed with no template chunks at all (its text is\n * only read from disk later). All of those pass straight through, unrecorded and unmocked.\n */\nexport const isSupportedPostgresJsQuery = (\n query: PostgresJsQueryLike,\n): boolean =>\n reconstructQueryText(query.strings) != null &&\n !query.onlyDescribe &&\n !query.streaming &&\n query.cursorFn == null &&\n query.cursorRows == null &&\n query.forEachFn == null;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,8BAA8B;AAC3C,MAAa,gCACX;AACF,MAAa,4BAA4B;AACzC,MAAa,0BAA0B;AACvC,MAAa,oCACX;AAIF,MAAa,yBAAyB;AAGtC,MAAa,8BAA8B,MAAM;AAajD,MAAa,kBAAkB,UAAsC;AACnE,KAAI,UAAU,SACZ,QAAO;AAET,QAAO,QAAQ,QAAQ;;;;;;;;;;AAWzB,MAAa,wBAAwB,YAAyC;AAC5E,KAAI,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,WAAW,EAChD;CAEF,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,QAAQ,QAAQ;AACtB,MAAI,OAAO,UAAU,SACnB;AAEF,UAAQ,MAAM,IAAI,QAAQ,IAAI,IAAI;;AAEpC,QAAO;;;;;;;;;;;;;AAcT,MAAa,2BAA2B,SAA0B;AAChE,KAAI;AACF,SAAO,KAAK,UAAU,MAAM,QAAQ,KAAK,GAAG,OAAO,EAAE,CAAC;SAChD;AACN,SAAO;;;;AAKX,MAAa,uBAAuB,SAClC,gBAAgB,KAAK,KAAK,GAAG,IAAI,aAAa,IAAI;AAqBpD,MAAM,YAAY,UAChB,SAAS,QAAQ,OAAO,UAAU,WAC7B,QACD,EAAE;AAER,MAAM,gBAAgB,UACpB,OAAO,UAAU,WAAW,QAAQ;;;;;;;;;;;AAYtC,MAAM,sBAAsB,QAA6C;CACvE,MAAM,OAAO;CACb,MAAM,YAAY,SAAS,MAAM,UAAU;AAC3C,QAAO;EACL,MAAM,MAAM,QAAQ,IAAI,GAAG,CAAC,GAAI,IAAkB,GAAG,EAAE;EACvD,OAAO,aAAa,MAAM,MAAM;EAChC,SAAS,OAAO,MAAM,YAAY,WAAW,KAAK,UAAU;EAC5D,SAAS,MAAM,QAAQ,MAAM,QAAQ,GACjC,KAAK,QAAQ,KAAK,WAAW;GAC3B,MAAM,IAAI,SAAS,OAAO;AAC1B,UAAO;IACL,MAAM,OAAO,EAAE,QAAQ,GAAG;IAC1B,MAAM,aAAa,EAAE,KAAK,IAAI;IAC9B,OAAO,aAAa,EAAE,MAAM,IAAI;IAChC,QAAQ,aAAa,EAAE,OAAO,IAAI;IACnC;IACD,GACF,EAAE;EACN,WACE,MAAM,aAAa,OACf;GACE,QAAQ,OAAO,UAAU,UAAU,GAAG;GACtC,OAAO,MAAM,QAAQ,UAAU,MAAM,GAChC,UAAU,QACX,EAAE;GACN,MAAM,OAAO,UAAU,QAAQ,GAAG;GACnC,GACD;EACP;;;;;;AAOH,MAAa,6BAA6B,WAA4B;CASpE,MAAM,aAHJ,MAAM,QAAQ,OAAO,IACrB,OAAO,SAAS,KAChB,OAAO,OAAO,MAAM,aAAa,EAAE,CAAC,GAEjC,OAAqB,IAAI,mBAAmB,GAC7C,mBAAmB,OAAO;AAC9B,QAAO,KAAK,UAAU,WAAW;;AAInC,MAAM,gBAAgB,UACpB,MAAM,QAAQ,MAAM,IACpB,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IACtD,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ;AAKtD,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAa,4BAA4B,UACvC,uBAAuB,OAAO,kBAAkB;;;;;;;AAwClD,MAAa,8BACX,UAEA,qBAAqB,MAAM,QAAQ,IAAI,QACvC,CAAC,MAAM,gBACP,CAAC,MAAM,aACP,MAAM,YAAY,QAClB,MAAM,cAAc,QACpB,MAAM,aAAa","debugId":"d01c8a65-211a-58f1-b35e-1ac030b75ece"}
@@ -0,0 +1,32 @@
1
+ //#region src/postgres/with-meticulous-postgres.d.ts
2
+ /**
3
+ * Records postgres.js queries a deployed Worker makes — typically over Hyperdrive, which is the
4
+ * only way a Worker reaches Postgres.
5
+ *
6
+ * There is no seam to patch automatically here. The Node recorder can hook Node's module loader
7
+ * to patch `postgres/{src,cjs/src,cf/src}/query.js`, but a Worker is a single bundle: `postgres`
8
+ * was inlined at build time and never passes through a loader. So the app hands us its client:
9
+ *
10
+ * const sql = withMeticulousPostgres(postgres(env.HYPERDRIVE.connectionString));
11
+ *
12
+ * Every postgres.js query funnels through one internal method, `Query.prototype.handle`, and this
13
+ * reaches that shared prototype *through the instance* — via a throwaway query, which is free
14
+ * because postgres.js queries are lazy and nothing executes until awaited. One call therefore also
15
+ * covers read-replica clients and any other client in the isolate.
16
+ *
17
+ * Apply it **outermost** if something else also wraps the client (Sentry's
18
+ * `instrumentPostgresJsSql`, a logging proxy): those wrappers return a proxy whose `handle` is
19
+ * still the shared prototype's, so ordering does not change what is captured — but keeping ours
20
+ * outermost matches the Node recorder's contract and stays correct if that ever changes.
21
+ *
22
+ * Record-only, deliberately. A Worker recording is replayed against the app running under Node,
23
+ * where `@alwaysmeticulous/backend-recorder-js` serves these queries from its own mock store —
24
+ * which is why the captured attributes here are exactly the ones that store reads. Outside a
25
+ * recorded request this is a complete pass-through, so it is safe to leave in deployed code.
26
+ *
27
+ * Returns the client unchanged; the patch is on the prototype.
28
+ */
29
+ declare const withMeticulousPostgres: <Sql>(sql: Sql) => Sql;
30
+ //#endregion
31
+ export { withMeticulousPostgres };
32
+ //# sourceMappingURL=with-meticulous-postgres.d.ts.map