@alwaysmeticulous/backend-recorder-workerd 2.330.0 → 2.331.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.
package/dist/context.js CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fd65e097-c18e-51ca-8f2b-59427a88d125")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b4629efe-f3f5-5849-8ba3-ffcc3cdc8ba4")}catch(e){}}();
3
3
  import { transportOrigin } from "./sidecar-transport.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  //#region src/context.ts
@@ -10,4 +10,4 @@ const sidecarOriginOf = (ctx) => ctx.mode === "replay" ? ctx.sidecarUrl : transp
10
10
  export { requestCaptureContext, sidecarOriginOf };
11
11
 
12
12
  //# sourceMappingURL=context.js.map
13
- //# debugId=fd65e097-c18e-51ca-8f2b-59427a88d125
13
+ //# debugId=b4629efe-f3f5-5849-8ba3-ffcc3cdc8ba4
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sources":["../src/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { CaptureBuffer } from \"./capture-buffer\";\nimport { type SidecarTransport, transportOrigin } from \"./sidecar-transport\";\n\n/**\n * Per-inbound-request context, entered by the withMeticulous wrapper and read by the fetch\n * patch. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag in the worker's\n * wrangler configuration.\n */\ninterface BaseRequestContext {\n requestId: string;\n /** The inbound request's ctx.waitUntil, for post-response capture work. */\n waitUntil: (promise: Promise<unknown>) => void;\n /**\n * Per-request line-coverage sink, indexed by the global line id the build\n * plugin assigned. Present only when the bundle is instrumented and the\n * request is being replayed.\n *\n * Lives here rather than in a module-scope global because workerd interleaves\n * concurrent requests in one isolate — a shared sink would merge sessions'\n * coverage together.\n */\n coverage?: Uint8Array;\n}\n\n/** Recording: capture events are reported to the sidecar, which stays out of the request path. */\nexport interface RequestCaptureContext extends BaseRequestContext {\n mode: \"record\";\n frontendSessionId: string | undefined;\n /**\n * Set only when we minted {@link frontendSessionId} ourselves for a document navigation the\n * browser could not tag, rather than reading it off the request. Recorded on the inbound\n * event so ingestion can tell an id the page may never have adopted from one it sent us.\n * See provisional-session-id.ts.\n */\n sessionIdOrigin?: \"backend\";\n transport: SidecarTransport;\n /** Batches this request's events into one report. */\n buffer: CaptureBuffer;\n /**\n * Trace this request's spans belong to, and the id of its SERVER span. Minted here rather than\n * by the sidecar so span assembly needs no cross-batch memory — see `CorrelatedEvent`.\n */\n traceId: string;\n serverSpanId: string;\n}\n\n/**\n * Replaying: outbound calls are looked up against the sidecar's mock store before they leave the\n * worker.\n *\n * Both ids are required here, unlike recording. Mocks are indexed by session and their\n * consume-once state is isolated by replay, so omitting either would make deterministic\n * lookup impossible. Making both non-optional keeps that state unrepresentable.\n *\n * Replay is URL-only, deliberately: the sidecar URL arrives on a request header the replay runner\n * injects, validated down to a loopback / docker-gateway / private-network origin. There is no\n * binding transport here because a replay sidecar is a Node process holding the mock store, never\n * a Worker.\n */\nexport interface RequestReplayContext extends BaseRequestContext {\n mode: \"replay\";\n frontendSessionId: string;\n replayId: string;\n /** Normalized origin of the sidecar, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n /**\n * Instant to freeze the worker's clock at while serving this session, or undefined to leave the\n * real clock alone.\n */\n clockAnchorMs: number | undefined;\n}\n\nexport type RequestContext = RequestCaptureContext | RequestReplayContext;\n\nexport const requestCaptureContext = new AsyncLocalStorage<RequestContext>();\n\n/** The origin the shim's own sidecar traffic goes to, for the self-capture guards. */\nexport const sidecarOriginOf = (ctx: RequestContext): string =>\n ctx.mode === \"replay\" ? ctx.sidecarUrl : transportOrigin(ctx.transport);\n"],"names":[],"mappings":";;;;;AA2EA,MAAa,wBAAwB,IAAI,mBAAmC;;AAG5E,MAAa,mBAAmB,QAC9B,IAAI,SAAS,WAAW,IAAI,aAAa,gBAAgB,IAAI,UAAU","debugId":"fd65e097-c18e-51ca-8f2b-59427a88d125"}
1
+ {"version":3,"file":"context.js","sources":["../src/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { CaptureBuffer } from \"./capture-buffer\";\nimport { type SidecarTransport, transportOrigin } from \"./sidecar-transport\";\n\n/**\n * Per-inbound-request context, entered by the withMeticulous wrapper and read by the fetch\n * patch. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag in the worker's\n * wrangler configuration.\n */\ninterface BaseRequestContext {\n requestId: string;\n /** The inbound request's ctx.waitUntil, for post-response capture work. */\n waitUntil: (promise: Promise<unknown>) => void;\n /**\n * Per-request line-coverage sink, indexed by the global line id the build\n * plugin assigned. Present only when the bundle is instrumented and the\n * request is being replayed.\n *\n * Lives here rather than in a module-scope global because workerd interleaves\n * concurrent requests in one isolate — a shared sink would merge sessions'\n * coverage together.\n */\n coverage?: Uint8Array;\n}\n\n/** Recording: capture events are reported to the sidecar, which stays out of the request path. */\nexport interface RequestCaptureContext extends BaseRequestContext {\n mode: \"record\";\n frontendSessionId: string | undefined;\n /**\n * Set only when we minted {@link frontendSessionId} ourselves for a document navigation the\n * browser could not tag, rather than reading it off the request. Recorded on the inbound\n * event so ingestion can tell an id the page may never have adopted from one it sent us.\n * See provisional-session-id.ts.\n */\n sessionIdOrigin?: \"backend\";\n transport: SidecarTransport;\n /** Batches this request's events into one report. */\n buffer: CaptureBuffer;\n /**\n * Trace this request's spans belong to, and the id of its SERVER span. Minted here rather than\n * by the sidecar so span assembly needs no cross-batch memory — see `CorrelatedEvent`.\n */\n traceId: string;\n serverSpanId: string;\n}\n\n/**\n * Replaying: outbound calls are looked up against the sidecar's mock store before they leave the\n * worker.\n *\n * Both ids are required here, unlike recording. Mocks are indexed by session and their\n * consume-once state is isolated by replay, so omitting either would make deterministic\n * lookup impossible. Making both non-optional keeps that state unrepresentable.\n *\n * Replay is URL-only, deliberately: the sidecar URL arrives on a request header the replay runner\n * injects, validated down to a loopback / docker-gateway / private-network origin. There is no\n * binding transport here because a replay sidecar is a Node process holding the mock store, never\n * a Worker.\n */\nexport interface RequestReplayContext extends BaseRequestContext {\n mode: \"replay\";\n frontendSessionId: string;\n replayId: string;\n /** Normalized origin of the sidecar, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n /**\n * Instant to freeze the worker's clock at while serving this session, or undefined to leave the\n * real clock alone.\n */\n clockAnchorMs: number | undefined;\n /**\n * Elapsed virtual time (ms from replay start) of this inbound request, when\n * the replay runner stamped {@link import(\"./protocol\").VIRTUAL_TIME_HEADER}.\n * Forwarded on every outbound lookup so concurrent calls from this request\n * share one timestamp and hash across the matching recorded burst.\n */\n virtualTimeMs?: number;\n}\n\nexport type RequestContext = RequestCaptureContext | RequestReplayContext;\n\nexport const requestCaptureContext = new AsyncLocalStorage<RequestContext>();\n\n/** The origin the shim's own sidecar traffic goes to, for the self-capture guards. */\nexport const sidecarOriginOf = (ctx: RequestContext): string =>\n ctx.mode === \"replay\" ? ctx.sidecarUrl : transportOrigin(ctx.transport);\n"],"names":[],"mappings":";;;;;AAkFA,MAAa,wBAAwB,IAAI,mBAAmC;;AAG5E,MAAa,mBAAmB,QAC9B,IAAI,SAAS,WAAW,IAAI,aAAa,gBAAgB,IAAI,UAAU","debugId":"b4629efe-f3f5-5849-8ba3-ffcc3cdc8ba4"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, SidecarFetcher, SidecarTransport } from "./sidecar-transport.js";
2
2
  import { MeticulousExecutionContext, WithMeticulousOptions } from "./with-meticulous.js";
3
- import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, CoverageReportRequest, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER } from "./protocol.js";
3
+ import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, CoverageReportRequest, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, VIRTUAL_TIME_HEADER, WORKERD_SHIM_VERSION_HEADER, parseVirtualTimeMs } from "./protocol.js";
4
4
  import { WORKERD_SHIM_VERSION } from "./version.js";
5
5
  import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
6
6
  import { headersToRecord } from "./outbound-capture.js";
@@ -85,5 +85,5 @@ declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler
85
85
  */
86
86
  declare const getMeticulousSessionId: () => string | undefined;
87
87
  //#endregion
88
- export { type BindingRequestEvent, CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, type CaptureEvent, type CaptureEventsPayload, type CapturedBody, type CoverageMap, type CoverageMapFile, type CoverageReportRequest, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, type HeaderReader, type HrTime, type InboundRequestEvent, type KvCaptureFields, type KvCaptureOutcome, type KvOmittedReason, type KvOperation, type KvOperationEvent, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, type MeticulousExecutionContext, type MeticulousPagesFunction, type MeticulousPagesFunctionContext, MeticulousWorkerHandler, type OutboundFetchLookupRequest, type OutboundFetchLookupResponse, type OutboundRequestEvent, 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, type PostgresJsQueryLike, type PostgresJsRowMode, type PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, type ReadableSpan, type ReplaySessionInfoResponse, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, type SerializedSpanBase, type SidecarFetcher, type SidecarTransport, SpanBuilder, type SpanStatus, 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, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, type WithMeticulousOptions, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
88
+ export { type BindingRequestEvent, CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, type CaptureEvent, type CaptureEventsPayload, type CapturedBody, type CoverageMap, type CoverageMapFile, type CoverageReportRequest, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, type HeaderReader, type HrTime, type InboundRequestEvent, type KvCaptureFields, type KvCaptureOutcome, type KvOmittedReason, type KvOperation, type KvOperationEvent, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, type MeticulousExecutionContext, type MeticulousPagesFunction, type MeticulousPagesFunctionContext, MeticulousWorkerHandler, type OutboundFetchLookupRequest, type OutboundFetchLookupResponse, type OutboundRequestEvent, 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, type PostgresJsQueryLike, type PostgresJsRowMode, type PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, type ReadableSpan, type ReplaySessionInfoResponse, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, type SerializedSpanBase, type SidecarFetcher, type SidecarTransport, SpanBuilder, type SpanStatus, VIRTUAL_TIME_HEADER, 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, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, type WithMeticulousOptions, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
89
89
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="22e3bcf8-6298-5815-a54c-39716473b677")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d20456bc-894a-5ea9-8719-58b43192282e")}catch(e){}}();
3
3
  import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY } from "./sidecar-transport.js";
4
4
  import { requestCaptureContext } from "./context.js";
5
5
  import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
6
6
  import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
7
- import { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER } from "./protocol.js";
7
+ import { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, VIRTUAL_TIME_HEADER, WORKERD_SHIM_VERSION_HEADER, parseVirtualTimeMs } from "./protocol.js";
8
8
  import { headersToRecord } from "./outbound-capture.js";
9
9
  import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap, getUnreportedCoverageFiles, markCoverageFilesReported, registerCoverageFile } from "./coverage/runtime.js";
10
10
  import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
@@ -93,7 +93,7 @@ const withMeticulous = (handler, options) => {
93
93
  */
94
94
  const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
95
95
  //#endregion
96
- export { CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, 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, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, SpanBuilder, 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, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
96
+ export { CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, MAX_BODY_CAPTURE_SIZE, MAX_POSTGRES_JS_RESULT_SIZE, METICULOUS_PASSTHROUGH_HEADER, 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, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, SIDECAR_URL_ENV_KEY, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, STR_REDACTED, SpanBuilder, VIRTUAL_TIME_HEADER, 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, WORKERD_SHIM_VERSION, WORKERD_SHIM_VERSION_HEADER, __mcEnter, __mcHit, buildServerTimingSessionEntry, collectHitIds, createCoverageSink, deserializeCapturedError, getCoverageMap, getMeticulousSessionId, getUnreportedCoverageFiles, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
97
97
 
98
98
  //# sourceMappingURL=index.js.map
99
- //# debugId=22e3bcf8-6298-5815-a54c-39716473b677
99
+ //# debugId=d20456bc-894a-5ea9-8719-58b43192282e
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport {\n type MeticulousExecutionContext,\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\nexport type {\n BindingRequestEvent,\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n CoverageReportRequest,\n InboundRequestEvent,\n KvOmittedReason,\n KvOperation,\n KvOperationEvent,\n OutboundFetchLookupRequest,\n OutboundFetchLookupResponse,\n OutboundRequestEvent,\n PostgresQueryEvent,\n ReplaySessionInfoResponse,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n METICULOUS_PASSTHROUGH_HEADER,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n WORKERD_SHIM_VERSION_HEADER,\n} from \"./protocol\";\nexport { WORKERD_SHIM_VERSION } from \"./version\";\nexport {\n MAX_BODY_CAPTURE_SIZE,\n readBodyWithCap,\n readRequestBodyWithCap,\n} from \"./body-capture\";\nexport { headersToRecord } from \"./outbound-capture\";\nexport { redactRequestBody, STR_REDACTED } from \"./redact-body\";\nexport type { CoverageMap, CoverageMapFile } from \"./coverage/coverage.types\";\n// __mcEnter / __mcHit are the entry points the Vite plugin's injected imports\n// bind to, so they are part of the package's public surface even though no\n// human writes a call to them.\nexport {\n __mcEnter,\n __mcHit,\n collectHitIds,\n createCoverageSink,\n getCoverageMap,\n getUnreportedCoverageFiles,\n markCoverageFilesReported,\n registerCoverageFile,\n} from \"./coverage/runtime\";\nexport type { MeticulousExecutionContext, WithMeticulousOptions };\nexport {\n type MeticulousPagesFunction,\n type MeticulousPagesFunctionContext,\n withMeticulousPagesFunction,\n} from \"./pages-function\";\nexport {\n SIDECAR_BINDING_ENV_KEY,\n SIDECAR_BINDING_ORIGIN,\n SIDECAR_URL_ENV_KEY,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\n/**\n * Exported for the Node recorder's `withMeticulousCloudflareEnv`, which records the same KV\n * operations from a process whose bindings come from wrangler's `getPlatformProxy`. Both surfaces\n * must produce identical fields, so the contract has exactly one implementation.\n */\nexport {\n type KvCaptureFields,\n type KvCaptureOutcome,\n serializeKvArgs,\n serializeKvCaptureFields,\n} from \"./kv-capture\";\n/**\n * Exported for the same reason: the Node recorder re-exports these so a postgres.js query, a\n * captured error and a built span are byte-identical whichever surface produced them. A\n * Worker-hosted sidecar needs them too, and cannot load the Node bundle.\n */\nexport {\n deserializeCapturedError,\n serializeCapturedError,\n} from \"./error-capture\";\nexport {\n isSupportedPostgresJsQuery,\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 type PostgresJsQueryLike,\n type PostgresJsRowMode,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./postgres/capture\";\nexport { withMeticulousPostgres } from \"./postgres/with-meticulous-postgres\";\nexport * from \"./spans/attributes\";\nexport {\n hrTimeToMs,\n msToHrTime,\n type HrTime,\n type ReadableSpan,\n type SerializedSpanBase,\n SPAN_KIND_CLIENT,\n SPAN_KIND_INTERNAL,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_OK,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./spans/readable-span\";\nexport { serializeSpan } from \"./spans/serialize-span\";\nexport { randomHex, SpanBuilder } from \"./spans/span-builder\";\nexport { redactUrlCredentials } from \"./spans/url-redaction\";\n/**\n * Exported for the Node recorder, whose own `provisional-session-id.ts` adapts these to\n * `http.IncomingMessage` / `http.ServerResponse`. The mint format, the rule for which requests\n * get an id and the `Server-Timing` metric are all wire contracts with the page, so — as with\n * the KV capture fields above — they have exactly one implementation.\n */\nexport {\n type HeaderReader,\n SERVER_TIMING_HEADER,\n SERVER_TIMING_SESSION_METRIC,\n buildServerTimingSessionEntry,\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\n\n/** Structural subset of an ES-module Worker handler (`export default {...}`). */\nexport interface MeticulousWorkerHandler<Env = never> {\n fetch(\n request: Request,\n env: Env,\n ctx: MeticulousExecutionContext,\n ): Response | Promise<Response>;\n}\n\n/**\n * Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP\n * behaviour:\n *\n * export default withMeticulous({\n * async fetch(request, env, ctx) { ... },\n * });\n *\n * For a Cloudflare Pages project, whose worker exports `onRequest` instead, use\n * {@link withMeticulousPagesFunction} — same behaviour, different handler shape.\n *\n * **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**\n * Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)\n * or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),\n * or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through\n * `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js\n * queries are reported to the sidecar, batched into one report per request, without affecting the\n * app. A binding beats a var if both are configured: only the binding can have been added to\n * *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels\n * with an image.\n *\n * **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the\n * Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then\n * served from the recording instead of reaching the real service — and a call the recording does\n * not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`\n * header on a request that must stay real. The clock is frozen at the recorded session's end so\n * recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /\n * `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that\n * session. Workerd cannot read container environment variables, which is why per-replay config\n * arrives as a request header; the shim validates it and only honours a loopback /\n * docker-gateway / private-network `http:` origin.\n *\n * Replay takes precedence over recording when both are configured. A stale sidecar var baked into\n * an image is far more likely than a spurious replay header (nothing but the replay runner emits\n * one), and letting the env win would silently record a replay instead of mocking it.\n *\n * With neither configured the wrapper is a complete pass-through, so it is safe to keep in\n * deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture\n * failure or unreachable sidecar never affects the app.\n */\nexport const withMeticulous = <H extends MeticulousWorkerHandler<never>>(\n handler: H,\n options?: WithMeticulousOptions,\n): H => {\n const wrappedFetch = (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> =>\n runWithMeticulous(\n {\n request,\n env,\n ctx,\n invokeHandler: () => handler.fetch(request, env as never, ctx),\n },\n options,\n );\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript cannot verify a\n // spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\n/**\n * The Meticulous session id of the request currently being served, or `undefined` if this\n * call cannot be tied to one — including whenever the wrapper is a pass-through.\n *\n * Exposed so a server-rendering worker can put the id into the HTML it is about to return,\n * typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,\n * cache-proof alternative to the `Server-Timing` header the shim publishes automatically;\n * doing both is fine, since the recorder prefers the rendered value. It is also how the id\n * reaches another process — a document navigation has no inbound header for the app to\n * forward, so a worker that fans out to its own services has to pass this on as\n * `x-meticulous-session-id` itself.\n *\n * During a replay this returns the replayed session's id, which is the same value the render\n * being replayed was given when it was recorded (the page adopted the minted id, so that is\n * what the recording is stored under, and it is what the replay runner injects back). An app\n * that renders the id therefore renders the same bytes both times, rather than the id turning\n * every server-rendered page into a diff.\n */\nexport const getMeticulousSessionId = (): string | undefined =>\n requestCaptureContext.getStore()?.frontendSessionId;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiMA,MAAa,kBACX,SACA,YACM;CACN,MAAM,gBACJ,SACA,KACA,QAEA,kBACE;EACE;EACA;EACA;EACA,qBAAqB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAC/D,EACD,QACD;AAIH,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;;;;;;;;;;;;;;;;;;;AAqB5C,MAAa,+BACX,sBAAsB,UAAU,EAAE","debugId":"22e3bcf8-6298-5815-a54c-39716473b677"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport {\n type MeticulousExecutionContext,\n runWithMeticulous,\n type WithMeticulousOptions,\n} from \"./with-meticulous\";\n\nexport type {\n BindingRequestEvent,\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n CoverageReportRequest,\n InboundRequestEvent,\n KvOmittedReason,\n KvOperation,\n KvOperationEvent,\n OutboundFetchLookupRequest,\n OutboundFetchLookupResponse,\n OutboundRequestEvent,\n PostgresQueryEvent,\n ReplaySessionInfoResponse,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n METICULOUS_PASSTHROUGH_HEADER,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_COVERAGE_PATH,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n VIRTUAL_TIME_HEADER,\n WORKERD_SHIM_VERSION_HEADER,\n} from \"./protocol\";\nexport { WORKERD_SHIM_VERSION } from \"./version\";\nexport {\n MAX_BODY_CAPTURE_SIZE,\n readBodyWithCap,\n readRequestBodyWithCap,\n} from \"./body-capture\";\nexport { headersToRecord } from \"./outbound-capture\";\nexport { redactRequestBody, STR_REDACTED } from \"./redact-body\";\nexport type { CoverageMap, CoverageMapFile } from \"./coverage/coverage.types\";\n// __mcEnter / __mcHit are the entry points the Vite plugin's injected imports\n// bind to, so they are part of the package's public surface even though no\n// human writes a call to them.\nexport {\n __mcEnter,\n __mcHit,\n collectHitIds,\n createCoverageSink,\n getCoverageMap,\n getUnreportedCoverageFiles,\n markCoverageFilesReported,\n registerCoverageFile,\n} from \"./coverage/runtime\";\nexport type { MeticulousExecutionContext, WithMeticulousOptions };\nexport {\n type MeticulousPagesFunction,\n type MeticulousPagesFunctionContext,\n withMeticulousPagesFunction,\n} from \"./pages-function\";\nexport {\n SIDECAR_BINDING_ENV_KEY,\n SIDECAR_BINDING_ORIGIN,\n SIDECAR_URL_ENV_KEY,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\n/**\n * Exported for the Node recorder's `withMeticulousCloudflareEnv`, which records the same KV\n * operations from a process whose bindings come from wrangler's `getPlatformProxy`. Both surfaces\n * must produce identical fields, so the contract has exactly one implementation.\n */\nexport {\n type KvCaptureFields,\n type KvCaptureOutcome,\n serializeKvArgs,\n serializeKvCaptureFields,\n} from \"./kv-capture\";\n/**\n * Exported for the same reason: the Node recorder re-exports these so a postgres.js query, a\n * captured error and a built span are byte-identical whichever surface produced them. A\n * Worker-hosted sidecar needs them too, and cannot load the Node bundle.\n */\nexport {\n deserializeCapturedError,\n serializeCapturedError,\n} from \"./error-capture\";\nexport {\n isSupportedPostgresJsQuery,\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 type PostgresJsQueryLike,\n type PostgresJsRowMode,\n reconstructQueryText,\n resolveRowMode,\n serializePostgresJsArgs,\n serializePostgresJsError,\n serializePostgresJsResult,\n} from \"./postgres/capture\";\nexport { withMeticulousPostgres } from \"./postgres/with-meticulous-postgres\";\nexport * from \"./spans/attributes\";\nexport {\n hrTimeToMs,\n msToHrTime,\n type HrTime,\n type ReadableSpan,\n type SerializedSpanBase,\n SPAN_KIND_CLIENT,\n SPAN_KIND_INTERNAL,\n SPAN_KIND_SERVER,\n SPAN_STATUS_ERROR,\n SPAN_STATUS_OK,\n SPAN_STATUS_UNSET,\n type SpanStatus,\n} from \"./spans/readable-span\";\nexport { serializeSpan } from \"./spans/serialize-span\";\nexport { randomHex, SpanBuilder } from \"./spans/span-builder\";\nexport { redactUrlCredentials } from \"./spans/url-redaction\";\n/**\n * Exported for the Node recorder, whose own `provisional-session-id.ts` adapts these to\n * `http.IncomingMessage` / `http.ServerResponse`. The mint format, the rule for which requests\n * get an id and the `Server-Timing` metric are all wire contracts with the page, so — as with\n * the KV capture fields above — they have exactly one implementation.\n */\nexport {\n type HeaderReader,\n SERVER_TIMING_HEADER,\n SERVER_TIMING_SESSION_METRIC,\n buildServerTimingSessionEntry,\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\n\n/** Structural subset of an ES-module Worker handler (`export default {...}`). */\nexport interface MeticulousWorkerHandler<Env = never> {\n fetch(\n request: Request,\n env: Env,\n ctx: MeticulousExecutionContext,\n ): Response | Promise<Response>;\n}\n\n/**\n * Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP\n * behaviour:\n *\n * export default withMeticulous({\n * async fetch(request, env, ctx) { ... },\n * });\n *\n * For a Cloudflare Pages project, whose worker exports `onRequest` instead, use\n * {@link withMeticulousPagesFunction} — same behaviour, different handler shape.\n *\n * **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**\n * Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)\n * or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),\n * or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through\n * `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js\n * queries are reported to the sidecar, batched into one report per request, without affecting the\n * app. A binding beats a var if both are configured: only the binding can have been added to\n * *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels\n * with an image.\n *\n * **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the\n * Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then\n * served from the recording instead of reaching the real service — and a call the recording does\n * not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`\n * header on a request that must stay real. The clock is frozen at the recorded session's end so\n * recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /\n * `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that\n * session. Workerd cannot read container environment variables, which is why per-replay config\n * arrives as a request header; the shim validates it and only honours a loopback /\n * docker-gateway / private-network `http:` origin.\n *\n * Replay takes precedence over recording when both are configured. A stale sidecar var baked into\n * an image is far more likely than a spurious replay header (nothing but the replay runner emits\n * one), and letting the env win would silently record a replay instead of mocking it.\n *\n * With neither configured the wrapper is a complete pass-through, so it is safe to keep in\n * deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture\n * failure or unreachable sidecar never affects the app.\n */\nexport const withMeticulous = <H extends MeticulousWorkerHandler<never>>(\n handler: H,\n options?: WithMeticulousOptions,\n): H => {\n const wrappedFetch = (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> =>\n runWithMeticulous(\n {\n request,\n env,\n ctx,\n invokeHandler: () => handler.fetch(request, env as never, ctx),\n },\n options,\n );\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript cannot verify a\n // spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\n/**\n * The Meticulous session id of the request currently being served, or `undefined` if this\n * call cannot be tied to one — including whenever the wrapper is a pass-through.\n *\n * Exposed so a server-rendering worker can put the id into the HTML it is about to return,\n * typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,\n * cache-proof alternative to the `Server-Timing` header the shim publishes automatically;\n * doing both is fine, since the recorder prefers the rendered value. It is also how the id\n * reaches another process — a document navigation has no inbound header for the app to\n * forward, so a worker that fans out to its own services has to pass this on as\n * `x-meticulous-session-id` itself.\n *\n * During a replay this returns the replayed session's id, which is the same value the render\n * being replayed was given when it was recorded (the page adopted the minted id, so that is\n * what the recording is stored under, and it is what the replay runner injects back). An app\n * that renders the id therefore renders the same bytes both times, rather than the id turning\n * every server-rendered page into a diff.\n */\nexport const getMeticulousSessionId = (): string | undefined =>\n requestCaptureContext.getStore()?.frontendSessionId;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmMA,MAAa,kBACX,SACA,YACM;CACN,MAAM,gBACJ,SACA,KACA,QAEA,kBACE;EACE;EACA;EACA;EACA,qBAAqB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAC/D,EACD,QACD;AAIH,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;;;;;;;;;;;;;;;;;;;AAqB5C,MAAa,+BACX,sBAAsB,UAAU,EAAE","debugId":"d20456bc-894a-5ea9-8719-58b43192282e"}
@@ -14,6 +14,18 @@ declare const SIDECAR_PROTOCOL_VERSION_HEADER = "x-meticulous-sidecar-protocol-v
14
14
  declare const FRONTEND_SESSION_ID_HEADER = "x-meticulous-session-id";
15
15
  /** Header identifying one replay attempt of a recorded frontend session. */
16
16
  declare const REPLAY_ID_HEADER = "x-meticulous-replay-id";
17
+ /**
18
+ * Elapsed virtual time (ms from replay start) of the triggering frontend
19
+ * request. Injected by the replay runner on app-url requests during backend
20
+ * testing; the sidecar uses it to pick the closest recorded burst when several
21
+ * candidates share a loosened match key.
22
+ */
23
+ declare const VIRTUAL_TIME_HEADER = "x-meticulous-virtual-time";
24
+ /**
25
+ * Parses {@link VIRTUAL_TIME_HEADER}. `0` is a valid start-of-session time;
26
+ * anything non-finite or negative is ignored so a garbage header cannot skew matching.
27
+ */
28
+ declare const parseVirtualTimeMs: (value: string | null | undefined) => number | undefined;
17
29
  /**
18
30
  * A request carrying this header set to `"true"` is never served from the recording and
19
31
  * never fails on a miss — it goes to the real network. Use it for a call that must be live
@@ -292,6 +304,15 @@ interface OutboundFetchLookupRequest {
292
304
  * record and replay sides.
293
305
  */
294
306
  requestBody?: CapturedBody;
307
+ /**
308
+ * Elapsed virtual time (ms from replay start) of the inbound request that
309
+ * dispatched this outbound call, forwarded from {@link VIRTUAL_TIME_HEADER}.
310
+ * The sidecar clusters candidates by recorded timestamp against this value.
311
+ *
312
+ * Additive and non-version-bumping: an older shim omits it, and matching
313
+ * then hashes across every candidate at the matched key.
314
+ */
315
+ virtualTimeMs?: number;
295
316
  }
296
317
  /**
297
318
  * A recorded response to serve, or a signal the caller must fail the request. Replay is
@@ -316,5 +337,5 @@ type OutboundFetchLookupResponse = {
316
337
  outcome: "no-mock";
317
338
  };
318
339
  //#endregion
319
- export { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, CoverageReportRequest, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER };
340
+ export { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, CoverageReportRequest, FRONTEND_SESSION_ID_HEADER, InboundRequestEvent, KvOmittedReason, KvOperation, KvOperationEvent, METICULOUS_PASSTHROUGH_HEADER, OutboundFetchLookupRequest, OutboundFetchLookupResponse, OutboundRequestEvent, PostgresQueryEvent, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, VIRTUAL_TIME_HEADER, WORKERD_SHIM_VERSION_HEADER, parseVirtualTimeMs };
320
341
  //# sourceMappingURL=protocol.d.ts.map
package/dist/protocol.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * is the single source of truth for the shape of capture events.
6
6
  */
7
7
 
8
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="195758e9-bc4f-5373-96e6-e0aef542ce8c")}catch(e){}}();
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7851ef28-53e0-596f-80dc-a26f37790747")}catch(e){}}();
9
9
  const SIDECAR_PROTOCOL_VERSION = "2";
10
10
  /** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */
11
11
  const SIDECAR_PROTOCOL_VERSION_HEADER = "x-meticulous-sidecar-protocol-version";
@@ -17,6 +17,23 @@ const FRONTEND_SESSION_ID_HEADER = "x-meticulous-session-id";
17
17
  /** Header identifying one replay attempt of a recorded frontend session. */
18
18
  const REPLAY_ID_HEADER = "x-meticulous-replay-id";
19
19
  /**
20
+ * Elapsed virtual time (ms from replay start) of the triggering frontend
21
+ * request. Injected by the replay runner on app-url requests during backend
22
+ * testing; the sidecar uses it to pick the closest recorded burst when several
23
+ * candidates share a loosened match key.
24
+ */
25
+ const VIRTUAL_TIME_HEADER = "x-meticulous-virtual-time";
26
+ /**
27
+ * Parses {@link VIRTUAL_TIME_HEADER}. `0` is a valid start-of-session time;
28
+ * anything non-finite or negative is ignored so a garbage header cannot skew matching.
29
+ */
30
+ const parseVirtualTimeMs = (value) => {
31
+ if (value == null || value === "") return;
32
+ const parsed = Number(value);
33
+ if (!Number.isFinite(parsed) || parsed < 0) return;
34
+ return parsed;
35
+ };
36
+ /**
20
37
  * A request carrying this header set to `"true"` is never served from the recording and
21
38
  * never fails on a miss — it goes to the real network. Use it for a call that must be live
22
39
  * during replay (a health probe, telemetry, a resource the recording cannot cover).
@@ -71,7 +88,7 @@ const WORKERD_SHIM_VERSION_HEADER = "x-meticulous-workerd-shim-version";
71
88
  */
72
89
  const CAPTURED_HEADERS = ["content-type", FRONTEND_SESSION_ID_HEADER];
73
90
  //#endregion
74
- export { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, WORKERD_SHIM_VERSION_HEADER };
91
+ export { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_ID_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_COVERAGE_PATH, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, VIRTUAL_TIME_HEADER, WORKERD_SHIM_VERSION_HEADER, parseVirtualTimeMs };
75
92
 
76
93
  //# sourceMappingURL=protocol.js.map
77
- //# debugId=195758e9-bc4f-5373-96e6-e0aef542ce8c
94
+ //# debugId=7851ef28-53e0-596f-80dc-a26f37790747
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Wire protocol between the in-worker shim and the Meticulous backend\n * recorder sidecar. The sidecar imports these types (type-only), so this file\n * is the single source of truth for the shape of capture events.\n */\n\nexport const SIDECAR_PROTOCOL_VERSION = \"2\";\n\n/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */\nexport const SIDECAR_PROTOCOL_VERSION_HEADER =\n \"x-meticulous-sidecar-protocol-version\";\n\n/**\n * Header the Meticulous frontend recorder stamps on requests made by the app\n * under recording; its value ties backend spans to the frontend session.\n */\nexport const FRONTEND_SESSION_ID_HEADER = \"x-meticulous-session-id\";\n\n/** Header identifying one replay attempt of a recorded frontend session. */\nexport const REPLAY_ID_HEADER = \"x-meticulous-replay-id\";\n\n/**\n * A request carrying this header set to `\"true\"` is never served from the recording and\n * never fails on a miss — it goes to the real network. Use it for a call that must be live\n * during replay (a health probe, telemetry, a resource the recording cannot cover).\n *\n * Same convention and value as the frontend network stubbing and the Node backend recorder\n * (`METICULOUS_PASSTHROUGH_HEADER` in `@alwaysmeticulous/backend-recorder-js`); the value\n * must stay in step with both.\n */\nexport const METICULOUS_PASSTHROUGH_HEADER = \"meticulous-passthrough\";\n\nexport const SIDECAR_EVENTS_PATH = \"/v1/events\";\n\n/**\n * Replay routes. A sidecar started in record mode answers 404 on both. Replay is hermetic,\n * so an unanswered outbound-fetch lookup fails the app's call rather than letting it reach\n * the real service — the same as any other failure to stub.\n */\nexport const SIDECAR_REPLAY_SESSION_PATH = \"/v1/replay/session\";\nexport const SIDECAR_REPLAY_OUTBOUND_FETCH_PATH = \"/v1/replay/outbound-fetch\";\n\n/**\n * Line-coverage reporting. Additive: an older sidecar answers 404, which the\n * shim treats as \"coverage not collected here\" and stops reporting for the life\n * of the isolate. Deliberately NOT guarded by a\n * {@link SIDECAR_PROTOCOL_VERSION} bump — that check is exact equality, so a\n * bump would stop every already-built customer image from mocking at all.\n */\nexport const SIDECAR_REPLAY_COVERAGE_PATH = \"/v1/replay/coverage\";\n\n/**\n * Header carrying the replay sidecar's origin, injected by the Meticulous replay runner\n * on requests to the app under test. Workerd cannot see container environment variables,\n * so an inbound header is the only way per-replay config can reach the shim.\n *\n * The shim validates the value before using it and only honours a loopback /\n * `host.docker.internal` / private-network `http:` origin — see `replay-sidecar-url.ts`.\n */\nexport const REPLAY_SIDECAR_URL_HEADER =\n \"x-meticulous-backend-replay-sidecar-url\";\n\n/**\n * Header the in-worker shim stamps on every app response (record and replay)\n * carrying the shim's npm package version. The replay runner reads this from\n * Puppeteer — workerd cannot see container env, and the execute pod cannot\n * reach the in-pod sidecar, so a response header is the only channel that\n * reports which bundled shim produced a replay's After screenshots.\n */\nexport const WORKERD_SHIM_VERSION_HEADER = \"x-meticulous-workerd-shim-version\";\n\n/**\n * The only headers persisted on capture events. Everything else — notably\n * authorization, cookie/set-cookie, and API-key headers — is dropped at\n * capture time, before it ever leaves the worker.\n *\n * The Node backend recorder persists the same two, plus the inbound request's\n * `Cookie` header on SERVER spans (`recorded-request-headers.ts`). This list\n * has not followed it there, so a workerd recording of an authenticated server\n * render still carries no session cookie.\n */\nexport const CAPTURED_HEADERS = [\n \"content-type\",\n FRONTEND_SESSION_ID_HEADER,\n] as const;\n\nexport interface CapturedBody {\n /** UTF-8 decoded body, truncated to the capture cap. */\n body: string;\n /** True when the body exceeded the capture cap or the read was cut short. */\n truncated: boolean;\n}\n\n/**\n * The fields every capture event carries to say which request, session and trace it belongs to.\n */\ninterface CorrelatedEvent {\n /** Correlates outbound events to the inbound request being handled. */\n requestId: string;\n /** Value of {@link FRONTEND_SESSION_ID_HEADER} on the inbound request, if any. */\n frontendSessionId?: string;\n /**\n * Trace the request's spans belong to (32 hex chars), minted by the shim.\n *\n * The shim mints it, rather than the sidecar deriving it from {@link requestId}, so span\n * assembly is stateless: a sidecar that had to remember `requestId → traceId` would give a\n * request's SERVER span and its CLIENT children different traces whenever it lost that memory\n * between two batches — which, for a Worker-hosted sidecar, an eviction can do at any moment.\n *\n * Optional so a sidecar keeps working against a shim that predates this field, in which case\n * it falls back to its own per-`requestId` map.\n */\n traceId?: string;\n /**\n * Span id of the request's SERVER span (16 hex chars), also minted by the shim: it is the\n * parent of every CLIENT span the request produced, and the inbound event's own span id.\n */\n serverSpanId?: string;\n}\n\ninterface BaseRequestEvent extends CorrelatedEvent {\n method: string;\n /** Full URL including query string. */\n url: string;\n /** Lower-cased header name → values. */\n requestHeaders: Record<string, string[]>;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the request failed before a response was produced. */\n error?: string;\n}\n\n/**\n * The worker's own inbound request (becomes a SERVER span). Bodies are not\n * captured, matching the Node backend recorder's SERVER-span contract.\n */\nexport interface InboundRequestEvent extends BaseRequestEvent {\n kind: \"inbound\";\n /**\n * Where {@link BaseRequestEvent.frontendSessionId} came from. Absent means the browser\n * minted it and sent it on the request, which is the normal case. `\"backend\"` means the\n * shim minted it for a document navigation the browser could not tag, and the page may or\n * may not have gone on to adopt it — a distinction ingestion needs, because an unadopted\n * backend-minted id names a session that will never exist and must be treated as unstamped.\n * The sidecar turns it into `meticulous.session_id_origin`.\n *\n * Optional, and therefore compatible with an older sidecar (which ignores it) — which is\n * why adding it does not bump {@link SIDECAR_PROTOCOL_VERSION}, a strict-equality check\n * whose bump would instead make this shim unusable against one.\n */\n sessionIdOrigin?: \"backend\";\n}\n\n/**\n * An outgoing `fetch` call made while handling an inbound request (becomes a\n * CLIENT span).\n */\nexport interface OutboundRequestEvent extends BaseRequestEvent {\n kind: \"outbound\";\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding, an assets binding, or a\n * Durable Object stub — made while handling an inbound request (becomes a CLIENT span).\n *\n * Request/Response-shaped like {@link OutboundRequestEvent}, but a distinct kind because the\n * call never leaves the isolate and its URL is whatever the caller invented. Keeping the two\n * apart means a binding call and real egress can never be confused for one another when\n * these spans are eventually mocked.\n */\nexport interface BindingRequestEvent extends BaseRequestEvent {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Absent when the instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` produces rather\n * than being a binding in its own right.\n */\n bindingName?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/** KV namespace methods the shim records. */\nexport type KvOperation = \"get\" | \"getWithMetadata\" | \"put\" | \"delete\" | \"list\";\n\n/**\n * Why a KV value is absent from an event: workerd handed it over in a form that cannot be\n * persisted as text. A `\"stream\"` value must not even be read — consuming it would take the\n * bytes away from the app — and binary values are skipped deliberately, since KV blobs are\n * large and would both bloat the recording and be mangled by UTF-8 capture.\n */\nexport type KvOmittedReason = \"binary\" | \"stream\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, made while handling an inbound request\n * (becomes a CLIENT span).\n *\n * KV is not HTTP: there is no method, URL or status code, so this event shares nothing with\n * {@link BaseRequestEvent} beyond correlation and timing. Values are JSON so that a single\n * `JSON.parse` reconstructs exactly what the app saw, whichever `type` the read asked for.\n */\nexport interface KvOperationEvent extends CorrelatedEvent {\n kind: \"kv\";\n /**\n * The `env` key the KV namespace was found under. Always present, unlike on a binding\n * event: a namespace has no factory equivalent to `DurableObjectNamespace.get()`, so one\n * that was never seen on `env` is not recorded at all.\n */\n bindingName: string;\n operation: KvOperation;\n /**\n * The key operated on, when it is a single string. Absent for `list` (which spans keys) and\n * for a bulk `get` (which takes an array) — both keep their arguments in {@link args}.\n */\n key?: string;\n /**\n * JSON of the call's arguments: the read `type`, the `put` options, the `list` selector.\n * A `put` value appears as `null` here — it is captured in {@link value} instead, so that\n * redaction applies to it.\n */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, with secret-looking fields redacted. */\n value?: CapturedBody;\n /**\n * JSON of what the operation returned: the value for `get`, `{value, metadata}` for\n * `getWithMetadata`, the key page for `list`. Absent for `put`/`delete`, which return\n * nothing, and for a value that could not be captured (see {@link omitted}).\n */\n result?: CapturedBody;\n /**\n * Why the operation's value is missing. Refers to the written value for `put` and to the\n * read value otherwise — a `put` has no result, so this is never ambiguous.\n */\n omitted?: KvOmittedReason;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the operation threw. */\n error?: string;\n}\n\n/**\n * One postgres.js query made while handling an inbound request (becomes a CLIENT span).\n *\n * Deliberately carries the same fields the Node recorder puts on a postgres.js span rather than\n * anything Worker-specific: `PostgresJsMockStore` builds its match key from those attributes at\n * load time, so a query recorded here replays through the existing Node mock path unchanged.\n * See `postgres/capture.ts` for the serialization, which both surfaces share.\n */\nexport interface PostgresQueryEvent extends CorrelatedEvent {\n kind: \"postgres\";\n /** SQL rebuilt from the tagged template's chunks, with `$1`, `$2`… placeholders. */\n queryText: string;\n /** JSON of the interpolated values, from `serializePostgresJsArgs`. */\n params: string;\n /** `\"\"`, `\"raw\"` or `\"values\"` — the row shape the query asked for. */\n rowMode: string;\n /** JSON of the resolved `Result`, capped at {@link MAX_POSTGRES_JS_RESULT_SIZE}. */\n result?: CapturedBody;\n /**\n * JSON of the rejection, from `serializePostgresJsError`. Set instead of {@link result}, and\n * distinct from the transport-level `error` string on the HTTP-shaped events: this one is a\n * recorded outcome the replay reproduces, not a note that capture failed.\n */\n errorJson?: string;\n startTimeMs: number;\n endTimeMs: number;\n}\n\nexport type CaptureEvent =\n | InboundRequestEvent\n | OutboundRequestEvent\n | BindingRequestEvent\n | KvOperationEvent\n | PostgresQueryEvent;\n\nexport interface CaptureEventsPayload {\n events: CaptureEvent[];\n}\n\n/**\n * Answer to `GET {SIDECAR_REPLAY_SESSION_PATH}?sessionId=<id>`: whether the sidecar holds\n * mocks for a session, and the virtual clock anchor to freeze `Date` at while serving it.\n */\nexport interface ReplaySessionInfoResponse {\n found: boolean;\n /**\n * Epoch ms of the session's last recorded activity. The worker freezes its clock here so\n * that credentials minted during the recording are already issued but not yet expired.\n */\n clockAnchorMs?: number;\n}\n\n/**\n * One request's line coverage, sent to {@link SIDECAR_REPLAY_COVERAGE_PATH}\n * after the response has been produced.\n *\n * `files` carries the build-time id→line map and is sent only on the first\n * report of an isolate: it is the same for every request the isolate serves and\n * is far larger than the hits themselves.\n */\nexport interface CoverageReportRequest {\n frontendSessionId: string;\n /** Global line ids marked during this request, ascending. */\n hitIds: number[];\n /** Present on an isolate's first report only. */\n files?: Array<{\n path: string;\n firstId: number;\n /** Inclusive 1-based line ranges, flattened as `[s1, e1, s2, e2, ...]`. */\n lineRanges: number[];\n }>;\n}\n\n/** Lookup for a single outbound `fetch`, sent to {@link SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}. */\nexport interface OutboundFetchLookupRequest {\n frontendSessionId: string;\n replayId: string;\n method: string;\n /** Full URL including query string. */\n url: string;\n /**\n * Request body as captured by `readBodyWithCap`. The sidecar hashes this itself, so the\n * hash it compares against a recording is derived from byte-identical input on both the\n * record and replay sides.\n */\n requestBody?: CapturedBody;\n}\n\n/**\n * A recorded response to serve, or a signal the caller must fail the request. Replay is\n * hermetic, so the shim never turns any of these into a real call — nor an outcome it does\n * not recognise. The only live path is the request-side `meticulous-passthrough` header,\n * handled before the lookup, not any response here.\n */\nexport type OutboundFetchLookupResponse =\n | {\n outcome: \"mock\";\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n }\n /**\n * The store holds nothing for this call — a genuine miss, or a call the sidecar could not\n * look up at all (an unparseable URL). Replay is hermetic, so the shim fails the request\n * rather than letting it reach the real service — matching the Node backend recorder's\n * http/undici mocks. There is deliberately no \"let the call through\" outcome: the only\n * live path is the request-side `meticulous-passthrough` header.\n */\n | { outcome: \"no-mock\" };\n"],"names":[],"mappings":";;;;;;;;AAMA,MAAa,2BAA2B;;AAGxC,MAAa,kCACX;;;;;AAMF,MAAa,6BAA6B;;AAG1C,MAAa,mBAAmB;;;;;;;;;;AAWhC,MAAa,gCAAgC;AAE7C,MAAa,sBAAsB;;;;;;AAOnC,MAAa,8BAA8B;AAC3C,MAAa,qCAAqC;;;;;;;;AASlD,MAAa,+BAA+B;;;;;;;;;AAU5C,MAAa,4BACX;;;;;;;;AASF,MAAa,8BAA8B;;;;;;;;;;;AAY3C,MAAa,mBAAmB,CAC9B,gBACA,2BACD","debugId":"195758e9-bc4f-5373-96e6-e0aef542ce8c"}
1
+ {"version":3,"file":"protocol.js","sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Wire protocol between the in-worker shim and the Meticulous backend\n * recorder sidecar. The sidecar imports these types (type-only), so this file\n * is the single source of truth for the shape of capture events.\n */\n\nexport const SIDECAR_PROTOCOL_VERSION = \"2\";\n\n/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */\nexport const SIDECAR_PROTOCOL_VERSION_HEADER =\n \"x-meticulous-sidecar-protocol-version\";\n\n/**\n * Header the Meticulous frontend recorder stamps on requests made by the app\n * under recording; its value ties backend spans to the frontend session.\n */\nexport const FRONTEND_SESSION_ID_HEADER = \"x-meticulous-session-id\";\n\n/** Header identifying one replay attempt of a recorded frontend session. */\nexport const REPLAY_ID_HEADER = \"x-meticulous-replay-id\";\n\n/**\n * Elapsed virtual time (ms from replay start) of the triggering frontend\n * request. Injected by the replay runner on app-url requests during backend\n * testing; the sidecar uses it to pick the closest recorded burst when several\n * candidates share a loosened match key.\n */\nexport const VIRTUAL_TIME_HEADER = \"x-meticulous-virtual-time\";\n\n/**\n * Parses {@link VIRTUAL_TIME_HEADER}. `0` is a valid start-of-session time;\n * anything non-finite or negative is ignored so a garbage header cannot skew matching.\n */\nexport const parseVirtualTimeMs = (\n value: string | null | undefined,\n): number | undefined => {\n if (value == null || value === \"\") {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return undefined;\n }\n return parsed;\n};\n\n/**\n * A request carrying this header set to `\"true\"` is never served from the recording and\n * never fails on a miss — it goes to the real network. Use it for a call that must be live\n * during replay (a health probe, telemetry, a resource the recording cannot cover).\n *\n * Same convention and value as the frontend network stubbing and the Node backend recorder\n * (`METICULOUS_PASSTHROUGH_HEADER` in `@alwaysmeticulous/backend-recorder-js`); the value\n * must stay in step with both.\n */\nexport const METICULOUS_PASSTHROUGH_HEADER = \"meticulous-passthrough\";\n\nexport const SIDECAR_EVENTS_PATH = \"/v1/events\";\n\n/**\n * Replay routes. A sidecar started in record mode answers 404 on both. Replay is hermetic,\n * so an unanswered outbound-fetch lookup fails the app's call rather than letting it reach\n * the real service — the same as any other failure to stub.\n */\nexport const SIDECAR_REPLAY_SESSION_PATH = \"/v1/replay/session\";\nexport const SIDECAR_REPLAY_OUTBOUND_FETCH_PATH = \"/v1/replay/outbound-fetch\";\n\n/**\n * Line-coverage reporting. Additive: an older sidecar answers 404, which the\n * shim treats as \"coverage not collected here\" and stops reporting for the life\n * of the isolate. Deliberately NOT guarded by a\n * {@link SIDECAR_PROTOCOL_VERSION} bump — that check is exact equality, so a\n * bump would stop every already-built customer image from mocking at all.\n */\nexport const SIDECAR_REPLAY_COVERAGE_PATH = \"/v1/replay/coverage\";\n\n/**\n * Header carrying the replay sidecar's origin, injected by the Meticulous replay runner\n * on requests to the app under test. Workerd cannot see container environment variables,\n * so an inbound header is the only way per-replay config can reach the shim.\n *\n * The shim validates the value before using it and only honours a loopback /\n * `host.docker.internal` / private-network `http:` origin — see `replay-sidecar-url.ts`.\n */\nexport const REPLAY_SIDECAR_URL_HEADER =\n \"x-meticulous-backend-replay-sidecar-url\";\n\n/**\n * Header the in-worker shim stamps on every app response (record and replay)\n * carrying the shim's npm package version. The replay runner reads this from\n * Puppeteer — workerd cannot see container env, and the execute pod cannot\n * reach the in-pod sidecar, so a response header is the only channel that\n * reports which bundled shim produced a replay's After screenshots.\n */\nexport const WORKERD_SHIM_VERSION_HEADER = \"x-meticulous-workerd-shim-version\";\n\n/**\n * The only headers persisted on capture events. Everything else — notably\n * authorization, cookie/set-cookie, and API-key headers — is dropped at\n * capture time, before it ever leaves the worker.\n *\n * The Node backend recorder persists the same two, plus the inbound request's\n * `Cookie` header on SERVER spans (`recorded-request-headers.ts`). This list\n * has not followed it there, so a workerd recording of an authenticated server\n * render still carries no session cookie.\n */\nexport const CAPTURED_HEADERS = [\n \"content-type\",\n FRONTEND_SESSION_ID_HEADER,\n] as const;\n\nexport interface CapturedBody {\n /** UTF-8 decoded body, truncated to the capture cap. */\n body: string;\n /** True when the body exceeded the capture cap or the read was cut short. */\n truncated: boolean;\n}\n\n/**\n * The fields every capture event carries to say which request, session and trace it belongs to.\n */\ninterface CorrelatedEvent {\n /** Correlates outbound events to the inbound request being handled. */\n requestId: string;\n /** Value of {@link FRONTEND_SESSION_ID_HEADER} on the inbound request, if any. */\n frontendSessionId?: string;\n /**\n * Trace the request's spans belong to (32 hex chars), minted by the shim.\n *\n * The shim mints it, rather than the sidecar deriving it from {@link requestId}, so span\n * assembly is stateless: a sidecar that had to remember `requestId → traceId` would give a\n * request's SERVER span and its CLIENT children different traces whenever it lost that memory\n * between two batches — which, for a Worker-hosted sidecar, an eviction can do at any moment.\n *\n * Optional so a sidecar keeps working against a shim that predates this field, in which case\n * it falls back to its own per-`requestId` map.\n */\n traceId?: string;\n /**\n * Span id of the request's SERVER span (16 hex chars), also minted by the shim: it is the\n * parent of every CLIENT span the request produced, and the inbound event's own span id.\n */\n serverSpanId?: string;\n}\n\ninterface BaseRequestEvent extends CorrelatedEvent {\n method: string;\n /** Full URL including query string. */\n url: string;\n /** Lower-cased header name → values. */\n requestHeaders: Record<string, string[]>;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the request failed before a response was produced. */\n error?: string;\n}\n\n/**\n * The worker's own inbound request (becomes a SERVER span). Bodies are not\n * captured, matching the Node backend recorder's SERVER-span contract.\n */\nexport interface InboundRequestEvent extends BaseRequestEvent {\n kind: \"inbound\";\n /**\n * Where {@link BaseRequestEvent.frontendSessionId} came from. Absent means the browser\n * minted it and sent it on the request, which is the normal case. `\"backend\"` means the\n * shim minted it for a document navigation the browser could not tag, and the page may or\n * may not have gone on to adopt it — a distinction ingestion needs, because an unadopted\n * backend-minted id names a session that will never exist and must be treated as unstamped.\n * The sidecar turns it into `meticulous.session_id_origin`.\n *\n * Optional, and therefore compatible with an older sidecar (which ignores it) — which is\n * why adding it does not bump {@link SIDECAR_PROTOCOL_VERSION}, a strict-equality check\n * whose bump would instead make this shim unusable against one.\n */\n sessionIdOrigin?: \"backend\";\n}\n\n/**\n * An outgoing `fetch` call made while handling an inbound request (becomes a\n * CLIENT span).\n */\nexport interface OutboundRequestEvent extends BaseRequestEvent {\n kind: \"outbound\";\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/**\n * A `fetch` through a Cloudflare binding — a service binding, an assets binding, or a\n * Durable Object stub — made while handling an inbound request (becomes a CLIENT span).\n *\n * Request/Response-shaped like {@link OutboundRequestEvent}, but a distinct kind because the\n * call never leaves the isolate and its URL is whatever the caller invented. Keeping the two\n * apart means a binding call and real egress can never be confused for one another when\n * these spans are eventually mocked.\n */\nexport interface BindingRequestEvent extends BaseRequestEvent {\n kind: \"binding\";\n /**\n * The `env` key the binding was found under. Absent when the instance was never seen on\n * `env` — most commonly a Durable Object stub, which `namespace.get()` produces rather\n * than being a binding in its own right.\n */\n bindingName?: string;\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\n/** KV namespace methods the shim records. */\nexport type KvOperation = \"get\" | \"getWithMetadata\" | \"put\" | \"delete\" | \"list\";\n\n/**\n * Why a KV value is absent from an event: workerd handed it over in a form that cannot be\n * persisted as text. A `\"stream\"` value must not even be read — consuming it would take the\n * bytes away from the app — and binary values are skipped deliberately, since KV blobs are\n * large and would both bloat the recording and be mangled by UTF-8 capture.\n */\nexport type KvOmittedReason = \"binary\" | \"stream\";\n\n/**\n * An operation on a Cloudflare KV namespace binding, made while handling an inbound request\n * (becomes a CLIENT span).\n *\n * KV is not HTTP: there is no method, URL or status code, so this event shares nothing with\n * {@link BaseRequestEvent} beyond correlation and timing. Values are JSON so that a single\n * `JSON.parse` reconstructs exactly what the app saw, whichever `type` the read asked for.\n */\nexport interface KvOperationEvent extends CorrelatedEvent {\n kind: \"kv\";\n /**\n * The `env` key the KV namespace was found under. Always present, unlike on a binding\n * event: a namespace has no factory equivalent to `DurableObjectNamespace.get()`, so one\n * that was never seen on `env` is not recorded at all.\n */\n bindingName: string;\n operation: KvOperation;\n /**\n * The key operated on, when it is a single string. Absent for `list` (which spans keys) and\n * for a bulk `get` (which takes an array) — both keep their arguments in {@link args}.\n */\n key?: string;\n /**\n * JSON of the call's arguments: the read `type`, the `put` options, the `list` selector.\n * A `put` value appears as `null` here — it is captured in {@link value} instead, so that\n * redaction applies to it.\n */\n args?: CapturedBody;\n /** JSON of the value a `put` wrote, with secret-looking fields redacted. */\n value?: CapturedBody;\n /**\n * JSON of what the operation returned: the value for `get`, `{value, metadata}` for\n * `getWithMetadata`, the key page for `list`. Absent for `put`/`delete`, which return\n * nothing, and for a value that could not be captured (see {@link omitted}).\n */\n result?: CapturedBody;\n /**\n * Why the operation's value is missing. Refers to the written value for `put` and to the\n * read value otherwise — a `put` has no result, so this is never ambiguous.\n */\n omitted?: KvOmittedReason;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the operation threw. */\n error?: string;\n}\n\n/**\n * One postgres.js query made while handling an inbound request (becomes a CLIENT span).\n *\n * Deliberately carries the same fields the Node recorder puts on a postgres.js span rather than\n * anything Worker-specific: `PostgresJsMockStore` builds its match key from those attributes at\n * load time, so a query recorded here replays through the existing Node mock path unchanged.\n * See `postgres/capture.ts` for the serialization, which both surfaces share.\n */\nexport interface PostgresQueryEvent extends CorrelatedEvent {\n kind: \"postgres\";\n /** SQL rebuilt from the tagged template's chunks, with `$1`, `$2`… placeholders. */\n queryText: string;\n /** JSON of the interpolated values, from `serializePostgresJsArgs`. */\n params: string;\n /** `\"\"`, `\"raw\"` or `\"values\"` — the row shape the query asked for. */\n rowMode: string;\n /** JSON of the resolved `Result`, capped at {@link MAX_POSTGRES_JS_RESULT_SIZE}. */\n result?: CapturedBody;\n /**\n * JSON of the rejection, from `serializePostgresJsError`. Set instead of {@link result}, and\n * distinct from the transport-level `error` string on the HTTP-shaped events: this one is a\n * recorded outcome the replay reproduces, not a note that capture failed.\n */\n errorJson?: string;\n startTimeMs: number;\n endTimeMs: number;\n}\n\nexport type CaptureEvent =\n | InboundRequestEvent\n | OutboundRequestEvent\n | BindingRequestEvent\n | KvOperationEvent\n | PostgresQueryEvent;\n\nexport interface CaptureEventsPayload {\n events: CaptureEvent[];\n}\n\n/**\n * Answer to `GET {SIDECAR_REPLAY_SESSION_PATH}?sessionId=<id>`: whether the sidecar holds\n * mocks for a session, and the virtual clock anchor to freeze `Date` at while serving it.\n */\nexport interface ReplaySessionInfoResponse {\n found: boolean;\n /**\n * Epoch ms of the session's last recorded activity. The worker freezes its clock here so\n * that credentials minted during the recording are already issued but not yet expired.\n */\n clockAnchorMs?: number;\n}\n\n/**\n * One request's line coverage, sent to {@link SIDECAR_REPLAY_COVERAGE_PATH}\n * after the response has been produced.\n *\n * `files` carries the build-time id→line map and is sent only on the first\n * report of an isolate: it is the same for every request the isolate serves and\n * is far larger than the hits themselves.\n */\nexport interface CoverageReportRequest {\n frontendSessionId: string;\n /** Global line ids marked during this request, ascending. */\n hitIds: number[];\n /** Present on an isolate's first report only. */\n files?: Array<{\n path: string;\n firstId: number;\n /** Inclusive 1-based line ranges, flattened as `[s1, e1, s2, e2, ...]`. */\n lineRanges: number[];\n }>;\n}\n\n/** Lookup for a single outbound `fetch`, sent to {@link SIDECAR_REPLAY_OUTBOUND_FETCH_PATH}. */\nexport interface OutboundFetchLookupRequest {\n frontendSessionId: string;\n replayId: string;\n method: string;\n /** Full URL including query string. */\n url: string;\n /**\n * Request body as captured by `readBodyWithCap`. The sidecar hashes this itself, so the\n * hash it compares against a recording is derived from byte-identical input on both the\n * record and replay sides.\n */\n requestBody?: CapturedBody;\n /**\n * Elapsed virtual time (ms from replay start) of the inbound request that\n * dispatched this outbound call, forwarded from {@link VIRTUAL_TIME_HEADER}.\n * The sidecar clusters candidates by recorded timestamp against this value.\n *\n * Additive and non-version-bumping: an older shim omits it, and matching\n * then hashes across every candidate at the matched key.\n */\n virtualTimeMs?: number;\n}\n\n/**\n * A recorded response to serve, or a signal the caller must fail the request. Replay is\n * hermetic, so the shim never turns any of these into a real call — nor an outcome it does\n * not recognise. The only live path is the request-side `meticulous-passthrough` header,\n * handled before the lookup, not any response here.\n */\nexport type OutboundFetchLookupResponse =\n | {\n outcome: \"mock\";\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n }\n /**\n * The store holds nothing for this call — a genuine miss, or a call the sidecar could not\n * look up at all (an unparseable URL). Replay is hermetic, so the shim fails the request\n * rather than letting it reach the real service — matching the Node backend recorder's\n * http/undici mocks. There is deliberately no \"let the call through\" outcome: the only\n * live path is the request-side `meticulous-passthrough` header.\n */\n | { outcome: \"no-mock\" };\n"],"names":[],"mappings":";;;;;;;;AAMA,MAAa,2BAA2B;;AAGxC,MAAa,kCACX;;;;;AAMF,MAAa,6BAA6B;;AAG1C,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,sBAAsB;;;;;AAMnC,MAAa,sBACX,UACuB;AACvB,KAAI,SAAS,QAAQ,UAAU,GAC7B;CAEF,MAAM,SAAS,OAAO,MAAM;AAC5B,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC;AAEF,QAAO;;;;;;;;;;;AAYT,MAAa,gCAAgC;AAE7C,MAAa,sBAAsB;;;;;;AAOnC,MAAa,8BAA8B;AAC3C,MAAa,qCAAqC;;;;;;;;AASlD,MAAa,+BAA+B;;;;;;;;;AAU5C,MAAa,4BACX;;;;;;;;AASF,MAAa,8BAA8B;;;;;;;;;;;AAY3C,MAAa,mBAAmB,CAC9B,gBACA,2BACD","debugId":"7851ef28-53e0-596f-80dc-a26f37790747"}
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="151bf2a1-75aa-51ce-be4a-6973d2815547")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="db9354dc-05d0-51c9-a548-f693044b9cc5")}catch(e){}}();
3
3
  import { warn, warnOnce } from "./log.js";
4
4
  import { readRequestBodyWithCap } from "./body-capture.js";
5
5
  import { METICULOUS_PASSTHROUGH_HEADER } from "./protocol.js";
@@ -34,6 +34,7 @@ const replayOutboundCall = async (ctx, request, invoke) => {
34
34
  replayId: ctx.replayId,
35
35
  method: request.method,
36
36
  url: request.url,
37
+ ...ctx.virtualTimeMs !== void 0 ? { virtualTimeMs: ctx.virtualTimeMs } : {},
37
38
  ...requestBody !== void 0 ? { requestBody } : {}
38
39
  };
39
40
  } catch (error) {
@@ -130,4 +131,4 @@ const buildMockResponse = (mock) => {
130
131
  export { replayOutboundCall };
131
132
 
132
133
  //# sourceMappingURL=replay-fetch.js.map
133
- //# debugId=151bf2a1-75aa-51ce-be4a-6973d2815547
134
+ //# debugId=db9354dc-05d0-51c9-a548-f693044b9cc5
@@ -1 +1 @@
1
- {"version":3,"file":"replay-fetch.js","sources":["../src/replay-fetch.ts"],"sourcesContent":["import { readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestReplayContext } from \"./context\";\nimport { warn, warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n METICULOUS_PASSTHROUGH_HEADER,\n type OutboundFetchLookupRequest,\n} from \"./protocol\";\nimport { postOutboundFetchLookup } from \"./sidecar-client\";\n\n/**\n * Serves a recorded response for an outbound call, or fails it.\n *\n * The mirror image of `captureOutboundCall`: recording tees a call that happens anyway,\n * whereas replay decides first and only then calls — so the sidecar sits in the request\n * path here.\n *\n * Replay is hermetic: a call the recording does not cover must not silently reach the real\n * service, since that turns a replay into live traffic against real credentials and hides\n * the gap in the recording behind a response no base replay will reproduce. So anything we\n * cannot serve from the recording — a miss, a call the sidecar cannot look up, an\n * unreachable sidecar, a mock that cannot be represented, or an outcome this shim does not\n * recognise — throws, matching the Node recorder's http/undici mocks.\n *\n * The only way out is an explicit `meticulous-passthrough: true` header on the request: a\n * call the customer has marked as one that must reach the real service. A failure to stub is\n * never treated as such a mark.\n */\nexport const replayOutboundCall = async (\n ctx: RequestReplayContext,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n if (hasPassthroughHeader(request)) {\n return invoke(request);\n }\n\n let lookup: OutboundFetchLookupRequest;\n try {\n // Read the clone and pass the original through untouched, mirroring the record path.\n // The sidecar hashes what we send here, and it was captured by the same\n // readRequestBodyWithCap the recorder used (redaction included), so the hashes on both\n // sides derive from byte-identical input.\n const requestBody = request.body\n ? await readRequestBodyWithCap(request.clone().body).catch(\n () => undefined,\n )\n : undefined;\n lookup = {\n frontendSessionId: ctx.frontendSessionId,\n replayId: ctx.replayId,\n method: request.method,\n url: request.url,\n ...(requestBody !== undefined ? { requestBody } : {}),\n };\n } catch (error) {\n // Without a lookup there is nothing to serve, and passing through would be exactly the\n // silent real call this path exists to prevent.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"could not prepare a mock lookup\",\n error,\n );\n }\n\n const result = await postOutboundFetchLookup(\n getOriginalFetch(),\n ctx.sidecarUrl,\n lookup,\n );\n if (result === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"the replay sidecar did not answer a mock lookup\",\n );\n }\n if (result.outcome === \"mock\") {\n const mocked = buildMockResponse(result);\n if (mocked === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n `the recorded response (status ${result.statusCode}) cannot be represented`,\n );\n }\n return mocked;\n }\n // Every other outcome is a failure to stub — a miss (`no-mock`) or an outcome this shim\n // does not recognise (e.g. a legacy `passthrough`). None may reach the real service, so we\n // fail the request; the only path to a live call is the explicit passthrough header\n // handled above.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n describeUnservedOutcome(result.outcome),\n );\n};\n\nconst describeUnservedOutcome = (outcome: string): string =>\n outcome === \"no-mock\"\n ? \"no recorded response\"\n : `an unrecognised sidecar outcome (${outcome})`;\n\nconst hasPassthroughHeader = (request: Request): boolean => {\n try {\n return request.headers.get(METICULOUS_PASSTHROUGH_HEADER) === \"true\";\n } catch {\n return false;\n }\n};\n\n/**\n * Builds — and logs — the error the app sees for a call replay cannot serve. Deliberately\n * names the shim and the session so an app-side stack trace is enough to tell a replay gap\n * from a real bug, and logs as well because an app that catches its own fetch errors would\n * otherwise swallow the only signal.\n *\n * The query string is left out on purpose — it routinely carries API keys, and the sidecar\n * logs the full path anyway.\n */\nconst buildReplayError = (\n request: Request,\n frontendSessionId: string,\n reason: string,\n cause?: unknown,\n): Error => {\n const error = new Error(\n `[backend-recorder] workerd replay: ${reason} for ${describeRequest(request)} (session ${frontendSessionId}). Set the \"${METICULOUS_PASSTHROUGH_HEADER}: true\" header on this request if it must reach the real service during replay.`,\n );\n if (cause !== undefined) {\n // `cause` in the constructor options is not universally available in workerd builds.\n (error as { cause?: unknown }).cause = cause;\n }\n warn(error.message);\n return error;\n};\n\nconst describeRequest = (request: Request): string => {\n let method = \"GET\";\n let url = \"\";\n try {\n method = request.method;\n url = request.url;\n } catch {\n return method;\n }\n try {\n const parsed = new URL(url);\n return `${method} ${parsed.origin}${parsed.pathname}`;\n } catch {\n return `${method} ${url}`;\n }\n};\n\n// Recorded bodies are already decoded and may have been truncated, so the recorded framing\n// headers no longer describe them; the Response constructor recomputes what it needs.\nconst HEADERS_TO_DROP = new Set([\n \"content-encoding\",\n \"content-length\",\n \"transfer-encoding\",\n]);\n\n// Statuses the Response constructor refuses to pair with a body.\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/**\n * Turns a recorded response into a real one, or undefined if it cannot be represented (the\n * constructor throws a RangeError on an out-of-range status, or a body on a null-body\n * status), in which case the caller fails the request.\n *\n * Note recorded headers are limited to CAPTURED_HEADERS, so a mocked response carries\n * essentially just content-type. In particular it can never set cookies — fine for the API\n * calls this path serves, but a real limitation for a backend that authenticates that way.\n */\nconst buildMockResponse = (mock: {\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n}): Response | undefined => {\n try {\n const headers = new Headers();\n for (const [name, value] of Object.entries(mock.headers)) {\n if (HEADERS_TO_DROP.has(name.toLowerCase())) {\n continue;\n }\n try {\n headers.set(name, value);\n } catch {\n // Skip a header name/value the runtime rejects rather than losing the whole mock.\n }\n }\n const body = NULL_BODY_STATUSES.has(mock.statusCode) ? null : mock.body;\n return new Response(body, { status: mock.statusCode, headers });\n } catch (error) {\n warnOnce(\n \"replay-mock-response\",\n \"Could not construct a mocked response.\",\n error,\n );\n return undefined;\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,qBAAqB,OAChC,KACA,SACA,WACsB;AACtB,KAAI,qBAAqB,QAAQ,CAC/B,QAAO,OAAO,QAAQ;CAGxB,IAAI;AACJ,KAAI;EAKF,MAAM,cAAc,QAAQ,OACxB,MAAM,uBAAuB,QAAQ,OAAO,CAAC,KAAK,CAAC,YAC3C,KAAA,EACP,GACD,KAAA;AACJ,WAAS;GACP,mBAAmB,IAAI;GACvB,UAAU,IAAI;GACd,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;GACrD;UACM,OAAO;AAGd,QAAM,iBACJ,SACA,IAAI,mBACJ,mCACA,MACD;;CAGH,MAAM,SAAS,MAAM,wBACnB,kBAAkB,EAClB,IAAI,YACJ,OACD;AACD,KAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,kDACD;AAEH,KAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,SAAS,kBAAkB,OAAO;AACxC,MAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,iCAAiC,OAAO,WAAW,yBACpD;AAEH,SAAO;;AAMT,OAAM,iBACJ,SACA,IAAI,mBACJ,wBAAwB,OAAO,QAAQ,CACxC;;AAGH,MAAM,2BAA2B,YAC/B,YAAY,YACR,yBACA,oCAAoC,QAAQ;AAElD,MAAM,wBAAwB,YAA8B;AAC1D,KAAI;AACF,SAAO,QAAQ,QAAQ,IAAI,8BAA8B,KAAK;SACxD;AACN,SAAO;;;;;;;;;;;;AAaX,MAAM,oBACJ,SACA,mBACA,QACA,UACU;CACV,MAAM,wBAAQ,IAAI,MAChB,sCAAsC,OAAO,OAAO,gBAAgB,QAAQ,CAAC,YAAY,kBAAkB,cAAc,8BAA8B,iFACxJ;AACD,KAAI,UAAU,KAAA,EAEX,OAA8B,QAAQ;AAEzC,MAAK,MAAM,QAAQ;AACnB,QAAO;;AAGT,MAAM,mBAAmB,YAA6B;CACpD,IAAI,SAAS;CACb,IAAI,MAAM;AACV,KAAI;AACF,WAAS,QAAQ;AACjB,QAAM,QAAQ;SACR;AACN,SAAO;;AAET,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,SAAO,GAAG,OAAO,GAAG,OAAO,SAAS,OAAO;SACrC;AACN,SAAO,GAAG,OAAO,GAAG;;;AAMxB,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACD,CAAC;AAGF,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAI,CAAC;;;;;;;;;;AAWxD,MAAM,qBAAqB,SAIC;AAC1B,KAAI;EACF,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE;AACxD,OAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC;AAEF,OAAI;AACF,YAAQ,IAAI,MAAM,MAAM;WAClB;;EAIV,MAAM,OAAO,mBAAmB,IAAI,KAAK,WAAW,GAAG,OAAO,KAAK;AACnE,SAAO,IAAI,SAAS,MAAM;GAAE,QAAQ,KAAK;GAAY;GAAS,CAAC;UACxD,OAAO;AACd,WACE,wBACA,0CACA,MACD;AACD","debugId":"151bf2a1-75aa-51ce-be4a-6973d2815547"}
1
+ {"version":3,"file":"replay-fetch.js","sources":["../src/replay-fetch.ts"],"sourcesContent":["import { readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestReplayContext } from \"./context\";\nimport { warn, warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n METICULOUS_PASSTHROUGH_HEADER,\n type OutboundFetchLookupRequest,\n} from \"./protocol\";\nimport { postOutboundFetchLookup } from \"./sidecar-client\";\n\n/**\n * Serves a recorded response for an outbound call, or fails it.\n *\n * The mirror image of `captureOutboundCall`: recording tees a call that happens anyway,\n * whereas replay decides first and only then calls — so the sidecar sits in the request\n * path here.\n *\n * Replay is hermetic: a call the recording does not cover must not silently reach the real\n * service, since that turns a replay into live traffic against real credentials and hides\n * the gap in the recording behind a response no base replay will reproduce. So anything we\n * cannot serve from the recording — a miss, a call the sidecar cannot look up, an\n * unreachable sidecar, a mock that cannot be represented, or an outcome this shim does not\n * recognise — throws, matching the Node recorder's http/undici mocks.\n *\n * The only way out is an explicit `meticulous-passthrough: true` header on the request: a\n * call the customer has marked as one that must reach the real service. A failure to stub is\n * never treated as such a mark.\n */\nexport const replayOutboundCall = async (\n ctx: RequestReplayContext,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n if (hasPassthroughHeader(request)) {\n return invoke(request);\n }\n\n let lookup: OutboundFetchLookupRequest;\n try {\n // Read the clone and pass the original through untouched, mirroring the record path.\n // The sidecar hashes what we send here, and it was captured by the same\n // readRequestBodyWithCap the recorder used (redaction included), so the hashes on both\n // sides derive from byte-identical input.\n const requestBody = request.body\n ? await readRequestBodyWithCap(request.clone().body).catch(\n () => undefined,\n )\n : undefined;\n lookup = {\n frontendSessionId: ctx.frontendSessionId,\n replayId: ctx.replayId,\n method: request.method,\n url: request.url,\n ...(ctx.virtualTimeMs !== undefined\n ? { virtualTimeMs: ctx.virtualTimeMs }\n : {}),\n ...(requestBody !== undefined ? { requestBody } : {}),\n };\n } catch (error) {\n // Without a lookup there is nothing to serve, and passing through would be exactly the\n // silent real call this path exists to prevent.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"could not prepare a mock lookup\",\n error,\n );\n }\n\n const result = await postOutboundFetchLookup(\n getOriginalFetch(),\n ctx.sidecarUrl,\n lookup,\n );\n if (result === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n \"the replay sidecar did not answer a mock lookup\",\n );\n }\n if (result.outcome === \"mock\") {\n const mocked = buildMockResponse(result);\n if (mocked === undefined) {\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n `the recorded response (status ${result.statusCode}) cannot be represented`,\n );\n }\n return mocked;\n }\n // Every other outcome is a failure to stub — a miss (`no-mock`) or an outcome this shim\n // does not recognise (e.g. a legacy `passthrough`). None may reach the real service, so we\n // fail the request; the only path to a live call is the explicit passthrough header\n // handled above.\n throw buildReplayError(\n request,\n ctx.frontendSessionId,\n describeUnservedOutcome(result.outcome),\n );\n};\n\nconst describeUnservedOutcome = (outcome: string): string =>\n outcome === \"no-mock\"\n ? \"no recorded response\"\n : `an unrecognised sidecar outcome (${outcome})`;\n\nconst hasPassthroughHeader = (request: Request): boolean => {\n try {\n return request.headers.get(METICULOUS_PASSTHROUGH_HEADER) === \"true\";\n } catch {\n return false;\n }\n};\n\n/**\n * Builds — and logs — the error the app sees for a call replay cannot serve. Deliberately\n * names the shim and the session so an app-side stack trace is enough to tell a replay gap\n * from a real bug, and logs as well because an app that catches its own fetch errors would\n * otherwise swallow the only signal.\n *\n * The query string is left out on purpose — it routinely carries API keys, and the sidecar\n * logs the full path anyway.\n */\nconst buildReplayError = (\n request: Request,\n frontendSessionId: string,\n reason: string,\n cause?: unknown,\n): Error => {\n const error = new Error(\n `[backend-recorder] workerd replay: ${reason} for ${describeRequest(request)} (session ${frontendSessionId}). Set the \"${METICULOUS_PASSTHROUGH_HEADER}: true\" header on this request if it must reach the real service during replay.`,\n );\n if (cause !== undefined) {\n // `cause` in the constructor options is not universally available in workerd builds.\n (error as { cause?: unknown }).cause = cause;\n }\n warn(error.message);\n return error;\n};\n\nconst describeRequest = (request: Request): string => {\n let method = \"GET\";\n let url = \"\";\n try {\n method = request.method;\n url = request.url;\n } catch {\n return method;\n }\n try {\n const parsed = new URL(url);\n return `${method} ${parsed.origin}${parsed.pathname}`;\n } catch {\n return `${method} ${url}`;\n }\n};\n\n// Recorded bodies are already decoded and may have been truncated, so the recorded framing\n// headers no longer describe them; the Response constructor recomputes what it needs.\nconst HEADERS_TO_DROP = new Set([\n \"content-encoding\",\n \"content-length\",\n \"transfer-encoding\",\n]);\n\n// Statuses the Response constructor refuses to pair with a body.\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/**\n * Turns a recorded response into a real one, or undefined if it cannot be represented (the\n * constructor throws a RangeError on an out-of-range status, or a body on a null-body\n * status), in which case the caller fails the request.\n *\n * Note recorded headers are limited to CAPTURED_HEADERS, so a mocked response carries\n * essentially just content-type. In particular it can never set cookies — fine for the API\n * calls this path serves, but a real limitation for a backend that authenticates that way.\n */\nconst buildMockResponse = (mock: {\n statusCode: number;\n body: string;\n headers: Record<string, string>;\n}): Response | undefined => {\n try {\n const headers = new Headers();\n for (const [name, value] of Object.entries(mock.headers)) {\n if (HEADERS_TO_DROP.has(name.toLowerCase())) {\n continue;\n }\n try {\n headers.set(name, value);\n } catch {\n // Skip a header name/value the runtime rejects rather than losing the whole mock.\n }\n }\n const body = NULL_BODY_STATUSES.has(mock.statusCode) ? null : mock.body;\n return new Response(body, { status: mock.statusCode, headers });\n } catch (error) {\n warnOnce(\n \"replay-mock-response\",\n \"Could not construct a mocked response.\",\n error,\n );\n return undefined;\n }\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,qBAAqB,OAChC,KACA,SACA,WACsB;AACtB,KAAI,qBAAqB,QAAQ,CAC/B,QAAO,OAAO,QAAQ;CAGxB,IAAI;AACJ,KAAI;EAKF,MAAM,cAAc,QAAQ,OACxB,MAAM,uBAAuB,QAAQ,OAAO,CAAC,KAAK,CAAC,YAC3C,KAAA,EACP,GACD,KAAA;AACJ,WAAS;GACP,mBAAmB,IAAI;GACvB,UAAU,IAAI;GACd,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,GAAI,IAAI,kBAAkB,KAAA,IACtB,EAAE,eAAe,IAAI,eAAe,GACpC,EAAE;GACN,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;GACrD;UACM,OAAO;AAGd,QAAM,iBACJ,SACA,IAAI,mBACJ,mCACA,MACD;;CAGH,MAAM,SAAS,MAAM,wBACnB,kBAAkB,EAClB,IAAI,YACJ,OACD;AACD,KAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,kDACD;AAEH,KAAI,OAAO,YAAY,QAAQ;EAC7B,MAAM,SAAS,kBAAkB,OAAO;AACxC,MAAI,WAAW,KAAA,EACb,OAAM,iBACJ,SACA,IAAI,mBACJ,iCAAiC,OAAO,WAAW,yBACpD;AAEH,SAAO;;AAMT,OAAM,iBACJ,SACA,IAAI,mBACJ,wBAAwB,OAAO,QAAQ,CACxC;;AAGH,MAAM,2BAA2B,YAC/B,YAAY,YACR,yBACA,oCAAoC,QAAQ;AAElD,MAAM,wBAAwB,YAA8B;AAC1D,KAAI;AACF,SAAO,QAAQ,QAAQ,IAAI,8BAA8B,KAAK;SACxD;AACN,SAAO;;;;;;;;;;;;AAaX,MAAM,oBACJ,SACA,mBACA,QACA,UACU;CACV,MAAM,wBAAQ,IAAI,MAChB,sCAAsC,OAAO,OAAO,gBAAgB,QAAQ,CAAC,YAAY,kBAAkB,cAAc,8BAA8B,iFACxJ;AACD,KAAI,UAAU,KAAA,EAEX,OAA8B,QAAQ;AAEzC,MAAK,MAAM,QAAQ;AACnB,QAAO;;AAGT,MAAM,mBAAmB,YAA6B;CACpD,IAAI,SAAS;CACb,IAAI,MAAM;AACV,KAAI;AACF,WAAS,QAAQ;AACjB,QAAM,QAAQ;SACR;AACN,SAAO;;AAET,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,SAAO,GAAG,OAAO,GAAG,OAAO,SAAS,OAAO;SACrC;AACN,SAAO,GAAG,OAAO,GAAG;;;AAMxB,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACD,CAAC;AAGF,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAI,CAAC;;;;;;;;;;AAWxD,MAAM,qBAAqB,SAIC;AAC1B,KAAI;EACF,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE;AACxD,OAAI,gBAAgB,IAAI,KAAK,aAAa,CAAC,CACzC;AAEF,OAAI;AACF,YAAQ,IAAI,MAAM,MAAM;WAClB;;EAIV,MAAM,OAAO,mBAAmB,IAAI,KAAK,WAAW,GAAG,OAAO,KAAK;AACnE,SAAO,IAAI,SAAS,MAAM;GAAE,QAAQ,KAAK;GAAY;GAAS,CAAC;UACxD,OAAO;AACd,WACE,wBACA,0CACA,MACD;AACD","debugId":"db9354dc-05d0-51c9-a548-f693044b9cc5"}
package/dist/version.js CHANGED
@@ -9,10 +9,10 @@
9
9
  * (vitest, typecheck) working, where no bundler has substituted the value.
10
10
  */
11
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]="90d63f09-8235-55d7-bcd6-ad69626771bc")}catch(e){}}();
13
- const WORKERD_SHIM_VERSION = "2.330.0";
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]="59d03d8a-6010-5910-963f-cdfa0b2ec0ac")}catch(e){}}();
13
+ const WORKERD_SHIM_VERSION = "2.331.0";
14
14
  //#endregion
15
15
  export { WORKERD_SHIM_VERSION };
16
16
 
17
17
  //# sourceMappingURL=version.js.map
18
- //# debugId=90d63f09-8235-55d7-bcd6-ad69626771bc
18
+ //# debugId=59d03d8a-6010-5910-963f-cdfa0b2ec0ac
@@ -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":"90d63f09-8235-55d7-bcd6-ad69626771bc"}
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":"59d03d8a-6010-5910-963f-cdfa0b2ec0ac"}
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.330.0",
3
+ "version": "2.331.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",