@alwaysmeticulous/backend-recorder-workerd 2.333.0 → 2.334.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +1 -0
  3. package/dist/binding-patch.js +1 -4
  4. package/dist/binding-patch.js.map +1 -1
  5. package/dist/body-capture.js +1 -4
  6. package/dist/body-capture.js.map +1 -1
  7. package/dist/capture-buffer.js +1 -4
  8. package/dist/capture-buffer.js.map +1 -1
  9. package/dist/context.js +1 -4
  10. package/dist/context.js.map +1 -1
  11. package/dist/coverage/replay-coverage.js +1 -4
  12. package/dist/coverage/replay-coverage.js.map +1 -1
  13. package/dist/coverage/runtime.js +1 -4
  14. package/dist/coverage/runtime.js.map +1 -1
  15. package/dist/error-capture.js +1 -4
  16. package/dist/error-capture.js.map +1 -1
  17. package/dist/fetch-patch.js +1 -4
  18. package/dist/fetch-patch.js.map +1 -1
  19. package/dist/health-probe-filter.d.ts +43 -0
  20. package/dist/health-probe-filter.js +79 -0
  21. package/dist/health-probe-filter.js.map +1 -0
  22. package/dist/health-probe.d.ts +29 -0
  23. package/dist/health-probe.js +76 -0
  24. package/dist/health-probe.js.map +1 -0
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.js +4 -5
  27. package/dist/index.js.map +1 -1
  28. package/dist/kv-capture.js +1 -4
  29. package/dist/kv-capture.js.map +1 -1
  30. package/dist/kv-patch.js +1 -4
  31. package/dist/kv-patch.js.map +1 -1
  32. package/dist/log.js +1 -4
  33. package/dist/log.js.map +1 -1
  34. package/dist/original-fetch.js +1 -4
  35. package/dist/original-fetch.js.map +1 -1
  36. package/dist/outbound-capture.js +1 -4
  37. package/dist/outbound-capture.js.map +1 -1
  38. package/dist/pages-function.js +1 -4
  39. package/dist/pages-function.js.map +1 -1
  40. package/dist/postgres/capture.js +1 -4
  41. package/dist/postgres/capture.js.map +1 -1
  42. package/dist/postgres/with-meticulous-postgres.js +1 -4
  43. package/dist/postgres/with-meticulous-postgres.js.map +1 -1
  44. package/dist/protocol.js +1 -4
  45. package/dist/protocol.js.map +1 -1
  46. package/dist/provisional-session-id.js +1 -4
  47. package/dist/provisional-session-id.js.map +1 -1
  48. package/dist/publish-session-id.js +1 -4
  49. package/dist/publish-session-id.js.map +1 -1
  50. package/dist/publish-shim-version.js +1 -4
  51. package/dist/publish-shim-version.js.map +1 -1
  52. package/dist/redact-body.js +1 -4
  53. package/dist/redact-body.js.map +1 -1
  54. package/dist/replay-fetch.js +1 -4
  55. package/dist/replay-fetch.js.map +1 -1
  56. package/dist/replay-log-tagging.js +1 -4
  57. package/dist/replay-log-tagging.js.map +1 -1
  58. package/dist/replay-sidecar-url.js +1 -4
  59. package/dist/replay-sidecar-url.js.map +1 -1
  60. package/dist/sidecar-client.js +1 -4
  61. package/dist/sidecar-client.js.map +1 -1
  62. package/dist/sidecar-transport.js +1 -4
  63. package/dist/sidecar-transport.js.map +1 -1
  64. package/dist/spans/attributes.js +1 -4
  65. package/dist/spans/attributes.js.map +1 -1
  66. package/dist/spans/readable-span.js +1 -4
  67. package/dist/spans/readable-span.js.map +1 -1
  68. package/dist/spans/serialize-span.js +1 -4
  69. package/dist/spans/serialize-span.js.map +1 -1
  70. package/dist/spans/span-builder.js +1 -4
  71. package/dist/spans/span-builder.js.map +1 -1
  72. package/dist/spans/url-redaction.js +1 -4
  73. package/dist/spans/url-redaction.js.map +1 -1
  74. package/dist/version.js +2 -5
  75. package/dist/version.js.map +1 -1
  76. package/dist/virtual-clock.js +1 -4
  77. package/dist/virtual-clock.js.map +1 -1
  78. package/dist/virtual-random.js +1 -4
  79. package/dist/virtual-random.js.map +1 -1
  80. package/dist/vite/index.js +13 -14
  81. package/dist/vite/index.js.map +1 -1
  82. package/dist/with-meticulous.js +0 -0
  83. package/dist/with-meticulous.js.map +1 -1
  84. package/package.json +2 -2
@@ -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":";;;;;;;;;;;;;;;;;;;;;;AAiEA,IAAa,cAAb,MAAyB;;;;;;CAMvB,gCAAiC,IAAI,KAAgC;CAErE,MAAM,OAA0C;AAC9C,UAAQ,MAAM,MAAd;GACE,KAAK,UACH,QAAO,KAAK,iBAAiB,MAAM;GACrC,KAAK,WACH,QAAO,KAAK,kBAAkB,MAAM;GACtC,KAAK,UACH,QAAO,KAAK,iBAAiB,MAAM;GACrC,KAAK,KACH,QAAO,KAAK,YAAY,MAAM;GAChC,KAAK,WACH,QAAO,KAAK,kBAAkB,MAAM;GACtC,QAEE,QAAO;;;CAIb,iBAAyB,OAA0C;EACjE,MAAM,MAAM,KAAK,WAAW,MAAM;EAClC,MAAM,MAAM,SAAS,MAAM,IAAI;EAC/B,MAAM,aAAa,oBAAoB,OAAO,IAAI;AAKlD,MAAI,MAAM,oBAAoB,KAAA,EAC5B,YAAW,0BAA0B,MAAM;AAG7C,SAAO;GACL,MAAM,GAAG,MAAM,OAAO,GAAG,IAAI;GAC7B,MAAA;GACA,QAAQ,YAAY,OAAA,EAAwB;GAC5C,WAAW,WAAW,MAAM,YAAY;GACxC,SAAS,WAAW,MAAM,UAAU;GACpC,UAAU,WAAW,KAAK,IAAI,GAAG,MAAM,YAAY,MAAM,YAAY,CAAC;GACtE;GACA,oBAAoB;IAClB,SAAS,IAAI;IACb,QAAQ,IAAI;IACb;GACF;;CAGH,kBAA0B,OAA2C;AACnE,SAAO,KAAK,oBACV,OACA,iCACA,MAAM,OACP;;;;;;CAOH,iBAAyB,OAA0C;AACjE,SAAO,KAAK,oBACV,OACA,mCACA,GAAG,MAAM,OAAO,GAAG,MAAM,eAAe,cACxC,MAAM,gBAAgB,KAAA,IAClB,GAAG,4BAA4B,MAAM,aAAa,GAClD,KAAA,EACL;;;;;;;;CASH,YAAoB,OAAuC;EACzD,MAAM,aAAsC;IACzC,yBAAyB;IACzB,4BAA4B,MAAM;GACnC,GAAI,MAAM,gBAAgB,KAAA,IACtB,GAAG,4BAA4B,MAAM,aAAa,GAClD,EAAE;GACN,GAAI,MAAM,QAAQ,KAAA,IAAY,GAAG,sBAAsB,MAAM,KAAK,GAAG,EAAE;GACvE,GAAI,MAAM,YAAY,KAAA,IAClB,GAAG,0BAA0B,MAAM,SAAS,GAC5C,EAAE;GACN,GAAI,MAAM,UAAU,KAAA,IAAY,EAAE,cAAc,MAAM,OAAO,GAAG,EAAE;GAClE,GAAG,yBAAyB,MAAM;GACnC;AACD,MAAI,MAAM,SAAS,KAAA,GAAW;AAC5B,cAAW,wBAAwB,MAAM,KAAK;AAC9C,cAAW,kCAAkC,MAAM,KAAK;;AAE1D,MAAI,MAAM,UAAU,KAAA,GAAW;AAC7B,cAAW,yBAAyB,MAAM,MAAM;AAChD,cAAW,mCAAmC,MAAM,MAAM;;AAE5D,MAAI,MAAM,WAAW,KAAA,GAAW;AAC9B,cAAW,0BAA0B,MAAM,OAAO;AAClD,cAAW,oCAAoC,MAAM,OAAO;;AAG9D,SAAO,KAAK,gBACV,OACA,MAAM,MAAM,eAAe,WAAW,GAAG,MAAM,aAC/C,YACA,MAAM,UAAU,KAAA,IACZ;GAAE,MAAA;GAAyB,SAAS,MAAM;GAAO,GACjD,EAAE,MAAA,GAAyB,CAChC;;;;;;CAOH,kBAA0B,OAAyC;EACjE,MAAM,aAAsC;IACzC,yBAAyB;IACzB,8BAA8B,MAAM;IACpC,gCAAgC,MAAM;IACtC,4BAA4B,MAAM;GACnC,GAAG,yBAAyB,MAAM;GACnC;AACD,MAAI,MAAM,WAAW,KAAA,GAAW;GAG9B,MAAM,YACJ,MAAM,OAAO,aACb,MAAM,OAAO,KAAK,SAAA;AACpB,cAAW,2BAA2B,YAClC,MAAM,OAAO,KAAK,MAAM,GAAG,4BAA4B,GACvD,MAAM,OAAO;AACjB,cAAW,qCAAqC;;AAElD,MAAI,MAAM,cAAc,KAAA,EACtB,YAAW,0BAA0B,MAAM;AAG7C,SAAO,KAAK,gBACV,OACA,cAAc,oBAAoB,MAAM,UAAU,IAClD,YACA,MAAM,cAAc,KAAA,IAChB,EAAE,MAAA,GAAyB,GAC3B,EAAE,MAAA,GAAyB,CAChC;;CAGH,oBACE,OACA,YACA,MACA,iBACc;EAEd,MAAM,aAAa,oBAAoB,OAD3B,SAAS,MAAM,IACsB,CAAC;AAClD,aAAW,0BAA0B;AACrC,SAAO,OAAO,YAAY,gBAAgB;AAC1C,MAAI,MAAM,gBAAgB,KAAA,GAAW;AACnC,cAAW,uBAAuB,MAAM,YAAY;AACpD,cAAW,iCAAiC,MAAM,YAAY;;AAEhE,MAAI,MAAM,iBAAiB,KAAA,GAAW;AACpC,cAAW,wBAAwB,MAAM,aAAa;AACtD,cAAW,kCAAkC,MAAM,aAAa;;AAGlE,SAAO,KAAK,gBACV,OACA,MACA,YACA,YAAY,OAAA,EAAwB,CACrC;;CAGH,gBACE,OACA,MACA,YACA,QACc;EACd,MAAM,MAAM,KAAK,WAAW,MAAM;EAClC,MAAM,SAAS,UAAU,EAAE;AAC3B,SAAO;GACL;GACA,cAAc,IAAI;GAClB,MAAA;GACA;GACA,WAAW,WAAW,MAAM,YAAY;GACxC,SAAS,WAAW,MAAM,UAAU;GACpC,UAAU,WAAW,KAAK,IAAI,GAAG,MAAM,YAAY,MAAM,YAAY,CAAC;GACtE;GACA,oBAAoB;IAAE,SAAS,IAAI;IAAS;IAAQ;GACrD;;;;;;CAOH,WAAmB,OAAwC;AACzD,MAAI,MAAM,YAAY,KAAA,KAAa,MAAM,iBAAiB,KAAA,EACxD,QAAO;GACL,SAAS,MAAM;GACf,cAAc,MAAM;GACpB,YAAY,MAAM;GACnB;EAEH,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM,UAAU;AACxD,MAAI,UAAU;AACZ,YAAS,aAAa,MAAM;AAC5B,UAAO;;AAET,OAAK,mBAAmB,MAAM,UAAU;EACxC,MAAM,QAA2B;GAC/B,SAAS,UAAU,GAAG;GACtB,cAAc,UAAU,EAAE;GAC1B,YAAY,MAAM;GACnB;AACD,OAAK,cAAc,IAAI,MAAM,WAAW,MAAM;AAC9C,SAAO;;CAGT,mBAA2B,OAAqB;AAC9C,MAAI,KAAK,cAAc,OAAO,8BAC5B;AAEF,OAAK,MAAM,CAAC,WAAW,UAAU,KAAK,cACpC,KAAI,QAAQ,MAAM,aAAa,qBAC7B,MAAK,cAAc,OAAO,UAAU;;;AAY5C,MAAM,uBAAuB,MAAU;AACvC,MAAM,gCAAgC;AAEtC,MAAM,MAAM;;AAGZ,MAAa,aAAa,UAA0B;CAClD,MAAM,SAAS,OAAO,gBAAgB,IAAI,WAAW,MAAM,CAAC;CAC5D,IAAI,MAAM;AACV,MAAK,MAAM,QAAQ,OACjB,QAAO,IAAI,QAAQ,KAAK,IAAI,OAAO;AAErC,QAAO;;AAGT,MAAM,kBAAkB,IAAI,IAAY,iBAAiB;AAWzD,MAAM,YAAY,QAA2B;AAC3C,KAAI;EACF,MAAM,MAAM,qBAAqB,IAAI,IAAI,IAAI,CAAC;EAC9C,MAAM,cAAc,IAAI,aAAa,WAAW,MAAM;AACtD,SAAO;GACL,MAAM,IAAI,UAAU;GACpB,MAAM,IAAI;GACV,OAAO,IAAI,SAAS,IAAI,OAAO,MAAM,EAAE,GAAG,KAAA;GAC1C,QAAQ,IAAI,SAAS,QAAQ,MAAM,GAAG;GACtC,MAAM,IAAI;GACV,MAAM,IAAI,OAAO,SAAS,IAAI,MAAM,GAAG,GAAG;GAC3C;SACK;AACN,SAAO;GACL,MAAM;GACN,MAAM,IAAI,MAAM,IAAI,CAAC,MAAM;GAC3B,OAAO,KAAA;GACP,QAAQ,KAAA;GACR,MAAM,KAAA;GACN,MAAM,KAAA;GACP;;;AAIL,MAAM,4BACJ,UAEA,MAAM,sBAAsB,KAAA,IACxB,GAAG,2BAA2B,MAAM,mBAAmB,GACvD,EAAE;AAER,MAAM,uBACJ,OACA,QAC4B;CAC5B,MAAM,aAAsC;EAC1C,uBAAuB,MAAM;EAC7B,YAAY,IAAI;EAChB,YAAY,IAAI;EAChB,GAAI,IAAI,UAAU,KAAA,IAAY,EAAE,aAAa,IAAI,OAAO,GAAG,EAAE;EAC7D,GAAI,IAAI,WAAW,KAAA,IAAY,EAAE,cAAc,IAAI,QAAQ,GAAG,EAAE;EAChE,GAAI,IAAI,SAAS,KAAA,IAAY,EAAE,kBAAkB,IAAI,MAAM,GAAG,EAAE;EAChE,GAAI,IAAI,SAAS,KAAA,IAAY,EAAE,eAAe,IAAI,MAAM,GAAG,EAAE;EAC7D,GAAI,MAAM,eAAe,KAAA,IACrB,EAAE,6BAA6B,MAAM,YAAY,GACjD,EAAE;EACN,GAAI,MAAM,UAAU,KAAA,IAAY,EAAE,cAAc,MAAM,OAAO,GAAG,EAAE;EAClE,GAAG,yBAAyB,MAAM;EACnC;AAGD,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,MAAM,eAAe,CAC/D,KAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC,YAAW,uBAAuB,UAAU;AAGhD,MAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,MAAM,mBAAmB,EAAE,CAAC,CACtE,KAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC,YAAW,wBAAwB,UAAU;AAGjD,QAAO;;AAGT,MAAM,eACJ,OACA,SACe;AACf,KAAI,MAAM,UAAU,KAAA,EAClB,QAAO;EAAE,MAAA;EAAyB,SAAS,MAAM;EAAO;CAG1D,MAAM,iBAAiB,SAAA,IAA4B,MAAM;AACzD,KAAI,MAAM,eAAe,KAAA,KAAa,MAAM,cAAc,eACxD,QAAO,EAAE,MAAA,GAAyB;AAEpC,QAAO,EAAE,MAAA,GAAyB"}
@@ -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]="562b72f7-cbbc-5923-8402-9ab98d9dfb40")}catch(e){}}();
13
- const WORKERD_SHIM_VERSION = "2.333.0";
11
+ const WORKERD_SHIM_VERSION = "2.334.0";
14
12
  //#endregion
15
13
  export { WORKERD_SHIM_VERSION };
16
14
 
17
- //# sourceMappingURL=version.js.map
18
- //# debugId=562b72f7-cbbc-5923-8402-9ab98d9dfb40
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":"562b72f7-cbbc-5923-8402-9ab98d9dfb40"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"virtual-clock.js","sources":["../src/virtual-clock.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\n\n/**\n * Freezes the worker's wall clock at the replayed session's last recorded activity.\n *\n * Without this, replay fails even when every outbound call is mocked correctly: credentials\n * minted during the recording (Firebase session cookies, OAuth access tokens) carry\n * short expiries, so by replay time the app rejects its own recorded credentials as stale.\n * Anchoring at the *end* of the recording means everything minted mid-session is already\n * issued but not yet expired.\n *\n * Ported from the Node backend recorder's replay clock, with two differences:\n * - the anchor comes from the request context rather than a session→anchor map, because\n * workerd's AsyncLocalStorage propagates reliably from the inbound handler. The Node\n * version needs a process-wide \"most recently seen session\" fallback, which can\n * resolve the wrong session on a replica serving several at once;\n * - the busy-wait breaker schedules on `setTimeout` — workerd has no `setImmediate`.\n *\n * Only `Date` is patched, matching the Node implementation's scope (no `Intl`, no\n * `Temporal`, no timer virtualisation).\n */\n\nconst OriginalDate = globalThis.Date;\n\n/** Real wall clock, immune to the patch below. */\nexport const nativeDateNow = (): number => OriginalDate.now();\n\n/**\n * The frozen time a replayed request with no anchor gets: 13 May 2026, World Cocktail Day.\n * A replay whose anchor is missing is already degraded, and live time there is the exact\n * non-determinism this file exists to remove — two replays of that session would render\n * different timestamps. A fixed recent date rather than the epoch, since app code routinely\n * renders or rejects an implausibly old timestamp differently. Matches the Node recorder's\n * unattributed fallback (`replay/virtual-clock.ts`).\n *\n * Exported for the tests: this is the time such a replay renders, so changing it changes\n * that replay's output.\n */\nexport const UNANCHORED_BASE_TIME_MS = OriginalDate.UTC(2026, 4, 13);\n\n// ---------------------------------------------------------------------------\n// Busy-wait loop breaker\n// ---------------------------------------------------------------------------\n\n// A frozen clock makes `while (Date.now() < deadline) {}` spin forever. Count reads that\n// return the same frozen value and, past the threshold, nudge the result forward 1ms per\n// extra read until the loop's deadline is met. The nudge is a pure function of the repeat\n// count, so a base and head replay break an identical loop identically.\nconst BREAK_AFTER = 20_000;\n\nlet lastLoopBreakBaseMs = 0;\nlet sameKeyCount = 0;\nlet loopBreakResetScheduled = false;\n\nconst applyLoopBreak = (baseMs: number): number => {\n // Reset at the end of the current macrotask. A synchronous busy-wait blocks the loop so\n // this cannot fire, letting the counter accumulate until it breaks the spin; once the\n // turn yields, the reset runs and the next turn starts fresh.\n if (!loopBreakResetScheduled) {\n loopBreakResetScheduled = true;\n try {\n setTimeout(() => {\n sameKeyCount = 0;\n loopBreakResetScheduled = false;\n }, 0);\n } catch {\n // Timers throw outside a request context in workerd. Nothing to reset in that case,\n // since clock reads there fall through to the real clock anyway.\n loopBreakResetScheduled = false;\n }\n }\n\n if (baseMs === lastLoopBreakBaseMs) {\n sameKeyCount++;\n } else {\n lastLoopBreakBaseMs = baseMs;\n sameKeyCount = 0;\n }\n return sameKeyCount <= BREAK_AFTER\n ? baseMs\n : baseMs + (sameKeyCount - BREAK_AFTER);\n};\n\n// ---------------------------------------------------------------------------\n// Resolver\n// ---------------------------------------------------------------------------\n\n/**\n * The virtual \"now\" for the request currently executing, or null when this is not a replayed\n * request — a recording, a deployed production worker, or a read outside any request (module\n * evaluation, background work) — in which case callers use the real clock. A replayed request\n * always gets a frozen time: its own anchor, or {@link UNANCHORED_BASE_TIME_MS} if it carries\n * none.\n */\nconst resolveVirtualNow = (): number | null => {\n const ctx = requestCaptureContext.getStore();\n if (ctx === undefined || ctx.mode !== \"replay\") {\n return null;\n }\n return applyLoopBreak(ctx.clockAnchorMs ?? UNANCHORED_BASE_TIME_MS);\n};\n\n// ---------------------------------------------------------------------------\n// Date shim\n// ---------------------------------------------------------------------------\n\n/**\n * What `Date.now()` reports. A standalone function rather than only a class static, so the\n * callable shim below can reuse it without referencing an unbound method — it touches no\n * `this`, so there is nothing to lose.\n */\nconst virtualDateNow = (): number => {\n const virtualNow = resolveVirtualNow();\n return virtualNow === null ? OriginalDate.now() : virtualNow;\n};\n\n// `Date` is heavily overloaded; capture args loosely and forward every explicit form\n// unchanged. Only the zero-arg form reads \"now\" and is virtualized.\nclass VirtualDateClass extends OriginalDate {\n constructor(...args: unknown[]) {\n if (args.length > 0) {\n super(...(args as ConstructorParameters<typeof OriginalDate>));\n return;\n }\n const virtualNow = resolveVirtualNow();\n if (virtualNow === null) {\n super();\n return;\n }\n super(virtualNow);\n }\n\n static override now(): number {\n return virtualDateNow();\n }\n}\n\n// Classes cannot be called without `new`, but native `Date()` can be (returning a string),\n// so wrap the class in a plain function supporting both.\nfunction VirtualDate(this: unknown, ...args: unknown[]): Date | string {\n if (!(this instanceof VirtualDate)) {\n return new VirtualDateClass().toString();\n }\n return new VirtualDateClass(...args);\n}\n\nVirtualDate.now = virtualDateNow;\nVirtualDate.parse = OriginalDate.parse;\nVirtualDate.UTC = OriginalDate.UTC;\n\n// Share the prototype so `new Date() instanceof Date` holds and every Date.prototype\n// method resolves on instances.\nObject.defineProperty(VirtualDate, \"prototype\", {\n value: VirtualDateClass.prototype,\n writable: false,\n configurable: false,\n});\n(VirtualDateClass.prototype as { constructor: unknown }).constructor =\n VirtualDate;\n\n// ---------------------------------------------------------------------------\n// Install\n// ---------------------------------------------------------------------------\n\nlet installed = false;\n\n/**\n * Replaces the global `Date` with the per-request virtual clock. Idempotent, and a no-op\n * failure: a worker that will not let its `Date` be replaced still replays, just without\n * clock virtualisation.\n *\n * Called lazily from the replay branch of the request wrapper rather than at module scope:\n * record mode and deployed production workers then pay nothing, and the package declares no\n * `sideEffects`, so a module-scope side effect could legitimately be tree-shaken away by a\n * customer's bundler.\n */\nexport const installVirtualClock = (): void => {\n if (installed) {\n return;\n }\n installed = true;\n try {\n globalThis.Date = VirtualDate as unknown as DateConstructor;\n } catch (error) {\n warnOnce(\n \"virtual-clock\",\n \"Could not install the Meticulous replay clock — recorded credentials may be treated as expired.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,eAAe,WAAW;;;;;;;;;;;;AAgBhC,MAAa,0BAA0B,aAAa,IAAI,MAAM,GAAG,GAAG;AAUpE,MAAM,cAAc;AAEpB,IAAI,sBAAsB;AAC1B,IAAI,eAAe;AACnB,IAAI,0BAA0B;AAE9B,MAAM,kBAAkB,WAA2B;AAIjD,KAAI,CAAC,yBAAyB;AAC5B,4BAA0B;AAC1B,MAAI;AACF,oBAAiB;AACf,mBAAe;AACf,8BAA0B;MACzB,EAAE;UACC;AAGN,6BAA0B;;;AAI9B,KAAI,WAAW,oBACb;MACK;AACL,wBAAsB;AACtB,iBAAe;;AAEjB,QAAO,gBAAgB,cACnB,SACA,UAAU,eAAe;;;;;;;;;AAc/B,MAAM,0BAAyC;CAC7C,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,QAAQ,KAAA,KAAa,IAAI,SAAS,SACpC,QAAO;AAET,QAAO,eAAe,IAAI,iBAAiB,wBAAwB;;;;;;;AAYrE,MAAM,uBAA+B;CACnC,MAAM,aAAa,mBAAmB;AACtC,QAAO,eAAe,OAAO,aAAa,KAAK,GAAG;;AAKpD,IAAM,mBAAN,cAA+B,aAAa;CAC1C,YAAY,GAAG,MAAiB;AAC9B,MAAI,KAAK,SAAS,GAAG;AACnB,SAAM,GAAI,KAAoD;AAC9D;;EAEF,MAAM,aAAa,mBAAmB;AACtC,MAAI,eAAe,MAAM;AACvB,UAAO;AACP;;AAEF,QAAM,WAAW;;CAGnB,OAAgB,MAAc;AAC5B,SAAO,gBAAgB;;;AAM3B,SAAS,YAA2B,GAAG,MAAgC;AACrE,KAAI,EAAE,gBAAgB,aACpB,QAAO,IAAI,kBAAkB,CAAC,UAAU;AAE1C,QAAO,IAAI,iBAAiB,GAAG,KAAK;;AAGtC,YAAY,MAAM;AAClB,YAAY,QAAQ,aAAa;AACjC,YAAY,MAAM,aAAa;AAI/B,OAAO,eAAe,aAAa,aAAa;CAC9C,OAAO,iBAAiB;CACxB,UAAU;CACV,cAAc;CACf,CAAC;AACF,iBAAkB,UAAuC,cACvD;AAMF,IAAI,YAAY;;;;;;;;;;;AAYhB,MAAa,4BAAkC;AAC7C,KAAI,UACF;AAEF,aAAY;AACZ,KAAI;AACF,aAAW,OAAO;UACX,OAAO;AACd,WACE,iBACA,mGACA,MACD","debugId":"39c427c3-1a1e-58aa-9021-0bd113903895"}
1
+ {"version":3,"file":"virtual-clock.js","names":[],"sources":["../src/virtual-clock.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\n\n/**\n * Freezes the worker's wall clock at the replayed session's last recorded activity.\n *\n * Without this, replay fails even when every outbound call is mocked correctly: credentials\n * minted during the recording (Firebase session cookies, OAuth access tokens) carry\n * short expiries, so by replay time the app rejects its own recorded credentials as stale.\n * Anchoring at the *end* of the recording means everything minted mid-session is already\n * issued but not yet expired.\n *\n * Ported from the Node backend recorder's replay clock, with two differences:\n * - the anchor comes from the request context rather than a session→anchor map, because\n * workerd's AsyncLocalStorage propagates reliably from the inbound handler. The Node\n * version needs a process-wide \"most recently seen session\" fallback, which can\n * resolve the wrong session on a replica serving several at once;\n * - the busy-wait breaker schedules on `setTimeout` — workerd has no `setImmediate`.\n *\n * Only `Date` is patched, matching the Node implementation's scope (no `Intl`, no\n * `Temporal`, no timer virtualisation).\n */\n\nconst OriginalDate = globalThis.Date;\n\n/** Real wall clock, immune to the patch below. */\nexport const nativeDateNow = (): number => OriginalDate.now();\n\n/**\n * The frozen time a replayed request with no anchor gets: 13 May 2026, World Cocktail Day.\n * A replay whose anchor is missing is already degraded, and live time there is the exact\n * non-determinism this file exists to remove — two replays of that session would render\n * different timestamps. A fixed recent date rather than the epoch, since app code routinely\n * renders or rejects an implausibly old timestamp differently. Matches the Node recorder's\n * unattributed fallback (`replay/virtual-clock.ts`).\n *\n * Exported for the tests: this is the time such a replay renders, so changing it changes\n * that replay's output.\n */\nexport const UNANCHORED_BASE_TIME_MS = OriginalDate.UTC(2026, 4, 13);\n\n// ---------------------------------------------------------------------------\n// Busy-wait loop breaker\n// ---------------------------------------------------------------------------\n\n// A frozen clock makes `while (Date.now() < deadline) {}` spin forever. Count reads that\n// return the same frozen value and, past the threshold, nudge the result forward 1ms per\n// extra read until the loop's deadline is met. The nudge is a pure function of the repeat\n// count, so a base and head replay break an identical loop identically.\nconst BREAK_AFTER = 20_000;\n\nlet lastLoopBreakBaseMs = 0;\nlet sameKeyCount = 0;\nlet loopBreakResetScheduled = false;\n\nconst applyLoopBreak = (baseMs: number): number => {\n // Reset at the end of the current macrotask. A synchronous busy-wait blocks the loop so\n // this cannot fire, letting the counter accumulate until it breaks the spin; once the\n // turn yields, the reset runs and the next turn starts fresh.\n if (!loopBreakResetScheduled) {\n loopBreakResetScheduled = true;\n try {\n setTimeout(() => {\n sameKeyCount = 0;\n loopBreakResetScheduled = false;\n }, 0);\n } catch {\n // Timers throw outside a request context in workerd. Nothing to reset in that case,\n // since clock reads there fall through to the real clock anyway.\n loopBreakResetScheduled = false;\n }\n }\n\n if (baseMs === lastLoopBreakBaseMs) {\n sameKeyCount++;\n } else {\n lastLoopBreakBaseMs = baseMs;\n sameKeyCount = 0;\n }\n return sameKeyCount <= BREAK_AFTER\n ? baseMs\n : baseMs + (sameKeyCount - BREAK_AFTER);\n};\n\n// ---------------------------------------------------------------------------\n// Resolver\n// ---------------------------------------------------------------------------\n\n/**\n * The virtual \"now\" for the request currently executing, or null when this is not a replayed\n * request — a recording, a deployed production worker, or a read outside any request (module\n * evaluation, background work) — in which case callers use the real clock. A replayed request\n * always gets a frozen time: its own anchor, or {@link UNANCHORED_BASE_TIME_MS} if it carries\n * none.\n */\nconst resolveVirtualNow = (): number | null => {\n const ctx = requestCaptureContext.getStore();\n if (ctx === undefined || ctx.mode !== \"replay\") {\n return null;\n }\n return applyLoopBreak(ctx.clockAnchorMs ?? UNANCHORED_BASE_TIME_MS);\n};\n\n// ---------------------------------------------------------------------------\n// Date shim\n// ---------------------------------------------------------------------------\n\n/**\n * What `Date.now()` reports. A standalone function rather than only a class static, so the\n * callable shim below can reuse it without referencing an unbound method — it touches no\n * `this`, so there is nothing to lose.\n */\nconst virtualDateNow = (): number => {\n const virtualNow = resolveVirtualNow();\n return virtualNow === null ? OriginalDate.now() : virtualNow;\n};\n\n// `Date` is heavily overloaded; capture args loosely and forward every explicit form\n// unchanged. Only the zero-arg form reads \"now\" and is virtualized.\nclass VirtualDateClass extends OriginalDate {\n constructor(...args: unknown[]) {\n if (args.length > 0) {\n super(...(args as ConstructorParameters<typeof OriginalDate>));\n return;\n }\n const virtualNow = resolveVirtualNow();\n if (virtualNow === null) {\n super();\n return;\n }\n super(virtualNow);\n }\n\n static override now(): number {\n return virtualDateNow();\n }\n}\n\n// Classes cannot be called without `new`, but native `Date()` can be (returning a string),\n// so wrap the class in a plain function supporting both.\nfunction VirtualDate(this: unknown, ...args: unknown[]): Date | string {\n if (!(this instanceof VirtualDate)) {\n return new VirtualDateClass().toString();\n }\n return new VirtualDateClass(...args);\n}\n\nVirtualDate.now = virtualDateNow;\nVirtualDate.parse = OriginalDate.parse;\nVirtualDate.UTC = OriginalDate.UTC;\n\n// Share the prototype so `new Date() instanceof Date` holds and every Date.prototype\n// method resolves on instances.\nObject.defineProperty(VirtualDate, \"prototype\", {\n value: VirtualDateClass.prototype,\n writable: false,\n configurable: false,\n});\n(VirtualDateClass.prototype as { constructor: unknown }).constructor =\n VirtualDate;\n\n// ---------------------------------------------------------------------------\n// Install\n// ---------------------------------------------------------------------------\n\nlet installed = false;\n\n/**\n * Replaces the global `Date` with the per-request virtual clock. Idempotent, and a no-op\n * failure: a worker that will not let its `Date` be replaced still replays, just without\n * clock virtualisation.\n *\n * Called lazily from the replay branch of the request wrapper rather than at module scope:\n * record mode and deployed production workers then pay nothing, and the package declares no\n * `sideEffects`, so a module-scope side effect could legitimately be tree-shaken away by a\n * customer's bundler.\n */\nexport const installVirtualClock = (): void => {\n if (installed) {\n return;\n }\n installed = true;\n try {\n globalThis.Date = VirtualDate as unknown as DateConstructor;\n } catch (error) {\n warnOnce(\n \"virtual-clock\",\n \"Could not install the Meticulous replay clock — recorded credentials may be treated as expired.\",\n error,\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,eAAe,WAAW;;;;;;;;;;;;AAgBhC,MAAa,0BAA0B,aAAa,IAAI,MAAM,GAAG,GAAG;AAUpE,MAAM,cAAc;AAEpB,IAAI,sBAAsB;AAC1B,IAAI,eAAe;AACnB,IAAI,0BAA0B;AAE9B,MAAM,kBAAkB,WAA2B;AAIjD,KAAI,CAAC,yBAAyB;AAC5B,4BAA0B;AAC1B,MAAI;AACF,oBAAiB;AACf,mBAAe;AACf,8BAA0B;MACzB,EAAE;UACC;AAGN,6BAA0B;;;AAI9B,KAAI,WAAW,oBACb;MACK;AACL,wBAAsB;AACtB,iBAAe;;AAEjB,QAAO,gBAAgB,cACnB,SACA,UAAU,eAAe;;;;;;;;;AAc/B,MAAM,0BAAyC;CAC7C,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,QAAQ,KAAA,KAAa,IAAI,SAAS,SACpC,QAAO;AAET,QAAO,eAAe,IAAI,iBAAiB,wBAAwB;;;;;;;AAYrE,MAAM,uBAA+B;CACnC,MAAM,aAAa,mBAAmB;AACtC,QAAO,eAAe,OAAO,aAAa,KAAK,GAAG;;AAKpD,IAAM,mBAAN,cAA+B,aAAa;CAC1C,YAAY,GAAG,MAAiB;AAC9B,MAAI,KAAK,SAAS,GAAG;AACnB,SAAM,GAAI,KAAoD;AAC9D;;EAEF,MAAM,aAAa,mBAAmB;AACtC,MAAI,eAAe,MAAM;AACvB,UAAO;AACP;;AAEF,QAAM,WAAW;;CAGnB,OAAgB,MAAc;AAC5B,SAAO,gBAAgB;;;AAM3B,SAAS,YAA2B,GAAG,MAAgC;AACrE,KAAI,EAAE,gBAAgB,aACpB,QAAO,IAAI,kBAAkB,CAAC,UAAU;AAE1C,QAAO,IAAI,iBAAiB,GAAG,KAAK;;AAGtC,YAAY,MAAM;AAClB,YAAY,QAAQ,aAAa;AACjC,YAAY,MAAM,aAAa;AAI/B,OAAO,eAAe,aAAa,aAAa;CAC9C,OAAO,iBAAiB;CACxB,UAAU;CACV,cAAc;CACf,CAAC;AACF,iBAAkB,UAAuC,cACvD;AAMF,IAAI,YAAY;;;;;;;;;;;AAYhB,MAAa,4BAAkC;AAC7C,KAAI,UACF;AAEF,aAAY;AACZ,KAAI;AACF,aAAW,OAAO;UACX,OAAO;AACd,WACE,iBACA,mGACA,MACD"}
@@ -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]="bae5b317-9875-525c-b298-245193a2eca5")}catch(e){}}();
3
1
  import { requestCaptureContext } from "./context.js";
4
2
  import { warnOnce } from "./log.js";
5
3
  //#region src/virtual-random.ts
@@ -253,5 +251,4 @@ const installVirtualRandom = () => {
253
251
  //#endregion
254
252
  export { installVirtualRandom };
255
253
 
256
- //# sourceMappingURL=virtual-random.js.map
257
- //# debugId=bae5b317-9875-525c-b298-245193a2eca5
254
+ //# sourceMappingURL=virtual-random.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"virtual-random.js","sources":["../src/virtual-random.ts"],"sourcesContent":["import { requestCaptureContext, type RequestReplayContext } from \"./context\";\nimport { warnOnce } from \"./log\";\n\n/**\n * Per-request randomness virtualization for replay mode.\n *\n * Mocking every outbound `fetch` is not enough to make a replay deterministic: a Worker that\n * mints an id (`crypto.randomUUID()` for a guest id, `getRandomValues` for a token) produces\n * a different value in the base replay than in the head replay, so the rendered HTML differs\n * and any screenshot showing that value diffs forever. This replaces the worker's sources of\n * randomness with seeded generators while serving a replayed request, so two replays of the\n * same recorded session render identically.\n *\n * Deliberately a *generator*, not a recording: like the browser replayer we do not capture\n * the numbers the original session produced, we produce fresh ones from a fixed seed. The\n * app still sees plausible random values, and two replays see the same ones.\n *\n * Ported from the Node backend recorder's `replay/virtual-random.ts`, which in turn mirrors\n * the browser replayer's `mockOutRandomNumberGeneration`, including its butterfly-effect\n * containment: a separate sequence per call stack, seeded from a per-call-site counter, so a\n * change in one part of the app does not shift the numbers another part gets, and a shared\n * helper called from two places does not return the same value twice. Sequences are scoped\n * to the inbound request, not the whole replay: two concurrent requests of the same session\n * that race to the same call site would otherwise consume one shared stream in arrival\n * order, and a Fisher-Yates shuffle (or any other draw) would then differ between replays.\n * Each request starts its sequences over from the same session-scoped seed, and every draw\n * inside a replayed request comes from the seeded PRNG.\n *\n * A call site's first draws used to return one repeated constant before the PRNG started, to\n * damp the seed's positional fragility (see {@link openSequence}). That is gone: returning the\n * same number to consecutive draws is not determinism, it is a collision, and randomness is far\n * more often consumed for uniqueness than for its value. Anything minting an id from\n * consecutive draws — `nanoid`, a hand-rolled uuid, a cache-buster — got one repeated value and\n * broke. The damping was not worth what it cost.\n *\n * Two differences from the Node version, both forced by the runtime:\n *\n * - the session comes from the request context rather than a session resolver with a\n * process-wide fallback, because workerd's AsyncLocalStorage propagates reliably from the\n * inbound handler;\n * - only the globals workerd actually has are patched — there is no `node:crypto` module to\n * mutate.\n *\n * Reads outside a replayed request — module evaluation, a recording, a deployed production\n * worker — fall through to the native generators untouched.\n */\n\nconst nativeMathRandom = Math.random.bind(Math);\nconst nativeRandomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);\nconst nativeGetRandomValues = globalThis.crypto?.getRandomValues?.bind(\n globalThis.crypto,\n);\n\n/**\n * Frames of the stack that belong to this module and must be skipped to reach the app's call\n * site, counted from the `new Error()` inside {@link getCallStack}.\n *\n * Getting this wrong degrades gracefully rather than breaking: a call site that resolves to\n * one of our own frames merges sequences that should have been separate, which costs\n * butterfly-effect isolation but is still perfectly deterministic.\n */\nconst FRAMES_TO_APP = 3; // getCallStack ← openSequence ← patched global ← app\n\n/** How much of the stack identifies a sequence. Ten frames tells call paths apart cheaply. */\nconst STACK_FRAMES = 10;\n\n/** Stack captures per request, after which sequences are keyed by call site alone. */\nconst MAX_STACK_CAPTURES = 10_000;\n\nconst UNKNOWN_CALL_SITE = \"unknown\";\n\ninterface RequestRandomState {\n frontendSessionId: string;\n generatorBySequenceKey: Map<string, () => number>;\n nextCallerIdByCallSite: Map<string, number>;\n stackCaptures: number;\n}\n\n/**\n * Held on the inbound request object, so two concurrent requests of one replay cannot share\n * a generator. The seed below stays session-scoped: base and head are different replays of\n * the same recording and must draw identical numbers, and two requests of that recording\n * must draw the same first values rather than racing to consume one stream.\n */\nconst stateByRequest = new WeakMap<RequestReplayContext, RequestRandomState>();\n\n// ---------------------------------------------------------------------------\n// Seeded PRNG (Alea)\n// ---------------------------------------------------------------------------\n\n/**\n * Alea, the same algorithm the browser replayer gets from `seedrandom`. Inlined because this\n * package is published to npm and ships no runtime dependencies — a customer's Worker bundle\n * should not grow for this.\n */\nexport const createSeededRandom = (seed: string): (() => number) => {\n let n = 0xefc8249d;\n const mash = (data: string): number => {\n for (let i = 0; i < data.length; i++) {\n n += data.charCodeAt(i);\n let h = 0.02519603282416938 * n;\n n = h >>> 0;\n h -= n;\n h *= n;\n n = h >>> 0;\n h -= n;\n n += h * 0x100000000;\n }\n return (n >>> 0) * 2.3283064365386963e-10;\n };\n\n let s0 = mash(\" \");\n let s1 = mash(\" \");\n let s2 = mash(\" \");\n let c = 1;\n s0 -= mash(seed);\n if (s0 < 0) {\n s0 += 1;\n }\n s1 -= mash(seed);\n if (s1 < 0) {\n s1 += 1;\n }\n s2 -= mash(seed);\n if (s2 < 0) {\n s2 += 1;\n }\n\n return () => {\n const t = 2091639 * s0 + c * 2.3283064365386963e-10;\n s0 = s1;\n s1 = s2;\n c = t | 0;\n s2 = t - c;\n return s2;\n };\n};\n\n// ---------------------------------------------------------------------------\n// Sequence resolution\n// ---------------------------------------------------------------------------\n\nconst getCallStack = (\n frameCount: number = STACK_FRAMES,\n): { stack: string; site: string } => {\n const originalLimit = Error.stackTraceLimit;\n try {\n Error.stackTraceLimit = FRAMES_TO_APP + frameCount;\n const stack = new Error().stack;\n if (stack == null) {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n }\n // Line 0 is the \"Error\" header; frame lines follow, innermost first, and the first\n // FRAMES_TO_APP of them are ours.\n const lines = stack.split(\"\\n\").slice(FRAMES_TO_APP + 1);\n return {\n stack: lines.join(\"\\n\") || UNKNOWN_CALL_SITE,\n site: lines[0] ?? UNKNOWN_CALL_SITE,\n };\n } catch {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n } finally {\n Error.stackTraceLimit = originalLimit;\n }\n};\n\nconst getRequestState = (): RequestRandomState | undefined => {\n const ctx = requestCaptureContext.getStore();\n if (ctx === undefined || ctx.mode !== \"replay\") {\n return undefined;\n }\n const existing = stateByRequest.get(ctx);\n if (existing !== undefined) {\n return existing;\n }\n const created: RequestRandomState = {\n frontendSessionId: ctx.frontendSessionId,\n generatorBySequenceKey: new Map(),\n nextCallerIdByCallSite: new Map(),\n stackCaptures: 0,\n };\n stateByRequest.set(ctx, created);\n return created;\n};\n\n/**\n * The generator for the sequence the current call belongs to, or undefined outside a\n * replayed request.\n *\n * Sequences are keyed by the whole call stack, so a second call down the same path continues\n * the same sequence and gets a different value. The *seed* comes from a counter kept per\n * immediate call site, which is what keeps a shared id helper honest: two call paths through\n * the same `generateId()` share a call site, so they are seeded 0 and 1 and return different\n * ids.\n *\n * Note what the seed deliberately does NOT contain: any part of the stack. Base and head are\n * different builds of the app, so the same logical call site has a different string in each —\n * a seed built from it would change on every rebuild. The consequence, which the browser\n * replayer shares, is that two *distinct* call sites each seen for the first time both seed at\n * 0 and hand out the same first value. That is the accepted trade: an alternative that\n * separated them (say, an ordinal per call site) would shift every later site's values as soon\n * as a PR added one randomness call, which is the butterfly effect this whole scheme exists to\n * avoid.\n *\n * Because callerIds are handed out in order of first arrival, every draw now depends on that\n * order — there is no longer a fixed-draw phase in front of the PRNG to absorb it, so a PR\n * that routes a new path through a shared helper can shift the values another path draws.\n * That fragility was the reason for the phase; a phase that returned one repeated constant\n * was not a workable way to pay for it.\n */\nconst openSequence = (): (() => number) | undefined => {\n const state = getRequestState();\n if (state === undefined) {\n return undefined;\n }\n\n let sequenceKey = UNKNOWN_CALL_SITE;\n let callSite = UNKNOWN_CALL_SITE;\n if (state.stackCaptures < MAX_STACK_CAPTURES) {\n state.stackCaptures++;\n const captured = getCallStack();\n sequenceKey = captured.stack;\n callSite = captured.site;\n }\n\n const existing = state.generatorBySequenceKey.get(sequenceKey);\n if (existing !== undefined) {\n return existing;\n }\n const callerId = state.nextCallerIdByCallSite.get(callSite) ?? 0;\n // Seeded from the counter only — never from the call-site or stack string itself. Those embed\n // bundle filenames and line numbers, which differ between the base build and the head build\n // of the very same code, so putting one in the seed would reseed every sequence on any\n // rebuild and bring back exactly the diffs this exists to remove. The strings are grouping\n // keys, nothing more.\n const generator = createSeededRandom(\n `meticulous-random-${state.frontendSessionId}-${callerId}`,\n );\n state.generatorBySequenceKey.set(sequenceKey, generator);\n state.nextCallerIdByCallSite.set(callSite, callerId + 1);\n return generator;\n};\n\n// ---------------------------------------------------------------------------\n// Derived primitives\n// ---------------------------------------------------------------------------\n\nconst HEX = \"0123456789abcdef\";\n/** The two bits of a v4 UUID's variant field are fixed, leaving these four values. */\nconst VARIANT = \"89ab\";\n\nconst buildUuid = (draw: () => number): string => {\n let uuid = \"\";\n for (let i = 0; i < 36; i++) {\n if (i === 8 || i === 13 || i === 18 || i === 23) {\n uuid += \"-\";\n } else if (i === 14) {\n uuid += \"4\";\n } else if (i === 19) {\n uuid += VARIANT[Math.floor(draw() * VARIANT.length)];\n } else {\n uuid += HEX[Math.floor(draw() * HEX.length)];\n }\n }\n return uuid;\n};\n\n/**\n * Fills the view's bytes rather than its elements, so every typed array — including the\n * BigInt ones, whose elements cannot be assigned a number — is handled by one path.\n */\nconst fillBytes = (view: ArrayBufferView, draw: () => number): void => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Math.floor(draw() * 256);\n }\n};\n\n// ---------------------------------------------------------------------------\n// Install\n// ---------------------------------------------------------------------------\n\nlet installed = false;\n\n/**\n * Replaces the worker's sources of randomness with per-request seeded generators. Idempotent,\n * and a no-op failure: a worker that will not let them be replaced still replays, just\n * without randomness virtualisation.\n *\n * Called lazily from the replay branch of the request wrapper rather than at module scope,\n * for the same reasons as the virtual clock: record mode and deployed production workers then\n * pay nothing, and the package declares no `sideEffects`, so a module-scope side effect could\n * legitimately be tree-shaken away by a customer's bundler.\n */\nexport const installVirtualRandom = (): void => {\n if (installed) {\n return;\n }\n installed = true;\n try {\n Math.random = () => openSequence()?.() ?? nativeMathRandom();\n\n if (globalThis.crypto != null) {\n Object.defineProperty(globalThis.crypto, \"randomUUID\", {\n value: (): string => {\n const draw = openSequence();\n if (draw !== undefined) {\n return buildUuid(draw);\n }\n return nativeRandomUUID?.() ?? buildUuid(nativeMathRandom);\n },\n configurable: true,\n writable: true,\n });\n\n Object.defineProperty(globalThis.crypto, \"getRandomValues\", {\n value: <T extends ArrayBufferView>(array: T): T => {\n const draw = openSequence();\n if (draw === undefined) {\n return nativeGetRandomValues?.(array) ?? array;\n }\n fillBytes(array, draw);\n return array;\n },\n configurable: true,\n writable: true,\n });\n }\n } catch (error) {\n warnOnce(\n \"virtual-random\",\n \"Could not install the Meticulous replay random-number generators — ids minted by the app will differ between replays.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":"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","debugId":"bae5b317-9875-525c-b298-245193a2eca5"}
1
+ {"version":3,"file":"virtual-random.js","names":[],"sources":["../src/virtual-random.ts"],"sourcesContent":["import { requestCaptureContext, type RequestReplayContext } from \"./context\";\nimport { warnOnce } from \"./log\";\n\n/**\n * Per-request randomness virtualization for replay mode.\n *\n * Mocking every outbound `fetch` is not enough to make a replay deterministic: a Worker that\n * mints an id (`crypto.randomUUID()` for a guest id, `getRandomValues` for a token) produces\n * a different value in the base replay than in the head replay, so the rendered HTML differs\n * and any screenshot showing that value diffs forever. This replaces the worker's sources of\n * randomness with seeded generators while serving a replayed request, so two replays of the\n * same recorded session render identically.\n *\n * Deliberately a *generator*, not a recording: like the browser replayer we do not capture\n * the numbers the original session produced, we produce fresh ones from a fixed seed. The\n * app still sees plausible random values, and two replays see the same ones.\n *\n * Ported from the Node backend recorder's `replay/virtual-random.ts`, which in turn mirrors\n * the browser replayer's `mockOutRandomNumberGeneration`, including its butterfly-effect\n * containment: a separate sequence per call stack, seeded from a per-call-site counter, so a\n * change in one part of the app does not shift the numbers another part gets, and a shared\n * helper called from two places does not return the same value twice. Sequences are scoped\n * to the inbound request, not the whole replay: two concurrent requests of the same session\n * that race to the same call site would otherwise consume one shared stream in arrival\n * order, and a Fisher-Yates shuffle (or any other draw) would then differ between replays.\n * Each request starts its sequences over from the same session-scoped seed, and every draw\n * inside a replayed request comes from the seeded PRNG.\n *\n * A call site's first draws used to return one repeated constant before the PRNG started, to\n * damp the seed's positional fragility (see {@link openSequence}). That is gone: returning the\n * same number to consecutive draws is not determinism, it is a collision, and randomness is far\n * more often consumed for uniqueness than for its value. Anything minting an id from\n * consecutive draws — `nanoid`, a hand-rolled uuid, a cache-buster — got one repeated value and\n * broke. The damping was not worth what it cost.\n *\n * Two differences from the Node version, both forced by the runtime:\n *\n * - the session comes from the request context rather than a session resolver with a\n * process-wide fallback, because workerd's AsyncLocalStorage propagates reliably from the\n * inbound handler;\n * - only the globals workerd actually has are patched — there is no `node:crypto` module to\n * mutate.\n *\n * Reads outside a replayed request — module evaluation, a recording, a deployed production\n * worker — fall through to the native generators untouched.\n */\n\nconst nativeMathRandom = Math.random.bind(Math);\nconst nativeRandomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);\nconst nativeGetRandomValues = globalThis.crypto?.getRandomValues?.bind(\n globalThis.crypto,\n);\n\n/**\n * Frames of the stack that belong to this module and must be skipped to reach the app's call\n * site, counted from the `new Error()` inside {@link getCallStack}.\n *\n * Getting this wrong degrades gracefully rather than breaking: a call site that resolves to\n * one of our own frames merges sequences that should have been separate, which costs\n * butterfly-effect isolation but is still perfectly deterministic.\n */\nconst FRAMES_TO_APP = 3; // getCallStack ← openSequence ← patched global ← app\n\n/** How much of the stack identifies a sequence. Ten frames tells call paths apart cheaply. */\nconst STACK_FRAMES = 10;\n\n/** Stack captures per request, after which sequences are keyed by call site alone. */\nconst MAX_STACK_CAPTURES = 10_000;\n\nconst UNKNOWN_CALL_SITE = \"unknown\";\n\ninterface RequestRandomState {\n frontendSessionId: string;\n generatorBySequenceKey: Map<string, () => number>;\n nextCallerIdByCallSite: Map<string, number>;\n stackCaptures: number;\n}\n\n/**\n * Held on the inbound request object, so two concurrent requests of one replay cannot share\n * a generator. The seed below stays session-scoped: base and head are different replays of\n * the same recording and must draw identical numbers, and two requests of that recording\n * must draw the same first values rather than racing to consume one stream.\n */\nconst stateByRequest = new WeakMap<RequestReplayContext, RequestRandomState>();\n\n// ---------------------------------------------------------------------------\n// Seeded PRNG (Alea)\n// ---------------------------------------------------------------------------\n\n/**\n * Alea, the same algorithm the browser replayer gets from `seedrandom`. Inlined because this\n * package is published to npm and ships no runtime dependencies — a customer's Worker bundle\n * should not grow for this.\n */\nexport const createSeededRandom = (seed: string): (() => number) => {\n let n = 0xefc8249d;\n const mash = (data: string): number => {\n for (let i = 0; i < data.length; i++) {\n n += data.charCodeAt(i);\n let h = 0.02519603282416938 * n;\n n = h >>> 0;\n h -= n;\n h *= n;\n n = h >>> 0;\n h -= n;\n n += h * 0x100000000;\n }\n return (n >>> 0) * 2.3283064365386963e-10;\n };\n\n let s0 = mash(\" \");\n let s1 = mash(\" \");\n let s2 = mash(\" \");\n let c = 1;\n s0 -= mash(seed);\n if (s0 < 0) {\n s0 += 1;\n }\n s1 -= mash(seed);\n if (s1 < 0) {\n s1 += 1;\n }\n s2 -= mash(seed);\n if (s2 < 0) {\n s2 += 1;\n }\n\n return () => {\n const t = 2091639 * s0 + c * 2.3283064365386963e-10;\n s0 = s1;\n s1 = s2;\n c = t | 0;\n s2 = t - c;\n return s2;\n };\n};\n\n// ---------------------------------------------------------------------------\n// Sequence resolution\n// ---------------------------------------------------------------------------\n\nconst getCallStack = (\n frameCount: number = STACK_FRAMES,\n): { stack: string; site: string } => {\n const originalLimit = Error.stackTraceLimit;\n try {\n Error.stackTraceLimit = FRAMES_TO_APP + frameCount;\n const stack = new Error().stack;\n if (stack == null) {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n }\n // Line 0 is the \"Error\" header; frame lines follow, innermost first, and the first\n // FRAMES_TO_APP of them are ours.\n const lines = stack.split(\"\\n\").slice(FRAMES_TO_APP + 1);\n return {\n stack: lines.join(\"\\n\") || UNKNOWN_CALL_SITE,\n site: lines[0] ?? UNKNOWN_CALL_SITE,\n };\n } catch {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n } finally {\n Error.stackTraceLimit = originalLimit;\n }\n};\n\nconst getRequestState = (): RequestRandomState | undefined => {\n const ctx = requestCaptureContext.getStore();\n if (ctx === undefined || ctx.mode !== \"replay\") {\n return undefined;\n }\n const existing = stateByRequest.get(ctx);\n if (existing !== undefined) {\n return existing;\n }\n const created: RequestRandomState = {\n frontendSessionId: ctx.frontendSessionId,\n generatorBySequenceKey: new Map(),\n nextCallerIdByCallSite: new Map(),\n stackCaptures: 0,\n };\n stateByRequest.set(ctx, created);\n return created;\n};\n\n/**\n * The generator for the sequence the current call belongs to, or undefined outside a\n * replayed request.\n *\n * Sequences are keyed by the whole call stack, so a second call down the same path continues\n * the same sequence and gets a different value. The *seed* comes from a counter kept per\n * immediate call site, which is what keeps a shared id helper honest: two call paths through\n * the same `generateId()` share a call site, so they are seeded 0 and 1 and return different\n * ids.\n *\n * Note what the seed deliberately does NOT contain: any part of the stack. Base and head are\n * different builds of the app, so the same logical call site has a different string in each —\n * a seed built from it would change on every rebuild. The consequence, which the browser\n * replayer shares, is that two *distinct* call sites each seen for the first time both seed at\n * 0 and hand out the same first value. That is the accepted trade: an alternative that\n * separated them (say, an ordinal per call site) would shift every later site's values as soon\n * as a PR added one randomness call, which is the butterfly effect this whole scheme exists to\n * avoid.\n *\n * Because callerIds are handed out in order of first arrival, every draw now depends on that\n * order — there is no longer a fixed-draw phase in front of the PRNG to absorb it, so a PR\n * that routes a new path through a shared helper can shift the values another path draws.\n * That fragility was the reason for the phase; a phase that returned one repeated constant\n * was not a workable way to pay for it.\n */\nconst openSequence = (): (() => number) | undefined => {\n const state = getRequestState();\n if (state === undefined) {\n return undefined;\n }\n\n let sequenceKey = UNKNOWN_CALL_SITE;\n let callSite = UNKNOWN_CALL_SITE;\n if (state.stackCaptures < MAX_STACK_CAPTURES) {\n state.stackCaptures++;\n const captured = getCallStack();\n sequenceKey = captured.stack;\n callSite = captured.site;\n }\n\n const existing = state.generatorBySequenceKey.get(sequenceKey);\n if (existing !== undefined) {\n return existing;\n }\n const callerId = state.nextCallerIdByCallSite.get(callSite) ?? 0;\n // Seeded from the counter only — never from the call-site or stack string itself. Those embed\n // bundle filenames and line numbers, which differ between the base build and the head build\n // of the very same code, so putting one in the seed would reseed every sequence on any\n // rebuild and bring back exactly the diffs this exists to remove. The strings are grouping\n // keys, nothing more.\n const generator = createSeededRandom(\n `meticulous-random-${state.frontendSessionId}-${callerId}`,\n );\n state.generatorBySequenceKey.set(sequenceKey, generator);\n state.nextCallerIdByCallSite.set(callSite, callerId + 1);\n return generator;\n};\n\n// ---------------------------------------------------------------------------\n// Derived primitives\n// ---------------------------------------------------------------------------\n\nconst HEX = \"0123456789abcdef\";\n/** The two bits of a v4 UUID's variant field are fixed, leaving these four values. */\nconst VARIANT = \"89ab\";\n\nconst buildUuid = (draw: () => number): string => {\n let uuid = \"\";\n for (let i = 0; i < 36; i++) {\n if (i === 8 || i === 13 || i === 18 || i === 23) {\n uuid += \"-\";\n } else if (i === 14) {\n uuid += \"4\";\n } else if (i === 19) {\n uuid += VARIANT[Math.floor(draw() * VARIANT.length)];\n } else {\n uuid += HEX[Math.floor(draw() * HEX.length)];\n }\n }\n return uuid;\n};\n\n/**\n * Fills the view's bytes rather than its elements, so every typed array — including the\n * BigInt ones, whose elements cannot be assigned a number — is handled by one path.\n */\nconst fillBytes = (view: ArrayBufferView, draw: () => number): void => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Math.floor(draw() * 256);\n }\n};\n\n// ---------------------------------------------------------------------------\n// Install\n// ---------------------------------------------------------------------------\n\nlet installed = false;\n\n/**\n * Replaces the worker's sources of randomness with per-request seeded generators. Idempotent,\n * and a no-op failure: a worker that will not let them be replaced still replays, just\n * without randomness virtualisation.\n *\n * Called lazily from the replay branch of the request wrapper rather than at module scope,\n * for the same reasons as the virtual clock: record mode and deployed production workers then\n * pay nothing, and the package declares no `sideEffects`, so a module-scope side effect could\n * legitimately be tree-shaken away by a customer's bundler.\n */\nexport const installVirtualRandom = (): void => {\n if (installed) {\n return;\n }\n installed = true;\n try {\n Math.random = () => openSequence()?.() ?? nativeMathRandom();\n\n if (globalThis.crypto != null) {\n Object.defineProperty(globalThis.crypto, \"randomUUID\", {\n value: (): string => {\n const draw = openSequence();\n if (draw !== undefined) {\n return buildUuid(draw);\n }\n return nativeRandomUUID?.() ?? buildUuid(nativeMathRandom);\n },\n configurable: true,\n writable: true,\n });\n\n Object.defineProperty(globalThis.crypto, \"getRandomValues\", {\n value: <T extends ArrayBufferView>(array: T): T => {\n const draw = openSequence();\n if (draw === undefined) {\n return nativeGetRandomValues?.(array) ?? array;\n }\n fillBytes(array, draw);\n return array;\n },\n configurable: true,\n writable: true,\n });\n }\n } catch (error) {\n warnOnce(\n \"virtual-random\",\n \"Could not install the Meticulous replay random-number generators — ids minted by the app will differ between replays.\",\n error,\n );\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAM,mBAAmB,KAAK,OAAO,KAAK,KAAK;AAC/C,MAAM,mBAAmB,WAAW,QAAQ,YAAY,KAAK,WAAW,OAAO;AAC/E,MAAM,wBAAwB,WAAW,QAAQ,iBAAiB,KAChE,WAAW,OACZ;;;;;;;;;AAUD,MAAM,gBAAgB;;AAGtB,MAAM,eAAe;;AAGrB,MAAM,qBAAqB;AAE3B,MAAM,oBAAoB;;;;;;;AAe1B,MAAM,iCAAiB,IAAI,SAAmD;;;;;;AAW9E,MAAa,sBAAsB,SAAiC;CAClE,IAAI,IAAI;CACR,MAAM,QAAQ,SAAyB;AACrC,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAK,KAAK,WAAW,EAAE;GACvB,IAAI,IAAI,qBAAsB;AAC9B,OAAI,MAAM;AACV,QAAK;AACL,QAAK;AACL,OAAI,MAAM;AACV,QAAK;AACL,QAAK,IAAI;;AAEX,UAAQ,MAAM,KAAK;;CAGrB,IAAI,KAAK,KAAK,IAAI;CAClB,IAAI,KAAK,KAAK,IAAI;CAClB,IAAI,KAAK,KAAK,IAAI;CAClB,IAAI,IAAI;AACR,OAAM,KAAK,KAAK;AAChB,KAAI,KAAK,EACP,OAAM;AAER,OAAM,KAAK,KAAK;AAChB,KAAI,KAAK,EACP,OAAM;AAER,OAAM,KAAK,KAAK;AAChB,KAAI,KAAK,EACP,OAAM;AAGR,cAAa;EACX,MAAM,IAAI,UAAU,KAAK,IAAI;AAC7B,OAAK;AACL,OAAK;AACL,MAAI,IAAI;AACR,OAAK,IAAI;AACT,SAAO;;;AAQX,MAAM,gBACJ,aAAqB,iBACe;CACpC,MAAM,gBAAgB,MAAM;AAC5B,KAAI;AACF,QAAM,kBAAkB,gBAAgB;EACxC,MAAM,yBAAQ,IAAI,OAAO,EAAC;AAC1B,MAAI,SAAS,KACX,QAAO;GAAE,OAAO;GAAmB,MAAM;GAAmB;EAI9D,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,MAAM,gBAAgB,EAAE;AACxD,SAAO;GACL,OAAO,MAAM,KAAK,KAAK,IAAI;GAC3B,MAAM,MAAM,MAAM;GACnB;SACK;AACN,SAAO;GAAE,OAAO;GAAmB,MAAM;GAAmB;WACpD;AACR,QAAM,kBAAkB;;;AAI5B,MAAM,wBAAwD;CAC5D,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,QAAQ,KAAA,KAAa,IAAI,SAAS,SACpC;CAEF,MAAM,WAAW,eAAe,IAAI,IAAI;AACxC,KAAI,aAAa,KAAA,EACf,QAAO;CAET,MAAM,UAA8B;EAClC,mBAAmB,IAAI;EACvB,wCAAwB,IAAI,KAAK;EACjC,wCAAwB,IAAI,KAAK;EACjC,eAAe;EAChB;AACD,gBAAe,IAAI,KAAK,QAAQ;AAChC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BT,MAAM,qBAAiD;CACrD,MAAM,QAAQ,iBAAiB;AAC/B,KAAI,UAAU,KAAA,EACZ;CAGF,IAAI,cAAc;CAClB,IAAI,WAAW;AACf,KAAI,MAAM,gBAAgB,oBAAoB;AAC5C,QAAM;EACN,MAAM,WAAW,cAAc;AAC/B,gBAAc,SAAS;AACvB,aAAW,SAAS;;CAGtB,MAAM,WAAW,MAAM,uBAAuB,IAAI,YAAY;AAC9D,KAAI,aAAa,KAAA,EACf,QAAO;CAET,MAAM,WAAW,MAAM,uBAAuB,IAAI,SAAS,IAAI;CAM/D,MAAM,YAAY,mBAChB,qBAAqB,MAAM,kBAAkB,GAAG,WACjD;AACD,OAAM,uBAAuB,IAAI,aAAa,UAAU;AACxD,OAAM,uBAAuB,IAAI,UAAU,WAAW,EAAE;AACxD,QAAO;;AAOT,MAAM,MAAM;;AAEZ,MAAM,UAAU;AAEhB,MAAM,aAAa,SAA+B;CAChD,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,IACtB,KAAI,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM,MAAM,GAC3C,SAAQ;UACC,MAAM,GACf,SAAQ;UACC,MAAM,GACf,SAAQ,QAAQ,KAAK,MAAM,MAAM,GAAG,EAAe;KAEnD,SAAQ,IAAI,KAAK,MAAM,MAAM,GAAG,GAAW;AAG/C,QAAO;;;;;;AAOT,MAAM,aAAa,MAAuB,SAA6B;CACrE,MAAM,QAAQ,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,WAAW;AAC3E,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,OAAM,KAAK,KAAK,MAAM,MAAM,GAAG,IAAI;;AAQvC,IAAI,YAAY;;;;;;;;;;;AAYhB,MAAa,6BAAmC;AAC9C,KAAI,UACF;AAEF,aAAY;AACZ,KAAI;AACF,OAAK,eAAe,cAAc,IAAI,IAAI,kBAAkB;AAE5D,MAAI,WAAW,UAAU,MAAM;AAC7B,UAAO,eAAe,WAAW,QAAQ,cAAc;IACrD,aAAqB;KACnB,MAAM,OAAO,cAAc;AAC3B,SAAI,SAAS,KAAA,EACX,QAAO,UAAU,KAAK;AAExB,YAAO,oBAAoB,IAAI,UAAU,iBAAiB;;IAE5D,cAAc;IACd,UAAU;IACX,CAAC;AAEF,UAAO,eAAe,WAAW,QAAQ,mBAAmB;IAC1D,QAAmC,UAAgB;KACjD,MAAM,OAAO,cAAc;AAC3B,SAAI,SAAS,KAAA,EACX,QAAO,wBAAwB,MAAM,IAAI;AAE3C,eAAU,OAAO,KAAK;AACtB,YAAO;;IAET,cAAc;IACd,UAAU;IACX,CAAC;;UAEG,OAAO;AACd,WACE,kBACA,yHACA,MACD"}
@@ -1,7 +1,5 @@
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]="ab5bdae8-c370-5978-a6b0-dc92b575118d")}catch(e){}}();
3
1
  import * as path from "node:path";
4
- //#region ../../node_modules/.pnpm/acorn@8.17.0/node_modules/acorn/dist/acorn.mjs
2
+ //#region ../../node_modules/.pnpm/acorn@8.18.0/node_modules/acorn/dist/acorn.mjs
5
3
  var astralIdentifierCodes = [
6
4
  509,
7
5
  0,
@@ -1230,6 +1228,7 @@ var defaultOptions = {
1230
1228
  allowHashBang: false,
1231
1229
  checkPrivateFields: true,
1232
1230
  locations: false,
1231
+ startLocation: null,
1233
1232
  onToken: null,
1234
1233
  onComment: null,
1235
1234
  ranges: false,
@@ -1295,14 +1294,15 @@ var Parser = function Parser(options, input, startPos) {
1295
1294
  this.reservedWordsStrictBind = wordsRegexp(reservedStrict + " " + reservedWords.strictBind);
1296
1295
  this.input = String(input);
1297
1296
  this.containsEsc = false;
1298
- if (startPos) {
1299
- this.pos = startPos;
1297
+ this.pos = startPos || 0;
1298
+ this.curLine = 1;
1299
+ if (options.startLocation) {
1300
+ this.lineStart = this.pos - options.startLocation.column;
1301
+ this.curLine = options.startLocation.line;
1302
+ } else if (startPos) {
1300
1303
  this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1;
1301
- this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length;
1302
- } else {
1303
- this.pos = this.lineStart = 0;
1304
- this.curLine = 1;
1305
- }
1304
+ if (this.options.locations) this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length;
1305
+ } else this.lineStart = 0;
1306
1306
  this.type = types$1.eof;
1307
1307
  this.value = null;
1308
1308
  this.start = this.end = this.pos;
@@ -2824,7 +2824,7 @@ pp$5.parseMaybeUnary = function(refDestructuringErrors, sawUnary, incDec, forIni
2824
2824
  expr = this.finishNode(node$1, "UpdateExpression");
2825
2825
  }
2826
2826
  }
2827
- if (!incDec && this.eat(types$1.starstar)) if (sawUnary) this.unexpected(this.lastTokStart);
2827
+ if (!incDec && !(expr.type === "ArrowFunctionExpression" && expr.start === startPos) && this.eat(types$1.starstar)) if (sawUnary) this.unexpected(this.lastTokStart);
2828
2828
  else return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false, false, forInit), "**", false);
2829
2829
  else return expr;
2830
2830
  };
@@ -5084,7 +5084,7 @@ pp.readWord = function() {
5084
5084
  };
5085
5085
  Parser.acorn = {
5086
5086
  Parser,
5087
- version: "8.17.0",
5087
+ version: "8.18.0",
5088
5088
  defaultOptions,
5089
5089
  Position,
5090
5090
  SourceLocation,
@@ -6766,5 +6766,4 @@ const toRelativePath = (filePath, root) => path.relative(root, filePath).split(p
6766
6766
  //#endregion
6767
6767
  export { meticulousCoverage };
6768
6768
 
6769
- //# sourceMappingURL=index.js.map
6770
- //# debugId=ab5bdae8-c370-5978-a6b0-dc92b575118d
6769
+ //# sourceMappingURL=index.js.map