@alwaysmeticulous/backend-recorder-workerd 2.325.0 → 2.326.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/README.md +94 -4
- package/dist/binding-patch.js +17 -8
- package/dist/binding-patch.js.map +1 -1
- package/dist/capture-buffer.js +114 -0
- package/dist/capture-buffer.js.map +1 -0
- package/dist/context.js +6 -3
- package/dist/context.js.map +1 -1
- package/dist/error-capture.d.ts +44 -0
- package/dist/error-capture.js +65 -0
- package/dist/error-capture.js.map +1 -0
- package/dist/fetch-patch.js +5 -5
- package/dist/fetch-patch.js.map +1 -1
- package/dist/index.d.ts +62 -39
- package/dist/index.js +65 -179
- package/dist/index.js.map +1 -1
- package/dist/kv-patch.js +9 -9
- package/dist/kv-patch.js.map +1 -1
- package/dist/outbound-capture.js +20 -28
- package/dist/outbound-capture.js.map +1 -1
- package/dist/pages-function.d.ts +33 -0
- package/dist/pages-function.js +28 -0
- package/dist/pages-function.js.map +1 -0
- package/dist/postgres/capture.d.ts +108 -0
- package/dist/postgres/capture.js +136 -0
- package/dist/postgres/capture.js.map +1 -0
- package/dist/postgres/with-meticulous-postgres.d.ts +32 -0
- package/dist/postgres/with-meticulous-postgres.js +152 -0
- package/dist/postgres/with-meticulous-postgres.js.map +1 -0
- package/dist/protocol.d.ts +76 -11
- package/dist/protocol.js +12 -7
- package/dist/protocol.js.map +1 -1
- package/dist/provisional-session-id.d.ts +58 -0
- package/dist/provisional-session-id.js +75 -0
- package/dist/provisional-session-id.js.map +1 -0
- package/dist/publish-session-id.js +41 -0
- package/dist/publish-session-id.js.map +1 -0
- package/dist/replay-fetch.js +4 -3
- package/dist/replay-fetch.js.map +1 -1
- package/dist/sidecar-client.js +29 -18
- package/dist/sidecar-client.js.map +1 -1
- package/dist/sidecar-transport.d.ts +38 -0
- package/dist/sidecar-transport.js +52 -0
- package/dist/sidecar-transport.js.map +1 -0
- package/dist/spans/attributes.d.ts +84 -0
- package/dist/spans/attributes.js +88 -0
- package/dist/spans/attributes.js.map +1 -0
- package/dist/spans/readable-span.d.ts +78 -0
- package/dist/spans/readable-span.js +35 -0
- package/dist/spans/readable-span.js.map +1 -0
- package/dist/spans/serialize-span.d.ts +14 -0
- package/dist/spans/serialize-span.js +38 -0
- package/dist/spans/serialize-span.js.map +1 -0
- package/dist/spans/span-builder.d.ts +61 -0
- package/dist/spans/span-builder.js +250 -0
- package/dist/spans/span-builder.js.map +1 -0
- package/dist/spans/url-redaction.d.ts +14 -0
- package/dist/spans/url-redaction.js +40 -0
- package/dist/spans/url-redaction.js.map +1 -0
- package/dist/with-meticulous.d.ts +32 -0
- package/dist/with-meticulous.js +0 -0
- package/dist/with-meticulous.js.map +1 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY, SidecarFetcher, SidecarTransport } from "./sidecar-transport.js";
|
|
2
|
+
import { MeticulousExecutionContext, WithMeticulousOptions } from "./with-meticulous.js";
|
|
3
|
+
import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPayload, CapturedBody, 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_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
|
|
2
4
|
import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "./body-capture.js";
|
|
3
5
|
import { headersToRecord } from "./outbound-capture.js";
|
|
4
6
|
import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
|
|
7
|
+
import { MeticulousPagesFunction, MeticulousPagesFunctionContext, withMeticulousPagesFunction } from "./pages-function.js";
|
|
5
8
|
import { KvCaptureFields, KvCaptureOutcome, serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
|
|
9
|
+
import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
|
|
10
|
+
import { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, PostgresJsQueryLike, PostgresJsRowMode, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./postgres/capture.js";
|
|
11
|
+
import { withMeticulousPostgres } from "./postgres/with-meticulous-postgres.js";
|
|
12
|
+
import { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR } from "./spans/attributes.js";
|
|
13
|
+
import { HrTime, ReadableSpan, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, SerializedSpanBase, SpanStatus, hrTimeToMs, msToHrTime } from "./spans/readable-span.js";
|
|
14
|
+
import { serializeSpan } from "./spans/serialize-span.js";
|
|
15
|
+
import { SpanBuilder, randomHex } from "./spans/span-builder.js";
|
|
16
|
+
import { redactUrlCredentials } from "./spans/url-redaction.js";
|
|
17
|
+
import { HeaderReader, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
6
18
|
|
|
7
19
|
//#region src/index.d.ts
|
|
8
|
-
/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */
|
|
9
|
-
interface MeticulousExecutionContext {
|
|
10
|
-
waitUntil(promise: Promise<unknown>): void;
|
|
11
|
-
}
|
|
12
20
|
/** Structural subset of an ES-module Worker handler (`export default {...}`). */
|
|
13
21
|
interface MeticulousWorkerHandler<Env = never> {
|
|
14
22
|
fetch(request: Request, env: Env, ctx: MeticulousExecutionContext): Response | Promise<Response>;
|
|
15
23
|
}
|
|
16
|
-
interface WithMeticulousOptions {
|
|
17
|
-
/** Overrides the sidecar origin; defaults to the METICULOUS_SIDECAR_URL var/binding. */
|
|
18
|
-
sidecarUrl?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Binding names to leave unrecorded, on top of the defaults (`ASSETS`,
|
|
21
|
-
* `__STATIC_CONTENT`). Use this for a binding whose traffic is high-volume or binary.
|
|
22
|
-
* Applies to KV namespaces as well as `fetch`-shaped bindings.
|
|
23
|
-
*/
|
|
24
|
-
skipBindings?: readonly string[];
|
|
25
|
-
}
|
|
26
24
|
/**
|
|
27
25
|
* Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP
|
|
28
26
|
* behaviour:
|
|
@@ -31,33 +29,58 @@ interface WithMeticulousOptions {
|
|
|
31
29
|
* async fetch(request, env, ctx) { ... },
|
|
32
30
|
* });
|
|
33
31
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* the
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
32
|
+
* For a Cloudflare Pages project, whose worker exports `onRequest` instead, use
|
|
33
|
+
* {@link withMeticulousPagesFunction} — same behaviour, different handler shape.
|
|
34
|
+
*
|
|
35
|
+
* **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**
|
|
36
|
+
* Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)
|
|
37
|
+
* or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),
|
|
38
|
+
* or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through
|
|
39
|
+
* `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js
|
|
40
|
+
* queries are reported to the sidecar, batched into one report per request, without affecting the
|
|
41
|
+
* app. A binding beats a var if both are configured: only the binding can have been added to
|
|
42
|
+
* *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels
|
|
43
|
+
* with an image.
|
|
44
|
+
*
|
|
45
|
+
* **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the
|
|
46
|
+
* Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then
|
|
47
|
+
* served from the recording instead of reaching the real service — and a call the recording does
|
|
48
|
+
* not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`
|
|
49
|
+
* header on a request that must stay real. The clock is frozen at the recorded session's end so
|
|
50
|
+
* recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /
|
|
51
|
+
* `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that
|
|
52
|
+
* session. Workerd cannot read container environment variables, which is why per-replay config
|
|
53
|
+
* arrives as a request header; the shim validates it and only honours a loopback /
|
|
54
|
+
* docker-gateway / private-network `http:` origin.
|
|
55
|
+
*
|
|
56
|
+
* Replay takes precedence over recording when both are configured. A stale sidecar var baked into
|
|
57
|
+
* an image is far more likely than a spurious replay header (nothing but the replay runner emits
|
|
58
|
+
* one), and letting the env win would silently record a replay instead of mocking it.
|
|
55
59
|
*
|
|
56
60
|
* With neither configured the wrapper is a complete pass-through, so it is safe to keep in
|
|
57
|
-
* deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A
|
|
58
|
-
*
|
|
61
|
+
* deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture
|
|
62
|
+
* failure or unreachable sidecar never affects the app.
|
|
59
63
|
*/
|
|
60
64
|
declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler: H, options?: WithMeticulousOptions) => H;
|
|
65
|
+
/**
|
|
66
|
+
* The Meticulous session id of the request currently being served, or `undefined` if this
|
|
67
|
+
* call cannot be tied to one — including whenever the wrapper is a pass-through.
|
|
68
|
+
*
|
|
69
|
+
* Exposed so a server-rendering worker can put the id into the HTML it is about to return,
|
|
70
|
+
* typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,
|
|
71
|
+
* cache-proof alternative to the `Server-Timing` header the shim publishes automatically;
|
|
72
|
+
* doing both is fine, since the recorder prefers the rendered value. It is also how the id
|
|
73
|
+
* reaches another process — a document navigation has no inbound header for the app to
|
|
74
|
+
* forward, so a worker that fans out to its own services has to pass this on as
|
|
75
|
+
* `x-meticulous-session-id` itself.
|
|
76
|
+
*
|
|
77
|
+
* During a replay this returns the replayed session's id, which is the same value the render
|
|
78
|
+
* being replayed was given when it was recorded (the page adopted the minted id, so that is
|
|
79
|
+
* what the recording is stored under, and it is what the replay runner injects back). An app
|
|
80
|
+
* that renders the id therefore renders the same bytes both times, rather than the id turning
|
|
81
|
+
* every server-rendered page into a diff.
|
|
82
|
+
*/
|
|
83
|
+
declare const getMeticulousSessionId: () => string | undefined;
|
|
61
84
|
//#endregion
|
|
62
|
-
export { type BindingRequestEvent, CAPTURED_HEADERS, type CaptureEvent, type CaptureEventsPayload, type CapturedBody, FRONTEND_SESSION_ID_HEADER, type InboundRequestEvent, type KvCaptureFields, type KvCaptureOutcome, type KvOmittedReason, type KvOperation, type KvOperationEvent, MAX_BODY_CAPTURE_SIZE, METICULOUS_PASSTHROUGH_HEADER, MeticulousExecutionContext, MeticulousWorkerHandler, type OutboundFetchLookupRequest, type OutboundFetchLookupResponse, type OutboundRequestEvent, REPLAY_SIDECAR_URL_HEADER, type ReplaySessionInfoResponse, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, STR_REDACTED, WithMeticulousOptions, headersToRecord, readBodyWithCap, readRequestBodyWithCap, redactRequestBody, serializeKvArgs, serializeKvCaptureFields, withMeticulous };
|
|
85
|
+
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, 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_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, type WithMeticulousOptions, buildServerTimingSessionEntry, deserializeCapturedError, getMeticulousSessionId, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
|
|
63
86
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
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]="
|
|
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]="f420c8b0-634e-586e-b9bf-34d3afde7944")}catch(e){}}();
|
|
3
|
+
import { SIDECAR_BINDING_ENV_KEY, SIDECAR_BINDING_ORIGIN, SIDECAR_URL_ENV_KEY } from "./sidecar-transport.js";
|
|
3
4
|
import { requestCaptureContext } from "./context.js";
|
|
4
|
-
import { warnOnce } from "./log.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 {
|
|
8
|
-
import { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, METICULOUS_PASSTHROUGH_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
|
|
9
|
-
import { getReplaySessionInfo, postCaptureEvents } from "./sidecar-client.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_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH } from "./protocol.js";
|
|
10
8
|
import { headersToRecord } from "./outbound-capture.js";
|
|
11
|
-
import { installBindingPatch } from "./binding-patch.js";
|
|
12
|
-
import { installFetchPatch } from "./fetch-patch.js";
|
|
13
9
|
import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
10
|
+
import { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
11
|
+
import { deserializeCapturedError, serializeCapturedError } from "./error-capture.js";
|
|
12
|
+
import { MAX_POSTGRES_JS_RESULT_SIZE, POSTGRES_JS_ERROR_ATTR, POSTGRES_JS_QUERY_PARAMS_ATTR, POSTGRES_JS_QUERY_TEXT_ATTR, POSTGRES_JS_RESULT_ATTR, POSTGRES_JS_RESULT_TRUNCATED_ATTR, POSTGRES_JS_ROW_MODE_ATTR, isSupportedPostgresJsQuery, postgresJsCommandOf, reconstructQueryText, resolveRowMode, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult } from "./postgres/capture.js";
|
|
13
|
+
import { CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, SESSION_ID_ORIGIN_ATTR, SESSION_ID_ORIGIN_BACKEND, WORKERD_BINDING_NAME_ATTR, WORKERD_KV_ARGS_ATTR, WORKERD_KV_ARGS_TRUNCATED_ATTR, WORKERD_KV_KEY_ATTR, WORKERD_KV_OMITTED_ATTR, WORKERD_KV_OPERATION_ATTR, WORKERD_KV_RESULT_ATTR, WORKERD_KV_RESULT_TRUNCATED_ATTR, WORKERD_KV_VALUE_ATTR, WORKERD_KV_VALUE_TRUNCATED_ATTR } from "./spans/attributes.js";
|
|
14
|
+
import { SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, hrTimeToMs, msToHrTime } from "./spans/readable-span.js";
|
|
15
|
+
import { redactUrlCredentials } from "./spans/url-redaction.js";
|
|
16
|
+
import { SpanBuilder, randomHex } from "./spans/span-builder.js";
|
|
17
|
+
import { runWithMeticulous } from "./with-meticulous.js";
|
|
18
|
+
import { withMeticulousPagesFunction } from "./pages-function.js";
|
|
19
|
+
import { withMeticulousPostgres } from "./postgres/with-meticulous-postgres.js";
|
|
20
|
+
import { serializeSpan } from "./spans/serialize-span.js";
|
|
18
21
|
//#region src/index.ts
|
|
19
22
|
/**
|
|
20
23
|
* Wraps an ES-module Worker handler so Meticulous can record, or replay, the app's HTTP
|
|
@@ -24,188 +27,71 @@ import { installVirtualRandom } from "./virtual-random.js";
|
|
|
24
27
|
* async fetch(request, env, ctx) { ... },
|
|
25
28
|
* });
|
|
26
29
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* calls, calls through `fetch`-shaped bindings and KV namespace operations are reported to
|
|
30
|
-
* the sidecar as capture events, without affecting the app.
|
|
30
|
+
* For a Cloudflare Pages project, whose worker exports `onRequest` instead, use
|
|
31
|
+
* {@link withMeticulousPagesFunction} — same behaviour, different handler shape.
|
|
31
32
|
*
|
|
32
|
-
* **
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* request header; the shim validates it and only honours a loopback / docker-gateway /
|
|
42
|
-
* private-network `http:` origin.
|
|
33
|
+
* **Recording** activates on either a `METICULOUS_SIDECAR` service binding (how a **deployed**
|
|
34
|
+
* Worker records: the binding points at a Meticulous recorder sidecar Worker in the same account)
|
|
35
|
+
* or a `METICULOUS_SIDECAR_URL` var (how `wrangler dev` records against a local sidecar process),
|
|
36
|
+
* or the matching `options`. Inbound requests, outgoing `fetch` calls, calls through
|
|
37
|
+
* `fetch`-shaped bindings, KV namespace operations and — with `withMeticulousPostgres` — postgres.js
|
|
38
|
+
* queries are reported to the sidecar, batched into one report per request, without affecting the
|
|
39
|
+
* app. A binding beats a var if both are configured: only the binding can have been added to
|
|
40
|
+
* *this* deployment's wrangler configuration deliberately, whereas a stale `.dev.vars` travels
|
|
41
|
+
* with an image.
|
|
43
42
|
*
|
|
44
|
-
* Replay
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by the
|
|
44
|
+
* Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are then
|
|
45
|
+
* served from the recording instead of reaching the real service — and a call the recording does
|
|
46
|
+
* not cover fails, rather than quietly becoming live traffic; put a `meticulous-passthrough: true`
|
|
47
|
+
* header on a request that must stay real. The clock is frozen at the recorded session's end so
|
|
48
|
+
* recorded credentials are still valid, and `Math.random` / `crypto.randomUUID` /
|
|
49
|
+
* `crypto.getRandomValues` are seeded so ids the app mints are the same in every replay of that
|
|
50
|
+
* session. Workerd cannot read container environment variables, which is why per-replay config
|
|
51
|
+
* arrives as a request header; the shim validates it and only honours a loopback /
|
|
52
|
+
* docker-gateway / private-network `http:` origin.
|
|
53
|
+
*
|
|
54
|
+
* Replay takes precedence over recording when both are configured. A stale sidecar var baked into
|
|
55
|
+
* an image is far more likely than a spurious replay header (nothing but the replay runner emits
|
|
56
|
+
* one), and letting the env win would silently record a replay instead of mocking it.
|
|
48
57
|
*
|
|
49
58
|
* With neither configured the wrapper is a complete pass-through, so it is safe to keep in
|
|
50
|
-
* deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A
|
|
51
|
-
*
|
|
59
|
+
* deployed code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag. A capture
|
|
60
|
+
* failure or unreachable sidecar never affects the app.
|
|
52
61
|
*/
|
|
53
62
|
const withMeticulous = (handler, options) => {
|
|
54
|
-
const wrappedFetch =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sidecarUrl = replayContext === void 0 ? resolveSidecarUrl(options, env) : void 0;
|
|
61
|
-
if (replayContext !== void 0 || sidecarUrl !== void 0) installFetchPatch();
|
|
62
|
-
if (sidecarUrl !== void 0) {
|
|
63
|
-
installBindingPatch(env, { skipBindings: options?.skipBindings });
|
|
64
|
-
installKvPatch(env, { skipBindings: options?.skipBindings });
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
warnOnce("shim-init", "Failed to initialize the Meticulous backend recorder shim — recording disabled.", error);
|
|
68
|
-
replayContext = void 0;
|
|
69
|
-
sidecarUrl = void 0;
|
|
70
|
-
}
|
|
71
|
-
if (replayContext !== void 0) {
|
|
72
|
-
installVirtualClock();
|
|
73
|
-
installVirtualRandom();
|
|
74
|
-
return requestCaptureContext.run(replayContext, invokeHandler);
|
|
75
|
-
}
|
|
76
|
-
if (sidecarUrl === void 0) return invokeHandler();
|
|
77
|
-
const captureCtx = buildCaptureContext(request, sidecarUrl, ctx);
|
|
78
|
-
const startTimeMs = Date.now();
|
|
79
|
-
let requestHeaders = {};
|
|
80
|
-
let method = "GET";
|
|
81
|
-
let url = "";
|
|
82
|
-
try {
|
|
83
|
-
requestHeaders = headersToRecord(request.headers);
|
|
84
|
-
method = request.method;
|
|
85
|
-
url = request.url;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
warnOnce("inbound-capture", "Failed to capture the inbound request.", error);
|
|
88
|
-
}
|
|
89
|
-
const reportInbound = (outcome) => {
|
|
90
|
-
try {
|
|
91
|
-
const event = {
|
|
92
|
-
kind: "inbound",
|
|
93
|
-
requestId: captureCtx.requestId,
|
|
94
|
-
...captureCtx.frontendSessionId !== void 0 ? { frontendSessionId: captureCtx.frontendSessionId } : {},
|
|
95
|
-
method,
|
|
96
|
-
url,
|
|
97
|
-
requestHeaders,
|
|
98
|
-
startTimeMs,
|
|
99
|
-
endTimeMs: Date.now(),
|
|
100
|
-
...outcome
|
|
101
|
-
};
|
|
102
|
-
captureCtx.waitUntil(postCaptureEvents(getOriginalFetch(), sidecarUrl, [event]));
|
|
103
|
-
} catch (error) {
|
|
104
|
-
warnOnce("inbound-report", "Failed to report the inbound request.", error);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
let response;
|
|
108
|
-
try {
|
|
109
|
-
response = await requestCaptureContext.run(captureCtx, invokeHandler);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
reportInbound({ error: String(error) });
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
reportInbound({
|
|
116
|
-
statusCode: response.status,
|
|
117
|
-
responseHeaders: headersToRecord(response.headers)
|
|
118
|
-
});
|
|
119
|
-
} catch (error) {
|
|
120
|
-
warnOnce("inbound-report", "Failed to report the inbound request.", error);
|
|
121
|
-
}
|
|
122
|
-
return response;
|
|
123
|
-
};
|
|
63
|
+
const wrappedFetch = (request, env, ctx) => runWithMeticulous({
|
|
64
|
+
request,
|
|
65
|
+
env,
|
|
66
|
+
ctx,
|
|
67
|
+
invokeHandler: () => handler.fetch(request, env, ctx)
|
|
68
|
+
}, options);
|
|
124
69
|
return {
|
|
125
70
|
...handler,
|
|
126
71
|
fetch: wrappedFetch
|
|
127
72
|
};
|
|
128
73
|
};
|
|
129
74
|
/**
|
|
130
|
-
*
|
|
75
|
+
* The Meticulous session id of the request currently being served, or `undefined` if this
|
|
76
|
+
* call cannot be tied to one — including whenever the wrapper is a pass-through.
|
|
77
|
+
*
|
|
78
|
+
* Exposed so a server-rendering worker can put the id into the HTML it is about to return,
|
|
79
|
+
* typically as `data-session-id` on the recorder's own `<script>` tag. That is the exact,
|
|
80
|
+
* cache-proof alternative to the `Server-Timing` header the shim publishes automatically;
|
|
81
|
+
* doing both is fine, since the recorder prefers the rendered value. It is also how the id
|
|
82
|
+
* reaches another process — a document navigation has no inbound header for the app to
|
|
83
|
+
* forward, so a worker that fans out to its own services has to pass this on as
|
|
84
|
+
* `x-meticulous-session-id` itself.
|
|
131
85
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
86
|
+
* During a replay this returns the replayed session's id, which is the same value the render
|
|
87
|
+
* being replayed was given when it was recorded (the page adopted the minted id, so that is
|
|
88
|
+
* what the recording is stored under, and it is what the replay runner injects back). An app
|
|
89
|
+
* that renders the id therefore renders the same bytes both times, rather than the id turning
|
|
90
|
+
* every server-rendered page into a diff.
|
|
137
91
|
*/
|
|
138
|
-
const
|
|
139
|
-
let sidecarUrl;
|
|
140
|
-
let frontendSessionId;
|
|
141
|
-
try {
|
|
142
|
-
sidecarUrl = parseReplaySidecarUrl(request.headers.get(REPLAY_SIDECAR_URL_HEADER));
|
|
143
|
-
frontendSessionId = request.headers.get("x-meticulous-session-id") ?? void 0;
|
|
144
|
-
} catch {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (sidecarUrl === void 0 || frontendSessionId === void 0) return;
|
|
148
|
-
const info = await getCachedReplaySessionInfo(sidecarUrl, frontendSessionId);
|
|
149
|
-
if (info === null) return;
|
|
150
|
-
return {
|
|
151
|
-
mode: "replay",
|
|
152
|
-
requestId: crypto.randomUUID(),
|
|
153
|
-
frontendSessionId,
|
|
154
|
-
sidecarUrl,
|
|
155
|
-
clockAnchorMs: info.clockAnchorMs,
|
|
156
|
-
waitUntil: buildWaitUntil(ctx)
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
const MAX_CACHED_SESSIONS = 50;
|
|
160
|
-
const replaySessionInfoCache = /* @__PURE__ */ new Map();
|
|
161
|
-
const getCachedReplaySessionInfo = (sidecarUrl, frontendSessionId) => {
|
|
162
|
-
const key = `${sidecarUrl}\u0000${frontendSessionId}`;
|
|
163
|
-
const cached = replaySessionInfoCache.get(key);
|
|
164
|
-
if (cached !== void 0) return cached;
|
|
165
|
-
const pending = getReplaySessionInfo(getOriginalFetch(), sidecarUrl, frontendSessionId).then((result) => {
|
|
166
|
-
if (result.outcome === "unreachable") {
|
|
167
|
-
replaySessionInfoCache.delete(key);
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
return result.outcome === "found" ? { clockAnchorMs: result.clockAnchorMs } : null;
|
|
171
|
-
});
|
|
172
|
-
if (replaySessionInfoCache.size >= MAX_CACHED_SESSIONS) {
|
|
173
|
-
const oldest = replaySessionInfoCache.keys().next();
|
|
174
|
-
if (!oldest.done) replaySessionInfoCache.delete(oldest.value);
|
|
175
|
-
}
|
|
176
|
-
replaySessionInfoCache.set(key, pending);
|
|
177
|
-
return pending;
|
|
178
|
-
};
|
|
179
|
-
const resolveSidecarUrl = (options, env) => {
|
|
180
|
-
const fromEnv = env !== null && typeof env === "object" ? env["METICULOUS_SIDECAR_URL"] : void 0;
|
|
181
|
-
const raw = options?.sidecarUrl ?? fromEnv;
|
|
182
|
-
if (typeof raw !== "string" || raw.length === 0) return;
|
|
183
|
-
return raw.replace(/\/+$/, "");
|
|
184
|
-
};
|
|
185
|
-
const buildCaptureContext = (request, sidecarUrl, ctx) => {
|
|
186
|
-
let frontendSessionId;
|
|
187
|
-
try {
|
|
188
|
-
frontendSessionId = request.headers.get("x-meticulous-session-id") ?? void 0;
|
|
189
|
-
} catch {
|
|
190
|
-
frontendSessionId = void 0;
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
mode: "record",
|
|
194
|
-
requestId: crypto.randomUUID(),
|
|
195
|
-
frontendSessionId,
|
|
196
|
-
sidecarUrl,
|
|
197
|
-
waitUntil: buildWaitUntil(ctx)
|
|
198
|
-
};
|
|
199
|
-
};
|
|
200
|
-
const buildWaitUntil = (ctx) => (promise) => {
|
|
201
|
-
try {
|
|
202
|
-
ctx.waitUntil(promise);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
warnOnce("wait-until", "ctx.waitUntil failed — capture events may be lost.", error);
|
|
205
|
-
}
|
|
206
|
-
};
|
|
92
|
+
const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
|
|
207
93
|
//#endregion
|
|
208
|
-
export { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, MAX_BODY_CAPTURE_SIZE, METICULOUS_PASSTHROUGH_HEADER, REPLAY_SIDECAR_URL_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, SIDECAR_REPLAY_OUTBOUND_FETCH_PATH, SIDECAR_REPLAY_SESSION_PATH, STR_REDACTED, headersToRecord, readBodyWithCap, readRequestBodyWithCap, redactRequestBody, serializeKvArgs, serializeKvCaptureFields, withMeticulous };
|
|
94
|
+
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_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, buildServerTimingSessionEntry, deserializeCapturedError, getMeticulousSessionId, headersToRecord, hrTimeToMs, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, mintProvisionalSessionId, msToHrTime, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
|
|
209
95
|
|
|
210
96
|
//# sourceMappingURL=index.js.map
|
|
211
|
-
//# debugId=
|
|
97
|
+
//# debugId=f420c8b0-634e-586e-b9bf-34d3afde7944
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { installBindingPatch } from \"./binding-patch\";\nimport {\n type RequestCaptureContext,\n type RequestReplayContext,\n requestCaptureContext,\n} from \"./context\";\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_SIDECAR_URL_HEADER,\n} from \"./protocol\";\nimport { parseReplaySidecarUrl } from \"./replay-sidecar-url\";\nimport { getReplaySessionInfo, postCaptureEvents } from \"./sidecar-client\";\nimport { installVirtualClock } from \"./virtual-clock\";\nimport { installVirtualRandom } from \"./virtual-random\";\n\nexport type {\n BindingRequestEvent,\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n InboundRequestEvent,\n KvOmittedReason,\n KvOperation,\n KvOperationEvent,\n OutboundFetchLookupRequest,\n OutboundFetchLookupResponse,\n OutboundRequestEvent,\n ReplaySessionInfoResponse,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n METICULOUS_PASSTHROUGH_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n SIDECAR_REPLAY_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\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\";\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\n * surfaces 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/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */\nexport interface MeticulousExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\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\nexport interface WithMeticulousOptions {\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`,\n * `__STATIC_CONTENT`). Use this for a binding whose traffic is high-volume or binary.\n * Applies to KV namespaces as well as `fetch`-shaped bindings.\n */\n skipBindings?: readonly string[];\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 * **Recording** activates on a `METICULOUS_SIDECAR_URL` var/binding (e.g. via `.dev.vars`\n * or `wrangler dev --var`) or `options.sidecarUrl`. Inbound requests, outgoing `fetch`\n * calls, calls through `fetch`-shaped bindings and KV namespace operations are reported to\n * the sidecar as capture events, without affecting the app.\n *\n * **Replay** activates on the `x-meticulous-backend-replay-sidecar-url` header, injected by\n * the Meticulous replay runner on requests to the app under test. Outgoing `fetch` calls are\n * then served from the recording instead of reaching the real service — and a call the\n * recording does not cover fails, rather than quietly becoming live traffic; put a\n * `meticulous-passthrough: true` header on a request that must stay real. The clock is frozen\n * at the recorded session's end so recorded credentials are still valid, and `Math.random` /\n * `crypto.randomUUID` / `crypto.getRandomValues` are seeded so ids the app mints are the same\n * in every replay of that session. Workerd\n * cannot read container environment variables, which is why per-replay config arrives as a\n * request header; the shim validates it and only honours a loopback / docker-gateway /\n * private-network `http:` origin.\n *\n * Replay takes precedence over recording when both are configured. A stale\n * `METICULOUS_SIDECAR_URL` baked into an image's `.dev.vars` is far more likely than a\n * spurious replay header (nothing but the replay runner emits it), and letting the env win\n * 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\n * capture 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 = async (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> => {\n const invokeHandler = () => handler.fetch(request, env as never, ctx);\n\n let sidecarUrl: string | undefined;\n let replayContext: RequestReplayContext | undefined;\n try {\n replayContext = await resolveReplayContext(request, ctx);\n sidecarUrl =\n replayContext === undefined\n ? resolveSidecarUrl(options, env)\n : undefined;\n if (replayContext !== undefined || sidecarUrl !== undefined) {\n installFetchPatch();\n }\n if (sidecarUrl !== undefined) {\n // Lazily, inside the request path, and never at module scope: the package declares\n // no `sideEffects` and is built with `platform: \"neutral\"`, so a module-scope side\n // effect can legitimately be tree-shaken by a customer's bundler. Re-run per\n // request because `env` is 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\n // only route them into the capture tee, which POSTs to an events route a replay\n // sidecar 404s.\n installBindingPatch(env, { skipBindings: options?.skipBindings });\n installKvPatch(env, { skipBindings: options?.skipBindings });\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 sidecarUrl = undefined;\n }\n\n if (replayContext !== undefined) {\n // Install the clock before any app code runs, so a module that reads Date.now() at\n // call time (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\n // request id) otherwise differs between a base and a head replay, and every rendered\n // page carrying it diffs forever.\n installVirtualRandom();\n // No inbound reporting in replay mode: there is no exporter behind the replay\n // sidecar, and the events route 404s there.\n return requestCaptureContext.run(replayContext, invokeHandler);\n }\n\n if (sidecarUrl === undefined) {\n return invokeHandler();\n }\n\n const captureCtx = buildCaptureContext(request, sidecarUrl, ctx);\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 method,\n url,\n requestHeaders,\n startTimeMs,\n endTimeMs: Date.now(),\n ...outcome,\n };\n captureCtx.waitUntil(\n postCaptureEvents(getOriginalFetch(), sidecarUrl, [event]),\n );\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 throw error;\n }\n\n try {\n reportInbound({\n statusCode: response.status,\n responseHeaders: headersToRecord(response.headers),\n });\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n return response;\n };\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript\n // cannot verify a spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\n/**\n * Whether this request should be replayed, and the context to serve it under.\n *\n * Both the session id and a usable sidecar URL must be present: mocks are indexed per\n * session, so replay without a session id could only miss. The sidecar is consulted once\n * per (sidecar, session) per isolate to confirm it holds mocks and to learn the clock\n * anchor — a sidecar that does not recognise the session, or the route, means replay is\n * 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 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 } catch {\n return undefined;\n }\n if (sidecarUrl === undefined || frontendSessionId === undefined) {\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 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 resolveSidecarUrl = (\n options: WithMeticulousOptions | undefined,\n env: unknown,\n): string | undefined => {\n const fromEnv =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)[\"METICULOUS_SIDECAR_URL\"]\n : undefined;\n const raw = options?.sidecarUrl ?? fromEnv;\n if (typeof raw !== \"string\" || raw.length === 0) {\n return undefined;\n }\n return raw.replace(/\\/+$/, \"\");\n};\n\nconst buildCaptureContext = (\n request: Request,\n sidecarUrl: string,\n ctx: MeticulousExecutionContext,\n): RequestCaptureContext => {\n let frontendSessionId: string | undefined;\n try {\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n } catch {\n frontendSessionId = undefined;\n }\n return {\n mode: \"record\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n sidecarUrl,\n waitUntil: buildWaitUntil(ctx),\n };\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4HA,MAAa,kBACX,SACA,YACM;CACN,MAAM,eAAe,OACnB,SACA,KACA,QACsB;EACtB,MAAM,sBAAsB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAErE,IAAI;EACJ,IAAI;AACJ,MAAI;AACF,mBAAgB,MAAM,qBAAqB,SAAS,IAAI;AACxD,gBACE,kBAAkB,KAAA,IACd,kBAAkB,SAAS,IAAI,GAC/B,KAAA;AACN,OAAI,kBAAkB,KAAA,KAAa,eAAe,KAAA,EAChD,oBAAmB;AAErB,OAAI,eAAe,KAAA,GAAW;AAW5B,wBAAoB,KAAK,EAAE,cAAc,SAAS,cAAc,CAAC;AACjE,mBAAe,KAAK,EAAE,cAAc,SAAS,cAAc,CAAC;;WAEvD,OAAO;AACd,YACE,aACA,mFACA,MACD;AACD,mBAAgB,KAAA;AAChB,gBAAa,KAAA;;AAGf,MAAI,kBAAkB,KAAA,GAAW;AAG/B,wBAAqB;AAIrB,yBAAsB;AAGtB,UAAO,sBAAsB,IAAI,eAAe,cAAc;;AAGhE,MAAI,eAAe,KAAA,EACjB,QAAO,eAAe;EAGxB,MAAM,aAAa,oBAAoB,SAAS,YAAY,IAAI;EAChE,MAAM,cAAc,KAAK,KAAK;EAC9B,IAAI,iBAA2C,EAAE;EACjD,IAAI,SAAS;EACb,IAAI,MAAM;AACV,MAAI;AACF,oBAAiB,gBAAgB,QAAQ,QAAQ;AACjD,YAAS,QAAQ;AACjB,SAAM,QAAQ;WACP,OAAO;AACd,YACE,mBACA,0CACA,MACD;;EAGH,MAAM,iBACJ,YAGS;AACT,OAAI;IACF,MAAM,QAA6B;KACjC,MAAM;KACN,WAAW,WAAW;KACtB,GAAI,WAAW,sBAAsB,KAAA,IACjC,EAAE,mBAAmB,WAAW,mBAAmB,GACnD,EAAE;KACN;KACA;KACA;KACA;KACA,WAAW,KAAK,KAAK;KACrB,GAAG;KACJ;AACD,eAAW,UACT,kBAAkB,kBAAkB,EAAE,YAAY,CAAC,MAAM,CAAC,CAC3D;YACM,OAAO;AACd,aACE,kBACA,yCACA,MACD;;;EAIL,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,sBAAsB,IAAI,YAAY,cAAc;WAC9D,OAAO;AACd,iBAAc,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;AACvC,SAAM;;AAGR,MAAI;AACF,iBAAc;IACZ,YAAY,SAAS;IACrB,iBAAiB,gBAAgB,SAAS,QAAQ;IACnD,CAAC;WACK,OAAO;AACd,YACE,kBACA,yCACA,MACD;;AAEH,SAAO;;AAKT,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;;;;;;;;;;AAY5C,MAAM,uBAAuB,OAC3B,SACA,QAC8C;CAC9C,IAAI;CACJ,IAAI;AACJ,KAAI;AACF,eAAa,sBACX,QAAQ,QAAQ,IAAI,0BAA0B,CAC/C;AACD,sBACE,QAAQ,QAAQ,IAAA,0BAA+B,IAAI,KAAA;SAC/C;AACN;;AAEF,KAAI,eAAe,KAAA,KAAa,sBAAsB,KAAA,EACpD;CAGF,MAAM,OAAO,MAAM,2BAA2B,YAAY,kBAAkB;AAC5E,KAAI,SAAS,KACX;AAGF,QAAO;EACL,MAAM;EACN,WAAW,OAAO,YAAY;EAC9B;EACA;EACA,eAAe,KAAK;EACpB,WAAW,eAAe,IAAI;EAC/B;;AAKH,MAAM,sBAAsB;AAC5B,MAAM,yCAAyB,IAAI,KAGhC;AAEH,MAAM,8BACJ,YACA,sBAC0D;CAC1D,MAAM,MAAM,GAAG,WAAW,QAAQ;CAClC,MAAM,SAAS,uBAAuB,IAAI,IAAI;AAC9C,KAAI,WAAW,KAAA,EACb,QAAO;CAET,MAAM,UAAU,qBACd,kBAAkB,EAClB,YACA,kBACD,CAAC,MAAM,WAAW;AACjB,MAAI,OAAO,YAAY,eAAe;AAKpC,0BAAuB,OAAO,IAAI;AAClC,UAAO;;AAET,SAAO,OAAO,YAAY,UACtB,EAAE,eAAe,OAAO,eAAe,GACvC;GACJ;AACF,KAAI,uBAAuB,QAAQ,qBAAqB;EACtD,MAAM,SAAS,uBAAuB,MAAM,CAAC,MAAM;AACnD,MAAI,CAAC,OAAO,KACV,wBAAuB,OAAO,OAAO,MAAM;;AAG/C,wBAAuB,IAAI,KAAK,QAAQ;AACxC,QAAO;;AAGT,MAAM,qBACJ,SACA,QACuB;CACvB,MAAM,UACJ,QAAQ,QAAQ,OAAO,QAAQ,WAC1B,IAAgC,4BACjC,KAAA;CACN,MAAM,MAAM,SAAS,cAAc;AACnC,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C;AAEF,QAAO,IAAI,QAAQ,QAAQ,GAAG;;AAGhC,MAAM,uBACJ,SACA,YACA,QAC0B;CAC1B,IAAI;AACJ,KAAI;AACF,sBACE,QAAQ,QAAQ,IAAA,0BAA+B,IAAI,KAAA;SAC/C;AACN,sBAAoB,KAAA;;AAEtB,QAAO;EACL,MAAM;EACN,WAAW,OAAO,YAAY;EAC9B;EACA;EACA,WAAW,eAAe,IAAI;EAC/B;;AAGH,MAAM,kBACH,SACA,YAAoC;AACnC,KAAI;AACF,MAAI,UAAU,QAAQ;UACf,OAAO;AACd,WACE,cACA,sDACA,MACD","debugId":"fc6d90d7-9399-56bc-9c2a-5135d61580c8"}
|
|
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 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_OUTBOUND_FETCH_PATH,\n SIDECAR_REPLAY_SESSION_PATH,\n} from \"./protocol\";\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 { 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+KA,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":"f420c8b0-634e-586e-b9bf-34d3afde7944"}
|
package/dist/kv-patch.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
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]="
|
|
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]="303b51b3-97aa-5dde-9000-ea94bcd5a2de")}catch(e){}}();
|
|
3
3
|
import { requestCaptureContext } from "./context.js";
|
|
4
4
|
import { warnOnce } from "./log.js";
|
|
5
|
-
import { getOriginalFetch } from "./original-fetch.js";
|
|
6
|
-
import { postCaptureEvents } from "./sidecar-client.js";
|
|
7
5
|
import { serializeKvCaptureFields } from "./kv-capture.js";
|
|
8
6
|
//#region src/kv-patch.ts
|
|
9
7
|
/**
|
|
@@ -57,7 +55,7 @@ const kvBindingNames = /* @__PURE__ */ new WeakMap();
|
|
|
57
55
|
*/
|
|
58
56
|
const installKvPatch = (env, options) => {
|
|
59
57
|
try {
|
|
60
|
-
for (const { name, value, skip } of kvBindings(env, options?.skipBindings)) {
|
|
58
|
+
for (const { name, value, skip } of kvBindings(env, options?.skipBindings, options?.skipInstances)) {
|
|
61
59
|
patchKvMethods(value);
|
|
62
60
|
if (!skip) kvBindingNames.set(value, name);
|
|
63
61
|
}
|
|
@@ -65,14 +63,15 @@ const installKvPatch = (env, options) => {
|
|
|
65
63
|
warnOnce("kv-patch-install", "Failed to install the Meticulous KV patch — KV operations will not be recorded.", error);
|
|
66
64
|
}
|
|
67
65
|
};
|
|
68
|
-
const kvBindings = (env, skipBindings) => {
|
|
66
|
+
const kvBindings = (env, skipBindings, skipInstances) => {
|
|
69
67
|
if (env === null || typeof env !== "object") return [];
|
|
70
68
|
const skipped = new Set(skipBindings ?? []);
|
|
69
|
+
const skippedInstances = new Set(skipInstances ?? []);
|
|
71
70
|
const found = [];
|
|
72
71
|
for (const [name, value] of Object.entries(env)) if (isKvNamespaceLike(value)) found.push({
|
|
73
72
|
name,
|
|
74
73
|
value,
|
|
75
|
-
skip: skipped.has(name)
|
|
74
|
+
skip: skipped.has(name) || skippedInstances.has(value)
|
|
76
75
|
});
|
|
77
76
|
return found;
|
|
78
77
|
};
|
|
@@ -164,8 +163,7 @@ const captureKvOperation = async (ctx, operation, bindingName, args, invoke) =>
|
|
|
164
163
|
};
|
|
165
164
|
const report = (ctx, operation, bindingName, args, startTimeMs, outcome) => {
|
|
166
165
|
try {
|
|
167
|
-
|
|
168
|
-
ctx.waitUntil(postCaptureEvents(getOriginalFetch(), ctx.sidecarUrl, [event]));
|
|
166
|
+
ctx.buffer.add(buildKvEvent(ctx, operation, bindingName, args, startTimeMs, outcome));
|
|
169
167
|
} catch (error) {
|
|
170
168
|
warnOnce("kv-report", "Failed to report a KV operation.", error);
|
|
171
169
|
}
|
|
@@ -174,6 +172,8 @@ const buildKvEvent = (ctx, operation, bindingName, args, startTimeMs, outcome) =
|
|
|
174
172
|
kind: "kv",
|
|
175
173
|
requestId: ctx.requestId,
|
|
176
174
|
...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
|
|
175
|
+
traceId: ctx.traceId,
|
|
176
|
+
serverSpanId: ctx.serverSpanId,
|
|
177
177
|
bindingName,
|
|
178
178
|
operation,
|
|
179
179
|
...serializeKvCaptureFields(operation, args, outcome),
|
|
@@ -185,4 +185,4 @@ const buildKvEvent = (ctx, operation, bindingName, args, startTimeMs, outcome) =
|
|
|
185
185
|
export { installKvPatch };
|
|
186
186
|
|
|
187
187
|
//# sourceMappingURL=kv-patch.js.map
|
|
188
|
-
//# debugId=
|
|
188
|
+
//# debugId=303b51b3-97aa-5dde-9000-ea94bcd5a2de
|