@alwaysmeticulous/backend-recorder-workerd 2.308.0 → 2.315.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/body-capture.js +4 -1
- package/dist/body-capture.js.map +1 -1
- package/dist/context.js +4 -1
- package/dist/context.js.map +1 -1
- package/dist/fetch-patch.js +4 -1
- package/dist/fetch-patch.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/log.js +4 -1
- package/dist/log.js.map +1 -1
- package/dist/protocol.js +4 -1
- package/dist/protocol.js.map +1 -1
- package/dist/sidecar-client.js +4 -1
- package/dist/sidecar-client.js.map +1 -1
- package/package.json +14 -14
package/dist/body-capture.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/body-capture.ts
|
|
2
2
|
/** Matches the Node backend recorder's body capture cap (met-http-instrumentation). */
|
|
3
|
+
|
|
4
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b096087d-759c-5fe6-bdea-cedbc645fade")}catch(e){}}();
|
|
3
5
|
const MAX_BODY_CAPTURE_SIZE = 256 * 1024;
|
|
4
6
|
/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */
|
|
5
7
|
const BODY_READ_TIMEOUT_MS = 1e4;
|
|
@@ -56,4 +58,5 @@ const readBodyWithCap = async (stream) => {
|
|
|
56
58
|
//#endregion
|
|
57
59
|
export { MAX_BODY_CAPTURE_SIZE, readBodyWithCap };
|
|
58
60
|
|
|
59
|
-
//# sourceMappingURL=body-capture.js.map
|
|
61
|
+
//# sourceMappingURL=body-capture.js.map
|
|
62
|
+
//# debugId=b096087d-759c-5fe6-bdea-cedbc645fade
|
package/dist/body-capture.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body-capture.js","
|
|
1
|
+
{"version":3,"file":"body-capture.js","sources":["../src/body-capture.ts"],"sourcesContent":["import type { CapturedBody } from \"./protocol\";\n\n/** Matches the Node backend recorder's body capture cap (met-http-instrumentation). */\nexport const MAX_BODY_CAPTURE_SIZE = 256 * 1024;\n\n/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */\nconst BODY_READ_TIMEOUT_MS = 10_000;\n\nconst TIMED_OUT = Symbol(\"meticulous.bodyReadTimedOut\");\n\n/**\n * Reads a body stream up to {@link MAX_BODY_CAPTURE_SIZE} bytes, decoding as\n * UTF-8. Stops (and marks the capture truncated) on the size cap or the time\n * guard, so a long-lived stream can never pin the capture work. Returns\n * undefined for absent bodies.\n */\nexport const readBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n if (!stream) {\n return undefined;\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), BODY_READ_TIMEOUT_MS);\n });\n\n try {\n while (true) {\n const result = await Promise.race([reader.read(), timedOut]);\n if (result === TIMED_OUT) {\n truncated = true;\n break;\n }\n const { done, value } = result;\n if (done) {\n break;\n }\n if (!value || value.byteLength === 0) {\n continue;\n }\n const remaining = MAX_BODY_CAPTURE_SIZE - bytes;\n if (value.byteLength >= remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n bytes += remaining;\n truncated = truncated || value.byteLength > remaining;\n // Even an exact-cap chunk means we stop reading, so a longer stream\n // counts as truncated.\n const next = await Promise.race([reader.read(), timedOut]);\n if (next === TIMED_OUT || !next.done) {\n truncated = true;\n }\n break;\n }\n text += decoder.decode(value, { stream: true });\n bytes += value.byteLength;\n }\n } finally {\n clearTimeout(timer);\n reader.cancel().catch(() => {});\n }\n\n text += decoder.decode();\n return { body: text, truncated };\n};\n"],"names":[],"mappings":";;;;AAGA,MAAa,wBAAwB,MAAM;;AAG3C,MAAM,uBAAuB;AAE7B,MAAM,YAAY,OAAO,8BAA8B;;;;;;;AAQvD,MAAa,kBAAkB,OAC7B,WACsC;AACtC,KAAI,CAAC,OACH;CAGF,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,UAAU,IAAI,YAAY,QAAQ;CACxC,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,YAAY;CAEhB,IAAI;CACJ,MAAM,WAAW,IAAI,SAA2B,YAAY;AAC1D,UAAQ,iBAAiB,QAAQ,UAAU,EAAE,qBAAqB;GAClE;AAEF,KAAI;AACF,SAAO,MAAM;GACX,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC5D,OAAI,WAAW,WAAW;AACxB,gBAAY;AACZ;;GAEF,MAAM,EAAE,MAAM,UAAU;AACxB,OAAI,KACF;AAEF,OAAI,CAAC,SAAS,MAAM,eAAe,EACjC;GAEF,MAAM,YAAY,wBAAwB;AAC1C,OAAI,MAAM,cAAc,WAAW;AACjC,YAAQ,QAAQ,OAAO,MAAM,SAAS,GAAG,UAAU,EAAE,EAAE,QAAQ,MAAM,CAAC;AACtE,aAAS;AACT,gBAAY,aAAa,MAAM,aAAa;IAG5C,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC1D,QAAI,SAAS,aAAa,CAAC,KAAK,KAC9B,aAAY;AAEd;;AAEF,WAAQ,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;AAC/C,YAAS,MAAM;;WAET;AACR,eAAa,MAAM;AACnB,SAAO,QAAQ,CAAC,YAAY,GAAG;;AAGjC,SAAQ,QAAQ,QAAQ;AACxB,QAAO;EAAE,MAAM;EAAM;EAAW","debugId":"b096087d-759c-5fe6-bdea-cedbc645fade"}
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a47c51ed-eac5-5c70-8b10-0b5be782b45f")}catch(e){}}();
|
|
1
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
4
|
//#region src/context.ts
|
|
3
5
|
const requestCaptureContext = new AsyncLocalStorage();
|
|
4
6
|
//#endregion
|
|
5
7
|
export { requestCaptureContext };
|
|
6
8
|
|
|
7
|
-
//# sourceMappingURL=context.js.map
|
|
9
|
+
//# sourceMappingURL=context.js.map
|
|
10
|
+
//# debugId=a47c51ed-eac5-5c70-8b10-0b5be782b45f
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","
|
|
1
|
+
{"version":3,"file":"context.js","sources":["../src/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Per-inbound-request context, entered by the withMeticulous wrapper and read\n * by the fetch patch. Requires the `nodejs_als` (or `nodejs_compat`)\n * compatibility flag in the worker's wrangler configuration.\n */\nexport interface RequestCaptureContext {\n requestId: string;\n frontendSessionId: string | undefined;\n /** Normalized origin of the sidecar, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n /** The inbound request's ctx.waitUntil, for post-response capture work. */\n waitUntil: (promise: Promise<unknown>) => void;\n}\n\nexport const requestCaptureContext =\n new AsyncLocalStorage<RequestCaptureContext>();\n"],"names":[],"mappings":";;;;AAgBA,MAAa,wBACX,IAAI,mBAA0C","debugId":"a47c51ed-eac5-5c70-8b10-0b5be782b45f"}
|
package/dist/fetch-patch.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="33d6d8d0-4fe6-5145-b962-add1dcf50d46")}catch(e){}}();
|
|
1
3
|
import { requestCaptureContext } from "./context.js";
|
|
2
4
|
import { readBodyWithCap } from "./body-capture.js";
|
|
3
5
|
import { warnOnce } from "./log.js";
|
|
@@ -124,4 +126,5 @@ const reportInBackground = (ctx, report) => {
|
|
|
124
126
|
//#endregion
|
|
125
127
|
export { getOriginalFetch, headersToRecord, installFetchPatch };
|
|
126
128
|
|
|
127
|
-
//# sourceMappingURL=fetch-patch.js.map
|
|
129
|
+
//# sourceMappingURL=fetch-patch.js.map
|
|
130
|
+
//# debugId=33d6d8d0-4fe6-5145-b962-add1dcf50d46
|
package/dist/fetch-patch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-patch.js","
|
|
1
|
+
{"version":3,"file":"fetch-patch.js","sources":["../src/fetch-patch.ts"],"sourcesContent":["import { readBodyWithCap } from \"./body-capture\";\nimport { requestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\nimport {\n CAPTURED_HEADERS,\n type CapturedBody,\n type OutboundRequestEvent,\n} from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\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\nlet originalFetch: FetchFn | undefined;\n\n/** The unpatched fetch — used for the shim's own sidecar requests. */\nexport const getOriginalFetch = (): FetchFn =>\n originalFetch ?? globalThis.fetch;\n\nconst capturedHeaders = new Set<string>(CAPTURED_HEADERS);\n\nexport const headersToRecord = (headers: Headers): Record<string, string[]> => {\n const record: Record<string, string[]> = {};\n headers.forEach((value, name) => {\n const lowerName = name.toLowerCase();\n if (capturedHeaders.has(lowerName)) {\n (record[lowerName] ??= []).push(value);\n }\n });\n return record;\n};\n\n/**\n * Replaces globalThis.fetch with a passive capture tee: the real fetch is\n * called directly (the sidecar is never in the request path); request and\n * response are cloned and reported to the sidecar in the background. Outside\n * a withMeticulous request context the patch is a pure pass-through.\n * 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 originalFetch = 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 capture the shim's own reports to the sidecar.\n if (request.url.startsWith(`${ctx.sidecarUrl}/`)) {\n return original(request);\n }\n\n const startTimeMs = Date.now();\n let meta:\n | {\n method: string;\n url: string;\n requestHeaders: Record<string, string[]>;\n }\n | undefined;\n let requestBodyPromise: Promise<CapturedBody | undefined> =\n Promise.resolve(undefined);\n try {\n meta = {\n method: request.method,\n url: request.url,\n requestHeaders: headersToRecord(request.headers),\n };\n if (request.body) {\n const bodyClone = request.clone();\n requestBodyPromise = readBodyWithCap(bodyClone.body).catch(\n () => undefined,\n );\n }\n } catch (error) {\n warnOnce(\n \"outbound-request-capture\",\n \"Failed to capture an outbound request.\",\n error,\n );\n meta = undefined;\n }\n\n let response: Response;\n try {\n response = await original(request);\n } catch (error) {\n if (meta) {\n const frozenMeta = meta;\n const endTimeMs = Date.now();\n const errorMessage = String(error);\n reportInBackground(ctx, async () => {\n const requestBody = await requestBodyPromise;\n const event: OutboundRequestEvent = {\n kind: \"outbound\",\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n ...frozenMeta,\n ...(requestBody !== undefined ? { requestBody } : {}),\n startTimeMs,\n endTimeMs,\n error: errorMessage,\n };\n await postCaptureEvents(original, ctx.sidecarUrl, [event]);\n });\n }\n throw error;\n }\n\n if (meta) {\n const frozenMeta = meta;\n try {\n const responseClone = response.clone();\n const responseHeaders = headersToRecord(response.headers);\n const statusCode = response.status;\n const endTimeMs = Date.now();\n ctx.waitUntil(\n (async () => {\n const [requestBody, responseBody] = await Promise.all([\n requestBodyPromise,\n readBodyWithCap(responseClone.body).catch(() => undefined),\n ]);\n const event: OutboundRequestEvent = {\n kind: \"outbound\",\n requestId: ctx.requestId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n ...frozenMeta,\n responseHeaders,\n statusCode,\n ...(requestBody !== undefined ? { requestBody } : {}),\n ...(responseBody !== undefined ? { responseBody } : {}),\n startTimeMs,\n endTimeMs,\n };\n await postCaptureEvents(original, ctx.sidecarUrl, [event]);\n })().catch((error) => {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-response-capture\",\n \"Failed to capture an outbound response.\",\n error,\n );\n }\n }\n\n return response;\n};\n\nconst reportInBackground = (\n ctx: { waitUntil: (p: Promise<unknown>) => void },\n report: () => Promise<void>,\n): void => {\n try {\n ctx.waitUntil(\n report().catch((error) => {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }),\n );\n } catch (error) {\n warnOnce(\n \"outbound-error-report\",\n \"Failed to report a failed outbound request.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":"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","debugId":"33d6d8d0-4fe6-5145-b962-add1dcf50d46"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a80cfad7-b180-56e8-a9ee-ceabd81be885")}catch(e){}}();
|
|
1
3
|
import { requestCaptureContext } from "./context.js";
|
|
2
4
|
import { MAX_BODY_CAPTURE_SIZE, readBodyWithCap } from "./body-capture.js";
|
|
3
5
|
import { warnOnce } from "./log.js";
|
|
@@ -113,4 +115,5 @@ const buildCaptureContext = (request, sidecarUrl, ctx) => {
|
|
|
113
115
|
//#endregion
|
|
114
116
|
export { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, MAX_BODY_CAPTURE_SIZE, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER, headersToRecord, readBodyWithCap, withMeticulous };
|
|
115
117
|
|
|
116
|
-
//# sourceMappingURL=index.js.map
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
119
|
+
//# debugId=a80cfad7-b180-56e8-a9ee-ceabd81be885
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { type RequestCaptureContext, requestCaptureContext } from \"./context\";\nimport {\n getOriginalFetch,\n headersToRecord,\n installFetchPatch,\n} from \"./fetch-patch\";\nimport { warnOnce } from \"./log\";\nimport {\n FRONTEND_SESSION_ID_HEADER,\n type InboundRequestEvent,\n} from \"./protocol\";\nimport { postCaptureEvents } from \"./sidecar-client\";\n\nexport type {\n CaptureEvent,\n CapturedBody,\n CaptureEventsPayload,\n InboundRequestEvent,\n OutboundRequestEvent,\n} from \"./protocol\";\nexport {\n CAPTURED_HEADERS,\n FRONTEND_SESSION_ID_HEADER,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n} from \"./protocol\";\nexport { MAX_BODY_CAPTURE_SIZE, readBodyWithCap } from \"./body-capture\";\nexport { headersToRecord } from \"./fetch-patch\";\n\n/** Structural subset of workerd's ExecutionContext — avoids a @cloudflare/workers-types dependency. */\nexport interface MeticulousExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/** Structural subset of an ES-module Worker handler (`export default {...}`). */\nexport interface MeticulousWorkerHandler<Env = never> {\n fetch(\n request: Request,\n env: Env,\n ctx: MeticulousExecutionContext,\n ): Response | Promise<Response>;\n}\n\nexport interface WithMeticulousOptions {\n /** Overrides the sidecar origin; defaults to the METICULOUS_SIDECAR_URL var/binding. */\n sidecarUrl?: string;\n}\n\n/**\n * Wraps an ES-module Worker handler so that, when a Meticulous recorder\n * sidecar is configured, inbound requests and outgoing `fetch` calls are\n * reported to it as backend capture events:\n *\n * export default withMeticulous({\n * async fetch(request, env, ctx) { ... },\n * });\n *\n * Activation requires a `METICULOUS_SIDECAR_URL` var/binding (e.g. via\n * `.dev.vars` or `wrangler dev --var`) or `options.sidecarUrl`; without one\n * the wrapper is a complete pass-through, so it is safe to keep in deployed\n * code. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag.\n * A capture failure or unreachable sidecar never affects the app.\n */\nexport const withMeticulous = <H extends MeticulousWorkerHandler<never>>(\n handler: H,\n options?: WithMeticulousOptions,\n): H => {\n const wrappedFetch = async (\n request: Request,\n env: unknown,\n ctx: MeticulousExecutionContext,\n ): Promise<Response> => {\n const invokeHandler = () => handler.fetch(request, env as never, ctx);\n\n let sidecarUrl: string | undefined;\n try {\n sidecarUrl = resolveSidecarUrl(options, env);\n if (sidecarUrl !== undefined) {\n installFetchPatch();\n }\n } catch (error) {\n warnOnce(\n \"shim-init\",\n \"Failed to initialize the Meticulous backend recorder shim — recording disabled.\",\n error,\n );\n sidecarUrl = undefined;\n }\n if (sidecarUrl === undefined) {\n return invokeHandler();\n }\n\n const captureCtx = buildCaptureContext(request, sidecarUrl, ctx);\n const startTimeMs = Date.now();\n let requestHeaders: Record<string, string[]> = {};\n let method = \"GET\";\n let url = \"\";\n try {\n requestHeaders = headersToRecord(request.headers);\n method = request.method;\n url = request.url;\n } catch (error) {\n warnOnce(\n \"inbound-capture\",\n \"Failed to capture the inbound request.\",\n error,\n );\n }\n\n const reportInbound = (\n outcome:\n | { statusCode: number; responseHeaders: Record<string, string[]> }\n | { error: string },\n ): void => {\n try {\n const event: InboundRequestEvent = {\n kind: \"inbound\",\n requestId: captureCtx.requestId,\n ...(captureCtx.frontendSessionId !== undefined\n ? { frontendSessionId: captureCtx.frontendSessionId }\n : {}),\n method,\n url,\n requestHeaders,\n startTimeMs,\n endTimeMs: Date.now(),\n ...outcome,\n };\n captureCtx.waitUntil(\n postCaptureEvents(getOriginalFetch(), sidecarUrl, [event]),\n );\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n };\n\n let response: Response;\n try {\n response = await requestCaptureContext.run(captureCtx, invokeHandler);\n } catch (error) {\n reportInbound({ error: String(error) });\n throw error;\n }\n\n try {\n reportInbound({\n statusCode: response.status,\n responseHeaders: headersToRecord(response.headers),\n });\n } catch (error) {\n warnOnce(\n \"inbound-report\",\n \"Failed to report the inbound request.\",\n error,\n );\n }\n return response;\n };\n\n // The wrapper only replaces `fetch` (same call signature), but TypeScript\n // cannot verify a spread against the bare generic H.\n return { ...handler, fetch: wrappedFetch } as H;\n};\n\nconst resolveSidecarUrl = (\n options: WithMeticulousOptions | undefined,\n env: unknown,\n): string | undefined => {\n const fromEnv =\n env !== null && typeof env === \"object\"\n ? (env as Record<string, unknown>)[\"METICULOUS_SIDECAR_URL\"]\n : undefined;\n const raw = options?.sidecarUrl ?? fromEnv;\n if (typeof raw !== \"string\" || raw.length === 0) {\n return undefined;\n }\n return raw.replace(/\\/+$/, \"\");\n};\n\nconst buildCaptureContext = (\n request: Request,\n sidecarUrl: string,\n ctx: MeticulousExecutionContext,\n): RequestCaptureContext => {\n let frontendSessionId: string | undefined;\n try {\n frontendSessionId =\n request.headers.get(FRONTEND_SESSION_ID_HEADER) ?? undefined;\n } catch {\n frontendSessionId = undefined;\n }\n return {\n requestId: crypto.randomUUID(),\n frontendSessionId,\n sidecarUrl,\n waitUntil: (promise) => {\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 };\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgEA,MAAa,kBACX,SACA,YACM;CACN,MAAM,eAAe,OACnB,SACA,KACA,QACsB;EACtB,MAAM,sBAAsB,QAAQ,MAAM,SAAS,KAAc,IAAI;EAErE,IAAI;AACJ,MAAI;AACF,gBAAa,kBAAkB,SAAS,IAAI;AAC5C,OAAI,eAAe,KAAA,EACjB,oBAAmB;WAEd,OAAO;AACd,YACE,aACA,mFACA,MACD;AACD,gBAAa,KAAA;;AAEf,MAAI,eAAe,KAAA,EACjB,QAAO,eAAe;EAGxB,MAAM,aAAa,oBAAoB,SAAS,YAAY,IAAI;EAChE,MAAM,cAAc,KAAK,KAAK;EAC9B,IAAI,iBAA2C,EAAE;EACjD,IAAI,SAAS;EACb,IAAI,MAAM;AACV,MAAI;AACF,oBAAiB,gBAAgB,QAAQ,QAAQ;AACjD,YAAS,QAAQ;AACjB,SAAM,QAAQ;WACP,OAAO;AACd,YACE,mBACA,0CACA,MACD;;EAGH,MAAM,iBACJ,YAGS;AACT,OAAI;IACF,MAAM,QAA6B;KACjC,MAAM;KACN,WAAW,WAAW;KACtB,GAAI,WAAW,sBAAsB,KAAA,IACjC,EAAE,mBAAmB,WAAW,mBAAmB,GACnD,EAAE;KACN;KACA;KACA;KACA;KACA,WAAW,KAAK,KAAK;KACrB,GAAG;KACJ;AACD,eAAW,UACT,kBAAkB,kBAAkB,EAAE,YAAY,CAAC,MAAM,CAAC,CAC3D;YACM,OAAO;AACd,aACE,kBACA,yCACA,MACD;;;EAIL,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,sBAAsB,IAAI,YAAY,cAAc;WAC9D,OAAO;AACd,iBAAc,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;AACvC,SAAM;;AAGR,MAAI;AACF,iBAAc;IACZ,YAAY,SAAS;IACrB,iBAAiB,gBAAgB,SAAS,QAAQ;IACnD,CAAC;WACK,OAAO;AACd,YACE,kBACA,yCACA,MACD;;AAEH,SAAO;;AAKT,QAAO;EAAE,GAAG;EAAS,OAAO;EAAc;;AAG5C,MAAM,qBACJ,SACA,QACuB;CACvB,MAAM,UACJ,QAAQ,QAAQ,OAAO,QAAQ,WAC1B,IAAgC,4BACjC,KAAA;CACN,MAAM,MAAM,SAAS,cAAc;AACnC,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C;AAEF,QAAO,IAAI,QAAQ,QAAQ,GAAG;;AAGhC,MAAM,uBACJ,SACA,YACA,QAC0B;CAC1B,IAAI;AACJ,KAAI;AACF,sBACE,QAAQ,QAAQ,IAAA,0BAA+B,IAAI,KAAA;SAC/C;AACN,sBAAoB,KAAA;;AAEtB,QAAO;EACL,WAAW,OAAO,YAAY;EAC9B;EACA;EACA,YAAY,YAAY;AACtB,OAAI;AACF,QAAI,UAAU,QAAQ;YACf,OAAO;AACd,aACE,cACA,sDACA,MACD;;;EAGN","debugId":"a80cfad7-b180-56e8-a9ee-ceabd81be885"}
|
package/dist/log.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region src/log.ts
|
|
2
|
+
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4a78549a-41a2-5625-a0b7-315324af09b5")}catch(e){}}();
|
|
2
4
|
const warned = /* @__PURE__ */ new Set();
|
|
3
5
|
/** console.warn once per unique key — capture problems must never spam or break the app. */
|
|
4
6
|
const warnOnce = (key, message, error) => {
|
|
@@ -10,4 +12,5 @@ const warnOnce = (key, message, error) => {
|
|
|
10
12
|
//#endregion
|
|
11
13
|
export { warnOnce };
|
|
12
14
|
|
|
13
|
-
//# sourceMappingURL=log.js.map
|
|
15
|
+
//# sourceMappingURL=log.js.map
|
|
16
|
+
//# debugId=4a78549a-41a2-5625-a0b7-315324af09b5
|
package/dist/log.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.js","
|
|
1
|
+
{"version":3,"file":"log.js","sources":["../src/log.ts"],"sourcesContent":["const warned = new Set<string>();\n\n/** console.warn once per unique key — capture problems must never spam or break the app. */\nexport const warnOnce = (\n key: string,\n message: string,\n error?: unknown,\n): void => {\n if (warned.has(key)) {\n return;\n }\n warned.add(key);\n if (error === undefined) {\n console.warn(`[meticulous] ${message}`);\n } else {\n console.warn(`[meticulous] ${message}`, error);\n }\n};\n"],"names":[],"mappings":";;;AAAA,MAAM,yBAAS,IAAI,KAAa;;AAGhC,MAAa,YACX,KACA,SACA,UACS;AACT,KAAI,OAAO,IAAI,IAAI,CACjB;AAEF,QAAO,IAAI,IAAI;AACf,KAAI,UAAU,KAAA,EACZ,SAAQ,KAAK,gBAAgB,UAAU;KAEvC,SAAQ,KAAK,gBAAgB,WAAW,MAAM","debugId":"4a78549a-41a2-5625-a0b7-315324af09b5"}
|
package/dist/protocol.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* recorder sidecar. The sidecar imports these types (type-only), so this file
|
|
5
5
|
* is the single source of truth for the shape of capture events.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f28d3e21-60a1-55c4-8586-5db3a0ec194b")}catch(e){}}();
|
|
7
9
|
const SIDECAR_PROTOCOL_VERSION = "1";
|
|
8
10
|
/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */
|
|
9
11
|
const SIDECAR_PROTOCOL_VERSION_HEADER = "x-meticulous-sidecar-protocol-version";
|
|
@@ -24,4 +26,5 @@ const CAPTURED_HEADERS = ["content-type", FRONTEND_SESSION_ID_HEADER];
|
|
|
24
26
|
//#endregion
|
|
25
27
|
export { CAPTURED_HEADERS, FRONTEND_SESSION_ID_HEADER, SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION, SIDECAR_PROTOCOL_VERSION_HEADER };
|
|
26
28
|
|
|
27
|
-
//# sourceMappingURL=protocol.js.map
|
|
29
|
+
//# sourceMappingURL=protocol.js.map
|
|
30
|
+
//# debugId=f28d3e21-60a1-55c4-8586-5db3a0ec194b
|
package/dist/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","
|
|
1
|
+
{"version":3,"file":"protocol.js","sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Wire protocol between the in-worker shim and the Meticulous backend\n * recorder sidecar. The sidecar imports these types (type-only), so this file\n * is the single source of truth for the shape of capture events.\n */\n\nexport const SIDECAR_PROTOCOL_VERSION = \"1\";\n\n/** Header carrying {@link SIDECAR_PROTOCOL_VERSION} on every shim → sidecar request. */\nexport const SIDECAR_PROTOCOL_VERSION_HEADER =\n \"x-meticulous-sidecar-protocol-version\";\n\n/**\n * Header the Meticulous frontend recorder stamps on requests made by the app\n * under recording; its value ties backend spans to the frontend session.\n */\nexport const FRONTEND_SESSION_ID_HEADER = \"x-meticulous-session-id\";\n\nexport const SIDECAR_EVENTS_PATH = \"/v1/events\";\n\n/**\n * The only headers persisted on capture events. Everything else — notably\n * authorization, cookie/set-cookie, and API-key headers — is dropped at\n * capture time, before it ever leaves the worker. Mirrors the Node backend\n * recorder, which persists only content-type plus the Meticulous session\n * header.\n */\nexport const CAPTURED_HEADERS = [\n \"content-type\",\n FRONTEND_SESSION_ID_HEADER,\n] as const;\n\nexport interface CapturedBody {\n /** UTF-8 decoded body, truncated to the capture cap. */\n body: string;\n /** True when the body exceeded the capture cap or the read was cut short. */\n truncated: boolean;\n}\n\ninterface BaseRequestEvent {\n /** Correlates outbound events to the inbound request being handled. */\n requestId: string;\n /** Value of {@link FRONTEND_SESSION_ID_HEADER} on the inbound request, if any. */\n frontendSessionId?: string;\n method: string;\n /** Full URL including query string. */\n url: string;\n /** Lower-cased header name → values. */\n requestHeaders: Record<string, string[]>;\n responseHeaders?: Record<string, string[]>;\n statusCode?: number;\n startTimeMs: number;\n endTimeMs: number;\n /** Set when the request failed before a response was produced. */\n error?: string;\n}\n\n/**\n * The worker's own inbound request (becomes a SERVER span). Bodies are not\n * captured, matching the Node backend recorder's SERVER-span contract.\n */\nexport interface InboundRequestEvent extends BaseRequestEvent {\n kind: \"inbound\";\n}\n\n/**\n * An outgoing `fetch` call made while handling an inbound request (becomes a\n * CLIENT span).\n */\nexport interface OutboundRequestEvent extends BaseRequestEvent {\n kind: \"outbound\";\n requestBody?: CapturedBody;\n responseBody?: CapturedBody;\n}\n\nexport type CaptureEvent = InboundRequestEvent | OutboundRequestEvent;\n\nexport interface CaptureEventsPayload {\n events: CaptureEvent[];\n}\n"],"names":[],"mappings":";;;;;;;;AAMA,MAAa,2BAA2B;;AAGxC,MAAa,kCACX;;;;;AAMF,MAAa,6BAA6B;AAE1C,MAAa,sBAAsB;;;;;;;;AASnC,MAAa,mBAAmB,CAC9B,gBACA,2BACD","debugId":"f28d3e21-60a1-55c4-8586-5db3a0ec194b"}
|
package/dist/sidecar-client.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4fd3fc64-97ce-5a60-ae98-b07904e43cf9")}catch(e){}}();
|
|
1
3
|
import { warnOnce } from "./log.js";
|
|
2
4
|
import { SIDECAR_EVENTS_PATH, SIDECAR_PROTOCOL_VERSION_HEADER } from "./protocol.js";
|
|
3
5
|
//#region src/sidecar-client.ts
|
|
@@ -22,4 +24,5 @@ const postCaptureEvents = async (fetchFn, sidecarUrl, events) => {
|
|
|
22
24
|
//#endregion
|
|
23
25
|
export { postCaptureEvents };
|
|
24
26
|
|
|
25
|
-
//# sourceMappingURL=sidecar-client.js.map
|
|
27
|
+
//# sourceMappingURL=sidecar-client.js.map
|
|
28
|
+
//# debugId=4fd3fc64-97ce-5a60-ae98-b07904e43cf9
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-client.js","
|
|
1
|
+
{"version":3,"file":"sidecar-client.js","sources":["../src/sidecar-client.ts"],"sourcesContent":["import { warnOnce } from \"./log\";\nimport {\n type CaptureEvent,\n type CaptureEventsPayload,\n SIDECAR_EVENTS_PATH,\n SIDECAR_PROTOCOL_VERSION,\n SIDECAR_PROTOCOL_VERSION_HEADER,\n} from \"./protocol\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n/** POSTs capture events to the sidecar. Never rejects — reporting failures only warn (once). */\nexport const postCaptureEvents = async (\n fetchFn: FetchFn,\n sidecarUrl: string,\n events: CaptureEvent[],\n): Promise<void> => {\n try {\n const payload: CaptureEventsPayload = { events };\n const response = await fetchFn(`${sidecarUrl}${SIDECAR_EVENTS_PATH}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [SIDECAR_PROTOCOL_VERSION_HEADER]: SIDECAR_PROTOCOL_VERSION,\n },\n body: JSON.stringify(payload),\n });\n if (!response.ok) {\n warnOnce(\n \"sidecar-rejected\",\n `Meticulous sidecar rejected capture events (HTTP ${response.status}).`,\n );\n }\n await response.arrayBuffer().catch(() => undefined);\n } catch (error) {\n warnOnce(\n \"sidecar-unreachable\",\n \"Could not reach the Meticulous backend recorder sidecar — backend session events are not being recorded.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":";;;;;;AAYA,MAAa,oBAAoB,OAC/B,SACA,YACA,WACkB;AAClB,KAAI;EACF,MAAM,UAAgC,EAAE,QAAQ;EAChD,MAAM,WAAW,MAAM,QAAQ,GAAG,aAAa,uBAAuB;GACpE,QAAQ;GACR,SAAS;IACP,gBAAgB;KACf,kCAAA;IACF;GACD,MAAM,KAAK,UAAU,QAAQ;GAC9B,CAAC;AACF,MAAI,CAAC,SAAS,GACZ,UACE,oBACA,oDAAoD,SAAS,OAAO,IACrE;AAEH,QAAM,SAAS,aAAa,CAAC,YAAY,KAAA,EAAU;UAC5C,OAAO;AACd,WACE,uBACA,4GACA,MACD","debugId":"4fd3fc64-97ce-5a60-ae98-b07904e43cf9"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwaysmeticulous/backend-recorder-workerd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.315.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",
|
|
@@ -20,21 +20,10 @@
|
|
|
20
20
|
"workerd",
|
|
21
21
|
"recorder"
|
|
22
22
|
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
25
|
-
"build": "tsdown",
|
|
26
|
-
"dev": "tsdown --watch",
|
|
27
|
-
"format": "prettier --write src",
|
|
28
|
-
"lint": "oxlint --type-aware src",
|
|
29
|
-
"lint:commit": "eslint --cache $(git diff --relative --name-only --diff-filter=ACMRTUXB master | grep -E \"(.js$|.ts$|.tsx$)\")",
|
|
30
|
-
"lint:fix": "oxlint --type-aware src --fix",
|
|
31
|
-
"depcheck": "depcheck --ignore-patterns=dist",
|
|
32
|
-
"test": "vitest run --passWithNoTests"
|
|
33
|
-
},
|
|
34
23
|
"devDependencies": {
|
|
35
24
|
"@types/node": "^20.0.0",
|
|
36
25
|
"tsdown": "^0.21.7",
|
|
37
|
-
"vitest": "
|
|
26
|
+
"vitest": "^4.1.7"
|
|
38
27
|
},
|
|
39
28
|
"author": {
|
|
40
29
|
"name": "The Meticulous Team",
|
|
@@ -52,5 +41,16 @@
|
|
|
52
41
|
},
|
|
53
42
|
"bugs": {
|
|
54
43
|
"url": "https://github.com/alwaysmeticulous/meticulous-sdk/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"dev": "tsdown --watch",
|
|
49
|
+
"format": "prettier --write src",
|
|
50
|
+
"lint": "oxlint --type-aware src",
|
|
51
|
+
"lint:commit": "eslint --cache $(git diff --relative --name-only --diff-filter=ACMRTUXB master | grep -E \"(.js$|.ts$|.tsx$)\")",
|
|
52
|
+
"lint:fix": "oxlint --type-aware src --fix",
|
|
53
|
+
"depcheck": "depcheck --ignore-patterns=dist",
|
|
54
|
+
"test": "vitest run --passWithNoTests"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|