@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":"redact-body.js","sources":["../src/redact-body.ts"],"sourcesContent":["/** Replacement for a redacted value. Matches the Node recorder's redaction marker. */\nexport const STR_REDACTED = \"REDACTED\";\n\n/**\n * Object keys whose values are replaced before a captured request body leaves the worker.\n *\n * Request bodies are captured verbatim, and plenty of APIs carry a credential in the body\n * rather than a header — an OAuth2 token exchange, or a feature-flag resolver that takes its\n * client secret as a JSON field. Header redaction (an allow-list of two) does nothing for\n * those, so without this the credential would be persisted in the recorded span.\n *\n * Compared case-insensitively, ignoring `_` and `-`, so `clientSecret`, `client_secret` and\n * `CLIENT-SECRET` all match one entry.\n */\nconst SECRET_KEYS = [\n \"accesstoken\",\n \"apikey\",\n \"authorization\",\n \"clientsecret\",\n \"credential\",\n \"idtoken\",\n \"password\",\n \"privatekey\",\n \"refreshtoken\",\n \"secret\",\n \"token\",\n] as const;\n\nconst secretKeys = new Set<string>(SECRET_KEYS);\n\nconst normalizeKey = (key: string): string =>\n key.toLowerCase().replace(/[_-]/g, \"\");\n\nconst isSecretKey = (key: string): boolean => secretKeys.has(normalizeKey(key));\n\n/**\n * Redacts secret-looking fields from a captured request body, returning the body unchanged\n * if it is not JSON.\n *\n * Applied to request bodies only. Response bodies must stay byte-exact: replay serves them\n * back to the app, so redacting one would corrupt what the app receives. Request bodies are\n * only ever used to key a lookup, and because the same function runs on both the record and\n * the replay side the keys still agree.\n *\n * Never throws — a body that cannot be parsed or re-serialized is returned as-is, which is\n * no worse than today's behaviour.\n */\nexport const redactRequestBody = (body: string): string => {\n if (body.length === 0) {\n return body;\n }\n try {\n const parsed: unknown = JSON.parse(body);\n const redacted = redactValue(parsed);\n const serialized = JSON.stringify(redacted);\n return serialized ?? body;\n } catch {\n // Not JSON (form-encoded, protobuf, plain text) — left alone rather than mangled.\n return body;\n }\n};\n\nconst redactValue = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(redactValue);\n }\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n result[key] = isSecretKey(key) ? STR_REDACTED : redactValue(nested);\n }\n return result;\n};\n"],"names":[],"mappings":";;;;AACA,MAAa,eAAe;AA2B5B,MAAM,aAAa,IAAI,IAAY;CAbjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAG4C,CAAC;AAE/C,MAAM,gBAAgB,QACpB,IAAI,aAAa,CAAC,QAAQ,SAAS,GAAG;AAExC,MAAM,eAAe,QAAyB,WAAW,IAAI,aAAa,IAAI,CAAC;;;;;;;;;;;;;AAc/E,MAAa,qBAAqB,SAAyB;AACzD,KAAI,KAAK,WAAW,EAClB,QAAO;AAET,KAAI;EAEF,MAAM,WAAW,YADO,KAAK,MAAM,KACA,CAAC;AAEpC,SADmB,KAAK,UAAU,SACjB,IAAI;SACf;AAEN,SAAO;;;AAIX,MAAM,eAAe,UAA4B;AAC/C,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,YAAY;AAE/B,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;CAET,MAAM,SAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,MAAM,CAC/C,QAAO,OAAO,YAAY,IAAI,GAAG,eAAe,YAAY,OAAO;AAErE,QAAO","debugId":"c9ea8914-5f33-5a57-a950-20eae083e1d7"}
1
+ {"version":3,"file":"redact-body.js","names":[],"sources":["../src/redact-body.ts"],"sourcesContent":["/** Replacement for a redacted value. Matches the Node recorder's redaction marker. */\nexport const STR_REDACTED = \"REDACTED\";\n\n/**\n * Object keys whose values are replaced before a captured request body leaves the worker.\n *\n * Request bodies are captured verbatim, and plenty of APIs carry a credential in the body\n * rather than a header — an OAuth2 token exchange, or a feature-flag resolver that takes its\n * client secret as a JSON field. Header redaction (an allow-list of two) does nothing for\n * those, so without this the credential would be persisted in the recorded span.\n *\n * Compared case-insensitively, ignoring `_` and `-`, so `clientSecret`, `client_secret` and\n * `CLIENT-SECRET` all match one entry.\n */\nconst SECRET_KEYS = [\n \"accesstoken\",\n \"apikey\",\n \"authorization\",\n \"clientsecret\",\n \"credential\",\n \"idtoken\",\n \"password\",\n \"privatekey\",\n \"refreshtoken\",\n \"secret\",\n \"token\",\n] as const;\n\nconst secretKeys = new Set<string>(SECRET_KEYS);\n\nconst normalizeKey = (key: string): string =>\n key.toLowerCase().replace(/[_-]/g, \"\");\n\nconst isSecretKey = (key: string): boolean => secretKeys.has(normalizeKey(key));\n\n/**\n * Redacts secret-looking fields from a captured request body, returning the body unchanged\n * if it is not JSON.\n *\n * Applied to request bodies only. Response bodies must stay byte-exact: replay serves them\n * back to the app, so redacting one would corrupt what the app receives. Request bodies are\n * only ever used to key a lookup, and because the same function runs on both the record and\n * the replay side the keys still agree.\n *\n * Never throws — a body that cannot be parsed or re-serialized is returned as-is, which is\n * no worse than today's behaviour.\n */\nexport const redactRequestBody = (body: string): string => {\n if (body.length === 0) {\n return body;\n }\n try {\n const parsed: unknown = JSON.parse(body);\n const redacted = redactValue(parsed);\n const serialized = JSON.stringify(redacted);\n return serialized ?? body;\n } catch {\n // Not JSON (form-encoded, protobuf, plain text) — left alone rather than mangled.\n return body;\n }\n};\n\nconst redactValue = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n return value.map(redactValue);\n }\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n result[key] = isSecretKey(key) ? STR_REDACTED : redactValue(nested);\n }\n return result;\n};\n"],"mappings":";;AACA,MAAa,eAAe;AA2B5B,MAAM,aAAa,IAAI,IAAY;CAbjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAG4C,CAAC;AAE/C,MAAM,gBAAgB,QACpB,IAAI,aAAa,CAAC,QAAQ,SAAS,GAAG;AAExC,MAAM,eAAe,QAAyB,WAAW,IAAI,aAAa,IAAI,CAAC;;;;;;;;;;;;;AAc/E,MAAa,qBAAqB,SAAyB;AACzD,KAAI,KAAK,WAAW,EAClB,QAAO;AAET,KAAI;EAEF,MAAM,WAAW,YADO,KAAK,MAAM,KACA,CAAC;AAEpC,SADmB,KAAK,UAAU,SACjB,IAAI;SACf;AAEN,SAAO;;;AAIX,MAAM,eAAe,UAA4B;AAC/C,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,YAAY;AAE/B,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;CAET,MAAM,SAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,MAAM,CAC/C,QAAO,OAAO,YAAY,IAAI,GAAG,eAAe,YAAY,OAAO;AAErE,QAAO"}
@@ -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]="db9354dc-05d0-51c9-a548-f693044b9cc5")}catch(e){}}();
3
1
  import { warn, warnOnce } from "./log.js";
4
2
  import { readRequestBodyWithCap } from "./body-capture.js";
5
3
  import { METICULOUS_PASSTHROUGH_HEADER } from "./protocol.js";
@@ -130,5 +128,4 @@ const buildMockResponse = (mock) => {
130
128
  //#endregion
131
129
  export { replayOutboundCall };
132
130
 
133
- //# sourceMappingURL=replay-fetch.js.map
134
- //# debugId=db9354dc-05d0-51c9-a548-f693044b9cc5
131
+ //# sourceMappingURL=replay-fetch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"replay-fetch.js","sources":["../src/replay-fetch.ts"],"sourcesContent":["import { readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestReplayContext } from \"./context\";\nimport { warn, warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n METICULOUS_PASSTHROUGH_HEADER,\n type OutboundFetchLookupRequest,\n} from \"./protocol\";\nimport { postOutboundFetchLookup } from \"./sidecar-client\";\n\n/**\n * Serves a recorded response for an outbound call, or fails it.\n *\n * The mirror image of `captureOutboundCall`: recording tees a call that happens anyway,\n * whereas replay decides first and only then calls — so the sidecar sits in the request\n * path here.\n *\n * Replay is hermetic: a call the recording does not cover must not silently reach the real\n * service, since that turns a replay into live traffic against real credentials and hides\n * the gap in the recording behind a response no base replay will reproduce. So anything we\n * cannot serve from the recording — a miss, a call the sidecar cannot look up, an\n * unreachable sidecar, a mock that cannot be represented, or an outcome this shim does not\n * recognise — throws, matching the Node recorder's http/undici mocks.\n *\n * The only way out is an explicit `meticulous-passthrough: true` header on the request: a\n * call the customer has marked as one that must reach the real service. A failure to stub is\n * never treated as such a mark.\n */\nexport const replayOutboundCall = async (\n ctx: RequestReplayContext,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n if (hasPassthroughHeader(request)) {\n return invoke(request);\n }\n\n let lookup: OutboundFetchLookupRequest;\n try {\n // Read the clone and pass the original through untouched, mirroring the record path.\n // The sidecar hashes what we send here, and it was captured by the same\n // readRequestBodyWithCap the recorder used (redaction included), so the hashes on both\n // sides derive from byte-identical input.\n const requestBody = request.body\n ? await readRequestBodyWithCap(request.clone().body).catch(\n () => undefined,\n )\n : undefined;\n lookup = {\n frontendSessionId: ctx.frontendSessionId,\n replayId: ctx.replayId,\n method: request.method,\n url: request.url,\n ...(ctx.virtualTimeMs !== undefined\n ? { virtualTimeMs: ctx.virtualTimeMs }\n : {}),\n ...(requestBody !== undefined ? { requestBody } : {}),\n };\n } catch (error) {\n // Without a lookup there is nothing to serve, and passing through would be exactly the\n // silent real call this path exists to prevent.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"could not prepare a mock lookup\",\n error,\n );\n }\n\n const result = await postOutboundFetchLookup(\n getOriginalFetch(),\n ctx.sidecarUrl,\n lookup,\n );\n if (result === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"the replay sidecar did not answer a mock lookup\",\n );\n }\n if (result.outcome === \"mock\") {\n const mocked = buildMockResponse(result);\n if (mocked === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n `the recorded response (status ${result.statusCode}) cannot be represented`,\n );\n }\n return mocked;\n }\n // Every other outcome is a failure to stub — a miss (`no-mock`) or an outcome this shim\n // does not recognise (e.g. a legacy `passthrough`). None may reach the real service, so we\n // fail the request; the only path to a live call is the explicit passthrough header\n // handled above.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n describeUnservedOutcome(result.outcome),\n );\n};\n\nconst describeUnservedOutcome = (outcome: string): string =>\n outcome === \"no-mock\"\n ? \"no recorded response\"\n : `an unrecognised sidecar outcome (${outcome})`;\n\nconst hasPassthroughHeader = (request: Request): boolean => {\n try {\n return request.headers.get(METICULOUS_PASSTHROUGH_HEADER) === \"true\";\n } catch {\n return false;\n }\n};\n\n/**\n * Builds — and logs — the error the app sees for a call replay cannot serve. Deliberately\n * names the shim and the session so an app-side stack trace is enough to tell a replay gap\n * from a real bug, and logs as well because an app that catches its own fetch errors would\n * otherwise swallow the only signal.\n *\n * The query string is left out on purpose — it routinely carries API keys, and the sidecar\n * logs the full path anyway.\n */\nconst buildReplayError = (\n request: Request,\n frontendSessionId: string,\n reason: string,\n cause?: unknown,\n): Error => {\n const error = new Error(\n `[backend-recorder] workerd replay: ${reason} for ${describeRequest(request)} (session ${frontendSessionId}). Set the \"${METICULOUS_PASSTHROUGH_HEADER}: true\" header on this request if it must reach the real service during replay.`,\n );\n if (cause !== undefined) {\n // `cause` in the constructor options is not universally available in workerd builds.\n (error as { cause?: unknown }).cause = cause;\n }\n warn(error.message);\n return error;\n};\n\nconst describeRequest = (request: Request): string => {\n let method = \"GET\";\n let url = \"\";\n try {\n method = request.method;\n url = request.url;\n } catch {\n return method;\n }\n try {\n const parsed = new URL(url);\n return `${method} ${parsed.origin}${parsed.pathname}`;\n } catch {\n return `${method} ${url}`;\n }\n};\n\n// Recorded bodies are already decoded and may have been truncated, so the recorded framing\n// headers no longer describe them; the Response constructor recomputes what it needs.\nconst HEADERS_TO_DROP = new Set([\n \"content-encoding\",\n \"content-length\",\n \"transfer-encoding\",\n]);\n\n// Statuses the Response constructor refuses to pair with a body.\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/**\n * Turns a recorded response into a real one, or undefined if it cannot be represented (the\n * constructor throws a RangeError on an out-of-range status, or a body on a null-body\n * status), in which case the caller fails the request.\n *\n * Note recorded headers are limited to CAPTURED_HEADERS, so a mocked response carries\n * essentially just content-type. In particular it can never set cookies — fine for the API\n * calls this path serves, but a real limitation for a backend that authenticates that way.\n */\nconst buildMockResponse = (mock: {\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n}): Response | undefined => {\n try {\n const headers = new Headers();\n for (const [name, value] of Object.entries(mock.headers)) {\n if (HEADERS_TO_DROP.has(name.toLowerCase())) {\n continue;\n }\n try {\n headers.set(name, value);\n } catch {\n // Skip a header name/value the runtime rejects rather than losing the whole mock.\n }\n }\n const body = NULL_BODY_STATUSES.has(mock.statusCode) ? null : mock.body;\n return new Response(body, { status: mock.statusCode, headers });\n } catch (error) {\n warnOnce(\n \"replay-mock-response\",\n \"Could not construct a mocked response.\",\n error,\n );\n return undefined;\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,qBAAqB,OAChC,KACA,SACA,WACsB;AACtB,KAAI,qBAAqB,QAAQ,CAC/B,QAAO,OAAO,QAAQ;CAGxB,IAAI;AACJ,KAAI;EAKF,MAAM,cAAc,QAAQ,OACxB,MAAM,uBAAuB,QAAQ,OAAO,CAAC,KAAK,CAAC,YAC3C,KAAA,EACP,GACD,KAAA;AACJ,WAAS;GACP,mBAAmB,IAAI;GACvB,UAAU,IAAI;GACd,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,GAAI,IAAI,kBAAkB,KAAA,IACtB,EAAE,eAAe,IAAI,eAAe,GACpC,EAAE;GACN,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;GACrD;UACM,OAAO;AAGd,QAAM,iBACJ,SACA,IAAI,mBACJ,mCACA,MACD;;CAGH,MAAM,SAAS,MAAM,wBACnB,kBAAkB,EAClB,IAAI,YACJ,OACD;AACD,KAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,kDACD;AAEH,KAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,SAAS,kBAAkB,OAAO;AACxC,MAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,iCAAiC,OAAO,WAAW,yBACpD;AAEH,SAAO;;AAMT,OAAM,iBACJ,SACA,IAAI,mBACJ,wBAAwB,OAAO,QAAQ,CACxC;;AAGH,MAAM,2BAA2B,YAC/B,YAAY,YACR,yBACA,oCAAoC,QAAQ;AAElD,MAAM,wBAAwB,YAA8B;AAC1D,KAAI;AACF,SAAO,QAAQ,QAAQ,IAAI,8BAA8B,KAAK;SACxD;AACN,SAAO;;;;;;;;;;;;AAaX,MAAM,oBACJ,SACA,mBACA,QACA,UACU;CACV,MAAM,wBAAQ,IAAI,MAChB,sCAAsC,OAAO,OAAO,gBAAgB,QAAQ,CAAC,YAAY,kBAAkB,cAAc,8BAA8B,iFACxJ;AACD,KAAI,UAAU,KAAA,EAEX,OAA8B,QAAQ;AAEzC,MAAK,MAAM,QAAQ;AACnB,QAAO;;AAGT,MAAM,mBAAmB,YAA6B;CACpD,IAAI,SAAS;CACb,IAAI,MAAM;AACV,KAAI;AACF,WAAS,QAAQ;AACjB,QAAM,QAAQ;SACR;AACN,SAAO;;AAET,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,SAAO,GAAG,OAAO,GAAG,OAAO,SAAS,OAAO;SACrC;AACN,SAAO,GAAG,OAAO,GAAG;;;AAMxB,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACD,CAAC;AAGF,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAI,CAAC;;;;;;;;;;AAWxD,MAAM,qBAAqB,SAIC;AAC1B,KAAI;EACF,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE;AACxD,OAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC;AAEF,OAAI;AACF,YAAQ,IAAI,MAAM,MAAM;WAClB;;EAIV,MAAM,OAAO,mBAAmB,IAAI,KAAK,WAAW,GAAG,OAAO,KAAK;AACnE,SAAO,IAAI,SAAS,MAAM;GAAE,QAAQ,KAAK;GAAY;GAAS,CAAC;UACxD,OAAO;AACd,WACE,wBACA,0CACA,MACD;AACD","debugId":"db9354dc-05d0-51c9-a548-f693044b9cc5"}
1
+ {"version":3,"file":"replay-fetch.js","names":[],"sources":["../src/replay-fetch.ts"],"sourcesContent":["import { readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestReplayContext } from \"./context\";\nimport { warn, warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n METICULOUS_PASSTHROUGH_HEADER,\n type OutboundFetchLookupRequest,\n} from \"./protocol\";\nimport { postOutboundFetchLookup } from \"./sidecar-client\";\n\n/**\n * Serves a recorded response for an outbound call, or fails it.\n *\n * The mirror image of `captureOutboundCall`: recording tees a call that happens anyway,\n * whereas replay decides first and only then calls — so the sidecar sits in the request\n * path here.\n *\n * Replay is hermetic: a call the recording does not cover must not silently reach the real\n * service, since that turns a replay into live traffic against real credentials and hides\n * the gap in the recording behind a response no base replay will reproduce. So anything we\n * cannot serve from the recording — a miss, a call the sidecar cannot look up, an\n * unreachable sidecar, a mock that cannot be represented, or an outcome this shim does not\n * recognise — throws, matching the Node recorder's http/undici mocks.\n *\n * The only way out is an explicit `meticulous-passthrough: true` header on the request: a\n * call the customer has marked as one that must reach the real service. A failure to stub is\n * never treated as such a mark.\n */\nexport const replayOutboundCall = async (\n ctx: RequestReplayContext,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n if (hasPassthroughHeader(request)) {\n return invoke(request);\n }\n\n let lookup: OutboundFetchLookupRequest;\n try {\n // Read the clone and pass the original through untouched, mirroring the record path.\n // The sidecar hashes what we send here, and it was captured by the same\n // readRequestBodyWithCap the recorder used (redaction included), so the hashes on both\n // sides derive from byte-identical input.\n const requestBody = request.body\n ? await readRequestBodyWithCap(request.clone().body).catch(\n () => undefined,\n )\n : undefined;\n lookup = {\n frontendSessionId: ctx.frontendSessionId,\n replayId: ctx.replayId,\n method: request.method,\n url: request.url,\n ...(ctx.virtualTimeMs !== undefined\n ? { virtualTimeMs: ctx.virtualTimeMs }\n : {}),\n ...(requestBody !== undefined ? { requestBody } : {}),\n };\n } catch (error) {\n // Without a lookup there is nothing to serve, and passing through would be exactly the\n // silent real call this path exists to prevent.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"could not prepare a mock lookup\",\n error,\n );\n }\n\n const result = await postOutboundFetchLookup(\n getOriginalFetch(),\n ctx.sidecarUrl,\n lookup,\n );\n if (result === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"the replay sidecar did not answer a mock lookup\",\n );\n }\n if (result.outcome === \"mock\") {\n const mocked = buildMockResponse(result);\n if (mocked === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n `the recorded response (status ${result.statusCode}) cannot be represented`,\n );\n }\n return mocked;\n }\n // Every other outcome is a failure to stub — a miss (`no-mock`) or an outcome this shim\n // does not recognise (e.g. a legacy `passthrough`). None may reach the real service, so we\n // fail the request; the only path to a live call is the explicit passthrough header\n // handled above.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n describeUnservedOutcome(result.outcome),\n );\n};\n\nconst describeUnservedOutcome = (outcome: string): string =>\n outcome === \"no-mock\"\n ? \"no recorded response\"\n : `an unrecognised sidecar outcome (${outcome})`;\n\nconst hasPassthroughHeader = (request: Request): boolean => {\n try {\n return request.headers.get(METICULOUS_PASSTHROUGH_HEADER) === \"true\";\n } catch {\n return false;\n }\n};\n\n/**\n * Builds — and logs — the error the app sees for a call replay cannot serve. Deliberately\n * names the shim and the session so an app-side stack trace is enough to tell a replay gap\n * from a real bug, and logs as well because an app that catches its own fetch errors would\n * otherwise swallow the only signal.\n *\n * The query string is left out on purpose — it routinely carries API keys, and the sidecar\n * logs the full path anyway.\n */\nconst buildReplayError = (\n request: Request,\n frontendSessionId: string,\n reason: string,\n cause?: unknown,\n): Error => {\n const error = new Error(\n `[backend-recorder] workerd replay: ${reason} for ${describeRequest(request)} (session ${frontendSessionId}). Set the \"${METICULOUS_PASSTHROUGH_HEADER}: true\" header on this request if it must reach the real service during replay.`,\n );\n if (cause !== undefined) {\n // `cause` in the constructor options is not universally available in workerd builds.\n (error as { cause?: unknown }).cause = cause;\n }\n warn(error.message);\n return error;\n};\n\nconst describeRequest = (request: Request): string => {\n let method = \"GET\";\n let url = \"\";\n try {\n method = request.method;\n url = request.url;\n } catch {\n return method;\n }\n try {\n const parsed = new URL(url);\n return `${method} ${parsed.origin}${parsed.pathname}`;\n } catch {\n return `${method} ${url}`;\n }\n};\n\n// Recorded bodies are already decoded and may have been truncated, so the recorded framing\n// headers no longer describe them; the Response constructor recomputes what it needs.\nconst HEADERS_TO_DROP = new Set([\n \"content-encoding\",\n \"content-length\",\n \"transfer-encoding\",\n]);\n\n// Statuses the Response constructor refuses to pair with a body.\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/**\n * Turns a recorded response into a real one, or undefined if it cannot be represented (the\n * constructor throws a RangeError on an out-of-range status, or a body on a null-body\n * status), in which case the caller fails the request.\n *\n * Note recorded headers are limited to CAPTURED_HEADERS, so a mocked response carries\n * essentially just content-type. In particular it can never set cookies — fine for the API\n * calls this path serves, but a real limitation for a backend that authenticates that way.\n */\nconst buildMockResponse = (mock: {\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n}): Response | undefined => {\n try {\n const headers = new Headers();\n for (const [name, value] of Object.entries(mock.headers)) {\n if (HEADERS_TO_DROP.has(name.toLowerCase())) {\n continue;\n }\n try {\n headers.set(name, value);\n } catch {\n // Skip a header name/value the runtime rejects rather than losing the whole mock.\n }\n }\n const body = NULL_BODY_STATUSES.has(mock.statusCode) ? null : mock.body;\n return new Response(body, { status: mock.statusCode, headers });\n } catch (error) {\n warnOnce(\n \"replay-mock-response\",\n \"Could not construct a mocked response.\",\n error,\n );\n return undefined;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,qBAAqB,OAChC,KACA,SACA,WACsB;AACtB,KAAI,qBAAqB,QAAQ,CAC/B,QAAO,OAAO,QAAQ;CAGxB,IAAI;AACJ,KAAI;EAKF,MAAM,cAAc,QAAQ,OACxB,MAAM,uBAAuB,QAAQ,OAAO,CAAC,KAAK,CAAC,YAC3C,KAAA,EACP,GACD,KAAA;AACJ,WAAS;GACP,mBAAmB,IAAI;GACvB,UAAU,IAAI;GACd,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,GAAI,IAAI,kBAAkB,KAAA,IACtB,EAAE,eAAe,IAAI,eAAe,GACpC,EAAE;GACN,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;GACrD;UACM,OAAO;AAGd,QAAM,iBACJ,SACA,IAAI,mBACJ,mCACA,MACD;;CAGH,MAAM,SAAS,MAAM,wBACnB,kBAAkB,EAClB,IAAI,YACJ,OACD;AACD,KAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,kDACD;AAEH,KAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,SAAS,kBAAkB,OAAO;AACxC,MAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,iCAAiC,OAAO,WAAW,yBACpD;AAEH,SAAO;;AAMT,OAAM,iBACJ,SACA,IAAI,mBACJ,wBAAwB,OAAO,QAAQ,CACxC;;AAGH,MAAM,2BAA2B,YAC/B,YAAY,YACR,yBACA,oCAAoC,QAAQ;AAElD,MAAM,wBAAwB,YAA8B;AAC1D,KAAI;AACF,SAAO,QAAQ,QAAQ,IAAI,8BAA8B,KAAK;SACxD;AACN,SAAO;;;;;;;;;;;;AAaX,MAAM,oBACJ,SACA,mBACA,QACA,UACU;CACV,MAAM,wBAAQ,IAAI,MAChB,sCAAsC,OAAO,OAAO,gBAAgB,QAAQ,CAAC,YAAY,kBAAkB,cAAc,8BAA8B,iFACxJ;AACD,KAAI,UAAU,KAAA,EAEX,OAA8B,QAAQ;AAEzC,MAAK,MAAM,QAAQ;AACnB,QAAO;;AAGT,MAAM,mBAAmB,YAA6B;CACpD,IAAI,SAAS;CACb,IAAI,MAAM;AACV,KAAI;AACF,WAAS,QAAQ;AACjB,QAAM,QAAQ;SACR;AACN,SAAO;;AAET,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,SAAO,GAAG,OAAO,GAAG,OAAO,SAAS,OAAO;SACrC;AACN,SAAO,GAAG,OAAO,GAAG;;;AAMxB,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACD,CAAC;AAGF,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAI,CAAC;;;;;;;;;;AAWxD,MAAM,qBAAqB,SAIC;AAC1B,KAAI;EACF,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE;AACxD,OAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC;AAEF,OAAI;AACF,YAAQ,IAAI,MAAM,MAAM;WAClB;;EAIV,MAAM,OAAO,mBAAmB,IAAI,KAAK,WAAW,GAAG,OAAO,KAAK;AACnE,SAAO,IAAI,SAAS,MAAM;GAAE,QAAQ,KAAK;GAAY;GAAS,CAAC;UACxD,OAAO;AACd,WACE,wBACA,0CACA,MACD;AACD"}
@@ -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]="962faa5b-f3e8-548c-be10-1e341f1f56bd")}catch(e){}}();
3
1
  import { requestCaptureContext } from "./context.js";
4
2
  //#region src/replay-log-tagging.ts
5
3
  const CONSOLE_METHODS = [
@@ -53,5 +51,4 @@ const installReplayLogTagging = (methods = console, holder = globalThis) => {
53
51
  //#endregion
54
52
  export { installReplayLogTagging };
55
53
 
56
- //# sourceMappingURL=replay-log-tagging.js.map
57
- //# debugId=962faa5b-f3e8-548c-be10-1e341f1f56bd
54
+ //# sourceMappingURL=replay-log-tagging.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"replay-log-tagging.js","sources":["../src/replay-log-tagging.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\n\ntype ConsoleLogMethod = (...args: unknown[]) => void;\n\nconst CONSOLE_METHODS = [\n \"debug\",\n \"error\",\n \"info\",\n \"log\",\n \"trace\",\n \"warn\",\n] as const;\n\ntype ConsoleMethodName = (typeof CONSOLE_METHODS)[number];\n\nconst CONSOLE_TAGGING_INSTALLED = Symbol.for(\n \"meticulous.workerd.consoleReplayTaggingInstalled\",\n);\n\n// This literal is part of the persisted log wire format. Keep it pinned here\n// rather than deriving it from the HTTP header, whose name can change\n// independently of logs that have already been written to S3.\nconst REPLAY_ID_LOG_TAG_NAME = \"meticulous-replay-id\";\n\nconst formatReplayIdLogTag = (replayId: string): string =>\n `[${REPLAY_ID_LOG_TAG_NAME}:${replayId}] `;\n\nconst tagContinuationLines = (value: string, prefix: string): string =>\n value.replace(/\\n(?=.)/g, `\\n${prefix}`);\n\nconst prepareConsoleArgument = (value: unknown, prefix: string): unknown => {\n if (typeof value === \"string\") {\n return tagContinuationLines(value, prefix);\n }\n if (value instanceof Error) {\n return tagContinuationLines(value.stack ?? value.message, prefix);\n }\n return value;\n};\n\n/**\n * Adds the active request's replay marker. Keeping a string first argument in\n * place preserves formatting such as `console.log(\"user=%s\", id)`. Embedded\n * newlines in strings and Error stacks are tagged too, since container runtimes\n * may emit their continuation lines as separate log records.\n */\nexport const createReplayTaggedConsoleMethod =\n (original: ConsoleLogMethod): ConsoleLogMethod =>\n (...args: unknown[]): void => {\n let outputArgs = args;\n try {\n const ctx = requestCaptureContext.getStore();\n if (ctx?.mode === \"replay\") {\n const prefix = formatReplayIdLogTag(ctx.replayId);\n const preparedArgs = args.map((arg) =>\n prepareConsoleArgument(arg, prefix),\n );\n outputArgs =\n typeof preparedArgs[0] === \"string\"\n ? [prefix + preparedArgs[0], ...preparedArgs.slice(1)]\n : [prefix, ...preparedArgs];\n }\n } catch {\n // Log tagging must never interfere with the customer's logger. The\n // original call below still receives its untouched arguments.\n }\n original(...outputArgs);\n };\n\n/**\n * Patches Workerd's output-producing console methods once. Outside a replayed\n * request the wrappers are transparent; inside one they resolve the replay id\n * from Workerd's request-scoped AsyncLocalStorage, so concurrent replays cannot\n * label each other's output.\n */\nexport const installReplayLogTagging = (\n methods = console as unknown as Record<ConsoleMethodName, ConsoleLogMethod>,\n holder = globalThis as typeof globalThis & {\n [CONSOLE_TAGGING_INSTALLED]?: boolean;\n },\n): void => {\n if (holder[CONSOLE_TAGGING_INSTALLED]) {\n return;\n }\n holder[CONSOLE_TAGGING_INSTALLED] = true;\n\n for (const method of CONSOLE_METHODS) {\n try {\n methods[method] = createReplayTaggedConsoleMethod(\n methods[method].bind(methods),\n );\n } catch {\n // A runtime may expose a non-writable console method. Keep the remaining\n // methods useful rather than making replay initialization fail.\n }\n }\n};\n"],"names":[],"mappings":";;;;AAIA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACD;AAID,MAAM,4BAA4B,OAAO,IACvC,mDACD;AAKD,MAAM,yBAAyB;AAE/B,MAAM,wBAAwB,aAC5B,IAAI,uBAAuB,GAAG,SAAS;AAEzC,MAAM,wBAAwB,OAAe,WAC3C,MAAM,QAAQ,YAAY,KAAK,SAAS;AAE1C,MAAM,0BAA0B,OAAgB,WAA4B;AAC1E,KAAI,OAAO,UAAU,SACnB,QAAO,qBAAqB,OAAO,OAAO;AAE5C,KAAI,iBAAiB,MACnB,QAAO,qBAAqB,MAAM,SAAS,MAAM,SAAS,OAAO;AAEnE,QAAO;;;;;;;;AAST,MAAa,mCACV,cACA,GAAG,SAA0B;CAC5B,IAAI,aAAa;AACjB,KAAI;EACF,MAAM,MAAM,sBAAsB,UAAU;AAC5C,MAAI,KAAK,SAAS,UAAU;GAC1B,MAAM,SAAS,qBAAqB,IAAI,SAAS;GACjD,MAAM,eAAe,KAAK,KAAK,QAC7B,uBAAuB,KAAK,OAAO,CACpC;AACD,gBACE,OAAO,aAAa,OAAO,WACvB,CAAC,SAAS,aAAa,IAAI,GAAG,aAAa,MAAM,EAAE,CAAC,GACpD,CAAC,QAAQ,GAAG,aAAa;;SAE3B;AAIR,UAAS,GAAG,WAAW;;;;;;;;AAS3B,MAAa,2BACX,UAAU,SACV,SAAS,eAGA;AACT,KAAI,OAAO,2BACT;AAEF,QAAO,6BAA6B;AAEpC,MAAK,MAAM,UAAU,gBACnB,KAAI;AACF,UAAQ,UAAU,gCAChB,QAAQ,QAAQ,KAAK,QAAQ,CAC9B;SACK","debugId":"962faa5b-f3e8-548c-be10-1e341f1f56bd"}
1
+ {"version":3,"file":"replay-log-tagging.js","names":[],"sources":["../src/replay-log-tagging.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\n\ntype ConsoleLogMethod = (...args: unknown[]) => void;\n\nconst CONSOLE_METHODS = [\n \"debug\",\n \"error\",\n \"info\",\n \"log\",\n \"trace\",\n \"warn\",\n] as const;\n\ntype ConsoleMethodName = (typeof CONSOLE_METHODS)[number];\n\nconst CONSOLE_TAGGING_INSTALLED = Symbol.for(\n \"meticulous.workerd.consoleReplayTaggingInstalled\",\n);\n\n// This literal is part of the persisted log wire format. Keep it pinned here\n// rather than deriving it from the HTTP header, whose name can change\n// independently of logs that have already been written to S3.\nconst REPLAY_ID_LOG_TAG_NAME = \"meticulous-replay-id\";\n\nconst formatReplayIdLogTag = (replayId: string): string =>\n `[${REPLAY_ID_LOG_TAG_NAME}:${replayId}] `;\n\nconst tagContinuationLines = (value: string, prefix: string): string =>\n value.replace(/\\n(?=.)/g, `\\n${prefix}`);\n\nconst prepareConsoleArgument = (value: unknown, prefix: string): unknown => {\n if (typeof value === \"string\") {\n return tagContinuationLines(value, prefix);\n }\n if (value instanceof Error) {\n return tagContinuationLines(value.stack ?? value.message, prefix);\n }\n return value;\n};\n\n/**\n * Adds the active request's replay marker. Keeping a string first argument in\n * place preserves formatting such as `console.log(\"user=%s\", id)`. Embedded\n * newlines in strings and Error stacks are tagged too, since container runtimes\n * may emit their continuation lines as separate log records.\n */\nexport const createReplayTaggedConsoleMethod =\n (original: ConsoleLogMethod): ConsoleLogMethod =>\n (...args: unknown[]): void => {\n let outputArgs = args;\n try {\n const ctx = requestCaptureContext.getStore();\n if (ctx?.mode === \"replay\") {\n const prefix = formatReplayIdLogTag(ctx.replayId);\n const preparedArgs = args.map((arg) =>\n prepareConsoleArgument(arg, prefix),\n );\n outputArgs =\n typeof preparedArgs[0] === \"string\"\n ? [prefix + preparedArgs[0], ...preparedArgs.slice(1)]\n : [prefix, ...preparedArgs];\n }\n } catch {\n // Log tagging must never interfere with the customer's logger. The\n // original call below still receives its untouched arguments.\n }\n original(...outputArgs);\n };\n\n/**\n * Patches Workerd's output-producing console methods once. Outside a replayed\n * request the wrappers are transparent; inside one they resolve the replay id\n * from Workerd's request-scoped AsyncLocalStorage, so concurrent replays cannot\n * label each other's output.\n */\nexport const installReplayLogTagging = (\n methods = console as unknown as Record<ConsoleMethodName, ConsoleLogMethod>,\n holder = globalThis as typeof globalThis & {\n [CONSOLE_TAGGING_INSTALLED]?: boolean;\n },\n): void => {\n if (holder[CONSOLE_TAGGING_INSTALLED]) {\n return;\n }\n holder[CONSOLE_TAGGING_INSTALLED] = true;\n\n for (const method of CONSOLE_METHODS) {\n try {\n methods[method] = createReplayTaggedConsoleMethod(\n methods[method].bind(methods),\n );\n } catch {\n // A runtime may expose a non-writable console method. Keep the remaining\n // methods useful rather than making replay initialization fail.\n }\n }\n};\n"],"mappings":";;AAIA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACD;AAID,MAAM,4BAA4B,OAAO,IACvC,mDACD;AAKD,MAAM,yBAAyB;AAE/B,MAAM,wBAAwB,aAC5B,IAAI,uBAAuB,GAAG,SAAS;AAEzC,MAAM,wBAAwB,OAAe,WAC3C,MAAM,QAAQ,YAAY,KAAK,SAAS;AAE1C,MAAM,0BAA0B,OAAgB,WAA4B;AAC1E,KAAI,OAAO,UAAU,SACnB,QAAO,qBAAqB,OAAO,OAAO;AAE5C,KAAI,iBAAiB,MACnB,QAAO,qBAAqB,MAAM,SAAS,MAAM,SAAS,OAAO;AAEnE,QAAO;;;;;;;;AAST,MAAa,mCACV,cACA,GAAG,SAA0B;CAC5B,IAAI,aAAa;AACjB,KAAI;EACF,MAAM,MAAM,sBAAsB,UAAU;AAC5C,MAAI,KAAK,SAAS,UAAU;GAC1B,MAAM,SAAS,qBAAqB,IAAI,SAAS;GACjD,MAAM,eAAe,KAAK,KAAK,QAC7B,uBAAuB,KAAK,OAAO,CACpC;AACD,gBACE,OAAO,aAAa,OAAO,WACvB,CAAC,SAAS,aAAa,IAAI,GAAG,aAAa,MAAM,EAAE,CAAC,GACpD,CAAC,QAAQ,GAAG,aAAa;;SAE3B;AAIR,UAAS,GAAG,WAAW;;;;;;;;AAS3B,MAAa,2BACX,UAAU,SACV,SAAS,eAGA;AACT,KAAI,OAAO,2BACT;AAEF,QAAO,6BAA6B;AAEpC,MAAK,MAAM,UAAU,gBACnB,KAAI;AACF,UAAQ,UAAU,gCAChB,QAAQ,QAAQ,KAAK,QAAQ,CAC9B;SACK"}
@@ -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]="4f7cf8f6-3a9a-5d41-95a3-192277e578fd")}catch(e){}}();
3
1
  import { warnOnce } from "./log.js";
4
2
  //#region src/replay-sidecar-url.ts
5
3
  /**
@@ -58,5 +56,4 @@ const isPrivateIpv4 = (hostname) => {
58
56
  //#endregion
59
57
  export { parseReplaySidecarUrl };
60
58
 
61
- //# sourceMappingURL=replay-sidecar-url.js.map
62
- //# debugId=4f7cf8f6-3a9a-5d41-95a3-192277e578fd
59
+ //# sourceMappingURL=replay-sidecar-url.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"replay-sidecar-url.js","sources":["../src/replay-sidecar-url.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\n\n/**\n * Validation for the replay sidecar origin carried on inbound requests.\n *\n * The value arrives in a request header, so in principle anything that can reach the worker\n * can propose one — and the shim POSTs outbound request bodies to whatever it accepts. The\n * guard therefore only honours an origin that could plausibly be a Meticulous replay\n * sidecar running alongside the app: plaintext HTTP on a loopback, docker-gateway or\n * private-network host.\n *\n * `https:` is rejected deliberately. A sidecar reachable over loopback or a pod network has\n * no TLS to offer, so permitting `https:` would buy nothing except a way for a forged header\n * to exfiltrate request bodies to an arbitrary internet host.\n */\n\nconst MAX_URL_LENGTH = 2048;\n\nconst ALLOWED_HOSTNAMES = new Set([\n \"localhost\",\n // Docker Desktop and Podman expose the host to containers under these names.\n \"host.docker.internal\",\n \"host.containers.internal\",\n]);\n\nexport const parseReplaySidecarUrl = (\n raw: string | null | undefined,\n): string | undefined => {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return undefined;\n }\n if (raw.length > MAX_URL_LENGTH) {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an implausibly long Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an unparseable Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n const isAcceptable =\n url.protocol === \"http:\" &&\n url.username === \"\" &&\n url.password === \"\" &&\n (url.pathname === \"\" || url.pathname === \"/\") &&\n url.search === \"\" &&\n url.hash === \"\" &&\n isLocalHostname(url.hostname);\n\n if (!isAcceptable) {\n warnOnce(\n \"replay-sidecar-url\",\n `Ignoring the Meticulous replay sidecar URL \"${raw}\" — only a plain http:// origin on a loopback, docker-gateway or private-network host is honoured.`,\n );\n return undefined;\n }\n\n // Return the parsed origin rather than the raw string: it is what `fetch` would resolve\n // the value to anyway, so the host that was validated is exactly the host that gets\n // called (alternative IPv4 spellings like \"0177.0.0.1\" normalize here, rather than being\n // checked in one form and dialled in another). Carries no trailing slash, matching\n // resolveSidecarUrl's normalization so the self-capture prefix check lines up.\n return url.origin;\n};\n\nconst isLocalHostname = (hostname: string): boolean => {\n const lower = hostname.toLowerCase();\n if (ALLOWED_HOSTNAMES.has(lower)) {\n return true;\n }\n // URL keeps IPv6 literals bracketed.\n const unbracketed =\n lower.startsWith(\"[\") && lower.endsWith(\"]\")\n ? lower.slice(1, -1)\n : undefined;\n if (unbracketed !== undefined) {\n return unbracketed === \"::1\";\n }\n return isPrivateIpv4(lower);\n};\n\nconst isPrivateIpv4 = (hostname: string): boolean => {\n const parts = hostname.split(\".\");\n if (parts.length !== 4) {\n return false;\n }\n const octets = parts.map((part) =>\n // Reject anything that is not a plain decimal octet: \"010\" and \"0x7f\" are parsed\n // inconsistently across resolvers, and \"1e2\" would pass a bare Number() check.\n /^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN,\n );\n if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) {\n return false;\n }\n const [first, second] = octets;\n // Deliberately excludes link-local (169.254.0.0/16): no sidecar is ever reachable there,\n // but the cloud metadata endpoints are (169.254.169.254), so allowing it would only widen\n // the SSRF surface. The rest of the codebase blocks that range for the same reason — see\n // `staging-backend-url.ts` and `assets-app-server.ts`.\n return (\n first === 127 || // loopback\n first === 10 || // RFC1918\n (first === 172 && second >= 16 && second <= 31) || // RFC1918\n (first === 192 && second === 168) // RFC1918\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAM,iBAAiB;AAEvB,MAAM,oBAAoB,IAAI,IAAI;CAChC;CAEA;CACA;CACD,CAAC;AAEF,MAAa,yBACX,QACuB;AACvB,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C;AAEF,KAAI,IAAI,SAAS,gBAAgB;AAC/B,WACE,sBACA,8DACD;AACD;;CAGF,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,IAAI;SACZ;AACN,WACE,sBACA,yDACD;AACD;;AAYF,KAAI,EARF,IAAI,aAAa,WACjB,IAAI,aAAa,MACjB,IAAI,aAAa,OAChB,IAAI,aAAa,MAAM,IAAI,aAAa,QACzC,IAAI,WAAW,MACf,IAAI,SAAS,MACb,gBAAgB,IAAI,SAAS,GAEZ;AACjB,WACE,sBACA,+CAA+C,IAAI,oGACpD;AACD;;AAQF,QAAO,IAAI;;AAGb,MAAM,mBAAmB,aAA8B;CACrD,MAAM,QAAQ,SAAS,aAAa;AACpC,KAAI,kBAAkB,IAAI,MAAM,CAC9B,QAAO;CAGT,MAAM,cACJ,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,GACxC,MAAM,MAAM,GAAG,GAAG,GAClB,KAAA;AACN,KAAI,gBAAgB,KAAA,EAClB,QAAO,gBAAgB;AAEzB,QAAO,cAAc,MAAM;;AAG7B,MAAM,iBAAiB,aAA8B;CACnD,MAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,KAAI,MAAM,WAAW,EACnB,QAAO;CAET,MAAM,SAAS,MAAM,KAAK,SAGxB,YAAY,KAAK,KAAK,GAAG,OAAO,KAAK,GAAG,IACzC;AACD,KAAI,OAAO,MAAM,UAAU,OAAO,MAAM,MAAM,IAAI,QAAQ,IAAI,CAC5D,QAAO;CAET,MAAM,CAAC,OAAO,UAAU;AAKxB,QACE,UAAU,OACV,UAAU,MACT,UAAU,OAAO,UAAU,MAAM,UAAU,MAC3C,UAAU,OAAO,WAAW","debugId":"4f7cf8f6-3a9a-5d41-95a3-192277e578fd"}
1
+ {"version":3,"file":"replay-sidecar-url.js","names":[],"sources":["../src/replay-sidecar-url.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\n\n/**\n * Validation for the replay sidecar origin carried on inbound requests.\n *\n * The value arrives in a request header, so in principle anything that can reach the worker\n * can propose one — and the shim POSTs outbound request bodies to whatever it accepts. The\n * guard therefore only honours an origin that could plausibly be a Meticulous replay\n * sidecar running alongside the app: plaintext HTTP on a loopback, docker-gateway or\n * private-network host.\n *\n * `https:` is rejected deliberately. A sidecar reachable over loopback or a pod network has\n * no TLS to offer, so permitting `https:` would buy nothing except a way for a forged header\n * to exfiltrate request bodies to an arbitrary internet host.\n */\n\nconst MAX_URL_LENGTH = 2048;\n\nconst ALLOWED_HOSTNAMES = new Set([\n \"localhost\",\n // Docker Desktop and Podman expose the host to containers under these names.\n \"host.docker.internal\",\n \"host.containers.internal\",\n]);\n\nexport const parseReplaySidecarUrl = (\n raw: string | null | undefined,\n): string | undefined => {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return undefined;\n }\n if (raw.length > MAX_URL_LENGTH) {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an implausibly long Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an unparseable Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n const isAcceptable =\n url.protocol === \"http:\" &&\n url.username === \"\" &&\n url.password === \"\" &&\n (url.pathname === \"\" || url.pathname === \"/\") &&\n url.search === \"\" &&\n url.hash === \"\" &&\n isLocalHostname(url.hostname);\n\n if (!isAcceptable) {\n warnOnce(\n \"replay-sidecar-url\",\n `Ignoring the Meticulous replay sidecar URL \"${raw}\" — only a plain http:// origin on a loopback, docker-gateway or private-network host is honoured.`,\n );\n return undefined;\n }\n\n // Return the parsed origin rather than the raw string: it is what `fetch` would resolve\n // the value to anyway, so the host that was validated is exactly the host that gets\n // called (alternative IPv4 spellings like \"0177.0.0.1\" normalize here, rather than being\n // checked in one form and dialled in another). Carries no trailing slash, matching\n // resolveSidecarUrl's normalization so the self-capture prefix check lines up.\n return url.origin;\n};\n\nconst isLocalHostname = (hostname: string): boolean => {\n const lower = hostname.toLowerCase();\n if (ALLOWED_HOSTNAMES.has(lower)) {\n return true;\n }\n // URL keeps IPv6 literals bracketed.\n const unbracketed =\n lower.startsWith(\"[\") && lower.endsWith(\"]\")\n ? lower.slice(1, -1)\n : undefined;\n if (unbracketed !== undefined) {\n return unbracketed === \"::1\";\n }\n return isPrivateIpv4(lower);\n};\n\nconst isPrivateIpv4 = (hostname: string): boolean => {\n const parts = hostname.split(\".\");\n if (parts.length !== 4) {\n return false;\n }\n const octets = parts.map((part) =>\n // Reject anything that is not a plain decimal octet: \"010\" and \"0x7f\" are parsed\n // inconsistently across resolvers, and \"1e2\" would pass a bare Number() check.\n /^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN,\n );\n if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) {\n return false;\n }\n const [first, second] = octets;\n // Deliberately excludes link-local (169.254.0.0/16): no sidecar is ever reachable there,\n // but the cloud metadata endpoints are (169.254.169.254), so allowing it would only widen\n // the SSRF surface. The rest of the codebase blocks that range for the same reason — see\n // `staging-backend-url.ts` and `assets-app-server.ts`.\n return (\n first === 127 || // loopback\n first === 10 || // RFC1918\n (first === 172 && second >= 16 && second <= 31) || // RFC1918\n (first === 192 && second === 168) // RFC1918\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,MAAM,iBAAiB;AAEvB,MAAM,oBAAoB,IAAI,IAAI;CAChC;CAEA;CACA;CACD,CAAC;AAEF,MAAa,yBACX,QACuB;AACvB,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C;AAEF,KAAI,IAAI,SAAS,gBAAgB;AAC/B,WACE,sBACA,8DACD;AACD;;CAGF,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,IAAI;SACZ;AACN,WACE,sBACA,yDACD;AACD;;AAYF,KAAI,EARF,IAAI,aAAa,WACjB,IAAI,aAAa,MACjB,IAAI,aAAa,OAChB,IAAI,aAAa,MAAM,IAAI,aAAa,QACzC,IAAI,WAAW,MACf,IAAI,SAAS,MACb,gBAAgB,IAAI,SAAS,GAEZ;AACjB,WACE,sBACA,+CAA+C,IAAI,oGACpD;AACD;;AAQF,QAAO,IAAI;;AAGb,MAAM,mBAAmB,aAA8B;CACrD,MAAM,QAAQ,SAAS,aAAa;AACpC,KAAI,kBAAkB,IAAI,MAAM,CAC9B,QAAO;CAGT,MAAM,cACJ,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,GACxC,MAAM,MAAM,GAAG,GAAG,GAClB,KAAA;AACN,KAAI,gBAAgB,KAAA,EAClB,QAAO,gBAAgB;AAEzB,QAAO,cAAc,MAAM;;AAG7B,MAAM,iBAAiB,aAA8B;CACnD,MAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,KAAI,MAAM,WAAW,EACnB,QAAO;CAET,MAAM,SAAS,MAAM,KAAK,SAGxB,YAAY,KAAK,KAAK,GAAG,OAAO,KAAK,GAAG,IACzC;AACD,KAAI,OAAO,MAAM,UAAU,OAAO,MAAM,MAAM,IAAI,QAAQ,IAAI,CAC5D,QAAO;CAET,MAAM,CAAC,OAAO,UAAU;AAKxB,QACE,UAAU,OACV,UAAU,MACT,UAAU,OAAO,UAAU,MAAM,UAAU,MAC3C,UAAU,OAAO,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]="4d6a9c0e-a5a7-52d6-8b38-28342d0f79e3")}catch(e){}}();
3
1
  import { sidecarFetch } from "./sidecar-transport.js";
4
2
  import { warnOnce } from "./log.js";
5
3
  import { SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
@@ -187,5 +185,4 @@ const withTimeout = async (ms, send) => {
187
185
  //#endregion
188
186
  export { getReplaySessionInfo, postCaptureEvents, postCoverageReport, postOutboundFetchLookup };
189
187
 
190
- //# sourceMappingURL=sidecar-client.js.map
191
- //# debugId=4d6a9c0e-a5a7-52d6-8b38-28342d0f79e3
188
+ //# sourceMappingURL=sidecar-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-client.js","sources":["../src/sidecar-client.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n type CaptureEvent,\n type CaptureEventsPayload,\n type CoverageReportRequest,\n type OutboundFetchLookupRequest,\n type OutboundFetchLookupResponse,\n type ReplaySessionInfoResponse,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\nimport { sidecarFetch, type SidecarTransport } from \"./sidecar-transport\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n/**\n * Replay calls sit in the request path, unlike record mode's fire-and-forget reporting, so\n * they must be bounded: without a timeout a wedged sidecar would hang every outbound fetch\n * the app makes, and therefore the whole replay, with nothing to look at.\n */\nconst SESSION_INFO_TIMEOUT_MS = 10_000;\nconst LOOKUP_TIMEOUT_MS = 5_000;\n\n/**\n * Capture reporting is off the response path (it runs under `ctx.waitUntil`), so a timeout here\n * is not about latency the app can see — it is about not holding the request context open on a\n * sidecar that will never answer. A refused connection rejects immediately, but a sidecar URL\n * pointing at a host that drops packets rather than refusing them (a firewalled container or LAN\n * address instead of loopback) would otherwise leave the POST pending until the runtime tears the\n * context down, on every captured call.\n *\n * Generous relative to what it bounds: a local sidecar answers a small batch in single-digit\n * milliseconds, so this only ever fires on one that is wedged or unreachable — never on one that\n * is merely busy. Being generous costs nothing because {@link withTimeout} clears the timer the\n * moment the POST settles.\n */\nconst CAPTURE_POST_TIMEOUT_MS = 2_000;\n\n/**\n * The same bound for a service-binding transport, where the sidecar is a Worker that hands the\n * batch to a Durable Object before answering. That object may live in another region and is\n * single-threaded, so a busy one can legitimately take far longer than a loopback process — and\n * failing the report early would drop spans a slightly longer wait would have recorded.\n * `waitUntil` allows 30s past the response, so this stays well inside its budget.\n */\nconst CAPTURE_POST_BINDING_TIMEOUT_MS = 10_000;\n\n/**\n * Coverage reports are larger than capture events — the first one of an isolate\n * carries the whole id→line map — so they get more headroom than\n * {@link CAPTURE_POST_TIMEOUT_MS}. Still bounded, and for the same reason: an\n * unreachable sidecar must not hold the request context open.\n */\nconst COVERAGE_POST_TIMEOUT_MS = 5_000;\n\n/** POSTs capture events to the sidecar. Never rejects — reporting failures only warn (once). */\nexport const postCaptureEvents = async (\n transport: SidecarTransport,\n events: CaptureEvent[],\n): Promise<void> => {\n const timeoutMs =\n transport.kind === \"binding\"\n ? CAPTURE_POST_BINDING_TIMEOUT_MS\n : CAPTURE_POST_TIMEOUT_MS;\n try {\n const payload: CaptureEventsPayload = { events };\n await withTimeout(timeoutMs, async (signal) => {\n const response = await sidecarFetch(\n transport,\n getOriginalFetch(),\n SIDECAR_EVENTS_PATH,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"sidecar-rejected\",\n `Meticulous sidecar rejected capture events (HTTP ${response.status}).`,\n );\n }\n await response.arrayBuffer().catch(() => undefined);\n });\n } catch (error) {\n warnOnce(\n \"sidecar-unreachable\",\n \"Could not reach the Meticulous backend recorder sidecar — backend session events are not being recorded.\",\n error,\n );\n }\n};\n\n/**\n * Outcome of the session handshake.\n *\n * `unavailable` and `unreachable` both mean \"do not replay\", but they are kept apart\n * because only the first is a settled answer. The caller caches per isolate, and caching a\n * one-off network blip would disable mocking for the rest of the session — silently sending\n * every later outbound call to the real service.\n */\nexport type ReplaySessionInfoResult =\n /** The sidecar holds mocks for this session. */\n | { outcome: \"found\"; clockAnchorMs: number | undefined }\n /** The sidecar answered and will never serve this session: no mocks, or no such route. */\n | { outcome: \"unavailable\" }\n /** Could not get an answer — timeout, transport error, 5xx. May succeed later. */\n | { outcome: \"unreachable\" };\n\n/**\n * Asks whether the sidecar holds mocks for a session, and at what instant to freeze the\n * clock while serving it.\n *\n * A 404 is how a record-only sidecar answers, so it counts as a settled `unavailable`\n * rather than a retryable blip.\n */\nexport const getReplaySessionInfo = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<ReplaySessionInfoResult> => {\n const url = `${sidecarUrl}${SIDECAR_REPLAY_SESSION_PATH}?sessionId=${encodeURIComponent(\n frontendSessionId,\n )}`;\n try {\n return await withTimeout(SESSION_INFO_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(url, {\n headers: {\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n signal,\n });\n if (!response.ok) {\n await response.arrayBuffer().catch(() => undefined);\n // 4xx is the sidecar telling us something definitive — no such route (a record-only\n // sidecar), or a protocol version it will not speak. Retrying cannot change either.\n // 5xx may well be a sidecar still finding its feet, so leave it retryable.\n if (response.status >= 500) {\n warnOnce(\n \"replay-session-unreachable\",\n `Meticulous replay sidecar failed the replay session handshake (HTTP ${response.status}) — will retry on the next request.`,\n );\n return { outcome: \"unreachable\" };\n }\n warnOnce(\n \"replay-session-unavailable\",\n `Meticulous replay sidecar did not recognise the replay session route (HTTP ${response.status}) — outbound calls will not be mocked.`,\n );\n return { outcome: \"unavailable\" };\n }\n const info = (await response.json()) as ReplaySessionInfoResponse;\n if (info.found !== true) {\n warnOnce(\n \"replay-session-unknown\",\n \"Meticulous replay sidecar holds no mocks for this session — outbound calls will not be mocked.\",\n );\n return { outcome: \"unavailable\" };\n }\n return {\n outcome: \"found\",\n clockAnchorMs:\n typeof info.clockAnchorMs === \"number\"\n ? info.clockAnchorMs\n : undefined,\n };\n });\n } catch (error) {\n warnOnce(\n \"replay-session-unreachable\",\n \"Could not reach the Meticulous replay sidecar — will retry on the next request.\",\n error,\n );\n return { outcome: \"unreachable\" };\n }\n};\n\n/**\n * Looks up a recorded response for one outbound call. Never rejects; returns undefined when\n * the sidecar could not be consulted, which the caller fails the request on — an unanswered\n * lookup is not evidence that a real call is safe.\n */\nexport const postOutboundFetchLookup = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: OutboundFetchLookupRequest,\n): Promise<OutboundFetchLookupResponse | undefined> => {\n try {\n return await withTimeout(LOOKUP_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"replay-lookup-rejected\",\n `Meticulous replay sidecar rejected a mock lookup (HTTP ${response.status}).`,\n );\n await response.arrayBuffer().catch(() => undefined);\n return undefined;\n }\n return (await response.json()) as OutboundFetchLookupResponse;\n });\n } catch (error) {\n warnOnce(\n \"replay-lookup-unreachable\",\n \"Could not reach the Meticulous replay sidecar for a mock lookup — the call will fail.\",\n error,\n );\n return undefined;\n }\n};\n\n/**\n * Outcome of a coverage report.\n *\n * `unsupported` and `failed` both mean the report did not land, but only the\n * first is settled: the caller latches `unsupported` and stops collecting, while\n * `failed` leaves the id→line map queued for the next report. Conflating them\n * would either disable coverage on one blip or lose a map permanently.\n */\nexport type CoverageReportOutcome = \"accepted\" | \"unsupported\" | \"failed\";\n\n/**\n * Reports one request's line coverage. Runs under `ctx.waitUntil`, so it is off\n * the response path like record-mode capture reporting.\n *\n * Returns `unsupported` when the sidecar will never accept coverage (404 — an\n * older sidecar, or one started without coverage enabled), which the caller\n * latches so it stops paying to serialise hits for the rest of the isolate's\n * life.\n */\nexport const postCoverageReport = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: CoverageReportRequest,\n): Promise<CoverageReportOutcome> => {\n try {\n return await withTimeout(COVERAGE_POST_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_COVERAGE_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n await response.arrayBuffer().catch(() => undefined);\n if (response.status === 404) {\n return \"unsupported\";\n }\n if (!response.ok) {\n warnOnce(\n \"coverage-rejected\",\n `Meticulous replay sidecar rejected a coverage report (HTTP ${response.status}).`,\n );\n return \"failed\";\n }\n return \"accepted\";\n });\n } catch (error) {\n warnOnce(\n \"coverage-unreachable\",\n \"Could not report line coverage to the Meticulous replay sidecar.\",\n error,\n );\n return \"failed\";\n }\n};\n\n/**\n * Runs `send` under an abort signal that fires after `ms`, clearing the timer as soon as the\n * call settles — including the response body read, which an abort must still cut short.\n *\n * Deliberately not `AbortSignal.timeout`: its timer cannot be cancelled, and a pending timer\n * keeps workerd's request context alive. Bounding a 5ms POST at 2s would then pin the context\n * for the remaining ~2s on every captured call — worse than the unbounded wait it exists to\n * prevent, and on the healthy path rather than the broken one. `body-capture.ts` uses this\n * same clearable shape, for the same reason.\n */\nconst withTimeout = async <T>(\n ms: number,\n send: (signal: AbortSignal) => Promise<T>,\n): Promise<T> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await send(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n};\n"],"names":[],"mappings":"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","debugId":"4d6a9c0e-a5a7-52d6-8b38-28342d0f79e3"}
1
+ {"version":3,"file":"sidecar-client.js","names":[],"sources":["../src/sidecar-client.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n type CaptureEvent,\n type CaptureEventsPayload,\n type CoverageReportRequest,\n type OutboundFetchLookupRequest,\n type OutboundFetchLookupResponse,\n type ReplaySessionInfoResponse,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\nimport { sidecarFetch, type SidecarTransport } from \"./sidecar-transport\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n/**\n * Replay calls sit in the request path, unlike record mode's fire-and-forget reporting, so\n * they must be bounded: without a timeout a wedged sidecar would hang every outbound fetch\n * the app makes, and therefore the whole replay, with nothing to look at.\n */\nconst SESSION_INFO_TIMEOUT_MS = 10_000;\nconst LOOKUP_TIMEOUT_MS = 5_000;\n\n/**\n * Capture reporting is off the response path (it runs under `ctx.waitUntil`), so a timeout here\n * is not about latency the app can see — it is about not holding the request context open on a\n * sidecar that will never answer. A refused connection rejects immediately, but a sidecar URL\n * pointing at a host that drops packets rather than refusing them (a firewalled container or LAN\n * address instead of loopback) would otherwise leave the POST pending until the runtime tears the\n * context down, on every captured call.\n *\n * Generous relative to what it bounds: a local sidecar answers a small batch in single-digit\n * milliseconds, so this only ever fires on one that is wedged or unreachable — never on one that\n * is merely busy. Being generous costs nothing because {@link withTimeout} clears the timer the\n * moment the POST settles.\n */\nconst CAPTURE_POST_TIMEOUT_MS = 2_000;\n\n/**\n * The same bound for a service-binding transport, where the sidecar is a Worker that hands the\n * batch to a Durable Object before answering. That object may live in another region and is\n * single-threaded, so a busy one can legitimately take far longer than a loopback process — and\n * failing the report early would drop spans a slightly longer wait would have recorded.\n * `waitUntil` allows 30s past the response, so this stays well inside its budget.\n */\nconst CAPTURE_POST_BINDING_TIMEOUT_MS = 10_000;\n\n/**\n * Coverage reports are larger than capture events — the first one of an isolate\n * carries the whole id→line map — so they get more headroom than\n * {@link CAPTURE_POST_TIMEOUT_MS}. Still bounded, and for the same reason: an\n * unreachable sidecar must not hold the request context open.\n */\nconst COVERAGE_POST_TIMEOUT_MS = 5_000;\n\n/** POSTs capture events to the sidecar. Never rejects — reporting failures only warn (once). */\nexport const postCaptureEvents = async (\n transport: SidecarTransport,\n events: CaptureEvent[],\n): Promise<void> => {\n const timeoutMs =\n transport.kind === \"binding\"\n ? CAPTURE_POST_BINDING_TIMEOUT_MS\n : CAPTURE_POST_TIMEOUT_MS;\n try {\n const payload: CaptureEventsPayload = { events };\n await withTimeout(timeoutMs, async (signal) => {\n const response = await sidecarFetch(\n transport,\n getOriginalFetch(),\n SIDECAR_EVENTS_PATH,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"sidecar-rejected\",\n `Meticulous sidecar rejected capture events (HTTP ${response.status}).`,\n );\n }\n await response.arrayBuffer().catch(() => undefined);\n });\n } catch (error) {\n warnOnce(\n \"sidecar-unreachable\",\n \"Could not reach the Meticulous backend recorder sidecar — backend session events are not being recorded.\",\n error,\n );\n }\n};\n\n/**\n * Outcome of the session handshake.\n *\n * `unavailable` and `unreachable` both mean \"do not replay\", but they are kept apart\n * because only the first is a settled answer. The caller caches per isolate, and caching a\n * one-off network blip would disable mocking for the rest of the session — silently sending\n * every later outbound call to the real service.\n */\nexport type ReplaySessionInfoResult =\n /** The sidecar holds mocks for this session. */\n | { outcome: \"found\"; clockAnchorMs: number | undefined }\n /** The sidecar answered and will never serve this session: no mocks, or no such route. */\n | { outcome: \"unavailable\" }\n /** Could not get an answer — timeout, transport error, 5xx. May succeed later. */\n | { outcome: \"unreachable\" };\n\n/**\n * Asks whether the sidecar holds mocks for a session, and at what instant to freeze the\n * clock while serving it.\n *\n * A 404 is how a record-only sidecar answers, so it counts as a settled `unavailable`\n * rather than a retryable blip.\n */\nexport const getReplaySessionInfo = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<ReplaySessionInfoResult> => {\n const url = `${sidecarUrl}${SIDECAR_REPLAY_SESSION_PATH}?sessionId=${encodeURIComponent(\n frontendSessionId,\n )}`;\n try {\n return await withTimeout(SESSION_INFO_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(url, {\n headers: {\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n signal,\n });\n if (!response.ok) {\n await response.arrayBuffer().catch(() => undefined);\n // 4xx is the sidecar telling us something definitive — no such route (a record-only\n // sidecar), or a protocol version it will not speak. Retrying cannot change either.\n // 5xx may well be a sidecar still finding its feet, so leave it retryable.\n if (response.status >= 500) {\n warnOnce(\n \"replay-session-unreachable\",\n `Meticulous replay sidecar failed the replay session handshake (HTTP ${response.status}) — will retry on the next request.`,\n );\n return { outcome: \"unreachable\" };\n }\n warnOnce(\n \"replay-session-unavailable\",\n `Meticulous replay sidecar did not recognise the replay session route (HTTP ${response.status}) — outbound calls will not be mocked.`,\n );\n return { outcome: \"unavailable\" };\n }\n const info = (await response.json()) as ReplaySessionInfoResponse;\n if (info.found !== true) {\n warnOnce(\n \"replay-session-unknown\",\n \"Meticulous replay sidecar holds no mocks for this session — outbound calls will not be mocked.\",\n );\n return { outcome: \"unavailable\" };\n }\n return {\n outcome: \"found\",\n clockAnchorMs:\n typeof info.clockAnchorMs === \"number\"\n ? info.clockAnchorMs\n : undefined,\n };\n });\n } catch (error) {\n warnOnce(\n \"replay-session-unreachable\",\n \"Could not reach the Meticulous replay sidecar — will retry on the next request.\",\n error,\n );\n return { outcome: \"unreachable\" };\n }\n};\n\n/**\n * Looks up a recorded response for one outbound call. Never rejects; returns undefined when\n * the sidecar could not be consulted, which the caller fails the request on — an unanswered\n * lookup is not evidence that a real call is safe.\n */\nexport const postOutboundFetchLookup = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: OutboundFetchLookupRequest,\n): Promise<OutboundFetchLookupResponse | undefined> => {\n try {\n return await withTimeout(LOOKUP_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n if (!response.ok) {\n warnOnce(\n \"replay-lookup-rejected\",\n `Meticulous replay sidecar rejected a mock lookup (HTTP ${response.status}).`,\n );\n await response.arrayBuffer().catch(() => undefined);\n return undefined;\n }\n return (await response.json()) as OutboundFetchLookupResponse;\n });\n } catch (error) {\n warnOnce(\n \"replay-lookup-unreachable\",\n \"Could not reach the Meticulous replay sidecar for a mock lookup — the call will fail.\",\n error,\n );\n return undefined;\n }\n};\n\n/**\n * Outcome of a coverage report.\n *\n * `unsupported` and `failed` both mean the report did not land, but only the\n * first is settled: the caller latches `unsupported` and stops collecting, while\n * `failed` leaves the id→line map queued for the next report. Conflating them\n * would either disable coverage on one blip or lose a map permanently.\n */\nexport type CoverageReportOutcome = \"accepted\" | \"unsupported\" | \"failed\";\n\n/**\n * Reports one request's line coverage. Runs under `ctx.waitUntil`, so it is off\n * the response path like record-mode capture reporting.\n *\n * Returns `unsupported` when the sidecar will never accept coverage (404 — an\n * older sidecar, or one started without coverage enabled), which the caller\n * latches so it stops paying to serialise hits for the rest of the isolate's\n * life.\n */\nexport const postCoverageReport = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n payload: CoverageReportRequest,\n): Promise<CoverageReportOutcome> => {\n try {\n return await withTimeout(COVERAGE_POST_TIMEOUT_MS, async (signal) => {\n const response = await fetchFn(\n `${sidecarUrl}${SIDECAR_REPLAY_COVERAGE_PATH}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n signal,\n },\n );\n await response.arrayBuffer().catch(() => undefined);\n if (response.status === 404) {\n return \"unsupported\";\n }\n if (!response.ok) {\n warnOnce(\n \"coverage-rejected\",\n `Meticulous replay sidecar rejected a coverage report (HTTP ${response.status}).`,\n );\n return \"failed\";\n }\n return \"accepted\";\n });\n } catch (error) {\n warnOnce(\n \"coverage-unreachable\",\n \"Could not report line coverage to the Meticulous replay sidecar.\",\n error,\n );\n return \"failed\";\n }\n};\n\n/**\n * Runs `send` under an abort signal that fires after `ms`, clearing the timer as soon as the\n * call settles — including the response body read, which an abort must still cut short.\n *\n * Deliberately not `AbortSignal.timeout`: its timer cannot be cancelled, and a pending timer\n * keeps workerd's request context alive. Bounding a 5ms POST at 2s would then pin the context\n * for the remaining ~2s on every captured call — worse than the unbounded wait it exists to\n * prevent, and on the healthy path rather than the broken one. `body-capture.ts` uses this\n * same clearable shape, for the same reason.\n */\nconst withTimeout = async <T>(\n ms: number,\n send: (signal: AbortSignal) => Promise<T>,\n): Promise<T> => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n try {\n return await send(controller.signal);\n } finally {\n clearTimeout(timer);\n }\n};\n"],"mappings":"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"}
@@ -4,8 +4,6 @@
4
4
  * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to
5
5
  * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.
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]="4c3fd120-a382-55e7-8105-c6ff77ac4d6e")}catch(e){}}();
9
7
  const SIDECAR_BINDING_ORIGIN = "https://meticulous-sidecar.invalid";
10
8
  /** Env key holding a service binding to the Meticulous recorder sidecar Worker. */
11
9
  const SIDECAR_BINDING_ENV_KEY = "METICULOUS_SIDECAR";
@@ -48,5 +46,4 @@ const isFetcher = (value) => value !== null && typeof value === "object" && type
48
46
  //#endregion
49
47
  export { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, resolveSidecarTransport, sidecarFetch, transportOrigin };
50
48
 
51
- //# sourceMappingURL=sidecar-transport.js.map
52
- //# debugId=4c3fd120-a382-55e7-8105-c6ff77ac4d6e
49
+ //# sourceMappingURL=sidecar-transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-transport.js","sources":["../src/sidecar-transport.ts"],"sourcesContent":["/**\n * Anything with a `fetch` — a Cloudflare service binding (`Fetcher`), or a Durable Object stub.\n * Structural so the shim needs no `@cloudflare/workers-types` dependency.\n */\nexport interface SidecarFetcher {\n fetch: (...args: unknown[]) => Promise<Response>;\n}\n\n/**\n * How capture events reach the sidecar.\n *\n * A local `wrangler dev` recording posts to a loopback URL. A deployed Worker cannot: there is\n * no sidecar process at the edge, and one HTTPS round trip per event to a distant host would be\n * both slow and a subrequest per captured call. So a deployed Worker instead talks to a\n * Meticulous sidecar Worker in the same account through a **service binding**, which stays\n * inside the colo and needs neither DNS nor TLS.\n */\nexport type SidecarTransport =\n | { kind: \"url\"; url: string }\n | { kind: \"binding\"; fetcher: SidecarFetcher; instance: object };\n\n/**\n * Origin the shim addresses a service-binding request to. A service binding delivers to the\n * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to\n * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.\n */\nexport const SIDECAR_BINDING_ORIGIN = \"https://meticulous-sidecar.invalid\";\n\n/** Env key holding a service binding to the Meticulous recorder sidecar Worker. */\nexport const SIDECAR_BINDING_ENV_KEY = \"METICULOUS_SIDECAR\";\n\n/** Env key holding the URL of a local recorder sidecar process. */\nexport const SIDECAR_URL_ENV_KEY = \"METICULOUS_SIDECAR_URL\";\n\n/** The origin the shim's own sidecar requests go to, for the self-capture guards. */\nexport const transportOrigin = (transport: SidecarTransport): string =>\n transport.kind === \"url\" ? transport.url : SIDECAR_BINDING_ORIGIN;\n\n/**\n * Sends one request to the sidecar, whichever transport is in use.\n *\n * `fetchFn` is the unpatched global fetch, passed in rather than imported so this module stays\n * free of the patch machinery; it is unused for a binding, whose `fetch` is its own method.\n */\nexport const sidecarFetch = (\n transport: SidecarTransport,\n fetchFn: typeof globalThis.fetch,\n path: string,\n init: RequestInit,\n): Promise<Response> => {\n const url = `${transportOrigin(transport)}${path}`;\n return transport.kind === \"url\"\n ? fetchFn(url, init)\n : transport.fetcher.fetch(new Request(url, init));\n};\n\nexport interface ResolveSidecarOptions {\n /** Explicit binding, taking precedence over anything found on `env`. */\n sidecarBinding?: SidecarFetcher;\n /** Explicit URL, taking precedence over the env var. */\n sidecarUrl?: string;\n}\n\n/**\n * Picks the transport for a recording, or undefined when recording is not configured.\n *\n * A binding wins over a URL. The two are only ever both present by accident — a `.dev.vars`\n * left in an image, say — and the binding is the deliberate one: it can only exist because\n * someone added it to the wrangler configuration of this deployment.\n */\nexport const resolveSidecarTransport = (\n options: ResolveSidecarOptions | undefined,\n env: unknown,\n): SidecarTransport | undefined => {\n const envRecord =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)\n : {};\n\n const binding = options?.sidecarBinding ?? envRecord[SIDECAR_BINDING_ENV_KEY];\n if (isFetcher(binding)) {\n return { kind: \"binding\", fetcher: binding, instance: binding };\n }\n\n const rawUrl = options?.sidecarUrl ?? envRecord[SIDECAR_URL_ENV_KEY];\n if (typeof rawUrl === \"string\" && rawUrl.length > 0) {\n return { kind: \"url\", url: rawUrl.replace(/\\/+$/, \"\") };\n }\n return undefined;\n};\n\nconst isFetcher = (value: unknown): value is SidecarFetcher & object =>\n value !== null &&\n typeof value === \"object\" &&\n typeof (value as Partial<SidecarFetcher>).fetch === \"function\";\n"],"names":[],"mappings":";;;;;;;;AA0BA,MAAa,yBAAyB;;AAGtC,MAAa,0BAA0B;;AAGvC,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB,cAC9B,UAAU,SAAS,QAAQ,UAAU,MAAM;;;;;;;AAQ7C,MAAa,gBACX,WACA,SACA,MACA,SACsB;CACtB,MAAM,MAAM,GAAG,gBAAgB,UAAU,GAAG;AAC5C,QAAO,UAAU,SAAS,QACtB,QAAQ,KAAK,KAAK,GAClB,UAAU,QAAQ,MAAM,IAAI,QAAQ,KAAK,KAAK,CAAC;;;;;;;;;AAiBrD,MAAa,2BACX,SACA,QACiC;CACjC,MAAM,YACJ,QAAQ,QAAQ,OAAO,QAAQ,WAC1B,MACD,EAAE;CAER,MAAM,UAAU,SAAS,kBAAkB,UAAA;AAC3C,KAAI,UAAU,QAAQ,CACpB,QAAO;EAAE,MAAM;EAAW,SAAS;EAAS,UAAU;EAAS;CAGjE,MAAM,SAAS,SAAS,cAAc,UAAA;AACtC,KAAI,OAAO,WAAW,YAAY,OAAO,SAAS,EAChD,QAAO;EAAE,MAAM;EAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;EAAE;;AAK3D,MAAM,aAAa,UACjB,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAAkC,UAAU","debugId":"4c3fd120-a382-55e7-8105-c6ff77ac4d6e"}
1
+ {"version":3,"file":"sidecar-transport.js","names":[],"sources":["../src/sidecar-transport.ts"],"sourcesContent":["/**\n * Anything with a `fetch` — a Cloudflare service binding (`Fetcher`), or a Durable Object stub.\n * Structural so the shim needs no `@cloudflare/workers-types` dependency.\n */\nexport interface SidecarFetcher {\n fetch: (...args: unknown[]) => Promise<Response>;\n}\n\n/**\n * How capture events reach the sidecar.\n *\n * A local `wrangler dev` recording posts to a loopback URL. A deployed Worker cannot: there is\n * no sidecar process at the edge, and one HTTPS round trip per event to a distant host would be\n * both slow and a subrequest per captured call. So a deployed Worker instead talks to a\n * Meticulous sidecar Worker in the same account through a **service binding**, which stays\n * inside the colo and needs neither DNS nor TLS.\n */\nexport type SidecarTransport =\n | { kind: \"url\"; url: string }\n | { kind: \"binding\"; fetcher: SidecarFetcher; instance: object };\n\n/**\n * Origin the shim addresses a service-binding request to. A service binding delivers to the\n * bound Worker whatever the host is, so only the path is load-bearing — but the URL still has to\n * parse, and using a reserved-by-RFC TLD makes it obvious in a log that no DNS was involved.\n */\nexport const SIDECAR_BINDING_ORIGIN = \"https://meticulous-sidecar.invalid\";\n\n/** Env key holding a service binding to the Meticulous recorder sidecar Worker. */\nexport const SIDECAR_BINDING_ENV_KEY = \"METICULOUS_SIDECAR\";\n\n/** Env key holding the URL of a local recorder sidecar process. */\nexport const SIDECAR_URL_ENV_KEY = \"METICULOUS_SIDECAR_URL\";\n\n/** The origin the shim's own sidecar requests go to, for the self-capture guards. */\nexport const transportOrigin = (transport: SidecarTransport): string =>\n transport.kind === \"url\" ? transport.url : SIDECAR_BINDING_ORIGIN;\n\n/**\n * Sends one request to the sidecar, whichever transport is in use.\n *\n * `fetchFn` is the unpatched global fetch, passed in rather than imported so this module stays\n * free of the patch machinery; it is unused for a binding, whose `fetch` is its own method.\n */\nexport const sidecarFetch = (\n transport: SidecarTransport,\n fetchFn: typeof globalThis.fetch,\n path: string,\n init: RequestInit,\n): Promise<Response> => {\n const url = `${transportOrigin(transport)}${path}`;\n return transport.kind === \"url\"\n ? fetchFn(url, init)\n : transport.fetcher.fetch(new Request(url, init));\n};\n\nexport interface ResolveSidecarOptions {\n /** Explicit binding, taking precedence over anything found on `env`. */\n sidecarBinding?: SidecarFetcher;\n /** Explicit URL, taking precedence over the env var. */\n sidecarUrl?: string;\n}\n\n/**\n * Picks the transport for a recording, or undefined when recording is not configured.\n *\n * A binding wins over a URL. The two are only ever both present by accident — a `.dev.vars`\n * left in an image, say — and the binding is the deliberate one: it can only exist because\n * someone added it to the wrangler configuration of this deployment.\n */\nexport const resolveSidecarTransport = (\n options: ResolveSidecarOptions | undefined,\n env: unknown,\n): SidecarTransport | undefined => {\n const envRecord =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)\n : {};\n\n const binding = options?.sidecarBinding ?? envRecord[SIDECAR_BINDING_ENV_KEY];\n if (isFetcher(binding)) {\n return { kind: \"binding\", fetcher: binding, instance: binding };\n }\n\n const rawUrl = options?.sidecarUrl ?? envRecord[SIDECAR_URL_ENV_KEY];\n if (typeof rawUrl === \"string\" && rawUrl.length > 0) {\n return { kind: \"url\", url: rawUrl.replace(/\\/+$/, \"\") };\n }\n return undefined;\n};\n\nconst isFetcher = (value: unknown): value is SidecarFetcher & object =>\n value !== null &&\n typeof value === \"object\" &&\n typeof (value as Partial<SidecarFetcher>).fetch === \"function\";\n"],"mappings":";;;;;;AA0BA,MAAa,yBAAyB;;AAGtC,MAAa,0BAA0B;;AAGvC,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB,cAC9B,UAAU,SAAS,QAAQ,UAAU,MAAM;;;;;;;AAQ7C,MAAa,gBACX,WACA,SACA,MACA,SACsB;CACtB,MAAM,MAAM,GAAG,gBAAgB,UAAU,GAAG;AAC5C,QAAO,UAAU,SAAS,QACtB,QAAQ,KAAK,KAAK,GAClB,UAAU,QAAQ,MAAM,IAAI,QAAQ,KAAK,KAAK,CAAC;;;;;;;;;AAiBrD,MAAa,2BACX,SACA,QACiC;CACjC,MAAM,YACJ,QAAQ,QAAQ,OAAO,QAAQ,WAC1B,MACD,EAAE;CAER,MAAM,UAAU,SAAS,kBAAkB,UAAA;AAC3C,KAAI,UAAU,QAAQ,CACpB,QAAO;EAAE,MAAM;EAAW,SAAS;EAAS,UAAU;EAAS;CAGjE,MAAM,SAAS,SAAS,cAAc,UAAA;AACtC,KAAI,OAAO,WAAW,YAAY,OAAO,SAAS,EAChD,QAAO;EAAE,MAAM;EAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;EAAE;;AAK3D,MAAM,aAAa,UACjB,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAAkC,UAAU"}
@@ -9,8 +9,6 @@
9
9
  * below, so the Node recorder keeps one import site and there is one definition.
10
10
  */
11
11
  /** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */
12
-
13
- !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]="9c7bae2d-7e8a-57e2-9065-56765d8c547b")}catch(e){}}();
14
12
  const CLIENT_TECHNOLOGY_ATTR = "meticulous.client_technology";
15
13
  /** Ties a backend span to the frontend session whose request produced it. */
16
14
  const FRONTEND_SESSION_ID_ATTR = "meticulous.frontend_session_id";
@@ -84,5 +82,4 @@ const WORKERD_KV_OMITTED_ATTR = "meticulous.workerd.kv.omitted";
84
82
  //#endregion
85
83
  export { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR };
86
84
 
87
- //# sourceMappingURL=attributes.js.map
88
- //# debugId=9c7bae2d-7e8a-57e2-9065-56765d8c547b
85
+ //# sourceMappingURL=attributes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.js","sources":["../../src/spans/attributes.ts"],"sourcesContent":["/**\n * Span attribute names and technology tags shared by the two recording surfaces.\n *\n * These live here, in the package the in-worker shim ships from, for the same reason\n * `kv-capture.ts` does: a session recorded through workerd and one recorded through the Node\n * recorder have to be attribute-for-attribute identical, or a recording made on one cannot be\n * replayed by the other. `packages/backend-recorder-js/src/constants.ts` re-exports every name\n * below, so the Node recorder keeps one import site and there is one definition.\n */\n\n/** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */\nexport const CLIENT_TECHNOLOGY_ATTR = \"meticulous.client_technology\";\n\n/** Ties a backend span to the frontend session whose request produced it. */\nexport const FRONTEND_SESSION_ID_ATTR = \"meticulous.frontend_session_id\";\n\n/**\n * Where the session id on a span came from. Absent means the browser minted it and sent it on\n * the request, which is the normal case. `\"backend\"` means we minted it ourselves for a document\n * navigation the browser could not tag (see provisional-session-id.ts), and the page may or may\n * not have gone on to adopt it.\n *\n * The distinction is load-bearing for ingestion: an unadopted backend-minted id names a session\n * that will never exist, and must be treated as *unstamped* so those spans stay eligible for\n * time-window attachment — otherwise stamping them makes attribution worse than leaving them\n * bare. See `hasAnySessionId` in lambda-upload-sessions.\n */\nexport const SESSION_ID_ORIGIN_ATTR = \"meticulous.session_id_origin\";\nexport const SESSION_ID_ORIGIN_BACKEND = \"backend\";\n\n/**\n * An outgoing `fetch` from a Worker. Served back on replay by a `MockStore(\"workerd-fetch\")`\n * living in the replay sidecar, which the in-worker shim queries over plain HTTP (workerd cannot\n * run the Node mock instrumentations).\n *\n * This value travels in recorded session data, so it is a wire contract: the replay orchestrator\n * reads it back as `WORKERD_FETCH_CLIENT_TECHNOLOGY` in `@alwaysmeticulous/api` to decide whether\n * a recording needs a workerd replay sidecar. Keep the two in step.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_FETCH = \"workerd-fetch\";\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding or a Durable Object stub. Recorded\n * from two places: the in-worker shim, and `withMeticulousCloudflareEnv` in a Node process whose\n * bindings come from wrangler's `getPlatformProxy`. Kept distinct from `workerd-fetch` because a\n * binding call never leaves the isolate and its URL is caller-invented, so the two must never be\n * matched against each other.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_BINDING = \"workerd-binding\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, from the same two places. Its own technology\n * because a KV operation is not Request/Response-shaped at all — there is no method, URL or status\n * to match on, only a binding, an operation and a key.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_KV = \"workerd-kv\";\n\n/**\n * A postgres.js query, whether captured in Node (by the require hook or `withMeticulousPostgres`)\n * or in a deployed Worker. Deliberately distinct from `postgres` (node-postgres) so the two stores\n * never match each other's spans even though both talk to Postgres.\n */\nexport const CLIENT_TECHNOLOGY_POSTGRES_JS = \"postgres-js\";\n\n/**\n * The `env` key a binding call went through. Absent when the binding instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` returns. Shared by\n * `fetch`-shaped binding spans and KV spans: both identify the binding this way.\n */\nexport const WORKERD_BINDING_NAME_ATTR = \"meticulous.workerd.binding\";\n\n/** Which KV method was called: `get`, `getWithMetadata`, `put`, `delete` or `list`. */\nexport const WORKERD_KV_OPERATION_ATTR = \"meticulous.workerd.kv.operation\";\n/** The key operated on. Absent for `list` and for a bulk `get`, whose keys are in the args. */\nexport const WORKERD_KV_KEY_ATTR = \"meticulous.workerd.kv.key\";\n/** JSON of the call's arguments; a `put` value appears as null (it lives in the value attr). */\nexport const WORKERD_KV_ARGS_ATTR = \"meticulous.workerd.kv.args\";\nexport const WORKERD_KV_ARGS_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.args.truncated\";\n/** JSON of the value a `put` wrote, with secret-looking fields redacted. */\nexport const WORKERD_KV_VALUE_ATTR = \"meticulous.workerd.kv.value\";\nexport const WORKERD_KV_VALUE_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.value.truncated\";\n/** JSON of what the operation returned. Absent for `put`/`delete`, which return nothing. */\nexport const WORKERD_KV_RESULT_ATTR = \"meticulous.workerd.kv.result\";\nexport const WORKERD_KV_RESULT_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.result.truncated\";\n/**\n * Why a value is missing: `\"stream\"` (reading it would take the bytes from the app) or `\"binary\"`\n * (skipped deliberately — KV blobs are large and not UTF-8).\n */\nexport const WORKERD_KV_OMITTED_ATTR = \"meticulous.workerd.kv.omitted\";\n"],"names":[],"mappings":";;;;;;;;;;;;;AAWA,MAAa,yBAAyB;;AAGtC,MAAa,2BAA2B;;;;;;;;;;;;AAaxC,MAAa,yBAAyB;AACtC,MAAa,4BAA4B;;;;;;;;;;AAWzC,MAAa,kCAAkC;;;;;;;;AAS/C,MAAa,oCAAoC;;;;;;AAOjD,MAAa,+BAA+B;;;;;;AAO5C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,4BAA4B;;AAGzC,MAAa,4BAA4B;;AAEzC,MAAa,sBAAsB;;AAEnC,MAAa,uBAAuB;AACpC,MAAa,iCACX;;AAEF,MAAa,wBAAwB;AACrC,MAAa,kCACX;;AAEF,MAAa,yBAAyB;AACtC,MAAa,mCACX;;;;;AAKF,MAAa,0BAA0B","debugId":"9c7bae2d-7e8a-57e2-9065-56765d8c547b"}
1
+ {"version":3,"file":"attributes.js","names":[],"sources":["../../src/spans/attributes.ts"],"sourcesContent":["/**\n * Span attribute names and technology tags shared by the two recording surfaces.\n *\n * These live here, in the package the in-worker shim ships from, for the same reason\n * `kv-capture.ts` does: a session recorded through workerd and one recorded through the Node\n * recorder have to be attribute-for-attribute identical, or a recording made on one cannot be\n * replayed by the other. `packages/backend-recorder-js/src/constants.ts` re-exports every name\n * below, so the Node recorder keeps one import site and there is one definition.\n */\n\n/** Tags which client library produced a CLIENT span. Absent on native `node:http` spans. */\nexport const CLIENT_TECHNOLOGY_ATTR = \"meticulous.client_technology\";\n\n/** Ties a backend span to the frontend session whose request produced it. */\nexport const FRONTEND_SESSION_ID_ATTR = \"meticulous.frontend_session_id\";\n\n/**\n * Where the session id on a span came from. Absent means the browser minted it and sent it on\n * the request, which is the normal case. `\"backend\"` means we minted it ourselves for a document\n * navigation the browser could not tag (see provisional-session-id.ts), and the page may or may\n * not have gone on to adopt it.\n *\n * The distinction is load-bearing for ingestion: an unadopted backend-minted id names a session\n * that will never exist, and must be treated as *unstamped* so those spans stay eligible for\n * time-window attachment — otherwise stamping them makes attribution worse than leaving them\n * bare. See `hasAnySessionId` in lambda-upload-sessions.\n */\nexport const SESSION_ID_ORIGIN_ATTR = \"meticulous.session_id_origin\";\nexport const SESSION_ID_ORIGIN_BACKEND = \"backend\";\n\n/**\n * An outgoing `fetch` from a Worker. Served back on replay by a `MockStore(\"workerd-fetch\")`\n * living in the replay sidecar, which the in-worker shim queries over plain HTTP (workerd cannot\n * run the Node mock instrumentations).\n *\n * This value travels in recorded session data, so it is a wire contract: the replay orchestrator\n * reads it back as `WORKERD_FETCH_CLIENT_TECHNOLOGY` in `@alwaysmeticulous/api` to decide whether\n * a recording needs a workerd replay sidecar. Keep the two in step.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_FETCH = \"workerd-fetch\";\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding or a Durable Object stub. Recorded\n * from two places: the in-worker shim, and `withMeticulousCloudflareEnv` in a Node process whose\n * bindings come from wrangler's `getPlatformProxy`. Kept distinct from `workerd-fetch` because a\n * binding call never leaves the isolate and its URL is caller-invented, so the two must never be\n * matched against each other.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_BINDING = \"workerd-binding\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, from the same two places. Its own technology\n * because a KV operation is not Request/Response-shaped at all — there is no method, URL or status\n * to match on, only a binding, an operation and a key.\n */\nexport const CLIENT_TECHNOLOGY_WORKERD_KV = \"workerd-kv\";\n\n/**\n * A postgres.js query, whether captured in Node (by the require hook or `withMeticulousPostgres`)\n * or in a deployed Worker. Deliberately distinct from `postgres` (node-postgres) so the two stores\n * never match each other's spans even though both talk to Postgres.\n */\nexport const CLIENT_TECHNOLOGY_POSTGRES_JS = \"postgres-js\";\n\n/**\n * The `env` key a binding call went through. Absent when the binding instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` returns. Shared by\n * `fetch`-shaped binding spans and KV spans: both identify the binding this way.\n */\nexport const WORKERD_BINDING_NAME_ATTR = \"meticulous.workerd.binding\";\n\n/** Which KV method was called: `get`, `getWithMetadata`, `put`, `delete` or `list`. */\nexport const WORKERD_KV_OPERATION_ATTR = \"meticulous.workerd.kv.operation\";\n/** The key operated on. Absent for `list` and for a bulk `get`, whose keys are in the args. */\nexport const WORKERD_KV_KEY_ATTR = \"meticulous.workerd.kv.key\";\n/** JSON of the call's arguments; a `put` value appears as null (it lives in the value attr). */\nexport const WORKERD_KV_ARGS_ATTR = \"meticulous.workerd.kv.args\";\nexport const WORKERD_KV_ARGS_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.args.truncated\";\n/** JSON of the value a `put` wrote, with secret-looking fields redacted. */\nexport const WORKERD_KV_VALUE_ATTR = \"meticulous.workerd.kv.value\";\nexport const WORKERD_KV_VALUE_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.value.truncated\";\n/** JSON of what the operation returned. Absent for `put`/`delete`, which return nothing. */\nexport const WORKERD_KV_RESULT_ATTR = \"meticulous.workerd.kv.result\";\nexport const WORKERD_KV_RESULT_TRUNCATED_ATTR =\n \"meticulous.workerd.kv.result.truncated\";\n/**\n * Why a value is missing: `\"stream\"` (reading it would take the bytes from the app) or `\"binary\"`\n * (skipped deliberately — KV blobs are large and not UTF-8).\n */\nexport const WORKERD_KV_OMITTED_ATTR = \"meticulous.workerd.kv.omitted\";\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,yBAAyB;;AAGtC,MAAa,2BAA2B;;;;;;;;;;;;AAaxC,MAAa,yBAAyB;AACtC,MAAa,4BAA4B;;;;;;;;;;AAWzC,MAAa,kCAAkC;;;;;;;;AAS/C,MAAa,oCAAoC;;;;;;AAOjD,MAAa,+BAA+B;;;;;;AAO5C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,4BAA4B;;AAGzC,MAAa,4BAA4B;;AAEzC,MAAa,sBAAsB;;AAEnC,MAAa,uBAAuB;AACpC,MAAa,iCACX;;AAEF,MAAa,wBAAwB;AACrC,MAAa,kCACX;;AAEF,MAAa,yBAAyB;AACtC,MAAa,mCACX;;;;;AAKF,MAAa,0BAA0B"}
@@ -14,8 +14,6 @@
14
14
  * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in
15
15
  * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.
16
16
  */
17
-
18
- !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]="4b8da1fb-2c09-5543-acf1-a30e8b358116")}catch(e){}}();
19
17
  const SPAN_KIND_INTERNAL = 0;
20
18
  const SPAN_KIND_SERVER = 1;
21
19
  const SPAN_KIND_CLIENT = 2;
@@ -31,5 +29,4 @@ const msToHrTime = (ms) => {
31
29
  //#endregion
32
30
  export { SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, hrTimeToMs, msToHrTime };
33
31
 
34
- //# sourceMappingURL=readable-span.js.map
35
- //# debugId=4b8da1fb-2c09-5543-acf1-a30e8b358116
32
+ //# sourceMappingURL=readable-span.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"readable-span.js","sources":["../../src/spans/readable-span.ts"],"sourcesContent":["/**\n * The span shape both recording surfaces produce, and the serialization that turns it into the\n * JSON a recorded session is stored as.\n *\n * Lives here rather than in the Node recorder because a Worker-hosted sidecar has to write the\n * same wire format from inside workerd, where neither `@opentelemetry/api` nor the Node\n * recorder's bundle can be loaded. `packages/backend-recorder-js/src/exporters/span-serializer.ts`\n * re-exports all of this and layers its richer, per-instrumentation attribute types on top, so\n * the *logic* has exactly one implementation while the documentation types stay where they are\n * used.\n */\n\n/**\n * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in\n * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.\n */\nexport const SPAN_KIND_INTERNAL = 0;\nexport const SPAN_KIND_SERVER = 1;\nexport const SPAN_KIND_CLIENT = 2;\n\n/** OpenTelemetry's `SpanStatusCode`, inlined for the same reason. */\nexport const SPAN_STATUS_UNSET = 0;\nexport const SPAN_STATUS_OK = 1;\nexport const SPAN_STATUS_ERROR = 2;\n\nexport interface SpanStatus {\n code: number;\n message?: string;\n}\n\n/** `[seconds, nanoseconds]` — OpenTelemetry's `HrTime`. */\nexport type HrTime = [number, number];\n\nexport interface ReadableSpan {\n name: string;\n /**\n * Set by the hand-built spans in `span-builder.ts`. OTel-SDK spans carry the parent in\n * {@link ReadableSpan.parentSpanContext} instead.\n */\n parentSpanId?: string;\n /**\n * How an OTel-SDK span reports its parent since `@opentelemetry/sdk-trace-base` v2, which\n * dropped the flat `parentSpanId` field.\n */\n parentSpanContext?: { spanId: string };\n kind: number;\n status: unknown;\n startTime: HrTime;\n endTime: HrTime;\n duration: HrTime;\n attributes?: Record<string, unknown>;\n spanContext(): { traceId: string; spanId: string };\n}\n\n/**\n * A serialized span, with the attribute bag left open. The Node recorder narrows `attributes`\n * to its union of per-instrumentation contracts; nothing about the wire format depends on that\n * narrowing.\n */\nexport interface SerializedSpanBase {\n name: string;\n traceId: string;\n spanId: string;\n parentSpanId: string | undefined;\n kind: number;\n status: unknown;\n startTimeMs: number;\n endTimeMs: number;\n durationMs: number;\n clientTechnology?: string;\n frontendSessionId?: string;\n attributes: Record<string, unknown>;\n}\n\nexport const hrTimeToMs = (hrTime: HrTime): number =>\n hrTime[0] * 1000 + hrTime[1] / 1e6;\n\nexport const msToHrTime = (ms: number): HrTime => {\n const seconds = Math.floor(ms / 1000);\n return [seconds, Math.round((ms - seconds * 1000) * 1e6)];\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,qBAAqB;AAClC,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;AACjC,MAAa,iBAAiB;AAC9B,MAAa,oBAAoB;AAmDjC,MAAa,cAAc,WACzB,OAAO,KAAK,MAAO,OAAO,KAAK;AAEjC,MAAa,cAAc,OAAuB;CAChD,MAAM,UAAU,KAAK,MAAM,KAAK,IAAK;AACrC,QAAO,CAAC,SAAS,KAAK,OAAO,KAAK,UAAU,OAAQ,IAAI,CAAC","debugId":"4b8da1fb-2c09-5543-acf1-a30e8b358116"}
1
+ {"version":3,"file":"readable-span.js","names":[],"sources":["../../src/spans/readable-span.ts"],"sourcesContent":["/**\n * The span shape both recording surfaces produce, and the serialization that turns it into the\n * JSON a recorded session is stored as.\n *\n * Lives here rather than in the Node recorder because a Worker-hosted sidecar has to write the\n * same wire format from inside workerd, where neither `@opentelemetry/api` nor the Node\n * recorder's bundle can be loaded. `packages/backend-recorder-js/src/exporters/span-serializer.ts`\n * re-exports all of this and layers its richer, per-instrumentation attribute types on top, so\n * the *logic* has exactly one implementation while the documentation types stay where they are\n * used.\n */\n\n/**\n * OpenTelemetry's `SpanKind`, inlined. The shim has no runtime dependencies — pulling in\n * `@opentelemetry/api` for two numeric enums would put it in every customer's worker bundle.\n */\nexport const SPAN_KIND_INTERNAL = 0;\nexport const SPAN_KIND_SERVER = 1;\nexport const SPAN_KIND_CLIENT = 2;\n\n/** OpenTelemetry's `SpanStatusCode`, inlined for the same reason. */\nexport const SPAN_STATUS_UNSET = 0;\nexport const SPAN_STATUS_OK = 1;\nexport const SPAN_STATUS_ERROR = 2;\n\nexport interface SpanStatus {\n code: number;\n message?: string;\n}\n\n/** `[seconds, nanoseconds]` — OpenTelemetry's `HrTime`. */\nexport type HrTime = [number, number];\n\nexport interface ReadableSpan {\n name: string;\n /**\n * Set by the hand-built spans in `span-builder.ts`. OTel-SDK spans carry the parent in\n * {@link ReadableSpan.parentSpanContext} instead.\n */\n parentSpanId?: string;\n /**\n * How an OTel-SDK span reports its parent since `@opentelemetry/sdk-trace-base` v2, which\n * dropped the flat `parentSpanId` field.\n */\n parentSpanContext?: { spanId: string };\n kind: number;\n status: unknown;\n startTime: HrTime;\n endTime: HrTime;\n duration: HrTime;\n attributes?: Record<string, unknown>;\n spanContext(): { traceId: string; spanId: string };\n}\n\n/**\n * A serialized span, with the attribute bag left open. The Node recorder narrows `attributes`\n * to its union of per-instrumentation contracts; nothing about the wire format depends on that\n * narrowing.\n */\nexport interface SerializedSpanBase {\n name: string;\n traceId: string;\n spanId: string;\n parentSpanId: string | undefined;\n kind: number;\n status: unknown;\n startTimeMs: number;\n endTimeMs: number;\n durationMs: number;\n clientTechnology?: string;\n frontendSessionId?: string;\n attributes: Record<string, unknown>;\n}\n\nexport const hrTimeToMs = (hrTime: HrTime): number =>\n hrTime[0] * 1000 + hrTime[1] / 1e6;\n\nexport const msToHrTime = (ms: number): HrTime => {\n const seconds = Math.floor(ms / 1000);\n return [seconds, Math.round((ms - seconds * 1000) * 1e6)];\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,MAAa,qBAAqB;AAClC,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;AACjC,MAAa,iBAAiB;AAC9B,MAAa,oBAAoB;AAmDjC,MAAa,cAAc,WACzB,OAAO,KAAK,MAAO,OAAO,KAAK;AAEjC,MAAa,cAAc,OAAuB;CAChD,MAAM,UAAU,KAAK,MAAM,KAAK,IAAK;AACrC,QAAO,CAAC,SAAS,KAAK,OAAO,KAAK,UAAU,OAAQ,IAAI,CAAC"}
@@ -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]="e12748a8-3496-5830-8c8f-229beae7068b")}catch(e){}}();
3
1
  import { CLIENT_TECHNOLOGY_ATTR, FRONTEND_SESSION_ID_ATTR } from "./attributes.js";
4
2
  import { hrTimeToMs } from "./readable-span.js";
5
3
  //#region src/spans/serialize-span.ts
@@ -34,5 +32,4 @@ const serializeSpan = (span) => {
34
32
  //#endregion
35
33
  export { serializeSpan };
36
34
 
37
- //# sourceMappingURL=serialize-span.js.map
38
- //# debugId=e12748a8-3496-5830-8c8f-229beae7068b
35
+ //# sourceMappingURL=serialize-span.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serialize-span.js","sources":["../../src/spans/serialize-span.ts"],"sourcesContent":["import { CLIENT_TECHNOLOGY_ATTR, FRONTEND_SESSION_ID_ATTR } from \"./attributes\";\nimport {\n hrTimeToMs,\n type ReadableSpan,\n type SerializedSpanBase,\n} from \"./readable-span\";\n\n/**\n * Turns a span into the JSON shape a recorded backend session stores.\n *\n * Shared by the Node exporter and the Worker-hosted sidecar so the two cannot drift: ingestion\n * and every replay mock store read these field names, and a sidecar writing a near-miss of\n * this shape would produce recordings that load but never match.\n */\nexport const serializeSpan = (span: ReadableSpan): SerializedSpanBase => {\n const ctx = span.spanContext();\n // Copy so we never mutate the live OTel span's attributes.\n const attributes = { ...(span.attributes ?? {}) };\n\n const clientTechnology = attributes[CLIENT_TECHNOLOGY_ATTR] as\n | string\n | undefined;\n const frontendSessionId = attributes[FRONTEND_SESSION_ID_ATTR] as\n | string\n | undefined;\n\n // client_technology moves to the top level; frontend_session_id is copied (left in place for\n // backward compatibility — see download-sessions.ts in the webapp repo).\n delete attributes[CLIENT_TECHNOLOGY_ATTR];\n\n return {\n name: span.name,\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n parentSpanId: span.parentSpanId ?? span.parentSpanContext?.spanId,\n kind: span.kind,\n status: span.status,\n startTimeMs: hrTimeToMs(span.startTime),\n endTimeMs: hrTimeToMs(span.endTime),\n durationMs: hrTimeToMs(span.duration),\n // Spread conditionally so we never emit `key: undefined` (exactOptionalPropertyTypes).\n ...(clientTechnology ? { clientTechnology } : {}),\n ...(frontendSessionId ? { frontendSessionId } : {}),\n attributes,\n };\n};\n"],"names":[],"mappings":";;;;;;;;;;;;AAcA,MAAa,iBAAiB,SAA2C;CACvE,MAAM,MAAM,KAAK,aAAa;CAE9B,MAAM,aAAa,EAAE,GAAI,KAAK,cAAc,EAAE,EAAG;CAEjD,MAAM,mBAAmB,WAAW;CAGpC,MAAM,oBAAoB,WAAW;AAMrC,QAAO,WAAW;AAElB,QAAO;EACL,MAAM,KAAK;EACX,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,cAAc,KAAK,gBAAgB,KAAK,mBAAmB;EAC3D,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,aAAa,WAAW,KAAK,UAAU;EACvC,WAAW,WAAW,KAAK,QAAQ;EACnC,YAAY,WAAW,KAAK,SAAS;EAErC,GAAI,mBAAmB,EAAE,kBAAkB,GAAG,EAAE;EAChD,GAAI,oBAAoB,EAAE,mBAAmB,GAAG,EAAE;EAClD;EACD","debugId":"e12748a8-3496-5830-8c8f-229beae7068b"}
1
+ {"version":3,"file":"serialize-span.js","names":[],"sources":["../../src/spans/serialize-span.ts"],"sourcesContent":["import { CLIENT_TECHNOLOGY_ATTR, FRONTEND_SESSION_ID_ATTR } from \"./attributes\";\nimport {\n hrTimeToMs,\n type ReadableSpan,\n type SerializedSpanBase,\n} from \"./readable-span\";\n\n/**\n * Turns a span into the JSON shape a recorded backend session stores.\n *\n * Shared by the Node exporter and the Worker-hosted sidecar so the two cannot drift: ingestion\n * and every replay mock store read these field names, and a sidecar writing a near-miss of\n * this shape would produce recordings that load but never match.\n */\nexport const serializeSpan = (span: ReadableSpan): SerializedSpanBase => {\n const ctx = span.spanContext();\n // Copy so we never mutate the live OTel span's attributes.\n const attributes = { ...(span.attributes ?? {}) };\n\n const clientTechnology = attributes[CLIENT_TECHNOLOGY_ATTR] as\n | string\n | undefined;\n const frontendSessionId = attributes[FRONTEND_SESSION_ID_ATTR] as\n | string\n | undefined;\n\n // client_technology moves to the top level; frontend_session_id is copied (left in place for\n // backward compatibility — see download-sessions.ts in the webapp repo).\n delete attributes[CLIENT_TECHNOLOGY_ATTR];\n\n return {\n name: span.name,\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n parentSpanId: span.parentSpanId ?? span.parentSpanContext?.spanId,\n kind: span.kind,\n status: span.status,\n startTimeMs: hrTimeToMs(span.startTime),\n endTimeMs: hrTimeToMs(span.endTime),\n durationMs: hrTimeToMs(span.duration),\n // Spread conditionally so we never emit `key: undefined` (exactOptionalPropertyTypes).\n ...(clientTechnology ? { clientTechnology } : {}),\n ...(frontendSessionId ? { frontendSessionId } : {}),\n attributes,\n };\n};\n"],"mappings":";;;;;;;;;;AAcA,MAAa,iBAAiB,SAA2C;CACvE,MAAM,MAAM,KAAK,aAAa;CAE9B,MAAM,aAAa,EAAE,GAAI,KAAK,cAAc,EAAE,EAAG;CAEjD,MAAM,mBAAmB,WAAW;CAGpC,MAAM,oBAAoB,WAAW;AAMrC,QAAO,WAAW;AAElB,QAAO;EACL,MAAM,KAAK;EACX,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,cAAc,KAAK,gBAAgB,KAAK,mBAAmB;EAC3D,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,aAAa,WAAW,KAAK,UAAU;EACvC,WAAW,WAAW,KAAK,QAAQ;EACnC,YAAY,WAAW,KAAK,SAAS;EAErC,GAAI,mBAAmB,EAAE,kBAAkB,GAAG,EAAE;EAChD,GAAI,oBAAoB,EAAE,mBAAmB,GAAG,EAAE;EAClD;EACD"}
@@ -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]="680329f2-970d-5fc7-89d6-78ab2702fa27")}catch(e){}}();
3
1
  import { CAPTURED_HEADERS } from "../protocol.js";
4
2
  import { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, postgresJsCommandOf } from "../postgres/capture.js";
5
3
  import { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR } from "./attributes.js";
@@ -246,5 +244,4 @@ const buildStatus = (event, kind) => {
246
244
  //#endregion
247
245
  export { SpanBuilder, randomHex };
248
246
 
249
- //# sourceMappingURL=span-builder.js.map
250
- //# debugId=680329f2-970d-5fc7-89d6-78ab2702fa27
247
+ //# sourceMappingURL=span-builder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"span-builder.js","sources":["../../src/spans/span-builder.ts"],"sourcesContent":["import {\n type BindingRequestEvent,\n CAPTURED_HEADERS,\n type CaptureEvent,\n type InboundRequestEvent,\n type KvOperationEvent,\n type OutboundRequestEvent,\n type PostgresQueryEvent,\n} from \"../protocol\";\nimport {\n MAX_POSTGRES_JS_RESULT_SIZE,\n POSTGRES_JS_ERROR_ATTR,\n POSTGRES_JS_QUERY_PARAMS_ATTR,\n POSTGRES_JS_QUERY_TEXT_ATTR,\n POSTGRES_JS_RESULT_ATTR,\n POSTGRES_JS_RESULT_TRUNCATED_ATTR,\n POSTGRES_JS_ROW_MODE_ATTR,\n postgresJsCommandOf,\n} from \"../postgres/capture\";\nimport {\n CLIENT_TECHNOLOGY_ATTR,\n CLIENT_TECHNOLOGY_POSTGRES_JS,\n CLIENT_TECHNOLOGY_WORKERD_BINDING,\n CLIENT_TECHNOLOGY_WORKERD_FETCH,\n CLIENT_TECHNOLOGY_WORKERD_KV,\n FRONTEND_SESSION_ID_ATTR,\n SESSION_ID_ORIGIN_ATTR,\n WORKERD_BINDING_NAME_ATTR,\n WORKERD_KV_ARGS_ATTR,\n WORKERD_KV_ARGS_TRUNCATED_ATTR,\n WORKERD_KV_KEY_ATTR,\n WORKERD_KV_OMITTED_ATTR,\n WORKERD_KV_OPERATION_ATTR,\n WORKERD_KV_RESULT_ATTR,\n WORKERD_KV_RESULT_TRUNCATED_ATTR,\n WORKERD_KV_VALUE_ATTR,\n WORKERD_KV_VALUE_TRUNCATED_ATTR,\n} from \"./attributes\";\nimport {\n msToHrTime,\n type ReadableSpan,\n SPAN_KIND_CLIENT,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./readable-span\";\nimport { redactUrlCredentials } from \"./url-redaction\";\n\n/**\n * Builds spans from shim capture events.\n *\n * Each inbound request becomes a SERVER span and the root of its own trace; every other event of\n * that request becomes a CLIENT span parented under it. The shim supplies the trace and\n * server-span ids (see `CorrelatedEvent`), so building a span needs no memory of earlier events\n * — which is what lets a Worker-hosted sidecar be evicted between two batches of one request\n * without splitting its trace.\n *\n * The `requestId → ids` fallback below exists only for a shim that predates those fields. It is\n * the reason this is a class rather than a function.\n *\n * Shared with the Node recorder's sidecar (`packages/backend-recorder-js/src/sidecar/`), which\n * re-exports it: both sidecars must produce identical spans or a recording made through one\n * cannot be replayed by the machinery built for the other.\n */\nexport class SpanBuilder {\n /**\n * Legacy fallback ids, keyed by `requestId`. Outbound events usually arrive BEFORE their\n * inbound event (the inbound is only reported once the handler completes), so an entry is\n * created lazily on first sight from either event kind.\n */\n private readonly traceContexts = new Map<string, TraceContextEntry>();\n\n build(event: CaptureEvent): ReadableSpan | null {\n switch (event.kind) {\n case \"inbound\":\n return this.buildInboundSpan(event);\n case \"outbound\":\n return this.buildOutboundSpan(event);\n case \"binding\":\n return this.buildBindingSpan(event);\n case \"kv\":\n return this.buildKvSpan(event);\n case \"postgres\":\n return this.buildPostgresSpan(event);\n default:\n // Wire input, possibly from a newer shim — skip rather than crash.\n return null;\n }\n }\n\n private buildInboundSpan(event: InboundRequestEvent): ReadableSpan {\n const ids = this.resolveIds(event);\n const url = parseUrl(event.url);\n const attributes = buildHttpAttributes(event, url);\n\n // Only the SERVER span carries it, matching MetHttpInstrumentation on the Node path: the\n // marker describes where this request's session id came from, and the CLIENT spans under\n // it share the same trace.\n if (event.sessionIdOrigin !== undefined) {\n attributes[SESSION_ID_ORIGIN_ATTR] = event.sessionIdOrigin;\n }\n\n return {\n name: `${event.method} ${url.path}`,\n kind: SPAN_KIND_SERVER,\n status: buildStatus(event, SPAN_KIND_SERVER),\n startTime: msToHrTime(event.startTimeMs),\n endTime: msToHrTime(event.endTimeMs),\n duration: msToHrTime(Math.max(0, event.endTimeMs - event.startTimeMs)),\n attributes,\n spanContext: () => ({\n traceId: ids.traceId,\n spanId: ids.serverSpanId,\n }),\n };\n }\n\n private buildOutboundSpan(event: OutboundRequestEvent): ReadableSpan {\n return this.buildHttpClientSpan(\n event,\n CLIENT_TECHNOLOGY_WORKERD_FETCH,\n event.method,\n );\n }\n\n /**\n * A binding call. Named after the binding rather than the bare method, since the `env` key is\n * what identifies the call — the URL is whatever the app invented.\n */\n private buildBindingSpan(event: BindingRequestEvent): ReadableSpan {\n return this.buildHttpClientSpan(\n event,\n CLIENT_TECHNOLOGY_WORKERD_BINDING,\n `${event.method} ${event.bindingName ?? \"$unknown\"}`,\n event.bindingName !== undefined\n ? { [WORKERD_BINDING_NAME_ATTR]: event.bindingName }\n : undefined,\n );\n }\n\n /**\n * A KV operation. Named `kv.<binding>.<operation>` after the Prisma convention\n * (`prisma.<model>.<operation>`) rather than the `METHOD TARGET` shape the HTTP-like spans use\n * — nothing about a KV call is an HTTP method, and the dotted name keeps a `get` on a\n * namespace distinguishable from a `GET` through a service binding at a glance.\n */\n private buildKvSpan(event: KvOperationEvent): ReadableSpan {\n const attributes: Record<string, unknown> = {\n [CLIENT_TECHNOLOGY_ATTR]: CLIENT_TECHNOLOGY_WORKERD_KV,\n [WORKERD_KV_OPERATION_ATTR]: event.operation,\n ...(event.bindingName !== undefined\n ? { [WORKERD_BINDING_NAME_ATTR]: event.bindingName }\n : {}),\n ...(event.key !== undefined ? { [WORKERD_KV_KEY_ATTR]: event.key } : {}),\n ...(event.omitted !== undefined\n ? { [WORKERD_KV_OMITTED_ATTR]: event.omitted }\n : {}),\n ...(event.error !== undefined ? { \"error.type\": event.error } : {}),\n ...frontendSessionAttribute(event),\n };\n if (event.args !== undefined) {\n attributes[WORKERD_KV_ARGS_ATTR] = event.args.body;\n attributes[WORKERD_KV_ARGS_TRUNCATED_ATTR] = event.args.truncated;\n }\n if (event.value !== undefined) {\n attributes[WORKERD_KV_VALUE_ATTR] = event.value.body;\n attributes[WORKERD_KV_VALUE_TRUNCATED_ATTR] = event.value.truncated;\n }\n if (event.result !== undefined) {\n attributes[WORKERD_KV_RESULT_ATTR] = event.result.body;\n attributes[WORKERD_KV_RESULT_TRUNCATED_ATTR] = event.result.truncated;\n }\n\n return this.buildClientSpan(\n event,\n `kv.${event.bindingName ?? \"$unknown\"}.${event.operation}`,\n attributes,\n event.error !== undefined\n ? { code: SPAN_STATUS_ERROR, message: event.error }\n : { code: SPAN_STATUS_UNSET },\n );\n }\n\n /**\n * A postgres.js query. Name and attributes match what the Node instrumentation emits\n * (`postgresjs.<command>`), because the same store loads both.\n */\n private buildPostgresSpan(event: PostgresQueryEvent): ReadableSpan {\n const attributes: Record<string, unknown> = {\n [CLIENT_TECHNOLOGY_ATTR]: CLIENT_TECHNOLOGY_POSTGRES_JS,\n [POSTGRES_JS_QUERY_TEXT_ATTR]: event.queryText,\n [POSTGRES_JS_QUERY_PARAMS_ATTR]: event.params,\n [POSTGRES_JS_ROW_MODE_ATTR]: event.rowMode,\n ...frontendSessionAttribute(event),\n };\n if (event.result !== undefined) {\n // Re-cap here as well as at capture time: the attribute contract is what the store\n // reads, and a shim that capped differently must not widen it.\n const truncated =\n event.result.truncated ||\n event.result.body.length > MAX_POSTGRES_JS_RESULT_SIZE;\n attributes[POSTGRES_JS_RESULT_ATTR] = truncated\n ? event.result.body.slice(0, MAX_POSTGRES_JS_RESULT_SIZE)\n : event.result.body;\n attributes[POSTGRES_JS_RESULT_TRUNCATED_ATTR] = truncated;\n }\n if (event.errorJson !== undefined) {\n attributes[POSTGRES_JS_ERROR_ATTR] = event.errorJson;\n }\n\n return this.buildClientSpan(\n event,\n `postgresjs.${postgresJsCommandOf(event.queryText)}`,\n attributes,\n event.errorJson !== undefined\n ? { code: SPAN_STATUS_ERROR }\n : { code: SPAN_STATUS_UNSET },\n );\n }\n\n private buildHttpClientSpan(\n event: OutboundRequestEvent | BindingRequestEvent,\n technology: string,\n name: string,\n extraAttributes?: Record<string, unknown>,\n ): ReadableSpan {\n const url = parseUrl(event.url);\n const attributes = buildHttpAttributes(event, url);\n attributes[CLIENT_TECHNOLOGY_ATTR] = technology;\n Object.assign(attributes, extraAttributes);\n if (event.requestBody !== undefined) {\n attributes[\"http.request.body\"] = event.requestBody.body;\n attributes[\"http.request.body.truncated\"] = event.requestBody.truncated;\n }\n if (event.responseBody !== undefined) {\n attributes[\"http.response.body\"] = event.responseBody.body;\n attributes[\"http.response.body.truncated\"] = event.responseBody.truncated;\n }\n\n return this.buildClientSpan(\n event,\n name,\n attributes,\n buildStatus(event, SPAN_KIND_CLIENT),\n );\n }\n\n private buildClientSpan(\n event: CaptureEvent,\n name: string,\n attributes: Record<string, unknown>,\n status: SpanStatus,\n ): ReadableSpan {\n const ids = this.resolveIds(event);\n const spanId = randomHex(8);\n return {\n name,\n parentSpanId: ids.serverSpanId,\n kind: SPAN_KIND_CLIENT,\n status,\n startTime: msToHrTime(event.startTimeMs),\n endTime: msToHrTime(event.endTimeMs),\n duration: msToHrTime(Math.max(0, event.endTimeMs - event.startTimeMs)),\n attributes,\n spanContext: () => ({ traceId: ids.traceId, spanId }),\n };\n }\n\n /**\n * The trace and server-span ids for an event: the shim's, when it sent them, and otherwise a\n * pair invented per `requestId` and remembered — the pre-`traceId` behaviour.\n */\n private resolveIds(event: CaptureEvent): TraceContextEntry {\n if (event.traceId !== undefined && event.serverSpanId !== undefined) {\n return {\n traceId: event.traceId,\n serverSpanId: event.serverSpanId,\n lastSeenMs: event.endTimeMs,\n };\n }\n const existing = this.traceContexts.get(event.requestId);\n if (existing) {\n existing.lastSeenMs = event.endTimeMs;\n return existing;\n }\n this.pruneTraceContexts(event.endTimeMs);\n const entry: TraceContextEntry = {\n traceId: randomHex(16),\n serverSpanId: randomHex(8),\n lastSeenMs: event.endTimeMs,\n };\n this.traceContexts.set(event.requestId, entry);\n return entry;\n }\n\n private pruneTraceContexts(nowMs: number): void {\n if (this.traceContexts.size < TRACE_CONTEXT_PRUNE_THRESHOLD) {\n return;\n }\n for (const [requestId, entry] of this.traceContexts) {\n if (nowMs - entry.lastSeenMs > TRACE_CONTEXT_TTL_MS) {\n this.traceContexts.delete(requestId);\n }\n }\n }\n}\n\ninterface TraceContextEntry {\n traceId: string;\n serverSpanId: string;\n lastSeenMs: number;\n}\n\nconst TRACE_CONTEXT_TTL_MS = 10 * 60 * 1000;\nconst TRACE_CONTEXT_PRUNE_THRESHOLD = 1_000;\n\nconst HEX = \"0123456789abcdef\";\n\n/** WebCrypto rather than `node:crypto`, so this runs in a Worker as well as in Node. */\nexport const randomHex = (bytes: number): string => {\n const buffer = crypto.getRandomValues(new Uint8Array(bytes));\n let hex = \"\";\n for (const byte of buffer) {\n hex += HEX[byte >> 4] + HEX[byte & 0x0f];\n }\n return hex;\n};\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\ninterface ParsedUrl {\n full: string;\n path: string;\n query: string | undefined;\n scheme: string | undefined;\n host: string | undefined;\n port: number | undefined;\n}\n\nconst parseUrl = (raw: string): ParsedUrl => {\n try {\n const url = redactUrlCredentials(new URL(raw));\n const defaultPort = url.protocol === \"https:\" ? 443 : 80;\n return {\n full: url.toString(),\n path: url.pathname,\n query: url.search ? url.search.slice(1) : undefined,\n scheme: url.protocol.replace(/:$/, \"\"),\n host: url.hostname,\n port: url.port ? parseInt(url.port, 10) : defaultPort,\n };\n } catch {\n return {\n full: raw,\n path: raw.split(\"?\")[0] || \"/\",\n query: undefined,\n scheme: undefined,\n host: undefined,\n port: undefined,\n };\n }\n};\n\nconst frontendSessionAttribute = (\n event: CaptureEvent,\n): Record<string, string> =>\n event.frontendSessionId !== undefined\n ? { [FRONTEND_SESSION_ID_ATTR]: event.frontendSessionId }\n : {};\n\nconst buildHttpAttributes = (\n event: InboundRequestEvent | OutboundRequestEvent | BindingRequestEvent,\n url: ParsedUrl,\n): Record<string, unknown> => {\n const attributes: Record<string, unknown> = {\n \"http.request.method\": event.method,\n \"url.full\": url.full,\n \"url.path\": url.path,\n ...(url.query !== undefined ? { \"url.query\": url.query } : {}),\n ...(url.scheme !== undefined ? { \"url.scheme\": url.scheme } : {}),\n ...(url.host !== undefined ? { \"server.address\": url.host } : {}),\n ...(url.port !== undefined ? { \"server.port\": url.port } : {}),\n ...(event.statusCode !== undefined\n ? { \"http.response.status_code\": event.statusCode }\n : {}),\n ...(event.error !== undefined ? { \"error.type\": event.error } : {}),\n ...frontendSessionAttribute(event),\n };\n // The shim already filters to CAPTURED_HEADERS; re-filter here so a stale or third-party\n // shim can't persist sensitive headers via the sidecar.\n for (const [name, values] of Object.entries(event.requestHeaders)) {\n if (capturedHeaders.has(name.toLowerCase())) {\n attributes[`http.request.header.${name}`] = values;\n }\n }\n for (const [name, values] of Object.entries(event.responseHeaders ?? {})) {\n if (capturedHeaders.has(name.toLowerCase())) {\n attributes[`http.response.header.${name}`] = values;\n }\n }\n return attributes;\n};\n\nconst buildStatus = (\n event: InboundRequestEvent | OutboundRequestEvent | BindingRequestEvent,\n kind: number,\n): SpanStatus => {\n if (event.error !== undefined) {\n return { code: SPAN_STATUS_ERROR, message: event.error };\n }\n // Standard OTel HTTP semantics: client spans error on 4xx/5xx, server spans only on 5xx.\n const errorThreshold = kind === SPAN_KIND_CLIENT ? 400 : 500;\n if (event.statusCode !== undefined && event.statusCode >= errorThreshold) {\n return { code: SPAN_STATUS_ERROR };\n }\n return { code: SPAN_STATUS_UNSET };\n};\n"],"names":[],"mappings":"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","debugId":"680329f2-970d-5fc7-89d6-78ab2702fa27"}
1
+ {"version":3,"file":"span-builder.js","names":[],"sources":["../../src/spans/span-builder.ts"],"sourcesContent":["import {\n type BindingRequestEvent,\n CAPTURED_HEADERS,\n type CaptureEvent,\n type InboundRequestEvent,\n type KvOperationEvent,\n type OutboundRequestEvent,\n type PostgresQueryEvent,\n} from \"../protocol\";\nimport {\n MAX_POSTGRES_JS_RESULT_SIZE,\n POSTGRES_JS_ERROR_ATTR,\n POSTGRES_JS_QUERY_PARAMS_ATTR,\n POSTGRES_JS_QUERY_TEXT_ATTR,\n POSTGRES_JS_RESULT_ATTR,\n POSTGRES_JS_RESULT_TRUNCATED_ATTR,\n POSTGRES_JS_ROW_MODE_ATTR,\n postgresJsCommandOf,\n} from \"../postgres/capture\";\nimport {\n CLIENT_TECHNOLOGY_ATTR,\n CLIENT_TECHNOLOGY_POSTGRES_JS,\n CLIENT_TECHNOLOGY_WORKERD_BINDING,\n CLIENT_TECHNOLOGY_WORKERD_FETCH,\n CLIENT_TECHNOLOGY_WORKERD_KV,\n FRONTEND_SESSION_ID_ATTR,\n SESSION_ID_ORIGIN_ATTR,\n WORKERD_BINDING_NAME_ATTR,\n WORKERD_KV_ARGS_ATTR,\n WORKERD_KV_ARGS_TRUNCATED_ATTR,\n WORKERD_KV_KEY_ATTR,\n WORKERD_KV_OMITTED_ATTR,\n WORKERD_KV_OPERATION_ATTR,\n WORKERD_KV_RESULT_ATTR,\n WORKERD_KV_RESULT_TRUNCATED_ATTR,\n WORKERD_KV_VALUE_ATTR,\n WORKERD_KV_VALUE_TRUNCATED_ATTR,\n} from \"./attributes\";\nimport {\n msToHrTime,\n type ReadableSpan,\n SPAN_KIND_CLIENT,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./readable-span\";\nimport { redactUrlCredentials } from \"./url-redaction\";\n\n/**\n * Builds spans from shim capture events.\n *\n * Each inbound request becomes a SERVER span and the root of its own trace; every other event of\n * that request becomes a CLIENT span parented under it. The shim supplies the trace and\n * server-span ids (see `CorrelatedEvent`), so building a span needs no memory of earlier events\n * — which is what lets a Worker-hosted sidecar be evicted between two batches of one request\n * without splitting its trace.\n *\n * The `requestId → ids` fallback below exists only for a shim that predates those fields. It is\n * the reason this is a class rather than a function.\n *\n * Shared with the Node recorder's sidecar (`packages/backend-recorder-js/src/sidecar/`), which\n * re-exports it: both sidecars must produce identical spans or a recording made through one\n * cannot be replayed by the machinery built for the other.\n */\nexport class SpanBuilder {\n /**\n * Legacy fallback ids, keyed by `requestId`. Outbound events usually arrive BEFORE their\n * inbound event (the inbound is only reported once the handler completes), so an entry is\n * created lazily on first sight from either event kind.\n */\n private readonly traceContexts = new Map<string, TraceContextEntry>();\n\n build(event: CaptureEvent): ReadableSpan | null {\n switch (event.kind) {\n case \"inbound\":\n return this.buildInboundSpan(event);\n case \"outbound\":\n return this.buildOutboundSpan(event);\n case \"binding\":\n return this.buildBindingSpan(event);\n case \"kv\":\n return this.buildKvSpan(event);\n case \"postgres\":\n return this.buildPostgresSpan(event);\n default:\n // Wire input, possibly from a newer shim — skip rather than crash.\n return null;\n }\n }\n\n private buildInboundSpan(event: InboundRequestEvent): ReadableSpan {\n const ids = this.resolveIds(event);\n const url = parseUrl(event.url);\n const attributes = buildHttpAttributes(event, url);\n\n // Only the SERVER span carries it, matching MetHttpInstrumentation on the Node path: the\n // marker describes where this request's session id came from, and the CLIENT spans under\n // it share the same trace.\n if (event.sessionIdOrigin !== undefined) {\n attributes[SESSION_ID_ORIGIN_ATTR] = event.sessionIdOrigin;\n }\n\n return {\n name: `${event.method} ${url.path}`,\n kind: SPAN_KIND_SERVER,\n status: buildStatus(event, SPAN_KIND_SERVER),\n startTime: msToHrTime(event.startTimeMs),\n endTime: msToHrTime(event.endTimeMs),\n duration: msToHrTime(Math.max(0, event.endTimeMs - event.startTimeMs)),\n attributes,\n spanContext: () => ({\n traceId: ids.traceId,\n spanId: ids.serverSpanId,\n }),\n };\n }\n\n private buildOutboundSpan(event: OutboundRequestEvent): ReadableSpan {\n return this.buildHttpClientSpan(\n event,\n CLIENT_TECHNOLOGY_WORKERD_FETCH,\n event.method,\n );\n }\n\n /**\n * A binding call. Named after the binding rather than the bare method, since the `env` key is\n * what identifies the call — the URL is whatever the app invented.\n */\n private buildBindingSpan(event: BindingRequestEvent): ReadableSpan {\n return this.buildHttpClientSpan(\n event,\n CLIENT_TECHNOLOGY_WORKERD_BINDING,\n `${event.method} ${event.bindingName ?? \"$unknown\"}`,\n event.bindingName !== undefined\n ? { [WORKERD_BINDING_NAME_ATTR]: event.bindingName }\n : undefined,\n );\n }\n\n /**\n * A KV operation. Named `kv.<binding>.<operation>` after the Prisma convention\n * (`prisma.<model>.<operation>`) rather than the `METHOD TARGET` shape the HTTP-like spans use\n * — nothing about a KV call is an HTTP method, and the dotted name keeps a `get` on a\n * namespace distinguishable from a `GET` through a service binding at a glance.\n */\n private buildKvSpan(event: KvOperationEvent): ReadableSpan {\n const attributes: Record<string, unknown> = {\n [CLIENT_TECHNOLOGY_ATTR]: CLIENT_TECHNOLOGY_WORKERD_KV,\n [WORKERD_KV_OPERATION_ATTR]: event.operation,\n ...(event.bindingName !== undefined\n ? { [WORKERD_BINDING_NAME_ATTR]: event.bindingName }\n : {}),\n ...(event.key !== undefined ? { [WORKERD_KV_KEY_ATTR]: event.key } : {}),\n ...(event.omitted !== undefined\n ? { [WORKERD_KV_OMITTED_ATTR]: event.omitted }\n : {}),\n ...(event.error !== undefined ? { \"error.type\": event.error } : {}),\n ...frontendSessionAttribute(event),\n };\n if (event.args !== undefined) {\n attributes[WORKERD_KV_ARGS_ATTR] = event.args.body;\n attributes[WORKERD_KV_ARGS_TRUNCATED_ATTR] = event.args.truncated;\n }\n if (event.value !== undefined) {\n attributes[WORKERD_KV_VALUE_ATTR] = event.value.body;\n attributes[WORKERD_KV_VALUE_TRUNCATED_ATTR] = event.value.truncated;\n }\n if (event.result !== undefined) {\n attributes[WORKERD_KV_RESULT_ATTR] = event.result.body;\n attributes[WORKERD_KV_RESULT_TRUNCATED_ATTR] = event.result.truncated;\n }\n\n return this.buildClientSpan(\n event,\n `kv.${event.bindingName ?? \"$unknown\"}.${event.operation}`,\n attributes,\n event.error !== undefined\n ? { code: SPAN_STATUS_ERROR, message: event.error }\n : { code: SPAN_STATUS_UNSET },\n );\n }\n\n /**\n * A postgres.js query. Name and attributes match what the Node instrumentation emits\n * (`postgresjs.<command>`), because the same store loads both.\n */\n private buildPostgresSpan(event: PostgresQueryEvent): ReadableSpan {\n const attributes: Record<string, unknown> = {\n [CLIENT_TECHNOLOGY_ATTR]: CLIENT_TECHNOLOGY_POSTGRES_JS,\n [POSTGRES_JS_QUERY_TEXT_ATTR]: event.queryText,\n [POSTGRES_JS_QUERY_PARAMS_ATTR]: event.params,\n [POSTGRES_JS_ROW_MODE_ATTR]: event.rowMode,\n ...frontendSessionAttribute(event),\n };\n if (event.result !== undefined) {\n // Re-cap here as well as at capture time: the attribute contract is what the store\n // reads, and a shim that capped differently must not widen it.\n const truncated =\n event.result.truncated ||\n event.result.body.length > MAX_POSTGRES_JS_RESULT_SIZE;\n attributes[POSTGRES_JS_RESULT_ATTR] = truncated\n ? event.result.body.slice(0, MAX_POSTGRES_JS_RESULT_SIZE)\n : event.result.body;\n attributes[POSTGRES_JS_RESULT_TRUNCATED_ATTR] = truncated;\n }\n if (event.errorJson !== undefined) {\n attributes[POSTGRES_JS_ERROR_ATTR] = event.errorJson;\n }\n\n return this.buildClientSpan(\n event,\n `postgresjs.${postgresJsCommandOf(event.queryText)}`,\n attributes,\n event.errorJson !== undefined\n ? { code: SPAN_STATUS_ERROR }\n : { code: SPAN_STATUS_UNSET },\n );\n }\n\n private buildHttpClientSpan(\n event: OutboundRequestEvent | BindingRequestEvent,\n technology: string,\n name: string,\n extraAttributes?: Record<string, unknown>,\n ): ReadableSpan {\n const url = parseUrl(event.url);\n const attributes = buildHttpAttributes(event, url);\n attributes[CLIENT_TECHNOLOGY_ATTR] = technology;\n Object.assign(attributes, extraAttributes);\n if (event.requestBody !== undefined) {\n attributes[\"http.request.body\"] = event.requestBody.body;\n attributes[\"http.request.body.truncated\"] = event.requestBody.truncated;\n }\n if (event.responseBody !== undefined) {\n attributes[\"http.response.body\"] = event.responseBody.body;\n attributes[\"http.response.body.truncated\"] = event.responseBody.truncated;\n }\n\n return this.buildClientSpan(\n event,\n name,\n attributes,\n buildStatus(event, SPAN_KIND_CLIENT),\n );\n }\n\n private buildClientSpan(\n event: CaptureEvent,\n name: string,\n attributes: Record<string, unknown>,\n status: SpanStatus,\n ): ReadableSpan {\n const ids = this.resolveIds(event);\n const spanId = randomHex(8);\n return {\n name,\n parentSpanId: ids.serverSpanId,\n kind: SPAN_KIND_CLIENT,\n status,\n startTime: msToHrTime(event.startTimeMs),\n endTime: msToHrTime(event.endTimeMs),\n duration: msToHrTime(Math.max(0, event.endTimeMs - event.startTimeMs)),\n attributes,\n spanContext: () => ({ traceId: ids.traceId, spanId }),\n };\n }\n\n /**\n * The trace and server-span ids for an event: the shim's, when it sent them, and otherwise a\n * pair invented per `requestId` and remembered — the pre-`traceId` behaviour.\n */\n private resolveIds(event: CaptureEvent): TraceContextEntry {\n if (event.traceId !== undefined && event.serverSpanId !== undefined) {\n return {\n traceId: event.traceId,\n serverSpanId: event.serverSpanId,\n lastSeenMs: event.endTimeMs,\n };\n }\n const existing = this.traceContexts.get(event.requestId);\n if (existing) {\n existing.lastSeenMs = event.endTimeMs;\n return existing;\n }\n this.pruneTraceContexts(event.endTimeMs);\n const entry: TraceContextEntry = {\n traceId: randomHex(16),\n serverSpanId: randomHex(8),\n lastSeenMs: event.endTimeMs,\n };\n this.traceContexts.set(event.requestId, entry);\n return entry;\n }\n\n private pruneTraceContexts(nowMs: number): void {\n if (this.traceContexts.size < TRACE_CONTEXT_PRUNE_THRESHOLD) {\n return;\n }\n for (const [requestId, entry] of this.traceContexts) {\n if (nowMs - entry.lastSeenMs > TRACE_CONTEXT_TTL_MS) {\n this.traceContexts.delete(requestId);\n }\n }\n }\n}\n\ninterface TraceContextEntry {\n traceId: string;\n serverSpanId: string;\n lastSeenMs: number;\n}\n\nconst TRACE_CONTEXT_TTL_MS = 10 * 60 * 1000;\nconst TRACE_CONTEXT_PRUNE_THRESHOLD = 1_000;\n\nconst HEX = \"0123456789abcdef\";\n\n/** WebCrypto rather than `node:crypto`, so this runs in a Worker as well as in Node. */\nexport const randomHex = (bytes: number): string => {\n const buffer = crypto.getRandomValues(new Uint8Array(bytes));\n let hex = \"\";\n for (const byte of buffer) {\n hex += HEX[byte >> 4] + HEX[byte & 0x0f];\n }\n return hex;\n};\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\ninterface ParsedUrl {\n full: string;\n path: string;\n query: string | undefined;\n scheme: string | undefined;\n host: string | undefined;\n port: number | undefined;\n}\n\nconst parseUrl = (raw: string): ParsedUrl => {\n try {\n const url = redactUrlCredentials(new URL(raw));\n const defaultPort = url.protocol === \"https:\" ? 443 : 80;\n return {\n full: url.toString(),\n path: url.pathname,\n query: url.search ? url.search.slice(1) : undefined,\n scheme: url.protocol.replace(/:$/, \"\"),\n host: url.hostname,\n port: url.port ? parseInt(url.port, 10) : defaultPort,\n };\n } catch {\n return {\n full: raw,\n path: raw.split(\"?\")[0] || \"/\",\n query: undefined,\n scheme: undefined,\n host: undefined,\n port: undefined,\n };\n }\n};\n\nconst frontendSessionAttribute = (\n event: CaptureEvent,\n): Record<string, string> =>\n event.frontendSessionId !== undefined\n ? { [FRONTEND_SESSION_ID_ATTR]: event.frontendSessionId }\n : {};\n\nconst buildHttpAttributes = (\n event: InboundRequestEvent | OutboundRequestEvent | BindingRequestEvent,\n url: ParsedUrl,\n): Record<string, unknown> => {\n const attributes: Record<string, unknown> = {\n \"http.request.method\": event.method,\n \"url.full\": url.full,\n \"url.path\": url.path,\n ...(url.query !== undefined ? { \"url.query\": url.query } : {}),\n ...(url.scheme !== undefined ? { \"url.scheme\": url.scheme } : {}),\n ...(url.host !== undefined ? { \"server.address\": url.host } : {}),\n ...(url.port !== undefined ? { \"server.port\": url.port } : {}),\n ...(event.statusCode !== undefined\n ? { \"http.response.status_code\": event.statusCode }\n : {}),\n ...(event.error !== undefined ? { \"error.type\": event.error } : {}),\n ...frontendSessionAttribute(event),\n };\n // The shim already filters to CAPTURED_HEADERS; re-filter here so a stale or third-party\n // shim can't persist sensitive headers via the sidecar.\n for (const [name, values] of Object.entries(event.requestHeaders)) {\n if (capturedHeaders.has(name.toLowerCase())) {\n attributes[`http.request.header.${name}`] = values;\n }\n }\n for (const [name, values] of Object.entries(event.responseHeaders ?? {})) {\n if (capturedHeaders.has(name.toLowerCase())) {\n attributes[`http.response.header.${name}`] = values;\n }\n }\n return attributes;\n};\n\nconst buildStatus = (\n event: InboundRequestEvent | OutboundRequestEvent | BindingRequestEvent,\n kind: number,\n): SpanStatus => {\n if (event.error !== undefined) {\n return { code: SPAN_STATUS_ERROR, message: event.error };\n }\n // Standard OTel HTTP semantics: client spans error on 4xx/5xx, server spans only on 5xx.\n const errorThreshold = kind === SPAN_KIND_CLIENT ? 400 : 500;\n if (event.statusCode !== undefined && event.statusCode >= errorThreshold) {\n return { code: SPAN_STATUS_ERROR };\n }\n return { code: SPAN_STATUS_UNSET };\n};\n"],"mappings":"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"}
@@ -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]="8b13fac8-683e-5692-90a7-5cdfd2b8035d")}catch(e){}}();
3
1
  import { STR_REDACTED } from "../redact-body.js";
4
2
  //#region src/spans/url-redaction.ts
5
3
  /**
@@ -36,5 +34,4 @@ const redactUrlCredentials = (url) => {
36
34
  //#endregion
37
35
  export { redactUrlCredentials };
38
36
 
39
- //# sourceMappingURL=url-redaction.js.map
40
- //# debugId=8b13fac8-683e-5692-90a7-5cdfd2b8035d
37
+ //# sourceMappingURL=url-redaction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"url-redaction.js","sources":["../../src/spans/url-redaction.ts"],"sourcesContent":["import { STR_REDACTED } from \"../redact-body\";\n\n/**\n * URL query keys that carry a credential rather than identify a resource.\n *\n * Mirrors `DEFAULT_QUERY_STRINGS_TO_REDACT` in the Node recorder's forked http\n * instrumentation. Both surfaces write URLs onto the same spans, so the two lists have to stay\n * identical or the same request records differently depending on where it was captured.\n */\nconst QUERY_STRINGS_TO_REDACT = [\n \"sig\",\n \"Signature\",\n \"AWSAccessKeyId\",\n \"X-Goog-Signature\",\n] as const;\n\n/**\n * Returns a copy of the URL with credential-bearing parts replaced: the signature-style query\n * parameters above, and any `user:password@` userinfo.\n *\n * Record side only. A replay lookup passes the live URL through unredacted, exactly as the\n * http, undici and sidecar lookup paths do, so a redacted parameter misses the exact tier and\n * is served by the drop-query tier instead — the right outcome, since a signature could not\n * have matched exactly across a record/replay boundary anyway.\n */\nexport const redactUrlCredentials = (url: URL): URL => {\n const redacted = new URL(url.href);\n for (const sensitiveParam of QUERY_STRINGS_TO_REDACT) {\n if (redacted.searchParams.get(sensitiveParam)) {\n redacted.searchParams.set(sensitiveParam, STR_REDACTED);\n }\n }\n if (redacted.username || redacted.password) {\n redacted.username = STR_REDACTED;\n redacted.password = STR_REDACTED;\n }\n return redacted;\n};\n"],"names":[],"mappings":";;;;;;;;;;;AASA,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACD;;;;;;;;;;AAWD,MAAa,wBAAwB,QAAkB;CACrD,MAAM,WAAW,IAAI,IAAI,IAAI,KAAK;AAClC,MAAK,MAAM,kBAAkB,wBAC3B,KAAI,SAAS,aAAa,IAAI,eAAe,CAC3C,UAAS,aAAa,IAAI,gBAAgB,aAAa;AAG3D,KAAI,SAAS,YAAY,SAAS,UAAU;AAC1C,WAAS,WAAW;AACpB,WAAS,WAAW;;AAEtB,QAAO","debugId":"8b13fac8-683e-5692-90a7-5cdfd2b8035d"}
1
+ {"version":3,"file":"url-redaction.js","names":[],"sources":["../../src/spans/url-redaction.ts"],"sourcesContent":["import { STR_REDACTED } from \"../redact-body\";\n\n/**\n * URL query keys that carry a credential rather than identify a resource.\n *\n * Mirrors `DEFAULT_QUERY_STRINGS_TO_REDACT` in the Node recorder's forked http\n * instrumentation. Both surfaces write URLs onto the same spans, so the two lists have to stay\n * identical or the same request records differently depending on where it was captured.\n */\nconst QUERY_STRINGS_TO_REDACT = [\n \"sig\",\n \"Signature\",\n \"AWSAccessKeyId\",\n \"X-Goog-Signature\",\n] as const;\n\n/**\n * Returns a copy of the URL with credential-bearing parts replaced: the signature-style query\n * parameters above, and any `user:password@` userinfo.\n *\n * Record side only. A replay lookup passes the live URL through unredacted, exactly as the\n * http, undici and sidecar lookup paths do, so a redacted parameter misses the exact tier and\n * is served by the drop-query tier instead — the right outcome, since a signature could not\n * have matched exactly across a record/replay boundary anyway.\n */\nexport const redactUrlCredentials = (url: URL): URL => {\n const redacted = new URL(url.href);\n for (const sensitiveParam of QUERY_STRINGS_TO_REDACT) {\n if (redacted.searchParams.get(sensitiveParam)) {\n redacted.searchParams.set(sensitiveParam, STR_REDACTED);\n }\n }\n if (redacted.username || redacted.password) {\n redacted.username = STR_REDACTED;\n redacted.password = STR_REDACTED;\n }\n return redacted;\n};\n"],"mappings":";;;;;;;;;AASA,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACD;;;;;;;;;;AAWD,MAAa,wBAAwB,QAAkB;CACrD,MAAM,WAAW,IAAI,IAAI,IAAI,KAAK;AAClC,MAAK,MAAM,kBAAkB,wBAC3B,KAAI,SAAS,aAAa,IAAI,eAAe,CAC3C,UAAS,aAAa,IAAI,gBAAgB,aAAa;AAG3D,KAAI,SAAS,YAAY,SAAS,UAAU;AAC1C,WAAS,WAAW;AACpB,WAAS,WAAW;;AAEtB,QAAO"}
package/dist/version.js CHANGED
@@ -8,11 +8,8 @@
8
8
  * Replaced by tsdown's `define`. The guard keeps a direct source import
9
9
  * (vitest, typecheck) working, where no bundler has substituted the value.
10
10
  */
11
-
12
- !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]="fda9e4d5-8d0c-5ea1-a5da-2b3069b3bd68")}catch(e){}}();
13
- const WORKERD_SHIM_VERSION = "2.331.3";
11
+ const WORKERD_SHIM_VERSION = "2.333.1";
14
12
  //#endregion
15
13
  export { WORKERD_SHIM_VERSION };
16
14
 
17
- //# sourceMappingURL=version.js.map
18
- //# debugId=fda9e4d5-8d0c-5ea1-a5da-2b3069b3bd68
15
+ //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sources":["../src/version.ts"],"sourcesContent":["declare const TSDOWN_WORKERD_SHIM_VERSION: string;\n\n/**\n * npm package version of this shim, baked in at build time. Customers bump\n * `@alwaysmeticulous/backend-recorder-workerd` independently of our cloud\n * deploy, so this is the only way a replay can tell which bundled shim\n * produced its After screenshots.\n *\n * Replaced by tsdown's `define`. The guard keeps a direct source import\n * (vitest, typecheck) working, where no bundler has substituted the value.\n */\nexport const WORKERD_SHIM_VERSION: string =\n typeof TSDOWN_WORKERD_SHIM_VERSION !== \"undefined\"\n ? TSDOWN_WORKERD_SHIM_VERSION\n : \"0.0.0-dev\";\n"],"names":[],"mappings":";;;;;;;;;;;;AAWA,MAAa,uBAAA","debugId":"fda9e4d5-8d0c-5ea1-a5da-2b3069b3bd68"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["declare const TSDOWN_WORKERD_SHIM_VERSION: string;\n\n/**\n * npm package version of this shim, baked in at build time. Customers bump\n * `@alwaysmeticulous/backend-recorder-workerd` independently of our cloud\n * deploy, so this is the only way a replay can tell which bundled shim\n * produced its After screenshots.\n *\n * Replaced by tsdown's `define`. The guard keeps a direct source import\n * (vitest, typecheck) working, where no bundler has substituted the value.\n */\nexport const WORKERD_SHIM_VERSION: string =\n typeof TSDOWN_WORKERD_SHIM_VERSION !== \"undefined\"\n ? TSDOWN_WORKERD_SHIM_VERSION\n : \"0.0.0-dev\";\n"],"mappings":";;;;;;;;;;AAWA,MAAa,uBAAA"}
@@ -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]="39c427c3-1a1e-58aa-9021-0bd113903895")}catch(e){}}();
3
1
  import { requestCaptureContext } from "./context.js";
4
2
  import { warnOnce } from "./log.js";
5
3
  //#region src/virtual-clock.ts
@@ -132,5 +130,4 @@ const installVirtualClock = () => {
132
130
  //#endregion
133
131
  export { installVirtualClock };
134
132
 
135
- //# sourceMappingURL=virtual-clock.js.map
136
- //# debugId=39c427c3-1a1e-58aa-9021-0bd113903895
133
+ //# sourceMappingURL=virtual-clock.js.map