@alwaysmeticulous/backend-recorder-workerd 2.342.0 → 2.346.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 (43) hide show
  1. package/README.md +9 -3
  2. package/dist/agent-swarm-testing-fetch.js +40 -0
  3. package/dist/agent-swarm-testing-fetch.js.map +1 -0
  4. package/dist/base64.js +25 -0
  5. package/dist/base64.js.map +1 -0
  6. package/dist/body-capture.d.ts +18 -3
  7. package/dist/body-capture.js +68 -24
  8. package/dist/body-capture.js.map +1 -1
  9. package/dist/body-encoding.d.ts +49 -0
  10. package/dist/body-encoding.js +68 -0
  11. package/dist/body-encoding.js.map +1 -0
  12. package/dist/capture-buffer.js +3 -3
  13. package/dist/capture-buffer.js.map +1 -1
  14. package/dist/captured-result-codec.js +1 -15
  15. package/dist/captured-result-codec.js.map +1 -1
  16. package/dist/context.js +1 -1
  17. package/dist/context.js.map +1 -1
  18. package/dist/fetch-patch.js +4 -1
  19. package/dist/fetch-patch.js.map +1 -1
  20. package/dist/index.d.ts +13 -3
  21. package/dist/index.js +13 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/kv-capture.js +1 -1
  24. package/dist/kv-capture.js.map +1 -1
  25. package/dist/outbound-capture.js +2 -2
  26. package/dist/outbound-capture.js.map +1 -1
  27. package/dist/postgres/capture.js.map +1 -1
  28. package/dist/protocol.d.ts +60 -8
  29. package/dist/protocol.js +15 -7
  30. package/dist/protocol.js.map +1 -1
  31. package/dist/replay-fetch.js +13 -2
  32. package/dist/replay-fetch.js.map +1 -1
  33. package/dist/sidecar-client.js +32 -2
  34. package/dist/sidecar-client.js.map +1 -1
  35. package/dist/{replay-sidecar-url.js → sidecar-url.js} +9 -8
  36. package/dist/sidecar-url.js.map +1 -0
  37. package/dist/spans/span-builder.js +2 -0
  38. package/dist/spans/span-builder.js.map +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/with-meticulous.js +0 -0
  41. package/dist/with-meticulous.js.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/replay-sidecar-url.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"with-meticulous.js","names":[],"sources":["../src/with-meticulous.ts"],"sourcesContent":["import { installBindingPatch } from \"./binding-patch\";\nimport { CaptureBuffer } from \"./capture-buffer\";\nimport {\n type RequestCaptureContext,\n type RequestReplayContext,\n requestCaptureContext,\n} from \"./context\";\nimport { runReplayWithCoverage } from \"./coverage/replay-coverage\";\nimport { installFetchPatch } from \"./fetch-patch\";\nimport { isHealthProbeRequest } from \"./health-probe\";\nimport { installKvPatch } from \"./kv-patch\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport { headersToRecord } from \"./outbound-capture\";\nimport {\n FRONTEND_SESSION_ID_HEADER,\n type InboundRequestEvent,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n VIRTUAL_TIME_HEADER,\n} from \"./protocol\";\nimport {\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\nimport { publishSessionIdOnResponse } from \"./publish-session-id\";\nimport { publishWorkerdShimVersionOnResponse } from \"./publish-shim-version\";\nimport { installReplayLogTagging } from \"./replay-log-tagging\";\nimport { parseReplaySidecarUrl } from \"./replay-sidecar-url\";\nimport { getReplaySessionInfo } from \"./sidecar-client\";\nimport {\n resolveSidecarTransport,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\nimport { randomHex } from \"./spans/span-builder\";\nimport { installVirtualClock } from \"./virtual-clock\";\nimport { installVirtualRandom } from \"./virtual-random\";\n\n/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */\nexport interface MeticulousExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface WithMeticulousOptions {\n /**\n * Overrides the sidecar service binding; defaults to the `METICULOUS_SIDECAR` binding on\n * `env`. This is how a **deployed** Worker records — see {@link SidecarTransport}.\n */\n sidecarBinding?: SidecarFetcher;\n /** Overrides the sidecar origin; defaults to the METICULOUS_SIDECAR_URL var/binding. */\n sidecarUrl?: string;\n /**\n * Binding names to leave unrecorded, on top of the defaults (`ASSETS`, `__STATIC_CONTENT`,\n * `METICULOUS_SIDECAR`). Use this for a binding whose traffic is high-volume or binary. Applies\n * to KV namespaces as well as `fetch`-shaped bindings.\n */\n skipBindings?: readonly string[];\n /**\n * Whether to mint a session id for a document navigation that carries none, so the\n * server-side render is recorded under the same session as the page it produces. On by\n * default; set `false` (or the `METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS` var to\n * `\"false\"`) to opt out. See provisional-session-id.ts.\n */\n mintProvisionalSessionIds?: boolean;\n}\n\n/** What the mode-agnostic core needs from whichever handler shape wraps it. */\nexport interface MeticulousInvocation {\n request: Request;\n env: unknown;\n ctx: MeticulousExecutionContext;\n invokeHandler: () => Response | Promise<Response>;\n}\n\n/**\n * Records, or replays, one inbound request. Shared by `withMeticulous` (an ES-module Worker's\n * `fetch`) and `withMeticulousPagesFunction` (a Pages Functions `onRequest`), which differ only\n * in how they get at the request, the env and `waitUntil`.\n */\nexport const runWithMeticulous = async (\n { request, env, ctx, invokeHandler }: MeticulousInvocation,\n options: WithMeticulousOptions | undefined,\n): Promise<Response> => {\n let transport: SidecarTransport | undefined;\n let replayContext: RequestReplayContext | undefined;\n try {\n replayContext = await resolveReplayContext(request, ctx);\n transport =\n replayContext === undefined\n ? resolveSidecarTransport(options, env)\n : undefined;\n if (replayContext !== undefined || transport !== undefined) {\n installFetchPatch();\n }\n if (transport !== undefined) {\n // Lazily, inside the request path, and never at module scope: the package declares no\n // `sideEffects` and is built with `platform: \"neutral\"`, so a module-scope side effect can\n // legitimately be tree-shaken by a customer's bundler. Re-run per request because `env` is\n // the only handle on the binding instances.\n //\n // Record mode only. Binding and KV calls are recorded as their own technologies\n // (`workerd-binding`, `workerd-kv`) and the replay sidecar's store holds only\n // `workerd-fetch` spans, so there is nothing to serve them from — and patching would only\n // route them into the capture tee, which POSTs to an events route a replay sidecar 404s.\n //\n // `skipInstances` keeps the sidecar's own binding out of the recording by identity rather\n // than by name, so it holds however the customer named it or passed it in.\n const patchOptions = {\n ...(options?.skipBindings !== undefined\n ? { skipBindings: options.skipBindings }\n : {}),\n ...(transport.kind === \"binding\"\n ? { skipInstances: [transport.instance] }\n : {}),\n };\n installBindingPatch(env, patchOptions);\n installKvPatch(env, patchOptions);\n }\n } catch (error) {\n warnOnce(\n \"shim-init\",\n \"Failed to initialize the Meticulous backend recorder shim — recording disabled.\",\n error,\n );\n replayContext = undefined;\n transport = undefined;\n }\n\n if (replayContext !== undefined) {\n // Workerd has no process.stdout/process.stderr seam like Node. Patch its\n // console methods instead; each call resolves this request's replay id from\n // the AsyncLocalStorage entered below.\n installReplayLogTagging();\n // Install the clock before any app code runs, so a module that reads Date.now() at call time\n // (the firebase auth libraries do) sees the session's frozen instant.\n installVirtualClock();\n // Likewise the random generators: an id the app mints during SSR (a guest id, a request id)\n // otherwise differs between a base and a head replay, and every rendered page carrying it\n // diffs forever.\n installVirtualRandom();\n // No inbound reporting in replay mode: there is no exporter behind the replay sidecar, and\n // the events route 404s there.\n const replayResponse = await runReplayWithCoverage(\n replayContext,\n invokeHandler,\n );\n return publishWorkerdShimVersionOnResponse(replayResponse);\n }\n\n if (transport === undefined) {\n return invokeHandler();\n }\n\n // Never enter the capture context for a health probe, so neither the inbound request nor\n // anything it fans out to is recorded. The fetch, binding and KV patches are all\n // pass-throughs without a context, so this covers the whole subtree in one place.\n if (\n isHealthProbeRequest(request.method, request.url, (name) =>\n request.headers.get(name),\n )\n ) {\n return invokeHandler();\n }\n\n const captureCtx = buildCaptureContext(\n request,\n transport,\n ctx,\n resolveMintProvisionalSessionIds(options, env),\n );\n const mintedSessionId =\n captureCtx.sessionIdOrigin === \"backend\"\n ? captureCtx.frontendSessionId\n : undefined;\n const startTimeMs = Date.now();\n let requestHeaders: Record<string, string[]> = {};\n let method = \"GET\";\n let url = \"\";\n try {\n requestHeaders = headersToRecord(request.headers);\n method = request.method;\n url = request.url;\n } catch (error) {\n warnOnce(\n \"inbound-capture\",\n \"Failed to capture the inbound request.\",\n error,\n );\n }\n\n const reportInbound = (\n outcome:\n | { statusCode: number; responseHeaders: Record<string, string[]> }\n | { error: string },\n ): void => {\n try {\n const event: InboundRequestEvent = {\n kind: \"inbound\",\n requestId: captureCtx.requestId,\n ...(captureCtx.frontendSessionId !== undefined\n ? { frontendSessionId: captureCtx.frontendSessionId }\n : {}),\n ...(captureCtx.sessionIdOrigin !== undefined\n ? { sessionIdOrigin: captureCtx.sessionIdOrigin }\n : {}),\n traceId: captureCtx.traceId,\n serverSpanId: captureCtx.serverSpanId,\n method,\n url,\n requestHeaders,\n startTimeMs,\n endTimeMs: Date.now(),\n ...outcome,\n };\n captureCtx.buffer.add(event);\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n };\n\n let response: Response;\n try {\n response = await requestCaptureContext.run(captureCtx, invokeHandler);\n } catch (error) {\n reportInbound({ error: String(error) });\n closeBuffer(captureCtx);\n throw error;\n }\n\n // Ahead of reporting, so what we record as the response headers is what the browser is\n // actually served.\n if (mintedSessionId !== undefined) {\n response = publishSessionIdOnResponse(response, mintedSessionId);\n }\n response = publishWorkerdShimVersionOnResponse(response);\n\n try {\n reportInbound({\n statusCode: response.status,\n responseHeaders: headersToRecord(response.headers),\n });\n } catch (error) {\n warnOnce(\"inbound-report\", \"Failed to report the inbound request.\", error);\n }\n closeBuffer(captureCtx);\n return response;\n};\n\n/**\n * Hands the request's single report to `waitUntil`. `close()` drains the outstanding body reads\n * first, which is why there is one `waitUntil` per request rather than one per captured call.\n */\nconst closeBuffer = (ctx: RequestCaptureContext): void => {\n try {\n ctx.waitUntil(ctx.buffer.close());\n } catch (error) {\n warnOnce(\n \"buffer-close\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n};\n\n/**\n * Whether this request should be replayed, and the context to serve it under.\n *\n * Both replay identity ids and a usable sidecar URL must be present: mocks are indexed per session\n * and consumed per replay. The sidecar is consulted once per (sidecar, session) per isolate to\n * confirm it holds mocks and to learn the clock anchor — a sidecar that does not recognise the\n * session, or the route, means replay is unavailable and the request runs normally.\n */\nconst resolveReplayContext = async (\n request: Request,\n ctx: MeticulousExecutionContext,\n): Promise<RequestReplayContext | undefined> => {\n let sidecarUrl: string | undefined;\n let frontendSessionId: string | undefined;\n let replayId: string | undefined;\n let virtualTimeMs: number | undefined;\n try {\n sidecarUrl = parseReplaySidecarUrl(\n request.headers.get(REPLAY_SIDECAR_URL_HEADER),\n );\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n replayId = request.headers.get(REPLAY_ID_HEADER) ?? undefined;\n virtualTimeMs = parseVirtualTimeMs(\n request.headers.get(VIRTUAL_TIME_HEADER),\n );\n } catch {\n return undefined;\n }\n if (\n sidecarUrl === undefined ||\n frontendSessionId === undefined ||\n replayId === undefined\n ) {\n return undefined;\n }\n\n const info = await getCachedReplaySessionInfo(sidecarUrl, frontendSessionId);\n if (info === null) {\n return undefined;\n }\n\n return {\n mode: \"replay\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n replayId,\n sidecarUrl,\n clockAnchorMs: info.clockAnchorMs,\n ...(virtualTimeMs !== undefined ? { virtualTimeMs } : {}),\n waitUntil: buildWaitUntil(ctx),\n };\n};\n\n// Caches the promise, not the result, so concurrent first requests share one round trip.\n// Bounded so a stream of unrecognised session ids cannot grow it without limit.\nconst MAX_CACHED_SESSIONS = 50;\nconst replaySessionInfoCache = new Map<\n string,\n Promise<{ clockAnchorMs: number | undefined } | null>\n>();\n\nconst getCachedReplaySessionInfo = (\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<{ clockAnchorMs: number | undefined } | null> => {\n const key = `${sidecarUrl}\u0000${frontendSessionId}`;\n const cached = replaySessionInfoCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n const pending = getReplaySessionInfo(\n getOriginalFetch(),\n sidecarUrl,\n frontendSessionId,\n ).then((result) => {\n if (result.outcome === \"unreachable\") {\n // Only settled answers are worth keeping. Caching a timeout or transport blip would\n // disable mocking for this session for the life of the isolate, quietly sending every\n // later outbound call to the real service — the failure this path exists to prevent.\n // Dropping the entry costs one extra handshake and lets the next request recover.\n replaySessionInfoCache.delete(key);\n return null;\n }\n return result.outcome === \"found\"\n ? { clockAnchorMs: result.clockAnchorMs }\n : null;\n });\n if (replaySessionInfoCache.size >= MAX_CACHED_SESSIONS) {\n const oldest = replaySessionInfoCache.keys().next();\n if (!oldest.done) {\n replaySessionInfoCache.delete(oldest.value);\n }\n }\n replaySessionInfoCache.set(key, pending);\n return pending;\n};\n\nconst buildCaptureContext = (\n request: Request,\n transport: SidecarTransport,\n ctx: MeticulousExecutionContext,\n mintProvisionalSessionIds: boolean,\n): RequestCaptureContext => {\n let frontendSessionId: string | undefined;\n let sessionIdOrigin: \"backend\" | undefined;\n try {\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n // A top-level document navigation cannot carry the header, so for one we mint the id\n // ourselves and hand it to the page on the response — otherwise the server-side render this\n // request is about is recorded against no session at all. Every outbound, binding, KV and\n // postgres event reads the id back off this context, so putting it here attributes the whole\n // request. See provisional-session-id.ts.\n if (\n mintProvisionalSessionIds &&\n frontendSessionId === undefined &&\n isProvisionalSessionIdCandidate(request.method, (name) =>\n request.headers.get(name),\n )\n ) {\n frontendSessionId = mintProvisionalSessionId();\n sessionIdOrigin = \"backend\";\n }\n } catch {\n frontendSessionId = undefined;\n sessionIdOrigin = undefined;\n }\n const waitUntil = buildWaitUntil(ctx);\n return {\n mode: \"record\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n ...(sessionIdOrigin !== undefined ? { sessionIdOrigin } : {}),\n transport,\n buffer: new CaptureBuffer(transport, waitUntil),\n traceId: randomHex(16),\n serverSpanId: randomHex(8),\n waitUntil,\n };\n};\n\n/**\n * On by default: the render it attributes — the page's very first — is otherwise recorded\n * against no session at all, and the only thing that reaches a replay for those spans is\n * ingestion's time-window guess.\n *\n * Opting out is a worker var rather than a host environment variable for the same reason the\n * sidecar URL is: workerd cannot see the host's environment.\n */\nconst resolveMintProvisionalSessionIds = (\n options: WithMeticulousOptions | undefined,\n env: unknown,\n): boolean => {\n if (options?.mintProvisionalSessionIds !== undefined) {\n return options.mintProvisionalSessionIds;\n }\n const fromEnv =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)[\n \"METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS\"\n ]\n : undefined;\n return fromEnv !== \"false\";\n};\n\nconst buildWaitUntil =\n (ctx: MeticulousExecutionContext) =>\n (promise: Promise<unknown>): void => {\n try {\n ctx.waitUntil(promise);\n } catch (error) {\n warnOnce(\n \"wait-until\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n };\n"],"mappings":"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"}
1
+ {"version":3,"file":"with-meticulous.js","names":[],"sources":["../src/with-meticulous.ts"],"sourcesContent":["import { installBindingPatch } from \"./binding-patch\";\nimport { CaptureBuffer } from \"./capture-buffer\";\nimport {\n type RequestAgentSwarmTestingContext,\n type RequestCaptureContext,\n type RequestReplayContext,\n requestCaptureContext,\n} from \"./context\";\nimport { runReplayWithCoverage } from \"./coverage/replay-coverage\";\nimport { installFetchPatch } from \"./fetch-patch\";\nimport { isHealthProbeRequest } from \"./health-probe\";\nimport { installKvPatch } from \"./kv-patch\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport { headersToRecord } from \"./outbound-capture\";\nimport {\n AGENT_SWARM_TESTING_RUN_ID_ENV_KEY,\n AGENT_SWARM_TESTING_SIDECAR_URL_ENV_KEY,\n FRONTEND_SESSION_ID_HEADER,\n type InboundRequestEvent,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n VIRTUAL_TIME_HEADER,\n} from \"./protocol\";\nimport {\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\nimport { publishSessionIdOnResponse } from \"./publish-session-id\";\nimport { publishWorkerdShimVersionOnResponse } from \"./publish-shim-version\";\nimport { installReplayLogTagging } from \"./replay-log-tagging\";\nimport { parseSidecarUrl } from \"./sidecar-url\";\nimport { getReplaySessionInfo } from \"./sidecar-client\";\nimport {\n resolveSidecarTransport,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\nimport { randomHex } from \"./spans/span-builder\";\nimport { installVirtualClock } from \"./virtual-clock\";\nimport { installVirtualRandom } from \"./virtual-random\";\n\n/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */\nexport interface MeticulousExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface WithMeticulousOptions {\n /**\n * Overrides the sidecar service binding; defaults to the `METICULOUS_SIDECAR` binding on\n * `env`. This is how a **deployed** Worker records — see {@link SidecarTransport}.\n */\n sidecarBinding?: SidecarFetcher;\n /** Overrides the sidecar origin; defaults to the METICULOUS_SIDECAR_URL var/binding. */\n sidecarUrl?: string;\n /**\n * Binding names to leave unrecorded, on top of the defaults (`ASSETS`, `__STATIC_CONTENT`,\n * `METICULOUS_SIDECAR`). Use this for a binding whose traffic is high-volume or binary. Applies\n * to KV namespaces as well as `fetch`-shaped bindings.\n */\n skipBindings?: readonly string[];\n /**\n * Whether to mint a session id for a document navigation that carries none, so the\n * server-side render is recorded under the same session as the page it produces. On by\n * default; set `false` (or the `METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS` var to\n * `\"false\"`) to opt out. See provisional-session-id.ts.\n */\n mintProvisionalSessionIds?: boolean;\n}\n\n/** What the mode-agnostic core needs from whichever handler shape wraps it. */\nexport interface MeticulousInvocation {\n request: Request;\n env: unknown;\n ctx: MeticulousExecutionContext;\n invokeHandler: () => Response | Promise<Response>;\n}\n\n/**\n * Records, replays, or runs agent-swarm testing on, one inbound request. Shared by\n * `withMeticulous` (an ES-module Worker's `fetch`) and `withMeticulousPagesFunction` (a Pages\n * Functions `onRequest`), which differ only in how they get at the request, the env and\n * `waitUntil`.\n *\n * Replay takes precedence over agent-swarm testing, and either over recording: nothing but\n * Meticulous's own runners emit the replay header, so a header present is a deliberate\n * choice, and the sidecar env vars a stale image can carry are not.\n */\nexport const runWithMeticulous = async (\n { request, env, ctx, invokeHandler }: MeticulousInvocation,\n options: WithMeticulousOptions | undefined,\n): Promise<Response> => {\n let transport: SidecarTransport | undefined;\n let replayContext: RequestReplayContext | undefined;\n let agentSwarmTestingContext: RequestAgentSwarmTestingContext | undefined;\n try {\n replayContext = await resolveReplayContext(request, ctx);\n if (replayContext === undefined) {\n agentSwarmTestingContext = resolveAgentSwarmTestingContext(\n request,\n env,\n ctx,\n );\n }\n transport =\n replayContext === undefined && agentSwarmTestingContext === undefined\n ? resolveSidecarTransport(options, env)\n : undefined;\n if (\n replayContext !== undefined ||\n agentSwarmTestingContext !== undefined ||\n transport !== undefined\n ) {\n installFetchPatch();\n }\n if (transport !== undefined) {\n // Lazily, inside the request path, and never at module scope: the package declares no\n // `sideEffects` and is built with `platform: \"neutral\"`, so a module-scope side effect can\n // legitimately be tree-shaken by a customer's bundler. Re-run per request because `env` is\n // the only handle on the binding instances.\n //\n // Record mode only. Binding and KV calls are recorded as their own technologies\n // (`workerd-binding`, `workerd-kv`) and the replay sidecar's store holds only\n // `workerd-fetch` spans, so there is nothing to serve them from — and patching would only\n // route them into the capture tee, which POSTs to an events route a replay sidecar 404s.\n //\n // `skipInstances` keeps the sidecar's own binding out of the recording by identity rather\n // than by name, so it holds however the customer named it or passed it in.\n const patchOptions = {\n ...(options?.skipBindings !== undefined\n ? { skipBindings: options.skipBindings }\n : {}),\n ...(transport.kind === \"binding\"\n ? { skipInstances: [transport.instance] }\n : {}),\n };\n installBindingPatch(env, patchOptions);\n installKvPatch(env, patchOptions);\n }\n } catch (error) {\n warnOnce(\n \"shim-init\",\n \"Failed to initialize the Meticulous backend recorder shim — recording disabled.\",\n error,\n );\n replayContext = undefined;\n agentSwarmTestingContext = undefined;\n transport = undefined;\n }\n\n if (replayContext !== undefined) {\n // Workerd has no process.stdout/process.stderr seam like Node. Patch its\n // console methods instead; each call resolves this request's replay id from\n // the AsyncLocalStorage entered below.\n installReplayLogTagging();\n // Install the clock before any app code runs, so a module that reads Date.now() at call time\n // (the firebase auth libraries do) sees the session's frozen instant.\n installVirtualClock();\n // Likewise the random generators: an id the app mints during SSR (a guest id, a request id)\n // otherwise differs between a base and a head replay, and every rendered page carrying it\n // diffs forever.\n installVirtualRandom();\n // No inbound reporting in replay mode: there is no exporter behind the replay sidecar, and\n // the events route 404s there.\n const replayResponse = await runReplayWithCoverage(\n replayContext,\n invokeHandler,\n );\n return publishWorkerdShimVersionOnResponse(replayResponse);\n }\n\n if (agentSwarmTestingContext !== undefined) {\n const testingResponse = await requestCaptureContext.run(\n agentSwarmTestingContext,\n invokeHandler,\n );\n return publishWorkerdShimVersionOnResponse(testingResponse);\n }\n\n if (transport === undefined) {\n return invokeHandler();\n }\n\n // Never enter the capture context for a health probe, so neither the inbound request nor\n // anything it fans out to is recorded. The fetch, binding and KV patches are all\n // pass-throughs without a context, so this covers the whole subtree in one place.\n if (\n isHealthProbeRequest(request.method, request.url, (name) =>\n request.headers.get(name),\n )\n ) {\n return invokeHandler();\n }\n\n const captureCtx = buildCaptureContext(\n request,\n transport,\n ctx,\n resolveMintProvisionalSessionIds(options, env),\n );\n const mintedSessionId =\n captureCtx.sessionIdOrigin === \"backend\"\n ? captureCtx.frontendSessionId\n : undefined;\n const startTimeMs = Date.now();\n let requestHeaders: Record<string, string[]> = {};\n let method = \"GET\";\n let url = \"\";\n try {\n requestHeaders = headersToRecord(request.headers);\n method = request.method;\n url = request.url;\n } catch (error) {\n warnOnce(\n \"inbound-capture\",\n \"Failed to capture the inbound request.\",\n error,\n );\n }\n\n const reportInbound = (\n outcome:\n | { statusCode: number; responseHeaders: Record<string, string[]> }\n | { error: string },\n ): void => {\n try {\n const event: InboundRequestEvent = {\n kind: \"inbound\",\n requestId: captureCtx.requestId,\n ...(captureCtx.frontendSessionId !== undefined\n ? { frontendSessionId: captureCtx.frontendSessionId }\n : {}),\n ...(captureCtx.sessionIdOrigin !== undefined\n ? { sessionIdOrigin: captureCtx.sessionIdOrigin }\n : {}),\n traceId: captureCtx.traceId,\n serverSpanId: captureCtx.serverSpanId,\n method,\n url,\n requestHeaders,\n startTimeMs,\n endTimeMs: Date.now(),\n ...outcome,\n };\n captureCtx.buffer.add(event);\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n };\n\n let response: Response;\n try {\n response = await requestCaptureContext.run(captureCtx, invokeHandler);\n } catch (error) {\n reportInbound({ error: String(error) });\n closeBuffer(captureCtx);\n throw error;\n }\n\n // Ahead of reporting, so what we record as the response headers is what the browser is\n // actually served.\n if (mintedSessionId !== undefined) {\n response = publishSessionIdOnResponse(response, mintedSessionId);\n }\n response = publishWorkerdShimVersionOnResponse(response);\n\n try {\n reportInbound({\n statusCode: response.status,\n responseHeaders: headersToRecord(response.headers),\n });\n } catch (error) {\n warnOnce(\"inbound-report\", \"Failed to report the inbound request.\", error);\n }\n closeBuffer(captureCtx);\n return response;\n};\n\n/**\n * Hands the request's single report to `waitUntil`. `close()` drains the outstanding body reads\n * first, which is why there is one `waitUntil` per request rather than one per captured call.\n */\nconst closeBuffer = (ctx: RequestCaptureContext): void => {\n try {\n ctx.waitUntil(ctx.buffer.close());\n } catch (error) {\n warnOnce(\n \"buffer-close\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n};\n\n/**\n * Whether this request should be replayed, and the context to serve it under.\n *\n * Both replay identity ids and a usable sidecar URL must be present: mocks are indexed per session\n * and consumed per replay. The sidecar is consulted once per (sidecar, session) per isolate to\n * confirm it holds mocks and to learn the clock anchor — a sidecar that does not recognise the\n * session, or the route, means replay is unavailable and the request runs normally.\n */\nconst resolveReplayContext = async (\n request: Request,\n ctx: MeticulousExecutionContext,\n): Promise<RequestReplayContext | undefined> => {\n let sidecarUrl: string | undefined;\n let frontendSessionId: string | undefined;\n let replayId: string | undefined;\n let virtualTimeMs: number | undefined;\n try {\n sidecarUrl = parseSidecarUrl(\n request.headers.get(REPLAY_SIDECAR_URL_HEADER),\n );\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n replayId = request.headers.get(REPLAY_ID_HEADER) ?? undefined;\n virtualTimeMs = parseVirtualTimeMs(\n request.headers.get(VIRTUAL_TIME_HEADER),\n );\n } catch {\n return undefined;\n }\n if (\n sidecarUrl === undefined ||\n frontendSessionId === undefined ||\n replayId === undefined\n ) {\n return undefined;\n }\n\n const info = await getCachedReplaySessionInfo(sidecarUrl, frontendSessionId);\n if (info === null) {\n return undefined;\n }\n\n return {\n mode: \"replay\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n replayId,\n sidecarUrl,\n clockAnchorMs: info.clockAnchorMs,\n ...(virtualTimeMs !== undefined ? { virtualTimeMs } : {}),\n waitUntil: buildWaitUntil(ctx),\n };\n};\n\n/**\n * Whether this deployment is running agent-swarm testing, and the context to serve requests\n * under.\n *\n * Activates on the two deployment env vars — a validated interceptor origin plus a run id —\n * neither of which changes per request, which is why env vars work here where replay needs\n * headers (replay is per-session, and workerd cannot see container environment variables;\n * these vars are set on the container by whoever deploys it for a swarm run). There is no\n * session handshake with the interceptor: the mode is best-effort by design, so nothing is\n * gated on the interceptor being reachable.\n */\nconst resolveAgentSwarmTestingContext = (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n): RequestAgentSwarmTestingContext | undefined => {\n let sidecarUrl: string | undefined;\n let runId: string | undefined;\n try {\n const envRecord =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)\n : {};\n const rawSidecarUrl = envRecord[AGENT_SWARM_TESTING_SIDECAR_URL_ENV_KEY];\n sidecarUrl =\n typeof rawSidecarUrl === \"string\"\n ? parseSidecarUrl(rawSidecarUrl)\n : undefined;\n const rawRunId = envRecord[AGENT_SWARM_TESTING_RUN_ID_ENV_KEY];\n runId =\n typeof rawRunId === \"string\" && rawRunId !== \"\" ? rawRunId : undefined;\n } catch {\n return undefined;\n }\n if (sidecarUrl === undefined || runId === undefined) {\n return undefined;\n }\n\n return {\n mode: \"agent-swarm-testing\",\n requestId: crypto.randomUUID(),\n runId,\n frontendSessionId:\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined,\n sidecarUrl,\n waitUntil: buildWaitUntil(ctx),\n };\n};\n\n// Caches the promise, not the result, so concurrent first requests share one round trip.\n// Bounded so a stream of unrecognised session ids cannot grow it without limit.\nconst MAX_CACHED_SESSIONS = 50;\nconst replaySessionInfoCache = new Map<\n string,\n Promise<{ clockAnchorMs: number | undefined } | null>\n>();\n\nconst getCachedReplaySessionInfo = (\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<{ clockAnchorMs: number | undefined } | null> => {\n const key = `${sidecarUrl}\u0000${frontendSessionId}`;\n const cached = replaySessionInfoCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n const pending = getReplaySessionInfo(\n getOriginalFetch(),\n sidecarUrl,\n frontendSessionId,\n ).then((result) => {\n if (result.outcome === \"unreachable\") {\n // Only settled answers are worth keeping. Caching a timeout or transport blip would\n // disable mocking for this session for the life of the isolate, quietly sending every\n // later outbound call to the real service — the failure this path exists to prevent.\n // Dropping the entry costs one extra handshake and lets the next request recover.\n replaySessionInfoCache.delete(key);\n return null;\n }\n return result.outcome === \"found\"\n ? { clockAnchorMs: result.clockAnchorMs }\n : null;\n });\n if (replaySessionInfoCache.size >= MAX_CACHED_SESSIONS) {\n const oldest = replaySessionInfoCache.keys().next();\n if (!oldest.done) {\n replaySessionInfoCache.delete(oldest.value);\n }\n }\n replaySessionInfoCache.set(key, pending);\n return pending;\n};\n\nconst buildCaptureContext = (\n request: Request,\n transport: SidecarTransport,\n ctx: MeticulousExecutionContext,\n mintProvisionalSessionIds: boolean,\n): RequestCaptureContext => {\n let frontendSessionId: string | undefined;\n let sessionIdOrigin: \"backend\" | undefined;\n try {\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n // A top-level document navigation cannot carry the header, so for one we mint the id\n // ourselves and hand it to the page on the response — otherwise the server-side render this\n // request is about is recorded against no session at all. Every outbound, binding, KV and\n // postgres event reads the id back off this context, so putting it here attributes the whole\n // request. See provisional-session-id.ts.\n if (\n mintProvisionalSessionIds &&\n frontendSessionId === undefined &&\n isProvisionalSessionIdCandidate(request.method, (name) =>\n request.headers.get(name),\n )\n ) {\n frontendSessionId = mintProvisionalSessionId();\n sessionIdOrigin = \"backend\";\n }\n } catch {\n frontendSessionId = undefined;\n sessionIdOrigin = undefined;\n }\n const waitUntil = buildWaitUntil(ctx);\n return {\n mode: \"record\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n ...(sessionIdOrigin !== undefined ? { sessionIdOrigin } : {}),\n transport,\n buffer: new CaptureBuffer(transport, waitUntil),\n traceId: randomHex(16),\n serverSpanId: randomHex(8),\n waitUntil,\n };\n};\n\n/**\n * On by default: the render it attributes — the page's very first — is otherwise recorded\n * against no session at all, and the only thing that reaches a replay for those spans is\n * ingestion's time-window guess.\n *\n * Opting out is a worker var rather than a host environment variable for the same reason the\n * sidecar URL is: workerd cannot see the host's environment.\n */\nconst resolveMintProvisionalSessionIds = (\n options: WithMeticulousOptions | undefined,\n env: unknown,\n): boolean => {\n if (options?.mintProvisionalSessionIds !== undefined) {\n return options.mintProvisionalSessionIds;\n }\n const fromEnv =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)[\n \"METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS\"\n ]\n : undefined;\n return fromEnv !== \"false\";\n};\n\nconst buildWaitUntil =\n (ctx: MeticulousExecutionContext) =>\n (promise: Promise<unknown>): void => {\n try {\n ctx.waitUntil(promise);\n } catch (error) {\n warnOnce(\n \"wait-until\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n };\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwaysmeticulous/backend-recorder-workerd",
3
- "version": "2.342.0",
3
+ "version": "2.346.0",
4
4
  "type": "module",
5
5
  "description": "Meticulous backend recorder shim for Cloudflare Workers (workerd). Reports inbound and outbound HTTP capture events to a local Meticulous recorder sidecar during development recording.",
6
6
  "license": "ISC",
@@ -1 +0,0 @@
1
- {"version":3,"file":"replay-sidecar-url.js","names":[],"sources":["../src/replay-sidecar-url.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\n\n/**\n * Validation for the replay sidecar origin carried on inbound requests.\n *\n * The value arrives in a request header, so in principle anything that can reach the worker\n * can propose one — and the shim POSTs outbound request bodies to whatever it accepts. The\n * guard therefore only honours an origin that could plausibly be a Meticulous replay\n * sidecar running alongside the app: plaintext HTTP on a loopback, docker-gateway or\n * private-network host.\n *\n * `https:` is rejected deliberately. A sidecar reachable over loopback or a pod network has\n * no TLS to offer, so permitting `https:` would buy nothing except a way for a forged header\n * to exfiltrate request bodies to an arbitrary internet host.\n */\n\nconst MAX_URL_LENGTH = 2048;\n\nconst ALLOWED_HOSTNAMES = new Set([\n \"localhost\",\n // Docker Desktop and Podman expose the host to containers under these names.\n \"host.docker.internal\",\n \"host.containers.internal\",\n]);\n\nexport const parseReplaySidecarUrl = (\n raw: string | null | undefined,\n): string | undefined => {\n if (typeof raw !== \"string\" || raw.length === 0) {\n return undefined;\n }\n if (raw.length > MAX_URL_LENGTH) {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an implausibly long Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n warnOnce(\n \"replay-sidecar-url\",\n \"Ignoring an unparseable Meticulous replay sidecar URL.\",\n );\n return undefined;\n }\n\n const isAcceptable =\n url.protocol === \"http:\" &&\n url.username === \"\" &&\n url.password === \"\" &&\n (url.pathname === \"\" || url.pathname === \"/\") &&\n url.search === \"\" &&\n url.hash === \"\" &&\n isLocalHostname(url.hostname);\n\n if (!isAcceptable) {\n warnOnce(\n \"replay-sidecar-url\",\n `Ignoring the Meticulous replay sidecar URL \"${raw}\" — only a plain http:// origin on a loopback, docker-gateway or private-network host is honoured.`,\n );\n return undefined;\n }\n\n // Return the parsed origin rather than the raw string: it is what `fetch` would resolve\n // the value to anyway, so the host that was validated is exactly the host that gets\n // called (alternative IPv4 spellings like \"0177.0.0.1\" normalize here, rather than being\n // checked in one form and dialled in another). Carries no trailing slash, matching\n // resolveSidecarUrl's normalization so the self-capture prefix check lines up.\n return url.origin;\n};\n\nconst isLocalHostname = (hostname: string): boolean => {\n const lower = hostname.toLowerCase();\n if (ALLOWED_HOSTNAMES.has(lower)) {\n return true;\n }\n // URL keeps IPv6 literals bracketed.\n const unbracketed =\n lower.startsWith(\"[\") && lower.endsWith(\"]\")\n ? lower.slice(1, -1)\n : undefined;\n if (unbracketed !== undefined) {\n return unbracketed === \"::1\";\n }\n return isPrivateIpv4(lower);\n};\n\nconst isPrivateIpv4 = (hostname: string): boolean => {\n const parts = hostname.split(\".\");\n if (parts.length !== 4) {\n return false;\n }\n const octets = parts.map((part) =>\n // Reject anything that is not a plain decimal octet: \"010\" and \"0x7f\" are parsed\n // inconsistently across resolvers, and \"1e2\" would pass a bare Number() check.\n /^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN,\n );\n if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) {\n return false;\n }\n const [first, second] = octets;\n // Deliberately excludes link-local (169.254.0.0/16): no sidecar is ever reachable there,\n // but the cloud metadata endpoints are (169.254.169.254), so allowing it would only widen\n // the SSRF surface. The rest of the codebase blocks that range for the same reason — see\n // `staging-backend-url.ts` and `assets-app-server.ts`.\n return (\n first === 127 || // loopback\n first === 10 || // RFC1918\n (first === 172 && second >= 16 && second <= 31) || // RFC1918\n (first === 192 && second === 168) // RFC1918\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,MAAM,iBAAiB;AAEvB,MAAM,oBAAoB,IAAI,IAAI;CAChC;CAEA;CACA;CACD,CAAC;AAEF,MAAa,yBACX,QACuB;AACvB,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C;AAEF,KAAI,IAAI,SAAS,gBAAgB;AAC/B,WACE,sBACA,8DACD;AACD;;CAGF,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,IAAI;SACZ;AACN,WACE,sBACA,yDACD;AACD;;AAYF,KAAI,EARF,IAAI,aAAa,WACjB,IAAI,aAAa,MACjB,IAAI,aAAa,OAChB,IAAI,aAAa,MAAM,IAAI,aAAa,QACzC,IAAI,WAAW,MACf,IAAI,SAAS,MACb,gBAAgB,IAAI,SAAS,GAEZ;AACjB,WACE,sBACA,+CAA+C,IAAI,oGACpD;AACD;;AAQF,QAAO,IAAI;;AAGb,MAAM,mBAAmB,aAA8B;CACrD,MAAM,QAAQ,SAAS,aAAa;AACpC,KAAI,kBAAkB,IAAI,MAAM,CAC9B,QAAO;CAGT,MAAM,cACJ,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,GACxC,MAAM,MAAM,GAAG,GAAG,GAClB,KAAA;AACN,KAAI,gBAAgB,KAAA,EAClB,QAAO,gBAAgB;AAEzB,QAAO,cAAc,MAAM;;AAG7B,MAAM,iBAAiB,aAA8B;CACnD,MAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,KAAI,MAAM,WAAW,EACnB,QAAO;CAET,MAAM,SAAS,MAAM,KAAK,SAGxB,YAAY,KAAK,KAAK,GAAG,OAAO,KAAK,GAAG,IACzC;AACD,KAAI,OAAO,MAAM,UAAU,OAAO,MAAM,MAAM,IAAI,QAAQ,IAAI,CAC5D,QAAO;CAET,MAAM,CAAC,OAAO,UAAU;AAKxB,QACE,UAAU,OACV,UAAU,MACT,UAAU,OAAO,UAAU,MAAM,UAAU,MAC3C,UAAU,OAAO,WAAW"}