@alwaysmeticulous/backend-recorder-workerd 2.334.0 → 2.336.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/datadog-agent.d.ts +45 -0
- package/dist/datadog-agent.js +85 -0
- package/dist/datadog-agent.js.map +1 -0
- package/dist/fetch-patch.js +2 -0
- package/dist/fetch-patch.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/version.js +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
|
+
- An outbound `fetch` headed for a Datadog agent, carrying no frontend session id, is recorded as nothing. A tracer flushes on its own timer, so nothing can attribute the call, and the payload is every span it had buffered. A destination counts as the agent only when its port is `8126` **and** its path is one of the agent's own API endpoints (`/info`, `/v0.4/traces` and the rest of that family, `/profiling/v1/input`, `/telemetry/proxy/…`, `/evp_proxy/…`, …); either half alone would risk dropping a real call, since an app may serve anything on 8126 and a `/v0.4/traces`-shaped path elsewhere is the app's own endpoint. Agentless submission to a `datadoghq.com` intake is not covered, and neither is a call a session made. Record mode only; a replay is unaffected.
|
|
202
203
|
- 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.
|
|
203
204
|
- A Durable Object stub is recorded without a binding name, since the stub comes from `namespace.get()` rather than being a binding itself.
|
|
204
205
|
- 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.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/datadog-agent.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where an outbound call is headed, in the port/path shape Node's `http.RequestOptions` gives
|
|
4
|
+
* us. A URL is the same two fields already parsed — see {@link isOrphanedDatadogAgentUrl}.
|
|
5
|
+
*/
|
|
6
|
+
interface OutboundCallTarget {
|
|
7
|
+
port: string | number | undefined;
|
|
8
|
+
/** Origin-form target (`/v0.4/traces?x=1`), or a URL's pathname. */
|
|
9
|
+
path: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether this outbound call is the app's own telemetry leaving for a Datadog agent, with
|
|
13
|
+
* nothing to tie it to a session.
|
|
14
|
+
*
|
|
15
|
+
* A tracer (`dd-trace`, an OTLP exporter pointed at the agent) flushes on a background timer
|
|
16
|
+
* for the lifetime of the process, so its calls sit outside every request's async context and
|
|
17
|
+
* can never be attributed. Recording them costs a steady stream of unstamped CLIENT spans —
|
|
18
|
+
* each a candidate for ingestion's time-window `inferred` tier, where a msgpack trace payload
|
|
19
|
+
* can be handed to an unrelated session as a fallback mock — and the payloads themselves are
|
|
20
|
+
* large and binary, so capturing the bodies is pure waste.
|
|
21
|
+
*
|
|
22
|
+
* Narrow on two axes, for the same reason `isHealthProbeRequest` is:
|
|
23
|
+
*
|
|
24
|
+
* - **No frontend session id.** A flush that _did_ happen inside a request the browser or an
|
|
25
|
+
* SSR fan-out named is attributable app traffic, and stays recorded. This can therefore only
|
|
26
|
+
* drop spans that ingestion's session-id match would have discarded anyway.
|
|
27
|
+
* - **The agent's port _and_ one of its own API paths.** Either alone is not enough: an app is
|
|
28
|
+
* free to serve anything on port 8126, and a path shaped like `/v0.4/traces` on some other
|
|
29
|
+
* port is the app's own endpoint. Requiring both is what makes a fixed path list safe without
|
|
30
|
+
* knowing the agent's hostname, which is unrecognisable in practice — `localhost`,
|
|
31
|
+
* `datadog-agent`, a Kubernetes node IP. Dropping a real call is far worse than recording a
|
|
32
|
+
* flush, since the call then has no mock and fails hermetically on replay.
|
|
33
|
+
*
|
|
34
|
+
* A consequence worth knowing: an agent on a non-default port (`DD_TRACE_AGENT_PORT`) and
|
|
35
|
+
* agentless submission straight to a `datadoghq.com` intake both keep being recorded.
|
|
36
|
+
*/
|
|
37
|
+
declare const isOrphanedDatadogAgentRequest: (target: OutboundCallTarget, frontendSessionId: string | undefined) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The same verdict from an absolute URL, which is the shape the workerd fetch patch and a
|
|
40
|
+
* capture event on the wire carry.
|
|
41
|
+
*/
|
|
42
|
+
declare const isOrphanedDatadogAgentUrl: (url: string, frontendSessionId: string | undefined) => boolean;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { OutboundCallTarget, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl };
|
|
45
|
+
//# sourceMappingURL=datadog-agent.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region src/datadog-agent.ts
|
|
2
|
+
/**
|
|
3
|
+
* The Datadog agent's HTTP (APM) port. Required, so the rule cannot fire on a host that
|
|
4
|
+
* merely happens to serve a path shaped like one of the agent's.
|
|
5
|
+
*/
|
|
6
|
+
const DATADOG_AGENT_PORT = "8126";
|
|
7
|
+
/**
|
|
8
|
+
* Agent endpoints whose whole path is fixed. `/info` is the capability probe every tracer
|
|
9
|
+
* polls at startup — far too generic a path to recognise anywhere else, and safe here only
|
|
10
|
+
* because the port has already been matched.
|
|
11
|
+
*/
|
|
12
|
+
const DATADOG_AGENT_PATHS = new Set([
|
|
13
|
+
"/info",
|
|
14
|
+
"/profiling/v1/input",
|
|
15
|
+
"/dogstatsd/v2/proxy",
|
|
16
|
+
"/tracer_flare/v1"
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Namespaces the agent proxies open-ended sub-paths under, so they can only be matched by
|
|
20
|
+
* prefix.
|
|
21
|
+
*/
|
|
22
|
+
const DATADOG_AGENT_PATH_PREFIXES = [
|
|
23
|
+
"/telemetry/proxy/",
|
|
24
|
+
"/evp_proxy/",
|
|
25
|
+
"/appsec/proxy/"
|
|
26
|
+
];
|
|
27
|
+
/** Agent endpoints whose path carries a protocol version the tracer negotiates. */
|
|
28
|
+
const DATADOG_AGENT_PATH_PATTERNS = [/^\/v\d+\.\d+\/(traces|stats|config|pipeline_stats)$/, /^\/(debugger|symdb)\/v\d+\/(input|diagnostics)$/];
|
|
29
|
+
/**
|
|
30
|
+
* Whether this outbound call is the app's own telemetry leaving for a Datadog agent, with
|
|
31
|
+
* nothing to tie it to a session.
|
|
32
|
+
*
|
|
33
|
+
* A tracer (`dd-trace`, an OTLP exporter pointed at the agent) flushes on a background timer
|
|
34
|
+
* for the lifetime of the process, so its calls sit outside every request's async context and
|
|
35
|
+
* can never be attributed. Recording them costs a steady stream of unstamped CLIENT spans —
|
|
36
|
+
* each a candidate for ingestion's time-window `inferred` tier, where a msgpack trace payload
|
|
37
|
+
* can be handed to an unrelated session as a fallback mock — and the payloads themselves are
|
|
38
|
+
* large and binary, so capturing the bodies is pure waste.
|
|
39
|
+
*
|
|
40
|
+
* Narrow on two axes, for the same reason `isHealthProbeRequest` is:
|
|
41
|
+
*
|
|
42
|
+
* - **No frontend session id.** A flush that _did_ happen inside a request the browser or an
|
|
43
|
+
* SSR fan-out named is attributable app traffic, and stays recorded. This can therefore only
|
|
44
|
+
* drop spans that ingestion's session-id match would have discarded anyway.
|
|
45
|
+
* - **The agent's port _and_ one of its own API paths.** Either alone is not enough: an app is
|
|
46
|
+
* free to serve anything on port 8126, and a path shaped like `/v0.4/traces` on some other
|
|
47
|
+
* port is the app's own endpoint. Requiring both is what makes a fixed path list safe without
|
|
48
|
+
* knowing the agent's hostname, which is unrecognisable in practice — `localhost`,
|
|
49
|
+
* `datadog-agent`, a Kubernetes node IP. Dropping a real call is far worse than recording a
|
|
50
|
+
* flush, since the call then has no mock and fails hermetically on replay.
|
|
51
|
+
*
|
|
52
|
+
* A consequence worth knowing: an agent on a non-default port (`DD_TRACE_AGENT_PORT`) and
|
|
53
|
+
* agentless submission straight to a `datadoghq.com` intake both keep being recorded.
|
|
54
|
+
*/
|
|
55
|
+
const isOrphanedDatadogAgentRequest = (target, frontendSessionId) => frontendSessionId == null && String(target.port ?? "") === DATADOG_AGENT_PORT && isDatadogAgentPath(target.path);
|
|
56
|
+
/**
|
|
57
|
+
* The same verdict from an absolute URL, which is the shape the workerd fetch patch and a
|
|
58
|
+
* capture event on the wire carry.
|
|
59
|
+
*/
|
|
60
|
+
const isOrphanedDatadogAgentUrl = (url, frontendSessionId) => {
|
|
61
|
+
if (frontendSessionId != null) return false;
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = new URL(url);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return isOrphanedDatadogAgentRequest({
|
|
69
|
+
port: parsed.port,
|
|
70
|
+
path: parsed.pathname
|
|
71
|
+
}, void 0);
|
|
72
|
+
};
|
|
73
|
+
const isDatadogAgentPath = (path) => {
|
|
74
|
+
const pathname = normalizeAgentPath(path);
|
|
75
|
+
if (pathname === void 0) return false;
|
|
76
|
+
return DATADOG_AGENT_PATHS.has(pathname) || DATADOG_AGENT_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix)) || DATADOG_AGENT_PATH_PATTERNS.some((pattern) => pattern.test(pathname));
|
|
77
|
+
};
|
|
78
|
+
const normalizeAgentPath = (path) => {
|
|
79
|
+
if (path === void 0 || !path.startsWith("/")) return;
|
|
80
|
+
return path.split("?")[0].split("#")[0].toLowerCase();
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=datadog-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datadog-agent.js","names":[],"sources":["../src/datadog-agent.ts"],"sourcesContent":["/**\n * The Datadog agent's HTTP (APM) port. Required, so the rule cannot fire on a host that\n * merely happens to serve a path shaped like one of the agent's.\n */\nconst DATADOG_AGENT_PORT = \"8126\";\n\n/**\n * Agent endpoints whose whole path is fixed. `/info` is the capability probe every tracer\n * polls at startup — far too generic a path to recognise anywhere else, and safe here only\n * because the port has already been matched.\n */\nconst DATADOG_AGENT_PATHS = new Set([\n \"/info\",\n \"/profiling/v1/input\",\n \"/dogstatsd/v2/proxy\",\n \"/tracer_flare/v1\",\n]);\n\n/**\n * Namespaces the agent proxies open-ended sub-paths under, so they can only be matched by\n * prefix.\n */\nconst DATADOG_AGENT_PATH_PREFIXES = [\n \"/telemetry/proxy/\",\n \"/evp_proxy/\",\n \"/appsec/proxy/\",\n];\n\n/** Agent endpoints whose path carries a protocol version the tracer negotiates. */\nconst DATADOG_AGENT_PATH_PATTERNS = [\n // Trace, stats, remote-config and data-streams submission: /v0.4/traces, /v0.5/traces,\n // /v0.6/stats, /v0.7/config, /v0.1/pipeline_stats.\n /^\\/v\\d+\\.\\d+\\/(traces|stats|config|pipeline_stats)$/,\n // Dynamic instrumentation and symbol upload: /debugger/v1/input,\n // /debugger/v1/diagnostics, /symdb/v1/input.\n /^\\/(debugger|symdb)\\/v\\d+\\/(input|diagnostics)$/,\n];\n\n/**\n * Where an outbound call is headed, in the port/path shape Node's `http.RequestOptions` gives\n * us. A URL is the same two fields already parsed — see {@link isOrphanedDatadogAgentUrl}.\n */\nexport interface OutboundCallTarget {\n port: string | number | undefined;\n /** Origin-form target (`/v0.4/traces?x=1`), or a URL's pathname. */\n path: string | undefined;\n}\n\n/**\n * Whether this outbound call is the app's own telemetry leaving for a Datadog agent, with\n * nothing to tie it to a session.\n *\n * A tracer (`dd-trace`, an OTLP exporter pointed at the agent) flushes on a background timer\n * for the lifetime of the process, so its calls sit outside every request's async context and\n * can never be attributed. Recording them costs a steady stream of unstamped CLIENT spans —\n * each a candidate for ingestion's time-window `inferred` tier, where a msgpack trace payload\n * can be handed to an unrelated session as a fallback mock — and the payloads themselves are\n * large and binary, so capturing the bodies is pure waste.\n *\n * Narrow on two axes, for the same reason `isHealthProbeRequest` is:\n *\n * - **No frontend session id.** A flush that _did_ happen inside a request the browser or an\n * SSR fan-out named is attributable app traffic, and stays recorded. This can therefore only\n * drop spans that ingestion's session-id match would have discarded anyway.\n * - **The agent's port _and_ one of its own API paths.** Either alone is not enough: an app is\n * free to serve anything on port 8126, and a path shaped like `/v0.4/traces` on some other\n * port is the app's own endpoint. Requiring both is what makes a fixed path list safe without\n * knowing the agent's hostname, which is unrecognisable in practice — `localhost`,\n * `datadog-agent`, a Kubernetes node IP. Dropping a real call is far worse than recording a\n * flush, since the call then has no mock and fails hermetically on replay.\n *\n * A consequence worth knowing: an agent on a non-default port (`DD_TRACE_AGENT_PORT`) and\n * agentless submission straight to a `datadoghq.com` intake both keep being recorded.\n */\nexport const isOrphanedDatadogAgentRequest = (\n target: OutboundCallTarget,\n frontendSessionId: string | undefined,\n): boolean =>\n frontendSessionId == null &&\n String(target.port ?? \"\") === DATADOG_AGENT_PORT &&\n isDatadogAgentPath(target.path);\n\n/**\n * The same verdict from an absolute URL, which is the shape the workerd fetch patch and a\n * capture event on the wire carry.\n */\nexport const isOrphanedDatadogAgentUrl = (\n url: string,\n frontendSessionId: string | undefined,\n): boolean => {\n if (frontendSessionId != null) {\n return false;\n }\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n return isOrphanedDatadogAgentRequest(\n { port: parsed.port, path: parsed.pathname },\n undefined,\n );\n};\n\nconst isDatadogAgentPath = (path: string | undefined): boolean => {\n const pathname = normalizeAgentPath(path);\n if (pathname === undefined) {\n return false;\n }\n return (\n DATADOG_AGENT_PATHS.has(pathname) ||\n DATADOG_AGENT_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix)) ||\n DATADOG_AGENT_PATH_PATTERNS.some((pattern) => pattern.test(pathname))\n );\n};\n\nconst normalizeAgentPath = (path: string | undefined): string | undefined => {\n if (path === undefined || !path.startsWith(\"/\")) {\n return undefined;\n }\n return path.split(\"?\")[0].split(\"#\")[0].toLowerCase();\n};\n"],"mappings":";;;;;AAIA,MAAM,qBAAqB;;;;;;AAO3B,MAAM,sBAAsB,IAAI,IAAI;CAClC;CACA;CACA;CACA;CACD,CAAC;;;;;AAMF,MAAM,8BAA8B;CAClC;CACA;CACA;CACD;;AAGD,MAAM,8BAA8B,CAGlC,uDAGA,kDACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCD,MAAa,iCACX,QACA,sBAEA,qBAAqB,QACrB,OAAO,OAAO,QAAQ,GAAG,KAAK,sBAC9B,mBAAmB,OAAO,KAAK;;;;;AAMjC,MAAa,6BACX,KACA,sBACY;AACZ,KAAI,qBAAqB,KACvB,QAAO;CAET,IAAI;AACJ,KAAI;AACF,WAAS,IAAI,IAAI,IAAI;SACf;AACN,SAAO;;AAET,QAAO,8BACL;EAAE,MAAM,OAAO;EAAM,MAAM,OAAO;EAAU,EAC5C,KAAA,EACD;;AAGH,MAAM,sBAAsB,SAAsC;CAChE,MAAM,WAAW,mBAAmB,KAAK;AACzC,KAAI,aAAa,KAAA,EACf,QAAO;AAET,QACE,oBAAoB,IAAI,SAAS,IACjC,4BAA4B,MAAM,WAAW,SAAS,WAAW,OAAO,CAAC,IACzE,4BAA4B,MAAM,YAAY,QAAQ,KAAK,SAAS,CAAC;;AAIzE,MAAM,sBAAsB,SAAiD;AAC3E,KAAI,SAAS,KAAA,KAAa,CAAC,KAAK,WAAW,IAAI,CAC7C;AAEF,QAAO,KAAK,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,aAAa"}
|
package/dist/fetch-patch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requestCaptureContext, sidecarOriginOf } from "./context.js";
|
|
2
2
|
import { captureOutboundCall } from "./outbound-capture.js";
|
|
3
3
|
import { getOriginalFetch, setOriginalFetch } from "./original-fetch.js";
|
|
4
|
+
import { isOrphanedDatadogAgentUrl } from "./datadog-agent.js";
|
|
4
5
|
import { replayOutboundCall } from "./replay-fetch.js";
|
|
5
6
|
//#region src/fetch-patch.ts
|
|
6
7
|
const FETCH_PATCHED = Symbol.for("meticulous.workerd.fetchPatched");
|
|
@@ -34,6 +35,7 @@ const patchedFetch = async (input, init) => {
|
|
|
34
35
|
return original(input, init);
|
|
35
36
|
}
|
|
36
37
|
if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) return original(request);
|
|
38
|
+
if (ctx.mode === "record" && isOrphanedDatadogAgentUrl(request.url, ctx.frontendSessionId)) return original(request);
|
|
37
39
|
return ctx.mode === "replay" ? replayOutboundCall(ctx, request, (req) => original(req)) : captureOutboundCall(ctx, { kind: "outbound" }, request, (req) => original(req));
|
|
38
40
|
};
|
|
39
41
|
//#endregion
|
package/dist/fetch-patch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-patch.js","names":[],"sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n return ctx.mode === \"replay\"\n ? replayOutboundCall(ctx, request, (req) => original(req))\n : captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"fetch-patch.js","names":[],"sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { isOrphanedDatadogAgentUrl } from \"./datadog-agent\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n // The app's own telemetry leaving for a Datadog agent or intake, with nothing to tie it to\n // a session, is recorded as nothing at all. Record mode only, by construction: a replay\n // context always carries a session id, so the rule could not fire there either way.\n if (\n ctx.mode === \"record\" &&\n isOrphanedDatadogAgentUrl(request.url, ctx.frontendSessionId)\n ) {\n return original(request);\n }\n\n return ctx.mode === \"replay\"\n ? replayOutboundCall(ctx, request, (req) => original(req))\n : captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"mappings":";;;;;;AASA,MAAM,gBAAgB,OAAO,IAAI,kCAAkC;;;;;;;;;;;;;AAiBnE,MAAa,0BAAgC;CAC3C,MAAM,SAAS;AACf,KAAI,OAAO,eACT;AAEF,QAAO,iBAAiB;AACxB,kBAAiB,WAAW,MAAM,KAAK,WAAW,CAAC;AACnD,YAAW,QAAQ;;AAGrB,MAAM,eAAwB,OAAO,OAAO,SAAS;CACnD,MAAM,WAAW,kBAAkB;CACnC,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,CAAC,IACH,QAAO,SAAS,OAAO,KAAK;CAM9B,IAAI;AACJ,KAAI;AACF,YAAU,IAAI,QAAQ,OAAO,KAAK;SAC5B;AAEN,SAAO,SAAS,OAAO,KAAK;;AAK9B,KAAI,QAAQ,IAAI,WAAW,GAAG,gBAAgB,IAAI,CAAC,GAAG,CACpD,QAAO,SAAS,QAAQ;AAM1B,KACE,IAAI,SAAS,YACb,0BAA0B,QAAQ,KAAK,IAAI,kBAAkB,CAE7D,QAAO,SAAS,QAAQ;AAG1B,QAAO,IAAI,SAAS,WAChB,mBAAmB,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC,GACxD,oBAAoB,KAAK,EAAE,MAAM,YAAY,EAAE,UAAU,QACvD,SAAS,IAAI,CACd"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap } from "
|
|
|
6
6
|
import { headersToRecord } from "./outbound-capture.js";
|
|
7
7
|
import { HeaderReader, SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
8
8
|
import { isHealthProbeRequest } from "./health-probe.js";
|
|
9
|
+
import { OutboundCallTarget, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl } from "./datadog-agent.js";
|
|
9
10
|
import { HealthProbeEventFilter } from "./health-probe-filter.js";
|
|
10
11
|
import { STR_REDACTED, redactRequestBody } from "./redact-body.js";
|
|
11
12
|
import { CoverageMap, CoverageMapFile } from "./coverage/coverage.types.js";
|
|
@@ -87,5 +88,5 @@ declare const withMeticulous: <H extends MeticulousWorkerHandler<never>>(handler
|
|
|
87
88
|
*/
|
|
88
89
|
declare const getMeticulousSessionId: () => string | undefined;
|
|
89
90
|
//#endregion
|
|
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 };
|
|
91
|
+
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 OutboundCallTarget, 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, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, 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 };
|
|
91
92
|
//# 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 { isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl } from "./datadog-agent.js";
|
|
8
9
|
import { isHealthProbeRequest } from "./health-probe.js";
|
|
9
10
|
import { serializeKvArgs, serializeKvCaptureFields } from "./kv-capture.js";
|
|
10
11
|
import { SERVER_TIMING_HEADER, SERVER_TIMING_SESSION_METRIC, buildServerTimingSessionEntry, isProvisionalSessionIdCandidate, mintProvisionalSessionId } from "./provisional-session-id.js";
|
|
@@ -93,6 +94,6 @@ const withMeticulous = (handler, options) => {
|
|
|
93
94
|
*/
|
|
94
95
|
const getMeticulousSessionId = () => requestCaptureContext.getStore()?.frontendSessionId;
|
|
95
96
|
//#endregion
|
|
96
|
-
export { CAPTURED_HEADERS, CLIENT_TECHNOLOGY_ATTR, CLIENT_TECHNOLOGY_POSTGRES_JS, CLIENT_TECHNOLOGY_WORKERD_BINDING, CLIENT_TECHNOLOGY_WORKERD_FETCH, CLIENT_TECHNOLOGY_WORKERD_KV, FRONTEND_SESSION_ID_ATTR, FRONTEND_SESSION_ID_HEADER, 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 };
|
|
97
|
+
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, isOrphanedDatadogAgentRequest, isOrphanedDatadogAgentUrl, isProvisionalSessionIdCandidate, isSupportedPostgresJsQuery, markCoverageFilesReported, mintProvisionalSessionId, msToHrTime, parseVirtualTimeMs, postgresJsCommandOf, randomHex, readBodyWithCap, readRequestBodyWithCap, reconstructQueryText, redactRequestBody, redactUrlCredentials, registerCoverageFile, resolveRowMode, serializeCapturedError, serializeKvArgs, serializeKvCaptureFields, serializePostgresJsArgs, serializePostgresJsError, serializePostgresJsResult, serializeSpan, withMeticulous, withMeticulousPagesFunction, withMeticulousPostgres };
|
|
97
98
|
|
|
98
99
|
//# 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\";\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"}
|
|
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 the Node recorder, whose `datadog-agent.ts` adapts these to the forked http and\n * undici instrumentations' ignore hooks. Which destinations count as a Datadog agent is a rule\n * both surfaces have to agree on, so — like the health-probe rule above — it has exactly one\n * implementation.\n */\nexport {\n isOrphanedDatadogAgentRequest,\n isOrphanedDatadogAgentUrl,\n type OutboundCallTarget,\n} from \"./datadog-agent\";\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6NA,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.336.0";
|
|
12
12
|
//#endregion
|
|
13
13
|
export { WORKERD_SHIM_VERSION };
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwaysmeticulous/backend-recorder-workerd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.336.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",
|