@alwaysmeticulous/backend-recorder-workerd 2.330.0 → 2.331.3
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.
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/coverage/coverage.types.d.ts +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +22 -1
- package/dist/protocol.js +20 -3
- package/dist/protocol.js.map +1 -1
- package/dist/replay-fetch.js +3 -2
- package/dist/replay-fetch.js.map +1 -1
- package/dist/version.js +3 -3
- package/dist/version.js.map +1 -1
- package/dist/vite/index.js +12 -6
- package/dist/vite/index.js.map +1 -1
- package/dist/with-meticulous.js +0 -0
- package/dist/with-meticulous.js.map +1 -1
- package/package.json +1 -1
package/dist/with-meticulous.js
CHANGED
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-meticulous.js","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 { 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 REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_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 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 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 } 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 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"],"names":[],"mappings":"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","debugId":"57cc8fcd-8567-55c5-a312-461fa11dcb67"}
|
|
1
|
+
{"version":3,"file":"with-meticulous.js","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 { 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 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"],"names":[],"mappings":"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","debugId":"a9ccc633-6a91-56bc-a928-df8201cbcd5b"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwaysmeticulous/backend-recorder-workerd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.331.3",
|
|
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",
|