@alwaysmeticulous/backend-recorder-workerd 2.333.1 → 2.334.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/health-probe-filter.d.ts +43 -0
- package/dist/health-probe-filter.js +79 -0
- package/dist/health-probe-filter.js.map +1 -0
- package/dist/health-probe.d.ts +29 -0
- package/dist/health-probe.js +76 -0
- package/dist/health-probe.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/with-meticulous.js +0 -0
- package/dist/with-meticulous.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,7 @@ Because each function re-reads the current request's sink, a callback that outli
|
|
|
199
199
|
- Bindings other than `fetch`-shaped ones and KV namespaces are not captured: D1, R2, Queues, and RPC method calls on a named entrypoint (`env.SVC.someMethod()`) — an RPC method is not a patchable property, so it cannot be intercepted this way.
|
|
200
200
|
- A KV value that is not text is recorded without its value: reading a `stream` would take the bytes away from the app, and binary values are skipped because KV blobs are large and not UTF-8. The operation, key and arguments are still recorded, with `omitted` saying which case it was.
|
|
201
201
|
- Assets bindings are skipped by default (`ASSETS`, `__STATIC_CONTENT`): asset bytes are large and often binary, and asset serving is usually a worker's highest-volume call. Use `options.skipBindings` to skip others.
|
|
202
|
+
- Health probes are recorded as nothing at all — not the inbound request, and not what it fans out to. A GET/HEAD to a conventional probe path (`/health`, `/healthz`, `/healthcheck`, `/health-check`, `/_health`, `/api/health`, `/api/healthz`, `/readyz`, `/livez`, `/ping`) carrying no `x-meticulous-session-id` never enters the capture context. A request that names its session is real app traffic and is recorded whatever its path, so an app is still free to serve `/api/health` as a page's data source. Record mode only; a replay is unaffected.
|
|
202
203
|
- A Durable Object stub is recorded without a binding name, since the stub comes from `namespace.get()` rather than being a binding itself.
|
|
203
204
|
- A binding reference bound before the first instrumented request (`const f = env.SVC.fetch.bind(env.SVC)` at module scope) escapes instrumentation. Holding the binding _object_ and calling `.fetch()` on it per request — what SDKs normally do — is captured.
|
|
204
205
|
- WebSocket upgrades and TCP sockets (`connect()`) are not captured.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CaptureEvent } from "./protocol.js";
|
|
2
|
+
|
|
3
|
+
//#region src/health-probe-filter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Drops health-probe events at the sidecar, as a safety net beneath the shim's own check.
|
|
6
|
+
*
|
|
7
|
+
* The shim already declines to record a probe (`with-meticulous.ts`), which costs nothing at
|
|
8
|
+
* all — no capture work, no report. But that check ships inside the customer's bundle, so a
|
|
9
|
+
* deployment pinning an older `@alwaysmeticulous/backend-recorder-workerd` keeps reporting probes
|
|
10
|
+
* until someone bumps the package and redeploys the app. A sidecar moves far more cheaply: the
|
|
11
|
+
* local one comes from the CLI, so it needs nothing from the app at all, and the deployed Worker
|
|
12
|
+
* is one `wrangler deploy` away with the app untouched. Repeating the verdict here is what makes
|
|
13
|
+
* the exclusion reach traffic the shim still reports.
|
|
14
|
+
*
|
|
15
|
+
* **Why a group rather than a per-event rule.** Only the inbound event carries the probe's
|
|
16
|
+
* path; an outbound `fetch`, binding, KV or postgres event carries its _own_ target. Those
|
|
17
|
+
* CLIENT-span events are the ones that actually matter (a mock store only ever serves CLIENT
|
|
18
|
+
* spans), so the filter learns which `requestId`s are probes from the inbound events and drops
|
|
19
|
+
* everything correlated with them.
|
|
20
|
+
*
|
|
21
|
+
* **What it cannot catch.** A batch is flushed once _past_ a size threshold, so a probe whose
|
|
22
|
+
* captured bodies exceed it mid-handler can post outbound events _before_ its inbound event
|
|
23
|
+
* exists. Those go through: nothing in that batch says they belong to a probe. Harmless in
|
|
24
|
+
* practice — a probe endpoint big enough to trip a multi-megabyte threshold is not a probe —
|
|
25
|
+
* and the shim-side check covers it for any up-to-date deployment.
|
|
26
|
+
*/
|
|
27
|
+
declare class HealthProbeEventFilter {
|
|
28
|
+
private readonly maxRemembered;
|
|
29
|
+
/** Insertion-ordered, so the oldest entry is the one to evict. */
|
|
30
|
+
private readonly probeRequestIds;
|
|
31
|
+
constructor(options?: {
|
|
32
|
+
maxRememberedRequestIds?: number;
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Returns the events worth keeping. The input array is never mutated, and is returned as-is
|
|
36
|
+
* when there is nothing to drop — the overwhelmingly common case, so it costs no copy.
|
|
37
|
+
*/
|
|
38
|
+
filter(events: CaptureEvent[]): CaptureEvent[];
|
|
39
|
+
private remember;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { HealthProbeEventFilter };
|
|
43
|
+
//# sourceMappingURL=health-probe-filter.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isHealthProbeRequest } from "./health-probe.js";
|
|
2
|
+
//#region src/health-probe-filter.ts
|
|
3
|
+
/**
|
|
4
|
+
* How many probe request ids to remember for stragglers. A batch is normally one whole
|
|
5
|
+
* request, so this only has to cover events the app queued in `waitUntil` and the shim sent
|
|
6
|
+
* after `CaptureBuffer.close()`. Bounded because on a Worker-hosted sidecar the memory lives
|
|
7
|
+
* in an isolate that may serve a busy deployment for a long time.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_REMEMBERED_REQUEST_IDS = 1e3;
|
|
10
|
+
/**
|
|
11
|
+
* Drops health-probe events at the sidecar, as a safety net beneath the shim's own check.
|
|
12
|
+
*
|
|
13
|
+
* The shim already declines to record a probe (`with-meticulous.ts`), which costs nothing at
|
|
14
|
+
* all — no capture work, no report. But that check ships inside the customer's bundle, so a
|
|
15
|
+
* deployment pinning an older `@alwaysmeticulous/backend-recorder-workerd` keeps reporting probes
|
|
16
|
+
* until someone bumps the package and redeploys the app. A sidecar moves far more cheaply: the
|
|
17
|
+
* local one comes from the CLI, so it needs nothing from the app at all, and the deployed Worker
|
|
18
|
+
* is one `wrangler deploy` away with the app untouched. Repeating the verdict here is what makes
|
|
19
|
+
* the exclusion reach traffic the shim still reports.
|
|
20
|
+
*
|
|
21
|
+
* **Why a group rather than a per-event rule.** Only the inbound event carries the probe's
|
|
22
|
+
* path; an outbound `fetch`, binding, KV or postgres event carries its _own_ target. Those
|
|
23
|
+
* CLIENT-span events are the ones that actually matter (a mock store only ever serves CLIENT
|
|
24
|
+
* spans), so the filter learns which `requestId`s are probes from the inbound events and drops
|
|
25
|
+
* everything correlated with them.
|
|
26
|
+
*
|
|
27
|
+
* **What it cannot catch.** A batch is flushed once _past_ a size threshold, so a probe whose
|
|
28
|
+
* captured bodies exceed it mid-handler can post outbound events _before_ its inbound event
|
|
29
|
+
* exists. Those go through: nothing in that batch says they belong to a probe. Harmless in
|
|
30
|
+
* practice — a probe endpoint big enough to trip a multi-megabyte threshold is not a probe —
|
|
31
|
+
* and the shim-side check covers it for any up-to-date deployment.
|
|
32
|
+
*/
|
|
33
|
+
var HealthProbeEventFilter = class {
|
|
34
|
+
maxRemembered;
|
|
35
|
+
/** Insertion-ordered, so the oldest entry is the one to evict. */
|
|
36
|
+
probeRequestIds = /* @__PURE__ */ new Set();
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.maxRemembered = options?.maxRememberedRequestIds ?? DEFAULT_MAX_REMEMBERED_REQUEST_IDS;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the events worth keeping. The input array is never mutated, and is returned as-is
|
|
42
|
+
* when there is nothing to drop — the overwhelmingly common case, so it costs no copy.
|
|
43
|
+
*/
|
|
44
|
+
filter(events) {
|
|
45
|
+
for (const event of events) if (event.kind === "inbound" && isHealthProbeInboundEvent(event)) this.remember(event.requestId);
|
|
46
|
+
if (this.probeRequestIds.size === 0) return events;
|
|
47
|
+
const kept = events.filter((event) => !this.probeRequestIds.has(event.requestId));
|
|
48
|
+
return kept.length === events.length ? events : kept;
|
|
49
|
+
}
|
|
50
|
+
remember(requestId) {
|
|
51
|
+
this.probeRequestIds.delete(requestId);
|
|
52
|
+
this.probeRequestIds.add(requestId);
|
|
53
|
+
while (this.probeRequestIds.size > this.maxRemembered) {
|
|
54
|
+
const oldest = this.probeRequestIds.values().next();
|
|
55
|
+
if (oldest.done === true) return;
|
|
56
|
+
this.probeRequestIds.delete(oldest.value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The same verdict {@link isHealthProbeRequest} reaches in the shim, from what survives on the
|
|
62
|
+
* wire. `requestHeaders` is the inbound request's captured headers, and
|
|
63
|
+
* `x-meticulous-session-id` is one of the two the shim persists (`CAPTURED_HEADERS`), so the
|
|
64
|
+
* "nothing can attribute this" half of the rule is reproducible here.
|
|
65
|
+
*
|
|
66
|
+
* A `frontendSessionId` the browser sent also blocks the drop, independently of the header.
|
|
67
|
+
* Both are the same fact, but disagreeing would mean dropping a request that _is_ attributable,
|
|
68
|
+
* and this filter should only ever err towards recording. A `"backend"`-origin id does not
|
|
69
|
+
* block it: the shim minted that one for a document navigation, so it says nothing about
|
|
70
|
+
* whether the caller identified itself — and a probe would never have qualified for minting.
|
|
71
|
+
*/
|
|
72
|
+
const isHealthProbeInboundEvent = (event) => {
|
|
73
|
+
if (event.frontendSessionId !== void 0 && event.sessionIdOrigin !== "backend") return false;
|
|
74
|
+
return isHealthProbeRequest(event.method, event.url, (name) => event.requestHeaders[name]?.[0]);
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { HealthProbeEventFilter };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=health-probe-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-probe-filter.js","names":[],"sources":["../src/health-probe-filter.ts"],"sourcesContent":["import { isHealthProbeRequest } from \"./health-probe\";\nimport type { CaptureEvent, InboundRequestEvent } from \"./protocol\";\n\n/**\n * How many probe request ids to remember for stragglers. A batch is normally one whole\n * request, so this only has to cover events the app queued in `waitUntil` and the shim sent\n * after `CaptureBuffer.close()`. Bounded because on a Worker-hosted sidecar the memory lives\n * in an isolate that may serve a busy deployment for a long time.\n */\nconst DEFAULT_MAX_REMEMBERED_REQUEST_IDS = 1_000;\n\n/**\n * Drops health-probe events at the sidecar, as a safety net beneath the shim's own check.\n *\n * The shim already declines to record a probe (`with-meticulous.ts`), which costs nothing at\n * all — no capture work, no report. But that check ships inside the customer's bundle, so a\n * deployment pinning an older `@alwaysmeticulous/backend-recorder-workerd` keeps reporting probes\n * until someone bumps the package and redeploys the app. A sidecar moves far more cheaply: the\n * local one comes from the CLI, so it needs nothing from the app at all, and the deployed Worker\n * is one `wrangler deploy` away with the app untouched. Repeating the verdict here is what makes\n * the exclusion reach traffic the shim still reports.\n *\n * **Why a group rather than a per-event rule.** Only the inbound event carries the probe's\n * path; an outbound `fetch`, binding, KV or postgres event carries its _own_ target. Those\n * CLIENT-span events are the ones that actually matter (a mock store only ever serves CLIENT\n * spans), so the filter learns which `requestId`s are probes from the inbound events and drops\n * everything correlated with them.\n *\n * **What it cannot catch.** A batch is flushed once _past_ a size threshold, so a probe whose\n * captured bodies exceed it mid-handler can post outbound events _before_ its inbound event\n * exists. Those go through: nothing in that batch says they belong to a probe. Harmless in\n * practice — a probe endpoint big enough to trip a multi-megabyte threshold is not a probe —\n * and the shim-side check covers it for any up-to-date deployment.\n */\nexport class HealthProbeEventFilter {\n private readonly maxRemembered: number;\n /** Insertion-ordered, so the oldest entry is the one to evict. */\n private readonly probeRequestIds = new Set<string>();\n\n constructor(options?: { maxRememberedRequestIds?: number }) {\n this.maxRemembered =\n options?.maxRememberedRequestIds ?? DEFAULT_MAX_REMEMBERED_REQUEST_IDS;\n }\n\n /**\n * Returns the events worth keeping. The input array is never mutated, and is returned as-is\n * when there is nothing to drop — the overwhelmingly common case, so it costs no copy.\n */\n filter(events: CaptureEvent[]): CaptureEvent[] {\n for (const event of events) {\n if (event.kind === \"inbound\" && isHealthProbeInboundEvent(event)) {\n this.remember(event.requestId);\n }\n }\n if (this.probeRequestIds.size === 0) {\n return events;\n }\n const kept = events.filter(\n (event) => !this.probeRequestIds.has(event.requestId),\n );\n return kept.length === events.length ? events : kept;\n }\n\n private remember(requestId: string): void {\n // Re-insert so a request still producing stragglers stays fresh rather than ageing out\n // while its own events are still arriving.\n this.probeRequestIds.delete(requestId);\n this.probeRequestIds.add(requestId);\n while (this.probeRequestIds.size > this.maxRemembered) {\n const oldest = this.probeRequestIds.values().next();\n if (oldest.done === true) {\n return;\n }\n this.probeRequestIds.delete(oldest.value);\n }\n }\n}\n\n/**\n * The same verdict {@link isHealthProbeRequest} reaches in the shim, from what survives on the\n * wire. `requestHeaders` is the inbound request's captured headers, and\n * `x-meticulous-session-id` is one of the two the shim persists (`CAPTURED_HEADERS`), so the\n * \"nothing can attribute this\" half of the rule is reproducible here.\n *\n * A `frontendSessionId` the browser sent also blocks the drop, independently of the header.\n * Both are the same fact, but disagreeing would mean dropping a request that _is_ attributable,\n * and this filter should only ever err towards recording. A `\"backend\"`-origin id does not\n * block it: the shim minted that one for a document navigation, so it says nothing about\n * whether the caller identified itself — and a probe would never have qualified for minting.\n */\nconst isHealthProbeInboundEvent = (event: InboundRequestEvent): boolean => {\n if (\n event.frontendSessionId !== undefined &&\n event.sessionIdOrigin !== \"backend\"\n ) {\n return false;\n }\n return isHealthProbeRequest(\n event.method,\n event.url,\n (name) => event.requestHeaders[name]?.[0],\n );\n};\n"],"mappings":";;;;;;;;AASA,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;;;;;AAyB3C,IAAa,yBAAb,MAAoC;CAClC;;CAEA,kCAAmC,IAAI,KAAa;CAEpD,YAAY,SAAgD;AAC1D,OAAK,gBACH,SAAS,2BAA2B;;;;;;CAOxC,OAAO,QAAwC;AAC7C,OAAK,MAAM,SAAS,OAClB,KAAI,MAAM,SAAS,aAAa,0BAA0B,MAAM,CAC9D,MAAK,SAAS,MAAM,UAAU;AAGlC,MAAI,KAAK,gBAAgB,SAAS,EAChC,QAAO;EAET,MAAM,OAAO,OAAO,QACjB,UAAU,CAAC,KAAK,gBAAgB,IAAI,MAAM,UAAU,CACtD;AACD,SAAO,KAAK,WAAW,OAAO,SAAS,SAAS;;CAGlD,SAAiB,WAAyB;AAGxC,OAAK,gBAAgB,OAAO,UAAU;AACtC,OAAK,gBAAgB,IAAI,UAAU;AACnC,SAAO,KAAK,gBAAgB,OAAO,KAAK,eAAe;GACrD,MAAM,SAAS,KAAK,gBAAgB,QAAQ,CAAC,MAAM;AACnD,OAAI,OAAO,SAAS,KAClB;AAEF,QAAK,gBAAgB,OAAO,OAAO,MAAM;;;;;;;;;;;;;;;;AAiB/C,MAAM,6BAA6B,UAAwC;AACzE,KACE,MAAM,sBAAsB,KAAA,KAC5B,MAAM,oBAAoB,UAE1B,QAAO;AAET,QAAO,qBACL,MAAM,QACN,MAAM,MACL,SAAS,MAAM,eAAe,QAAQ,GACxC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HeaderReader } from "./provisional-session-id.js";
|
|
2
|
+
|
|
3
|
+
//#region src/health-probe.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether this inbound request is a health probe whose spans are worth nothing to a replay.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately narrow on two axes beyond the path:
|
|
8
|
+
*
|
|
9
|
+
* - **No session id.** A request the browser or an SSR fan-out made carries
|
|
10
|
+
* `x-meticulous-session-id`, and that is real app traffic however it is named — an app is
|
|
11
|
+
* free to expose `/api/health` as a page's data source. Only a request nothing can attribute
|
|
12
|
+
* is treated as a probe, so this can never drop a span that ingestion's session-id match
|
|
13
|
+
* would have kept; it only removes spans that reach a session through the time-window
|
|
14
|
+
* fallback (see "Inferred span attachment" in the Node recorder's README), which is exactly
|
|
15
|
+
* where a probe would otherwise land.
|
|
16
|
+
* - **GET/HEAD only.** Every probe convention is a read. A POST to `/health` is the app's own
|
|
17
|
+
* endpoint.
|
|
18
|
+
*
|
|
19
|
+
* Suppressing the inbound span suppresses the whole subtree beneath it (the Node surface does
|
|
20
|
+
* this with OTel's `suppressTracing`, the workerd surface by never entering the capture
|
|
21
|
+
* context), so a probe that does reach a database or an upstream service contributes nothing
|
|
22
|
+
* either. That subtree is the part that actually matters: an unstamped `SELECT 1` or an
|
|
23
|
+
* upstream `GET /health` is a CLIENT span, and CLIENT spans are what the replay mock stores
|
|
24
|
+
* serve.
|
|
25
|
+
*/
|
|
26
|
+
declare const isHealthProbeRequest: (method: string, path: string | undefined, getHeader: HeaderReader) => boolean;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { isHealthProbeRequest };
|
|
29
|
+
//# sourceMappingURL=health-probe.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "./protocol.js";
|
|
2
|
+
//#region src/health-probe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Well-known container health/readiness probe paths. A Kubernetes probe, a load balancer and
|
|
5
|
+
* the replay orchestrator's own availability check all poll one of these for the lifetime of
|
|
6
|
+
* the pod, at a fixed cadence and with no session identity, so recording them adds a steady
|
|
7
|
+
* stream of unstamped spans that no replay can ever use.
|
|
8
|
+
*
|
|
9
|
+
* Kept to conventional names only. A path outside this set is the app's own endpoint until
|
|
10
|
+
* proven otherwise, and dropping a real call is far worse than recording a probe: the call
|
|
11
|
+
* then has no mock and fails hermetically on replay.
|
|
12
|
+
*/
|
|
13
|
+
const HEALTH_PROBE_PATHS = new Set([
|
|
14
|
+
"/health",
|
|
15
|
+
"/healthz",
|
|
16
|
+
"/healthcheck",
|
|
17
|
+
"/health-check",
|
|
18
|
+
"/_health",
|
|
19
|
+
"/api/health",
|
|
20
|
+
"/api/healthz",
|
|
21
|
+
"/readyz",
|
|
22
|
+
"/livez",
|
|
23
|
+
"/ping"
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Whether this inbound request is a health probe whose spans are worth nothing to a replay.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately narrow on two axes beyond the path:
|
|
29
|
+
*
|
|
30
|
+
* - **No session id.** A request the browser or an SSR fan-out made carries
|
|
31
|
+
* `x-meticulous-session-id`, and that is real app traffic however it is named — an app is
|
|
32
|
+
* free to expose `/api/health` as a page's data source. Only a request nothing can attribute
|
|
33
|
+
* is treated as a probe, so this can never drop a span that ingestion's session-id match
|
|
34
|
+
* would have kept; it only removes spans that reach a session through the time-window
|
|
35
|
+
* fallback (see "Inferred span attachment" in the Node recorder's README), which is exactly
|
|
36
|
+
* where a probe would otherwise land.
|
|
37
|
+
* - **GET/HEAD only.** Every probe convention is a read. A POST to `/health` is the app's own
|
|
38
|
+
* endpoint.
|
|
39
|
+
*
|
|
40
|
+
* Suppressing the inbound span suppresses the whole subtree beneath it (the Node surface does
|
|
41
|
+
* this with OTel's `suppressTracing`, the workerd surface by never entering the capture
|
|
42
|
+
* context), so a probe that does reach a database or an upstream service contributes nothing
|
|
43
|
+
* either. That subtree is the part that actually matters: an unstamped `SELECT 1` or an
|
|
44
|
+
* upstream `GET /health` is a CLIENT span, and CLIENT spans are what the replay mock stores
|
|
45
|
+
* serve.
|
|
46
|
+
*/
|
|
47
|
+
const isHealthProbeRequest = (method, path, getHeader) => {
|
|
48
|
+
if (getHeader("x-meticulous-session-id") != null) return false;
|
|
49
|
+
const normalizedMethod = method.toUpperCase();
|
|
50
|
+
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") return false;
|
|
51
|
+
const normalizedPath = normalizeProbePath(path);
|
|
52
|
+
return normalizedPath !== void 0 && HEALTH_PROBE_PATHS.has(normalizedPath);
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Node hands us an origin-form target (`/health?deep=1`), workerd an absolute URL, so both
|
|
56
|
+
* shapes have to reduce to the same pathname. A trailing slash is stripped because `/health/`
|
|
57
|
+
* and `/health` are the same route to every framework that serves one.
|
|
58
|
+
*/
|
|
59
|
+
const normalizeProbePath = (path) => {
|
|
60
|
+
if (path === void 0 || path === "") return;
|
|
61
|
+
const pathname = path.startsWith("/") ? path.split("?")[0].split("#")[0] : pathnameOf(path);
|
|
62
|
+
if (pathname === void 0) return;
|
|
63
|
+
const lowercased = pathname.toLowerCase();
|
|
64
|
+
return lowercased.length > 1 && lowercased.endsWith("/") ? lowercased.slice(0, -1) : lowercased;
|
|
65
|
+
};
|
|
66
|
+
const pathnameOf = (url) => {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url).pathname;
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { isHealthProbeRequest };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=health-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-probe.js","names":[],"sources":["../src/health-probe.ts"],"sourcesContent":["import { FRONTEND_SESSION_ID_HEADER } from \"./protocol\";\nimport type { HeaderReader } from \"./provisional-session-id\";\n\n/**\n * Well-known container health/readiness probe paths. A Kubernetes probe, a load balancer and\n * the replay orchestrator's own availability check all poll one of these for the lifetime of\n * the pod, at a fixed cadence and with no session identity, so recording them adds a steady\n * stream of unstamped spans that no replay can ever use.\n *\n * Kept to conventional names only. A path outside this set is the app's own endpoint until\n * proven otherwise, and dropping a real call is far worse than recording a probe: the call\n * then has no mock and fails hermetically on replay.\n */\nconst HEALTH_PROBE_PATHS = new Set([\n \"/health\",\n \"/healthz\",\n \"/healthcheck\",\n \"/health-check\",\n \"/_health\",\n \"/api/health\",\n \"/api/healthz\",\n \"/readyz\",\n \"/livez\",\n \"/ping\",\n]);\n\n/**\n * Whether this inbound request is a health probe whose spans are worth nothing to a replay.\n *\n * Deliberately narrow on two axes beyond the path:\n *\n * - **No session id.** A request the browser or an SSR fan-out made carries\n * `x-meticulous-session-id`, and that is real app traffic however it is named — an app is\n * free to expose `/api/health` as a page's data source. Only a request nothing can attribute\n * is treated as a probe, so this can never drop a span that ingestion's session-id match\n * would have kept; it only removes spans that reach a session through the time-window\n * fallback (see \"Inferred span attachment\" in the Node recorder's README), which is exactly\n * where a probe would otherwise land.\n * - **GET/HEAD only.** Every probe convention is a read. A POST to `/health` is the app's own\n * endpoint.\n *\n * Suppressing the inbound span suppresses the whole subtree beneath it (the Node surface does\n * this with OTel's `suppressTracing`, the workerd surface by never entering the capture\n * context), so a probe that does reach a database or an upstream service contributes nothing\n * either. That subtree is the part that actually matters: an unstamped `SELECT 1` or an\n * upstream `GET /health` is a CLIENT span, and CLIENT spans are what the replay mock stores\n * serve.\n */\nexport const isHealthProbeRequest = (\n method: string,\n path: string | undefined,\n getHeader: HeaderReader,\n): boolean => {\n if (getHeader(FRONTEND_SESSION_ID_HEADER) != null) {\n return false;\n }\n const normalizedMethod = method.toUpperCase();\n if (normalizedMethod !== \"GET\" && normalizedMethod !== \"HEAD\") {\n return false;\n }\n const normalizedPath = normalizeProbePath(path);\n return normalizedPath !== undefined && HEALTH_PROBE_PATHS.has(normalizedPath);\n};\n\n/**\n * Node hands us an origin-form target (`/health?deep=1`), workerd an absolute URL, so both\n * shapes have to reduce to the same pathname. A trailing slash is stripped because `/health/`\n * and `/health` are the same route to every framework that serves one.\n */\nconst normalizeProbePath = (path: string | undefined): string | undefined => {\n if (path === undefined || path === \"\") {\n return undefined;\n }\n const pathname = path.startsWith(\"/\")\n ? path.split(\"?\")[0].split(\"#\")[0]\n : pathnameOf(path);\n if (pathname === undefined) {\n return undefined;\n }\n const lowercased = pathname.toLowerCase();\n return lowercased.length > 1 && lowercased.endsWith(\"/\")\n ? lowercased.slice(0, -1)\n : lowercased;\n};\n\nconst pathnameOf = (url: string): string | undefined => {\n try {\n return new URL(url).pathname;\n } catch {\n return undefined;\n }\n};\n"],"mappings":";;;;;;;;;;;;AAaA,MAAM,qBAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,wBACX,QACA,MACA,cACY;AACZ,KAAI,UAAA,0BAAqC,IAAI,KAC3C,QAAO;CAET,MAAM,mBAAmB,OAAO,aAAa;AAC7C,KAAI,qBAAqB,SAAS,qBAAqB,OACrD,QAAO;CAET,MAAM,iBAAiB,mBAAmB,KAAK;AAC/C,QAAO,mBAAmB,KAAA,KAAa,mBAAmB,IAAI,eAAe;;;;;;;AAQ/E,MAAM,sBAAsB,SAAiD;AAC3E,KAAI,SAAS,KAAA,KAAa,SAAS,GACjC;CAEF,MAAM,WAAW,KAAK,WAAW,IAAI,GACjC,KAAK,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,KAC9B,WAAW,KAAK;AACpB,KAAI,aAAa,KAAA,EACf;CAEF,MAAM,aAAa,SAAS,aAAa;AACzC,QAAO,WAAW,SAAS,KAAK,WAAW,SAAS,IAAI,GACpD,WAAW,MAAM,GAAG,GAAG,GACvB;;AAGN,MAAM,cAAc,QAAoC;AACtD,KAAI;AACF,SAAO,IAAI,IAAI,IAAI,CAAC;SACd;AACN"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { BindingRequestEvent, CAPTURED_HEADERS, CaptureEvent, CaptureEventsPaylo
|
|
|
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";
|
|
7
|
+
import { HeaderReader, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
8
|
+
import { isHealthProbeRequest } from "./health-probe.js";
|
|
9
|
+
import { HealthProbeEventFilter } from "./health-probe-filter.js";
|
|
7
10
|
import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
|
|
8
11
|
import { CoverageMap, CoverageMapFile } from "./coverage/coverage.types.js";
|
|
9
12
|
import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap, getUnreportedCoverageFiles, markCoverageFilesReported, registerCoverageFile } from "./coverage/runtime.js";
|
|
@@ -17,7 +20,6 @@ import { HrTime, ReadableSpan, SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_S
|
|
|
17
20
|
import { serializeSpan } from "./spans/serialize-span.js";
|
|
18
21
|
import { SpanBuilder, randomHex } from "./spans/span-builder.js";
|
|
19
22
|
import { redactUrlCredentials } from "./spans/url-redaction.js";
|
|
20
|
-
import { HeaderReader, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
21
23
|
|
|
22
24
|
//#region src/index.d.ts
|
|
23
25
|
/** Structural subset of an ES-module Worker handler (`export default {...}`). */
|
|
@@ -85,5 +87,5 @@ declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler
|
|
|
85
87
|
*/
|
|
86
88
|
declare const getMeticulousSessionId: () => string | undefined;
|
|
87
89
|
//#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, 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 };
|
|
90
|
+
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, HealthProbeEventFilter, 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, isHealthProbeRequest, 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
91
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "
|
|
|
5
5
|
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";
|
|
6
6
|
import { headersToRecord } from "./outbound-capture.js";
|
|
7
7
|
import { __mcEnter, __mcHit, collectHitIds, createCoverageSink, getCoverageMap, getUnreportedCoverageFiles, markCoverageFilesReported, registerCoverageFile } from "./coverage/runtime.js";
|
|
8
|
+
import { isHealthProbeRequest } from "./health-probe.js";
|
|
8
9
|
import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
|
|
9
10
|
import { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
10
11
|
import { WORKERD_SHIM_VERSION } from "./version.js";
|
|
@@ -15,6 +16,7 @@ import { SPAN_KIND_CLIENT, SPAN_KIND_INTERNAL, SPAN_KIND_SERVER, SPAN_STATUS_ERR
|
|
|
15
16
|
import { redactUrlCredentials } from "./spans/url-redaction.js";
|
|
16
17
|
import { SpanBuilder, randomHex } from "./spans/span-builder.js";
|
|
17
18
|
import { runWithMeticulous } from "./with-meticulous.js";
|
|
19
|
+
import { HealthProbeEventFilter } from "./health-probe-filter.js";
|
|
18
20
|
import { withMeticulousPagesFunction } from "./pages-function.js";
|
|
19
21
|
import { withMeticulousPostgres } from "./postgres/with-meticulous-postgres.js";
|
|
20
22
|
import { serializeSpan } from "./spans/serialize-span.js";
|
|
@@ -91,6 +93,6 @@ const withMeticulous = (handler, options) => {
|
|
|
91
93
|
*/
|
|
92
94
|
const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
|
|
93
95
|
//#endregion
|
|
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_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 };
|
|
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, HealthProbeEventFilter, 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, isHealthProbeRequest, 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 };
|
|
95
97
|
|
|
96
98
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"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"],"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"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\";\n/**\n * Exported for the Node recorder, whose `health-probe.ts` adapts this to\n * `http.IncomingMessage`. Which requests count as a health probe is a rule both surfaces\n * have to agree on, so — like the provisional-session-id rules below — it has exactly one\n * implementation.\n */\nexport { isHealthProbeRequest } from \"./health-probe\";\n/**\n * Exported for both sidecar implementations — the deployed Worker\n * (`@alwaysmeticulous/backend-recorder-sidecar-worker`) and the local Node one\n * (`backend-recorder-js`'s `sidecar/server.ts`) — which repeat the shim's health-probe\n * verdict on the events they receive, so the exclusion reaches a deployment whose bundled\n * shim predates it.\n */\nexport { HealthProbeEventFilter } from \"./health-probe-filter\";\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkNA,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"}
|
package/dist/version.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Replaced by tsdown's `define`. The guard keeps a direct source import
|
|
9
9
|
* (vitest, typecheck) working, where no bundler has substituted the value.
|
|
10
10
|
*/
|
|
11
|
-
const WORKERD_SHIM_VERSION = "2.
|
|
11
|
+
const WORKERD_SHIM_VERSION = "2.334.0";
|
|
12
12
|
//#endregion
|
|
13
13
|
export { WORKERD_SHIM_VERSION };
|
|
14
14
|
|
package/dist/with-meticulous.js
CHANGED
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-meticulous.js","names":[],"sources":["../src/with-meticulous.ts"],"sourcesContent":["import { installBindingPatch } from \"./binding-patch\";\nimport { CaptureBuffer } from \"./capture-buffer\";\nimport {\n type RequestCaptureContext,\n type RequestReplayContext,\n requestCaptureContext,\n} from \"./context\";\nimport { runReplayWithCoverage } from \"./coverage/replay-coverage\";\nimport { installFetchPatch } from \"./fetch-patch\";\nimport { 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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"with-meticulous.js","names":[],"sources":["../src/with-meticulous.ts"],"sourcesContent":["import { installBindingPatch } from \"./binding-patch\";\nimport { CaptureBuffer } from \"./capture-buffer\";\nimport {\n type RequestCaptureContext,\n type RequestReplayContext,\n requestCaptureContext,\n} from \"./context\";\nimport { runReplayWithCoverage } from \"./coverage/replay-coverage\";\nimport { installFetchPatch } from \"./fetch-patch\";\nimport { isHealthProbeRequest } from \"./health-probe\";\nimport { installKvPatch } from \"./kv-patch\";\nimport { warnOnce } from \"./log\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport { headersToRecord } from \"./outbound-capture\";\nimport {\n FRONTEND_SESSION_ID_HEADER,\n type InboundRequestEvent,\n parseVirtualTimeMs,\n REPLAY_ID_HEADER,\n REPLAY_SIDECAR_URL_HEADER,\n VIRTUAL_TIME_HEADER,\n} from \"./protocol\";\nimport {\n isProvisionalSessionIdCandidate,\n mintProvisionalSessionId,\n} from \"./provisional-session-id\";\nimport { publishSessionIdOnResponse } from \"./publish-session-id\";\nimport { publishWorkerdShimVersionOnResponse } from \"./publish-shim-version\";\nimport { installReplayLogTagging } from \"./replay-log-tagging\";\nimport { parseReplaySidecarUrl } from \"./replay-sidecar-url\";\nimport { getReplaySessionInfo } from \"./sidecar-client\";\nimport {\n resolveSidecarTransport,\n type SidecarFetcher,\n type SidecarTransport,\n} from \"./sidecar-transport\";\nimport { randomHex } from \"./spans/span-builder\";\nimport { installVirtualClock } from \"./virtual-clock\";\nimport { installVirtualRandom } from \"./virtual-random\";\n\n/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */\nexport interface MeticulousExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface WithMeticulousOptions {\n /**\n * Overrides the sidecar service binding; defaults to the `METICULOUS_SIDECAR` binding on\n * `env`. This is how a **deployed** Worker records — see {@link SidecarTransport}.\n */\n sidecarBinding?: SidecarFetcher;\n /** Overrides the sidecar origin; defaults to the METICULOUS_SIDECAR_URL var/binding. */\n sidecarUrl?: string;\n /**\n * Binding names to leave unrecorded, on top of the defaults (`ASSETS`, `__STATIC_CONTENT`,\n * `METICULOUS_SIDECAR`). Use this for a binding whose traffic is high-volume or binary. Applies\n * to KV namespaces as well as `fetch`-shaped bindings.\n */\n skipBindings?: readonly string[];\n /**\n * Whether to mint a session id for a document navigation that carries none, so the\n * server-side render is recorded under the same session as the page it produces. On by\n * default; set `false` (or the `METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS` var to\n * `\"false\"`) to opt out. See provisional-session-id.ts.\n */\n mintProvisionalSessionIds?: boolean;\n}\n\n/** What the mode-agnostic core needs from whichever handler shape wraps it. */\nexport interface MeticulousInvocation {\n request: Request;\n env: unknown;\n ctx: MeticulousExecutionContext;\n invokeHandler: () => Response | Promise<Response>;\n}\n\n/**\n * Records, or replays, one inbound request. Shared by `withMeticulous` (an ES-module Worker's\n * `fetch`) and `withMeticulousPagesFunction` (a Pages Functions `onRequest`), which differ only\n * in how they get at the request, the env and `waitUntil`.\n */\nexport const runWithMeticulous = async (\n { request, env, ctx, invokeHandler }: MeticulousInvocation,\n options: WithMeticulousOptions | undefined,\n): Promise<Response> => {\n let transport: SidecarTransport | undefined;\n let replayContext: RequestReplayContext | undefined;\n try {\n replayContext = await resolveReplayContext(request, ctx);\n transport =\n replayContext === undefined\n ? resolveSidecarTransport(options, env)\n : undefined;\n if (replayContext !== undefined || transport !== undefined) {\n installFetchPatch();\n }\n if (transport !== undefined) {\n // Lazily, inside the request path, and never at module scope: the package declares no\n // `sideEffects` and is built with `platform: \"neutral\"`, so a module-scope side effect can\n // legitimately be tree-shaken by a customer's bundler. Re-run per request because `env` is\n // the only handle on the binding instances.\n //\n // Record mode only. Binding and KV calls are recorded as their own technologies\n // (`workerd-binding`, `workerd-kv`) and the replay sidecar's store holds only\n // `workerd-fetch` spans, so there is nothing to serve them from — and patching would only\n // route them into the capture tee, which POSTs to an events route a replay sidecar 404s.\n //\n // `skipInstances` keeps the sidecar's own binding out of the recording by identity rather\n // than by name, so it holds however the customer named it or passed it in.\n const patchOptions = {\n ...(options?.skipBindings !== undefined\n ? { skipBindings: options.skipBindings }\n : {}),\n ...(transport.kind === \"binding\"\n ? { skipInstances: [transport.instance] }\n : {}),\n };\n installBindingPatch(env, patchOptions);\n installKvPatch(env, patchOptions);\n }\n } catch (error) {\n warnOnce(\n \"shim-init\",\n \"Failed to initialize the Meticulous backend recorder shim — recording disabled.\",\n error,\n );\n replayContext = undefined;\n transport = undefined;\n }\n\n if (replayContext !== undefined) {\n // Workerd has no process.stdout/process.stderr seam like Node. Patch its\n // console methods instead; each call resolves this request's replay id from\n // the AsyncLocalStorage entered below.\n installReplayLogTagging();\n // Install the clock before any app code runs, so a module that reads Date.now() at call time\n // (the firebase auth libraries do) sees the session's frozen instant.\n installVirtualClock();\n // Likewise the random generators: an id the app mints during SSR (a guest id, a request id)\n // otherwise differs between a base and a head replay, and every rendered page carrying it\n // diffs forever.\n installVirtualRandom();\n // No inbound reporting in replay mode: there is no exporter behind the replay sidecar, and\n // the events route 404s there.\n const replayResponse = await runReplayWithCoverage(\n replayContext,\n invokeHandler,\n );\n return publishWorkerdShimVersionOnResponse(replayResponse);\n }\n\n if (transport === undefined) {\n return invokeHandler();\n }\n\n // Never enter the capture context for a health probe, so neither the inbound request nor\n // anything it fans out to is recorded. The fetch, binding and KV patches are all\n // pass-throughs without a context, so this covers the whole subtree in one place.\n if (\n isHealthProbeRequest(request.method, request.url, (name) =>\n request.headers.get(name),\n )\n ) {\n return invokeHandler();\n }\n\n const captureCtx = buildCaptureContext(\n request,\n transport,\n ctx,\n resolveMintProvisionalSessionIds(options, env),\n );\n const mintedSessionId =\n captureCtx.sessionIdOrigin === \"backend\"\n ? captureCtx.frontendSessionId\n : undefined;\n const startTimeMs = Date.now();\n let requestHeaders: Record<string, string[]> = {};\n let method = \"GET\";\n let url = \"\";\n try {\n requestHeaders = headersToRecord(request.headers);\n method = request.method;\n url = request.url;\n } catch (error) {\n warnOnce(\n \"inbound-capture\",\n \"Failed to capture the inbound request.\",\n error,\n );\n }\n\n const reportInbound = (\n outcome:\n | { statusCode: number; responseHeaders: Record<string, string[]> }\n | { error: string },\n ): void => {\n try {\n const event: InboundRequestEvent = {\n kind: \"inbound\",\n requestId: captureCtx.requestId,\n ...(captureCtx.frontendSessionId !== undefined\n ? { frontendSessionId: captureCtx.frontendSessionId }\n : {}),\n ...(captureCtx.sessionIdOrigin !== undefined\n ? { sessionIdOrigin: captureCtx.sessionIdOrigin }\n : {}),\n traceId: captureCtx.traceId,\n serverSpanId: captureCtx.serverSpanId,\n method,\n url,\n requestHeaders,\n startTimeMs,\n endTimeMs: Date.now(),\n ...outcome,\n };\n captureCtx.buffer.add(event);\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n };\n\n let response: Response;\n try {\n response = await requestCaptureContext.run(captureCtx, invokeHandler);\n } catch (error) {\n reportInbound({ error: String(error) });\n closeBuffer(captureCtx);\n throw error;\n }\n\n // Ahead of reporting, so what we record as the response headers is what the browser is\n // actually served.\n if (mintedSessionId !== undefined) {\n response = publishSessionIdOnResponse(response, mintedSessionId);\n }\n response = publishWorkerdShimVersionOnResponse(response);\n\n try {\n reportInbound({\n statusCode: response.status,\n responseHeaders: headersToRecord(response.headers),\n });\n } catch (error) {\n warnOnce(\"inbound-report\", \"Failed to report the inbound request.\", error);\n }\n closeBuffer(captureCtx);\n return response;\n};\n\n/**\n * Hands the request's single report to `waitUntil`. `close()` drains the outstanding body reads\n * first, which is why there is one `waitUntil` per request rather than one per captured call.\n */\nconst closeBuffer = (ctx: RequestCaptureContext): void => {\n try {\n ctx.waitUntil(ctx.buffer.close());\n } catch (error) {\n warnOnce(\n \"buffer-close\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n};\n\n/**\n * Whether this request should be replayed, and the context to serve it under.\n *\n * Both replay identity ids and a usable sidecar URL must be present: mocks are indexed per session\n * and consumed per replay. The sidecar is consulted once per (sidecar, session) per isolate to\n * confirm it holds mocks and to learn the clock anchor — a sidecar that does not recognise the\n * session, or the route, means replay is unavailable and the request runs normally.\n */\nconst resolveReplayContext = async (\n request: Request,\n ctx: MeticulousExecutionContext,\n): Promise<RequestReplayContext | undefined> => {\n let sidecarUrl: string | undefined;\n let frontendSessionId: string | undefined;\n let replayId: string | undefined;\n let virtualTimeMs: number | undefined;\n try {\n sidecarUrl = parseReplaySidecarUrl(\n request.headers.get(REPLAY_SIDECAR_URL_HEADER),\n );\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n replayId = request.headers.get(REPLAY_ID_HEADER) ?? undefined;\n virtualTimeMs = parseVirtualTimeMs(\n request.headers.get(VIRTUAL_TIME_HEADER),\n );\n } catch {\n return undefined;\n }\n if (\n sidecarUrl === undefined ||\n frontendSessionId === undefined ||\n replayId === undefined\n ) {\n return undefined;\n }\n\n const info = await getCachedReplaySessionInfo(sidecarUrl, frontendSessionId);\n if (info === null) {\n return undefined;\n }\n\n return {\n mode: \"replay\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n replayId,\n sidecarUrl,\n clockAnchorMs: info.clockAnchorMs,\n ...(virtualTimeMs !== undefined ? { virtualTimeMs } : {}),\n waitUntil: buildWaitUntil(ctx),\n };\n};\n\n// Caches the promise, not the result, so concurrent first requests share one round trip.\n// Bounded so a stream of unrecognised session ids cannot grow it without limit.\nconst MAX_CACHED_SESSIONS = 50;\nconst replaySessionInfoCache = new Map<\n string,\n Promise<{ clockAnchorMs: number | undefined } | null>\n>();\n\nconst getCachedReplaySessionInfo = (\n sidecarUrl: string,\n frontendSessionId: string,\n): Promise<{ clockAnchorMs: number | undefined } | null> => {\n const key = `${sidecarUrl}\u0000${frontendSessionId}`;\n const cached = replaySessionInfoCache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n const pending = getReplaySessionInfo(\n getOriginalFetch(),\n sidecarUrl,\n frontendSessionId,\n ).then((result) => {\n if (result.outcome === \"unreachable\") {\n // Only settled answers are worth keeping. Caching a timeout or transport blip would\n // disable mocking for this session for the life of the isolate, quietly sending every\n // later outbound call to the real service — the failure this path exists to prevent.\n // Dropping the entry costs one extra handshake and lets the next request recover.\n replaySessionInfoCache.delete(key);\n return null;\n }\n return result.outcome === \"found\"\n ? { clockAnchorMs: result.clockAnchorMs }\n : null;\n });\n if (replaySessionInfoCache.size >= MAX_CACHED_SESSIONS) {\n const oldest = replaySessionInfoCache.keys().next();\n if (!oldest.done) {\n replaySessionInfoCache.delete(oldest.value);\n }\n }\n replaySessionInfoCache.set(key, pending);\n return pending;\n};\n\nconst buildCaptureContext = (\n request: Request,\n transport: SidecarTransport,\n ctx: MeticulousExecutionContext,\n mintProvisionalSessionIds: boolean,\n): RequestCaptureContext => {\n let frontendSessionId: string | undefined;\n let sessionIdOrigin: \"backend\" | undefined;\n try {\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n // A top-level document navigation cannot carry the header, so for one we mint the id\n // ourselves and hand it to the page on the response — otherwise the server-side render this\n // request is about is recorded against no session at all. Every outbound, binding, KV and\n // postgres event reads the id back off this context, so putting it here attributes the whole\n // request. See provisional-session-id.ts.\n if (\n mintProvisionalSessionIds &&\n frontendSessionId === undefined &&\n isProvisionalSessionIdCandidate(request.method, (name) =>\n request.headers.get(name),\n )\n ) {\n frontendSessionId = mintProvisionalSessionId();\n sessionIdOrigin = \"backend\";\n }\n } catch {\n frontendSessionId = undefined;\n sessionIdOrigin = undefined;\n }\n const waitUntil = buildWaitUntil(ctx);\n return {\n mode: \"record\",\n requestId: crypto.randomUUID(),\n frontendSessionId,\n ...(sessionIdOrigin !== undefined ? { sessionIdOrigin } : {}),\n transport,\n buffer: new CaptureBuffer(transport, waitUntil),\n traceId: randomHex(16),\n serverSpanId: randomHex(8),\n waitUntil,\n };\n};\n\n/**\n * On by default: the render it attributes — the page's very first — is otherwise recorded\n * against no session at all, and the only thing that reaches a replay for those spans is\n * ingestion's time-window guess.\n *\n * Opting out is a worker var rather than a host environment variable for the same reason the\n * sidecar URL is: workerd cannot see the host's environment.\n */\nconst resolveMintProvisionalSessionIds = (\n options: WithMeticulousOptions | undefined,\n env: unknown,\n): boolean => {\n if (options?.mintProvisionalSessionIds !== undefined) {\n return options.mintProvisionalSessionIds;\n }\n const fromEnv =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)[\n \"METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS\"\n ]\n : undefined;\n return fromEnv !== \"false\";\n};\n\nconst buildWaitUntil =\n (ctx: MeticulousExecutionContext) =>\n (promise: Promise<unknown>): void => {\n try {\n ctx.waitUntil(promise);\n } catch (error) {\n warnOnce(\n \"wait-until\",\n \"ctx.waitUntil failed — capture events may be lost.\",\n error,\n );\n }\n };\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwaysmeticulous/backend-recorder-workerd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.334.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",
|